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
28 changes: 28 additions & 0 deletions .github/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,31 @@ check:
@if command -v actionlint >/dev/null 2>&1; then actionlint; fi
{{ source_directory() }}/scripts/alert.sh check-coverage
@if command -v rustc >/dev/null 2>&1; then {{ source_directory() }}/scripts/package-binary.test.sh; fi
just gh gates-test
@if command -v cargo >/dev/null 2>&1; then just gh select-test; fi

# Which end-to-end lanes a diff needs, as `<lane>=true|false` lines. Takes the
# newline-separated changed-file list `just _changed` prints, and defaults to
# this branch's own diff so the answer is reproducible outside CI:
#
# just gh select
# just gh select "$(just _changed '')"
#
# gates.yml appends the output to $GITHUB_OUTPUT and drives one job per lane.
select $FILES="":
#!/usr/bin/env bash
set -euo pipefail
if [[ -z "$FILES" ]]; then
FILES=$(just _changed "")
fi
printf '%s' "$FILES" | {{ source_directory() }}/scripts/select.sh

# Check the impact map against the diff shapes it exists to catch.
[private]
select-test:
{{ source_directory() }}/scripts/select.test.sh

# Check that the aggregate verdict tells an irrelevant lane from a missing one.
[private]
gates-test:
{{ source_directory() }}/scripts/gates.test.sh
11 changes: 7 additions & 4 deletions .github/scripts/alert.sh
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,12 @@ non_pr_workflow_names() {

printf "%s\n" "${files[@]}" | bun -e '
const files = (await Bun.stdin.text()).split("\n").filter(Boolean);
// Both report their failure as a check on the PR itself, so alert.yml skips
// them at runtime and a workflow triggered only by these needs no entry.
const PR_EVENTS = new Set(["pull_request", "pull_request_target"]);
// Triggers that report somewhere else, so a workflow with only these needs no
// entry. The pull request events report as a check on the PR itself, which is
// what alert.yml skips at runtime. workflow_call reports as part of the caller:
// a reusable workflow raises no workflow_run event of its own, so an entry for
// one would sit in alert.yml never firing.
const DELEGATED_EVENTS = new Set(["pull_request", "pull_request_target", "workflow_call"]);
const names = [];
for (const file of files) {
const doc = Bun.YAML.parse(await Bun.file(file).text());
Expand All @@ -131,7 +134,7 @@ for (const file of files) {
console.error("alert.sh: cannot read the on: value of " + file);
process.exit(2);
}
if (!triggers.some((t) => !PR_EVENTS.has(t))) continue;
if (!triggers.some((t) => !DELEGATED_EVENTS.has(t))) continue;

const name = doc.name;
if (typeof name !== "string" || name.trim() === "") {
Expand Down
84 changes: 84 additions & 0 deletions .github/scripts/gates.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
#
# The aggregate verdict for gates.yml: one stable result a branch ruleset can
# require, whatever the diff selected.
#
# A required check has to report on every pull request, including the docs-only
# ones no lane covers. A path-filtered workflow cannot: it never starts, so its
# context never appears and the merge waits forever. So every lane is a
# conditional job inside one workflow that always starts, and this decides.
#
# The point of the script rather than a `needs` list is that `skipped` is
# ambiguous. GitHub reports the same word whether a lane was irrelevant to the
# diff or was never given the chance to run, and only the selector knows which.
# So each lane is checked against what the selector asked for:
#
# selected, success pass
# selected, anything else fail: the lane the diff needed did not pass
# not selected, skipped pass: irrelevant to this diff
# not selected, anything else fail: the workflow and the impact map disagree
#
# The last two rules also make the wiring self-checking: a lane the impact map
# emits with no job behind it, or a job with no lane in front of it, fails here
# rather than passing silently for however long nobody notices.
#
# Reads `toJSON(needs)` from GATES_NEEDS: a map of job id to `{result, outputs}`,
# where the `select` job's outputs are the impact map.

set -euo pipefail

: "${GATES_NEEDS:?GATES_NEEDS must hold toJSON(needs)}"

needs="$GATES_NEEDS"

# Without the selector there is nothing to compare a lane against, and treating
# an absent map as "nothing was selected" would pass every lane by skipping it.
selector="$(jq -r '.select.result // "missing"' <<<"$needs")"
if [[ "$selector" != success ]]; then
echo "gates: the selector did not succeed ($selector); no lane can be verified" >&2
exit 1
fi

report="$(jq -r '
. as $needs
| ($needs.select.outputs // {}) as $map
| ($map | keys) as $lanes
| (($needs | keys) - ["select"]) as $jobs
| (
($jobs[] | {
name: .,
selected: ($map[.] // "no-lane"),
result: ($needs[.].result // "missing")
}),
(($lanes - $jobs)[] | { name: ., selected: $map[.], result: "no-job" })
)
| "\(.name) \(.selected) \(.result)"
' <<<"$needs" | sort)"

if [[ -z "$report" ]]; then
echo "gates: no lanes and no jobs; the impact map and the workflow are both empty" >&2
exit 1
fi

status=0
while read -r lane selected result; do
case "$selected/$result" in
true/success | false/skipped)
printf ' ok %-12s selected=%s result=%s\n' "$lane" "$selected" "$result"
;;
true/*)
printf ' FAILED %-12s selected=%s result=%s\n' "$lane" "$selected" "$result"
status=1
;;
*)
printf ' MISWIRED %-12s selected=%s result=%s\n' "$lane" "$selected" "$result"
status=1
;;
esac
done <<<"$report"

if ((status)); then
echo "gates: a selected lane did not pass, or the impact map and the workflow disagree" >&2
fi

exit "$status"
77 changes: 77 additions & 0 deletions .github/scripts/gates.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
#
# Fixtures for the aggregate verdict. The cases that matter are the ones that
# look green: a lane the diff selected that never ran reports `skipped`, exactly
# like a lane the diff did not need.

set -euo pipefail

scripts="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

fail() {
echo "gates: $1" >&2
exit 1
}

# A `toJSON(needs)` payload: the selector's map plus one result per job.
needs() {
local map=$1 results=$2
printf '{"select":{"result":"success","outputs":%s},%s}' "$map" "$results"
}

passes() {
GATES_NEEDS="$1" "$scripts/gates.sh" >/dev/null 2>&1 || fail "$2"
}

fails() {
! GATES_NEEDS="$1" "$scripts/gates.sh" >/dev/null 2>&1 || fail "$2"
}

map='{"smoke":"true","wasm":"false"}'

passes "$(needs "$map" '"smoke":{"result":"success"},"wasm":{"result":"skipped"}')" \
"a selected lane that passed and an irrelevant one must aggregate green"

# The whole reason this is a script. Both lanes report `skipped`; only the
# selector knows that one of them was needed.
fails "$(needs "$map" '"smoke":{"result":"skipped"},"wasm":{"result":"skipped"}')" \
"a selected lane that never ran must not pass as irrelevant"

fails "$(needs "$map" '"smoke":{"result":"failure"},"wasm":{"result":"skipped"}')" \
"a failed lane must fail"

# A timed-out job reports `failure`; a superseded one reports `cancelled`. Both
# mean the lane did not prove anything.
fails "$(needs "$map" '"smoke":{"result":"cancelled"},"wasm":{"result":"skipped"}')" \
"a cancelled lane must fail"

# The wiring checks. A lane with no job behind it never runs and never reports,
# and a job with no lane in front of it is never selected and never runs.
fails "$(needs '{"smoke":"true","ts":"true"}' '"smoke":{"result":"success"}')" \
"a lane with no job must fail"
fails "$(needs '{"smoke":"true"}' '"smoke":{"result":"success"},"ts":{"result":"success"}')" \
"a job with no lane must fail"

# An unselected lane that ran anyway means the job's `if` and the impact map
# disagree, which is the same bug seen from the other side.
fails "$(needs "$map" '"smoke":{"result":"success"},"wasm":{"result":"success"}')" \
"an unselected lane that ran must fail"

# Docs-only: nothing selected, nothing ran, and the required check still reports.
passes "$(needs '{"smoke":"false","wasm":"false"}' '"smoke":{"result":"skipped"},"wasm":{"result":"skipped"}')" \
"a docs-only pull request must aggregate green"

# Without the selector every lane would be compared against an empty map and
# pass by being skipped.
fails '{"select":{"result":"failure","outputs":{}},"smoke":{"result":"skipped"}}' \
"a failed selector must fail the aggregate"

fails '{"select":{"result":"success","outputs":{}}}' \
"an empty impact map must fail rather than pass vacuously"

# A merged pull request's closed event has the base branch ref, so the pull
# request number is the stable identity that cancels its still-running jobs.
grep -qF "group: gates-\${{ github.event.pull_request.number }}" "$scripts/../workflows/gates.yml" ||
fail "the concurrency group must stay stable across pull request events"

echo "gates: aggregate ok"
Loading
Loading