Skip to content

fix(wfctl): bootstrap skips regeneration for write-only secret providers#417

Merged
intel352 merged 2 commits into
mainfrom
fix/bootstrap-secret-existence-write-only-providers
Apr 20, 2026
Merged

fix(wfctl): bootstrap skips regeneration for write-only secret providers#417
intel352 merged 2 commits into
mainfrom
fix/bootstrap-secret-existence-write-only-providers

Conversation

@intel352

Copy link
Copy Markdown
Contributor

Summary

`wfctl infra bootstrap` regenerated every secret on every run when the secrets provider was write-only (GitHub Actions). `GitHubSecretsProvider.Get` returns `ErrUnsupported`, and the bootstrap loop treated that as "unknown, regenerate" — which for `provider_credential` entries (e.g. `digitalocean.spaces`) created a new upstream credential every run. A consumer reported three orphaned `bmw-deploy-key` Spaces keys from three bootstrap invocations.

Fix

  • Fall back to `provider.List()` when `Get` returns `ErrUnsupported`.
  • Probe `_access_key` for `provider_credential` entries (that is the sub-key actually stored by bootstrap).
  • Preserve existing behaviour if both `Get` and `List` are unsupported (best-effort regenerate).
  • `List` is cached per bootstrap run so many-secret configs only hit the provider once.

Test plan

  • New unit tests in `cmd/wfctl/infra_bootstrap_secrets_test.go` cover: existing secret skipped, missing secret generated, `List` unsupported falls through to regenerate, `provider_credential` sub-key probe.
  • `go test ./cmd/wfctl/ -run 'Bootstrap|Secret'` passes.

🤖 Generated with Claude Code

bootstrapSecrets previously called provider.Get() to check whether a
secret already existed, then treated ErrUnsupported as "proceed to
regenerate". GitHub Actions secrets are write-only (Get always returns
ErrUnsupported), so every wfctl infra bootstrap run regenerated all
secrets. For provider_credential generators that reach out to cloud
APIs (e.g. digitalocean.spaces), this created a new upstream credential
on every run and left the previous ones orphaned — one repo reported
three duplicate DO Spaces access keys from three bootstrap invocations.

Fall back to provider.List() when Get returns ErrUnsupported, and probe
for <key>_access_key for provider_credential entries since that is the
sub-key actually stored. If List is also unsupported, preserve the
existing behaviour of regenerating (best-effort fallback).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 20, 2026 06:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 bootstrap repeatedly regenerating secrets when the configured secrets provider is write-only (e.g., GitHub Actions), preventing orphaned upstream credentials (notably for provider_credential sources like DigitalOcean Spaces).

Changes:

  • Update bootstrapSecrets to treat ErrUnsupported from Get as “check via List” (cached per bootstrap run) instead of “regenerate”.
  • Adjust existence probing for provider_credential generators to check the stored sub-key name (<key>_access_key).
  • Add unit tests covering write-only provider behavior and List fallback/unsupported cases.

Reviewed changes

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

File Description
cmd/wfctl/infra_bootstrap.go Adds GetList fallback with caching and probe-key logic for provider_credential secrets to avoid unnecessary regeneration.
cmd/wfctl/infra_bootstrap_secrets_test.go Introduces tests for write-only provider behavior, including skip/regenerate paths and probe-key behavior.

Comment thread cmd/wfctl/infra_bootstrap.go Outdated
Comment thread cmd/wfctl/infra_bootstrap.go Outdated
Comment thread cmd/wfctl/infra_bootstrap_secrets_test.go Outdated
@github-actions

github-actions Bot commented Apr 20, 2026

Copy link
Copy Markdown

⏱ Benchmark Results

No significant performance regressions detected.

benchstat comparison (baseline → PR)
## benchstat: baseline → PR
baseline-bench.txt:245: parsing iteration count: invalid syntax
baseline-bench.txt:347718: parsing iteration count: invalid syntax
baseline-bench.txt:680848: parsing iteration count: invalid syntax
baseline-bench.txt:1018592: parsing iteration count: invalid syntax
baseline-bench.txt:1349751: parsing iteration count: invalid syntax
baseline-bench.txt:1623226: parsing iteration count: invalid syntax
benchmark-results.txt:245: parsing iteration count: invalid syntax
benchmark-results.txt:358905: parsing iteration count: invalid syntax
benchmark-results.txt:717301: parsing iteration count: invalid syntax
benchmark-results.txt:1070264: parsing iteration count: invalid syntax
benchmark-results.txt:1445250: parsing iteration count: invalid syntax
benchmark-results.txt:1763938: parsing iteration count: invalid syntax
goos: linux
goarch: amd64
pkg: github.com/GoCodeAlone/workflow/dynamic
cpu: AMD EPYC 9V74 80-Core Processor                
                            │ baseline-bench.txt │
                            │       sec/op       │
InterpreterCreation-4              3.260m ± 174%
ComponentLoad-4                    3.456m ±   2%
ComponentExecute-4                 1.825µ ±   2%
PoolContention/workers-1-4         1.034µ ±   4%
PoolContention/workers-2-4         1.019µ ±   2%
PoolContention/workers-4-4         1.016µ ±   2%
PoolContention/workers-8-4         1.014µ ±   2%
PoolContention/workers-16-4        1.012µ ±   2%
ComponentLifecycle-4               3.467m ±   0%
SourceValidation-4                 2.068µ ±   9%
RegistryConcurrent-4               753.4n ±   2%
LoaderLoadFromString-4             3.509m ±   0%
geomean                            16.56µ

                            │ baseline-bench.txt │
                            │        B/op        │
InterpreterCreation-4               2.027Mi ± 0%
ComponentLoad-4                     2.180Mi ± 0%
ComponentExecute-4                  1.203Ki ± 0%
PoolContention/workers-1-4          1.203Ki ± 0%
PoolContention/workers-2-4          1.203Ki ± 0%
PoolContention/workers-4-4          1.203Ki ± 0%
PoolContention/workers-8-4          1.203Ki ± 0%
PoolContention/workers-16-4         1.203Ki ± 0%
ComponentLifecycle-4                2.183Mi ± 0%
SourceValidation-4                  1.984Ki ± 0%
RegistryConcurrent-4                1.133Ki ± 0%
LoaderLoadFromString-4              2.182Mi ± 0%
geomean                             15.25Ki

                            │ baseline-bench.txt │
                            │     allocs/op      │
InterpreterCreation-4                15.68k ± 0%
ComponentLoad-4                      18.02k ± 0%
ComponentExecute-4                    25.00 ± 0%
PoolContention/workers-1-4            25.00 ± 0%
PoolContention/workers-2-4            25.00 ± 0%
PoolContention/workers-4-4            25.00 ± 0%
PoolContention/workers-8-4            25.00 ± 0%
PoolContention/workers-16-4           25.00 ± 0%
ComponentLifecycle-4                 18.07k ± 0%
SourceValidation-4                    32.00 ± 0%
RegistryConcurrent-4                  2.000 ± 0%
LoaderLoadFromString-4               18.06k ± 0%
geomean                               183.3

cpu: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz
                            │ benchmark-results.txt │
                            │        sec/op         │
InterpreterCreation-4                 3.406m ± 195%
ComponentLoad-4                       3.518m ±   6%
ComponentExecute-4                    1.891µ ±   2%
PoolContention/workers-1-4            1.181µ ±   1%
PoolContention/workers-2-4            1.177µ ±   1%
PoolContention/workers-4-4            1.177µ ±   1%
PoolContention/workers-8-4            1.189µ ±   2%
PoolContention/workers-16-4           1.185µ ±   2%
ComponentLifecycle-4                  3.551m ±   3%
SourceValidation-4                    2.249µ ±   4%
RegistryConcurrent-4                  896.2n ±   2%
LoaderLoadFromString-4                3.618m ±  11%
geomean                               18.24µ

                            │ benchmark-results.txt │
                            │         B/op          │
InterpreterCreation-4                  2.027Mi ± 0%
ComponentLoad-4                        2.180Mi ± 0%
ComponentExecute-4                     1.203Ki ± 0%
PoolContention/workers-1-4             1.203Ki ± 0%
PoolContention/workers-2-4             1.203Ki ± 0%
PoolContention/workers-4-4             1.203Ki ± 0%
PoolContention/workers-8-4             1.203Ki ± 0%
PoolContention/workers-16-4            1.203Ki ± 0%
ComponentLifecycle-4                   2.183Mi ± 0%
SourceValidation-4                     1.984Ki ± 0%
RegistryConcurrent-4                   1.133Ki ± 0%
LoaderLoadFromString-4                 2.182Mi ± 0%
geomean                                15.25Ki

                            │ benchmark-results.txt │
                            │       allocs/op       │
InterpreterCreation-4                   15.68k ± 0%
ComponentLoad-4                         18.02k ± 0%
ComponentExecute-4                       25.00 ± 0%
PoolContention/workers-1-4               25.00 ± 0%
PoolContention/workers-2-4               25.00 ± 0%
PoolContention/workers-4-4               25.00 ± 0%
PoolContention/workers-8-4               25.00 ± 0%
PoolContention/workers-16-4              25.00 ± 0%
ComponentLifecycle-4                    18.07k ± 0%
SourceValidation-4                       32.00 ± 0%
RegistryConcurrent-4                     2.000 ± 0%
LoaderLoadFromString-4                  18.06k ± 0%
geomean                                  183.3

pkg: github.com/GoCodeAlone/workflow/middleware
cpu: AMD EPYC 9V74 80-Core Processor                
                                  │ baseline-bench.txt │
                                  │       sec/op       │
CircuitBreakerDetection-4                  296.5n ± 3%
CircuitBreakerExecution_Success-4          22.67n ± 0%
CircuitBreakerExecution_Failure-4          70.91n ± 1%
geomean                                    78.11n

                                  │ baseline-bench.txt │
                                  │        B/op        │
CircuitBreakerDetection-4                 144.0 ± 0%
CircuitBreakerExecution_Success-4         0.000 ± 0%
CircuitBreakerExecution_Failure-4         0.000 ± 0%
geomean                                              ¹
¹ summaries must be >0 to compute geomean

                                  │ baseline-bench.txt │
                                  │     allocs/op      │
CircuitBreakerDetection-4                 1.000 ± 0%
CircuitBreakerExecution_Success-4         0.000 ± 0%
CircuitBreakerExecution_Failure-4         0.000 ± 0%
geomean                                              ¹
¹ summaries must be >0 to compute geomean

cpu: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz
                                  │ benchmark-results.txt │
                                  │        sec/op         │
CircuitBreakerDetection-4                     456.9n ± 1%
CircuitBreakerExecution_Success-4             59.66n ± 0%
CircuitBreakerExecution_Failure-4             64.98n ± 1%
geomean                                       121.0n

                                  │ benchmark-results.txt │
                                  │         B/op          │
CircuitBreakerDetection-4                    144.0 ± 0%
CircuitBreakerExecution_Success-4            0.000 ± 0%
CircuitBreakerExecution_Failure-4            0.000 ± 0%
geomean                                                 ¹
¹ summaries must be >0 to compute geomean

                                  │ benchmark-results.txt │
                                  │       allocs/op       │
CircuitBreakerDetection-4                    1.000 ± 0%
CircuitBreakerExecution_Success-4            0.000 ± 0%
CircuitBreakerExecution_Failure-4            0.000 ± 0%
geomean                                                 ¹
¹ summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/module
cpu: AMD EPYC 9V74 80-Core Processor                
                                 │ baseline-bench.txt │
                                 │       sec/op       │
JQTransform_Simple-4                     929.3n ± 16%
JQTransform_ObjectConstruction-4         1.398µ ±  1%
JQTransform_ArraySelect-4                3.360µ ±  1%
JQTransform_Complex-4                    40.73µ ±  1%
JQTransform_Throughput-4                 1.726µ ±  1%
SSEPublishDelivery-4                     64.47n ±  1%
geomean                                  1.645µ

                                 │ baseline-bench.txt │
                                 │        B/op        │
JQTransform_Simple-4                   1.273Ki ± 0%
JQTransform_ObjectConstruction-4       1.773Ki ± 0%
JQTransform_ArraySelect-4              2.625Ki ± 0%
JQTransform_Complex-4                  16.22Ki ± 0%
JQTransform_Throughput-4               1.984Ki ± 0%
SSEPublishDelivery-4                     0.000 ± 0%
geomean                                             ¹
¹ summaries must be >0 to compute geomean

                                 │ baseline-bench.txt │
                                 │     allocs/op      │
JQTransform_Simple-4                     10.00 ± 0%
JQTransform_ObjectConstruction-4         15.00 ± 0%
JQTransform_ArraySelect-4                30.00 ± 0%
JQTransform_Complex-4                    324.0 ± 0%
JQTransform_Throughput-4                 17.00 ± 0%
SSEPublishDelivery-4                     0.000 ± 0%
geomean                                             ¹
¹ summaries must be >0 to compute geomean

cpu: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz
                                 │ benchmark-results.txt │
                                 │        sec/op         │
JQTransform_Simple-4                        924.1n ± 29%
JQTransform_ObjectConstruction-4            1.509µ ±  3%
JQTransform_ArraySelect-4                   3.186µ ±  1%
JQTransform_Complex-4                       35.74µ ±  1%
JQTransform_Throughput-4                    1.784µ ±  2%
SSEPublishDelivery-4                        76.53n ±  1%
geomean                                     1.670µ

                                 │ benchmark-results.txt │
                                 │         B/op          │
JQTransform_Simple-4                      1.273Ki ± 0%
JQTransform_ObjectConstruction-4          1.773Ki ± 0%
JQTransform_ArraySelect-4                 2.625Ki ± 0%
JQTransform_Complex-4                     16.22Ki ± 0%
JQTransform_Throughput-4                  1.984Ki ± 0%
SSEPublishDelivery-4                        0.000 ± 0%
geomean                                                ¹
¹ summaries must be >0 to compute geomean

                                 │ benchmark-results.txt │
                                 │       allocs/op       │
JQTransform_Simple-4                        10.00 ± 0%
JQTransform_ObjectConstruction-4            15.00 ± 0%
JQTransform_ArraySelect-4                   30.00 ± 0%
JQTransform_Complex-4                       324.0 ± 0%
JQTransform_Throughput-4                    17.00 ± 0%
SSEPublishDelivery-4                        0.000 ± 0%
geomean                                                ¹
¹ summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/schema
cpu: AMD EPYC 9V74 80-Core Processor                
                                    │ baseline-bench.txt │
                                    │       sec/op       │
SchemaValidation_Simple-4                    1.106µ ± 4%
SchemaValidation_AllFields-4                 1.611µ ± 5%
SchemaValidation_FormatValidation-4          1.555µ ± 2%
SchemaValidation_ManySchemas-4               1.585µ ± 3%
geomean                                      1.447µ

                                    │ baseline-bench.txt │
                                    │        B/op        │
SchemaValidation_Simple-4                   0.000 ± 0%
SchemaValidation_AllFields-4                0.000 ± 0%
SchemaValidation_FormatValidation-4         0.000 ± 0%
SchemaValidation_ManySchemas-4              0.000 ± 0%
geomean                                                ¹
¹ summaries must be >0 to compute geomean

                                    │ baseline-bench.txt │
                                    │     allocs/op      │
SchemaValidation_Simple-4                   0.000 ± 0%
SchemaValidation_AllFields-4                0.000 ± 0%
SchemaValidation_FormatValidation-4         0.000 ± 0%
SchemaValidation_ManySchemas-4              0.000 ± 0%
geomean                                                ¹
¹ summaries must be >0 to compute geomean

cpu: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz
                                    │ benchmark-results.txt │
                                    │        sec/op         │
SchemaValidation_Simple-4                       997.4n ± 2%
SchemaValidation_AllFields-4                    1.498µ ± 4%
SchemaValidation_FormatValidation-4             1.456µ ± 1%
SchemaValidation_ManySchemas-4                  1.473µ ± 4%
geomean                                         1.338µ

                                    │ benchmark-results.txt │
                                    │         B/op          │
SchemaValidation_Simple-4                      0.000 ± 0%
SchemaValidation_AllFields-4                   0.000 ± 0%
SchemaValidation_FormatValidation-4            0.000 ± 0%
SchemaValidation_ManySchemas-4                 0.000 ± 0%
geomean                                                   ¹
¹ summaries must be >0 to compute geomean

                                    │ benchmark-results.txt │
                                    │       allocs/op       │
SchemaValidation_Simple-4                      0.000 ± 0%
SchemaValidation_AllFields-4                   0.000 ± 0%
SchemaValidation_FormatValidation-4            0.000 ± 0%
SchemaValidation_ManySchemas-4                 0.000 ± 0%
geomean                                                   ¹
¹ summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/store
cpu: AMD EPYC 9V74 80-Core Processor                
                                   │ baseline-bench.txt │
                                   │       sec/op       │
EventStoreAppend_InMemory-4                1.085µ ± 25%
EventStoreAppend_SQLite-4                  1.122m ±  4%
GetTimeline_InMemory/events-10-4           12.55µ ±  2%
GetTimeline_InMemory/events-50-4           69.89µ ±  2%
GetTimeline_InMemory/events-100-4          107.0µ ± 31%
GetTimeline_InMemory/events-500-4          550.1µ ±  1%
GetTimeline_InMemory/events-1000-4         1.115m ±  1%
GetTimeline_SQLite/events-10-4             84.49µ ±  1%
GetTimeline_SQLite/events-50-4             225.9µ ±  1%
GetTimeline_SQLite/events-100-4            396.3µ ±  4%
GetTimeline_SQLite/events-500-4            1.708m ±  1%
GetTimeline_SQLite/events-1000-4           3.336m ±  1%
geomean                                    195.0µ

                                   │ baseline-bench.txt │
                                   │        B/op        │
EventStoreAppend_InMemory-4                  767.5 ± 8%
EventStoreAppend_SQLite-4                  1.985Ki ± 2%
GetTimeline_InMemory/events-10-4           7.953Ki ± 0%
GetTimeline_InMemory/events-50-4           46.62Ki ± 0%
GetTimeline_InMemory/events-100-4          94.48Ki ± 0%
GetTimeline_InMemory/events-500-4          472.8Ki ± 0%
GetTimeline_InMemory/events-1000-4         944.3Ki ± 0%
GetTimeline_SQLite/events-10-4             16.74Ki ± 0%
GetTimeline_SQLite/events-50-4             87.14Ki ± 0%
GetTimeline_SQLite/events-100-4            175.4Ki ± 0%
GetTimeline_SQLite/events-500-4            846.1Ki ± 0%
GetTimeline_SQLite/events-1000-4           1.639Mi ± 0%
geomean                                    67.18Ki

                                   │ baseline-bench.txt │
                                   │     allocs/op      │
EventStoreAppend_InMemory-4                  7.000 ± 0%
EventStoreAppend_SQLite-4                    53.00 ± 0%
GetTimeline_InMemory/events-10-4             125.0 ± 0%
GetTimeline_InMemory/events-50-4             653.0 ± 0%
GetTimeline_InMemory/events-100-4           1.306k ± 0%
GetTimeline_InMemory/events-500-4           6.514k ± 0%
GetTimeline_InMemory/events-1000-4          13.02k ± 0%
GetTimeline_SQLite/events-10-4               382.0 ± 0%
GetTimeline_SQLite/events-50-4              1.852k ± 0%
GetTimeline_SQLite/events-100-4             3.681k ± 0%
GetTimeline_SQLite/events-500-4             18.54k ± 0%
GetTimeline_SQLite/events-1000-4            37.29k ± 0%
geomean                                     1.162k

cpu: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz
                                   │ benchmark-results.txt │
                                   │        sec/op         │
EventStoreAppend_InMemory-4                    1.166µ ± 8%
EventStoreAppend_SQLite-4                      981.7µ ± 4%
GetTimeline_InMemory/events-10-4               13.61µ ± 6%
GetTimeline_InMemory/events-50-4               76.83µ ± 6%
GetTimeline_InMemory/events-100-4              119.5µ ± 3%
GetTimeline_InMemory/events-500-4              600.2µ ± 1%
GetTimeline_InMemory/events-1000-4             1.216m ± 1%
GetTimeline_SQLite/events-10-4                 83.17µ ± 2%
GetTimeline_SQLite/events-50-4                 232.3µ ± 4%
GetTimeline_SQLite/events-100-4                439.8µ ± 5%
GetTimeline_SQLite/events-500-4                1.972m ± 4%
GetTimeline_SQLite/events-1000-4               3.728m ± 2%
geomean                                        207.9µ

                                   │ benchmark-results.txt │
                                   │         B/op          │
EventStoreAppend_InMemory-4                    833.5 ± 10%
EventStoreAppend_SQLite-4                    1.983Ki ±  2%
GetTimeline_InMemory/events-10-4             7.953Ki ±  0%
GetTimeline_InMemory/events-50-4             46.62Ki ±  0%
GetTimeline_InMemory/events-100-4            94.48Ki ±  0%
GetTimeline_InMemory/events-500-4            472.8Ki ±  0%
GetTimeline_InMemory/events-1000-4           944.3Ki ±  0%
GetTimeline_SQLite/events-10-4               16.74Ki ±  0%
GetTimeline_SQLite/events-50-4               87.14Ki ±  0%
GetTimeline_SQLite/events-100-4              175.4Ki ±  0%
GetTimeline_SQLite/events-500-4              846.1Ki ±  0%
GetTimeline_SQLite/events-1000-4             1.639Mi ±  0%
geomean                                      67.64Ki

                                   │ benchmark-results.txt │
                                   │       allocs/op       │
EventStoreAppend_InMemory-4                     7.000 ± 0%
EventStoreAppend_SQLite-4                       53.00 ± 0%
GetTimeline_InMemory/events-10-4                125.0 ± 0%
GetTimeline_InMemory/events-50-4                653.0 ± 0%
GetTimeline_InMemory/events-100-4              1.306k ± 0%
GetTimeline_InMemory/events-500-4              6.514k ± 0%
GetTimeline_InMemory/events-1000-4             13.02k ± 0%
GetTimeline_SQLite/events-10-4                  382.0 ± 0%
GetTimeline_SQLite/events-50-4                 1.852k ± 0%
GetTimeline_SQLite/events-100-4                3.681k ± 0%
GetTimeline_SQLite/events-500-4                18.54k ± 0%
GetTimeline_SQLite/events-1000-4               37.29k ± 0%
geomean                                        1.162k

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

Three issues from review:

1. Partial provider_credential state — checking only <key>_access_key
   skipped regeneration even when <key>_secret_key was missing (e.g.
   manually deleted). Replace single-probe with a known sub-keys table
   (providerCredentialSubKeys) and require ALL expected keys to be
   present before skipping.

2. O(N*M) linear scan — lookupViaList now builds a map[string]struct{}
   once from the List() result, so each subsequent probe is O(1).

3. Test coverage — add a package-level generateSecret hook so tests
   can stub provider_credential generation without contacting DO.
   New tests cover: both sub-keys present skips, partial state
   regenerates, bare key without sub-keys still regenerates. The
   existing test that conflated random_hex with provider_credential
   semantics is replaced with dedicated cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@intel352 intel352 merged commit 78415eb into main Apr 20, 2026
18 checks passed
@intel352 intel352 deleted the fix/bootstrap-secret-existence-write-only-providers branch April 20, 2026 15:47
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