diff --git a/.github/workflows/release-plz-crates.yml b/.github/workflows/release-plz-crates.yml index 19bfa1a..af63154 100644 --- a/.github/workflows/release-plz-crates.yml +++ b/.github/workflows/release-plz-crates.yml @@ -2,6 +2,11 @@ name: Crate Release on: workflow_call: + inputs: + required-check-runs: + description: Comma-separated check-run names that must succeed on the release revision before publication + required: true + type: string outputs: releases: description: JSON metadata for releases created by release-plz @@ -38,10 +43,82 @@ jobs: - name: Verify publishable packages run: cargo publish --workspace --locked --dry-run + release-ci-gate: + name: Wait for release-revision CI + if: github.repository_owner == 'wrightkit' + needs: package-preflight + runs-on: ubuntu-latest + timeout-minutes: 45 + permissions: + checks: read + steps: + - name: Wait for required checks on the release revision + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REQUIRED_CHECK_RUNS: ${{ inputs.required-check-runs }} + run: | + set -euo pipefail + repo="${{ github.repository }}" + sha="${{ github.sha }}" + deadline=$(( SECONDS + 2400 )) + pending_file=$(mktemp) + trap 'rm -f "$pending_file"' EXIT + if [ "$REQUIRED_CHECK_RUNS" != "${REQUIRED_CHECK_RUNS//$'\n'/,}" ]; then + echo "required-check-runs must be a single comma-separated line" >&2 + exit 1 + fi + IFS=',' read -r -a names <<< "$REQUIRED_CHECK_RUNS" + count=0 + for raw in "${names[@]}"; do + name="$(echo "$raw" | xargs)" + if [ -n "$name" ]; then + printf '%s\n' "$name" >> "$pending_file" + count=$(( count + 1 )) + fi + done + if [ "$count" -eq 0 ]; then + echo "required-check-runs resolved to an empty set; refusing to publish without a named gate" >&2 + exit 1 + fi + while [ -s "$pending_file" ]; do + if [ "$SECONDS" -ge "$deadline" ]; then + echo "Timed out waiting for release-revision checks:" >&2 + cat "$pending_file" >&2 + exit 1 + fi + sleep 30 + keep="" + while IFS= read -r name; do + [ -z "$name" ] && continue + if ! out=$(gh api "repos/$repo/commits/$sha/check-runs" -f "check_name=$name" \ + --jq '(.check_runs | sort_by(.id) | last) // {status: "missing", conclusion: "", html_url: ""} | "\(.status) \(.conclusion // "-") \(.html_url // "-")"' 2>&1); then + echo "check-runs API call failed for '$name': $out" >&2 + exit 1 + fi + status="${out%% *}" + rest="${out#* }" + conclusion="${rest%% *}" + case "$status" in + completed) + if [ "$conclusion" = "success" ]; then + echo "PASS $name" + else + echo "FAIL $name concluded '$conclusion' ($out)" >&2 + exit 1 + fi ;; + *) + keep="${keep}${name}"$'\n' + echo "WAIT $name ($status)" ;; + esac + done < "$pending_file" + printf '%s' "$keep" > "$pending_file" + done + echo "All required checks succeeded on $sha" + release: name: Publish crates and create release tag if: github.repository_owner == 'wrightkit' - needs: package-preflight + needs: [package-preflight, release-ci-gate] outputs: releases: ${{ steps.release-plz.outputs.releases }} releases_created: ${{ steps.release-plz.outputs.releases_created }} diff --git a/docs/release-engineering.md b/docs/release-engineering.md index 146aebf..a47e847 100644 --- a/docs/release-engineering.md +++ b/docs/release-engineering.md @@ -67,6 +67,8 @@ Publication for a release revision must not run ahead of the authoritative CI re A required-checks gate inside the release workflow is the encoded form; a Release-PR merge that already requires green CI is the implicit form. Prefer the encoded form where the publication mechanism permits it, and record which form a repository uses in its release documentation. +The shared `release-plz-crates.yml` carries the encoded form: callers declare `required-check-runs`, a comma-separated list of check-run names that must conclude `success` on the release revision before crates publication and tag creation run. The gate polls the check-runs API and fails on any non-success conclusion rather than publishing ahead of CI. Callers must grant `checks: read` on the reusable-workflow call for the gate to read their check runs; the release repository itself needs no extra credential. + ### Generated workflows have exclusive ownership If a tool generates a workflow and verifies that generated file against its configuration, choose one ownership model: