Compare commits

..
2 Commits
Author SHA1 Message Date
mathias 6838baa071 fix(ci): gate the release-bump commit — nothing has ever tested it
CI / Check / Test (push) Successful in 3s
CI / Tag release (push) Successful in 1s
Found from the other end: the koala cluster's CIHeadUncovered{repo=skills}
alert had been firing for 85 hours, and it is a true positive.

    mathias/skills main HEAD   f031c2b
    workflow runs for that sha  0

Not a one-off. The previous release-bump commit has the same property:

    6cc224f  chore(release): plugin.json version 0.19.4  ->  0 runs
    f031c2b  chore(release): plugin.json version 0.19.5  ->  0 runs

The `tag` job pushes the manifest bump itself with secrets.GITEA_TOKEN, and a
push authenticated with the Actions token does not trigger workflows -- that is
deliberate platform behaviour to stop CI recursing. The `Skip-Release: true`
trailer stops the TAGGER recursing and works; nothing stopped the whole run,
`gate` included, from never firing on that commit.

So after every release, main's HEAD is a commit no CI has seen, until an
unrelated human push moves it. CIHeadUncovered therefore cannot clear on its
own -- and an alert that can only be cleared by doing something unrelated is
worse than a noisy one, because it teaches the reader that red is the normal
state of that rule.

The commit is not cosmetic either. plugin.json's version is what Claude Code
gates plugin updates on, `task check` asserts it is not behind the latest tag,
and the sed-and-retry logic that writes it already shipped a mismatched
manifest once -- v0.10.0 tagged with the manifest still reading 0.9.0, per the
comment above that very loop.

Fix: run `task check` on the bump commit before pushing it. The commit is
produced in that step, so it can be validated in that step -- one check instead
of a second full CI run, and no recursion risk to reintroduce.

Considered and rejected: re-triggering CI with a different credential after the
push (costs a full run per release and brings back what the token behaviour
exists to prevent), and teaching CIHeadUncovered to ignore manifest-only
commits (narrowing the measure to fit the system, and it would hide a real gap
too).

The assertion in tests/ci_gate_test.sh had to be anchored with ^[^#]*: the tag
job's own prose says "`task check` asserts the manifest is not behind the
latest tag", so a bare grep matched the comment explaining the problem and
reported the problem fixed.

Closes #46.
2026-09-11 11:11:16 +02:00
mathias f031c2b77d chore(release): plugin.json version 0.19.5
Skip-Release: true
2026-09-08 19:35:09 +00:00
3 changed files with 54 additions and 1 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "skills",
"description": "Mathias's engineering skills library, plus the hooks and MCP servers that make it enforceable: TDD, clean code, debugging, specs, discovery, CI, and secret hygiene.",
"version": "0.19.4",
"version": "0.19.5",
"author": {
"name": "Mathias",
"url": "https://git.d-ma.be/mathias"
+27
View File
@@ -133,6 +133,33 @@ jobs:
git add "$manifest"
# The trailer below stops this push from re-triggering the tagger.
git commit -m "chore(release): plugin.json version $version" -m "Skip-Release: true"
# skills#46. This commit is pushed with secrets.GITEA_TOKEN, and a
# push authenticated with the Actions token does NOT trigger
# workflows -- deliberate platform behaviour to stop CI recursing.
# `Skip-Release: true` stops the tagger recursing; nothing stopped
# the whole run, `gate` included, from never firing on this commit.
#
# So every release left main's HEAD with zero CI runs. Measured:
# 6cc224f (0.19.4) and f031c2b (0.19.5) each have none, and the
# koala cluster's CIHeadUncovered{repo=skills} had been firing on it
# for 85 hours -- an alert that can only be cleared by an unrelated
# human push.
#
# The commit is not cosmetic: plugin.json's version is what Claude
# Code gates plugin updates on, `task check` asserts it is not
# behind the latest tag, and the sed-and-retry logic above already
# shipped a mismatched manifest once (v0.10.0 reading 0.9.0).
#
# Gating it HERE rather than re-triggering CI afterwards: the commit
# is produced here, so it can be validated here, at the cost of one
# `task check` instead of a second full run -- and with no recursion
# risk to reintroduce.
if ! task check; then
echo "::error::the release-bump commit fails task check -- not pushing it" >&2
exit 1
fi
if git push origin HEAD:main; then
echo "bumped $manifest to $version"
exit 0
+26
View File
@@ -53,5 +53,31 @@ printf '%s' "$tag_job" | grep -qE '^ +needs: *gate' \
&& ok 'ungated release.yml removed' \
|| no 'ungated release.yml removed'
# skills#46. The manifest-bump commit is created and pushed by the `tag` job
# using secrets.GITEA_TOKEN, and a push authenticated with the Actions token
# does not trigger workflows -- deliberate platform behaviour to stop CI
# recursing. So the ONE commit in this repo that no CI run has ever seen is the
# one the release job writes, every release:
#
# 6cc224f chore(release): plugin.json version 0.19.4 -> 0 runs
# f031c2b chore(release): plugin.json version 0.19.5 -> 0 runs
#
# The koala cluster's CIHeadUncovered{repo=skills} alert had been firing on it
# for 85 hours when this was found, and it cannot clear on its own -- only an
# unrelated human push moves main's HEAD to a tested commit.
#
# The commit is not nothing: plugin.json's version is what Claude Code gates
# plugin updates on, `task check` asserts it is not behind the latest tag, and
# the sed-and-retry logic that writes it already shipped a mismatched manifest
# once (v0.10.0 with the manifest reading 0.9.0).
#
# Anchored past comments: the tag job's prose already SAYS "`task check`
# asserts the manifest is not behind the latest tag", and a bare grep matches
# that and passes. A grep for a concept matches the rationale, and the more
# carefully a thing is documented the more likely that is.
printf '%s' "$tag_job" | grep -qE '^[^#]*task check' \
&& ok 'the release-bump commit is gated before it is pushed' \
|| no 'the release-bump commit is pushed with no gate -- nothing ever tests it (#46)'
printf '\nci_gate: %d passed, %d failed\n' "$pass" "$fail"
[ "$fail" -eq 0 ]