From 73c7a35617ba5fdf556929b19faa297224524a2c Mon Sep 17 00:00:00 2001 From: David Taylor Date: Wed, 7 Oct 2026 15:09:37 +1100 Subject: [PATCH 1/3] ci: cancel obsolete pull request validation runs --- .github/workflows/pr-validation.yml | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 7851347..bfbdb83 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -3,6 +3,7 @@ name: PR Validation on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] branches: - main paths: @@ -19,6 +20,12 @@ on: - '.gitignore' - '.github/workflows/**' +# Only newer runs of this workflow for the same PR supersede older work. +# Non-PR runs have unique keys, preserving every main/manual/scheduled run. +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('run-{0}', github.run_id) }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: validate-build: runs-on: ubuntu-latest @@ -75,6 +82,15 @@ jobs: echo "✅ No future posts found" fi + # Keep cheap checks on drafts, but never report full CI as successful. + # ready_for_review reruns this job at the same SHA; synchronize tests + # every later ready update. Conversion to draft cancels obsolete PR work. + - name: Require review readiness for full verification + if: github.event_name == 'pull_request' && github.event.pull_request.draft + run: | + echo '::error::Full verification is deferred while this PR is a draft. Mark it ready for review to run all checks.' + exit 1 + - name: Test Build (Development) run: | echo "Testing development build..." @@ -220,4 +236,11 @@ jobs: fi echo "" >> $GITHUB_STEP_SUMMARY - echo "Your PR is ready to merge! 🚀" >> $GITHUB_STEP_SUMMARY + if [ "${{ needs.validate-build.result }}" != "success" ] || + [ "${{ needs.validate-content.result }}" != "success" ] || + [ "${{ needs.validate-images.result }}" != "success" ] || + [ "${{ github.event.pull_request.draft }}" = "true" ]; then + echo "Full PR validation has not passed." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "All PR validation checks passed." >> $GITHUB_STEP_SUMMARY From cbad2736bcc75954ab06fc9052e7c573294f91b0 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Wed, 7 Oct 2026 18:34:28 +1100 Subject: [PATCH 2/3] ci: verify live pr readiness and tested head --- .github/workflows/pr-validation.yml | 66 +++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index bfbdb83..fcbb9da 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -20,7 +20,8 @@ on: - '.gitignore' - '.github/workflows/**' -# Only newer runs of this workflow for the same PR supersede older work. +# Runs of this workflow for the same PR share a cancellation group. +# GitHub does not guarantee event ordering; live state/head guards reject stale runs. # Non-PR runs have unique keys, preserving every main/manual/scheduled run. concurrency: group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('run-{0}', github.run_id) }} @@ -82,14 +83,25 @@ jobs: echo "✅ No future posts found" fi - # Keep cheap checks on drafts, but never report full CI as successful. - # ready_for_review reruns this job at the same SHA; synchronize tests - # every later ready update. Conversion to draft cancels obsolete PR work. + # Cheap checks precede this live read; event payloads can be stale. + # Fail drafts and stale heads before DB/browser/build setup. - name: Require review readiness for full verification - if: github.event_name == 'pull_request' && github.event.pull_request.draft + if: github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | - echo '::error::Full verification is deferred while this PR is a draft. Mark it ready for review to run all checks.' - exit 1 + set -euo pipefail + : "${EXPECTED_HEAD_SHA:?}" "${PR_NUMBER:?}" + pr="$(gh api --method GET --header 'Cache-Control: no-cache' \ + "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}")" + if ! jq -e --arg head "$EXPECTED_HEAD_SHA" \ + '.state == "open" and .draft == false and .head.sha == $head' \ + <<< "$pr" >/dev/null; then + echo '::error::Full verification is deferred or stale. The PR must be open, ready, and at the tested head.' + exit 1 + fi - name: Test Build (Development) run: | @@ -129,6 +141,25 @@ jobs: echo "✅ Build output looks good" + # Recheck after expensive work before this job can report success. + - name: Confirm current PR state after full verification + if: github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + : "${EXPECTED_HEAD_SHA:?}" "${PR_NUMBER:?}" + pr="$(gh api --method GET --header 'Cache-Control: no-cache' \ + "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}")" + if ! jq -e --arg head "$EXPECTED_HEAD_SHA" \ + '.state == "open" and .draft == false and .head.sha == $head' \ + <<< "$pr" >/dev/null; then + echo '::error::Full verification is deferred or stale. The PR must be open, ready, and at the tested head.' + exit 1 + fi + validate-content: runs-on: ubuntu-latest steps: @@ -212,6 +243,24 @@ jobs: needs: [validate-build, validate-content, validate-images] if: always() steps: + - name: Confirm current PR state before validation summary + if: github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + : "${EXPECTED_HEAD_SHA:?}" "${PR_NUMBER:?}" + pr="$(gh api --method GET --header 'Cache-Control: no-cache' \ + "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}")" + if ! jq -e --arg head "$EXPECTED_HEAD_SHA" \ + '.state == "open" and .draft == false and .head.sha == $head' \ + <<< "$pr" >/dev/null; then + echo '::error::Full verification is deferred or stale. The PR must be open, ready, and at the tested head.' + exit 1 + fi + - name: PR Validation Summary run: | echo "## PR Validation Results 📝" >> $GITHUB_STEP_SUMMARY @@ -238,8 +287,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY if [ "${{ needs.validate-build.result }}" != "success" ] || [ "${{ needs.validate-content.result }}" != "success" ] || - [ "${{ needs.validate-images.result }}" != "success" ] || - [ "${{ github.event.pull_request.draft }}" = "true" ]; then + [ "${{ needs.validate-images.result }}" != "success" ]; then echo "Full PR validation has not passed." >> $GITHUB_STEP_SUMMARY exit 1 fi From 0ec4e4e37bfcc0ef3f2d28022865172d5935a0d1 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Wed, 7 Oct 2026 18:47:09 +1100 Subject: [PATCH 3/3] ci: isolate tested revisions and verify current base --- .github/workflows/pr-validation.yml | 33 ++++++++++++++++------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index fcbb9da..d1f3933 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -20,11 +20,11 @@ on: - '.gitignore' - '.github/workflows/**' -# Runs of this workflow for the same PR share a cancellation group. -# GitHub does not guarantee event ordering; live state/head guards reject stale runs. +# Duplicate runs for the same PR/head/base share a cancellation group. +# Different revisions cannot cancel current verification; stale runs fail live guards. # Non-PR runs have unique keys, preserving every main/manual/scheduled run. concurrency: - group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('run-{0}', github.run_id) }} + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('pr-{0}-{1}-{2}', github.event.pull_request.number, github.event.pull_request.head.sha, github.event.pull_request.base.sha) || format('run-{0}', github.run_id) }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: @@ -91,15 +91,16 @@ jobs: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number }} EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + EXPECTED_BASE_SHA: ${{ github.event.pull_request.base.sha }} run: | set -euo pipefail - : "${EXPECTED_HEAD_SHA:?}" "${PR_NUMBER:?}" + : "${EXPECTED_HEAD_SHA:?}" "${EXPECTED_BASE_SHA:?}" "${PR_NUMBER:?}" pr="$(gh api --method GET --header 'Cache-Control: no-cache' \ "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}")" - if ! jq -e --arg head "$EXPECTED_HEAD_SHA" \ - '.state == "open" and .draft == false and .head.sha == $head' \ + if ! jq -e --arg head "$EXPECTED_HEAD_SHA" --arg base "$EXPECTED_BASE_SHA" \ + '.state == "open" and .draft == false and .head.sha == $head and .base.sha == $base' \ <<< "$pr" >/dev/null; then - echo '::error::Full verification is deferred or stale. The PR must be open, ready, and at the tested head.' + echo '::error::Full verification is deferred or stale. The PR must be open, ready, and at the tested head and base.' exit 1 fi @@ -148,15 +149,16 @@ jobs: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number }} EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + EXPECTED_BASE_SHA: ${{ github.event.pull_request.base.sha }} run: | set -euo pipefail - : "${EXPECTED_HEAD_SHA:?}" "${PR_NUMBER:?}" + : "${EXPECTED_HEAD_SHA:?}" "${EXPECTED_BASE_SHA:?}" "${PR_NUMBER:?}" pr="$(gh api --method GET --header 'Cache-Control: no-cache' \ "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}")" - if ! jq -e --arg head "$EXPECTED_HEAD_SHA" \ - '.state == "open" and .draft == false and .head.sha == $head' \ + if ! jq -e --arg head "$EXPECTED_HEAD_SHA" --arg base "$EXPECTED_BASE_SHA" \ + '.state == "open" and .draft == false and .head.sha == $head and .base.sha == $base' \ <<< "$pr" >/dev/null; then - echo '::error::Full verification is deferred or stale. The PR must be open, ready, and at the tested head.' + echo '::error::Full verification is deferred or stale. The PR must be open, ready, and at the tested head and base.' exit 1 fi @@ -249,15 +251,16 @@ jobs: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number }} EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + EXPECTED_BASE_SHA: ${{ github.event.pull_request.base.sha }} run: | set -euo pipefail - : "${EXPECTED_HEAD_SHA:?}" "${PR_NUMBER:?}" + : "${EXPECTED_HEAD_SHA:?}" "${EXPECTED_BASE_SHA:?}" "${PR_NUMBER:?}" pr="$(gh api --method GET --header 'Cache-Control: no-cache' \ "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}")" - if ! jq -e --arg head "$EXPECTED_HEAD_SHA" \ - '.state == "open" and .draft == false and .head.sha == $head' \ + if ! jq -e --arg head "$EXPECTED_HEAD_SHA" --arg base "$EXPECTED_BASE_SHA" \ + '.state == "open" and .draft == false and .head.sha == $head and .base.sha == $base' \ <<< "$pr" >/dev/null; then - echo '::error::Full verification is deferred or stale. The PR must be open, ready, and at the tested head.' + echo '::error::Full verification is deferred or stale. The PR must be open, ready, and at the tested head and base.' exit 1 fi