Skip to content

Commit d736fa9

Browse files
authored
Merge pull request #65 from lorem-dev/release/0.7.2
release: 0.7.2
2 parents ea293d9 + 81ace3c commit d736fa9

21 files changed

Lines changed: 693 additions & 57 deletions

File tree

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
---
2+
name: check-glibc-floor
3+
description: >
4+
Verify the Linux glibc floor is consistent everywhere it is stated: the
5+
release workflow's runner image is the source of truth, and scripts/install.sh,
6+
docs/, and the release download labels must all agree with it. Also confirms
7+
the statically linked musl CLI is still built and published.
8+
---
9+
10+
# check-glibc-floor
11+
12+
The prebuilt `*-linux-gnu` CLI and the desktop bundles are linked against the
13+
glibc of the runner image that builds them. glibc versions its symbols, and the
14+
compatibility runs one way only: a newer glibc runs older binaries, an older one
15+
cannot run newer binaries. So that image's version is a hard floor -- below it
16+
the binary exits with ``libc.so.6: version `GLIBC_2.34' not found`` before
17+
running any code.
18+
19+
That floor is a single fact restated across six files: `release.yml` itself,
20+
`scripts/install.sh`, `scripts/release-downloads.mjs`, `docs/getting-started.md`,
21+
`docs/troubleshooting.md`, and `CHANGES.md`. Several of them say it more than
22+
once, so check every occurrence rather than counting them. This skill verifies
23+
they all still say the same number, and that the musl escape hatch is still
24+
shipped.
25+
26+
**The source of truth is the Linux runner image in
27+
`.github/workflows/release.yml`.** Nothing else. When they disagree, the image
28+
is right and the other places are stale.
29+
30+
## Runner image to glibc
31+
32+
| Image | glibc |
33+
|---|---|
34+
| `ubuntu-20.04` | 2.31 (image retired; here for reading older workflows) |
35+
| `ubuntu-22.04`, `ubuntu-22.04-arm` | 2.35 |
36+
| `ubuntu-24.04`, `ubuntu-24.04-arm` | 2.39 |
37+
| `ubuntu-latest` | whatever it currently aliases (2.39 at the time of writing) |
38+
39+
If the image is not in this table, look up its glibc before continuing rather
40+
than guessing, and add the row.
41+
42+
## Steps
43+
44+
### 1. Read the floor from the release workflow
45+
46+
```bash
47+
grep -n "os: ubuntu" .github/workflows/release.yml
48+
```
49+
50+
Map every Linux leg through the table above. Expect one image across all Linux
51+
legs; if the x64 and arm64 legs differ, the floor is the HIGHER of the two and
52+
that divergence is itself a finding worth reporting.
53+
54+
`ubuntu-latest` on a Linux leg is a finding on its own: it is a moving alias, so
55+
it silently raises the floor whenever the platform re-points it. The Linux legs
56+
must pin an explicit image.
57+
58+
Call the result FLOOR (for example `2.35`).
59+
60+
### 2. Verify scripts/install.sh
61+
62+
```bash
63+
grep -n "MIN_GLIBC_MAJOR\|MIN_GLIBC_MINOR" scripts/install.sh
64+
```
65+
66+
- `MIN_GLIBC_MAJOR` and `MIN_GLIBC_MINOR` must equal FLOOR.
67+
68+
Then confirm each of these is still present and wired up; every one of them is
69+
load-bearing, and the script silently installs an unrunnable binary if any is
70+
dropped:
71+
72+
- `glibc_version` detects the host version, trying `getconf GNU_LIBC_VERSION`
73+
first and `ldd --version` second, and yields nothing on a musl host.
74+
- It rejects any value that is not a dotted number, because the comparison
75+
below it is arithmetic.
76+
- `linux_libc` compares against `MIN_GLIBC_*` and returns `musl` when the host
77+
is below the floor, when there is no glibc at all, and when the version could
78+
not be read.
79+
- `SKILLKEEPER_LIBC` is validated at the TOP LEVEL of the script, not inside
80+
`linux_libc`. `linux_libc` runs in a command substitution, where `err` would
81+
exit only the subshell and leave the caller building a truncated target
82+
triple.
83+
- The Linux branch composes the triple from the detected libc, rather than
84+
hardcoding `-gnu`.
85+
- A missing musl asset falls back to the gnu archive with a warning (older
86+
releases predate the musl archives), and that fallback is skipped when
87+
`SKILLKEEPER_LIBC` named a flavour explicitly.
88+
- The final smoke run points at `SKILLKEEPER_LIBC=musl` when the installed gnu
89+
binary fails to start.
90+
91+
Exercise the detection rather than only reading it. This stubs `getconf` and
92+
`ldd` on PATH, sources the two real functions out of the script, and reads the
93+
real `MIN_GLIBC_*`, so it tests the shipped threshold rather than a copy of it.
94+
It runs anywhere, the developer's macOS included:
95+
96+
```bash
97+
d=$(mktemp -d)
98+
# Anchored on the function syntax, not on comment text, and then checked: a
99+
# range that failed to find its end would run to EOF and capture the whole
100+
# install flow, which the `.` below would EXECUTE -- downloading and writing to
101+
# $HOME, once per row. The two guards make that impossible rather than unlikely.
102+
sed -n '/^glibc_version() {/,/^}/p;/^linux_libc() {/,/^}/p' scripts/install.sh > "$d/funcs.sh"
103+
grep -qE 'download |INSTALL_DIR|asset_url|Done\.' "$d/funcs.sh" &&
104+
{ echo "ABORT: extraction captured installer flow, do not source it"; exit 1; }
105+
[ "$(grep -c '^}' "$d/funcs.sh")" = 2 ] ||
106+
{ echo "ABORT: expected exactly 2 functions"; exit 1; }
107+
108+
# Heredocs, not printf: a printf format string containing %s consumes it
109+
# itself and writes a stub that ignores its argument, which silently turns
110+
# every row below into "no glibc" and the whole table into musl.
111+
cat > "$d/getconf" <<'EOF'
112+
#!/bin/sh
113+
[ "${1:-}" = GNU_LIBC_VERSION ] || exit 1
114+
[ -n "${SK_GLIBC:-}" ] || exit 1
115+
printf 'glibc %s\n' "$SK_GLIBC"
116+
EOF
117+
cat > "$d/ldd" <<'EOF'
118+
#!/bin/sh
119+
echo "musl libc (x86_64)"
120+
EOF
121+
chmod +x "$d/getconf" "$d/ldd"
122+
123+
for v in 2.28 2.31 2.34 2.35 2.39 ""; do
124+
out=$(SK_GLIBC="$v" PATH="$d:$PATH" SK_F="$d/funcs.sh" sh -c '
125+
eval "$(grep "^MIN_GLIBC_" scripts/install.sh)"
126+
LIBC=auto
127+
err() { echo "error: $1" >&2; exit 1; }
128+
. "$SK_F"
129+
linux_libc')
130+
printf 'glibc %-6s -> %s\n' "${v:-none}" "$out"
131+
done
132+
rm -rf "$d"
133+
```
134+
135+
Expected, for a floor of 2.35. The boundary is exercised from both sides, and
136+
`none` stands for a host with no glibc at all, as on Alpine:
137+
138+
```
139+
glibc 2.28 -> musl
140+
glibc 2.31 -> musl
141+
glibc 2.34 -> musl
142+
glibc 2.35 -> gnu
143+
glibc 2.39 -> gnu
144+
glibc none -> musl
145+
```
146+
147+
Any `ABORT` line, or a table differing from this one, is a FAIL: either the
148+
detection changed or the extraction no longer matches the functions. Do not
149+
adjust the test to make it pass. Also run `sh -n scripts/install.sh`.
150+
151+
### 3. Verify the musl builds are still published
152+
153+
```bash
154+
grep -n "musl" .github/workflows/release.yml
155+
```
156+
157+
- Both Linux legs carry a `musl_target` (`x86_64-unknown-linux-musl` and
158+
`aarch64-unknown-linux-musl`); every non-Linux leg carries an empty one.
159+
- The toolchain step installs the musl target alongside the leg's own.
160+
- A `Build the CLI (static, musl)` step and its `tar.gz` archive step both run
161+
when `musl_target` is non-empty, and the archive lands in `dist-cli/`, which
162+
the upload step collects.
163+
164+
The musl archive is published ALONGSIDE the gnu one, never instead of it:
165+
`SKILLKEEPER_VERSION` can pin an older release, and existing installs resolve
166+
the asset name they already know.
167+
168+
### 4. Verify the documentation states FLOOR
169+
170+
```bash
171+
grep -rn "glibc" docs/ README.md CHANGES.md
172+
```
173+
174+
`CHANGES.md` is in scope deliberately: it states the floor in the entry that
175+
introduced the musl builds, and a runner-image bump would otherwise leave the
176+
changelog quietly asserting the old number while this gate reported CONSISTENT.
177+
178+
Every stated version must equal FLOOR. At the time of writing that is:
179+
180+
- `docs/getting-started.md` -- the System requirements table (CLI and desktop
181+
rows) and the install-script paragraph.
182+
- `docs/troubleshooting.md` -- the `GLIBC_2.34' not found` section.
183+
- `CHANGES.md` -- the musl entry under the current version.
184+
185+
Distribution versions named as remedies are part of this check, not decoration:
186+
verify each one actually clears FLOOR before repeating it. RHEL 9, for
187+
instance, ships glibc 2.34 and does NOT clear a floor of 2.35.
188+
189+
The troubleshooting section must still say all of: the download is not corrupt,
190+
the musl build is the fix, `SKILLKEEPER_LIBC=musl` is the command, building from
191+
source also works, and upgrading glibc in place is not the answer (the
192+
distribution release pins it). Removing the last point invites someone to break
193+
a machine.
194+
195+
### 5. Verify the release download labels
196+
197+
```bash
198+
grep -n "note:" scripts/release-downloads.mjs
199+
```
200+
201+
The two `*-linux-gnu` entries must carry a `glibc <FLOOR>+` note and the two
202+
`*-linux-musl` entries a `static, musl` note, so a reader picking an asset by
203+
hand can tell which one runs on their host. All four Linux triples must be in
204+
`CLI_TARGETS`; an unlisted one degrades to a bare triple label.
205+
206+
### 6. Check whether the floor rose since the last release
207+
208+
This is the one case that reaches users who already have the app installed.
209+
210+
```bash
211+
git diff "$(git describe --tags --abbrev=0)" -- .github/workflows/release.yml | grep -E "^[+-].*os: ubuntu"
212+
```
213+
214+
No `..HEAD` on purpose: that form compares committed history only, so during
215+
release prep -- exactly when this skill runs -- a runner-image bump still
216+
sitting in the working tree would read as "floor unchanged". The two-dot-free
217+
form covers committed and uncommitted alike. No output means the image did not
218+
move.
219+
220+
If the Linux runner image changed since the last release tag, say so loudly and
221+
report the old and new floors. A raised floor means hosts between the two
222+
versions run the current desktop app fine but cannot run the next one, and the
223+
self-updater has no glibc gate: `platform.rs::asset_key` selects on os/arch
224+
only, and `versions.json` records no minimum (see `scanAssets` in
225+
`scripts/gen-versions-json.mjs`). Those hosts would download, install, and end
226+
up with an app that will not start.
227+
228+
Raising the floor is therefore a release decision, not an incidental bump. Flag
229+
it for the developer; do not decide it inside this skill.
230+
231+
### 7. Report
232+
233+
```
234+
runner image / floor: ubuntu-22.04 -> glibc 2.35
235+
install.sh MIN_GLIBC_*: PASS / FAIL
236+
install.sh detection: PASS / FAIL
237+
musl builds published: PASS / FAIL
238+
docs state the floor: PASS / FAIL
239+
download labels: PASS / FAIL
240+
floor unchanged since tag: PASS / RAISED (2.35 -> 2.39)
241+
242+
glibc floor: CONSISTENT / INCONSISTENT
243+
```
244+
245+
List every mismatch with its file and line and the value it should hold. Do not
246+
edit files automatically -- propose the corrections and let the developer apply
247+
them.

‎.agents/skills/pre-release-check/SKILL.md‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
name: pre-release-check
33
description: >
44
Gate a release by running check-changes, check-docs, run-tests-and-linters,
5-
check-licenses, and check-fixture-repo, plus verifying the version bump and
6-
that all commits since the last release follow the conventional-commits
7-
format.
5+
check-licenses, check-fixture-repo, and check-glibc-floor, plus verifying the
6+
version bump and that all commits since the last release follow the
7+
conventional-commits format.
88
---
99

1010
# pre-release-check
@@ -13,7 +13,7 @@ Run the full release gate. All checks must pass before tagging a release.
1313

1414
## Steps
1515

16-
### 1. Run the five component skills
16+
### 1. Run the six component skills
1717

1818
Run each skill in order and collect its result (PASS or FAIL with details):
1919

@@ -25,7 +25,13 @@ Run each skill in order and collect its result (PASS or FAIL with details):
2525
against a real working tree, so it catches wiring regressions the unit tests
2626
(which run against an in-memory filesystem) cannot see.
2727
4. **check-docs** -- README.md, docs/ nav, command accuracy, version refs.
28-
5. **check-changes** -- CHANGES.md Development section vs. commit history.
28+
5. **check-glibc-floor** -- the Linux glibc floor agrees across the release
29+
workflow, `scripts/install.sh`, docs/, and the download labels, and the musl
30+
CLI is still published. Belongs in the release gate because the floor is set
31+
by the runner image the release itself builds on: a bumped image quietly
32+
invalidates every stated version, and a raised floor strands hosts that the
33+
self-updater will still offer the update to.
34+
6. **check-changes** -- CHANGES.md Development section vs. commit history.
2935

3036
If check-licenses, run-tests-and-linters, or check-fixture-repo fails, report the
3137
failure and stop. The remaining checks can still be reported for completeness,
@@ -86,6 +92,7 @@ check-licenses: PASS / FAIL
8692
run-tests-and-linters: PASS / FAIL
8793
check-fixture-repo: PASS / FAIL
8894
check-docs: PASS / FAIL
95+
check-glibc-floor: PASS / FAIL
8996
check-changes: PASS / FAIL
9097
tag provenance (branch): PASS / FAIL
9198
version bump consistent: PASS / FAIL

‎.github/workflows/ci.yml‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,11 @@ jobs:
163163
uses: dtolnay/rust-toolchain@4716b85f2fac3e324e64fa2810f6b5c3905760a5 # 1.97.1
164164
with:
165165
components: rustfmt, clippy
166+
# The two musl targets the release builds the static CLI for, in the
167+
# same comma-separated form release.yml passes. Installing them here
168+
# means a broken toolchain pin or a mistyped target list fails on a
169+
# pull request instead of on the tag, where it would kill the release.
170+
targets: x86_64-unknown-linux-musl,aarch64-unknown-linux-musl
166171

167172
- name: Cache cargo registry and target
168173
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
@@ -208,6 +213,22 @@ jobs:
208213
- name: Test workspace
209214
run: cargo test --workspace
210215

216+
# The release publishes a statically linked CLI for Linux (see the
217+
# "Build the CLI (static, musl)" step in release.yml). Without this, the
218+
# first musl link of any change happens on the tag -- and a failure there
219+
# stops `publish`, so the tag is dead and has to be deleted and re-cut.
220+
#
221+
# x64 gets a full `build`, which actually links, because that is what the
222+
# release does. arm64 gets `check`: `cargo check` never invokes the
223+
# linker, so it cross-compiles here without an aarch64 musl linker, while
224+
# still proving the target is installed and the tree compiles for it.
225+
# Only the CLI is built -- the desktop app links the system WebKitGTK and
226+
# cannot be static.
227+
- name: Build the CLI for musl (release parity)
228+
run: |
229+
cargo build -p skillkeeper-cli --target x86_64-unknown-linux-musl
230+
cargo check -p skillkeeper-cli --target aarch64-unknown-linux-musl
231+
211232
# `cargo test` WRITES the generated artifacts (ts-rs bindings, the mcp.yml
212233
# JSON Schema) rather than asserting them, so a commit that changes a Rust
213234
# type but forgets to commit the regenerated file is green here and only

0 commit comments

Comments
 (0)