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
24 changes: 23 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
46 changes: 46 additions & 0 deletions scripts/select-gates-test.sh
Original file line number Diff line number Diff line change
@@ -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 <expected> <description> <changed files...>
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"
32 changes: 32 additions & 0 deletions scripts/select-gates.sh
Original file line number Diff line number Diff line change
@@ -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
Loading