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
5 changes: 5 additions & 0 deletions .changeset/watch-for-a-stale-bundled-driver-pin.md
Original file line number Diff line number Diff line change
@@ -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.
96 changes: 96 additions & 0 deletions .github/workflows/bundled-driver-pin.yml
Original file line number Diff line number Diff line change
@@ -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<<REPORT_EOF"
echo "$output"
echo "REPORT_EOF"
} >> "$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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
93 changes: 78 additions & 15 deletions scripts/sync-bundled-drivers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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=()
Expand Down Expand Up @@ -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
Expand All @@ -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 <<MSG

The bundled driver pin is out of date.

pinned: ${PINNED}
upstream: ${COMMIT} (${REPOSITORY}@${UPSTREAM_BRANCH})

The drivers listed above have changed upstream. A gateway that boots offline
runs the bundled copy, so until the pin moves it keeps running the old one.

1. set .commit in drivers/BUNDLED_SOURCE.json to ${COMMIT}
2. run scripts/sync-bundled-drivers.sh
3. commit the result

Read what changed before taking it -- moving the pin takes every driver at
that commit, not only the ones you came for.
MSG
exit 1
fi
if [ "$PINNED" = "$COMMIT" ]; then
echo "pin is current: ${REPOSITORY}@${COMMIT:0:12}"
else
echo "pin is ${PINNED:0:12}, ${UPSTREAM_BRANCH} is ${COMMIT:0:12}, and no bundled driver differs"
fi
elif [ "$CHECK" = "1" ]; then
if [ "$drift" = "1" ]; then
cat >&2 <<'MSG'

Expand Down