Three runs failed with `! [rejected] HEAD -> main (non-fast-forward)`. The runner is maxParallel=1 and shared with mathias/infra, so those jobs started five minutes and three commits after their own push. The tagger checked out its commit and pushed that stale HEAD to main, which main had long left behind. My own change made it worse: pairing the manifest bump and the tag in a single `git push` turned a partial failure into a total one, so no tag was created either. Except once — v0.10.0 landed through the no-bump-needed branch while the manifest stayed at 0.9.0, which is precisely the "installed hosts never see the release" state, and which #34's ordering check then caught on main. The tag now goes up first and alone: it names the commit this run checked out and cannot be rejected however far main has moved. The bump is a separate concern that must land on whatever main is NOW, so it re-fetches, rebuilds the commit on origin/main and retries up to three times, and steps aside entirely if a later run has already bumped past it. plugin.json is set to 0.10.0 in this commit so the gate can pass at all — main was red on manifest-behind-tag, which would have blocked the tag job that fixes the tagger. Refs #22 Bump-Type: patch
145 lines
5.6 KiB
YAML
145 lines
5.6 KiB
YAML
name: CI
|
|
|
|
# Two jobs, in order:
|
|
#
|
|
# 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":
|
|
push:
|
|
branches: ["**"]
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
jobs:
|
|
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
|
|
with:
|
|
fetch-depth: 0
|
|
token: ${{ secrets.GITEA_TOKEN }}
|
|
|
|
- name: derive next version
|
|
id: bump
|
|
run: |
|
|
set -euo pipefail
|
|
last_commit=$(git log -1 --pretty=%B)
|
|
# Delegated to bin/should-skip-release so the rule is covered by
|
|
# tests/release_skip_test.sh rather than living untested in YAML.
|
|
if printf '%s\n' "$last_commit" | sh bin/should-skip-release; then
|
|
echo "skipping — commit carries the Skip-Release trailer"
|
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
latest=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
|
major=$(echo "$latest" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\1/')
|
|
minor=$(echo "$latest" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\2/')
|
|
patch=$(echo "$latest" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\3/')
|
|
bump=$(printf '%s' "$last_commit" | grep -iE '^Bump-Type:' | head -1 | awk '{print tolower($2)}' || true)
|
|
case "$bump" in
|
|
major) major=$((major + 1)); minor=0; patch=0 ;;
|
|
minor) minor=$((minor + 1)); patch=0 ;;
|
|
*) patch=$((patch + 1)) ;;
|
|
esac
|
|
next="v${major}.${minor}.${patch}"
|
|
echo "previous=$latest"
|
|
echo "bump=${bump:-patch}"
|
|
echo "next=$next"
|
|
echo "tag=$next" >> "$GITHUB_OUTPUT"
|
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: tag + push
|
|
if: steps.bump.outputs.skip != 'true'
|
|
env:
|
|
TAG: ${{ steps.bump.outputs.tag }}
|
|
GITEA_ACTOR: ${{ gitea.actor }}
|
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
git config user.name "${GITEA_ACTOR}"
|
|
git config user.email "${GITEA_ACTOR}@noreply.gitea.d-ma.be"
|
|
|
|
# The tag goes up FIRST and on its own. It names the commit this run
|
|
# checked out and can never be rejected, however far main has moved.
|
|
git tag -a "$TAG" -m "release $TAG"
|
|
git push origin "$TAG"
|
|
echo "pushed $TAG"
|
|
|
|
# The manifest bump is a separate concern and must land on whatever
|
|
# main is NOW. The runner is maxParallel=1 and shared with other
|
|
# repos, so a job routinely starts several commits after its own —
|
|
# pushing this run's stale HEAD to main is then rejected
|
|
# non-fast-forward, which is how v0.10.0 ended up tagged with the
|
|
# manifest still reading 0.9.0.
|
|
#
|
|
# Claude Code gates plugin updates on this field: if it does not
|
|
# move, installed hosts never see the release. `task check` asserts
|
|
# the manifest is not behind the latest tag, so a failure here is a
|
|
# red main, not a cosmetic miss.
|
|
manifest=.claude-plugin/plugin.json
|
|
version="${TAG#v}"
|
|
|
|
for attempt in 1 2 3; do
|
|
git fetch --quiet origin main
|
|
git checkout --quiet -B release-bump origin/main
|
|
|
|
current=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -1)
|
|
if [ "$current" = "$version" ]; then
|
|
echo "manifest already at $version"
|
|
exit 0
|
|
fi
|
|
# A later run may have bumped past us while this one waited.
|
|
if [ "$(sh bin/newer-version "$current" "$version")" = "$current" ]; then
|
|
echo "manifest is $current, already newer than $version — leaving it"
|
|
exit 0
|
|
fi
|
|
|
|
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"
|
|
if git push origin HEAD:main; then
|
|
echo "bumped $manifest to $version"
|
|
exit 0
|
|
fi
|
|
echo "main moved while bumping — retry $attempt"
|
|
done
|
|
|
|
echo "could not land the manifest bump after 3 attempts" >&2
|
|
exit 1
|