Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .github/scripts/verify-ci-checks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
# Verify that the LATEST CI check run per check name passed on a commit.
#
# Called by auto-tag.yml's "Verify CI checks passed" step, and runnable
# directly (read-only; needs only `gh` auth) to debug any SHA:
#
# REPO=owner/name HEAD_SHA=<sha> bash .github/scripts/verify-ci-checks.sh
#
# Why "latest per (app, name)" instead of "every run on the SHA": the
# check-runs API returns runs from EVERY check suite ever started on the
# SHA. Two triggers landing on the same SHA — e.g. `gh pr create --label`
# fires `opened` and `labeled` simultaneously, and the CI workflow's
# concurrency group cancels one of the twin runs — park a cancelled or
# failed run next to the successful same-named run, and those superseded
# runs never turn green. Counting all of them blocked SynthBench v0.3.1
# (PR #334) and SynthPanel's first v1.6.0 attempt, forcing manual tags.
# Only the newest run per (app, name) reflects the commit's real status;
# GitHub's own merge box collapses check runs the same way.
#
# CHECK_RUNS_FILTER=all additionally surfaces superseded ATTEMPTS of
# re-run check runs (the API default, `latest`, already collapses those).
# Useful only for reproducing historical states; the verdict grouping
# makes both filters converge on the same latest-run answer.

set -euo pipefail

: "${REPO:?REPO (owner/name) is required}"
: "${HEAD_SHA:?HEAD_SHA is required}"
FILTER="${CHECK_RUNS_FILTER:-latest}"

echo "Verifying CI status for commit: $HEAD_SHA (filter=${FILTER})"

# --paginate emits one JSON array per page; `jq -s 'add'` flattens them.
# The auto-tag job's own check run is excluded, as before — a prior
# failed tagging attempt on the SHA must not block the recovery re-run.
ALL_RUNS=$(gh api "repos/${REPO}/commits/${HEAD_SHA}/check-runs?filter=${FILTER}" \
--paginate \
--jq '[.check_runs[] | select(.name != "auto-tag")
| {name, status, conclusion, started_at, id, app: (.app.slug // "unknown")}]' \
| jq -s 'add // []')

TOTAL=$(echo "$ALL_RUNS" | jq 'length')
if [ "$TOTAL" -eq 0 ]; then
echo "::error::No CI check runs found for commit $HEAD_SHA — refusing to tag"
exit 1
fi

# Latest run per (app, name): started_at orders the runs; id breaks
# same-second ties (a later-created run always has a higher id).
RUNS=$(echo "$ALL_RUNS" | jq 'group_by([.app, .name]) | map(sort_by(.started_at, .id) | last)')

KEPT=$(echo "$RUNS" | jq 'length')
if [ "$KEPT" -lt "$TOTAL" ]; then
echo "Ignoring $((TOTAL - KEPT)) superseded run(s) shadowed by a newer run of the same check:"
echo "$ALL_RUNS" | jq 'group_by([.app, .name]) | map(sort_by(.started_at, .id) | .[:-1]) | add'
fi

echo "Latest check run per name:"
echo "$RUNS" | jq '.'

INCOMPLETE=$(echo "$RUNS" | jq '[.[] | select(.status != "completed")] | length')
if [ "$INCOMPLETE" -gt 0 ]; then
echo "::error::${INCOMPLETE} check(s) still running — refusing to tag"
echo "$RUNS" | jq '[.[] | select(.status != "completed")]'
exit 1
fi

FAILED=$(echo "$RUNS" | jq '[.[] | select(.conclusion != "success" and .conclusion != "skipped" and .conclusion != "neutral")] | length')
if [ "$FAILED" -gt 0 ]; then
echo "::error::${FAILED} check(s) did not pass — refusing to tag"
echo "$RUNS" | jq '[.[] | select(.conclusion != "success" and .conclusion != "skipped" and .conclusion != "neutral")]'
exit 1
fi

echo "All CI checks passed ✓"
43 changes: 12 additions & 31 deletions .github/workflows/auto-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,40 +74,19 @@ jobs:
echo "skipped=false" >> "$GITHUB_OUTPUT"
echo "Bump type: $BUMP"

# Latest run per check name only. The check-runs API returns runs from
# every check suite on the SHA, so a workflow double-triggered on one
# SHA (label events don't move the head) parks superseded red/cancelled
# runs there forever — counting them blocked the first v1.6.0 attempt.
# The logic lives in a standalone script so it can be exercised
# directly against any SHA (see the script header for usage).
- name: Verify CI checks passed
if: steps.check.outputs.skipped != 'true'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
echo "Verifying CI status for PR head commit: $HEAD_SHA"

RUNS=$(gh api "repos/${{ github.repository }}/commits/${HEAD_SHA}/check-runs" \
--paginate --jq '.check_runs[] | select(.name != "auto-tag") | {name: .name, status: .status, conclusion: .conclusion}')

if [ -z "$RUNS" ]; then
echo "::error::No CI check runs found for commit $HEAD_SHA — refusing to tag"
exit 1
fi

echo "Check runs:"
echo "$RUNS" | jq -s '.'

INCOMPLETE=$(echo "$RUNS" | jq -s '[.[] | select(.status != "completed")] | length')
if [ "$INCOMPLETE" -gt 0 ]; then
echo "::error::${INCOMPLETE} check(s) still running — refusing to tag"
echo "$RUNS" | jq -s '[.[] | select(.status != "completed")]'
exit 1
fi

FAILED=$(echo "$RUNS" | jq -s '[.[] | select(.conclusion != "success" and .conclusion != "skipped" and .conclusion != "neutral")] | length')
if [ "$FAILED" -gt 0 ]; then
echo "::error::${FAILED} check(s) did not pass — refusing to tag"
echo "$RUNS" | jq -s '[.[] | select(.conclusion != "success" and .conclusion != "skipped" and .conclusion != "neutral")]'
exit 1
fi

echo "All CI checks passed ✓"
run: bash .github/scripts/verify-ci-checks.sh

- name: Compute next version
if: steps.check.outputs.skipped != 'true'
Expand Down Expand Up @@ -222,9 +201,11 @@ jobs:
# because render_site_markdown.py may rewrite multiple .md
# files (docs/, blog/, mcp/, recommended-models/) on a single
# bump; constraining to specific paths here would silently
# drop a refreshed file from the bump commit.
# drop a refreshed file from the bump commit. Root server.json
# is the second descriptor render_server_card.py rewrites —
# omitting it here left it drifted on main and bit PR #583.
git add src/synth_panel/__version__.py site/index.html site/ \
site/.well-known/mcp/server-card.json
site/.well-known/mcp/server-card.json server.json
if git diff --staged --quiet; then
echo "Version artifacts already at ${NEW_TAG} (pre-bumped in PR) — nothing to commit."
else
Expand Down
51 changes: 14 additions & 37 deletions .github/workflows/semver-label.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,29 @@ name: Semver Label
# It is wired as a required status check on main separately (branch ruleset,
# managed in dataviking-infra) — this file only defines the check.
#
# Two design constraints, both forced by auto-tag.yml's "Verify CI checks
# passed" step, which refuses to tag while ANY non-green check run sits on
# the merged head SHA:
# Labels are fetched fresh from the API, not read from the event payload.
# Label events don't move the head SHA, and reruns replay the original
# payload — a payload-based `opened` run that failed before the label was
# added could never be manually rerun to green.
#
# 1. Labels are fetched fresh from the API, not read from the event
# payload. Label events don't move the head SHA, and reruns replay the
# original payload — a payload-based run that failed before the label
# was added could never be rerun to green, permanently poisoning the
# SHA for auto-tag.
# A failed `opened` run superseded moments later by a green `labeled` run
# is harmless: both GitHub's merge box and auto-tag.yml's verify step
# (.github/scripts/verify-ci-checks.sh) evaluate only the LATEST run per
# check name on the SHA. This workflow briefly (PR #584) carried a
# self-heal step that re-ran superseded non-green runs of itself so they
# wouldn't block auto-tag — dead weight once the verify step stopped
# counting superseded runs, so it was removed (with its actions:write
# permission).
#
# 2. Once the check passes, it re-runs any superseded non-green runs of
# itself on the same SHA (e.g. the `opened` run that correctly failed
# before the label existed, moments later). Thanks to (1), those
# reruns re-fetch the labels and flip green, leaving the SHA clean.
#
# No concurrency/cancel-in-progress: a cancelled run is just another
# non-green conclusion parked on the SHA (this exact failure blocked the
# v1.6.0 recovery), and runs here take ~2 seconds anyway.
# No concurrency/cancel-in-progress: runs take ~2 seconds, and the newest
# run wins everywhere that matters anyway.

on:
pull_request:
types: [opened, reopened, synchronize, labeled, unlabeled]

permissions:
contents: read
actions: write # rerun superseded runs of this workflow (step 2)

jobs:
semver-label:
Expand Down Expand Up @@ -73,23 +70,3 @@ jobs:
exit 1
;;
esac

- name: Flip superseded non-green runs of this check green
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
RUN_IDS=$(gh api "repos/${GITHUB_REPOSITORY}/actions/workflows/semver-label.yml/runs?head_sha=${HEAD_SHA}&per_page=100" \
--jq ".workflow_runs[] | select(.status == \"completed\" and .conclusion != \"success\" and .id != ${GITHUB_RUN_ID}) | .id")

if [ -z "$RUN_IDS" ]; then
echo "No superseded non-green runs on ${HEAD_SHA} — nothing to heal."
exit 0
fi

for id in $RUN_IDS; do
echo "Re-running superseded run ${id} so it re-fetches labels and goes green"
gh api -X POST "repos/${GITHUB_REPOSITORY}/actions/runs/${id}/rerun" \
|| echo "::warning::Could not re-run ${id} (fork PR token lacks actions:write?). Re-run it manually before merge, or Auto Semver Tag will refuse to tag this SHA."
done
Loading