diff --git a/.changeset/watch-for-a-stale-bundled-driver-pin.md b/.changeset/watch-for-a-stale-bundled-driver-pin.md new file mode 100644 index 00000000..3cb93a7e --- /dev/null +++ b/.changeset/watch-for-a-stale-bundled-driver-pin.md @@ -0,0 +1,5 @@ +--- +"ftw": patch +--- + +Watch for a stale bundled-driver pin. A daily job compares `drivers/` against device-drivers `main` and opens one tracking issue when a bundled driver has changed upstream. diff --git a/.github/workflows/bundled-driver-pin.yml b/.github/workflows/bundled-driver-pin.yml new file mode 100644 index 00000000..8fe5d279 --- /dev/null +++ b/.github/workflows/bundled-driver-pin.yml @@ -0,0 +1,96 @@ +name: bundled driver pin + +# drivers/ is a snapshot of srcfl/device-drivers, and two things can go wrong +# with it. Someone edits a driver here, which the `drivers` job in test.yml +# already catches on every pull request. Or the pin is left behind while a fix +# lands upstream, which nothing caught -- a Pixii poll-counter flap reached +# customer hardware a second time that way, fixed upstream and still bundled. +# +# This is the second one. It opens a single issue and keeps editing it, so a +# pin left alone for a month costs one thread rather than thirty. + +on: + schedule: + # 06:00 UTC daily. The check is quiet unless a bundled driver actually + # changed upstream, so a daily run is not a daily notification. + - cron: "0 6 * * *" + workflow_dispatch: + +permissions: + contents: read + issues: write + +concurrency: + group: bundled-driver-pin + cancel-in-progress: false + +jobs: + behind: + name: pin follows device-drivers + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Compare the bundled drivers against device-drivers main + id: compare + env: + # Lifts the unauthenticated GitHub API rate limit. Read-only, and the + # tarball it fetches is public either way. + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set +e + output="$(bash scripts/sync-bundled-drivers.sh --behind 2>&1)" + status=$? + set -e + echo "$output" + { + echo "status=${status}" + echo "report<> "$GITHUB_OUTPUT" + # A network or API failure must not read as "the pin is fine", and + # must not open an issue saying drivers changed when we never found + # out. 1 means outdated, 0 means current, anything else is broken. + if [ "$status" != "0" ] && [ "$status" != "1" ]; then + echo "::error::the pin check could not run (exit ${status})" + exit "$status" + fi + + - name: Open or update the tracking issue + if: steps.compare.outputs.status == '1' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPORT: ${{ steps.compare.outputs.report }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + title="Bundled driver pin is behind device-drivers" + body="$(printf '%s\n\n```\n%s\n```\n\nFrom %s\n\nThis issue is rewritten by each run of the `bundled driver pin` workflow. Closing it without moving the pin means it comes back tomorrow.\n' \ + "A driver bundled in \`drivers/\` has changed in srcfl/device-drivers. A gateway that boots offline runs the bundled copy, so it keeps running the old driver until the pin moves." \ + "${REPORT}" "${RUN_URL}")" + + existing="$(gh issue list --state open --search "in:title \"${title}\"" \ + --json number,title --jq "[.[] | select(.title == \"${title}\")][0].number")" + + if [ -n "$existing" ] && [ "$existing" != "null" ]; then + gh issue edit "$existing" --body "$body" + echo "updated issue #${existing}" + else + gh issue create --title "$title" --body "$body" + fi + + - name: Close the tracking issue once the pin is current + if: steps.compare.outputs.status == '0' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + title="Bundled driver pin is behind device-drivers" + existing="$(gh issue list --state open --search "in:title \"${title}\"" \ + --json number,title --jq "[.[] | select(.title == \"${title}\")][0].number")" + if [ -n "$existing" ] && [ "$existing" != "null" ]; then + gh issue close "$existing" --comment "The pin now matches every bundled driver in device-drivers. Closed by the \`bundled driver pin\` workflow." + echo "closed issue #${existing}" + else + echo "pin is current and no issue is open" + fi diff --git a/README.md b/README.md index 7ecd35e4..c7fcddf8 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,12 @@ file changed without its `DRIVER` version moving — the signed channel refuses publish changed bytes under a version it already published, and a gateway offered the same version twice cannot tell them apart. +The pin needs watching in both directions. Every pull request runs +`--check`, which catches a driver edited here. A daily job runs `--behind`, +which catches the opposite: the pin left in place while a fix lands upstream. +It opens one issue and keeps it up to date, and stays quiet unless a bundled +driver actually changed. + ## Install on Linux The installer supports Raspberry Pi OS, Debian and Ubuntu: diff --git a/scripts/sync-bundled-drivers.sh b/scripts/sync-bundled-drivers.sh index 492acf76..9cadf4c7 100755 --- a/scripts/sync-bundled-drivers.sh +++ b/scripts/sync-bundled-drivers.sh @@ -11,16 +11,32 @@ # Usage: # scripts/sync-bundled-drivers.sh # write the snapshot # scripts/sync-bundled-drivers.sh --check # fail if drivers/ has drifted +# scripts/sync-bundled-drivers.sh --behind # fail if the pin is out of date # # To take a newer driver: move the commit in BUNDLED_SOURCE.json, run this, and # commit the result. To add or drop a bundled driver, edit the list there -- # which drivers are bundled is FTW's decision, and device-drivers publishes # more than FTW needs to carry for recovery. +# +# --check and --behind ask opposite questions. --check catches someone editing +# a driver here, and runs on every pull request. --behind catches the pin being +# left behind while a fix lands upstream, and runs on a schedule -- nothing +# noticed that until a Pixii flap reached customer hardware a second time. +# +# --behind compares against the drivers upstream, not against the commit id, so +# a doc change or a dependency bump in device-drivers stays quiet. It only +# speaks up when a bundled driver would actually change. set -euo pipefail CHECK=0 -[ "${1:-}" = "--check" ] && CHECK=1 +BEHIND=0 +case "${1:-}" in + --check) CHECK=1 ;; + --behind) BEHIND=1; CHECK=1 ;; + "") ;; + *) echo "unknown option '${1}'; see the header for usage" >&2; exit 2 ;; +esac ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PIN="${ROOT}/drivers/BUNDLED_SOURCE.json" @@ -32,6 +48,20 @@ command -v jq >/dev/null || { echo "jq is required" >&2; exit 2; } REPOSITORY="$(jq -r .repository "$PIN")" COMMIT="$(jq -r .commit "$PIN")" SOURCE_DIR="$(jq -r .source_dir "$PIN")" +PINNED="$COMMIT" + +# The branch to measure against. device-drivers releases from main. +UPSTREAM_BRANCH="${UPSTREAM_BRANCH:-main}" + +if [ "$BEHIND" = "1" ]; then + COMMIT="$(curl -fsSL \ + -H "Accept: application/vnd.github+json" \ + ${GITHUB_TOKEN:+-H "Authorization: Bearer ${GITHUB_TOKEN}"} \ + "https://api.github.com/repos/${REPOSITORY}/commits/${UPSTREAM_BRANCH}" \ + | jq -r .sha)" + [ -n "$COMMIT" ] && [ "$COMMIT" != "null" ] \ + || { echo "could not resolve ${REPOSITORY}@${UPSTREAM_BRANCH}" >&2; exit 2; } +fi # Read into an array the long way: the bash that ships with macOS has no # mapfile, and this script has to run the same locally and in CI. DRIVERS=() @@ -68,7 +98,11 @@ for id in "${DRIVERS[@]}"; do fi if [ "$CHECK" = "1" ]; then if ! cmp -s "$from" "$to"; then - echo "DRIFTED ${id}.lua" + if [ "$BEHIND" = "1" ]; then + echo "OUTDATED ${id}.lua" + else + echo "DRIFTED ${id}.lua" + fi drift=1 fi else @@ -77,26 +111,55 @@ for id in "${DRIVERS[@]}"; do done # A .lua in drivers/ that the pin does not list is a file nothing regenerates, -# which is how the second source of truth grew in the first place. -for path in "${DEST}"/*.lua; do - [ -e "$path" ] || continue - id="$(basename "$path" .lua)" - listed=0 - for known in "${DRIVERS[@]}"; do - [ "$id" = "$known" ] && { listed=1; break; } +# which is how the second source of truth grew in the first place. Skipped in +# --behind mode, which asks about the pin rather than about local files. +if [ "$BEHIND" = "0" ]; then + for path in "${DEST}"/*.lua; do + [ -e "$path" ] || continue + id="$(basename "$path" .lua)" + listed=0 + for known in "${DRIVERS[@]}"; do + [ "$id" = "$known" ] && { listed=1; break; } + done + if [ "$listed" = "0" ]; then + echo "UNLISTED ${id}.lua is in drivers/ but not in BUNDLED_SOURCE.json" >&2 + drift=1 + fi done - if [ "$listed" = "0" ]; then - echo "UNLISTED ${id}.lua is in drivers/ but not in BUNDLED_SOURCE.json" >&2 - drift=1 - fi -done +fi if [ "$missing" = "1" ]; then echo "the pin names drivers that do not exist upstream" >&2 exit 1 fi -if [ "$CHECK" = "1" ]; then +if [ "$BEHIND" = "1" ]; then + if [ "$drift" = "1" ]; then + cat >&2 <&2 <<'MSG'