From 791f68df2c6a170d81c7f98cb9a4e29dc621cf24 Mon Sep 17 00:00:00 2001 From: Mitja Kleider <48825+emka@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:35:04 +0200 Subject: [PATCH] ci: run only quick for changes no other gate reads Every pull request started all gates, including the ones that build the firmware, for edits to documents or to another workflow, which cannot change what they check. Minutes are billed and limited. The branch rules still get their all gates check, because the matrix shrinks and the check still runs. --- .github/workflows/ci.yml | 24 ++++++++++++++++++- justfile | 1 + scripts/select-gates-test.sh | 46 ++++++++++++++++++++++++++++++++++++ scripts/select-gates.sh | 32 +++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100755 scripts/select-gates-test.sh create mode 100755 scripts/select-gates.sh 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