diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3b39c88a..0161653d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -206,10 +206,19 @@ jobs: .claude-plugin/plugin.json .codex-plugin/plugin.json if git diff --quiet -- .claude-plugin/plugin.json .codex-plugin/plugin.json; then echo "plugin manifests already at $RELEASE_VERSION; nothing to commit" - exit 0 + else + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git commit -m "release: stamp plugin manifests to $RELEASE_VERSION" \ + -- .claude-plugin/plugin.json .codex-plugin/plugin.json + git push origin main fi - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git commit -m "release: stamp plugin manifests to $RELEASE_VERSION" \ - -- .claude-plugin/plugin.json .codex-plugin/plugin.json - git push origin main + # Advance the stable channel ref to the stamped release commit. The + # spacedock-dev/marketplace stable entry pins source.ref=stable (a moving + # branch, not a per-release tag), so a fresh `spacedock@spacedock` install + # resolves whatever this branch points at — this push is what publishes the + # release to the stable channel, replacing a hand-edit of the marketplace + # repo. main HEAD is the stamped release commit and stable is a prior + # release commit, so this fast-forwards. Same-repo push, so the default + # GITHUB_TOKEN suffices (no cross-repo PAT, unlike the homebrew-tap push). + git push origin main:refs/heads/stable diff --git a/internal/release/channel_agreement_guard_test.go b/internal/release/channel_agreement_guard_test.go index 09a62b00..e3522aa6 100644 --- a/internal/release/channel_agreement_guard_test.go +++ b/internal/release/channel_agreement_guard_test.go @@ -30,7 +30,9 @@ const stableChannelBranch = "main" // It reads the step's run block, finds the `git switch ` and `git push // origin ` commands, and returns the branch only when BOTH name the same // branch (a switch/push split would itself be a drift). Returns "" when the step, -// or either command, is absent. +// or either command, is absent. The step also pushes the stable channel ref +// (`git push origin main:refs/heads/stable`); that refspec push (target contains +// `:`) is skipped here so it does not shadow the bare-branch stamp target. func releaseStampTarget(workflow string) string { for _, step := range parseWorkflowSteps(workflow) { if step.name != "Stamp plugin manifests to the release version" { @@ -42,7 +44,7 @@ func releaseStampTarget(workflow string) string { if len(fields) == 3 && fields[0] == "git" && fields[1] == "switch" { switchTo = fields[2] } - if len(fields) == 4 && fields[0] == "git" && fields[1] == "push" && fields[2] == "origin" { + if len(fields) == 4 && fields[0] == "git" && fields[1] == "push" && fields[2] == "origin" && !strings.Contains(fields[3], ":") { pushTo = fields[3] } } @@ -150,6 +152,37 @@ func TestStableChannelBinaryPairAgreesOnMain(t *testing.T) { } } +// stampStepAdvancesStableRef reports whether the "Stamp plugin manifests" step +// pushes the stamped commit to the stable channel ref. It looks for a +// `git push origin :refs/heads/stable` command in the step's run block. +func stampStepAdvancesStableRef(workflow string) bool { + for _, step := range parseWorkflowSteps(workflow) { + if step.name != "Stamp plugin manifests to the release version" { + continue + } + for _, command := range executableShellCommands(step.run) { + fields := strings.Fields(command) + if len(fields) == 4 && fields[0] == "git" && fields[1] == "push" && fields[2] == "origin" && strings.HasSuffix(fields[3], ":refs/heads/stable") { + return true + } + } + } + return false +} + +// TestStampStepAdvancesStableRef locks the stable-channel publish mechanism: the +// release stamp step MUST push the release commit to the `stable` ref, because the +// spacedock-dev/marketplace stable entry pins source.ref=stable. Without this push +// the stable channel would freeze at the prior release forever (a fresh +// `spacedock@spacedock` install would resolve the old commit), since the marketplace +// manifest is intentionally static and no longer hand-edited per release. The +// command is parsed out of the real release.yml, so dropping the push reds this. +func TestStampStepAdvancesStableRef(t *testing.T) { + if !stampStepAdvancesStableRef(readReleaseWorkflow(t)) { + t.Error("release.yml stamp step does not push to refs/heads/stable; the stable marketplace channel (source.ref=stable) would never advance past the prior release") + } +} + // TestEdgeChannelStampsNext locks the channel-separation half: the edge build // must keep stamping `next` even as the stable build moves to `main`, so the two // channels resolve distinct plugin sources rather than collapsing to one branch.