priority lanes for rebase-cascade pushes, escalate on stack promotion #327
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # =========================================================================== | |
| # Test strategy | |
| # | |
| # count_nulls can be arrived at several ways, each of which can break | |
| # differently, so each is exercised by its own job below: | |
| # | |
| # test -- FRESH install: CREATE EXTENSION at the current | |
| # version, across every supported PostgreSQL | |
| # major. Also proves the IN-PLACE extension | |
| # update path (CREATE EXTENSION at 0.9.6, then | |
| # ALTER EXTENSION UPDATE - same PostgreSQL, no | |
| # pg_upgrade) in the same job/matrix, rather | |
| # than a dedicated job: a load mode is just an | |
| # input the same assertions run against, not a | |
| # real environment difference, so giving it its | |
| # own job would only duplicate this job's own | |
| # per-PG-version container/checkout setup for | |
| # no added confidence. | |
| # pg-upgrade-test -- BINARY pg_upgrade: install 0.9.6 on the OLD | |
| # PostgreSQL floor (the same META.json-derived | |
| # floor the `changes` job's supported_pg uses - | |
| # see its floor_pg output), then binary-upgrade | |
| # the cluster to the newest supported major | |
| # (changes job's newest_pg). A single leg (not | |
| # the full PG matrix - by far the most | |
| # expensive job here, installing two full | |
| # PostgreSQL majors and running the real | |
| # pg_upgrade binary), which via twin databases | |
| # migrated by the SAME binary pg_upgrade call | |
| # compares BOTH update-vs-pg_upgrade orderings | |
| # a real user could hit - see the job's own | |
| # comment for why one floor is enough to cover | |
| # both. | |
| # pg-tle-test -- pg_tle DEPLOYMENT: fresh install AND the | |
| # 0.9.6 -> current update path, both registered | |
| # through AWS pg_tle's database-backed catalog | |
| # instead of a filesystem .control file. | |
| # | |
| # During testing we always install into a freshly, randomly generated | |
| # schema (see test/install/load.sql; test/README.md covers how | |
| # test/expected/extension_tests.out stays schema-invariant). | |
| # | |
| # `changes` is a cheap gate that lets the heavy jobs above skip themselves on | |
| # doc-only pushes, and also derives the shared PostgreSQL-major list those | |
| # jobs consume from a single set of constants. `all-checks-passed` is the | |
| # single stable required-status-check name. | |
| # | |
| # Draft PRs get a further reduction, independent of `changes`/docs_only, | |
| # aimed at cutting shared-runner load while a PR is still being iterated on | |
| # (this repo's org-wide Actions queue backs up easily): `lint` always runs | |
| # in full; `test`'s matrix drops to just the newest supported PostgreSQL | |
| # major (see its own comment) instead of running full or being skipped | |
| # outright, since it's cheap per-leg and a draft author still wants signal | |
| # on every push; every other heavy job (`pg-tle-test`, `pg-upgrade-test`) | |
| # is skipped entirely via an added `&& github.event.pull_request.draft != | |
| # true` on its existing `if:`. None of this applies to a `push` event (e.g. | |
| # the post-merge run on master) or a non-draft PR, both of which always run | |
| # the full suite exactly as before. `pull_request.types` below includes | |
| # `ready_for_review` specifically so marking a draft PR ready retriggers | |
| # immediately at full scope (draft is already false by the time that event | |
| # fires) instead of leaving the reduced draft-time result on the PR's last | |
| # commit as its current status until some later real push. | |
| # | |
| # CI PRIORITY: a `gh stack rebase` cascade pushes to every PR above the one | |
| # actually being changed, purely to move it onto a new base - the PR's own | |
| # diff (what it actually contributes) is unchanged. Running that at the | |
| # same priority as real new commits means a burst of cascade pushes can | |
| # crowd out the runner capacity a genuinely active push needs for fast | |
| # feedback. `changes` tells the two apart via a base-independent content | |
| # hash (bin/patch_id_hash, robust to rebase-induced context shifts - see | |
| # its own comment - persisted per-PR across pushes via actions/cache since | |
| # GitHub Actions has no other cross-run memory): a synchronize push whose | |
| # hash matches the last one observed for this PR is routed to a | |
| # LOW-priority lane (`ci-<job>-lowprio-<0..2>`, a small fixed set shared | |
| # across ALL PRs, capping rebase noise to at most 3 concurrent runs | |
| # repo-wide instead of letting it consume the account's whole concurrency | |
| # budget) rather than the per-PR high-priority lane real pushes use. It | |
| # still gets the FULL matrix either way, just possibly later - a rebase CAN | |
| # break something the diff itself didn't touch, and this repo won't merge | |
| # without a clean run regardless. `edited` is in `pull_request.types` so a | |
| # base retarget (the signal gh stack sends when this PR is promoted to the | |
| # bottom of its stack - the next one due to merge) can be caught by the | |
| # `escalate` step and bumped to an immediate high-priority run bypassing | |
| # whatever lane its last push landed in, UNLESS a full run already exists | |
| # for that exact head SHA (checked via the Checks API), in which case | |
| # there's nothing to gain by re-running it. `ready_for_review` gets the | |
| # same escalation unconditionally (no existing-run check): its prior run, | |
| # if any, was necessarily the reduced draft-time one above, which doesn't | |
| # count as "already had one". | |
| # =========================================================================== | |
| name: CI | |
| on: | |
| push: | |
| branches: | |
| - master | |
| pull_request: | |
| # Explicit (not the GitHub default of [opened, synchronize, reopened]): | |
| # `ready_for_review` retriggers immediately when a draft PR is marked | |
| # ready (see the top-of-file comment); `edited` catches a base retarget | |
| # for the priority-escalation check in the `changes` job below. | |
| types: [opened, synchronize, reopened, edited, ready_for_review] | |
| jobs: | |
| # Cheap gate that lets the heavy jobs below skip themselves on commits that | |
| # touch only docs. Must run on every push/pull_request (no paths-ignore on | |
| # the workflow itself), otherwise the required all-checks-passed check | |
| # would never report on doc-only pushes and get stuck Pending in branch | |
| # protection. | |
| # | |
| # Also derives the supported-PostgreSQL-major list every other job reads, | |
| # from a single NEWEST constant plus META.json's own declared minimum (not | |
| # a second hand-maintained floor constant) - every job that cares which | |
| # majors are supported reads the SAME values, so they can't silently drift | |
| # onto different sets, and adding a new major is a one-line change here | |
| # instead of an edit in several jobs. | |
| # - supported_pg: the full descending list (NEWEST down to the | |
| # META.json-derived floor), as a JSON array, for jobs that need every | |
| # supported major. | |
| # - newest_pg: just the single newest major, as a bare scalar, for | |
| # anything that needs one value instead of the full array. | |
| # - floor_pg: just the single oldest (META.json-derived) major, as a | |
| # bare scalar, for the same reason. | |
| changes: | |
| name: 🔍 Detect docs-only changes & derive PG matrix | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| checks: read # escalate step: has this SHA already got a full run? | |
| outputs: | |
| docs_only: ${{ steps.diff.outputs.docs_only }} | |
| supported_pg: ${{ steps.pg.outputs.supported_pg }} | |
| newest_pg: ${{ steps.pg.outputs.newest_pg }} | |
| floor_pg: ${{ steps.pg.outputs.floor_pg }} | |
| priority: ${{ steps.diff.outputs.priority }} | |
| lane: ${{ steps.lane.outputs.lane }} | |
| steps: | |
| - name: Check out the repo | |
| uses: actions/checkout@v4 | |
| with: | |
| # Full history needed so BASE and HEAD below are both reachable | |
| # for `git diff`. | |
| fetch-depth: 0 | |
| - name: Base retargeted onto a new branch? Check whether this SHA already has a full run | |
| # Fires only for `edited` WITH a base change - the gh-stack-promotion | |
| # signal described at the top of this file. A plain title/body edit | |
| # also fires `edited` but leaves `github.event.changes.base` absent, | |
| # so it's excluded here and falls through to the diff step's | |
| # edited-with-nothing-to-do branch below. | |
| id: escalate | |
| if: >- | |
| github.event_name == 'pull_request' && | |
| github.event.action == 'edited' && | |
| github.event.changes.base != null | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| existing=$(gh api "repos/$REPO/commits/$SHA/check-runs" --paginate \ | |
| --jq '[.check_runs[] | select(.name == "all-checks-passed")]' 2>/dev/null || echo '[]') | |
| count=$(echo "$existing" | jq 'length') | |
| if [ "$count" -gt 0 ]; then | |
| echo "escalate=false" >> "$GITHUB_OUTPUT" | |
| echo "SHA $SHA already has an all-checks-passed run - nothing to escalate" | |
| else | |
| echo "escalate=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Restore this PR's previously observed content patch-id | |
| id: patchid-restore | |
| if: github.event_name == 'pull_request' && github.event.action != 'edited' | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: /tmp/prev_patch_id | |
| # A key guaranteed not to already exist, so this always falls | |
| # through to restore-keys (prefix match, picks the MOST RECENT | |
| # save) instead of ever hitting its own key directly. | |
| key: unused-${{ github.run_id }} | |
| restore-keys: | | |
| ci-patchid-pr-${{ github.event.pull_request.number }}- | |
| - name: Compute per-push changed files, docs-only status, and CI priority | |
| id: diff | |
| run: | | |
| # Fail safe to running the full matrix at high priority: default | |
| # both immediately, before anything below has a chance to compute | |
| # or fail. Writing the same GITHUB_OUTPUT key twice is fine (the | |
| # last write wins), so the only way this step ends with | |
| # docs_only=true or priority=low is by genuinely proving it | |
| # further down - never by skipping past an edge case with a | |
| # default. | |
| echo "docs_only=false" >> "$GITHUB_OUTPUT" | |
| echo "priority=high" >> "$GITHUB_OUTPUT" | |
| if [ "${{ github.event.action }}" = "edited" ]; then | |
| # Either a base retarget with nothing outstanding to run | |
| # (escalate above said false - an all-checks-passed run already | |
| # covers this SHA), or a plain title/description edit (no base | |
| # change at all, so escalate didn't even run). Either way, | |
| # there's no code to test right now. | |
| if [ "${{ steps.escalate.outputs.escalate }}" != "true" ]; then | |
| echo "docs_only=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| exit 0 | |
| fi | |
| if [ "${{ github.event_name }}" = "pull_request" ] && \ | |
| [ "${{ github.event.action }}" = "synchronize" ] && \ | |
| [ -n "${{ github.event.before }}" ]; then | |
| # A push to an already-open PR: before/after give the true | |
| # per-push diff, same as for a branch push. | |
| BASE="${{ github.event.before }}" | |
| HEAD="${{ github.event.after }}" | |
| elif [ "${{ github.event_name }}" = "pull_request" ]; then | |
| # First run for this PR (opened/reopened/etc, or synchronize | |
| # without a usable before): fall back to the whole base...head | |
| # diff. | |
| BASE="${{ github.event.pull_request.base.sha }}" | |
| HEAD="${{ github.event.pull_request.head.sha }}" | |
| else | |
| BASE="${{ github.event.before }}" | |
| HEAD="${{ github.event.after }}" | |
| fi | |
| echo "base=$BASE" | |
| echo "head=$HEAD" | |
| # A missing HEAD, or an all-zeros BASE (e.g. a new branch's first | |
| # push, where GitHub reports no prior commit), means we can't | |
| # compute a real diff. docs_only is already false from above; | |
| # just stop here rather than risk skipping tests. | |
| if [ -z "$HEAD" ] || [ -z "$BASE" ] || [[ "$BASE" =~ ^0+$ ]]; then | |
| exit 0 | |
| fi | |
| CHANGED=$(git diff --name-only "$BASE" "$HEAD" || echo __DIFF_FAILED__) | |
| DOCS_ONLY=true | |
| if [ "$CHANGED" = "__DIFF_FAILED__" ] || [ -z "$CHANGED" ]; then | |
| DOCS_ONLY=false | |
| else | |
| while IFS= read -r f; do | |
| if ! [[ "$f" =~ \.(md|asc)$ ]]; then | |
| DOCS_ONLY=false | |
| break | |
| fi | |
| done <<< "$CHANGED" | |
| fi | |
| echo "changed files:" | |
| echo "$CHANGED" | |
| echo "docs_only=$DOCS_ONLY" >> "$GITHUB_OUTPUT" | |
| # Priority: a synchronize push whose own patch-id (the PR's | |
| # actual contribution, independent of what it's based on) matches | |
| # the last one observed for this PR is a pure gh-stack rebase | |
| # cascade, not new work - safe to route to the low-priority lane. | |
| # Real new commits, and this PR's first appearance (nothing to | |
| # compare against yet), keep the high priority set above. | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| NEW_PATCH_ID=$(bin/patch_id_hash "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}") | |
| echo "content patch-id: $NEW_PATCH_ID" | |
| if [ "${{ github.event.action }}" = "synchronize" ] && \ | |
| [ -f /tmp/prev_patch_id ] && \ | |
| [ "$(cat /tmp/prev_patch_id)" = "$NEW_PATCH_ID" ]; then | |
| echo "priority=low" >> "$GITHUB_OUTPUT" | |
| fi | |
| echo "$NEW_PATCH_ID" > /tmp/prev_patch_id | |
| fi | |
| - name: Save this push's content patch-id for the next push to compare against | |
| if: github.event_name == 'pull_request' && github.event.action != 'edited' | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: /tmp/prev_patch_id | |
| key: ci-patchid-pr-${{ github.event.pull_request.number }}-${{ github.run_id }} | |
| - name: Assign a low-priority lane number | |
| id: lane | |
| # A fixed small number of shared lanes (not one lane per PR) bounds | |
| # how much of the account's concurrent-runner budget rebase-cascade | |
| # noise can ever occupy at once, so it can't crowd out real-work | |
| # runs waiting for a runner - at the cost of not deduping a stale | |
| # rebase run against a newer one for the SAME PR if they land in the | |
| # same lane as an unrelated PR's job (acceptable: a stale completed | |
| # check on an old SHA doesn't block merging the new SHA). | |
| if: github.event_name == 'pull_request' | |
| run: echo "lane=$(( ${{ github.event.pull_request.number }} % 3 ))" >> "$GITHUB_OUTPUT" | |
| - name: Derive the supported-PostgreSQL-major list | |
| id: pg | |
| run: | | |
| # A dozen-odd lines to replace what looks like a handful of version | |
| # references, but it buys CONSISTENCY: every job that needs the | |
| # supported-major list derives it from this ONE source, so none of | |
| # them can silently drift onto a different list. Adding a new | |
| # major is a one-line NEWEST bump here, not an edit in N places. | |
| # | |
| # The floor itself is NOT a second hardcoded constant here: it's | |
| # read straight from META.json's own build prereq (see | |
| # META.in.json's comment: "Depends on JSONB, created in 9.4"), | |
| # which is the actual source of truth for count_nulls' minimum | |
| # supported major. Hand-maintaining a duplicate floor constant in | |
| # this workflow is exactly how it ended up testing down to 10 while | |
| # META.json claimed 9.4 - 9.4/9.5/9.6 went untested for a while | |
| # even though pg-start genuinely installs them fine on current | |
| # Actions infra, and nobody noticed the gap. | |
| NEWEST=18 | |
| DECLARED_FLOOR=$(jq -r '.prereqs.build.requires.PostgreSQL' META.json) # e.g. "9.4.0" | |
| FLOOR_MAJOR=$(echo "$DECLARED_FLOOR" | cut -d. -f1) | |
| if [ "$FLOOR_MAJOR" -lt 10 ]; then | |
| # PostgreSQL's pre-10 versioning used X.Y as the major identifier | |
| # (10+ switched to a single integer) - e.g. "9.4.0" -> major | |
| # "9.4", not "9". | |
| FLOOR=$(echo "$DECLARED_FLOOR" | cut -d. -f1,2) | |
| else | |
| FLOOR="$FLOOR_MAJOR" | |
| fi | |
| # A sub-10 floor needs two `seq` runs stitched together: one for | |
| # the integer majors (10..NEWEST) and one for the X.Y majors | |
| # (9.FLOOR_MINOR..9.6, since 9.6 was the last 9.x release). | |
| if [[ "$FLOOR" == 9.* ]]; then | |
| FLOOR_MINOR=$(echo "$FLOOR" | cut -d. -f2) | |
| supported="$(seq "$NEWEST" -1 10) $(seq 6 -1 "$FLOOR_MINOR" | sed 's/^/9./')" | |
| else | |
| supported=$(seq "$NEWEST" -1 "$FLOOR") | |
| fi | |
| # Emit a JSON array of STRINGS, not bare numbers - "9.4" isn't a | |
| # valid bare integer token, and keeping every leg (10+ and 9.x | |
| # alike) as a string keeps matrix.pg comparisons/interpolations | |
| # consistent regardless of which leg they're in. $supported is | |
| # deliberately unquoted below: it word-splits (on both the spaces | |
| # AND the newlines `seq`/the concatenation above produce) into | |
| # individual version tokens for printf to repeat over. | |
| json() { printf '"%s",' $supported | sed 's/,$//; s/^/[/; s/$/]/'; } | |
| echo "supported_pg=$(json)" >> "$GITHUB_OUTPUT" | |
| # Also emitted as a bare scalar (not a JSON array), for anything | |
| # that needs just the single newest major instead of the full | |
| # array - so NEWEST still only needs to change in one place. | |
| echo "newest_pg=$NEWEST" >> "$GITHUB_OUTPUT" | |
| # Same idea for the OLDEST supported major: emit the exact $FLOOR | |
| # value supported_pg above already derived from META.json, as a | |
| # bare scalar, for anything that needs just the single oldest | |
| # major instead of the full array - rather than hand-maintaining a | |
| # THIRD copy of this number. | |
| echo "floor_pg=$FLOOR" >> "$GITHUB_OUTPUT" | |
| lint: | |
| name: 🧹 SQL lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check out the repo | |
| uses: actions/checkout@v4 | |
| - name: make lint | |
| # Deliberately not pre-initializing the .vendor/linter submodule via | |
| # `submodules:` above -- letting `make lint` self-init it (lint.mk) | |
| # is what actually proves that works from a plain clone. | |
| run: make lint | |
| # Fresh install, then the in-place extension update path, both across the | |
| # PG matrix. The update leg CREATE EXTENSIONs at the oldest version we | |
| # still ship a full install script for (0.9.6), then ALTER EXTENSION | |
| # UPDATEs to current (no pg_upgrade, same PostgreSQL) and reruns the | |
| # suite - a single job rather than a dedicated one, since a load mode is | |
| # just an input the same assertions run against, not a real environment | |
| # difference, and the per-version container/checkout setup would | |
| # otherwise be duplicated across two jobs with the same PG matrix. Each | |
| # install (both legs) always targets its own freshly, randomly generated | |
| # schema (see test/install/load.sql), so both legs pass against the SAME | |
| # test/expected/extension_tests.out (see test/README.md for how the | |
| # suite keeps its output schema-invariant). | |
| test: | |
| needs: [changes] | |
| if: needs.changes.outputs.docs_only != 'true' | |
| # "ci-test-" prefix, distinct from pg-upgrade-test's/pg-tle-test's below: | |
| # those run CONCURRENTLY with this job within the same PR's workflow run, | |
| # so sharing a group name across job types would make them cancel/queue | |
| # behind EACH OTHER instead of behind their own prior runs for this job. | |
| # See the top-of-file CI PRIORITY comment for the low-priority lane | |
| # scheme this implements. | |
| # | |
| # The high-priority group includes ${{ matrix.pg }}: without it, every | |
| # leg of THIS SAME push's own matrix would collide on one shared group | |
| # name and cancel-in-progress would cancel siblings, not just a prior | |
| # push's run - the exact opposite of "always runs the full matrix | |
| # either way". Confirmed live: before this split, most legs of a normal | |
| # (non-rebase) push came back cancelled instead of completing. | |
| # | |
| # KNOWN OPEN ISSUE, not yet resolved: the low-priority group deliberately | |
| # has NO such per-leg split (that's the point - it's the shared, | |
| # capped-concurrency lane), but GitHub Actions' default concurrency | |
| # behavior only keeps ONE job PENDING per group at a time and CANCELS | |
| # the previous pending one when a new one arrives (no `queue:` support | |
| # for an expression, and `queue: max` can't combine with a | |
| # conditionally-true `cancel-in-progress`) - confirmed live: pushing an | |
| # unchanged-patch-id follow-up here cancelled most of this job's own | |
| # matrix legs instead of queueing them, contradicting "still gets the | |
| # FULL matrix either way" above for any push whose matrix has more legs | |
| # than fit through instantaneously. Needs a real decision (accept a | |
| # weaker/no concurrency cap, split low-priority into its own dedicated | |
| # job with `queue: max`, or something else) before relying on this path. | |
| concurrency: | |
| group: >- | |
| ${{ needs.changes.outputs.priority == 'low' | |
| && format('ci-test-lowprio-{0}', needs.changes.outputs.lane) | |
| || format('ci-test-{0}-{1}', github.event.pull_request.number || github.sha, matrix.pg) }} | |
| cancel-in-progress: ${{ needs.changes.outputs.priority != 'low' }} | |
| strategy: | |
| matrix: | |
| # From the single source in the changes job. On a draft PR, reduced | |
| # to just the newest supported major (never skipped outright, unlike | |
| # the other heavy jobs below - this is the one signal a draft author | |
| # still wants on every push): `github.event.pull_request.draft` is | |
| # null/falsy for a push event (e.g. the post-merge run on master), so | |
| # this expression falls through to the full list there with no extra | |
| # guard needed. | |
| pg: ${{ github.event.pull_request.draft && fromJSON(format('[{0}]', needs.changes.outputs.newest_pg)) || fromJSON(needs.changes.outputs.supported_pg) }} | |
| name: 🐘 PostgreSQL ${{ matrix.pg }} | |
| runs-on: ubuntu-latest | |
| container: pgxn/pgxn-tools | |
| steps: | |
| - name: Start PostgreSQL ${{ matrix.pg }} | |
| run: pg-start ${{ matrix.pg }} | |
| - name: Check out the repo | |
| uses: actions/checkout@v4 | |
| - name: Test on PostgreSQL ${{ matrix.pg }} | |
| run: make test | |
| - name: Install count_nulls | |
| run: make install | |
| - name: Update 0.9.6 -> current and run the suite | |
| run: make verify-results TEST_LOAD_SOURCE=update | |
| - name: Structurally compare the updated objects against a fresh install | |
| # A fixed pgTAP suite only proves the specific behaviors it asserts | |
| # still hold; it can't catch an update script that leaves some | |
| # definition/comment/ACL subtly different from what a fresh install | |
| # of the same version produces. bin/compare_fresh_vs_update installs | |
| # both ways itself, in its own scratch databases - each an | |
| # unqualified `CREATE EXTENSION`, so both land in the same default | |
| # schema by construction, which is all the diff needs to isolate | |
| # real update-vs-fresh divergence rather than a schema-name | |
| # difference - and diffs every object the extension owns; any | |
| # nonempty diff fails the step. Not a make target (it's a | |
| # standalone script, not `make test`), so invoked directly here. | |
| run: bin/compare_fresh_vs_update 0.9.6 | |
| # Proves count_nulls survives a BINARY pg_upgrade (in-place catalog | |
| # migration to a newer PostgreSQL major). Installs 0.9.6 on the oldest | |
| # supported PostgreSQL floor, plants a dependency guard, binary- | |
| # pg_upgrades to the newest supported major, then runs the suite against | |
| # the REAL migrated objects in existing mode. | |
| # | |
| # A single old_pg/new_pg leg (old_pg from the changes job's floor_pg | |
| # output, new_pg from its newest_pg output - see that job's comment; NOT | |
| # a second/third hardcoded copy of either number), which via TWIN | |
| # databases in the SAME cluster, migrated by a SINGLE pg_upgrade call, | |
| # compares BOTH update-vs-pg_upgrade orderings a real user could hit: | |
| # upgrade_oldest_first -- stays at 0.9.6 through the | |
| # binary pg_upgrade, updated to current AFTER it (on the NEW | |
| # cluster). Proves pg_upgrade itself correctly preserves objects | |
| # that are STILL at the oldest supported version at the moment of | |
| # the upgrade - a real-world scenario (a user upgrading their | |
| # PostgreSQL major while still running an old extension version). | |
| # upgrade_current_first -- updated to current BEFORE the | |
| # binary pg_upgrade (on the OLD cluster). Proves pg_upgrade | |
| # correctly migrates the objects count_nulls' CURRENT code actually | |
| # creates - migrating 0.9.6's objects and updating afterward would | |
| # instead test whether pg_upgrade can migrate a legacy structure | |
| # frozen in the past, which isn't actionable on its own (that | |
| # version already shipped; nothing to fix if it turned out | |
| # fragile) - which is exactly why this leg exists alongside | |
| # oldest_first rather than instead of it. | |
| # The ordering comparison is a property of pg_upgrade itself, not of | |
| # which PG floor it runs from, so a single floor is enough to cover | |
| # both orderings - there was never a technical reason for a second | |
| # old_pg floor here (the previous old_pg=12 leg predated this ordering | |
| # comparison and had no purpose once it existed). | |
| # | |
| # Both databases still end up at the current version by the time | |
| # run-suite runs, so nothing downstream (the pgTAP suite, the | |
| # dependency guard check) needs to differ - only which side(s) of the | |
| # pg_upgrade call each database's update happens on. No bridge-update | |
| # step first: count_nulls has always been pure SQL functions with no | |
| # SELECT-*-over-catalog views, so it has no known pg_upgrade-unsafe old | |
| # version to bridge past. | |
| # | |
| # Deliberately not doing a stepwise every-major-in-sequence climb (one | |
| # cluster walking 10->11->12->...->newest, vs. the single big jump | |
| # here): that would catch a regression specific to one particular | |
| # major-to-major boundary, which would matter if count_nulls had views/ | |
| # functions touching catalog internals, but it doesn't - pure SQL | |
| # functions over anyarray/json/jsonb, nothing version-sensitive to | |
| # break at a specific boundary. Revisit if count_nulls ever grows | |
| # something catalog-touching. | |
| pg-upgrade-test: | |
| needs: [changes] | |
| # Skipped outright (not just matrix-reduced like `test`) on a draft PR: | |
| # this is a heavy job, and a draft author doesn't need a real binary | |
| # pg_upgrade re-proven on every push while still iterating. | |
| if: needs.changes.outputs.docs_only != 'true' && github.event.pull_request.draft != true | |
| # "ci-pgupgrade-" prefix, matrix.old_pg/new_pg split - see the `test` | |
| # job's concurrency comment above (currently a single-combination | |
| # matrix, but split the same way regardless in case that ever changes). | |
| concurrency: | |
| group: >- | |
| ${{ needs.changes.outputs.priority == 'low' | |
| && format('ci-pgupgrade-lowprio-{0}', needs.changes.outputs.lane) | |
| || format('ci-pgupgrade-{0}-{1}-{2}', github.event.pull_request.number || github.sha, matrix.old_pg, matrix.new_pg) }} | |
| cancel-in-progress: ${{ needs.changes.outputs.priority != 'low' }} | |
| strategy: | |
| matrix: | |
| # Single-element lists built from the changes job's floor_pg/ | |
| # newest_pg scalar outputs (see that job's comment), NOT a second/ | |
| # third hardcoded copy of either number - keeping this a `matrix:` | |
| # (rather than plain `env:`) preserves the ${{ matrix.old_pg }}/ | |
| # ${{ matrix.new_pg }} interpolations used throughout the steps | |
| # below unchanged. | |
| old_pg: ["${{ needs.changes.outputs.floor_pg }}"] | |
| new_pg: ["${{ needs.changes.outputs.newest_pg }}"] | |
| name: 🔄 Binary pg_upgrade ${{ matrix.old_pg }} → ${{ matrix.new_pg }} | |
| runs-on: ubuntu-latest | |
| container: pgxn/pgxn-tools | |
| env: | |
| # Both clusters must use the same initdb options or pg_upgrade | |
| # refuses to run. | |
| INITDB_OPTS: --data-checksums --auth trust | |
| steps: | |
| - name: Install PostgreSQL ${{ matrix.old_pg }} (cluster created next, with the right initdb options from the start) | |
| run: NO_CLUSTER=1 pg-start ${{ matrix.old_pg }} | |
| - name: Create old cluster with data checksums enabled | |
| run: | | |
| # -p 5432: pg_createcluster would otherwise assign the next | |
| # available port; force 5432 so subsequent psql/createdb calls | |
| # connect without -p. | |
| pg_createcluster -p 5432 ${{ matrix.old_pg }} test -- $INITDB_OPTS | |
| pg_ctlcluster ${{ matrix.old_pg }} test start | |
| pg_isready -t 30 | |
| - name: Check out the repo | |
| uses: actions/checkout@v4 | |
| - name: Install count_nulls into old cluster | |
| run: make install | |
| - name: Prepare the old cluster (install + dependency guard), twin databases | |
| # Two separate databases (distinct names, one per ordering) so | |
| # both exist in the SAME cluster ahead of the single pg_upgrade | |
| # call below - that one binary upgrade migrates both at once. | |
| # prepare-old installs count_nulls at 0.9.6 in each, then plants + | |
| # proves the dependency guard, so a later accidental CASCADE drop | |
| # anywhere in this job cannot silently make the eventual | |
| # existing-mode run test a fresh install instead. | |
| run: | | |
| bin/test_existing prepare-old upgrade_oldest_first 0.9.6 | |
| bin/test_existing prepare-old upgrade_current_first 0.9.6 | |
| - name: Update to current before pg_upgrade (proves pg_upgrade preserves CURRENT objects) | |
| # Updates ONLY upgrade_current_first | |
| # (upgrade_oldest_first must NOT be touched here - it | |
| # stays at 0.9.6 until AFTER pg_upgrade, below). Exercises ALTER | |
| # EXTENSION UPDATE on the OLD cluster, BEFORE the binary pg_upgrade | |
| # below, running the 0.9.6->stable update script, so pg_upgrade | |
| # runs against already-current objects, not 0.9.6 ones. `make | |
| # install` above already installed the current version's update | |
| # scripts/control file into this (old) cluster's sharedir, so | |
| # they're in place for this ALTER EXTENSION UPDATE to use. | |
| run: bin/test_existing update upgrade_current_first | |
| - name: Install PostgreSQL ${{ matrix.new_pg }} | |
| run: apt-get install -y postgresql-${{ matrix.new_pg }} postgresql-server-dev-${{ matrix.new_pg }} | |
| - name: Install count_nulls into new cluster | |
| # PG_CONFIG must be specified explicitly: at this point both old | |
| # and new PostgreSQL are installed, and the default pg_config on | |
| # PATH may not be the new version's. | |
| run: make install PG_CONFIG=/usr/lib/postgresql/${{ matrix.new_pg }}/bin/pg_config | |
| - name: Stop old cluster, binary pg_upgrade to PostgreSQL ${{ matrix.new_pg }}, start new cluster | |
| run: | | |
| pg_ctlcluster ${{ matrix.old_pg }} test stop | |
| pg_createcluster -p 5432 ${{ matrix.new_pg }} test -- $INITDB_OPTS | |
| # PG17+ writes logs to $new_datadir/pg_upgrade_output.d/; older | |
| # versions write to CWD. Search both on failure. | |
| mkdir -p /tmp/pg_upgrade_logs | |
| chown postgres:postgres /tmp/pg_upgrade_logs | |
| su -c "cd /tmp/pg_upgrade_logs && /usr/lib/postgresql/${{ matrix.new_pg }}/bin/pg_upgrade \ | |
| -b /usr/lib/postgresql/${{ matrix.old_pg }}/bin \ | |
| -B /usr/lib/postgresql/${{ matrix.new_pg }}/bin \ | |
| -d /var/lib/postgresql/${{ matrix.old_pg }}/test \ | |
| -D /var/lib/postgresql/${{ matrix.new_pg }}/test \ | |
| -o '-c config_file=/etc/postgresql/${{ matrix.old_pg }}/test/postgresql.conf' \ | |
| -O '-c config_file=/etc/postgresql/${{ matrix.new_pg }}/test/postgresql.conf'" postgres \ | |
| || { find /tmp/pg_upgrade_logs \ | |
| /var/lib/postgresql/${{ matrix.new_pg }}/test/pg_upgrade_output.d \ | |
| -name '*.log' 2>/dev/null | sort | xargs -r tail -n +1; exit 1; } | |
| pg_ctlcluster ${{ matrix.new_pg }} test start | |
| - name: Update to current after pg_upgrade (proves pg_upgrade preserves OLDEST-version objects) | |
| # Exercises ALTER EXTENSION UPDATE AFTER the binary pg_upgrade | |
| # above, against the NEW cluster (already started by the previous | |
| # step, and the only cluster listening on 5432 at this point, | |
| # since the old one was stopped in that same step), on | |
| # upgrade_oldest_first ONLY (upgrade_current_first | |
| # was already updated before the upgrade, above, and must not be | |
| # updated again here) - the 0.9.6 objects prepare-old planted into | |
| # that database are thus still at 0.9.6 at the moment pg_upgrade | |
| # itself runs, so this proves pg_upgrade correctly preserves | |
| # objects still frozen at the oldest supported version, not just | |
| # already-current ones. `make install` into the new cluster above | |
| # already installed the current version's update scripts/control | |
| # file into ITS sharedir, so they're in place for this ALTER | |
| # EXTENSION UPDATE to use. | |
| run: bin/test_existing update upgrade_oldest_first | |
| - name: Run the suite against the pg_upgraded database(s) (existing mode), twin databases | |
| # run-suite asserts the version, re-proves the dependency guard | |
| # still blocks a non-CASCADE drop (i.e. it survived both the update | |
| # and pg_upgrade), drops the guard, then runs the suite against the | |
| # REAL pg_upgraded database via --use-existing (so pg_regress does | |
| # not drop/recreate it) - a plain fresh `make test` would silently | |
| # test a fresh install instead of the migrated objects. Once per | |
| # database, since each holds an independent ordering's result. | |
| run: | | |
| bin/test_existing run-suite upgrade_oldest_first | |
| bin/test_existing run-suite upgrade_current_first | |
| - name: Structurally compare the pg_upgraded databases against a fresh install | |
| # Same rationale as the test job's own update leg's use of this tool | |
| # (see above), but here the "other side" is a REAL database a binary | |
| # pg_upgrade + ALTER EXTENSION UPDATE (in one order or the other) | |
| # just produced, not a scratch database this tool created itself - | |
| # passed as EXISTING_DB so the script queries it in place instead of | |
| # re-deriving it, discovering that database's OWN randomly generated | |
| # schema (each of the twin databases got an independent one from | |
| # bin/test_existing prepare-old) rather than generating a new one, so | |
| # both sides of each comparison still land in the same schema. Once | |
| # per database, since each holds an independent ordering's result. | |
| # Catches a divergence class the fixed pgTAP suite above doesn't: an | |
| # object left subtly different (body, comment, ACL) by surviving a | |
| # real catalog migration, as opposed to only an in-place update. | |
| run: | | |
| bin/compare_fresh_vs_update 0.9.6 upgrade_oldest_first | |
| bin/compare_fresh_vs_update 0.9.6 upgrade_current_first | |
| # Covers both a fresh install AND the 0.9.6 -> current update path, both | |
| # purely via pg_tle. pgxntool 2.3.0's fix for installcheck's ordering bug | |
| # (Postgres-Extensions/pgxntool#83) made `installcheck` (and so `make | |
| # test`) unconditionally depend on `install`, which writes a real | |
| # .control file to disk - defeating the entire point of proving a pg_tle | |
| # deployment never touches the filesystem. There's still no upstream fix | |
| # for that (Postgres-Extensions/pgxntool#90, open) that would let | |
| # bin/test_existing's real pgTAP suite run without it, so the update-path | |
| # steps below use TEST_EXISTING_DEPLOY=pgtle (see bin/test_existing), | |
| # which instead sandboxes `make test`'s install step behind a scratch | |
| # DESTDIR - harmless here since a pg_tle-deployed database never needs | |
| # those files. | |
| pg-tle-test: | |
| needs: [changes] | |
| # Skipped outright (not just matrix-reduced like `test` above) on a | |
| # draft PR: this is a heavy job, and a draft author doesn't need the | |
| # pg_tle deployment path re-proven on every push while still iterating. | |
| if: needs.changes.outputs.docs_only != 'true' && github.event.pull_request.draft != true | |
| # "ci-pgtle-" prefix, matrix.pg split - see the `test` job's concurrency | |
| # comment above. | |
| concurrency: | |
| group: >- | |
| ${{ needs.changes.outputs.priority == 'low' | |
| && format('ci-pgtle-lowprio-{0}', needs.changes.outputs.lane) | |
| || format('ci-pgtle-{0}-{1}', github.event.pull_request.number || github.sha, matrix.pg) }} | |
| cancel-in-progress: ${{ needs.changes.outputs.priority != 'low' }} | |
| strategy: | |
| matrix: | |
| # Intersection of count_nulls' own supported range (10-18, see the | |
| # `test` job above / #21) with pg_tle 1.5.2's supported range (12-18, | |
| # see pgxntool/pgtle_versions.md): drop 10 and 11 since pg_tle doesn't | |
| # support them. | |
| pg: [18, 17, 16, 15, 14, 13, 12] | |
| name: 🧩 pg_tle ${{ matrix.pg }} | |
| runs-on: ubuntu-latest | |
| container: pgxn/pgxn-tools | |
| env: | |
| # NOT named PGTLE_VERSION: on the pgxntool version this job was | |
| # written against, that collided with pgxntool's own Makefile variable | |
| # of the same name (`pgtle:`'s | |
| # `$(if $(PGTLE_VERSION),--pgtle-version $(PGTLE_VERSION))`), which Make | |
| # auto-imports from the environment - `make run-pgtle` would silently | |
| # generate into pg_tle/1.5.2/ (the literal version) instead of the | |
| # correct range directory pg_tle/1.5.0+/, then fail when --run looked | |
| # for the range directory and didn't find it. Filed as | |
| # Postgres-Extensions/pgxntool#78, fixed upstream in pgxntool 2.3.0 | |
| # (renamed to PGXNTOOL_PGTLE_VERSION - see this repo's pgxntool bump). | |
| # Keeping our own name distinct from EITHER of those regardless - | |
| # still the simplest way to guarantee no future collision. | |
| PG_TLE_BUILD_VERSION: "1.5.2" | |
| steps: | |
| # A dedicated cluster, never shared with the other jobs in this | |
| # workflow: pg_tle requires shared_preload_libraries and mixing | |
| # pg_tle/non-pg_tle extension installs on one cluster can misbehave. | |
| - name: Start PostgreSQL ${{ matrix.pg }} | |
| run: pg-start ${{ matrix.pg }} | |
| - name: Check out the repo | |
| uses: actions/checkout@v4 | |
| - name: Install rsync | |
| run: apt-get install -y rsync | |
| - name: Snapshot filesystem extension control files (pre-pgtap baseline) | |
| # Whatever ships on disk by default (e.g. contrib), before installing | |
| # pgTAP. Lets the next step prove pgTAP is the ONLY thing `make pgtap` | |
| # puts on disk, instead of trusting that and folding whatever it did | |
| # into the pre-pg_tle baseline unexamined -- a future pgxntool change | |
| # to `make pgtap` that also happened to touch count_nulls' own files | |
| # would otherwise be silently absorbed into that baseline and never | |
| # get flagged by any later check. | |
| run: bin/assert_fs_clean snapshot ${{ matrix.pg }} /tmp/pre_pgtap_baseline.txt | |
| - name: Install pgtap (test harness dependency) | |
| # pgTAP is a filesystem-installed dependency of the TEST HARNESS, not | |
| # part of what this job proves is pg_tle-only -- it's not being | |
| # deployed via pg_tle here, and never will be. | |
| run: make pgtap | |
| - name: Verify make pgtap installed exactly pgtap.control, nothing else | |
| run: bin/assert_fs_clean verify ${{ matrix.pg }} /tmp/pre_pgtap_baseline.txt pgtap.control | |
| - name: Snapshot filesystem extension control files (pre-pg_tle baseline) | |
| # Everything on disk now that pgtap is confirmed the only addition | |
| # (contrib, pgtap). bin/assert_fs_clean's later checks diff against | |
| # this, so they flag ANY extension that lands on disk instead of | |
| # being registered via pg_tle -- not just count_nulls -- without | |
| # hardcoding contrib/pgtap names. | |
| run: bin/assert_fs_clean snapshot ${{ matrix.pg }} /tmp/control_baseline.txt | |
| - name: Build and install pg_tle ${{ env.PG_TLE_BUILD_VERSION }} | |
| # flex/bison/libkrb5-dev aren't in the pgxn-tools image; pg_tle's build | |
| # needs them (guc-file.l, and clientauth.c includes gssapi.h). | |
| run: | | |
| apt-get install -y flex bison libkrb5-dev | |
| git clone --branch v${{ env.PG_TLE_BUILD_VERSION }} --depth 1 https://github.com/aws/pg_tle.git /tmp/pg_tle | |
| make -C /tmp/pg_tle install | |
| - name: Enable pg_tle and restart PostgreSQL ${{ matrix.pg }} | |
| run: | | |
| echo "shared_preload_libraries = 'pg_tle'" >> /etc/postgresql/${{ matrix.pg }}/test/postgresql.conf | |
| pg_ctlcluster ${{ matrix.pg }} test restart | |
| pg_isready -t 30 | |
| - name: Register pg_tle + count_nulls against template1 | |
| # template1, not the ambient default db: pg_tle's registration catalog | |
| # is per-database, and `createdb` only inherits it because it copies | |
| # template1 by default. Every count_nulls database used below (the | |
| # smoke-test db) is created AFTER this step specifically so it | |
| # inherits both registrations. | |
| run: | | |
| psql -d template1 -c "CREATE EXTENSION pg_tle" | |
| PGDATABASE=template1 make run-pgtle | |
| - name: Verify no stray extension control files landed on the filesystem | |
| # CRITICAL, and intentionally redundant with the count_nulls-specific | |
| # check in the next step: a filesystem control file silently wins | |
| # over a pg_tle-registered extension of the same name, which would | |
| # make this whole job a false pass without ever raising an error. Run | |
| # again after every step below that could plausibly write extension | |
| # files to disk -- never trust a single check to catch everything. | |
| run: bin/assert_fs_clean verify ${{ matrix.pg }} /tmp/control_baseline.txt pg_tle.control | |
| - name: Install count_nulls purely via pg_tle (fresh install, no filesystem trace) | |
| # count_nulls is never `make install`ed in this job, so a successful | |
| # CREATE EXTENSION here can only be resolving through pg_tle's | |
| # registration, not a control file on disk. Checked explicitly here | |
| # too (not just via the comprehensive check above) as a guard | |
| # specifically for the extension under test, in case that check's | |
| # exclude-list logic has a bug. | |
| run: | | |
| test ! -e /usr/share/postgresql/${{ matrix.pg }}/extension/count_nulls.control | |
| createdb count_nulls_smoke | |
| psql -d count_nulls_smoke -c "CREATE EXTENSION count_nulls" | |
| - name: Verify count_nulls works when deployed via pg_tle | |
| run: | | |
| INSTALLED=$(psql -d count_nulls_smoke -tAc "SELECT extversion FROM pg_extension WHERE extname = 'count_nulls'") | |
| # EXTENSION_count_nulls_VERSION (the .control file's default_version), | |
| # NOT PGXNVERSION (the PGXN distribution version, from META.in.json) | |
| # -- a version-less CREATE EXTENSION installs whatever the control | |
| # file's default_version says, and count_nulls' is currently the | |
| # 'stable' pseudo-version, not the last real release. See | |
| # RELEASE.md's note on distribution vs. extension versions. | |
| EXPECTED=$(make -s print-EXTENSION_count_nulls_VERSION 2>/dev/null | sed -n 's/.*set to "\(.*\)"$/\1/p') | |
| echo "installed=$INSTALLED expected=$EXPECTED" | |
| if [ -z "$INSTALLED" ] || [ -z "$EXPECTED" ] || [ "$INSTALLED" != "$EXPECTED" ]; then | |
| echo "FAIL: installed='$INSTALLED' expected='$EXPECTED'"; exit 1 | |
| fi | |
| RESULT=$(psql -d count_nulls_smoke -v ON_ERROR_STOP=1 -tAc "SELECT null_count(1, NULL, 2)") | |
| echo "null_count(1, NULL, 2)=$RESULT" | |
| if [ "$RESULT" != "1" ]; then | |
| echo "FAIL: expected null_count(1, NULL, 2) = 1, got '$RESULT'"; exit 1 | |
| fi | |
| - name: Verify no stray extension control files after the fresh-install smoke test | |
| run: bin/assert_fs_clean verify ${{ matrix.pg }} /tmp/control_baseline.txt pg_tle.control | |
| - name: Install count_nulls at 0.9.6, purely via pg_tle (update-path prep) | |
| # A SECOND, separate scratch database, created after the template1 | |
| # registration above so it inherits both registrations too (same | |
| # reasoning as count_nulls_smoke). prepare-old creates the database, | |
| # CREATE EXTENSIONs at 0.9.6 (pure SQL - resolves through pg_tle's | |
| # catalog, no `make install` call, which would defeat the whole | |
| # point), then plants + proves the dependency guard so a stray | |
| # CASCADE drop anywhere below can't silently turn the eventual | |
| # existing-mode run into a fresh install instead. | |
| run: | | |
| test ! -e /usr/share/postgresql/${{ matrix.pg }}/extension/count_nulls.control | |
| bin/test_existing prepare-old count_nulls_pgtle_update 0.9.6 | |
| - name: Verify no stray extension control files after installing 0.9.6 via pg_tle | |
| run: bin/assert_fs_clean verify ${{ matrix.pg }} /tmp/control_baseline.txt pg_tle.control | |
| - name: Update 0.9.6 -> current, purely via pg_tle | |
| # Pure SQL (ALTER EXTENSION ... UPDATE), no filesystem write either. | |
| run: bin/test_existing update count_nulls_pgtle_update | |
| - name: Verify no stray extension control files after the pg_tle update | |
| run: bin/assert_fs_clean verify ${{ matrix.pg }} /tmp/control_baseline.txt pg_tle.control | |
| - name: Run the real pgTAP suite against the pg_tle-updated database (existing mode) | |
| # run-suite re-proves the dependency guard, drops it, then runs the | |
| # FULL suite via --use-existing against the real pg_tle-deployed + | |
| # updated database - the same suite/expected-output as every other | |
| # leg (see test/README.md). TEST_EXISTING_DEPLOY=pgtle makes | |
| # run-suite sandbox `make test`'s otherwise-unavoidable `install` | |
| # step behind a scratch DESTDIR instead of writing to the real | |
| # extension directory (see bin/test_existing's TEST_EXISTING_DEPLOY | |
| # comment), and makes test/install/load.sql's existing-mode | |
| # assertion cross-check pgtle.available_extensions() instead of | |
| # pg_available_extensions (which never sees pg_tle registrations - | |
| # see the Makefile's TEST_EXISTING_DEPLOY comment). | |
| run: TEST_EXISTING_DEPLOY=pgtle bin/test_existing run-suite count_nulls_pgtle_update | |
| - name: Verify no stray extension control files after the pgTAP suite | |
| # THE step that actually proves the DESTDIR sandboxing worked: the | |
| # real extension directory must still be clean after `make test` | |
| # ran (with its otherwise-unavoidable `install` step) sandboxed | |
| # behind a scratch DESTDIR. | |
| run: bin/assert_fs_clean verify ${{ matrix.pg }} /tmp/control_baseline.txt pg_tle.control | |
| # A single stable check name for use as a required status check in branch | |
| # protection rules. Matrix jobs produce check names like | |
| # "🐘 PostgreSQL 14" (one per supported major) which would all need to be | |
| # listed individually and updated whenever the matrix changes. This job | |
| # passes if all others passed or were skipped (e.g. 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, lint, test, pg-upgrade-test, pg-tle-test] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Verify all jobs are listed in needs | |
| # Ensures this job won't silently ignore a newly-added job that was | |
| # omitted from the needs list above. | |
| run: | | |
| DEFINED=$(python3 -c " | |
| import yaml | |
| with open('.github/workflows/ci.yml') as f: | |
| w = yaml.safe_load(f) | |
| print('\n'.join(sorted(j for j in w['jobs'] if j != 'all-checks-passed'))) | |
| ") | |
| NEEDED=$(echo '${{ toJson(needs) }}' | python3 -c " | |
| import json, sys | |
| print('\n'.join(sorted(json.load(sys.stdin)))) | |
| ") | |
| if [ "$DEFINED" != "$NEEDED" ]; then | |
| echo "Some jobs are missing from all-checks-passed needs:" | |
| diff <(echo "$DEFINED") <(echo "$NEEDED") | |
| exit 1 | |
| fi | |
| - name: Check all jobs passed or were skipped | |
| run: | | |
| if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then | |
| echo "One or more jobs failed or were cancelled" | |
| exit 1 | |
| fi | |
| # vi: expandtab ts=2 sw=2 |