Skip to content

Commit d66cf14

Browse files
authored
feat: attested quality-gate seam wiring, release process, and Diátaxis docs (#463)
1 parent 409a561 commit d66cf14

14 files changed

Lines changed: 852 additions & 483 deletions

File tree

.claude/documentation-review.local.md

Lines changed: 0 additions & 77 deletions
This file was deleted.

.github/workflows/release.yml

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Attested source release for zircote/.github.
2+
#
3+
# This repo preaches attested delivery, so its own releases are attested: a
4+
# deterministic source bundle, a SLSA build-provenance attestation signed by
5+
# THIS workflow's OIDC identity, and a fail-closed self-verify. Consumers keep
6+
# pinning reusable workflows/actions by 40-char SHA; tags + Releases give them a
7+
# human-readable changelog and a verifiable artifact per release.
8+
#
9+
# Cut a release:
10+
# git tag v1.2.0 && git push origin v1.2.0
11+
# or run this workflow manually (workflow_dispatch) against an existing tag.
12+
# Verify a release artifact: see RELEASING.md.
13+
14+
name: release
15+
16+
on:
17+
push:
18+
tags:
19+
# GitHub Actions tag filters are glob, not regex. These coarse globs match
20+
# any vX.Y.Z[-pre]; the job's first step validates strict semver.
21+
- 'v*.*.*' # stable, e.g. v1.2.0
22+
- 'v*.*.*-*' # prerelease, e.g. v1.2.0-rc.1
23+
workflow_dispatch:
24+
inputs:
25+
tag:
26+
description: 'Existing semver tag to (re)release, e.g. v1.2.0'
27+
required: true
28+
type: string
29+
30+
permissions:
31+
contents: read
32+
33+
concurrency:
34+
group: release-${{ github.ref_name }}
35+
cancel-in-progress: false
36+
37+
jobs:
38+
release:
39+
name: Attested source release
40+
runs-on: ubuntu-latest
41+
permissions:
42+
contents: write # create the GitHub Release, read the tag
43+
id-token: write # OIDC for keyless Sigstore signing
44+
attestations: write # write the build-provenance attestation
45+
steps:
46+
- name: Resolve and validate tag
47+
id: tag
48+
env:
49+
EVENT: ${{ github.event_name }}
50+
REF_NAME: ${{ github.ref_name }}
51+
DISPATCH_TAG: ${{ inputs.tag }}
52+
run: |
53+
if [ "$EVENT" = "workflow_dispatch" ]; then
54+
TAG="$DISPATCH_TAG"
55+
else
56+
TAG="$REF_NAME"
57+
fi
58+
# Fail closed on anything that is not vMAJOR.MINOR.PATCH[-prerelease].
59+
if ! printf '%s' "$TAG" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
60+
echo "::error::Tag '$TAG' is not semver (expected vX.Y.Z or vX.Y.Z-pre)"
61+
exit 1
62+
fi
63+
PRERELEASE=false
64+
case "$TAG" in *-*) PRERELEASE=true ;; esac
65+
{
66+
echo "tag=$TAG"
67+
echo "version=${TAG#v}"
68+
echo "prerelease=$PRERELEASE"
69+
} >> "$GITHUB_OUTPUT"
70+
71+
- name: Checkout at tag
72+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
73+
with:
74+
ref: ${{ steps.tag.outputs.tag }}
75+
fetch-depth: 0
76+
fetch-tags: true
77+
78+
- name: Build deterministic source bundle
79+
id: bundle
80+
env:
81+
TAG: ${{ steps.tag.outputs.tag }}
82+
VERSION: ${{ steps.tag.outputs.version }}
83+
run: |
84+
ARTIFACT="zircote-github-${VERSION}.tar.gz"
85+
# git archive is reproducible: same tree -> identical bytes/digest.
86+
git archive --format=tar.gz --prefix="zircote-github-${VERSION}/" \
87+
-o "$ARTIFACT" "$TAG"
88+
sha256sum "$ARTIFACT" | tee "$ARTIFACT.sha256"
89+
echo "artifact=$ARTIFACT" >> "$GITHUB_OUTPUT"
90+
91+
- name: Attest build provenance
92+
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
93+
with:
94+
subject-path: ${{ steps.bundle.outputs.artifact }}
95+
96+
- name: Create GitHub Release
97+
env:
98+
GH_TOKEN: ${{ github.token }}
99+
REPO: ${{ github.repository }}
100+
TAG: ${{ steps.tag.outputs.tag }}
101+
ARTIFACT: ${{ steps.bundle.outputs.artifact }}
102+
PRERELEASE: ${{ steps.tag.outputs.prerelease }}
103+
run: |
104+
PRE_FLAG=""
105+
[ "$PRERELEASE" = "true" ] && PRE_FLAG="--prerelease"
106+
# Idempotent: a re-dispatch against an existing tag refreshes the
107+
# release assets instead of failing on an already-published release.
108+
if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then
109+
echo "Release $TAG already exists; refreshing assets."
110+
gh release upload "$TAG" "$ARTIFACT" "$ARTIFACT.sha256" \
111+
--repo "$REPO" --clobber
112+
else
113+
gh release create "$TAG" \
114+
--repo "$REPO" \
115+
--title "$TAG" \
116+
--generate-notes \
117+
--verify-tag \
118+
$PRE_FLAG \
119+
"$ARTIFACT" "$ARTIFACT.sha256"
120+
fi
121+
122+
- name: Verify the attestation (fail-closed dogfood)
123+
env:
124+
GH_TOKEN: ${{ github.token }}
125+
REPO: ${{ github.repository }}
126+
ARTIFACT: ${{ steps.bundle.outputs.artifact }}
127+
run: |
128+
SIGNER="${REPO}/.github/workflows/release.yml"
129+
# Tolerate brief attestation-index propagation, but fail closed.
130+
for attempt in 1 2 3 4 5; do
131+
if gh attestation verify "$ARTIFACT" \
132+
--repo "$REPO" --signer-workflow "$SIGNER"; then
133+
echo "Attestation verified on attempt $attempt."
134+
exit 0
135+
fi
136+
echo "Not yet verifiable (attempt $attempt); retrying in 10s..."
137+
sleep 10
138+
done
139+
echo "::error::Attestation did not verify — refusing to leave an unverified release."
140+
exit 1

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@
55

66
# subcog tool-generated audit logs (anywhere in the tree)
77
**/.subcog/
8+
9+
# Local-only Claude config (kept on disk, never committed)
10+
.claude/**/*.local.*

CLAUDE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,17 @@ Cross-repo MCP access uses a GitHub App. Required App permissions must match the
297297
- Keep commits atomic and focused
298298
- Reference issues in commit messages when applicable
299299

300+
## Release Process
301+
302+
This repo is released by its own **attested** workflow (`.github/workflows/release.yml`,
303+
tag-driven). Push a semver tag (`git tag v1.2.0 && git push origin v1.2.0`) to
304+
build a deterministic `git archive` source bundle, attest SLSA build provenance
305+
over it (signed by `release.yml`), publish a GitHub Release with auto-notes, and
306+
fail-closed self-verify. Conventional-commit bump: `fix:`→patch, `feat:`→minor,
307+
`feat!:`/`BREAKING CHANGE:`→major. Full how-to + the consumer verify command:
308+
`RELEASING.md`. (`reusable-release.yml` is the separate `workflow_call` reusable
309+
for consumer **app** repos and does not attest.)
310+
300311
## Language Standards (for Templates)
301312

302313
| Language | Version | Package Manager | Linter/Formatter | Type Checker |

RELEASING.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Releasing zircote/.github
2+
3+
This repo ships an **attested** release process: each release is a deterministic
4+
source bundle carrying a SLSA build-provenance attestation signed by the
5+
`release.yml` workflow's OIDC identity, plus a GitHub Release with
6+
auto-generated notes. It dogfoods the same attested-delivery doctrine this repo
7+
provides to consumers.
8+
9+
> Consumers continue to pin reusable workflows and actions by full 40-char
10+
> commit SHA (Dependabot keeps them fresh). Tags and Releases add a
11+
> human-readable changelog and a verifiable artifact per version — they do not
12+
> change the SHA-pinning rule.
13+
14+
## Cut a release
15+
16+
Releases are **tag-driven**. From an up-to-date `main`:
17+
18+
```bash
19+
git tag v1.2.0 # semver: vMAJOR.MINOR.PATCH (or vX.Y.Z-rc.1 for a prerelease)
20+
git push origin v1.2.0
21+
```
22+
23+
The `release` workflow then:
24+
25+
1. Validates the tag is semver (fails closed otherwise).
26+
2. Builds `zircote-github-<version>.tar.gz` with `git archive` (reproducible).
27+
3. Attests SLSA build provenance over the bundle (`actions/attest-build-provenance`).
28+
4. Creates the GitHub Release with auto-generated notes and attaches the bundle
29+
plus its `.sha256`.
30+
5. Re-verifies the attestation fail-closed before finishing.
31+
32+
To re-run against an existing tag, dispatch the workflow manually
33+
(**Actions → release → Run workflow**) and supply the tag. A re-dispatch is
34+
idempotent: if a Release already exists for the tag it refreshes the attached
35+
assets (bundle + `.sha256`) rather than failing; the auto-generated notes from
36+
the original run are kept.
37+
38+
Version bump convention (Conventional Commits): `fix:` → patch, `feat:` → minor,
39+
`feat!:` / `BREAKING CHANGE:` → major.
40+
41+
## Verify a release artifact
42+
43+
Download the `.tar.gz` from the release, then — independently from a
44+
workstation:
45+
46+
```bash
47+
gh attestation verify zircote-github-<version>.tar.gz \
48+
--repo zircote/.github \
49+
--signer-workflow zircote/.github/.github/workflows/release.yml
50+
```
51+
52+
`--signer-workflow` is required: under SLSA L3 the Fulcio SAN is the signing
53+
workflow. Inspect the provenance predicate with `--format json | jq` to confirm
54+
the source commit and build inputs. A successful verify proves the bundle is
55+
authentic and unmodified since it was built from the tag.

docs/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ documentation for four kinds of need:
1919
- [Your first attested release](tutorials/first-attested-release.md) — build,
2020
sign, attest, and verify a container image end to end using the centralized
2121
workflows.
22+
- [Wire your first attested quality gate](tutorials/first-attested-quality-gate.md)
23+
— run a CodeQL SAST gate and verify its signed, digest-bound verdict.
2224

2325
## How-to guides
2426

@@ -39,6 +41,9 @@ executable, fully self-contained onboarding protocol for any org or repo.
3941

4042
- [Reusable workflows](reference/workflows.md) — every centralized
4143
attested-delivery workflow: inputs, outputs, secrets, permissions.
44+
- [Quality-gate workflows](reference/quality-gate-workflows.md) — the SAST, SCA,
45+
Trivy, Scorecard, VEX, k6, ZAP, seam, and verify-gates workflows: role, key
46+
inputs, evidence, predicate type, signer.
4247
- [Attestation predicate definitions](reference/attestation-predicates/README.md)
4348
— the custom predicate types the quality gates sign: URI, body format, verdict
4449
rule, JSON Schema.
@@ -50,6 +55,9 @@ executable, fully self-contained onboarding protocol for any org or repo.
5055
- [Why attested delivery](explanation/attested-delivery.md) — the promotion
5156
invariant, the signing-isolation boundary, admission-time enforcement, and
5257
the change-record gate.
58+
- [Why attested quality gates](explanation/attested-quality-gates.md) — the
59+
verdict-as-attestation model, signer pinning, custom predicate types, and the
60+
"signed ≠ passed" caveat.
5361

5462
## Project plans
5563

@@ -74,3 +82,5 @@ the Diátaxis quadrants:
7482
| `dora-emit.yml` || Yes | Yes | Yes |
7583
| `pin-check.yml` || Yes | Yes ||
7684
| Admission enforcement (Kyverno / pre-deploy gate) || Yes || Yes |
85+
| Attested quality gates (SAST/SCA/Trivy/Scorecard/VEX/k6/ZAP) | Yes (SAST) | Yes | Yes | Yes |
86+
| Attestation seam (`reusable-attest-scan.yml`) | Yes | Yes | Yes | Yes |

0 commit comments

Comments
 (0)