Skip to content

Commit 6e14aa0

Browse files
committed
Add reusable DKMS version-guard workflow
Packaged content changing without PACKAGE_VERSION moving leaves two installs claiming one version while differing in /usr/src, which breaks the only version users can quote from dkms status. Bump was a human habit until now and got missed once already. Guard diffs the packaged paths setup.sh copies against the PR base and demands a bump. Repo-only files stay free. A pre-release version suspends the demand for a whole series, so early or multi-PR work is not forced to invent a bump per PR. Selftest replays the step over scratch repo fixtures, extracting the script from the workflow so the cases cannot drift from what CI runs.
1 parent 7c2ca39 commit 6e14aa0

3 files changed

Lines changed: 250 additions & 0 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
name: Version guard
2+
3+
# Fails a PR that changes packaged content (what setup.sh copies into
4+
# /usr/src/<pkg>-<version>) without moving PACKAGE_VERSION in dkms.conf.
5+
# A pre-release version suspends the bump for its series.
6+
#
7+
# Pull request only. Push has no base to diff against.
8+
9+
on:
10+
workflow_call:
11+
12+
permissions:
13+
contents: read
14+
15+
jobs:
16+
packaged-content:
17+
name: PACKAGE_VERSION
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Checkout driver source
21+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
22+
with:
23+
fetch-depth: 0
24+
persist-credentials: false
25+
26+
- name: Check PACKAGE_VERSION against packaged changes
27+
env:
28+
BASE_REF: ${{ github.base_ref }}
29+
run: |
30+
set -eu
31+
32+
# Fork point, resolved live. A base frozen at PR open can inherit a
33+
# bump main made since and credit it to this PR.
34+
BASE=$(git merge-base "origin/$BASE_REF" HEAD)
35+
36+
# Superset of both platforms' setup.sh cp lists. RPi skips *.h and
37+
# scripts/, absent paths simply never match.
38+
changed=$(git diff --name-only "$BASE" HEAD -- \
39+
dkms.conf dkms.postinst Makefile '*.c' '*.h' '*.dts' scripts)
40+
41+
if [ -z "$changed" ]; then
42+
echo "No packaged content changed, PACKAGE_VERSION may stay put."
43+
exit 0
44+
fi
45+
46+
echo "Packaged content changed:"
47+
echo "$changed" | sed 's/^/ /'
48+
49+
version_of() { grep '^PACKAGE_VERSION=' | cut -d'"' -f2 | tr -d '\r'; }
50+
head_version=$(version_of < dkms.conf)
51+
# Empty when the PR is the one introducing dkms.conf, nothing to bump from.
52+
base_version=$(git show "$BASE:dkms.conf" 2> /dev/null | version_of)
53+
echo "PACKAGE_VERSION: $base_version -> $head_version"
54+
55+
# Warn, do not fail, on pre-release spelling the release pipeline
56+
# would later reject. Grammar is enforced for real at release time.
57+
case "$head_version" in
58+
*-*)
59+
case "${head_version#*-}" in
60+
alpha.[0-9]* | beta.[0-9]* | rc.[0-9]*) ;;
61+
*) echo "::warning::pre-release $head_version is not (alpha|beta|rc).N, release.sh will reject it" ;;
62+
esac
63+
;;
64+
esac
65+
66+
if [ "$head_version" = "$base_version" ]; then
67+
case "$head_version" in
68+
*-*)
69+
echo "::notice::$head_version is a pre-release, bump suspended for the series"
70+
exit 0
71+
;;
72+
esac
73+
echo "::error::packaged content changed but PACKAGE_VERSION is still $head_version"
74+
echo "Bump patch for a fix, minor for new capability (mode, control, link rate)."
75+
echo "Mid-surgery across several PRs, set a pre-release such as 0.2.0-alpha.1 instead."
76+
exit 1
77+
fi
78+
79+
# Compare release cores only. sort -V has no semver pre-release rule,
80+
# and equal cores are legitimate churn (alpha.1 -> alpha.2, or a
81+
# promotion to plain), so there is nothing to order.
82+
head_core=${head_version%%-*}
83+
base_core=${base_version%%-*}
84+
if [ "$head_core" != "$base_core" ] &&
85+
[ "$(printf '%s\n%s\n' "$base_core" "$head_core" | sort -V | tail -1)" != "$head_core" ]; then
86+
echo "::error::PACKAGE_VERSION went backwards, $base_version to $head_version"
87+
exit 1
88+
fi
89+
90+
echo "PACKAGE_VERSION moved with the packaged content."

.github/workflows/selftest.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ name: Selftest
44
# selftest/* branches and asserts the artifact sets: a plain version on
55
# selftest/upstream + selftest/recipe, a semver pre-release on the
66
# selftest/*-pre pair. Sign and publish have no selftest.
7+
#
8+
# Also replays dkms-version-guard.yml over scratch repo fixtures, see
9+
# tests/version-guard-cases.sh.
710

811
on:
912
push:
@@ -35,6 +38,17 @@ jobs:
3538
recipe-ref: selftest/recipe-pre
3639
upstream-ref: selftest/upstream-pre
3740

41+
version-guard:
42+
name: Version guard cases
43+
runs-on: ubuntu-latest
44+
steps:
45+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
46+
with:
47+
persist-credentials: false
48+
49+
- name: Replay guard cases
50+
run: bash tests/version-guard-cases.sh
51+
3852
assert:
3953
needs: build
4054
name: Assert artifact set

tests/version-guard-cases.sh

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#!/usr/bin/env bash
2+
# Selftest for dkms-version-guard.yml. Builds a scratch driver repo, replays
3+
# the guard step over base/head pairs and asserts pass or fail per case.
4+
set -uo pipefail
5+
6+
REPO_ROOT=$(git -C "$(dirname "$0")" rev-parse --show-toplevel)
7+
WORKFLOW=$REPO_ROOT/.github/workflows/dkms-version-guard.yml
8+
9+
WORK=$(mktemp -d)
10+
trap 'rm -rf "$WORK"' EXIT
11+
12+
GUARD=$WORK/guard-step.sh
13+
14+
extract() {
15+
"$1" - "$WORKFLOW" "$GUARD" <<'PY'
16+
import sys, yaml
17+
workflow, out = sys.argv[1], sys.argv[2]
18+
steps = yaml.safe_load(open(workflow))["jobs"]["packaged-content"]["steps"]
19+
scripts = [s["run"] for s in steps if "run" in s]
20+
assert len(scripts) == 1, f"expected one run step, found {len(scripts)}"
21+
open(out, "w", newline="\n").write(scripts[0])
22+
PY
23+
}
24+
25+
for py in python3 python; do
26+
command -v "$py" > /dev/null 2>&1 || continue
27+
extract "$py" && break
28+
done
29+
30+
# Without this every case would fail closed and the err cases would pass for
31+
# the wrong reason, reporting a broken harness as a working guard.
32+
[ -s "$GUARD" ] || {
33+
echo "::error::could not extract the guard step from $WORKFLOW" >&2
34+
exit 1
35+
}
36+
37+
dkms_conf() {
38+
printf 'PACKAGE_NAME="imx585-jetson-dkms"\nPACKAGE_VERSION="%s"\nBUILT_MODULE_NAME[0]="nv_imx585"\n' "$1"
39+
}
40+
41+
cd "$WORK"
42+
git init -q repo
43+
cd repo
44+
git config user.email selftest@kurokesu.com
45+
git config user.name selftest
46+
# Fixtures are byte-exact on purpose, one case turns dkms.conf CRLF.
47+
git config core.autocrlf false
48+
mkdir -p scripts
49+
dkms_conf 0.1.0 > dkms.conf
50+
echo 'int probe(void) { return 0; }' > nv_imx585.c
51+
echo 'sensor {};' > imx585.dts
52+
echo 'obj-m += nv_imx585.o' > Makefile
53+
echo 'depmod -a' > dkms.postinst
54+
echo 'exit 0' > scripts/conftest.sh
55+
echo '# imx585' > README.md
56+
echo 'set -e' > setup.sh
57+
git add -A
58+
git commit -qm 'Fixture base'
59+
BASE=$(git rev-parse HEAD)
60+
# Guard resolves its base as merge-base of origin/<base ref> and HEAD.
61+
git update-ref refs/remotes/origin/main "$BASE"
62+
63+
pass=0
64+
fail=0
65+
66+
# case_run <name> <ok|err> <shell edits applied on top of base>
67+
case_run() {
68+
local name=$1 expect=$2 edits=$3
69+
git checkout -q -B pr "$BASE"
70+
bash -c "$edits"
71+
git add -A
72+
git commit -qm "$name" --allow-empty
73+
local out rc got=ok
74+
out=$(BASE_REF=main bash "$GUARD" 2>&1) || rc=$?
75+
[ "${rc:-0}" -eq 0 ] || got=err
76+
if [ "$got" = "$expect" ]; then
77+
pass=$((pass + 1))
78+
printf ' PASS %-42s (%s)\n' "$name" "$got"
79+
else
80+
fail=$((fail + 1))
81+
printf ' FAIL %-42s expected %s got %s\n%s\n' "$name" "$expect" "$got" "$out"
82+
fi
83+
}
84+
85+
echo '== packaged versus repo-only =='
86+
case_run 'readme only, no bump' ok 'echo more >> README.md'
87+
case_run 'setup.sh only, no bump' ok 'echo more >> setup.sh'
88+
case_run 'source changed, no bump' err 'echo "// fix" >> nv_imx585.c'
89+
case_run 'dts changed, no bump' err 'echo "// x" >> imx585.dts'
90+
case_run 'makefile changed, no bump' err 'echo "# x" >> Makefile'
91+
case_run 'postinst changed, no bump' err 'echo "# x" >> dkms.postinst'
92+
case_run 'scripts changed, no bump' err 'echo "# x" >> scripts/conftest.sh'
93+
case_run 'header added, no bump' err 'echo "#define X 1" > mode_tbls.h'
94+
case_run 'packaged file deleted, no bump' err 'rm imx585.dts'
95+
96+
echo '== bumps =='
97+
case_run 'source changed, patch bump' ok 'echo "// fix" >> nv_imx585.c; sed -i s/0.1.0/0.1.1/ dkms.conf'
98+
case_run 'source changed, minor bump' ok 'echo "// cap" >> nv_imx585.c; sed -i s/0.1.0/0.2.0/ dkms.conf'
99+
case_run 'bump alone, no packaged change' ok 'sed -i s/0.1.0/0.1.1/ dkms.conf'
100+
case_run 'version went backwards' err 'echo "// x" >> nv_imx585.c; sed -i s/0.1.0/0.0.9/ dkms.conf'
101+
case_run 'crlf dkms.conf, patch bump' ok 'echo "// x" >> nv_imx585.c; sed -i s/0.1.0/0.1.1/ dkms.conf; sed -i "s/$/\r/" dkms.conf'
102+
103+
echo '== base moved after fork point =='
104+
# origin/main has bumped to 0.1.1 on its own. Judged against origin tip
105+
# instead of the fork point, PR's matching bump would read as no bump.
106+
git checkout -q -B mainline "$BASE"
107+
echo '// mainline fix' >> nv_imx585.c
108+
sed -i s/0.1.0/0.1.1/ dkms.conf
109+
git add -A
110+
git commit -qm 'Mainline bump'
111+
git update-ref refs/remotes/origin/main "$(git rev-parse HEAD)"
112+
case_run 'PR bump matches moved base tip' ok 'echo "// pr" >> nv_imx585.c; sed -i s/0.1.0/0.1.1/ dkms.conf'
113+
git update-ref refs/remotes/origin/main "$BASE"
114+
115+
echo '== pre-release opens a series =='
116+
case_run 'plain to pre-release' ok 'echo "// x" >> nv_imx585.c; sed -i s/0.1.0/0.2.0-alpha.1/ dkms.conf'
117+
case_run 'off-grammar pre-release, warns' ok 'echo "// x" >> nv_imx585.c; sed -i s/0.1.0/0.2.0-wip/ dkms.conf'
118+
119+
echo '== mid-series, base is already a pre-release =='
120+
git checkout -q -B series "$BASE"
121+
echo '// series start' >> nv_imx585.c
122+
sed -i s/0.1.0/0.2.0-alpha.1/ dkms.conf
123+
git add -A
124+
git commit -qm 'Fixture pre-release base'
125+
BASE=$(git rev-parse HEAD)
126+
git update-ref refs/remotes/origin/main "$BASE"
127+
case_run 'later PR in series, no bump' ok 'echo "// more" >> nv_imx585.c'
128+
case_run 'alpha.1 to alpha.2' ok 'echo "// x" >> nv_imx585.c; sed -i s/alpha.1/alpha.2/ dkms.conf'
129+
case_run 'promote to plain release' ok 'echo "// last" >> nv_imx585.c; sed -i s/0.2.0-alpha.1/0.2.0/ dkms.conf'
130+
case_run 'series version went backwards' err 'echo "// x" >> nv_imx585.c; sed -i s/0.2.0-alpha.1/0.1.5/ dkms.conf'
131+
132+
echo '== base predates dkms.conf =='
133+
git checkout -q -B bare "$BASE"
134+
git rm -q dkms.conf
135+
git commit -qm 'Fixture without dkms.conf'
136+
BASE=$(git rev-parse HEAD)
137+
git update-ref refs/remotes/origin/main "$BASE"
138+
case_run 'PR introduces dkms.conf' ok 'printf %s\\n PACKAGE_VERSION=\"0.1.0\" > dkms.conf'
139+
140+
echo
141+
if [ "$fail" -eq 0 ]; then
142+
echo "OK: $pass cases passed."
143+
else
144+
echo "::error::$fail of $((pass + fail)) version guard cases failed."
145+
fi
146+
[ "$fail" -eq 0 ]

0 commit comments

Comments
 (0)