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
45 changes: 45 additions & 0 deletions .github/workflows/cancel-on-close.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Cancel stale runs on PR close

# GitHub Actions has no direct "cancel in-flight runs when a PR closes/merges"
# trigger -- concurrency groups only cancel a running job/workflow when a NEW
# entrant joins the SAME group. These two jobs deliberately do nothing except
# join the exact concurrency groups that ci.yml and claude-code-review.yml
# already use, so a PR closing (merged or not) cancels any CI matrix -- and,
# more importantly, any still-running (money-costing) Claude review -- for a
# PR nobody can act on anymore. Concurrency groups are enforced repo-wide,
# not scoped to the workflow file that declares them, so joining the same
# group string from here works.
#
# COUPLING: these group strings are copies, not references -- if ci.yml's
# `name:` or its `concurrency.group:` formula, or claude-code-review.yml's
# `concurrency.group:` formula, ever changes, update the matching string
# below or cancellation here silently stops matching (no error, it just
# becomes its own no-op group). ci.yml's verify-cancel-on-close-coupling job
# guards this automatically on every push -- it fails CI if those files or
# fields drift, so this comment is a "why", not the only line of defense.
on:
pull_request:
types: [closed]

permissions: {}

jobs:
cancel-ci:
# Matches ci.yml's `group: ${{ github.workflow }}-${{ github.ref }}`,
# where github.workflow is that file's `name: CI` and github.ref for a
# pull_request-triggered run is refs/pull/<number>/merge.
concurrency:
group: CI-refs/pull/${{ github.event.pull_request.number }}/merge
cancel-in-progress: true
runs-on: ubuntu-latest
steps:
- run: echo "Cancelled any in-flight CI run for PR #${{ github.event.pull_request.number }}"

cancel-claude-review:
# Matches claude-code-review.yml's `group: claude-review-<pr-number>`.
concurrency:
group: claude-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
runs-on: ubuntu-latest
steps:
- run: echo "Cancelled any in-flight Claude review for PR #${{ github.event.pull_request.number }}"
107 changes: 96 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ on:
branches:
- master
pull_request:
concurrency:
# Don't let a superseded push's full matrix (dozens of legs, several
# compiling pg_tle from source) keep occupying runner slots once a newer
# push on the same ref supersedes it. Not for master: every push there is
# its own point in history and should get a complete, uncancelled run.
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
env:
PGUSER: postgres
jobs:
Expand Down Expand Up @@ -328,6 +335,74 @@ jobs:
- name: Lint SQL
run: make lint

# cancel-on-close.yml cancels in-flight CI/claude-review runs on PR close by
# joining the SAME concurrency group strings this workflow and
# claude-code-review.yml use -- but it only works because those strings are
# hand-copied there, not referenced. No `if: docs_only` gate here on
# purpose: this is a near-instant metadata check, not worth ever skipping,
# and workflow files aren't "docs" anyway.
verify-cancel-on-close-coupling:
name: 🔗 Verify cancel-on-close.yml coupling
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v6
- name: Confirm ci.yml / claude-code-review.yml still match cancel-on-close.yml's assumptions
run: |
python3 - <<'PY'
import os, sys, yaml
# GitHub Actions pre-substitutes any expression marker (dollar sign
# directly followed by double-open-brace) it finds anywhere in a
# run: block's raw text -- including inside this heredoc -- before
# the script ever runs, regardless of quoting. D keeps that four-
# character sequence from ever appearing contiguously in this file
# (not even in this comment -- deliberately not spelled out here),
# so the expected strings below stay literal text for Python to
# compare against, instead of being evaluated away by Actions
# itself (which would make this check compare each expression's
# live value against itself -- always "equal", catching nothing).
D = "$"
def load(path):
if not os.path.isfile(path):
return None, f"{path} does not exist"
with open(path) as f:
return yaml.safe_load(f), None
errors = []
ci, err = load('.github/workflows/ci.yml')
if err:
errors.append(err)
else:
if ci.get('name') != 'CI':
errors.append(f"ci.yml name is {ci.get('name')!r}, expected 'CI'")
ci_group = (ci.get('concurrency') or {}).get('group')
expected = D + "{{ github.workflow }}-" + D + "{{ github.ref }}"
if ci_group != expected:
errors.append(f"ci.yml concurrency.group is {ci_group!r}, expected {expected!r}")
review, err = load('.github/workflows/claude-code-review.yml')
if err:
errors.append(err)
elif not review.get('jobs', {}).get('claude-review'):
errors.append("claude-code-review.yml no longer has a 'claude-review' job")
else:
review_group = (review.get('concurrency') or {}).get('group')
expected = "claude-review-" + D + "{{ github.event.pull_request.number }}"
if review_group != expected:
errors.append(f"claude-code-review.yml concurrency.group is {review_group!r}, expected {expected!r}")
if errors:
print("cancel-on-close.yml's assumptions about ci.yml / claude-code-review.yml no longer hold:")
for e in errors:
print(f" - {e}")
print("Update the hardcoded group: strings in .github/workflows/cancel-on-close.yml (or restore the files/fields) to match.")
sys.exit(1)
print("OK: cancel-on-close.yml's group strings still match ci.yml and claude-code-review.yml.")
PY
# Proves cat_tools survives a BINARY pg_upgrade (in-place catalog migration to a
# newer PostgreSQL major), not just an in-place extension update. Each leg is a
# SINGLE jump: install an old cat_tools on an old cluster, plant a dependency
Expand All @@ -338,8 +413,14 @@ jobs:
# new one. Every leg's new_pg supports the current version (PG12+); the
# version-by-version climb through EVERY major is pg-upgrade-stepwise.
pg-upgrade-test:
needs: [changes]
if: needs.changes.outputs.docs_only != 'true'
# Gated behind test+lint (not just changes): this matrix is expensive
# (multiple binary pg_upgrades per leg), and every leg would fail anyway
# on a baseline that's already broken by a failing fresh-install test or
# lint violation. success() is required explicitly once a job's `if:`
# references anything -- GitHub only assumes success() as a default when
# no `if:` is written at all.
needs: [changes, test, lint]
if: success() && needs.changes.outputs.docs_only != 'true'
strategy:
matrix:
include:
Expand Down Expand Up @@ -457,8 +538,9 @@ jobs:
# the guard survived each step. CI-heavy on purpose: it installs PostgreSQL 10
# through 18, performs 8 sequential pg_upgrades, and runs the suite 7 times.
pg-upgrade-stepwise:
needs: [changes]
if: needs.changes.outputs.docs_only != 'true'
# Gated behind test+lint -- see the comment on pg-upgrade-test's needs.
needs: [changes, test, lint]
if: success() && needs.changes.outputs.docs_only != 'true'
name: 🪜 Stepwise pg_upgrade 10 → 18 (from 0.2.0)
runs-on: ubuntu-latest
container: pgxn/pgxn-tools
Expand Down Expand Up @@ -558,8 +640,9 @@ jobs:
# sole version where they still load. Complements pg-upgrade-test, which covers
# the cross-major-version binary upgrade instead.
extension-update-test:
needs: [changes]
if: needs.changes.outputs.docs_only != 'true'
# Gated behind test+lint -- see the comment on pg-upgrade-test's needs.
needs: [changes, test, lint]
if: success() && needs.changes.outputs.docs_only != 'true'
strategy:
matrix:
# PG12+: exercise the WIDEST update path we support — CREATE EXTENSION at
Expand Down Expand Up @@ -641,8 +724,9 @@ jobs:
run: bin/test_existing update-scenario cat_tools_update 0.2.2

pg-tle-test:
needs: [changes]
if: needs.changes.outputs.docs_only != 'true'
# Gated behind test+lint -- see the comment on pg-upgrade-test's needs.
needs: [changes, test, lint]
if: success() && needs.changes.outputs.docs_only != 'true'
strategy:
matrix:
# Current-supported majors, from the single source in the changes job
Expand Down Expand Up @@ -768,8 +852,9 @@ jobs:
run: bin/assert_fs_clean verify ${{ matrix.pg }} /tmp/control_baseline.txt

pg-tle-upgrade-test:
needs: [changes]
if: needs.changes.outputs.docs_only != 'true'
# Gated behind test+lint -- see the comment on pg-upgrade-test's needs.
needs: [changes, test, lint]
if: success() && needs.changes.outputs.docs_only != 'true'
strategy:
matrix:
include:
Expand Down Expand Up @@ -892,7 +977,7 @@ jobs:
# the heavy jobs gated off by the `changes` job on a docs-only push), and
# fails if any failed or were cancelled.
all-checks-passed:
needs: [changes, test, lint, pg-upgrade-test, pg-upgrade-stepwise, extension-update-test, pg-tle-test, pg-tle-upgrade-test]
needs: [changes, test, lint, verify-cancel-on-close-coupling, pg-upgrade-test, pg-upgrade-stepwise, extension-update-test, pg-tle-test, pg-tle-upgrade-test]
if: always()
runs-on: ubuntu-latest
steps:
Expand Down
Loading