Skip to content

chore(frontseat-plugin-release-flow): GH-168 guard the generated workflow - #247

Open
jbadeau wants to merge 3 commits into
mainfrom
chore/168-workflow-sync-guard
Open

chore(frontseat-plugin-release-flow): GH-168 guard the generated workflow#247
jbadeau wants to merge 3 commits into
mainfrom
chore/168-workflow-sync-guard

Conversation

@jbadeau

@jbadeau jbadeau commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

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 committed ci.yml. Immune to installed-plugin version skew (the failure mode that misreported drift today). -update regenerates the file daemon-free.
  • hk pre-commit step (hk.pkl): runs that test on any commit touching .github/workflows/*.yml or 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; run hk install once) — the project toolchain in mise.toml is untouched.
  • CLAUDE.md: documents the rule (sync-generated files are never hand-edited; template change → mise devfrontseat daemon restartfrontseat sync --fix, commit both) and the version-skew trap.

🤖 Generated with Claude Code

Fixes #168

Jose Badeau and others added 2 commits July 28, 2026 19:38
… 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>
@jbadeau jbadeau changed the title feat(frontseat-plugin-release-flow): GH-168 content-hash stamp + sync guard chore(frontseat-plugin-release-flow): GH-168 guard the generated workflow Jul 28, 2026
@jbadeau
jbadeau force-pushed the chore/168-workflow-sync-guard branch from e9ae59c to 31bc9c2 Compare July 28, 2026 18:50
jbadeau pushed a commit that referenced this pull request Jul 28, 2026
The title GH-<issue> prefix resolves to nothing — no autolink is configured,
so only a body reference links a PR to its issue. #247 demonstrates it: a
GH-168 title, no body mention, and absent from #168's timeline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment on lines +55 to +57
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"+

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: not gofmt-clean, so go:format fails. gofmt wants a space before each + when the operands are on separate lines.

Suggested change
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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both done in 3a8911egofmt -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

Comment on lines +36 to +39
// 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 {

@joaodinissf joaodinissf Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: true

Both 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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

@joaodinissf joaodinissf Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@joaodinissf joaodinissf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — the guard adds no CI enforcement, and it fails against the repo's own plugin config.

Three inline, by severity:

  1. Configure(ctx, nil) ignores the repo's real config. frontseat.yaml sets release_secrets and bootstrap_from_source on 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 -update strips those credentials out of it.
  2. This test contributes zero CI enforcement. ci.yml is never staged into the hermetic go:test sandbox, so it skips every run, silently. Deleting ci.yml also skips it green.
  3. Not gofmt-cleango:format fails. 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
@jbadeau
jbadeau force-pushed the fix/168-ci-go-cache branch from 721de1c to d27aeef Compare July 30, 2026 08:55
@joaodinissf
joaodinissf deleted the branch main July 30, 2026 16:06
@joaodinissf

joaodinissf commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Apologies — I closed this by accident. I merged #246 with gh pr merge --delete-branch, which deletes the base branch, and GitHub closes any PR stacked on it rather than retargeting.

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_BASE

It is on main now and mergeable — 721de1c drops out automatically, since git rebase skips commits whose patch-id already exists upstream.

I checked the cause rather than guessing: two identical stacks in a scratch repo, one base merged with --delete-branch and the other without. The first was closed and stranded; the second retargeted cleanly and stayed open. #275 puts the rule and the recipe in CLAUDE.md.

On the review: everything looks addressed at 3a8911erepoRoot() walking to go.work fixes the -trimpath case, deleting ci.yml now fails rather than skipping, and releaseFlowOptions reading frontseat.yaml removes the whole class.

This comment was published with assistance from Claude.

@joaodinissf joaodinissf reopened this Jul 30, 2026
@joaodinissf
joaodinissf changed the base branch from fix/168-ci-go-cache to main July 30, 2026 19:20
joaodinissf added a commit that referenced this pull request Jul 30, 2026
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>
@joaodinissf joaodinissf reopened this Jul 30, 2026
joaodinissf added a commit that referenced this pull request Jul 30, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci: Go module cache restore fails with thousands of tar 'File exists' errors

3 participants