chore(frontseat-plugin-release-flow): GH-168 guard the generated workflow - #247
chore(frontseat-plugin-release-flow): GH-168 guard the generated workflow#247jbadeau wants to merge 3 commits into
Conversation
… store twice The "Cache frontseat resolution" step cached ~/.cache/frontseat — the parent of ~/.cache/frontseat/localgrid, which has its own restore/save pair. Restoring the parent extracted an older copy of the grid store over the freshly restored one (thousands of tar "File exists" errors on the 0444 CAS blobs, silently regressing the store), and its save re-uploaded the multi-GB store a second time every run. Cache the resolution entries individually instead. Template and generated .github/workflows/ci.yml updated in the same commit (github:sync byte-compares them). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…flow TestRepoWorkflowInSync renders the CI workflow from the checked-out template source and byte-compares it against the committed .github/workflows/ci.yml — immune to installed-plugin version skew; -update regenerates the file daemon-free. An hk pre-commit step runs it on any commit touching the workflow or the plugin sources, so hand edits to the generated file and template changes committed without regenerating both fail at commit time. hk stays a local dev tool (mise.local.toml, gitignored). CLAUDE.md documents the workflow and the version-skew trap; the general fingerprint-stamped drift detection is issue GH-248. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
e9ae59c to
31bc9c2
Compare
| t.Fatalf(".github/workflows/ci.yml does not match the template in sync.go.\n"+ | ||
| "Never edit the workflow by hand; regenerate it:\n"+ | ||
| " mise dev && frontseat daemon restart && frontseat sync --fix\n"+ |
There was a problem hiding this comment.
Blocking: not gofmt-clean, so go:format fails. gofmt wants a space before each + when the operands are on separate lines.
| t.Fatalf(".github/workflows/ci.yml does not match the template in sync.go.\n"+ | |
| "Never edit the workflow by hand; regenerate it:\n"+ | |
| " mise dev && frontseat daemon restart && frontseat sync --fix\n"+ | |
| t.Fatalf(".github/workflows/ci.yml does not match the template in sync.go.\n" + | |
| "Never edit the workflow by hand; regenerate it:\n" + | |
| " mise dev && frontseat daemon restart && frontseat sync --fix\n" + |
Also, mise dev already restarts the daemon (mise.toml, and CLAUDE.md documents it), so && frontseat daemon restart is redundant in this message and in the one on line 25.
There was a problem hiding this comment.
Both done in 3a8911e — gofmt -w moved the + to line-end (the message is now gofmt-clean), and I dropped the redundant && frontseat daemon restart from the drift message and the doc comment. Also narrowed the hk glob to .github/workflows/ci.yml so it no longer fires on the hand-written onboarding-eval.yml. Left hk in the gitignored mise.local.toml as designed.
Generated by Claude Code
| // This repo configures frontseat/release-flow with no options | ||
| // (frontseat.yaml), so defaults apply: manual trigger, no bootstrap. | ||
| p := &ReleaseFlowPlugin{} | ||
| if err := p.Configure(context.Background(), nil); err != nil { |
There was a problem hiding this comment.
Configure(ctx, nil) renders with defaults, but the repo configures this plugin in frontseat.yaml:
frontseat/release-flow:
options:
release_secrets: [MAVEN_CENTRAL_USERNAME, MAVEN_CENTRAL_PASSWORD, TAP_TOKEN]
bootstrap_from_source: trueBoth reach the render: bootstrap_from_source gates the Bootstrap frontseat from source step, and release_secrets is threaded through renderGithubWorkflow into the env: blocks of the daemon-start, build and ship steps.
So rendering with nil produces a workflow with no bootstrap step and no MAVEN_CENTRAL_USERNAME / MAVEN_CENTRAL_PASSWORD / TAP_TOKEN entries in any of those three blocks. The test fails against the committed ci.yml, and -update strips the release credentials out of it — a worse outcome than the drift it is meant to catch.
Read the plugin options from frontseat.yaml rather than passing nil; that removes the whole class, since every future option would otherwise reintroduce it.
There was a problem hiding this comment.
Fixed in 3a8911e. releaseFlowOptions(root) reads spec.plugins["frontseat/release-flow"].options from frontseat.yaml and passes them to Configure, so the render tracks the workspace config instead of hardcoded defaults — which removes the whole class, as you said: any option added later flows through automatically, and -update can't strip workspace settings out of the committed workflow. (On this stacked branch release-flow is currently {}, so it's a no-op today, but it's correct the moment bootstrap_from_source lands via the retarget onto main.)
Generated by Claude Code
| } | ||
| root := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", "..")) | ||
| if _, err := os.Stat(filepath.Join(root, ".github", "workflows", "ci.yml")); err != nil { | ||
| t.Skipf("repo workflow not present (hermetic sandbox): %v", err) |
There was a problem hiding this comment.
This skip is what stops the test contributing anything in CI.
Under frontseat build // the hermetic go:test action stages the package inputs, not the repo workflow — so ci.yml is never present, os.Stat fails, and the test skips. Every run. Silently, since go test prints skips only under -v.
A second mechanism defeats it even if the workflow were staged: frontseat-plugin-go/tasks.go sets GOFLAGS: -mod=mod -trimpath -modcacherw, so runtime.Caller(0) returns a module-relative path and filepath.Dir(thisFile)/../.. resolves to github.com/frontseat/frontseat. Plain go test passes; GOFLAGS=-trimpath go test skips. Locating the workflow relative to the process CWD is the prerequisite for either fix.
Net CI enforcement from this test is zero. The enforcement in CI is github:sync, which renders the template from the installed plugin and byte-compares — reporting out-of-sync when the file differs or is missing, so it catches hand edits and deletion alike. What this test adds over that is a source-level check immune to plugin-version skew, and only for developers who opt into hk by hand out of a gitignored mise.local.toml.
Same mechanism gives a second hole: deleting .github/workflows/ci.yml also passes this test. os.Stat fails, skip, green. github:sync still catches it, so nothing is unguarded — but a guard whose stated job is the generated workflow should not go quiet on the one edit that removes it.
There was a problem hiding this comment.
Fixed in 3a8911e. The test now locates the repo by walking up to go.work from the process CWD (repoRoot()), not runtime.Caller, so GOFLAGS=-trimpath no longer sends it to a module-relative path that never resolves — it ran green by skipping locally too, not just in the sandbox. And the skip logic is inverted from what let deletion pass: it skips only when go.work is unreachable (genuine hermetic sandbox); once the root is found, a missing ci.yml produces drift → t.Fatalf, so deleting the workflow now fails. Verified it runs (not skips) under a plain go test.
Agreed this is a source-level guard for local runs (hk / go test) complementing github:sync in CI, not a CI check itself — the doc comment now says so.
Generated by Claude Code
There was a problem hiding this comment.
Requesting changes — the guard adds no CI enforcement, and it fails against the repo's own plugin config.
Three inline, by severity:
Configure(ctx, nil)ignores the repo's real config.frontseat.yamlsetsrelease_secretsandbootstrap_from_sourceon this plugin; rendering with defaults drops the bootstrap step and all three release-secret env blocks, so the test fails against the committed workflow and-updatestrips those credentials out of it.- This test contributes zero CI enforcement.
ci.ymlis never staged into the hermeticgo:testsandbox, so it skips every run, silently. Deletingci.ymlalso skips it green. - Not gofmt-clean —
go:formatfails. One-click suggestion attached.
Smaller: the hk glob .github/workflows/*.yml also matches the hand-written onboarding-eval.yml; and hk is absent from the project mise.toml while mise.local.toml is gitignored, so this runs only for developers who opt in by hand.
The scope trim to the repo-side guard is right, and deferring generator-side fingerprinting to #248 is correct.
This review was published with assistance from Claude.
…ally run Address review: the guard skipped on every run and rendered against the wrong config. - Locate the repo by walking up to go.work from the process CWD instead of runtime.Caller, which GOFLAGS=-trimpath (set by the go plugin) rewrites to a module-relative path that never resolves — the test skipped locally too, not only in the hermetic sandbox. Skip only when go.work is genuinely unreachable; once the root is found a missing ci.yml is drift, not a green skip, so deleting the workflow now fails the test. - Render with the plugin's real options read from frontseat.yaml (spec.plugins["frontseat/release-flow"].options) rather than nil, so the render matches what `frontseat sync` produces and -update can't strip workspace-configured settings out of the committed workflow. - gofmt the drift message; drop the redundant `frontseat daemon restart` (mise dev already restarts it). - Narrow the hk glob to .github/workflows/ci.yml so it no longer fires on the hand-written onboarding-eval.yml. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MYAgCD9HZHZWEuH4vF17p1
721de1c to
d27aeef
Compare
|
Apologies — I closed this by accident. I merged #246 with It's back now, with the review history and your authorship intact. Restoring the deleted ref is what makes that possible, and the order matters — a closed PR cannot be retargeted, and a PR whose base is missing cannot be reopened: gh api -X POST repos/OWNER/REPO/git/refs -f ref=refs/heads/DELETED_BASE -f sha=MERGED_HEAD_SHA
gh pr reopen N
gh api -X PATCH repos/OWNER/REPO/pulls/N -f base=main
gh api -X DELETE repos/OWNER/REPO/git/refs/heads/DELETED_BASEIt is on I checked the cause rather than guessing: two identical stacks in a scratch repo, one base merged with On the review: everything looks addressed at This comment was published with assistance from Claude. |
The repository already deletes merged branches, and letting it do so is what preserves stacked pull requests: GitHub retargets any PR based on the merged branch onto its base. Deleting the ref from the client pre-empts that step, and GitHub closes the dependent PR instead — permanently, since a closed PR cannot be reopened while its base is missing and cannot be retargeted while it is closed. This closed #247, which was stacked on #246. Verified in a scratch repository with two identical stacks differing only in the delete method: the flagged one left its dependent PR closed and stranded, the other retargeted cleanly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The repository already deletes merged branches, and letting it do so is what preserves stacked pull requests: GitHub retargets any PR based on the merged branch onto its base. Deleting the ref from the client pre-empts that step, and GitHub closes the dependent PR instead — permanently, since a closed PR cannot be reopened while its base is missing and cannot be retargeted while it is closed. This closed #247, which was stacked on #246. Verified in a scratch repository with two identical stacks differing only in the delete method: the flagged one left its dependent PR closed and stranded, the other retargeted cleanly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stacked on #246 (merge that first, then retarget this to main). Scope deliberately trimmed to the repo-side guard — the generator-side fix (framework-owned fingerprint/content-hash stamping so sync can classify drift without rendering) is designed in #248 and belongs in the plugin-API track with #205.
TestRepoWorkflowInSync: renders the workflow from the checked-out template source and byte-compares against the committedci.yml. Immune to installed-plugin version skew (the failure mode that misreported drift today).-updateregenerates the file daemon-free.hk.pkl): runs that test on any commit touching.github/workflows/*.ymlor the release-flow plugin sources — catches hand edits to the generated file and template changes committed without regenerating. Verified live both ways. hk itself stays a local dev tool (mise.local.toml, gitignored; runhk installonce) — the project toolchain inmise.tomlis untouched.mise dev→frontseat daemon restart→frontseat sync --fix, commit both) and the version-skew trap.🤖 Generated with Claude Code
Fixes #168