diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 06386fc..e4f4da6 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -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.9.0", + "version": "0.10.0", "author": { "name": "Mathias", "url": "https://git.d-ma.be/mathias" diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 55b87f7..8f54585 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -94,26 +94,51 @@ jobs: git config user.name "${GITEA_ACTOR}" git config user.email "${GITEA_ACTOR}@noreply.gitea.d-ma.be" - # Claude Code gates plugin updates on plugin.json's version field: - # if it does not move, installed hosts never see the new tag. Bump - # it in the same commit train so the tag and the manifest agree — - # `task check` enforces that equality on every run. + # 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}" - current=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -1) - git tag -a "$TAG" -m "release $TAG" - if [ "$current" != "$version" ]; then + 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" - # 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 - echo "pushed $TAG" + 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