chore(ci): gate the release tagger on check + test
CI / Check / Test (push) Successful in 0s
CI / Tag release (push) Successful in 0s

The repo had a `task check` structure gate and a 28-case shell test suite
that nothing ever ran. The only workflow auto-tagged every push to main, so
CI reported green having tested nothing — a gate that fails open, and the
reason `mathias/skills` shows up as repository-blocked in the estate backlog
classification.

release.yml becomes ci.yml with two jobs: `gate` runs check + test on every
push and PR; `tag` keeps the existing semver-from-trailer logic but declares
`needs: gate`, so a red check cannot ship a tag. The pull_request trigger
also gives `tbd_ship` a CI run to read on a branch head — without one it
fails closed and never merges (#22).

The tagger now pushes the manifest bump commit and the tag in a single
`git push`. Two pushes left a window where the gate ran `task check` on the
bump commit before the tag existed, and the manifest/tag parity assertion
would fail on a commit that was in fact correct.

tests/ci_gate_test.sh pins the wiring: PR trigger present, both tasks run,
tag needs gate, no second ungated workflow file.

Refs: #22
Bump-Type: minor
This commit is contained in:
2026-08-29 09:46:10 +02:00
parent 51a8c25b4d
commit 3112b4ce92
2 changed files with 107 additions and 18 deletions
@@ -1,23 +1,52 @@
name: release
name: CI
# Auto-tag every push to main with a semver bump derived from the latest
# commit footer:
# Bump-Type: major → vX.0.0
# Bump-Type: minor → v0.Y.0
# (default) → v0.0.Z (patch)
# Two jobs, in order:
#
# Skips when the latest commit is itself the auto-tagger (commit message
# contains "[skip-release]") to prevent recursion. Tag is pushed back
# to the repo using the GITEA_TOKEN provided by act_runner.
# gate — `task check` (structure, frontmatter, manifest/tag parity) plus
# `task test` (hooks, doctor, release-skip). Runs on every push and
# every PR. Before this existed the suite ran nowhere: CI reported
# green having tested nothing (#22).
#
# tag — auto-tags a push to main with a semver bump derived from the latest
# commit footer:
# Bump-Type: major → vX.0.0
# Bump-Type: minor → v0.Y.0
# (default) → v0.0.Z (patch)
# Skips when the commit carries the `Skip-Release: true` trailer (the
# tagger's own manifest bump) to prevent recursion. Gated on `gate`,
# so a red check cannot ship a tag.
on:
"on":
push:
branches:
- main
branches: ["**"]
pull_request:
branches: [main]
jobs:
tag:
gate:
name: Check / Test
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
with:
# `task check` compares plugin.json's version to `git describe`;
# a shallow checkout has no tags and would skip that assertion.
fetch-depth: 0
- name: Verify toolchain
run: task --version
- name: Structure gate
run: task check
- name: Shell test suite
run: task test
tag:
name: Tag release
needs: gate
runs-on: self-hosted
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: checkout
uses: actions/checkout@v4
@@ -72,16 +101,19 @@ jobs:
manifest=.claude-plugin/plugin.json
version="${TAG#v}"
current=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -1)
git tag -a "$TAG" -m "release $TAG"
if [ "$current" != "$version" ]; then
sed -i "s/\"version\"[[:space:]]*:[[:space:]]*\"$current\"/\"version\": \"$version\"/" "$manifest"
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"
git push origin HEAD:main
# Commit and tag go up in ONE push: the gate job runs `task check`
# on the bump commit, and that asserts manifest == latest tag. Two
# separate pushes leave a window where it legitimately fails.
git push origin HEAD:main "$TAG"
echo "bumped $manifest to $version"
else
git push origin "$TAG"
fi
git tag -a "$TAG" -m "release $TAG"
# gitea-actions checkout uses an x-access-token URL; reuse it.
git push origin "$TAG"
echo "pushed $TAG"