Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Release

on:
push:
tags:
- "v*.*.*"

permissions: {}

jobs:
release:
name: Verify and publish tagged release
runs-on: ubuntu-latest
permissions:
contents: write
attestations: write
id-token: write
steps:
- name: Check out the tagged commit
uses: actions/checkout@v7
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.11"

- name: Install test tools
run: python -m pip install --upgrade pip pytest ruff

- name: Lint
run: ruff check .

- name: Compile scripts
run: python -m compileall -q skills tests scripts

- name: Run tests
run: pytest -q

- name: Build skill archives
run: ./package-skills.sh

- name: Verify release gate
run: python3 scripts/verify_release_gate.py "${GITHUB_REF_NAME}" --commit "${GITHUB_SHA}"

- name: Attest build provenance for skill archives
uses: actions/attest-build-provenance@v4
with:
subject-path: |
dist/agent-security.skill
dist/healthcheck.skill

- name: Publish GitHub release
run: |
set -euo pipefail
NOTES_FILE="$(mktemp)"
printf 'See CHANGELOG.md for the %s release notes.\n' "${GITHUB_REF_NAME}" > "${NOTES_FILE}"
gh release create "${GITHUB_REF_NAME}" \
--repo "${GITHUB_REPOSITORY}" \
--verify-tag \
--title "${GITHUB_REF_NAME}" \
--notes-file "${NOTES_FILE}"
gh release upload "${GITHUB_REF_NAME}" \
--repo "${GITHUB_REPOSITORY}" \
dist/agent-security.skill dist/healthcheck.skill dist/MANIFEST.json
env:
GH_TOKEN: ${{ github.token }}
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ This project follows semantic-versioning guidance once recurring releases are ta

### Script CLI changes

- Added `scripts/verify_release_gate.py` as a fail-closed pre-release gate for tagged releases: stable tag shape, tag/commit binding (including annotated-tag peeling), changelog version-section coverage, exact dist inventory, manifest validation, archive digest verification, secret-shaped archive scanning, and the Phase 18 deterministic rebuild check. Exit `0` on pass, `1` on gate failure, `2` on usage errors; no repository, dist, or remote mutation.
- Added the tag-gated Release workflow (`.github/workflows/release.yml`) that runs the quality gate, rebuilds archives, runs the release gate, attests build provenance for both `.skill` archives, and publishes the GitHub release with `--verify-tag`; job-scoped permissions only, and pull requests never receive release permissions. Documented in `docs/release-automation.md` with regression coverage in `tests/test_phase19_release_gate.py`.
- Replaced ad-hoc ZIP packaging with reproducible `scripts/package_skills.py`, deterministic `dist/MANIFEST.json` release metadata, and a non-mutating `--check` drift gate while preserving `./package-skills.sh`.
- Added `--format json|markdown` to `skills/agent-security/scripts/flag_prompt_injection_signals.py` for review-friendly prompt-injection signal summaries while keeping JSON as the default.
- Added `--output-dir` to `skills/agent-security/scripts/summarize_prompt_injection_corpus.py` for paired JSON/Markdown prompt-corpus review packets with no manifest or fixture mutation.
Expand Down
9 changes: 9 additions & 0 deletions docs/installation-and-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ window, and process interruption or power loss can stop between replacements.
Concurrent modification is unsupported. After any interruption, run `--check`;
do not release unless the exact inventory and every byte match.

## Tagged release automation

Tag pushes matching `v*.*.*` are verified and published by the Release workflow
described in [`docs/release-automation.md`](release-automation.md). The workflow
runs the full quality gate, rebuilds the archives deterministically, verifies them
with `scripts/verify_release_gate.py`, attests build provenance, and attaches
`agent-security.skill`, `healthcheck.skill`, and `MANIFEST.json` to the GitHub
release. See that guide for the maintainer release flow and retry path.

## Release checklist

Before tagging or publishing a release, complete this checklist from a clean checkout:
Expand Down
94 changes: 94 additions & 0 deletions docs/release-automation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Release Automation and Attestations

Phase 19 adds a review-gated tagged-release workflow. Pushing a stable `vX.Y.Z` tag
runs the full quality gate, rebuilds the skill archives deterministically, verifies
the release with a fail-closed gate script, attests build provenance for the
archives, and publishes the GitHub release. The workflow never runs on pull
requests and does not grant pull requests (or any other context) release
permissions.

## What the release workflow does

[`../.github/workflows/release.yml`](../.github/workflows/release.yml) triggers
only on tag pushes matching `v*.*.*`:

1. Checks out the tagged commit with `persist-credentials: false`.
2. Runs the same quality gate as CI: ruff, `compileall`, pytest.
3. Rebuilds the archives with `./package-skills.sh`.
4. Runs `scripts/verify_release_gate.py` (see below). Any failure blocks the release.
5. Attests build provenance for `dist/agent-security.skill` and
`dist/healthcheck.skill` with `actions/attest-build-provenance@v4`, which
requires and receives only `attestations: write` and `id-token: write` for this job.
6. Publishes the GitHub release with `gh release create --verify-tag` and uploads
`dist/agent-security.skill`, `dist/healthcheck.skill`, and `dist/MANIFEST.json`.

The job-level permissions are exactly `contents: write` (to create the release),
`attestations: write`, and `id-token: write`. The workflow-level default is
`permissions: {}`, so every permission is explicit and job-scoped. Pull requests
never receive release permissions from this workflow.

## The release gate

`scripts/verify_release_gate.py <tag> [--dist-dir dist] [--commit <sha>]` fails
closed on all of the following:

- **Tag shape:** the tag must be a stable `vMAJOR.MINOR.PATCH` tag. Pre-release
suffixes are rejected for now.
- **Tag/commit binding:** the tag must resolve to the exact commit the workflow is
building. Both sides are peeled to commit SHAs, so an annotated tag object SHA
binds correctly to its commit. This prevents publishing a release built from a
different commit than the one tagged.
- **Changelog coverage:** `CHANGELOG.md` must contain a `## X.Y.Z` section for the
tag, with released sections ordered above older releases. This stops tagging a
release whose notes still live under `Unreleased`.
- **Artifact inventory:** `dist/` must contain exactly `agent-security.skill`,
`healthcheck.skill`, and `MANIFEST.json` — nothing missing, nothing extra.
- **Manifest validity:** `MANIFEST.json` must parse, carry a schema version, and
list exactly the two published skills.
- **Digest verification:** every archive's recomputed SHA-256 must match the
manifest, so tampered or stale archives fail before publication.
- **Secret scan:** archive bytes are scanned for secret-shaped content (GitHub
tokens, AWS access key IDs, Slack tokens, private key blocks, Google API keys).
Matches are reported by label only and never echoed.
- **Deterministic rebuild:** the Phase 18 packager `--check` gate must confirm the
built artifacts byte-match a clean rebuild of the tagged sources.

Exit codes: `0` when every gate passes, `1` when any gate fails, `2` on usage
errors. The gate never modifies the repository, the dist tree, or remote state.

Run it locally before tagging:

```bash
./package-skills.sh
python3 scripts/verify_release_gate.py v0.2.0
```

## Release flow for maintainers

1. Move the intended `Unreleased` changelog entries into a new `## X.Y.Z` section
(see [versioning guidance](installation-and-release.md#versioning-guidance)).
2. Commit the changelog update and wait for CI to pass on that commit.
3. Tag the commit — annotated tags are recommended:

```bash
git tag -a vX.Y.Z -m "Release vX.Y.Z"
git push origin vX.Y.Z
```

4. The Release workflow runs the gate, attests the archives, and publishes the
GitHub release with the three dist artifacts attached.
5. Verify the release page shows the expected artifacts, and check the attestation
via the release workflow's summary or `gh attestation verify` with the
artifact digest from `dist/MANIFEST.json`.

If any gate step fails, no release is created. Fix the underlying issue, delete the
local tag, re-tag, and push again. Deleting and re-pushing a tag is the documented
retry path; the workflow itself never force-pushes or rewrites history.

## Relationship to the release checklist

The workflow automates the mechanical steps of the
[release checklist](installation-and-release.md#release-checklist) (clean rebuild,
quality gate, archive inspection, secret scan, manifest digest verification). The
human review steps — rule doc review, fixture review, and changelog wording —
still happen before tagging.
15 changes: 14 additions & 1 deletion docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,9 +360,22 @@ Before starting new roadmap work, check open PRs and avoid duplicating any branc

## Phase 19: Tagged release automation and attestations

**Status:** Planned
**Status:** Shipped
**Goal:** Add a review-gated tagged-release workflow that publishes verified archives, manifest digests, and provenance without granting pull requests release permissions.

### Shipped scope

1. Added [`scripts/verify_release_gate.py`](../scripts/verify_release_gate.py), a fail-closed pre-release gate that verifies stable tag shape, tag/commit binding with annotated-tag peeling, a matching `CHANGELOG.md` version section, exact dist inventory, manifest structure, archive SHA-256 digests against the manifest, a secret-shaped content scan over packaged archives, and the Phase 18 deterministic rebuild check.
2. Added [`docs/release-automation.md`](release-automation.md) covering the workflow's steps, gate checks, maintainer release flow, retry path, and explicit non-goals for pull-request permissions.
3. Added `.github/workflows/release.yml`, triggered only by `v*.*.*` tag pushes, with job-scoped `contents: write`, `attestations: write`, and `id-token: write` permissions, `persist-credentials: false` checkout, the full CI quality gate, deterministic rebuild, the release gate, `actions/attest-build-provenance@v4` attestations for both skill archives, and `gh release create --verify-tag` publication with `agent-security.skill`, `healthcheck.skill`, and `MANIFEST.json` attached.
4. Added `tests/test_phase19_release_gate.py` covering tag-shape validation, changelog section ordering, dist inventory/manifest/digest failure modes, annotated-tag commit binding, secret-scan reporting without echoing matches, packager-check drift detection, end-to-end gate pass/fail runs, and workflow least-privilege shape.

### Acceptance criteria

- Pushing a `vX.Y.Z` tag runs the quality gate, rebuild, release gate, and attestation steps; any failure blocks publication.
- Pull requests never gain release permissions from this workflow.
- The gate fails closed on tag/commit mismatch, missing changelog sections, artifact drift, digest mismatch, and secret-shaped archive content.

## Phase 20: Scanner report authenticity envelopes

**Status:** Planned
Expand Down
Loading
Loading