Files

134 lines
6.2 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
: "${SOURCE_RUN_ID:?SOURCE_RUN_ID is required}"
: "${SOURCE_ATTEMPT:?SOURCE_ATTEMPT is required}"
: "${FACTORY_REPORT_REF:?FACTORY_REPORT_REF is required}"
: "${GH_TOKEN:?GH_TOKEN is required}"
if [[ ! "$SOURCE_RUN_ID" =~ ^[1-9][0-9]*$ || ! "$SOURCE_ATTEMPT" =~ ^[1-9][0-9]*$ ]]; then
echo "Source run and attempt must be positive integers." >&2
exit 2
fi
# GITHUB_TOKEN can explicitly dispatch workflows even when a bot-triggered
# continuation does not produce the downstream workflow_run event.
# A request normally returns in a second or two. Worst case per report: three
# 15 s attempts (plus 5 s to kill each) and the 2 s and 8 s backoffs, 70 s; six
# reports and a comment round's queue request take 490 s, inside the 10-minute
# dispatch-reports job in code-agent-task.yml. task-report-dispatch.test.mjs
# checks that budget.
#
# A request killed by the timeout may already have been accepted, so a retry
# can dispatch the same report twice. That is safe by design, not by luck:
# every target already receives duplicate requests for one source run (this
# step and its workflow_run copy both ran before report-dispatch-gate.yml, and
# a replay or an operator can request any of them again), so each one is
# idempotent per source run and attempt:
# report-task-progress rewrites one comment; an equal or older record is refused
# report-task-usage archives and comments under the run/attempt marker
# publish-agent-history uploads content-addressed archives, upserts by marker
# publish-retro upserts its comment and ledger row by marker
# publish-visual-report upserts its PR comment by marker
# deploy-preview keeps a running instance of the same build (no force)
# comment-build-queue reconciles receipts; a second pass finds nothing to do
# Retrying only before a connection was established would trade that for lost
# reports whenever GitHub accepts slowly, which continuations cannot recover
# (they emit no workflow_run). task-report-dispatch.test.mjs keeps this list in
# step with the accepted workflows.
DISPATCH_ATTEMPT_TIMEOUT=15s
DISPATCH_KILL_AFTER=5s
DISPATCH_BACKOFF=(2 8)
dispatch() {
local workflow="$1"
local fields=(--field "run_id=$SOURCE_RUN_ID" --field "attempt=$SOURCE_ATTEMPT")
# The progress reporter serializes per Issue, so a continuation's snapshots
# and this run's final report cannot rewrite the comment at the same time.
# The Issue only names the concurrency group; the publisher still derives
# the target from the source run itself.
if [[ "$workflow" == report-task-progress.yml && "${ISSUE_NUMBER:-}" =~ ^[1-9][0-9]*$ ]]; then
fields+=(--field "issue=$ISSUE_NUMBER")
fi
# The comment queue reconciles one Issue once this run has completed.
if [[ "$workflow" == comment-build-queue.yml ]]; then
fields=(--field "issue_number=$ISSUE_NUMBER" --field "run_id=$SOURCE_RUN_ID")
fi
for attempt in 1 2 3; do
if timeout --kill-after="$DISPATCH_KILL_AFTER" "$DISPATCH_ATTEMPT_TIMEOUT" gh workflow run "$workflow" \
--repo "$GITHUB_REPOSITORY" --ref "$FACTORY_REPORT_REF" "${fields[@]}"; then
echo "Requested $workflow for run $SOURCE_RUN_ID, attempt $SOURCE_ATTEMPT."
return 0
fi
if [[ "$attempt" -lt 3 ]]; then sleep "${DISPATCH_BACKOFF[attempt - 1]}"; fi
done
echo "::warning::Could not dispatch $workflow; replay it with run_id=$SOURCE_RUN_ID and attempt=$SOURCE_ATTEMPT."
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
printf '\nReport dispatch failed: `%s`. Replay with `run_id=%s`, `attempt=%s`; do not rebuild the application.\n' \
"$workflow" "$SOURCE_RUN_ID" "$SOURCE_ATTEMPT" >> "$GITHUB_STEP_SUMMARY"
fi
return 1
}
# Published failed work has evidence and deserves a preview attempt too.
published="${FACTORY_TASK_PUBLISHED:-false}"
# A rejected GitHub Re-run did no work: its attempt has no progress, usage,
# history or retro of its own, and a report for it would rank as the Issue's
# latest and replace the real one (the progress comment included, which
# orders attempts by number). No report is requested; only the comment queue
# still moves on. Each step still succeeds, so report-dispatch-gate.yml counts
# it as handled and the workflow_run copy does not publish the empty report.
rerun_rejected="${FACTORY_RERUN_REJECTED:-false}"
# Rejected in prepare ("Re-run all jobs"), the attempt claimed no comment
# either: nothing at all is requested.
prepare_rejected="${FACTORY_PREPARE_REJECTED:-false}"
request() {
local workflow="$1"
if [[ "$prepare_rejected" == 'true' ]]; then
echo "Not requesting $workflow: prepare rejected attempt $SOURCE_ATTEMPT as a GitHub Re-run."
return 0
fi
if [[ "$rerun_rejected" == 'true' && "$workflow" != comment-build-queue.yml ]]; then
echo "Not requesting $workflow: attempt $SOURCE_ATTEMPT was a rejected GitHub Re-run."
return 0
fi
case "$workflow" in
publish-visual-report.yml | deploy-preview.yml)
# Requested explicitly for the same reason as the media report: a preview
# is expected to appear after a delivery, and the workflow_run event is not
# guaranteed for a bot-triggered continuation.
if [[ "$published" != 'true' ]]; then
echo "Not requesting $workflow: the task published no work."
return 0
fi
;;
esac
dispatch "$workflow"
}
# The task requests each report in a step of its own, so
# report-dispatch-gate.yml can tell which ones went out. The dispatcher is
# checked out at the calling workflow's own commit, so every caller names its
# workflows.
if (( $# == 0 )); then
echo "Name the report workflows to request." >&2
exit 2
fi
# The interaction history and retro are worth keeping for failures and
# handoffs too, not only deliveries.
failed=0
for workflow in "$@"; do
case "$workflow" in
report-task-progress.yml | report-task-usage.yml | publish-agent-history.yml | \
publish-retro.yml | publish-visual-report.yml | deploy-preview.yml) ;;
comment-build-queue.yml)
if [[ ! "${ISSUE_NUMBER:-}" =~ ^[1-9][0-9]*$ ]]; then
echo "comment-build-queue.yml needs ISSUE_NUMBER." >&2
exit 2
fi
;;
*) echo "Unknown report workflow: $workflow" >&2; exit 2 ;;
esac
request "$workflow" || failed=1
done
exit "$failed"