Skip to content

fix(wfctl): bootstrapSecrets returns rotation results for force-rotated keys (v0.27.3)#594

Merged
intel352 merged 5 commits into
mainfrom
fix/bootstrap-rotation-count
May 10, 2026
Merged

fix(wfctl): bootstrapSecrets returns rotation results for force-rotated keys (v0.27.3)#594
intel352 merged 5 commits into
mainfrom
fix/bootstrap-rotation-count

Conversation

@intel352
Copy link
Copy Markdown
Contributor

Summary

Closes false-negative rotate-and-prune: expected 1 rotation result, got 0 from core-dump rotate-spaces-key staging dispatch run 25616807427 (2026-05-09). Rotation succeeded operationally — DO API minted a new key, GH Secret SPACES_access_key was updated, WFCTL_NEW_KEY_CREATED_AT sidecar was emitted to stderr — but bootstrapSecrets returned an empty rotations slice, so rotate-and-prune errored out after side effects already committed. Operator left with a freshly-rotated credential, the old credential still live, and no automated cleanup path.

Root cause

wfctl infra rotate-and-prune --name <RESOURCE_NAME> builds forceRotate := map[string]bool{name: true} keyed by the cloud-side resource name (per ADR 0023 + docs/runbooks/spaces-key-prune.md; matches secrets.generate[].name). But bootstrapSecrets checks forceRotate[gen.Key] keyed by the canonical generator key (e.g. SPACES).

For core-dump's infra.yaml (key: SPACES, name: coredump-deploy-key), forceRotate["coredump-deploy-key"]=true but bootstrapSecrets checks forceRotate["SPACES"]=false → force-rotate code path silently bypassed → "normal" first-time-create path runs (printing created instead of rotated) → side effects commit → rotations slice never appended.

Fix

Two-layer architectural fix:

  1. CLI translation (primary)runInfraRotateAndPrune now uses a new buildRotateAndPruneForceRotateSet helper that translates --namegen.Key via secrets.generate[].name match (fallback to .key for legacy configs), with a fast-fail typo guard mirroring buildForceRotateSet in infra_bootstrap.go.
  2. Strict-contract guard (defense in depth)bootstrapSecrets now pre-validates that every forceRotate entry matches a known secrets.generate[].Key, erroring loud if not. Per workspace memory feedback_proper_fixes_over_workarounds and the user mandate "no soft fallback paths": never silently absorb the contract violation.

Files

  • cmd/wfctl/infra_bootstrap.go — strict-contract guard at top of bootstrapSecrets
  • cmd/wfctl/infra_rotate_and_prune.gobuildRotateAndPruneForceRotateSet helper + CLI call-site update
  • cmd/wfctl/infra_bootstrap_rotation_count_test.go (new) — failing-side test reproducing the staging-dispatch shape (key=SPACES, name=coredump-deploy-key) + bootstrapSecrets unit-level contract test
  • cmd/wfctl/infra_rotate_and_prune_test.go — fixture extended with a second generate entry so existing tests' --name canonical-name continues working (previously masked by stub-bootstrapSecrets absorption)

Test plan

  • Failing test reproduces the bug on origin/main code (verified via stash)
  • Both new tests pass after fix
  • All existing cmd/wfctl/ tests pass: GOWORK=off go test ./cmd/wfctl/... -count=1 => ok
  • Full repo test suite passes: GOWORK=off go test ./... => 0 failures
  • Lint clean: GOWORK=off golangci-lint run ./cmd/wfctl/... => 0 issues
  • Race tests pass: GOWORK=off go test -race ./cmd/wfctl/ -count=1 -run "ForceRotate|RotateAndPrune|BootstrapSecrets" => ok
  • Real-world re-dispatch of core-dump rotate-spaces-key workflow against wfctl@v0.27.3 confirms the false-negative is closed (orchestrator validation post-merge)

🤖 Generated with Claude Code

intel352 and others added 2 commits May 10, 2026 00:01
…s bug

Adds two TDD-style tests pinning the rotation-result contract that
broke in core-dump rotate-spaces-key staging dispatch run 25616807427
(2026-05-09):

  Step 1: rotating credential "coredump-deploy-key" (type infra.spaces_key)...
    secret "SPACES_access_key": created
  WFCTL_NEW_KEY_CREATED_AT=2026-05-10T01:41:58Z
  rotate-and-prune: expected 1 rotation result, got 0

Root cause: `wfctl infra rotate-and-prune --name <RESOURCE_NAME>` builds
forceRotate keyed by the cloud-side resource name (per ADR 0023 +
docs/runbooks/spaces-key-prune.md), but bootstrapSecrets checks
forceRotate[gen.Key] keyed by the canonical generator key. Mismatch =>
force-rotate code path silently bypassed for that gen, "normal" first-
time-create path runs instead, prints "created" instead of "rotated",
and crucially never appends to the rotations slice. Side effects (DO
API mint + GH Secrets store + WFCTL_NEW_KEY_CREATED_AT sidecar) commit
under the disguise of a normal create; rotate-and-prune then errors
out before pruning, leaving the operator with a freshly-rotated
credential, the old credential still live, and no automated cleanup
path.

The first test exercises the full runInfraRotateAndPrune integration
with key="SPACES" + name="coredump-deploy-key" (the exact shape of
core-dump's infra.yaml on the failing run) and asserts code==0 with a
populated rotations result. Existing rotate-and-prune tests stub
bootstrapSecrets entirely, so they never hit the real keying contract.

The second test pins the bootstrapSecrets unit contract: when
forceRotate is properly keyed by gen.Key AND the underlying
generator+Set commits, rotations MUST contain a RotationResult. This
holds regardless of which architectural fix lands (CLI-side translation
or bootstrapSecrets-side multi-key tolerance).

Per workspace memories: feedback_proper_fixes_over_workarounds (no
soft fallbacks), feedback_force_strict_contracts_no_compat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ed keys (v0.27.3)

Closes the false-negative "expected 1 rotation result, got 0" surfaced
in core-dump rotate-spaces-key staging dispatch run 25616807427
(2026-05-09). Two-layer architectural fix:

1. CLI translation (primary fix). runInfraRotateAndPrune now uses a new
   buildRotateAndPruneForceRotateSet helper that translates the
   cloud-side --name (per ADR 0023 + docs/runbooks/spaces-key-prune.md;
   matches secrets.generate[].name) into the canonical gen.Key values
   that bootstrapSecrets keys forceRotate by. Match precedence:
   .name first, fallback to .key for configs that omit .name. Rejects
   unknown names with a fast-fail error message — same UX shape as
   buildForceRotateSet's typo guard for `wfctl infra bootstrap
   --force-rotate`. The CLI surface is the architecturally correct
   place for this translation: the --name flag is documented to take
   the cloud-side name, and bootstrapSecrets rightly keys by the
   canonical generator identity.

2. Strict-contract guard (defense in depth). bootstrapSecrets now
   pre-validates that every forceRotate entry matches a known
   secrets.generate[].Key, returning an error before touching the
   store if not. This prevents future callers from silently bypassing
   the force-rotate code path when they pass mismatched keys — the
   exact shape of the staging-dispatch bug. Per workspace memory
   feedback_proper_fixes_over_workarounds and the user mandate "no
   soft fallback paths": the guard errors LOUD rather than tolerating
   the mismatch, ensuring callers get a structured error instead of a
   committed-side-effect-with-empty-result false-negative.

Test fixture writeMinimalRotationConfig is extended with a second
generate entry whose key != name so existing rotate-and-prune tests
that pass --name canonical-name continue to work; previously those
tests relied on stub-bootstrapSecrets to absorb the keying mismatch,
which masked the bug.

Per workspace memories: feedback_proper_fixes_over_workarounds,
feedback_force_strict_contracts_no_compat,
feedback_no_invented_interfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 10, 2026 04:03
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes wfctl infra rotate-and-prune returning a false-negative rotation result when --name is a cloud-side resource name (secrets.generate[].name) but bootstrapSecrets keys force-rotate by canonical generator key (secrets.generate[].key). Adds a defense-in-depth contract check in bootstrapSecrets and regression tests covering the staging failure shape.

Changes:

  • Add buildRotateAndPruneForceRotateSet to translate --name (resource name) into secrets.generate[].Key before calling bootstrapSecrets.
  • Add a strict-contract guard in bootstrapSecrets to error if any forceRotate key doesn’t match secrets.generate[].Key.
  • Add/adjust tests and fixtures to reproduce and prevent the “expected 1 rotation result, got 0” false-negative.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
cmd/wfctl/infra_rotate_and_prune.go Adds --namegen.Key translation helper and uses it before rotation.
cmd/wfctl/infra_rotate_and_prune_test.go Extends the minimal fixture to include a second secrets.generate entry for name→key translation coverage.
cmd/wfctl/infra_bootstrap.go Adds strict validation that forceRotate keys must match secrets.generate[].Key.
cmd/wfctl/infra_bootstrap_rotation_count_test.go Adds regression tests reproducing key/name mismatch and asserting rotation results are surfaced.

Comment thread cmd/wfctl/infra_rotate_and_prune.go Outdated
Comment on lines +128 to +141
// TestBootstrapSecrets_ForceRotate_NameOnlyMatch_StillRotatesAndAppends
// is the unit-level lock for the keying contract: when force-rotate
// contains an entry that matches secrets.generate[].name (NOT .key),
// rotation must still run and rotations must be appended. Without this,
// the rotate-and-prune integration is fragile to internal CLI changes.
//
// This test is the architecturally-correct contract for the bug. It can
// be satisfied by either:
// - bootstrapSecrets accepting forceRotate keyed by name OR key, OR
// - runInfraRotateAndPrune translating name→key before the call.
//
// Whichever fix lands, this test pins the invariant: a successful
// force-rotate where Set committed must produce a RotationResult.
func TestBootstrapSecrets_ForceRotate_AppendsRotationResultEvenWhenStoreEmpty(t *testing.T) {
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 10, 2026

⏱ Benchmark Results

No significant performance regressions detected.

benchstat comparison (baseline → PR)
## benchstat: baseline → PR
baseline-bench.txt:262: parsing iteration count: invalid syntax
baseline-bench.txt:352157: parsing iteration count: invalid syntax
baseline-bench.txt:630762: parsing iteration count: invalid syntax
baseline-bench.txt:896224: parsing iteration count: invalid syntax
baseline-bench.txt:1229858: parsing iteration count: invalid syntax
baseline-bench.txt:1546437: parsing iteration count: invalid syntax
benchmark-results.txt:262: parsing iteration count: invalid syntax
benchmark-results.txt:364091: parsing iteration count: invalid syntax
benchmark-results.txt:658608: parsing iteration count: invalid syntax
benchmark-results.txt:965776: parsing iteration count: invalid syntax
benchmark-results.txt:1252868: parsing iteration count: invalid syntax
benchmark-results.txt:1584246: parsing iteration count: invalid syntax
goos: linux
goarch: amd64
pkg: github.com/GoCodeAlone/workflow/dynamic
cpu: AMD EPYC 7763 64-Core Processor                
                            │ baseline-bench.txt │        benchmark-results.txt        │
                            │       sec/op       │    sec/op      vs base              │
InterpreterCreation-4              3.959m ± 160%   4.224m ± 153%       ~ (p=0.699 n=6)
ComponentLoad-4                    3.577m ±   2%   3.557m ±  13%       ~ (p=0.310 n=6)
ComponentExecute-4                 1.934µ ±   2%   1.924µ ±   1%       ~ (p=0.084 n=6)
PoolContention/workers-1-4         1.098µ ±   3%   1.083µ ±   4%       ~ (p=0.169 n=6)
PoolContention/workers-2-4         1.095µ ±   2%   1.075µ ±   2%       ~ (p=0.069 n=6)
PoolContention/workers-4-4         1.097µ ±   1%   1.085µ ±   2%  -1.09% (p=0.013 n=6)
PoolContention/workers-8-4         1.107µ ±   1%   1.081µ ±   1%  -2.35% (p=0.002 n=6)
PoolContention/workers-16-4        1.108µ ±   2%   1.080µ ±   1%  -2.53% (p=0.002 n=6)
ComponentLifecycle-4               3.706m ±   1%   3.576m ±   1%  -3.50% (p=0.002 n=6)
SourceValidation-4                 2.425µ ±   2%   2.295µ ±   1%  -5.36% (p=0.002 n=6)
RegistryConcurrent-4               875.5n ±   4%   810.9n ±   5%  -7.38% (p=0.002 n=6)
LoaderLoadFromString-4             3.719m ±   0%   3.585m ±   1%  -3.62% (p=0.002 n=6)
geomean                            18.17µ          17.81µ         -2.00%

                            │ baseline-bench.txt │        benchmark-results.txt         │
                            │        B/op        │     B/op      vs base                │
InterpreterCreation-4               2.027Mi ± 0%   2.027Mi ± 0%       ~ (p=0.669 n=6)
ComponentLoad-4                     2.180Mi ± 0%   2.180Mi ± 0%       ~ (p=0.985 n=6)
ComponentExecute-4                  1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-1-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-2-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-4-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-8-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-16-4         1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
ComponentLifecycle-4                2.183Mi ± 0%   2.183Mi ± 0%       ~ (p=0.117 n=6)
SourceValidation-4                  1.984Ki ± 0%   1.984Ki ± 0%       ~ (p=1.000 n=6) ¹
RegistryConcurrent-4                1.133Ki ± 0%   1.133Ki ± 0%       ~ (p=1.000 n=6) ¹
LoaderLoadFromString-4              2.182Mi ± 0%   2.182Mi ± 0%       ~ (p=0.675 n=6)
geomean                             15.25Ki        15.25Ki       -0.00%
¹ all samples are equal

                            │ baseline-bench.txt │        benchmark-results.txt        │
                            │     allocs/op      │  allocs/op   vs base                │
InterpreterCreation-4                15.68k ± 0%   15.68k ± 0%       ~ (p=1.000 n=6)
ComponentLoad-4                      18.02k ± 0%   18.02k ± 0%       ~ (p=1.000 n=6)
ComponentExecute-4                    25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-1-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-2-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-4-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-8-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-16-4           25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
ComponentLifecycle-4                 18.07k ± 0%   18.07k ± 0%       ~ (p=1.000 n=6) ¹
SourceValidation-4                    32.00 ± 0%    32.00 ± 0%       ~ (p=1.000 n=6) ¹
RegistryConcurrent-4                  2.000 ± 0%    2.000 ± 0%       ~ (p=1.000 n=6) ¹
LoaderLoadFromString-4               18.06k ± 0%   18.06k ± 0%       ~ (p=1.000 n=6) ¹
geomean                               183.3         183.3       +0.00%
¹ all samples are equal

pkg: github.com/GoCodeAlone/workflow/middleware
                                  │ baseline-bench.txt │       benchmark-results.txt       │
                                  │       sec/op       │   sec/op     vs base              │
CircuitBreakerDetection-4                  284.0n ± 2%   286.0n ± 4%       ~ (p=0.121 n=6)
CircuitBreakerExecution_Success-4          21.50n ± 0%   21.48n ± 1%       ~ (p=0.727 n=6)
CircuitBreakerExecution_Failure-4          66.19n ± 0%   66.15n ± 0%       ~ (p=0.465 n=6)
geomean                                    73.93n        74.07n       +0.19%

                                  │ baseline-bench.txt │       benchmark-results.txt        │
                                  │        B/op        │    B/op     vs base                │
CircuitBreakerDetection-4                 144.0 ± 0%     144.0 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Success-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Failure-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                              ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                  │ baseline-bench.txt │       benchmark-results.txt        │
                                  │     allocs/op      │ allocs/op   vs base                │
CircuitBreakerDetection-4                 1.000 ± 0%     1.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Success-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Failure-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                              ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/module
                                 │ baseline-bench.txt │        benchmark-results.txt        │
                                 │       sec/op       │    sec/op      vs base              │
JQTransform_Simple-4                     880.3n ± 27%   867.8n ±  29%       ~ (p=0.180 n=6)
JQTransform_ObjectConstruction-4         1.453µ ±  1%   1.449µ ±   2%       ~ (p=0.225 n=6)
JQTransform_ArraySelect-4                3.332µ ±  1%   3.282µ ±   0%  -1.52% (p=0.002 n=6)
JQTransform_Complex-4                    38.24µ ±  1%   37.61µ ±   0%  -1.63% (p=0.002 n=6)
JQTransform_Throughput-4                 1.777µ ±  0%   1.769µ ±   0%  -0.45% (p=0.011 n=6)
SSEPublishDelivery-4                     64.40n ±  1%   66.96n ± 118%       ~ (p=0.370 n=6)
geomean                                  1.628µ         1.624µ         -0.24%

                                 │ baseline-bench.txt │        benchmark-results.txt         │
                                 │        B/op        │     B/op      vs base                │
JQTransform_Simple-4                   1.273Ki ± 0%     1.273Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ObjectConstruction-4       1.773Ki ± 0%     1.773Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ArraySelect-4              2.625Ki ± 0%     2.625Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Complex-4                  16.22Ki ± 0%     16.22Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Throughput-4               1.984Ki ± 0%     1.984Ki ± 0%       ~ (p=1.000 n=6) ¹
SSEPublishDelivery-4                     0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                             ²                 +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                 │ baseline-bench.txt │       benchmark-results.txt        │
                                 │     allocs/op      │ allocs/op   vs base                │
JQTransform_Simple-4                     10.00 ± 0%     10.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ObjectConstruction-4         15.00 ± 0%     15.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ArraySelect-4                30.00 ± 0%     30.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Complex-4                    324.0 ± 0%     324.0 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Throughput-4                 17.00 ± 0%     17.00 ± 0%       ~ (p=1.000 n=6) ¹
SSEPublishDelivery-4                     0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                             ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/schema
                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │       sec/op       │    sec/op     vs base              │
SchemaValidation_Simple-4                   1.119µ ± 13%   1.089µ ±  2%       ~ (p=0.065 n=6)
SchemaValidation_AllFields-4                1.666µ ±  2%   1.679µ ± 13%       ~ (p=0.310 n=6)
SchemaValidation_FormatValidation-4         1.590µ ±  1%   1.597µ ±  2%       ~ (p=0.563 n=6)
SchemaValidation_ManySchemas-4              1.774µ ±  4%   1.852µ ±  4%  +4.40% (p=0.015 n=6)
geomean                                     1.514µ         1.525µ        +0.70%

                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │        B/op        │    B/op     vs base                │
SchemaValidation_Simple-4                   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_AllFields-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_FormatValidation-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_ManySchemas-4              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │     allocs/op      │ allocs/op   vs base                │
SchemaValidation_Simple-4                   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_AllFields-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_FormatValidation-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_ManySchemas-4              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/store
                                   │ baseline-bench.txt │       benchmark-results.txt        │
                                   │       sec/op       │    sec/op     vs base              │
EventStoreAppend_InMemory-4                1.215µ ± 10%   1.160µ ± 24%       ~ (p=1.000 n=6)
EventStoreAppend_SQLite-4                  1.280m ±  4%   1.206m ±  5%  -5.76% (p=0.015 n=6)
GetTimeline_InMemory/events-10-4           13.79µ ±  2%   13.45µ ±  2%  -2.47% (p=0.009 n=6)
GetTimeline_InMemory/events-50-4           76.19µ ± 18%   74.84µ ±  3%       ~ (p=0.589 n=6)
GetTimeline_InMemory/events-100-4          125.1µ ±  1%   120.7µ ±  1%  -3.52% (p=0.002 n=6)
GetTimeline_InMemory/events-500-4          643.2µ ±  1%   620.0µ ±  0%  -3.61% (p=0.002 n=6)
GetTimeline_InMemory/events-1000-4         1.316m ±  1%   1.267m ±  1%  -3.72% (p=0.002 n=6)
GetTimeline_SQLite/events-10-4             106.3µ ±  1%   105.0µ ±  1%  -1.19% (p=0.002 n=6)
GetTimeline_SQLite/events-50-4             251.1µ ±  0%   245.2µ ±  1%  -2.34% (p=0.002 n=6)
GetTimeline_SQLite/events-100-4            427.1µ ±  1%   415.2µ ±  1%  -2.77% (p=0.002 n=6)
GetTimeline_SQLite/events-500-4            1.830m ±  0%   1.771m ±  2%  -3.25% (p=0.002 n=6)
GetTimeline_SQLite/events-1000-4           3.565m ±  0%   3.459m ±  0%  -2.95% (p=0.002 n=6)
geomean                                    220.0µ         213.0µ        -3.16%

                                   │ baseline-bench.txt │         benchmark-results.txt         │
                                   │        B/op        │     B/op       vs base                │
EventStoreAppend_InMemory-4                  802.0 ± 6%     757.0 ± 10%       ~ (p=0.093 n=6)
EventStoreAppend_SQLite-4                  1.987Ki ± 2%   1.985Ki ±  1%       ~ (p=0.736 n=6)
GetTimeline_InMemory/events-10-4           7.953Ki ± 0%   7.953Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-50-4           46.62Ki ± 0%   46.62Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-100-4          94.48Ki ± 0%   94.48Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-500-4          472.8Ki ± 0%   472.8Ki ±  0%  +0.00% (p=0.015 n=6)
GetTimeline_InMemory/events-1000-4         944.3Ki ± 0%   944.3Ki ±  0%  +0.00% (p=0.006 n=6)
GetTimeline_SQLite/events-10-4             16.74Ki ± 0%   16.74Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-50-4             87.14Ki ± 0%   87.14Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-100-4            175.4Ki ± 0%   175.4Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-500-4            846.1Ki ± 0%   846.1Ki ±  0%  -0.00% (p=0.022 n=6)
GetTimeline_SQLite/events-1000-4           1.639Mi ± 0%   1.639Mi ±  0%       ~ (p=0.253 n=6)
geomean                                    67.44Ki        67.11Ki        -0.49%
¹ all samples are equal

                                   │ baseline-bench.txt │        benchmark-results.txt        │
                                   │     allocs/op      │  allocs/op   vs base                │
EventStoreAppend_InMemory-4                  7.000 ± 0%    7.000 ± 0%       ~ (p=1.000 n=6) ¹
EventStoreAppend_SQLite-4                    53.00 ± 0%    53.00 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-10-4             125.0 ± 0%    125.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-50-4             653.0 ± 0%    653.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-100-4           1.306k ± 0%   1.306k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-500-4           6.514k ± 0%   6.514k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-1000-4          13.02k ± 0%   13.02k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-10-4               382.0 ± 0%    382.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-50-4              1.852k ± 0%   1.852k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-100-4             3.681k ± 0%   3.681k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-500-4             18.54k ± 0%   18.54k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-1000-4            37.29k ± 0%   37.29k ± 0%       ~ (p=1.000 n=6) ¹
geomean                                     1.162k        1.162k       +0.00%
¹ all samples are equal

Benchmarks run with go test -bench=. -benchmem -count=6.
Regressions ≥ 20% are flagged. Results compared via benchstat.

intel352 and others added 2 commits May 10, 2026 06:35
…view

Addresses Copilot findings on PR 594:

1. buildRotateAndPruneForceRotateSet (cmd/wfctl/infra_rotate_and_prune.go:185)
   was permissive: multi-Name match returned multiple Keys, and
   non-provider_credential matches were accepted. Both classes reintroduced
   the false-negative-after-side-effects failure mode this PR was opened to
   fix:
   - multi-Name → multi-rotate → fails len(rotations)==1 after side effects
   - non-provider_credential → rotates without appending RotationResult

   Helper now enforces a strict contract:
   - exactly one matching gen entry (zero or two-plus reject with named keys)
   - matched gen MUST be type=provider_credential

2. TestBootstrapSecrets_ForceRotate doc comment claimed bootstrapSecrets
   accepted forceRotate keyed by either .name OR .key. Actual implementation
   strictly requires .Key. Comment updated to document strict contract.

Adds TestBuildRotateAndPruneForceRotateSet covering 8 cases including
multi-match + non-provider_credential rejection.

Local tests PASS.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 10, 2026 10:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Comment thread cmd/wfctl/infra_bootstrap_rotation_count_test.go Outdated
Comment thread cmd/wfctl/infra_bootstrap_rotation_count_test.go Outdated
Comment on lines +226 to +229
if matched[0].Type != "provider_credential" {
return nil, fmt.Errorf("--name %q matches secrets.generate entry of type %q; rotate-and-prune only operates on provider_credential entries (use `wfctl infra bootstrap --force-rotate` for other rotatable types)", name, matched[0].Type)
}
return map[string]bool{matched[0].Key: true}, nil
…ilot

Three Copilot findings on PR 594 round 2:

1. infra_rotate_and_prune.go: buildRotateAndPruneForceRotateSet caught
   multi-Name collisions but missed multi-Key-with-different-Names.
   Two gens with same .Key (different Names) → forceRotate[key]=true →
   bootstrapSecrets rotates BOTH → fails len(rotations)==1 after side
   effects committed (the same false-negative class via Key collision
   instead of Name collision). Added defense-in-depth Key-uniqueness
   check that rejects with named collision list.

2. infra_bootstrap_rotation_count_test.go: doc comment header listed a
   stale test name (TestBootstrapSecrets_ForceRotate_AppendsRotationResult_NameKeyMismatch)
   while the actual function is TestRotateAndPrune_KeyNameMismatch_ReturnsRotationResult.
   Synced.

3. infra_bootstrap_rotation_count_test.go: comment said the fix could be
   satisfied by "CLI translation OR bootstrapSecrets accepting both
   keying forms". Actual contract: bootstrapSecrets strictly requires
   forceRotate keyed by gen.Key (rejects unknown entries); CLI does the
   translation at buildRotateAndPruneForceRotateSet. Updated to match.

Adds matched_key_shared_with_another_gen_rejected test case to
TestBuildRotateAndPruneForceRotateSet covering the new defense-in-depth
guard. All 9 cases PASS.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@intel352 intel352 merged commit 11f3c56 into main May 10, 2026
20 checks passed
@intel352 intel352 deleted the fix/bootstrap-rotation-count branch May 10, 2026 11:26
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.

2 participants