Skip to content

Dispatch MSBuild Investigation (2 parallel) #8

Dispatch MSBuild Investigation (2 parallel)

Dispatch MSBuild Investigation (2 parallel) #8

# Throttled dispatcher for msbuild-investigate-issue workflows.
#
# Dispatches upstream issues in batches with a configurable parallelism limit,
# waiting for each batch to complete before starting the next.
# After all investigation batches finish, dispatches the summary workflow
# to consolidate results into the discussion.
#
# Designed to be dispatched from the MSBuild Weekly Report agent with a
# JSON array of upstream issue numbers and the discussion ID.
#
# Timing budget (worst-case):
# A single investigation can take up to ~45 min (agent: 20m + safe_outputs: 15m
# + activation/conclusion: ~10m).
# With max_parallel=2 and 10 issues → 5 batches × ~60 min = ~300 min.
# Job timeout is set to 360 min (6 hours) to accommodate this.
name: "Dispatch MSBuild Investigation Batch"
on:
workflow_dispatch:
inputs:
issues_json:
description: 'JSON array of upstream issue numbers (e.g. ["1234","5678"])'
required: true
type: string
upstream_repo:
description: 'Upstream repo (owner/name)'
required: false
default: 'dotnet/msbuild'
type: string
max_parallel:
description: 'Max number of investigation workflows running concurrently'
required: false
default: '2'
type: string
permissions:
actions: write
run-name: "Dispatch MSBuild Investigation (${{ inputs.max_parallel }} parallel)"
jobs:
dispatch:
runs-on: ubuntu-latest
# 6 hours — enough for batches at ~45 min per investigation
timeout-minutes: 360
steps:
- name: Validate inputs
env:
ISSUES_JSON: ${{ inputs.issues_json }}
MAX_PARALLEL: ${{ inputs.max_parallel }}
run: |
if ! echo "$ISSUES_JSON" | jq -e 'type == "array" and length > 0' > /dev/null 2>&1; then
echo "::error::issues_json must be a non-empty JSON array"
exit 1
fi
if ! [[ "$MAX_PARALLEL" =~ ^[1-9][0-9]*$ ]]; then
echo "::error::max_parallel must be a positive integer"
exit 1
fi
COUNT=$(echo "$ISSUES_JSON" | jq 'length')
echo "### Dispatch MSBuild Investigation Batch" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- **Issues to investigate:** ${COUNT}" >> "$GITHUB_STEP_SUMMARY"
echo "- **Max parallel:** ${MAX_PARALLEL}" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
- name: Find latest weekly report discussion
id: find-discussion
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
# Parse owner and repo name
OWNER="${REPO%%/*}"
REPO_NAME="${REPO##*/}"
# Search for the most recent MSBuild Weekly Report discussion
DISCUSSION_ID=$(gh api graphql -f query='
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
discussions(first: 5, orderBy: {field: CREATED_AT, direction: DESC}) {
nodes {
number
title
}
}
}
}
' -f owner="$OWNER" -f repo="$REPO_NAME" \
--jq '.data.repository.discussions.nodes[] | select(.title | startswith("MSBuild Weekly Report")) | .number' \
| head -1)
if [ -z "$DISCUSSION_ID" ]; then
echo "::warning::No MSBuild Weekly Report discussion found. Investigation comments will not be posted."
DISCUSSION_ID="0"
fi
echo "discussion_id=${DISCUSSION_ID}" >> "$GITHUB_OUTPUT"
echo "Found discussion #${DISCUSSION_ID}"
echo "- **Discussion:** #${DISCUSSION_ID}" >> "$GITHUB_STEP_SUMMARY"
- name: Dispatch investigation workflows in throttled batches
env:
GH_TOKEN: ${{ github.token }}
ISSUES_JSON: ${{ inputs.issues_json }}
MAX_PARALLEL: ${{ inputs.max_parallel }}
UPSTREAM_REPO: ${{ inputs.upstream_repo }}
DISCUSSION_ID: ${{ steps.find-discussion.outputs.discussion_id }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
readarray -t ISSUES < <(echo "$ISSUES_JSON" | jq -r '.[]')
TOTAL=${#ISSUES[@]}
echo "Total issues to investigate: ${TOTAL}"
echo "Max parallel: ${MAX_PARALLEL}"
echo "Discussion ID: ${DISCUSSION_ID}"
echo ""
batch_num=0
dispatched=0
all_results=""
for ((i=0; i<TOTAL; i+=MAX_PARALLEL)); do
batch_num=$((batch_num + 1))
BATCH=("${ISSUES[@]:i:MAX_PARALLEL}")
BATCH_SIZE=${#BATCH[@]}
echo "=========================================="
echo "Batch ${batch_num}: ${BATCH_SIZE} issue(s) — ${BATCH[*]}"
echo "=========================================="
# Dispatch all issues in this batch
for ISSUE in "${BATCH[@]}"; do
echo " → Dispatching investigation for upstream issue #${ISSUE}"
gh workflow run msbuild-investigate-issue.lock.yml \
--repo "${REPO}" \
-f upstream_issue="${ISSUE}" \
-f upstream_repo="${UPSTREAM_REPO}" \
-f discussion_id="${DISCUSSION_ID}"
dispatched=$((dispatched + 1))
all_results="${all_results}| ${ISSUE} | dispatched |"$'\n'
# Rate-limit dispatches to avoid GitHub API throttling
sleep 5
done
# Check if there are more batches after this one
REMAINING=$((TOTAL - i - BATCH_SIZE))
if [ "$REMAINING" -le 0 ]; then
echo ""
echo "Last batch dispatched. Waiting for completion..."
else
echo ""
echo "Waiting for batch ${batch_num} to complete before dispatching next ${REMAINING} issue(s)..."
fi
# Wait for dispatched runs to register in the API
sleep 30
# Poll until all active investigation runs finish
# A single investigation can take up to ~45 min, so we allow 75 min per batch
BATCH_TIMEOUT=4500 # 75 minutes
ELAPSED=0
POLL_INTERVAL=30
while [ $ELAPSED -lt $BATCH_TIMEOUT ]; do
# Count in-progress, queued, and waiting investigation runs
ACTIVE_IN_PROGRESS=$(gh api \
"repos/${REPO}/actions/workflows/msbuild-investigate-issue.lock.yml/runs?status=in_progress" \
--jq '.total_count' 2>/dev/null || echo "0")
ACTIVE_QUEUED=$(gh api \
"repos/${REPO}/actions/workflows/msbuild-investigate-issue.lock.yml/runs?status=queued" \
--jq '.total_count' 2>/dev/null || echo "0")
ACTIVE_WAITING=$(gh api \
"repos/${REPO}/actions/workflows/msbuild-investigate-issue.lock.yml/runs?status=waiting" \
--jq '.total_count' 2>/dev/null || echo "0")
ACTIVE=$((ACTIVE_IN_PROGRESS + ACTIVE_QUEUED + ACTIVE_WAITING))
if [ "$ACTIVE" -le 0 ]; then
echo " ✅ Batch ${batch_num} complete — all investigation runs finished."
break
fi
MINUTES=$((ELAPSED / 60))
echo " ⏳ ${ACTIVE} investigation run(s) still active (${MINUTES}m elapsed, timeout at $((BATCH_TIMEOUT / 60))m)..."
sleep $POLL_INTERVAL
ELAPSED=$((ELAPSED + POLL_INTERVAL))
done
if [ $ELAPSED -ge $BATCH_TIMEOUT ]; then
echo " ⚠️ Batch ${batch_num} timed out after $((BATCH_TIMEOUT / 60))m — some runs may still be active."
echo " Proceeding to next batch anyway."
fi
echo ""
done
echo "=========================================="
echo "Summary: Dispatched ${dispatched}/${TOTAL} investigation workflows"
echo " across ${batch_num} batch(es)"
echo "=========================================="
# Write summary table
{
echo "| Issue | Status |"
echo "|-------|--------|"
echo "$all_results"
} >> "$GITHUB_STEP_SUMMARY"
- name: Dispatch summary workflow
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
DISCUSSION_ID: ${{ steps.find-discussion.outputs.discussion_id }}
run: |
echo "Dispatching investigation summary workflow..."
gh workflow run msbuild-investigation-summary.lock.yml \
--repo "${REPO}" \
-f discussion_id="${DISCUSSION_ID}"
echo " ✅ Summary workflow dispatched for discussion #${DISCUSSION_ID}"