diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc76c18..8bb8740 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,15 +28,37 @@ jobs: # be added to one without the other. Plain text parsing rather than `just # --dump`: this job would otherwise spend three minutes entering the Nix # shell to produce a list. + # + # A change that touches nothing but documents and the other workflows needs + # only the `quick` gate, which includes the workflow lint; select-gates.sh + # decides, and falls back to every gate whenever the change cannot be + # compared with its base. The branch rules name `all gates`, which still + # runs, so a skipped gate never leaves a required check pending. gates: runs-on: ubuntu-latest outputs: gates: ${{ steps.read.outputs.gates }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 - id: read + env: + EVENT: ${{ github.event_name }} + PR_BASE: ${{ github.event.pull_request.base.sha }} + PUSH_BEFORE: ${{ github.event.before }} run: | - gates="$(sed -n 's/^check: *//p' justfile | jq -Rce 'split(" ") | map(select(length > 0)) | select(length > 0)')" + all="$(sed -n 's/^check: *//p' justfile | jq -Rce 'split(" ") | map(select(length > 0)) | select(length > 0)')" + case "$EVENT" in + pull_request) base="$PR_BASE" ;; + push) base="$PUSH_BEFORE" ;; + *) base="" ;; + esac + changed="" + if [ -n "$base" ]; then + changed="$(git diff --name-only "$base" HEAD)" || changed="" + fi + gates="$(printf '%s\n' "$changed" | scripts/select-gates.sh "$all")" echo "gates=$gates" >> "$GITHUB_OUTPUT" # One job per gate, so a red build names the gate that failed and a slow gate diff --git a/justfile b/justfile index e1821cb..4e9c5a2 100644 --- a/justfile +++ b/justfile @@ -96,6 +96,7 @@ scripts: python3 scripts/sleep-check.py --self-test python3 scripts/console.py --self-test python3 scripts/bench-console.py --self-test + bash scripts/select-gates-test.sh # Proves the library crates are genuinely no_std, against the target the # firmware actually runs on. rustc ships no prebuilt core for xtensa. diff --git a/scripts/select-gates-test.sh b/scripts/select-gates-test.sh new file mode 100755 index 0000000..1a95103 --- /dev/null +++ b/scripts/select-gates-test.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Behaviour of select-gates.sh: which CI gates a set of changed files needs. +set -u +cd "$(dirname "$0")" || exit 1 + +FULL='["quick","lint","host-tests","device-checks","images"]' +QUICK='["quick"]' +failed=0 + +# selected +selected() { + local expected=$1 description=$2 actual + shift 2 + actual=$(printf '%s\n' "$@" | ./select-gates.sh "$FULL") + if [ "$actual" != "$expected" ]; then + echo "FAIL: $description: expected $expected, got $actual" + failed=1 + fi +} + +# Given / When / Then in each call: the files are the given, the call is the +# when, the expected list is the then. +selected "$QUICK" "a root markdown file needs only quick" README.md +selected "$QUICK" "documents need only quick" docs/public-checklist.md +selected "$QUICK" "another workflow needs only quick" .github/workflows/release-please.yml +selected "$QUICK" "the dependabot config needs only quick" .github/dependabot.yml +selected "$QUICK" "a licence file needs only quick" LICENSE-MIT +selected "$QUICK" "several unrelated files need only quick" README.md CHANGELOG.md docs/a.md +selected "$FULL" "the pipeline itself needs every gate" .github/workflows/ci.yml +selected "$FULL" "source needs every gate" crates/teddiebox-core/src/lib.rs +selected "$FULL" "a markdown file inside a crate needs every gate" crates/teddiebox-core/README.md +selected "$FULL" "the justfile needs every gate" justfile +selected "$FULL" "a lockfile needs every gate" firmware/Cargo.lock +selected "$FULL" "one related file among unrelated ones needs every gate" README.md crates/trf7962a/src/lib.rs +selected "$FULL" "no changed files needs every gate" + +# Given a gate list without quick, When only unrelated files changed, +# Then nothing can be skipped safely. +actual=$(printf 'README.md\n' | ./select-gates.sh '["lint","images"]') +if [ "$actual" != '["lint","images"]' ]; then + echo "FAIL: a list without quick runs in full: got $actual" + failed=1 +fi + +[ "$failed" = 0 ] && echo "select-gates: all passed" +exit "$failed" diff --git a/scripts/select-gates.sh b/scripts/select-gates.sh new file mode 100755 index 0000000..a6d940d --- /dev/null +++ b/scripts/select-gates.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Reads the changed file names on stdin and the JSON list of every CI gate as +# the first argument, and prints the JSON list of gates the change needs. +# +# A change confined to files no compiler, test or linter but `quick` reads +# needs only `quick`. Anything else, an empty change, or a list without +# `quick` needs every gate: a wrong skip hides a break, a wrong run costs +# minutes. +set -eu + +all=$1 +needs_all=false +seen=false + +while IFS= read -r file; do + [ -n "$file" ] || continue + seen=true + case "$file" in + .github/workflows/ci.yml) needs_all=true ;; + .github/workflows/* | .github/dependabot.yml | docs/*) ;; + */*) needs_all=true ;; + *.md | LICENSE-*) ;; + *) needs_all=true ;; + esac +done + +quick=$(jq -c 'map(select(. == "quick"))' <<<"$all") +if [ "$seen" = false ] || [ "$needs_all" = true ] || [ "$quick" = '[]' ]; then + jq -c . <<<"$all" +else + echo "$quick" +fi