Summary
The AICR-side Sigstore signing in pkg/bundler/attestation/signing.go
performs a single-pass call to sign.Bundle() with no retry-with-backoff
on transient Rekor/Fulcio errors. The goreleaser cosign attest-blob
post-hook in .goreleaser.yaml similarly omits --retry. Rekor's public
instance occasionally has slow responses, and a single-attempt budget
turns ordinary upstream latency into a CI failure.
Bump the retry budget at both call sites so transient Rekor flakes
don't gate merges.
Background — observed failures
Two PRs hit this flake within 24h with the identical failure
fingerprint:
PR #1244 (feat/1223-registry-lint-guard) — aicr recipe sign-catalog post-hook in goreleaser:
[cli] command failed: error=[TIMEOUT] sigstore signing timed out:
Post "https://rekor.sigstore.dev/api/v1/log/entries":
giving up after 1 attempt(s): context deadline exceeded exitCode=5
PR #1245 (feat/1222-enhance-existing-checks) —
cli-bundle-attestation-ci chainsaw test invoking aicr bundle:
[cli] command failed: error=[INTERNAL] bundle attestation failed:
[TIMEOUT] sigstore signing timed out:
Post "https://rekor.sigstore.dev/api/v1/log/entries":
giving up after 1 attempt(s): context deadline exceeded exitCode=8
--- FAIL: chainsaw/cli-bundle-attestation-ci (51.97s)
Both surface the same giving up after 1 attempt(s) shape — that's the
AICR-side sign.Bundle() wrapper at
pkg/bundler/attestation/signing.go:141-157. The 1 attempt budget is
not configurable today.
Affected Surfaces
pkg/bundler/attestation/signing.go (lines ~130-157) — the
sign.Bundle() invocation that backs aicr bundle --sign,
aicr recipe sign-catalog, and aicr evidence. Single-pass under
defaults.SigstoreSignTimeout. The classification logic distinguishes
ErrCodeTimeout (context.DeadlineExceeded) from ErrCodeUnavailable
(other Rekor/Fulcio errors) but doesn't retry either class.
.goreleaser.yaml (lines ~36-46) — the binary-attestation
post-hook calling cosign attest-blob. cosign's default retry budget
is 1; no --retry flag is passed.
Proposed Approach
AICR-side (Go)
Add retry-with-backoff around sign.Bundle() for the transient class:
- Retry on:
ErrCodeUnavailable (Fulcio refused, Rekor 5xx,
transient network) — these are the upstream-flake class.
- Don't retry on:
ErrCodeTimeout (caller's context deadline) —
those are caller-intent failures and a retry would just chew through
the budget. EXCEPT when the deadline is internal (i.e., we set
SigstoreSignTimeout); then optionally retry up to a wall-clock cap.
- Budget: 3 attempts total with exponential backoff (e.g., 1s, 5s,
15s — Rekor's typical recovery window). Caps via
defaults.SigstoreRetryBudget (new named constant).
- The retry wrapper is in
pkg/bundler/attestation/signing.go, behind
the existing classification logic so callers still see structured
errors with the same codes.
goreleaser-side
Add --retry 5 (cosign default backoff is reasonable) to the
cosign attest-blob invocation in .goreleaser.yaml. Costs nothing
when Rekor is healthy; saves an entire release run when it's slow.
Out of Scope
- Switching off public Rekor for a private instance — out of scope.
- Removing Rekor from the signing flow (
--no-tlog) — out of scope;
the supply-chain provenance contract requires the transparency log.
- Per-call configurable retry budget exposed in the CLI flag set —
not needed; the named constant in pkg/defaults is the single
source of truth.
Acceptance Criteria
pkg/bundler/attestation/signing.go retries ErrCodeUnavailable
failures from sign.Bundle() up to defaults.SigstoreRetryBudget
attempts (initial proposal: 3) with exponential backoff.
- The retry happens INSIDE the named
SigstoreSignTimeout ceiling
— a slow Rekor doesn't blow past the structured deadline contract.
ErrCodeTimeout (caller ctx deadline exceeded) is propagated
as-is without retry.
.goreleaser.yaml cosign attest-blob calls pass --retry 5.
- New
defaults.SigstoreRetryBudget constant with a TestSigstoreRetryBudgetInvariant
that gates against zero/negative values.
- Unit test covers: success on first try, success on 2nd attempt
after one ErrCodeUnavailable, full-budget exhaustion returns the
last error, ErrCodeTimeout skips retry.
- Documentation update in
docs/contributor/ or the relevant
attestation/signing reference doc mentioning the retry contract.
Related
Summary
The AICR-side Sigstore signing in
pkg/bundler/attestation/signing.goperforms a single-pass call to
sign.Bundle()with no retry-with-backoffon transient Rekor/Fulcio errors. The goreleaser
cosign attest-blobpost-hook in
.goreleaser.yamlsimilarly omits--retry. Rekor's publicinstance occasionally has slow responses, and a single-attempt budget
turns ordinary upstream latency into a CI failure.
Bump the retry budget at both call sites so transient Rekor flakes
don't gate merges.
Background — observed failures
Two PRs hit this flake within 24h with the identical failure
fingerprint:
PR #1244 (
feat/1223-registry-lint-guard) —aicr recipe sign-catalogpost-hook in goreleaser:PR #1245 (
feat/1222-enhance-existing-checks) —cli-bundle-attestation-cichainsaw test invokingaicr bundle:Both surface the same
giving up after 1 attempt(s)shape — that's theAICR-side
sign.Bundle()wrapper atpkg/bundler/attestation/signing.go:141-157. The1 attemptbudget isnot configurable today.
Affected Surfaces
pkg/bundler/attestation/signing.go(lines ~130-157) — thesign.Bundle()invocation that backsaicr bundle --sign,aicr recipe sign-catalog, andaicr evidence. Single-pass underdefaults.SigstoreSignTimeout. The classification logic distinguishesErrCodeTimeout(context.DeadlineExceeded) fromErrCodeUnavailable(other Rekor/Fulcio errors) but doesn't retry either class.
.goreleaser.yaml(lines ~36-46) — the binary-attestationpost-hook calling
cosign attest-blob. cosign's default retry budgetis 1; no
--retryflag is passed.Proposed Approach
AICR-side (Go)
Add retry-with-backoff around
sign.Bundle()for the transient class:ErrCodeUnavailable(Fulcio refused, Rekor 5xx,transient network) — these are the upstream-flake class.
ErrCodeTimeout(caller's context deadline) —those are caller-intent failures and a retry would just chew through
the budget. EXCEPT when the deadline is internal (i.e., we set
SigstoreSignTimeout); then optionally retry up to a wall-clock cap.15s — Rekor's typical recovery window). Caps via
defaults.SigstoreRetryBudget(new named constant).pkg/bundler/attestation/signing.go, behindthe existing classification logic so callers still see structured
errors with the same codes.
goreleaser-side
Add
--retry 5(cosign default backoff is reasonable) to thecosign attest-blobinvocation in.goreleaser.yaml. Costs nothingwhen Rekor is healthy; saves an entire release run when it's slow.
Out of Scope
--no-tlog) — out of scope;the supply-chain provenance contract requires the transparency log.
not needed; the named constant in
pkg/defaultsis the singlesource of truth.
Acceptance Criteria
pkg/bundler/attestation/signing.goretriesErrCodeUnavailablefailures from
sign.Bundle()up todefaults.SigstoreRetryBudgetattempts (initial proposal: 3) with exponential backoff.
SigstoreSignTimeoutceiling— a slow Rekor doesn't blow past the structured deadline contract.
ErrCodeTimeout(caller ctx deadline exceeded) is propagatedas-is without retry.
.goreleaser.yamlcosign attest-blobcalls pass--retry 5.defaults.SigstoreRetryBudgetconstant with aTestSigstoreRetryBudgetInvariantthat gates against zero/negative values.
after one ErrCodeUnavailable, full-budget exhaustion returns the
last error, ErrCodeTimeout skips retry.
docs/contributor/or the relevantattestation/signing reference doc mentioning the retry contract.
Related
defaults.SigstoreSignTimeout— the wall-clock ceiling theretry-with-backoff must respect
https://docs.sigstore.dev/about/public-deployment/ — uptime is
best-effort, transient failures are expected