Repository navigation
Description
Summary
When generator_container_slsa3.yml is called from a private repository
with private-repository: true, the inner privacy-check action fails
with Repository is private. The workflow has halted in order to keep the repository name from being exposed in the public transparency log. Set 'private-repository' to override. even though the override IS set
in the caller's with: block.
The bug reproduces on both v2.0.0 and v2.1.0 (latest stable as
of this writing). The caller-side workaround of quoting the input is
rejected by GitHub Actions at dispatch time due to the reusable
workflow's type: boolean declaration on private-repository.
Reproduction
- Caller: private GitHub repository on Free, Pro, or Team plan
(Enterprise Cloud not required for repro) - Caller workflow:
attest: needs: [build] permissions: id-token: write packages: write actions: read uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@<sha> with: image: ghcr.io/<owner>/<repo> digest: ${{ needs.build.outputs.digest }} registry-username: ${{ github.actor }} private-repository: true secrets: registry-password: ${{ secrets.GITHUB_TOKEN }}
- Versions tested:
v2.0.0(SHA5a775b367a56d5bd118a224a811bba288150a563)v2.1.0(SHAf7dd8c54c2067bafc12ca7a55595d5ee9b75204a)
Both fail identically.
Observed failure (v2.0.0 + v2.1.0, identical)
In the attest / generator job, the Generate builder step (which has
continue-on-error: true, so it appears ✓ in the UI) emits this error:
error_message: Repository is private. The workflow has halted in order
to keep the repository name from being exposed in the public
transparency log. Set 'private-repository' to override.
This silently sets the step's outcome to failure. The cascade then is:
Generate builderstep: outcome = failure (silent due to continue-on-error)- Generator job's
Final outcomestep:SUCCESS = falsebecausesteps.generate-builder.outcome == 'failure' - Generator job output:
outcome = failure - Outer
finaljob:SUCCESS = (detect-env.outcome != 'failure' && generator.outcome != 'failure')→false finalstep:[ "$CONTINUE" == "true" ] || [ "$SUCCESS" == "true" ] || exit 27→ exit 27 ❌
Job inputs log confirms private-repository: true was passed:
##[group] Inputs
image: ghcr.io/<owner>/<repo>
digest: sha256:<digest>
registry-username: <actor>
compile-generator: false
private-repository: true
continue-on-error: false
Caller-side workaround attempted (and why it fails)
Quoting the input as a string from the caller:
- private-repository: true
+ private-repository: "true"Result: GitHub Actions rejects the dispatch with startup_failure
because the reusable workflow declares inputs.private-repository as
type: boolean and a quoted string fails the type check. Zero jobs
run. The caller cannot work around this from their side.
Hypothesis
The propagation chain through composite-action boundaries appears to
corrupt the boolean's serialized form. The chain is:
- Caller sends
private-repository: true(YAML boolean) — typed correctly. - Reusable workflow forwards via unquoted interpolation:
Note: this is the only unquoted
# .github/workflows/generator_container_slsa3.yml line ~156 allow-private-repository: ${{ inputs.private-repository }}
${{ }}in thatwith:block;
every other input in the same block is double-quoted. - Inner composite action
generate-builder/action.ymlforwards via
another unquoted interpolation:# .github/actions/generate-builder/action.yml line ~75 override: ${{ inputs.allow-private-repository }}
- JS action
privacy-check/src/main.tsdoes strict string equality:Any value other than the literal lowercase stringconst override = core.getInput("override") === "true";
"true"yields
override = false, triggeringcore.setFailed()on a private repo.
The two unquoted interpolations across composite-action boundaries
are the most likely corruption point. GitHub Actions' handling of
type: boolean action inputs through nested with: blocks has
documented edge cases where the value can arrive at a downstream JS
action as "True", "TRUE", or "1" rather than "true" —
whichever it is, it doesn't strict-equal "true".
Why this hasn't been widely reported
Most generator_container_slsa3.yml callers either:
(a) call it from a public repo, where privacy-check passes
regardless of the override value, OR
(b) don't pass private-repository: true at all.
Our case (private repo + needing the override) genuinely exercises
the bug.
Suggested fixes (not prescriptive)
Any one of these would address the root cause:
- Quote the interpolations in the composite-action chain so the
string representation is stable across boundary crossings:allow-private-repository: "${{ inputs.private-repository }}"
override: "${{ inputs.allow-private-repository }}"
- Relax the privacy-check string comparison to accept common
boolean string forms:Or useconst override = ["true", "True", "TRUE", "1"].includes( core.getInput("override") );
core.getBooleanInput("override")(Actions Toolkit
helper that handles the YAML 1.2 boolean variants). - Change input types from
booleantostringwith explicit
casting at a single defined point, eliminating the implicit
serialization-through-boundaries.
(2) is probably the lowest-blast-radius fix; (1) is the most
mechanically minimal.
Downstream impact
Our project (a private repository on the GitHub Team plan) was
unable to ship SLSA L3 provenance via slsa-github-generator because
of this bug. We considered:
- Pin downgrade v2.1.0 → v2.0.0: failed identically (this report).
- Caller-side quoting workaround: rejected at dispatch (this report).
- Switch to
actions/attest-build-provenance: blocked — that
action requires GitHub Enterprise Cloud for private repos. - Fork the reusable workflow to fix the unquoted interpolations:
too much surface to maintain for our use case.
We ended up implementing self-rolled cosign keyless attestation
inline in our workflow (without slsa-github-generator), which
produces SLSA L2-equivalent provenance rather than L3. We retain
build-identity binding + Sigstore Rekor transparency-log
auditability + offline verifiability, but lose the L3
hosted-isolated-builder property that the reusable workflow
provides.
Environment
- GitHub Actions hosted runners (ubuntu-24.04)
- Caller plan: GitHub Team
- Caller repo visibility: private
- cosign-installer (used downstream of generate-builder): not the
source of the failure; the privacy-check halt occurs before it runs
Happy to provide any additional reproduction details or test against
proposed fixes.
Activity
deps: rollup k8s 0.36.0, ubuntu 26.04, codeql 4.35.3 + pin SLSA v2.0.0 (