Skip to content

feat: GitHub Action with Sigstore attestation and pinned-version verification (#6) - #17

Merged
gkanitz merged 7 commits into
mainfrom
feat/6-action-attestation
Jun 12, 2026
Merged

feat: GitHub Action with Sigstore attestation and pinned-version verification (#6)#17
gkanitz merged 7 commits into
mainfrom
feat/6-action-attestation

Conversation

@gkanitz

@gkanitz gkanitz commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Closes #6

Summary

The GitHub trust chain for CodeRepute reports:

  • Verification-block population (report/verification.go, new): inside GitHub Actions the report's mandatory verification block upgrades to verified and records provider, repository, workflow_ref, run_id, run_url, plus an attestation pointer (type, attestations URL, exact gh attestation verify command). Outside CI the explicit unverified block from Build stands — the CLI never claims more than its environment proves. The CLI seam is additive: one report.CIVerification(getenv) call in cmd/coderepute/main.go.
  • Composite Action (action.yml, new): builds the CLI from the action's own pinned source, generates report.json + report.html, attests report.json with actions/attest-build-provenance, uploads both as artifacts. All step actions pinned by commit SHA.
  • Canonical reusable workflow (.github/workflows/coderepute-report.yml, new): the machine-checkable identity. Consumers pin grkanitz/CodeRepute/.github/workflows/coderepute-report.yml@vX.Y.Z; the Sigstore certificate's job_workflow_ref then names this file at the pinned version, so gh attestation verify --signer-workflow grkanitz/CodeRepute/.github/workflows/coderepute-report.yml fails for a modified fork. It checks out the action source at github.job_workflow_sha, so the binary cannot diverge from the pinned workflow version.
  • Docs (docs/verification.md, new): the trust chain, the two-step verification (attestation + workflow-identity-vs-canonical-action), exactly what passing proves and what it does not, pinned-version convention, platform requirements. README gains a CI usage section.
  • Demo workflow (.github/workflows/demo-report.yml, new): workflow_dispatch, exercises both consumption paths against this repo.
  • HTML verification section now renders the producing workflow identity and verify command.

Test output

$ go test -count=1 ./... && go vet ./...
ok  github.com/grkanitz/coderepute/cmd/coderepute  0.674s
ok  github.com/grkanitz/coderepute/metrics         0.357s
?   github.com/grkanitz/coderepute/provider        [no test files]
ok  github.com/grkanitz/coderepute/provider/github 0.957s
ok  github.com/grkanitz/coderepute/render          1.490s
ok  github.com/grkanitz/coderepute/report          1.212s

Live demo evidence (runs 27398528483, 27398606723)

workflow_dispatch is not dispatchable until the workflow exists on the default branch, so the demo was run live via a temporary branch-scoped push trigger (removed again in the final commit).

What the live runs proved end-to-end on a real runner:

  • Both paths (direct composite action and reusable workflow) built the CLI from pinned source, fetched real data, and produced the report. CI-produced report.json from run 27398606723 (artifact coderepute-report-action):
"verification": {
  "status": "verified",
  "provider": "github-actions",
  "repository": "grkanitz/CodeRepute",
  "workflow_ref": "grkanitz/CodeRepute/.github/workflows/demo-report.yml@refs/heads/feat/6-action-attestation",
  "run_id": "27398606723",
  "run_url": "https://github.com/grkanitz/CodeRepute/actions/runs/27398606723",
  "attestation": {
    "type": "sigstore-github-artifact-attestation",
    "url": "https://github.com/grkanitz/CodeRepute/attestations",
    "verify_command": "gh attestation verify report.json --repo grkanitz/CodeRepute"
  }
}
  • The attest step reached Sigstore signing and was blocked only at persistence by a platform limitation (run 27398528483):
Attestation type: Build Provenance
Error: Failed to persist attestation: Feature not available for user-owned
private repositories. To enable this feature, please make this repository
public. - https://docs.github.com/rest/repos/attestations#create-an-attestation

GitHub artifact attestations require a public repository (or GitHub Enterprise Cloud for private ones). This repo is user-owned private, so no plan setting unblocks it short of going public.

Post-merge verification (once the repo is public, or from any public consumer repo)

gh workflow run demo-report.yml -R grkanitz/CodeRepute
gh run download <run-id> -R grkanitz/CodeRepute -n coderepute-report-reusable -D out
gh attestation verify out/report.json --repo grkanitz/CodeRepute
gh attestation verify out/report.json --repo grkanitz/CodeRepute \
  --signer-workflow grkanitz/CodeRepute/.github/workflows/coderepute-report.yml

The last command is the fork-fails check: an artifact attested by a fork's copy of the workflow carries a different job_workflow_ref and fails.

Parallel-work contract

Shared files touched, all additive: report/report.go (new optional fields on the Verification sub-struct I own), cmd/coderepute/main.go (one 3-line CI seam), README.md (new section), render/render_test.go + cmd/coderepute/main_test.go (new tests only), render/templates/sections/20-verification.tmpl (verification section, my territory). No metrics, ActivitySet, collaboration/cadence/coverage, or adapter files touched.

🤖 Generated with Claude Code

gkanitz and others added 7 commits June 12, 2026 12:28
Incomplete TDD state; resume from here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
In GitHub Actions the report's verification block upgrades to verified,
recording provider, repository, workflow ref, run id/url, and a pointer
to the Sigstore attestation with the exact gh verify command. Outside CI
the explicit unverified block stands. The HTML verification section now
surfaces the producing workflow identity and verify command.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…fication docs

- action.yml builds the CLI from the pinned action source, generates
  report.json + report.html, attests report.json with
  actions/attest-build-provenance, and uploads artifacts. All step
  actions pinned by commit SHA.
- .github/workflows/coderepute-report.yml is the canonical reusable
  workflow: its job_workflow_ref is the machine-checkable identity, so
  gh attestation verify --signer-workflow fails for modified forks; it
  checks out the action source at github.job_workflow_sha so the binary
  cannot diverge from the pinned version.
- .github/workflows/demo-report.yml: workflow_dispatch demo exercising
  both consumption paths against this repository.
- docs/verification.md documents the trust chain, the two-step
  verification, exactly what passing proves, and platform requirements.
- README gains a CI usage section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
workflow_dispatch is not dispatchable until the workflow exists on the
default branch; a push trigger scoped to this branch gives a live run.
Removed before merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The restricted job token blocked the CLI's pull list with 403 Resource
not accessible by integration. Documented in the action header, the
reusable-workflow usage snippet, README, and verification docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Persisting attestations is unavailable for user-owned private repos;
this lets the demo upload the CI-produced report artifact as evidence.
Temporary, removed before merge with the push trigger.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… claim

Removes the temporary branch push trigger and the attest skip used to
capture live evidence while the repo is private. Adds a docs note that
the report's verification block is untrusted until gh attestation
verify passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@gkanitz
gkanitz force-pushed the feat/6-action-attestation branch from 0f3b9f5 to 6976761 Compare June 12, 2026 11:30
@gkanitz

gkanitz commented Jun 12, 2026

Copy link
Copy Markdown
Owner Author

Pre-review rebase

Rebased feat/6-action-attestation onto a939594 (origin/main, after #14 cadence, #15 collaboration, #16 org-coverage merged).

Conflicted files and resolutions

cmd/coderepute/main.go — single conflict at the report.Build call site.

  • Kept main's call with &result.Cadence and report.WithTokenScopeClass(...).
  • Added this branch's if v := report.CIVerification(getenv); v != nil { r.Verification = v } block immediately after.

cmd/coderepute/main_test.go — conflict between TestRunTrimsRepoListWhitespace (main) and TestRunInGitHubActionsUpgradesVerification (this branch). Both tests kept; they test orthogonal behaviour.

render/render_test.go — conflict between TestHTMLCadenceIsSubordinateContext (main) and TestHTMLShowsVerifiedIdentity (this branch). Both tests kept.

report/report.go — auto-resolved cleanly: main's full Collaboration/Cadence/TrendBucket/DurationStats/ReworkStats/ReviewStats/BuildOption/WithTokenScopeClass was already in the index, and the commit applied this branch's enriched Verification struct (Provider, Repository, WorkflowRef, RunID, RunURL, Attestation) on top.

No other files conflicted. This branch did not touch ActivitySet, metrics, or adapter files.

Test results

ok  github.com/grkanitz/coderepute/cmd/coderepute   0.948s
ok  github.com/grkanitz/coderepute/metrics           0.545s
ok  github.com/grkanitz/coderepute/provider/github   1.991s
ok  github.com/grkanitz/coderepute/render            1.203s
ok  github.com/grkanitz/coderepute/report            1.470s

go build ./... and go vet ./... clean. Force-pushed as 6976761.

@gkanitz

gkanitz commented Jun 12, 2026

Copy link
Copy Markdown
Owner Author

Review round 1 — approved

Independent review of PR #17 against issue #6. Built, vetted, and tested the rebased head 6976761 locally (go build, go vet, go test -count=1 ./... all clean); CI check green; PR MERGEABLE/CLEAN.

Acceptance criteria

  1. Action run → report + verifiable attestation: APPROVED UNDER THE DOCUMENTED-LIMITATION FALLBACK. This is the one criterion not fully demonstrable, and I verified the claim rather than taking it on faith:
    • Live runs 27398528483 / 27398606723 show both consumption paths (direct composite action, canonical reusable workflow) executing on real runners, building the CLI from pinned source and producing the report. I downloaded the coderepute-report-action artifact from run 27398606723 and inspected report.json: the verification block is populated exactly as claimed (workflow_ref, run_id/url, attestation pointer + verify command).
    • Run logs confirm the attest step reached Sigstore signing (Attestation type: Build Provenance) and failed only at persistence with GitHub's explicit platform error: "Feature not available for user-owned private repositories." This is a real platform limitation (artifact attestations require a public repo or GHEC), not a defect in this PR.
    • I ran gh attestation verify report.json --repo grkanitz/CodeRepute myself: HTTP 404, consistent — no attestation persisted, as documented. No hand-waving found; the PR body states the blocker precisely and gives exact post-merge commands.
  2. Verification block identifies attestation + producing workflow ref, populated from Actions env: confirmed in report/verification.go + tests (TestCIVerificationInGitHubActions, TestRunInGitHubActionsUpgradesVerification) and in the actual CI-produced artifact.
  3. Docs explain what passing proves and that a modified fork fails: docs/verification.md is precise — two-step verification, --signer-workflow fork-fails check, "what passing proves / what it does not", pinned-version convention. Crucially it does not conflate claim with proof: it states outright that the report's verification block is untrusted input until gh attestation verify passes.
  4. Outside CI → explicit unverified: Build default stands, CIVerification returns nil unless GITHUB_ACTIONS is exactly "true"; tested.

Security

  • Minimal permissions documented and declared (contents: read, pull-requests: read, id-token: write, attestations: write); reusable workflow scopes them at job level.
  • All actions inside the composite and workflows pinned by commit SHA.
  • Token and CLI inputs passed via env, never interpolated into shell commands. No secret leakage.
  • Nit (non-blocking): CIVerification stamps status: "verified" whenever it runs under Actions, even if the caller sets attest: false or the attest step later fails — the JSON then claims verified with no attestation behind it. The docs handle this honestly (the block is a claim; verification is the proof), so acceptable, but worth keeping in mind for the verify-page work.

Parallel-work contract

Clean. Diff vs origin/main touches only owned territory: action/workflow YAML, docs/verification.md, report/verification.go(+test), the Verification sub-struct (additive optional fields), a 3-line CLI seam, the verification HTML template, README section, new tests. No metrics, ActivitySet, collaboration/cadence/coverage sub-structs, or adapter files touched. Temporary demo scaffolding (branch push trigger, attest skip) confirmed removed in the final commit.

Owner follow-up (required to close the loop on criterion 1)

Once the repo is public (or from any public consumer repo pinning this workflow):

gh workflow run demo-report.yml -R grkanitz/CodeRepute
gh run download <run-id> -R grkanitz/CodeRepute -n coderepute-report-reusable -D out
gh attestation verify out/report.json --repo grkanitz/CodeRepute
gh attestation verify out/report.json --repo grkanitz/CodeRepute \
  --signer-workflow grkanitz/CodeRepute/.github/workflows/coderepute-report.yml

The last command is the fork-fails check and must pass only for artifacts attested by the canonical workflow.

@gkanitz
gkanitz merged commit 497161d into main Jun 12, 2026
1 check passed
@gkanitz
gkanitz deleted the feat/6-action-attestation branch June 12, 2026 11:35
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.

GitHub Action wrapper with Sigstore attestation and pinned-version verification

1 participant