diff --git a/.github/scripts/require-floor-run.sh b/.github/scripts/require-floor-run.sh new file mode 100755 index 0000000..5f8b06a --- /dev/null +++ b/.github/scripts/require-floor-run.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +WORKFLOW="scheduled.yml" +CANDIDATE_LIMIT=30 + +tag_sha=$(git rev-parse HEAD) +tag_name=${GITHUB_REF_NAME:-$tag_sha} + +# "The most recent run is green" would not have held 1.8.0 back: the newest floors run at that +# moment WAS green, because it predated the commit that broke the floor. So the run has to have +# tested code that already contains this tag, i.e. the tagged commit is an ancestor of (or equal +# to) the commit the floors ran on. +candidate_shas=$( + gh run list --workflow "$WORKFLOW" --status success --limit "$CANDIDATE_LIMIT" --json headSha --jq '.[].headSha' +) + +while read -r candidate_sha; do + [ -n "$candidate_sha" ] || continue + # A run dispatched by hand counts as much as a scheduled one; it verifies the same thing. Its + # commit may not be here yet, and may not be fetchable at all if its branch is gone. + git cat-file -e "${candidate_sha}^{commit}" 2>/dev/null \ + || git fetch --quiet --no-tags origin "$candidate_sha" 2>/dev/null \ + || continue + if git merge-base --is-ancestor "$tag_sha" "$candidate_sha"; then + echo "Dependency floors verified by $WORKFLOW at $candidate_sha, which contains $tag_sha." + exit 0 + fi +done <<< "$candidate_shas" + +cat >&2 < create the GitHub Release; id-token: write -> OIDC for PyPI Trusted Publishing. -permissions: - contents: write - id-token: write +permissions: {} jobs: + # Gates the tag on the dependency floors, which _checks.yml runs only for scheduled.yml, never + # for pull requests. A floor break can therefore merge with every check green, which is how the + # crash in 1.8.0 reached PyPI (#245). Placed before release so a failure costs nothing. + floors-gate: + runs-on: ubuntu-latest + # contents: read -> the history the ancestor check walks; actions: read -> list workflow runs. + permissions: + contents: read + actions: read + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 # the ancestor check needs history, not just the tagged commit + - run: .github/scripts/require-floor-run.sh + env: + GH_TOKEN: ${{ github.token }} + release: + needs: floors-gate runs-on: ubuntu-latest environment: pypi # scopes the PyPI Trusted Publisher; hook for approval rules + # contents: write -> create the GitHub Release; id-token: write -> OIDC for PyPI Trusted Publishing. + permissions: + contents: write + id-token: write steps: - uses: actions/checkout@v6 - uses: extractions/setup-just@v4