diff --git a/assets/quickstart/wizard/release-management.svg b/assets/quickstart/wizard/release-management.svg
index bc94356a8..b5cb54174 100644
--- a/assets/quickstart/wizard/release-management.svg
+++ b/assets/quickstart/wizard/release-management.svg
@@ -42,7 +42,7 @@
pmc-roster.md
Who is binding
release-build.md
- How this project builds and signs artefacts: build…
+ How this project builds and signs artefacts:…
release-management-config.md
Vote window and pass rule, distribution backend and…
release-trains.md
diff --git a/docs/designs/2026-09-20-reproducible-releases.md b/docs/designs/2026-09-20-reproducible-releases.md
new file mode 100644
index 000000000..b238ab5dc
--- /dev/null
+++ b/docs/designs/2026-09-20-reproducible-releases.md
@@ -0,0 +1,240 @@
+
+
+
+
+**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
+
+- [Reproducible releases](#reproducible-releases)
+ - [What was wrong](#what-was-wrong)
+ - [Decisions](#decisions)
+ - [The source artefact is a function of the tag](#the-source-artefact-is-a-function-of-the-tag)
+ - [What ships is decided once, in the open, and committed before the tag](#what-ships-is-decided-once-in-the-open-and-committed-before-the-tag)
+ - [The record: commit, SWHID, origin, epoch, digest](#the-record-commit-swhid-origin-epoch-digest)
+ - [Convenience artefacts are the project's, and reproducibility is their acceptance test](#convenience-artefacts-are-the-projects-and-reproducibility-is-their-acceptance-test)
+ - [The vote text carries the verification path](#the-vote-text-carries-the-verification-path)
+ - [Automated signing is an ASF option, gated on all of the above](#automated-signing-is-an-asf-option-gated-on-all-of-the-above)
+ - [What was built](#what-was-built)
+ - [Alternatives considered](#alternatives-considered)
+ - [Known limits](#known-limits)
+ - [Related work outside this repository](#related-work-outside-this-repository)
+
+
+
+
+
+# Reproducible releases
+
+| | |
+|---|---|
+| **Status** | Built, in [apache/magpie#1296](https://github.com/apache/magpie/pull/1296). The ASF automated-signing option is designed and wired but no project has exercised it end to end; the ATR side of the SWHID comparison depends on ATR exposing the value it computes. |
+| **Created** | 2026-09-20 |
+| **Origin** | Magpie's own `0.1.0-rc1` got a `-1` for a source artefact nobody could regenerate; the follow-up asked for the full reproducible-builds.org treatment, an education step for what a source release should contain, and a path to CI-signed releases. |
+
+A signature proves who packed an archive. It says nothing about whether the
+archive is the tree that was voted on. Reproducibility is what closes that
+gap: if two people can regenerate the same bytes from the same tag, the
+release manager's machine stops being a trust boundary. This design makes
+that property the default for the source artefact, the acceptance test for
+anything shipped besides it, and the precondition for letting CI sign.
+
+## What was wrong
+
+- The release skills said *"build the artefact"* and left the how to each
+ project. Magpie's own first RC was a `zip -r` of a working tree: it carried
+ `__pycache__`, dangling agent-view symlinks, and a shape no voter could
+ reproduce. `git archive` fixed the contents, but its bytes still depend on
+ the `git` version that runs it, so two voters could not compare digests.
+- Nothing in the process asked a project, once, what belongs in its source
+ release. `.gitattributes` `export-ignore` was known folklore, not a step.
+- "Binary" meant one global rebuild command in the config and a hard-coded
+ list of the framework's own validators in `release-verify-rc`. Both were
+ Magpie-specific; neither fitted a project shipping wheels, jars and a
+ container image.
+- The `[VOTE]` mail told voters where the artefacts were and nothing about
+ how to check them. ATR's default vote text links only the candidate page.
+
+## Decisions
+
+### The source artefact is a function of the tag
+
+The source archive is exported from the tag, never packed from a working
+tree, and the export is normalised by a tool the framework ships
+([`tools/reproducible-archive`](../../tools/reproducible-archive/README.md))
+rather than by whatever `tar`, `zip`, `gzip` or `git` the release manager
+has. The tool applies every rule on
+[reproducible-builds.org § Archive metadata](https://reproducible-builds.org/docs/archives/)
+— one `SOURCE_DATE_EPOCH` (the tag's committer time), locale-independent
+ordering, uid/gid 0, `a=rX,u+w` modes, no PAX `atime`/`ctime`, `gzip -n`,
+`zip -X` — and pins the two inputs the page does not mention that still vary
+between machines: the builder's `core.autocrlf` / `core.eol` (which `git
+archive` would apply to `text` files) and the archive writer itself. It is
+stdlib-only and single-file so a CI workflow or a project that does not adopt
+Magpie can embed it verbatim.
+
+The reproducibility of the *source* is the property that matters most, not
+the least. The `xz` compromise was a source tarball that did not match the
+repository at its stated commit; a distribution that could rebuild the
+tarball from the tag and compare would have caught it. Convenience binaries
+are the place where "build it yourself" is the traditional advice; the source
+archive is where nobody used to check, because it looked like it could not be
+tampered with.
+
+### What ships is decided once, in the open, and committed before the tag
+
+`git archive` reads `export-ignore` from the tree it archives, so the
+decision about what a source release contains has to be in the tree before
+the RC tag exists. That put the review into `release-prepare prep`, as an
+education step on the first release: list what would ship, classify every
+top-level path (source, legal files, inputs to the checks voters run,
+foundation metadata, VCS / CI / editor / lint / agent-view metadata, scratch),
+check references before proposing an exclusion, confirm each entry with the
+release manager, land `.gitattributes` in the prep PR, and record that the
+review happened. `release-rc-cut` refuses to cut while it is outstanding.
+Later releases get a drift check on new top-level paths.
+
+### The record: commit, SWHID, origin, epoch, digest
+
+Every RC records, on the planning issue and in the `[VOTE]`:
+
+- the commit the tag points to, and the URL of the repository it lives in;
+- the [Software Heritage identifier](https://swhid.org/) of the archive's
+ expanded content, `swh:1:dir:`, with `origin=` and
+ `anchor=swh:1:rev:` qualifiers;
+- `SOURCE_DATE_EPOCH` and the sha512 of the archive.
+
+The SWHID is the load-bearing one. A directory SWHID is computed exactly as
+git computes a tree id — from names, modes and contents alone — so it is
+intrinsic to the files: a voter recomputes it from the staged bytes without
+git, ATR computes the same value at compose time, and it equals `git
+rev-parse ^{tree}` unless `.gitattributes` altered the export (in which
+case the difference is itself the record of what was left out). Unlike a
+digest of the archive it does not depend on the container, so a `.tar.gz`
+and a `.zip` of the same tree carry the same SWHID, and a convenience
+artefact can name the source SWHID it was built from. Recording the SWHID
+next to the commit and the origin, rather than the commit alone, gives a
+reference that outlives the repository's hosting and that ATR can be checked
+against directly.
+
+### Convenience artefacts are the project's, and reproducibility is their acceptance test
+
+What a project ships besides the source — a binary tarball, wheels, jars, a
+container image, a chart — is project-specific by nature, so the framework
+assumes none. `release-build.md § Convenience artefacts` declares each one
+with its own build command, staging target, reproducibility mode, vote scope
+and publish channel, and every lifecycle step reads the list: `rc-cut` builds
+and stages, `verify-rc` rebuilds and compares, `vote-draft` lists,
+`promote` publishes, `announce-draft` names channels.
+
+A convenience artefact is *good* only if it is demonstrably built from the
+voted source. A binary cannot be reviewed, only rebuilt; rebuilding from the
+tag under the same `SOURCE_DATE_EPOCH` and comparing — bit for bit, or with
+every difference written down — is the one check that establishes what it
+contains. `release-promote` withholds the publish command for an artefact
+whose `verify-rc` rebuild did not reproduce, and never holds the source
+promotion back for it: the source is the release, the artefact is a courtesy,
+and a courtesy that cannot be verified is withheld, not shipped.
+
+### The vote text carries the verification path
+
+Every `[VOTE]` body carries a *How to verify this candidate* section: the
+record above, the agentic one-liner (`verify-rc`), the project's
+human-readable verification page at the RC tag, the convenience artefacts and
+their vote scope, the ATR candidate page, and the voter-obligation sentence
+from the release policy. Under ATR the drafted body is what the release
+manager supplies to the platform; the default text is not allowed to stand.
+
+### Automated signing is an ASF option, gated on all of the above
+
+[Infra § Automated release signing](https://infra.apache.org/release-signing.html#automated-release-signing)
+lets an ASF project have CI sign what it builds, provided every signed
+artefact is reproducible, CI stages only, and a committer re-validates every
+artefact bit-for-bit on trusted hardware before publication. The framework
+offers it only under `organization: ASF` (resolved from the organization
+manifest, `null` elsewhere), as a one-time drafting sub-command that produces
+the Infra ticket, the Security Team notification and a workflow PR with no
+key material, and it changes the lifecycle only once enabled: the RM pushes a
+signed tag, CI builds and stages, `verify-rc` becomes mandatory at a
+byte-identical bar with the committer's own trusted-hardware assertion, and
+`promote` blocks without that attestation. The agent still holds no key.
+
+## What was built
+
+| Piece | Where |
+|---|---|
+| `repro-archive build` / `check` / `compare` / `swhid` / `recipe` / `epoch` | [`tools/reproducible-archive`](../../tools/reproducible-archive/README.md) |
+| `§ Source archive`, `§ Convenience artefacts`, `§ Source-tree validators`, `§ Reproducibility checks` | [`projects/_template/release-build.md`](../../projects/_template/release-build.md) |
+| `vote_verification_doc_url`, `reproducibility_doc_url`, `vote_verification_skill`, `automated_release_signing` | [`projects/_template/release-management-config.md`](../../projects/_template/release-management-config.md) |
+| First-release `.gitattributes` review (prep Step 2e); `automated-signing` sub-command | [`release-prepare`](../../skills/release-prepare/SKILL.md) |
+| Reproducible source build, per-artefact builds, self-check (Step 2b), CI-signed flow (Step 2c), the record | [`release-rc-cut`](../../skills/release-rc-cut/SKILL.md) |
+| Config-driven Step 7 validators; Step 9 rebuild-and-compare with SWHID check | [`release-verify-rc`](../../skills/release-verify-rc/SKILL.md) |
+| *How to verify* section with the record and the artefact list | [`release-vote-draft`](../../skills/release-vote-draft/SKILL.md) |
+| Per-artefact publish, reproducibility gate, trusted-hardware gate | [`release-promote`](../../skills/release-promote/SKILL.md) |
+| Rationale for adopters and voters | [`docs/release-management/reproducibility.md`](../release-management/reproducibility.md) |
+| CI workflow template (ASF automated signing) | [`projects/_template/workflows/release-candidate.yml`](../../projects/_template/workflows/release-candidate.yml) |
+
+## Alternatives considered
+
+- **Plain `git archive` as the build command.** Correct contents, but the
+ bytes depend on the `git` version: the zip writer's compression and the
+ tar's PAX handling have changed between releases. A voter on another
+ version gets `content-identical`, never `identical`, and cannot compare
+ digests with the RM or with ATR. Kept as the documented fallback recipe
+ (`repro-archive recipe` prints the GNU tar / Info-ZIP equivalent).
+- **Shell recipe instead of a Python tool.** The reproducible-builds.org
+ recipe needs GNU tar ≥ 1.28 and Info-ZIP flags that macOS's stock tools
+ lack, and a shell script cannot be unit-tested against git's own tree
+ hashing. The tool is stdlib-only and single-file so it costs nothing to
+ embed; the shell recipe stays available.
+- **One global `binary_rebuild_command`.** Fitted a project with one binary
+ and none with several; said nothing about where each goes or whether it is
+ in the vote. Replaced by the per-artefact list.
+- **Holding the source promotion until every convenience artefact
+ reproduces.** Rejected: the source is the release and its vote is what
+ passed; a courtesy artefact that cannot be verified is withheld on its own.
+- **Extracting the archive and running `swh identify` / `asfswhid` for the
+ SWHID.** Same value, but it adds a dependency and an extraction step to a
+ tool that already has every member in memory. The in-memory tree hashing is
+ tested against `git write-tree` over the extracted tree, so the two agree
+ by construction; a voter who prefers `asfswhid` gets the same identifier.
+- **Recording only the commit and repository URL.** A commit id identifies a
+ repository object; the SWHID of the expanded content identifies what
+ shipped, survives a repository move, is the same across archive formats,
+ and is what ATR computes. Both are recorded; the SWHID is the one to compare.
+- **Offering automated signing to every organization.** The policy, the key
+ provisioning and the approval body are ASF Infra's. Other organizations can
+ add an equivalent block to their manifest; without one the option does not
+ exist in the skills.
+
+## Known limits
+
+- **Compression bytes.** `identical` requires the same deflate output. zlib's
+ output at a given level has been stable for years, but zlib-ng or a
+ different Python build can differ. The SWHID and `compare`'s
+ `content-identical` verdict are container-independent; the record carries
+ both so a voter can tell "different bytes, same tree" from "different tree".
+- **`export-subst`.** Deterministic for a given commit but it rewrites
+ content, so the archive's SWHID differs from the repository tree's; the
+ note `repro-archive build` prints says which case applies.
+- **Registry-staged artefacts.** Container images and packages staged in a
+ registry are compared by digest against a local rebuild; the mechanics of
+ pulling by digest are the project's, not the framework's.
+- **The ATR comparison** depends on ATR showing its SWHID for the candidate;
+ the value is computed today but not yet exposed in the interface.
+
+## Related work outside this repository
+
+- [apache/tooling-actions#37](https://github.com/apache/tooling-actions/pull/37),
+ an `upload-source-to-atr` action that builds a `git archive | gzip -n`
+ source archive on CI, computes its SWHID with
+ [`asfswhid`](https://github.com/apache/tooling-asfswhid), signs it and
+ uploads to ATR. Same rules, same identifier; the discussion there is where
+ the "SWHID next to the commit" decision comes from, and where binaries were
+ agreed to be each project's concern — which is what `§ Convenience
+ artefacts` encodes.
+- [reproducible-builds.org § Archive metadata](https://reproducible-builds.org/docs/archives/)
+ and [§ `SOURCE_DATE_EPOCH`](https://reproducible-builds.org/docs/source-date-epoch/).
+- [SWHID specification](https://swhid.org/) (ISO/IEC 18670:2025) and the
+ [Software Heritage persistent identifiers](https://docs.softwareheritage.org/devel/swh-model/persistent-identifiers.html)
+ documentation.
diff --git a/docs/designs/README.md b/docs/designs/README.md
index 95d0b6811..ba831158b 100644
--- a/docs/designs/README.md
+++ b/docs/designs/README.md
@@ -23,6 +23,7 @@ what was designed and deliberately not built.
|---|---|
| [Install, adopt, upgrade](2026-09-13-install-adopt-upgrade.md) | Built, bar two items it names |
| [Body-owned configuration layers](2026-09-17-body-owned-config-layers.md) | Proposed — depends on the Incubator PMC and ComDev |
+| [Reproducible releases](2026-09-20-reproducible-releases.md) | Built (apache/magpie#1296); the ASF automated-signing path and the ATR SWHID comparison await first use |
One document per subject, describing the result rather than the phases it was
built in. While a design is being implemented it may be split into plans; when
diff --git a/docs/labels-and-capabilities.md b/docs/labels-and-capabilities.md
index 92dbbbf4d..61d099eac 100644
--- a/docs/labels-and-capabilities.md
+++ b/docs/labels-and-capabilities.md
@@ -144,6 +144,7 @@ framework substrate:
| `substrate:action-guard` | substrate | Deterministic pre-tool-use command guards. |
| `substrate:privacy` | substrate | PII redaction / approved-LLM gating. |
| `substrate:framework-dev` | substrate | Build / validate / eval the framework itself. |
+| `substrate:release` | substrate | Release-artefact helpers an adopter's release process runs: reproducible-archive build, lint and comparison. |
### Coverage qualifiers
@@ -337,6 +338,7 @@ or a contract-free mix of substrates (e.g. `tools/spec-inventory` is
| [`tools/spec-status-index`](../tools/spec-status-index/) | `substrate:framework-dev` + `substrate:analytics` | Index of spec / RFC implementation status — framework-dev substrate that also doubles as a governance/stats view (`analytics`) |
| [`tools/vendor-neutrality-score`](../tools/vendor-neutrality-score/) | `substrate:framework-dev` + `substrate:analytics` | Deterministic vendor-neutrality score — reads each contract tool's `**Kind:**` / `**Vendor:**` metadata and scores per-contract + per-skill neutrality (`analytics`); backs the score block in [`docs/vendor-neutrality.md`](vendor-neutrality.md) |
| [`tools/spec-validator`](../tools/spec-validator/) | `substrate:framework-dev` | Spec-frontmatter and body-section validator — counterpart to `skill-and-tool-validator` for `tools/spec-loop/specs/` |
+| [`tools/reproducible-archive`](../tools/reproducible-archive/) | `substrate:release` | `repro-archive` — build, lint and compare reproducible source archives from a git ref (`git archive` + `.gitattributes` `export-ignore`, every reproducible-builds.org archive rule applied); used by `release-rc-cut` Step 2/2b and `release-verify-rc` Step 9 |
| [`tools/symlink-lint`](../tools/symlink-lint/) | `substrate:framework-dev` | Self-adoption symlink hygiene — rejects cyclic symlinks, misdirected skill relays (canonical/relay target-correctness), and incomplete self-adoption symlink sets |
| [`tools/pilot-report-validator`](../tools/pilot-report-validator/) | `substrate:framework-dev` | Adopter pilot-report validator — required frontmatter keys, no unfilled placeholders, valid profile, and required body sections; counterpart to `spec-validator` for `docs/pilot-report-template.md` |
| [`tools/skill-reconciler-diff`](../tools/skill-reconciler-diff/) | `substrate:framework-dev` | Deterministic structural diff between two skill trees — parses frontmatter, section headings, step inventory, placeholders, support files, and safety-baseline clauses into a JSON diff object for the `skill-reconciler` skill |
diff --git a/docs/mode-economics.md b/docs/mode-economics.md
index 4cf9745bf..db2049709 100644
--- a/docs/mode-economics.md
+++ b/docs/mode-economics.md
@@ -84,7 +84,7 @@ history separately provides its publication revision and date.
-Measured on (UTC): 2026-09-19.
+Measured on (UTC): 2026-09-20.
Tokenizer: **tiktoken 0.14.0, `cl100k_base`**. Method: full UTF-8 file,
including frontmatter and comments; line endings normalized to LF;
@@ -92,7 +92,7 @@ special-token spellings counted as ordinary text.
Coverage: **75 of 75 local `skills/*/SKILL.md` files**.
External `source.md` redirects and harness symlinks are excluded.
-Measurement manifest SHA-256: `316bec2ddc04557527ec9ab4010987220709ccced9df8acc2f3e7a27dcaf439d`.
+Measurement manifest SHA-256: `0ef06f4874382739a4738d35ec92e689f0483db6e99292c4accc72836d0619ad`.
| Skill file | Measured tokens | Source SHA-256 (first 16 characters) |
|---|---:|---|
@@ -131,15 +131,15 @@ Measurement manifest SHA-256: `316bec2ddc04557527ec9ab4010987220709ccced9df8acc2
| [pr-management-triage](../skills/pr-management-triage/SKILL.md) | 12,685 | `bdd0cae06e589165` |
| [pr-stale-sweep](../skills/pr-stale-sweep/SKILL.md) | 7,804 | `c63539a5c0662d52` |
| [pre-first-pr-check](../skills/pre-first-pr-check/SKILL.md) | 4,525 | `1a901a80838e83b0` |
-| [release-announce-draft](../skills/release-announce-draft/SKILL.md) | 6,991 | `7fd2b0720eaae5ca` |
+| [release-announce-draft](../skills/release-announce-draft/SKILL.md) | 7,053 | `dfd3058bc1b8034f` |
| [release-archive-sweep](../skills/release-archive-sweep/SKILL.md) | 5,604 | `6e30100ea5a633dd` |
| [release-audit-report](../skills/release-audit-report/SKILL.md) | 6,774 | `ce849ac8e2a217d1` |
| [release-keys-sync](../skills/release-keys-sync/SKILL.md) | 5,945 | `3c11551de0e1e5f9` |
-| [release-prepare](../skills/release-prepare/SKILL.md) | 7,960 | `5b76de4222c13166` |
-| [release-promote](../skills/release-promote/SKILL.md) | 7,174 | `6f6bff9d291fab91` |
-| [release-rc-cut](../skills/release-rc-cut/SKILL.md) | 7,614 | `e557be43124d9c5b` |
-| [release-verify-rc](../skills/release-verify-rc/SKILL.md) | 8,777 | `1561f8f82f15a5eb` |
-| [release-vote-draft](../skills/release-vote-draft/SKILL.md) | 6,327 | `73c6490d4351da9e` |
+| [release-prepare](../skills/release-prepare/SKILL.md) | 11,986 | `78f302ca28e1aa40` |
+| [release-promote](../skills/release-promote/SKILL.md) | 8,044 | `c17053e63005b55b` |
+| [release-rc-cut](../skills/release-rc-cut/SKILL.md) | 12,942 | `25d4f5d7b6d5a1ab` |
+| [release-verify-rc](../skills/release-verify-rc/SKILL.md) | 11,881 | `07eb370e2462f1b5` |
+| [release-vote-draft](../skills/release-vote-draft/SKILL.md) | 7,821 | `d0bc0fd3c11914a9` |
| [release-vote-tally](../skills/release-vote-tally/SKILL.md) | 6,696 | `c848e809e2d877cd` |
| [report-framework-issue](../skills/report-framework-issue/SKILL.md) | 5,703 | `30b2ce8b774ea68c` |
| [reviewer-routing](../skills/reviewer-routing/SKILL.md) | 6,272 | `dcd75b720d42af34` |
diff --git a/docs/release-management/README.md b/docs/release-management/README.md
index 9cf12562d..f7c5a64e2 100644
--- a/docs/release-management/README.md
+++ b/docs/release-management/README.md
@@ -123,7 +123,7 @@ says which file is missing.
| File | What it carries | Read by |
|---|---|---|
| [`pmc-roster.md`](../../projects/_template/pmc-roster.md) | Who is binding. Read wherever a vote is counted or a PMC-only action is gated. | `promote`, `vote-tally` |
-| [`release-build.md`](../../projects/_template/release-build.md) | How this project builds and signs artefacts: build command, artefact names, checksum algorithm, signing-key expectations. | `rc-cut`, `verify-rc` |
+| [`release-build.md`](../../projects/_template/release-build.md) | How this project builds and signs artefacts: reproducible source-archive recipe (`git archive` + `.gitattributes`), build command, the project's optional convenience artefacts (each with its own build, staging, reproducibility mode, vote scope and publish channel), artefact names, checksum algorithm, optional reproducibility checks. | `rc-cut`, `verify-rc` |
| [`release-management-config.md`](../../projects/_template/release-management-config.md) | Vote window and pass rule, distribution backend and paths, announce/vote list addresses, retention rule. | `announce-draft`, `archive-sweep`, `audit-report`, `keys-sync`, `prepare`, `promote`, `rc-cut`, `verify-rc`, `vote-draft`, `vote-tally` |
| [`release-trains.md`](../../projects/_template/release-trains.md) | Active release branches, release-manager attribution per cut, rotation rosters, security-team roster. | `archive-sweep`, `prepare` |
diff --git a/docs/release-management/atr-release-runbook.md b/docs/release-management/atr-release-runbook.md
index d84b89eef..fd61168ea 100644
--- a/docs/release-management/atr-release-runbook.md
+++ b/docs/release-management/atr-release-runbook.md
@@ -374,7 +374,19 @@ the binding votes.
It produces the subject
(`[VOTE] Release Apache Magpie from -rcN`) and
body — pointing voters at the ATR candidate page and its check
- results.
+ results, and carrying the **How to verify this candidate** section
+ every Magpie `[VOTE]` has: the reproducibility record (source
+ commit, `SOURCE_DATE_EPOCH`, sha512 from the planning issue), the
+ agentic one-liner
+ (`/magpie-release-management:verify-rc -rcN`), the
+ human-readable page
+ ([`manual-release-process.md` § Manual verification](manual-release-process.md#manual-verification--what-a-voter-runs-before-1)
+ at the RC tag), the [reproducibility background](reproducibility.md),
+ and the voter-obligation sentence. ATR's default vote text links
+ only the candidate page, so **the drafted body is what the RM
+ supplies to ATR** (the client's body option or the vote form on the
+ candidate page; confirm with `atr vote start --help`) — do not let
+ the default stand.
2. **Start the vote in ATR.** The RM triggers the vote for the
composed candidate; ATR sends the `[VOTE]` email to
`dev@magpie.apache.org` and opens the tabulation. Starting the
@@ -393,6 +405,22 @@ the binding votes.
[release-policy § release approval](https://www.apache.org/legal/release-policy.html#release-approval);
the Magpie config may lengthen but not shorten it
([`release-management-config.md` § Vote](../../projects/_template/release-management-config.md#vote)).
+ What a PMC member does during the window, in either order:
+ - **Agentic:** `/magpie-release-management:verify-rc -rcN`
+ from any Magpie-enabled agent — read-only; it fetches the staged
+ artefacts, checks signature / checksum / RAT / LICENSE-NOTICE /
+ binaries / links / version strings, rebuilds the source archive
+ from the tag and reports `identical`, `content-identical` or
+ `differs`.
+ - **Manual:** the linked
+ [verification page](manual-release-process.md#manual-verification--what-a-voter-runs-before-1)
+ at the RC tag — the same checks longhand, ending with a build and
+ test run from the unpacked source.
+ - **Reply** on the thread using the
+ [reply template](manual-release-process.md#reply-template--what-to-put-in-your-vote):
+ the commit you verified, the `compare` verdict, and what you built
+ and tested on. A binding `+1` is the voter's own statement, not the
+ tool's.
4. **Tally** (Step 9). ATR tabulates the replies; cross-check with
[`release-vote-tally`](../../skills/release-vote-tally/SKILL.md),
which classifies each reply binding-vs-non-binding against the
@@ -473,7 +501,13 @@ This does **not** move the signing key into CI unless the project has
adopted a reproducible-build + trusted-publishing model the PMC has
explicitly signed off on. For Magpie's first releases, prefer the
local `atr` client path above (Step C); revisit CI-driven compose once
-the build is demonstrably reproducible. See the
+the build is demonstrably reproducible. When it is, `release-prepare
+automated-signing` drafts the Infra key request, the Security Team
+notification and the workflow PR (template:
+[`projects/_template/workflows/release-candidate.yml`](../../projects/_template/workflows/release-candidate.yml))
+under the conditions in
+[Infra § Automated release signing](https://infra.apache.org/release-signing.html#automated-release-signing);
+see [`reproducibility.md`](reproducibility.md#automated-release-signing--asf-specific-optional). See the
[`tooling-asf-example`](https://github.com/apache/tooling-asf-example)
repository for a worked GitHub Actions example.
diff --git a/docs/release-management/manual-release-process.md b/docs/release-management/manual-release-process.md
index 211a61844..a28bb1b77 100644
--- a/docs/release-management/manual-release-process.md
+++ b/docs/release-management/manual-release-process.md
@@ -24,6 +24,7 @@
- [5. Confirm the source matches the tagged commit](#5-confirm-the-source-matches-the-tagged-commit)
- [6. License headers (Apache RAT)](#6-license-headers-apache-rat)
- [7. Optional — reproduce the project's own checks from pristine source](#7-optional--reproduce-the-projects-own-checks-from-pristine-source)
+ - [Reply template — what to put in your vote](#reply-template--what-to-put-in-your-vote)
- [Caveats hit during rc1 / rc2](#caveats-hit-during-rc1--rc2)
- [Release Manager checklist](#release-manager-checklist)
- [Cross-references](#cross-references)
@@ -339,6 +340,19 @@ the `diff` output. (If your `git` version happens to produce byte-identical
but the tree `diff` is the version-independent check.) If the tag is signed,
`git tag -v "${VERSION}-${RC}"` also confirms it has not moved.
+The framework's `repro-archive` tool does the same check in one step and
+tells the two cases apart — `identical` (same bytes), `content-identical`
+(same tree, archive metadata differs) or `differs` — using the
+`SOURCE_DATE_EPOCH` the RM recorded on the planning issue
+([reproducibility.md](reproducibility.md)):
+
+```bash
+uv run --project tools/reproducible-archive repro-archive build \
+ --ref "${VERSION}-${RC}" --format zip --prefix "apache-magpie-${VERSION}" \
+ --epoch "" -o /tmp/rebuilt.zip
+uv run --project tools/reproducible-archive repro-archive compare "../${ARTIFACT}" /tmp/rebuilt.zip
+```
+
### 6. License headers (Apache RAT)
Run [Apache RAT](https://creadur.apache.org/rat/) over the unpacked tree
@@ -370,6 +384,34 @@ Only after these pass should a voter post `+1` (binding voters: your `+1`
carries the release). Report any failure on the `[VOTE]` thread with the
exact command and output.
+The whole sequence above is what
+[`release-verify-rc`](../../skills/release-verify-rc/SKILL.md) runs for
+you (`/magpie-release-management:verify-rc -rcN` from any
+Magpie-enabled agent): it emits each command, records the results, and
+adds the source rebuild-and-compare from
+[`reproducibility.md`](reproducibility.md). Use whichever path you
+prefer; the `[VOTE]` email links both.
+
+### Reply template — what to put in your vote
+
+State what you verified, so the tally reads as evidence rather than a
+count:
+
+```text
++1 (binding)
+
+Verified apache-magpie--source.zip from dist/dev at r:
+- signature OK against KEYS (), sha512 matches
+- tag -rcN = commit ; rebuilt with repro-archive at
+ SOURCE_DATE_EPOCH : identical (or: content-identical / differs — say which)
+- RAT clean; LICENSE + NOTICE present; no binaries, no dangling links
+- built and ran the test suite from the unpacked source on
+```
+
+Under automated release signing add *"rebuilt on my own hardware"* —
+that line is the trusted-hardware validation the policy requires
+([`reproducibility.md` § Automated release signing](reproducibility.md#automated-release-signing--asf-specific-optional)).
+
## Caveats hit during rc1 / rc2
Real friction from the `0.1.0` iterations, recorded so the next RM expects
diff --git a/docs/release-management/process.md b/docs/release-management/process.md
index d1b041f1b..4d5bd79a4 100644
--- a/docs/release-management/process.md
+++ b/docs/release-management/process.md
@@ -189,6 +189,19 @@ clears.
Output: a single PR proposed against the release branch, RM merges
after their own review.
+On a project's **first release** (or whenever
+`release-build.md § Source archive` has no `export_ignore_reviewed`
+marker) the same prep PR carries the **source-archive contents
+review**: a guided walk through every top-level path of the
+repository that classifies what the `git archive` source artefact
+ships, proposes the `.gitattributes` `export-ignore` entries that keep
+VCS / CI / editor metadata out (never `LICENSE`, `NOTICE`, build
+inputs, or a path a shipped file references), and records the
+decision. The attributes must be committed before the RC tag exists,
+which is why the review lives here and why Step 4 blocks while it is
+outstanding. See
+[`reproducibility.md` § The first-release `.gitattributes` review](reproducibility.md#the-first-release-gitattributes-review).
+
> [!NOTE]
> The Step 2 `LICENSE` / `NOTICE` draft is *provisional*. It is
> drafted before the build, so it covers only content the skill can
@@ -226,18 +239,49 @@ Agentic Drafting.
The skill emits a paste-ready command sequence:
1. `git tag -s -rcN -m "..."` (signed tag, RM's key).
-2. Build invocation, project-specific
- (`/release-build.md`).
-3. `gpg --detach-sign --armor ` for each artefact.
-4. `sha512sum > .sha512` for each artefact.
+2. Build invocation. The **source artefact** is, by default, a
+ reproducible export of the tag — `repro-archive build`, which is
+ `git archive` (tracked files only, `.gitattributes` `export-ignore`
+ honoured) with every
+ [reproducible-builds.org archive rule](https://reproducible-builds.org/docs/archives/)
+ applied, so a voter rebuilding from the tag gets byte-identical
+ bytes. **Convenience artefacts** — whatever the project ships
+ besides the source (binary tarball, wheels, jars, a container
+ image, a chart) — are project-specific by nature and are declared
+ one by one in
+ [`/release-build.md` § Convenience artefacts](../../projects/_template/release-build.md);
+ each entry's own `build_command` follows, under the same
+ `SOURCE_DATE_EPOCH`. A source-only project declares none.
+3. *Optional* reproducibility self-check (`release-build.md
+ § Reproducibility checks`): lint the archive, rebuild it, compare;
+ rebuild every convenience artefact and compare. A `differs` stops
+ the cut before anything is signed — a convenience artefact that
+ cannot be rebuilt from the tag is not known to be what the source
+ produces.
+4. `gpg --detach-sign --armor ` for each artefact.
+5. `sha512sum > .sha512` for each artefact.
The skill writes nothing to disk and runs nothing locally. The RM
runs every command on their own machine, with their own key, in
their own checkout. The skill's output is the *recipe*; correctness
of the recipe is reviewable independently from execution. After the
-RM reports back the artefact list + checksums + sig filenames, the
-skill records them in the planning issue's audit-trail comment for
-Step 13.
+RM reports back the artefact list + checksums + sig filenames — plus
+the source commit, `SOURCE_DATE_EPOCH` and sha512 that make the
+artefact reproducible — the skill records them in the planning
+issue's audit-trail comment for Step 13.
+
+> [!NOTE]
+> **🪶 ASF-specific option — automated release signing.** An ASF
+> project may, after the one-time setup in `release-prepare
+> automated-signing` (Infra-provisioned key, Security Team approval,
+> reproducible-build workflow), let CI sign and stage the artefacts
+> ([Infra § Automated release signing](https://infra.apache.org/release-signing.html#automated-release-signing)).
+> Step 4 then reduces to pushing the RM-signed tag; steps 3 and 5 run
+> in CI; Step 6's reproducibility check becomes mandatory as the
+> policy's validation on trusted hardware; and Step 10 refuses to
+> promote without it. See
+> [`reproducibility.md` § Automated release signing](reproducibility.md#automated-release-signing--asf-specific-optional).
+> The option is offered only under `organization: ASF`.
> [!NOTE]
> Detached `.asc` signatures and `.sha512` checksums are the ASF
@@ -292,6 +336,19 @@ Read-only. The skill fetches the staged artefacts from
binary-exclusion list).
- **Version string consistency** between artefact filename, embedded
manifests, and tag.
+- **Reproducibility** (optional, per
+ [`/release-build.md` § Reproducibility checks](../../projects/_template/release-build.md);
+ mandatory under automated release signing): the source artefact is
+ rebuilt from the tag with `repro-archive build` at the recorded
+ `SOURCE_DATE_EPOCH` and compared with the staged one —
+ `identical`, `content-identical` (only archive metadata differs) or
+ `differs` (not the tagged tree, a `-1`); every convenience artefact
+ is rebuilt and compared byte-for-byte, or against the project's
+ documented divergences. For a convenience artefact this is the
+ check that decides whether it is *good*: a binary cannot be
+ reviewed, only rebuilt, and one that does not reproduce from the
+ voted source is withheld from publication in Step 10. See
+ [`reproducibility.md`](reproducibility.md).
The skill emits a pass/fail report to the planning issue. A failure
does not auto-flip any label; the RM decides whether to roll a new
@@ -380,6 +437,24 @@ commit` under their own ASF credentials.
This is **the moment of release**. The skill writes nothing and
runs nothing; the human commit is the act.
+When the project declares **convenience artefacts**
+([`release-build.md` § Convenience artefacts](../../projects/_template/release-build.md)),
+the skill follows the source promotion with each artefact's own
+`publish_command` to its declared channel (PyPI, Maven Central, a
+container registry, a chart repository, …) — project-specific
+commands the config supplies, never invented. It emits a publish
+command only for an artefact that `release-verify-rc` reproduced from
+the voted tag (`identical`, or documented divergence only); an
+artefact that did not reproduce gets a HOLD note instead. The source
+promotion is never held back by a convenience artefact.
+
+🪶 ASF-specific: under `automated_release_signing: enabled` the skill
+additionally requires the planning issue to carry a `release-verify-rc`
+attestation that every artefact was rebuilt bit-by-bit identical on a
+committer's own hardware — the validation step
+[Infra § Automated release signing](https://infra.apache.org/release-signing.html#automated-release-signing)
+mandates before publication — and blocks without it.
+
The `dist/release/` tree is PMC-write-only by default
([release-policy.html](https://www.apache.org/legal/release-policy.html)).
If the RM is a committer but not a PMC member, the `svn mv` will
@@ -531,6 +606,12 @@ state machine participant.
- [`spec.md`](spec.md), per-skill scope, state-change boundary,
hand-off protocol, adopter knobs.
- [`projects/_template/release-management-config.md`](../../projects/_template/release-management-config.md), adopter contract scaffold.
+- [`reproducibility.md`](reproducibility.md), the reproducible source
+ archive (`git archive` + `.gitattributes` + the reproducible-builds.org
+ rules), the optional reproducibility checks for source and binaries,
+ and the 🪶 ASF-specific automated-release-signing option.
+- [`tools/reproducible-archive`](../../tools/reproducible-archive/README.md),
+ the `repro-archive` tool Steps 4 and 6 use.
- [`docs/modes.md` § Drafting](../modes.md#drafting),
[`§ Triage`](../modes.md#triage), the modes the skills inhabit.
- [`MISSION.md` § Initial Goals](../../MISSION.md#initial-goals),
diff --git a/docs/release-management/reproducibility.md b/docs/release-management/reproducibility.md
new file mode 100644
index 000000000..f7369e70e
--- /dev/null
+++ b/docs/release-management/reproducibility.md
@@ -0,0 +1,270 @@
+
+
+
+
+**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
+
+- [Reproducible source archives, reproducibility checks, and automated signing](#reproducible-source-archives-reproducibility-checks-and-automated-signing)
+ - [Why](#why)
+ - [The source archive is `git archive` plus `.gitattributes`](#the-source-archive-is-git-archive-plus-gitattributes)
+ - [The first-release `.gitattributes` review](#the-first-release-gitattributes-review)
+ - [Later releases: drift](#later-releases-drift)
+ - [The archive rules from reproducible-builds.org](#the-archive-rules-from-reproducible-buildsorg)
+ - [The record: commit, SWHID, origin](#the-record-commit-swhid-origin)
+ - [Reproducibility checks](#reproducibility-checks)
+ - [Source (`reproducibility_source`)](#source-reproducibility_source)
+ - [Convenience artefacts (`reproducibility_binaries`, per-artefact `reproducibility`)](#convenience-artefacts-reproducibility_binaries-per-artefact-reproducibility)
+ - [Where the checks run](#where-the-checks-run)
+ - [Automated release signing (🪶 ASF-specific, optional)](#automated-release-signing--asf-specific-optional)
+ - [What the policy requires](#what-the-policy-requires)
+ - [One-time setup: `release-prepare automated-signing`](#one-time-setup-release-prepare-automated-signing)
+ - [What changes in the lifecycle once it is enabled](#what-changes-in-the-lifecycle-once-it-is-enabled)
+ - [What does not change](#what-does-not-change)
+ - [Cross-references](#cross-references)
+
+
+
+
+
+# Reproducible source archives, reproducibility checks, and automated signing
+
+How the release-management family produces a source artefact that is a function of the tag alone,
+how a Release Manager (RM) and every voter confirm that the staged artefacts really are that function,
+and, for ASF projects, how that opens the door to CI-signed releases.
+
+The configuration keys this page refers to live in
+[`/release-build.md`](../../projects/_template/release-build.md)
+(`§ Source archive`, `§ Reproducibility checks`) and
+[`/release-management-config.md`](../../projects/_template/release-management-config.md)
+(`§ Signing › Automated release signing`).
+The tool that does the mechanical work is
+[`tools/reproducible-archive`](../../tools/reproducible-archive/README.md) (`repro-archive`).
+
+## Why
+
+[`PRINCIPLES.md` § 11](../../PRINCIPLES.md#11-releases-are-reproducible-from-signed-source):
+releases are reproducible from signed source to the extent the toolchain permits;
+where byte-identical output is achievable it is required,
+and where it is not, the process documents the divergence and provides a verification path a contributor can run locally.
+Reproducibility is what makes a signature worth verifying:
+a `+1` on a source artefact means "I confirmed these bytes are the tagged tree", and that is only checkable if the tagged tree yields those bytes again.
+
+The framework learnt this the hard way.
+Magpie's own `0.1.0-rc1` got a `-1` because the artefact was a `zip -r` of a working tree:
+it carried `__pycache__/*.pyc`, its dangling agent-view symlinks pointed at directories the archive had stripped,
+and no voter could regenerate it to compare.
+`0.1.0-rc2` switched to `git archive` with `.gitattributes` `export-ignore`, and
+[`docs/source-release-contents.md`](../source-release-contents.md) records what ships and why.
+This page generalises that fix for every adopter.
+
+## The source archive is `git archive` plus `.gitattributes`
+
+With `source_archive_method: git-archive` (the default in `release-build.md`) the source artefact is not a build output.
+It is an export of the tagged tree:
+
+- **only tracked files at the tag** — an untracked `.pyc`, a stray editor backup, a local `.env`, cannot ship;
+- **minus the paths marked `export-ignore`** in the repository's root
+ [`.gitattributes`](https://git-scm.com/docs/gitattributes#_creating_an_archive) — VCS, CI and editor metadata a source consumer never needs;
+- **wrapped by `repro-archive build`**, which applies the reproducible-builds.org archive rules below so that the output is byte-identical on every machine.
+
+`.gitattributes` is therefore part of the release definition and must be **committed before the RC tag is cut**;
+`git archive` reads the attributes from the tree it archives, not from the working copy.
+That is why the review lands in the prep PR (`release-prepare prep`) and why `release-rc-cut` refuses to cut an RC while the review is outstanding.
+
+### The first-release `.gitattributes` review
+
+`release-prepare prep` Step 2f runs a guided review on the first release
+(or whenever `export_ignore_reviewed` is unset in `release-build.md`, or on `--review-archive`).
+It is an education step: the goal is that the operator understands *why* each path ships or does not, not that the agent guesses.
+The skill:
+
+1. **Lists what would ship today** — `git archive --format=tar HEAD | tar -tf -` — and every top-level entry of `git ls-files`.
+2. **Classifies each top-level path** into one of these buckets and says which:
+
+ | Bucket | Typical paths | Default |
+ |---|---|---|
+ | Source, docs, build descriptors, packaging metadata | `src/`, `docs/`, `pom.xml`, `pyproject.toml`, lock files, `README*` | **ship** |
+ | Legal files | `LICENSE`, `NOTICE`, `DISCLAIMER` (incubating), `licenses/` | **ship, never excludable** |
+ | Inputs to the checks voters run | RAT excludes (`.rat-excludes`), in-tree validators | **ship** |
+ | Foundation / project metadata | `.asf.yaml`, `doap_*.rdf` | ship (project's call; ASF projects conventionally ship them) |
+ | VCS metadata | `.gitattributes`, `.gitmodules`, `.mailmap` | exclude (`.gitignore` is the project's call) |
+ | CI and bot configuration | `.github/workflows/`, `.github/dependabot.yml`, `.gitlab-ci.yml`, `.travis.yml`, `.circleci/`, `.pre-commit-config.yaml` | exclude |
+ | Editor and IDE state | `.idea/`, `.vscode/`, `.devcontainer/` | exclude |
+ | Linter and formatter configuration not needed to build | `.lychee.toml`, `.markdownlint.json`, `.typos.toml`, `.zizmor.yml`, `.yamllint`, `.codespellrc` | exclude |
+ | Agent-view directories | `.claude/`, `.agents/`, `.kiro/`, `.cursor/` | exclude the relay symlink dirs; keep a single-hop canonical view if shipped files link into it |
+ | Large assets not needed to build or verify | screenshots, recordings, demo data | project's call; say what they are for |
+ | Release-tooling scratch | `.apache-magpie.session-state.json`, `.apache-magpie.local.lock` | exclude |
+
+3. **Checks references before proposing an exclusion**: `git grep -l ''` over tracked files.
+ A path that a shipped file links to (a doc, a validator, a symlink target) must not be excluded,
+ or `release-verify-rc` Step 7 will fail the RC with a dangling reference.
+ The skill names the referrers and offers the alternative (keep the path, or repoint the reference).
+4. **Checks symlinks**: every committed symlink whose target would be stripped is flagged; chained symlinks (link to a link) are flagged because safe extractors reject them.
+5. **Proposes the entries**, root-anchored with a leading `/` for root-only files, one rationale comment per entry,
+ and shows the before/after archive listing diff (`repro-archive build` twice, `repro-archive compare`).
+6. **Records the decision**: `.gitattributes` joins the prep PR's file set, and the prep PR also sets
+ `export_ignore_reviewed: ` in `release-build.md` so the review does not repeat.
+
+Everything is a proposal; the RM confirms each entry.
+The agent never edits `.gitattributes` without that confirmation and never marks the review done on its own.
+
+### Later releases: drift
+
+On every subsequent `release-prepare prep` the skill runs a cheap drift check:
+top-level entries added since the last reviewed tag (`git diff --name-only HEAD`, top level only)
+that fall in an *exclude* bucket are surfaced as candidates.
+`--review-archive` forces the full review again.
+
+## The archive rules from reproducible-builds.org
+
+`git archive` on its own is only deterministic for one `git` version:
+the zip writer's compression, the tar's PAX handling and the mtime source have changed between releases,
+so two voters with different `git` versions get different bytes from the same tag.
+[reproducible-builds.org § Archive metadata](https://reproducible-builds.org/docs/archives/)
+lists what has to be pinned for an archive to be reproducible.
+`repro-archive build` applies every rule; `repro-archive check` verifies each one on any archive; `repro-archive recipe` prints the equivalent GNU tar / Info-ZIP shell commands.
+
+| Rule | Standard-tool form | `repro-archive` |
+|---|---|---|
+| One modification time for every member | `tar --mtime="@${SOURCE_DATE_EPOCH}"` / `touch --date="@${SOURCE_DATE_EPOCH}"` | mtime = `SOURCE_DATE_EPOCH`, default the committer timestamp of the ref |
+| Locale-independent file ordering | `tar --sort=name`, `find … \| LC_ALL=C sort -z` | per-directory byte order |
+| No ownership leakage | `--owner=0 --group=0 --numeric-owner` | uid/gid 0, empty names |
+| No umask leakage | `--mode=a=rX,u+w` | `0644` / `0755` |
+| No PAX `atime`/`ctime`/PID headers | `--pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime` | none written; `check` catches `PaxHeaders.` |
+| gzip carries no timestamp or filename | `gzip -n` | header mtime 0, no name |
+| zip carries no extra attributes | `zip -X`, unzip with `TZ=UTC` | no extra fields, UTC DOS time |
+| Static libraries deterministic | `ARFLAGS=Dcvr`, `ranlib -D` | belongs in the *binary* build command (`release-build.md § Build invocation`) |
+
+`SOURCE_DATE_EPOCH` is the one input two builders must agree on.
+Deriving it from the ref's committer timestamp makes it a property of the tag, which is why `release-rc-cut` records it on the planning issue alongside the commit hash and the sha512.
+Two inputs the page does not list are pinned as well: the builder's `core.autocrlf` / `core.eol` (which `git archive` would apply to `text` files, so a Windows-configured builder exports different bytes) and the archive writer itself (the tool, not the local `tar` / `zip` / `git` version).
+
+### The record: commit, SWHID, origin
+
+Every RC carries a record on the planning issue and in the `[VOTE]`, printed by `repro-archive build` and pasted back by the RM:
+
+| Line | What it is for |
+|---|---|
+| `commit ` and the repository URL | Where the tree comes from. A voter rebuilds from this. |
+| `swhid_dir swh:1:dir:;origin=;anchor=swh:1:rev:` | The [Software Heritage identifier](https://swhid.org/) (ISO/IEC 18670:2025) of the archive's **expanded content**. Computed from names, modes and contents alone, exactly as git computes a tree id, so it does not depend on the archive format, the compression or who packed it: a voter recomputes it from the staged bytes with `repro-archive swhid ` (or `asfswhid` / `swh identify` after extracting), ATR computes the same value for the candidate at compose time, and a `.tar.gz` and a `.zip` of the same tree carry the same value. |
+| `swhid_rev swh:1:rev:;origin=` | The commit as a SWHID, the `anchor` of the content identifier. |
+| `swhid_dir_note` | Whether the content SWHID equals `git rev-parse ^{tree}`. It does unless `.gitattributes` altered the export (`export-ignore`, `export-subst`, `text` / `eol`); when it differs, the difference is itself the record that something was left out, and `release-prepare`'s review is where that was decided. |
+| `SOURCE_DATE_EPOCH ` | The one input to feed back into a rebuild. |
+| `sha512 ` | Byte-level comparison; `identical` in `compare` terms. |
+
+Why the SWHID and not just the commit: a commit id names a repository object; the content SWHID names what shipped, survives a repository move, is the same across archive formats, and is what ATR computes — so it is the value to compare, with a voter's own recomputation and with the platform.
+It also gives a convenience artefact something precise to point at: each one records the source `swh:1:dir:` it was built from.
+`release-verify-rc` checks the staged archive against the recorded SWHID (`repro-archive check --swhid …`) and reports `swhid_matches`.
+
+## Reproducibility checks
+
+Both checks are **optional** and configured in `release-build.md § Reproducibility checks`.
+They are `on` for the source archive by default (it costs one rebuild), `off` for binaries by default (most adopters ship none).
+They become **mandatory** under automated release signing (below).
+
+### Source (`reproducibility_source`)
+
+`on`: rebuild the source artefact from the tag with `repro-archive build` (same prefix, format and `SOURCE_DATE_EPOCH` as recorded on the planning issue) and `repro-archive compare` it against the staged artefact.
+
+| `compare` verdict | Meaning | Outcome |
+|---|---|---|
+| `identical` | Byte-for-byte the same file | `PASS` |
+| `content-identical` | Same members, same bytes, same modes; only archive metadata differs | `WARN` (RM-key mode: the RM built with a plain `git archive` or a different tool version; switch to `repro-archive build` for the next RC) / `FAIL` (automated signing) |
+| `differs` | Members added, removed or changed | `FAIL` — the artefact is not the tagged tree; a `-1` |
+
+`repro-archive check --epoch ` on the staged artefact runs alongside and reports any rule the staged archive violates.
+
+### Convenience artefacts (`reproducibility_binaries`, per-artefact `reproducibility`)
+
+What a project ships besides the source — a binary tarball, wheels, jars, a container image, a Helm chart — is **project-specific by nature**, so the framework does not assume any.
+Each one is declared in `release-build.md § Convenience artefacts` with its own build command, staging target, reproducibility mode, vote scope and publish channel, and every `release-*` skill reads that list: `release-rc-cut` builds and stages them, `release-verify-rc` rebuilds and compares them, `release-vote-draft` lists them, `release-promote` publishes them.
+A source-only project leaves the list empty and the skills say so.
+
+**Reproducibility is what makes a convenience artefact "good".**
+The source is the release, and a voter can read it; a binary cannot be read, only rebuilt.
+The single check that establishes that a convenience artefact is what the voted source produces is to rebuild it from the tag, under the same `SOURCE_DATE_EPOCH`, and compare.
+An artefact that reproduces bit-for-bit is known-good; one whose every difference is written down and explained is acceptably good; one that cannot be reproduced either way is of unknown provenance, whatever the vote said about the source, and `release-promote` withholds its publish command until a `release-verify-rc` run reproduces it.
+
+| Mode (per artefact; default `reproducibility_binaries`) | What runs | Outcome |
+|---|---|---|
+| `off` | nothing | `SKIP`, stated explicitly in the report, with the note that the artefact is published on trust |
+| `byte-identical` | `export SOURCE_DATE_EPOCH=…` then the entry's `build_command` at the tag; the rebuilt artefact compared byte-for-byte with the staged one | any mismatch is `FAIL` and the artefact is held back from publication |
+| `documented-divergence` | the rebuild, then the entry's `verification_command` (for example [`diffoscope`](https://diffoscope.org/)) against the staged artefact | differences that match the entry's `known_divergences` are `WARN` and listed; anything else is `FAIL` |
+
+`documented-divergence` is the honest mode for toolchains that cannot yet produce identical bytes (a JIT-compiled bundle, a signed installer, a platform that embeds the build host).
+The known divergences are part of the release documentation, which is exactly what `PRINCIPLES.md § 11` asks for.
+Typical levers for getting to `byte-identical`: honour `SOURCE_DATE_EPOCH` (most build tools do), pin the toolchain, `ARFLAGS=Dcvr` / `ranlib -D` for static libraries, `gzip -n`, sorted inputs, and no absolute build paths.
+
+### Where the checks run
+
+- **`release-rc-cut` Step 2b** — the RM's self-check right after building, before signing: `check` the artefact, rebuild into a scratch directory, `compare`. Catching a non-reproducible build here saves a whole RC round-trip.
+- **`release-verify-rc` Step 9** — every voter's check against the *staged* artefact, in the same read-only pairing loop as signatures and checksums. The report's step summary carries the verdict and the recorded `SOURCE_DATE_EPOCH`.
+- **`release-promote` Step 0** — under automated signing only: refuses to promote unless the planning issue carries a `release-verify-rc` reproducibility attestation from a run on trusted hardware.
+
+## Automated release signing (🪶 ASF-specific, optional)
+
+> **Scope.** This section applies to projects whose `project.md` declares `organization: ASF`.
+> The skills offer the option only to those projects
+> (`organizations/ASF/organization.md` → `release_process.automated_signing`;
+> the `independent` organization sets it to `null` and the sub-command is not shown).
+> Non-ASF adopters keep the RM-key flow; a foundation with its own CI-signing policy can add an equivalent block to its organization manifest.
+
+### What the policy requires
+
+[Infra § Automated release signing](https://infra.apache.org/release-signing.html#automated-release-signing)
+lets an ASF project have CI (for example GitHub Actions) sign the artefacts it builds, **provided that**:
+
+- *all* artefacts being signed can be built reproducibly;
+- CI deploys the artefacts to a **staging** environment only;
+- the release process contains a **validation step on trusted hardware** (explicitly *not* GitHub Actions) that rebuilds every artefact from source and confirms it is **bit-by-bit identical** to what was staged, before anything is published to end users.
+
+The Apache Security Team must be notified of the request and approve the workflow before it is used
+(the request should spell out the trusted-hardware validation; `INFRA-23996` is the background ticket).
+The key is requested through an Infra Jira ticket and is provisioned by Infra:
+4096-bit RSA, signing-only, private half held by infra-root and made available to the chosen CI system only,
+a PGP-encrypted revocation certificate placed in the project's private repository,
+and the public key sent to the project or added to its `KEYS`.
+[release-policy § release signing](https://www.apache.org/legal/release-policy.html#release-signing)
+allows signatures by "the automated release infrastructure, where the underlying implementation MUST follow the principles outlined by the Apache Security Team".
+
+### One-time setup: `release-prepare automated-signing`
+
+The sub-command is a **drafting** step: it produces the artefacts the RM files, and files none of them.
+
+1. **Eligibility gate.** `organization: ASF`; `reproducibility_source: on`; `reproducibility_binaries: byte-identical` for every convenience binary in `expected_artefacts` (or none); the most recent RC's `release-verify-rc` report shows `identical` for every artefact. If the build is not yet demonstrably reproducible the skill stops here and says what to fix first.
+2. **Infra Jira ticket draft** requesting the CI signing key, naming the workflow, the staging target (ATR trusted publishing via [`apache/tooling-actions/upload-to-atr`](https://github.com/apache/tooling-actions), pinned by commit SHA), and the trusted-hardware validation step.
+3. **Security Team notification draft** for `security@apache.org` (the mail is drafted, never sent — [spec § Boundary 3](spec.md#boundary-3-agent-never-sends-mail-to-dev-users-announce)).
+4. **Workflow PR proposal** from the template
+ [`projects/_template/workflows/release-candidate.yml`](../../projects/_template/workflows/release-candidate.yml):
+ build the source archive with the embedded `repro-archive` script, build binaries under `SOURCE_DATE_EPOCH`, self-check reproducibility in CI, upload to ATR with OIDC. The workflow contains no key material; signing is performed by the infra-managed mechanism agreed on the ticket.
+5. **Config diff proposal**: `automated_release_signing: requested` now, `enabled` plus `ci_signing_key_fingerprint` and `ci_signing_infra_ticket` once Infra has provisioned the key and the public block is in `KEYS` (`release-keys-sync` handles the `KEYS` diff).
+
+### What changes in the lifecycle once it is enabled
+
+| Step | RM-key flow | `automated_release_signing: enabled` |
+|---|---|---|
+| 4 (`release-rc-cut`) | tag, build, `gpg --detach-sign`, `sha512sum` | tag (still signed by the RM) and push; the push triggers the workflow which builds, uploads to ATR, and stages. The skill emits the tag push plus the commands to watch the run and fetch the staged artefact list. |
+| 5 (`release-rc-cut`) | `svn import` to `dist/dev/` | performed by CI; the skill records the run URL and staging URL on the planning issue |
+| 6 (`release-verify-rc`) | reproducibility step optional | **mandatory**, `compare --require-identical` for every artefact, run on a committer's own hardware; the `--post-to` comment carries a trusted-hardware attestation |
+| 10 (`release-promote`) | promote after `vote-passed` | additionally requires the attestation on the planning issue; blocks without it |
+
+### What does not change
+
+- The agent still holds no key of any kind ([spec § Boundary 1](spec.md#boundary-1-agent-never-holds-the-rms-signing-key)) — not the RM's, not the CI key.
+- The RM still signs the **tag** with their own key; the RC is still voted on `dev@` by people who downloaded, rebuilt and tested it.
+- Nothing is published by the agent or by CI; promotion stays a PMC member's `svn mv` (or ATR finish) after the vote ([spec § Boundary 2](spec.md#boundary-2-agent-never-publishes-the-release)).
+
+## Cross-references
+
+- [`tools/reproducible-archive/README.md`](../../tools/reproducible-archive/README.md) — the tool, rule by rule.
+- [`docs/source-release-contents.md`](../source-release-contents.md) — Magpie's own `.gitattributes` decisions, the worked example of the review.
+- [`process.md` § Step 2, 4, 5, 6, 10](process.md) — where each piece sits in the 14-step lifecycle.
+- [`spec.md`](spec.md) — per-skill contract changes (`release-prepare`, `release-rc-cut`, `release-verify-rc`, `release-promote`).
+- [`manual-release-process.md` § 5](manual-release-process.md#5-confirm-the-source-matches-the-tagged-commit) — the same source check written out longhand for a voter without the skill.
+- [`atr-release-runbook.md` § GitHub Actions path](atr-release-runbook.md#github-actions-path-reproducible-builds) — the ATR trusted-publishing path the workflow template uses.
+- [reproducible-builds.org § Archive metadata](https://reproducible-builds.org/docs/archives/) — the rule set.
+- [reproducible-builds.org § `SOURCE_DATE_EPOCH`](https://reproducible-builds.org/docs/source-date-epoch/) — the timestamp convention.
+- [Infra § Automated release signing](https://infra.apache.org/release-signing.html#automated-release-signing) — the ASF policy.
+- [`apache/tooling-actions`](https://github.com/apache/tooling-actions) — ATR trusted-publishing actions (pin by commit SHA).
diff --git a/docs/release-management/spec.md b/docs/release-management/spec.md
index e0bc32428..66e22ac17 100644
--- a/docs/release-management/spec.md
+++ b/docs/release-management/spec.md
@@ -89,6 +89,15 @@ Practical consequences:
from, ASF-owned infrastructure
([release-policy.html](https://www.apache.org/legal/release-policy.html)).
The skill states this in its hand-off text; it cannot enforce it.
+- 🪶 ASF-specific automated release signing does not move the
+ boundary. When an ASF project enables it
+ ([Infra § Automated release signing](https://infra.apache.org/release-signing.html#automated-release-signing)),
+ the CI key is provisioned and held by Infra, never by the agent or
+ the project; the workflow template the skills propose contains no
+ key material and no signing step; the RM still signs the tag; and
+ the policy's bit-by-bit validation on trusted hardware is a
+ committer's `release-verify-rc` run, not the agent's. See
+ [`reproducibility.md`](reproducibility.md#automated-release-signing--asf-specific-optional).
### Boundary 2: Agent never publishes the release
@@ -180,12 +189,29 @@ PR.
**Outputs.**
- A planning-issue body (markdown), labelled `release-planning`.
-- A prep PR (separate invocation), labelled `prep-pr-open`.
+- A prep PR (separate invocation), labelled `prep-pr-open`. On the
+ first release (no `export_ignore_reviewed` in `release-build.md`)
+ the prep PR also carries the **source-archive contents review**:
+ `.gitattributes` `export-ignore` entries proposed per top-level
+ path with a rationale each, after a reference check so nothing a
+ shipped file links to is stripped, plus the
+ `export_ignore_reviewed: ` marker. Later releases get a
+ drift check only. See
+ [`reproducibility.md`](reproducibility.md#the-first-release-gitattributes-review).
- A post-release bump PR (third invocation), unlabelled.
+- 🪶 ASF-specific, `automated-signing` sub-command (version-less,
+ offered only under `organization: ASF`): an Infra Jira ticket
+ draft requesting the CI signing key, a Security Team notification
+ draft, a reproducible-build workflow PR rendered from
+ [`projects/_template/workflows/release-candidate.yml`](../../projects/_template/workflows/release-candidate.yml),
+ and the `release-management-config.md § Signing` diff — gated on the
+ build being demonstrably reproducible. Nothing is filed or sent.
**State-change boundary.** The skill opens the planning issue and
opens the PRs *as drafts*. The RM marks them ready and merges. The
-skill never marks ready, never merges, never closes.
+skill never marks ready, never merges, never closes, never edits
+`.gitattributes` without per-entry confirmation, never files a
+ticket and never sends mail.
**Hand-off conditions.**
@@ -281,15 +307,39 @@ to the adopter's distribution backend (default `svn import` to
**Inputs.**
- `/release-build.md`, build invocation, digest
- set (`sha512`, optionally `sha256`), binary-exclude list.
+ set (`sha512`, optionally `sha256`), binary-exclude list; `§ Source
+ archive` (`source_archive_method`, format, prefix,
+ `export_ignore_reviewed`) and `§ Reproducibility checks`.
- Current HEAD of the configured release branch.
- The RC number (from the trigger).
**Outputs.**
- A four-section markdown block: (1) `git tag -s` command,
- (2) build command, (3) `gpg --detach-sign` for each expected
+ (2) build command — for the source artefact, by default,
+ `repro-archive build --ref -rcN` (a `git archive` export
+ honouring `.gitattributes` `export-ignore`, with every
+ [reproducible-builds.org archive rule](https://reproducible-builds.org/docs/archives/)
+ applied; never an archive of a working tree), then the adopter's
+ `build_command` for convenience binaries under the tag's
+ `SOURCE_DATE_EPOCH`; (3) `gpg --detach-sign` for each expected
artefact, (4) `sha512sum > artefact.sha512` for each artefact.
+- An optional reproducibility self-check block (Step 2b): lint,
+ rebuild, `repro-archive compare`; binaries rebuilt and compared
+ per `reproducibility_binaries`. A `differs` stops the cut.
+- The planning-issue record of source commit, `SOURCE_DATE_EPOCH` and
+ sha512, so voters can rebuild.
+- 🪶 ASF-specific, under `automated_release_signing: enabled` only
+ (`organization: ASF`): sections (3) and (4) and the staging block
+ are replaced by the RM-signed tag push that triggers the CI
+ workflow, plus the commands to watch the run; the hand-off states
+ that a committer must validate on trusted hardware before
+ promotion.
+
+The skill blocks while the first-release `.gitattributes` review is
+outstanding (`export_ignore_reviewed` unset with
+`source_archive_method: git-archive`); `--allow-unreviewed-archive`
+is the logged override.
- A second markdown block with the backend-shaped staging command
sequence. For `svnpubsub` (ASF default): `svn import` into
`dist/dev//-rcN/`. For `github-releases`:
@@ -324,7 +374,13 @@ own ASF credentials.
**Adopter knobs.** Inherits `/release-build.md`
verbatim, see the
[`projects/_template/release-build.md`](../../projects/_template/release-build.md)
-scaffold.
+scaffold. Convenience artefacts — anything the project ships besides
+the source — are project-specific by nature and are declared one by
+one under `§ Convenience artefacts` (`build_command`, `staging` /
+`stage_command`, `reproducibility`, `vote_included`,
+`publish_channel` / `publish_command`); the skill emits each entry's
+own build and staging commands under the tag's `SOURCE_DATE_EPOCH`
+and never assumes an artefact the config does not declare.
### `release-verify-rc`
@@ -352,9 +408,19 @@ loop before posting `+1`.
- A pass/fail report per check (signatures, checksums, license
headers via Apache RAT, NOTICE / LICENSE presence + diff vs
- previous release, no prohibited binaries, version-string
- consistency).
+ previous release, no prohibited binaries, source-tree integrity,
+ version-string consistency, and — optional per
+ `release-build.md § Reproducibility checks` — reproducibility: the
+ source artefact rebuilt from the tag with `repro-archive build` at
+ the recorded `SOURCE_DATE_EPOCH` and compared (`identical` /
+ `content-identical` / `differs`), binaries rebuilt and compared
+ byte-for-byte or against documented divergences).
- A summary classification: `PASS`, `PASS-WITH-WARNINGS`, `FAIL`.
+- 🪶 ASF-specific, under `automated_release_signing: enabled`: the
+ reproducibility check is mandatory with a byte-identical bar, and
+ the `--post-to` comment carries the *validated on trusted hardware*
+ attestation (only with the committer's `--trusted-hardware`
+ assertion) that `release-promote` requires.
The report is a mechanical aid, not a vote. A `PASS` does not
discharge a voter's own ASF obligation to download, build, and
@@ -374,8 +440,17 @@ posting.
- A binary appears that the binary-exclude list neither permits
nor names, the skill reports `FAIL` and points at the file;
the RM decides whether to exclude or pull the binary.
-
-**Adopter knobs.** Inherits `/release-build.md`.
+- A convenience artefact does not reproduce from the voted tag
+ (`DIFFERS`, or differences outside its documented set), the skill
+ reports `FAIL` for that artefact and names it in
+ `binaries.differs`; `release-promote` withholds its publication.
+ Reproducibility is the check that decides whether a convenience
+ artefact is good, since a binary cannot be reviewed, only rebuilt.
+
+**Adopter knobs.** Inherits `/release-build.md`,
+including `§ Convenience artefacts` (per-artefact `build_command`
+and `reproducibility` mode) and `§ Source-tree validators` (the
+project's own integrity checks Step 7 runs; none are assumed).
### `release-vote-draft`
@@ -542,6 +617,13 @@ For `self-hosted`: the promote half of `release_publish_command_template`.
- A markdown block with the `svn` command sequence (`svn mv`,
`svn commit -m`, expected mirror-propagation note).
+- When `/release-build.md § Convenience artefacts`
+ declares any: a second block with each artefact's own
+ `publish_command` to its declared channel (project-specific; PyPI,
+ Maven Central, a container registry, …), emitted only for artefacts
+ the recorded `release-verify-rc` run reproduced from the voted tag;
+ a `HOLD` note for any that did not. The source promotion is never
+ held back by a convenience artefact.
- A proposed next label: `promoted`.
The mirror-propagation note also records the earliest time the
@@ -568,6 +650,11 @@ hard skill-side denylist; removing it requires a skill PR.
([release-policy.html](https://www.apache.org/legal/release-policy.html));
the skill emits an "ask a PMC member to publish" hand-off
instead of the `svn mv` command set.
+- 🪶 ASF-specific: `automated_release_signing: enabled` and the
+ planning issue carries no `release-verify-rc` trusted-hardware
+ attestation for this RC → hard blocker; the policy's validation
+ step has not happened
+ ([Infra § Automated release signing](https://infra.apache.org/release-signing.html#automated-release-signing)).
### `release-announce-draft`
diff --git a/docs/release-management/svn-release-runbook.md b/docs/release-management/svn-release-runbook.md
index 99418fb3b..2c1439d29 100644
--- a/docs/release-management/svn-release-runbook.md
+++ b/docs/release-management/svn-release-runbook.md
@@ -154,10 +154,31 @@ build output. The `--prefix` puts everything under a versioned top
folder so the unpacked tree is `apache-magpie-/`.
```bash
-git archive --format=zip \
- --prefix="apache-magpie-${VERSION}/" \
- -o "${ARTIFACT}" \
- "${RC_TAG}"
+# Preferred: git archive wrapped by the framework's reproducible-archive
+# tool, which applies every https://reproducible-builds.org/docs/archives/
+# rule so the bytes do not depend on your git version. Prints the commit,
+# SOURCE_DATE_EPOCH and sha512 to record on the planning issue.
+uv run --project tools/reproducible-archive repro-archive build \
+ --ref "${RC_TAG}" --format zip \
+ --prefix "apache-magpie-${VERSION}" \
+ -o "${ARTIFACT}"
+
+# Plain git archive gives the same *contents* (a voter's `repro-archive
+# compare` reports `content-identical`), but not the same bytes across
+# git versions:
+# git archive --format=zip --prefix="apache-magpie-${VERSION}/" -o "${ARTIFACT}" "${RC_TAG}"
+```
+
+Self-check that the archive is reproducible before signing it
+([reproducibility.md](reproducibility.md)):
+
+```bash
+uv run --project tools/reproducible-archive repro-archive check "${ARTIFACT}"
+mkdir -p rebuild
+uv run --project tools/reproducible-archive repro-archive build \
+ --ref "${RC_TAG}" --format zip --prefix "apache-magpie-${VERSION}" -o "rebuild/${ARTIFACT}"
+uv run --project tools/reproducible-archive repro-archive compare --require-identical \
+ "${ARTIFACT}" "rebuild/${ARTIFACT}"
```
Quick sanity check that `LICENSE` and `NOTICE` are present at the
diff --git a/docs/setup/marketplace.md b/docs/setup/marketplace.md
index 3b58f0969..18224ce4b 100644
--- a/docs/setup/marketplace.md
+++ b/docs/setup/marketplace.md
@@ -156,7 +156,7 @@ can say so, because it is the floor everything else is managed from.
|---|---|---|
| `magpie-security` | 15 | ~2.0k |
| `magpie-setup` | 10 | ~1.3k |
-| `magpie-release-management` | 10 | ~1.0k |
+| `magpie-release-management` | 10 | ~1.3k |
| `magpie-pr-management` | 8 | ~1.0k |
| `magpie-issue` | 8 | ~0.8k |
| `magpie-repo-health` | 7 | ~0.7k |
diff --git a/docs/source-release-contents.md b/docs/source-release-contents.md
index 08ef631ba..1dac87d8e 100644
--- a/docs/source-release-contents.md
+++ b/docs/source-release-contents.md
@@ -31,7 +31,11 @@ review discussion on the `0.1.0-rc2` `[VOTE]` thread on
`release-verify-rc` re-checks the unpacked archive (symlink-lint,
validators, no `.pyc`), so a regression in what ships fails the RC before
-the vote.
+the vote. This page is the worked example of the first-release
+source-archive review every adopter goes through in `release-prepare
+prep`; the generic rules, the classification buckets, and how the
+archive is made byte-reproducible are in
+[`release-management/reproducibility.md`](release-management/reproducibility.md).
## Files kept in the source archive
diff --git a/docs/vendor-neutrality.md b/docs/vendor-neutrality.md
index 84f773a05..cef10247d 100644
--- a/docs/vendor-neutrality.md
+++ b/docs/vendor-neutrality.md
@@ -591,7 +591,7 @@ Organization scope (declared, orthogonal to vendor): ASF = 14, agnostic = 61.
**LLM / agent-integration neutrality**
-**Agent harness: 24/24 substrate tools run under any harness unchanged (100%).** Substrate tools are Magpie's own machinery; each declares the agent harness it integrates with (`**Harness:**`), or `agnostic`. A tool is neutral when it is harness-agnostic or supports two or more harnesses; *coupled* when it targets a single harness.
+**Agent harness: 25/25 substrate tools run under any harness unchanged (100%).** Substrate tools are Magpie's own machinery; each declares the agent harness it integrates with (`**Harness:**`), or `agnostic`. A tool is neutral when it is harness-agnostic or supports two or more harnesses; *coupled* when it targets a single harness.
| Substrate tool | Substrate | Harness support | Verdict |
|---|---|---|---|
@@ -606,6 +606,7 @@ Organization scope (declared, orthogonal to vendor): ASF = 14, agnostic = 61.
| `preflight-audit` | analytics | any | ✅ agnostic |
| `privacy-llm` | privacy | any | ✅ agnostic |
| `probe-templates` | sandbox | any | ✅ agnostic |
+| `reproducible-archive` | release | any | ✅ agnostic |
| `sandbox-lint` | sandbox | Claude Code, Codex, Cursor, Gemini CLI, Kiro, OpenCode | ✅ portable |
| `security-tracker-stats-dashboard` | analytics | any | ✅ agnostic |
| `skill-and-tool-validator` | framework-dev | any | ✅ agnostic |
@@ -628,7 +629,7 @@ Harness → substrate tools it supports:
- **Gemini CLI** (3): `agent-guard`, `sandbox-lint`, `spec-loop`
- **Kiro** (3): `agent-guard`, `sandbox-lint`, `spec-loop`
- **OpenCode** (3): `agent-guard`, `sandbox-lint`, `spec-loop`
-- **any harness** (21): `agent-isolation`, `dashboard-generator`, `dev`, `egress-gateway`, `permission-audit`, `pilot-report-validator`, `pr-management-stats`, `preflight-audit`, `privacy-llm`, `probe-templates`, `security-tracker-stats-dashboard`, `skill-and-tool-validator`, `skill-evals`, `skill-reconciler-diff`, `skill-token-count`, `spec-inventory`, `spec-status-index`, `spec-validator`, `symlink-lint`, `vendor-neutrality-score`, `vetted-ops`
+- **any harness** (22): `agent-isolation`, `dashboard-generator`, `dev`, `egress-gateway`, `permission-audit`, `pilot-report-validator`, `pr-management-stats`, `preflight-audit`, `privacy-llm`, `probe-templates`, `reproducible-archive`, `security-tracker-stats-dashboard`, `skill-and-tool-validator`, `skill-evals`, `skill-reconciler-diff`, `skill-token-count`, `spec-inventory`, `spec-status-index`, `spec-validator`, `symlink-lint`, `vendor-neutrality-score`, `vetted-ops`
**Model endpoint: neutral by construction — 4 default-approved endpoint classes across independent trust domains, plus adopter opt-in.** From the [`privacy-llm` registry](../tools/privacy-llm/models.md): the framework keys approval on *endpoint identity*, not on who hosts the model, so no single LLM vendor is privileged.
diff --git a/organizations/ASF/organization.md b/organizations/ASF/organization.md
index 9c5040f44..fb73a4c35 100644
--- a/organizations/ASF/organization.md
+++ b/organizations/ASF/organization.md
@@ -252,6 +252,17 @@ release_process:
release_dist: https://dist.apache.org/repos/dist #
project_wiki: https://cwiki.apache.org/confluence/display/ #
announce_list: announce@apache.org #
+ # Automated (CI) release signing — ASF-specific option, offered by
+ # `release-prepare automated-signing` only under this organization.
+ # Policy: https://infra.apache.org/release-signing.html#automated-release-signing
+ automated_signing:
+ policy_url: https://infra.apache.org/release-signing.html#automated-release-signing
+ key_request_channel: infra-jira # https://issues.apache.org/jira/projects/INFRA
+ key_request_background: INFRA-23996
+ approval_body: security@apache.org # Security Team approves the workflow before use
+ key_spec: "4096-bit RSA, signing-only, private half held by infra-root only"
+ trusted_publishing_action: apache/tooling-actions/upload-to-atr # pin by commit SHA
+ validation: "every signed artefact rebuilt bit-by-bit identical on trusted hardware before publication"
```
## Roster
diff --git a/organizations/independent/organization.md b/organizations/independent/organization.md
index ce9a2e849..8733bbdd8 100644
--- a/organizations/independent/organization.md
+++ b/organizations/independent/organization.md
@@ -121,6 +121,7 @@ release_process:
release_dist: null # GitHub Releases (no svn dist area)
project_wiki: null
announce_list: null # announcements via GitHub Releases / Discussions
+ automated_signing: null # ASF-specific option; not offered here
```
## Roster / tracker
diff --git a/plugins/magpie-release-management/skills/announce-draft/SKILL.md b/plugins/magpie-release-management/skills/announce-draft/SKILL.md
index 81298e3c3..4b079eba0 100644
--- a/plugins/magpie-release-management/skills/announce-draft/SKILL.md
+++ b/plugins/magpie-release-management/skills/announce-draft/SKILL.md
@@ -434,6 +434,9 @@ Release notes / changelog for :
Keys used to sign the release artifacts:
+Convenience artefacts, built from the released source, are also available: ← include only when release-build.md § Convenience artefacts declares any that release-promote published
+ : //>
+
Questions, feedback, and contributions are welcome on the
. General user support is available on .
diff --git a/plugins/magpie-release-management/skills/prepare/SKILL.md b/plugins/magpie-release-management/skills/prepare/SKILL.md
index d25788f09..d4172296f 100644
--- a/plugins/magpie-release-management/skills/prepare/SKILL.md
+++ b/plugins/magpie-release-management/skills/prepare/SKILL.md
@@ -10,23 +10,33 @@ requires_config:
- release-trains.md
description: |
Draft release preparation artefacts for ``: the planning
- issue, the version-bump and changelog prep PR, or the post-release
- development-version bump PR. Reads release metadata from
- `/release-trains.md` and
+ issue, the version-bump and changelog prep PR (which, on a project's
+ first release, includes a guided review of what the `git archive`
+ source artefact ships and the `.gitattributes` `export-ignore`
+ entries that keep VCS/CI/editor metadata out), or the post-release
+ development-version bump PR. For ASF projects, the one-time
+ `automated-signing` setup drafts the Infra key request, the Security
+ Team notification and the reproducible-build workflow PR. Reads
+ release metadata from `/release-trains.md` and
`/release-management-config.md`. Every output is a
draft confirmed by the Release Manager before filing; the agent never
- marks a PR ready, never merges, and never closes any artefact.
+ marks a PR ready, never merges, never closes any artefact, never files
+ a ticket and never sends mail.
when_to_use: |
Invoke when a Release Manager says "prepare the release",
"draft the planning issue for ", "open the prep PR for
", "write the version bump for ", "draft the
- post-release bump for ", or similar. Covers three lifecycle
- moments: planning-issue creation (`/release-prepare `),
- version-bump prep PR (`/release-prepare prep `), and
- post-release dev-version bump (`/release-prepare post `).
- Requires `/release-management-config.md` and
+ post-release bump for ", "review what goes into the source
+ release", "set up CI release signing", or similar. Covers three
+ lifecycle moments: planning-issue creation (`/release-prepare
+ `), version-bump prep PR (`/release-prepare prep `,
+ which also runs the first-release source-archive review), and
+ post-release dev-version bump (`/release-prepare post `);
+ plus the version-less, 🪶 ASF-only `/release-prepare
+ automated-signing` setup. Requires
+ `/release-management-config.md` and
`/release-trains.md` to exist.
-argument-hint: "[prep | post] "
+argument-hint: "[prep | post] [--review-archive] | automated-signing"
capability: capability:resolve
license: Apache-2.0
---
@@ -311,25 +321,46 @@ For Step 14 (`post`):
- **Planning issue labelled `announced`** — confirms Steps 10–11
completed. Accepted via `--planning-issue `.
+For Step 2's source-archive review (`prep`, Step 2f) — optional:
+- **`/release-build.md § Source archive`** —
+ `source_archive_method` (default `git-archive`) and
+ `export_ignore_reviewed`. Absent file or key = the review has not
+ happened yet, which is exactly when the sub-step runs.
+- **A local clone of ``** at the release branch tip (the
+ resolved `user.md` clone path) — the review lists what `git archive`
+ would ship from *that* tree.
+
+For Step A (`automated-signing`, 🪶 ASF-specific):
+- **`project.md` declares `organization: ASF`.** The sub-command is
+ not offered otherwise; see [`organizations/ASF/organization.md`](../../../../organizations/ASF/organization.md)
+ → `release_process.automated_signing`.
+- **`release-build.md § Reproducibility checks`** — `reproducibility_source: on`
+ and `reproducibility_binaries: byte-identical` (or no binaries).
+
---
## Inputs
| Selector | Resolves to |
|---|---|
-| `[prep \| post]` (optional first argument) | Sub-command: `prep` = Step 2, `post` = Step 14, omit = Step 1 |
+| `[prep \| post \| automated-signing]` (optional first argument) | Sub-command: `prep` = Step 2, `post` = Step 14, `automated-signing` = Step A (🪶 ASF-only, no ``), omit = Step 1 |
| `` (positional) | Target release version string |
| `--planning-issue ` | Explicit planning issue URL (auto-detected if omitted) |
| `--release-branch ` | Override the base branch for the prep or post PR |
| `--previous-tag ` | Override the previous release tag for the merged-PR query |
| `--skip-empty-check` | Allow Step 1 with an empty merged-PR set; reason logged on planning issue |
+| `--review-archive` | Force the full Step 2f source-archive review even when `export_ignore_reviewed` is already set |
---
## Step 0 — Pre-flight check
1. **Sub-command parsed.** Argument is one of: `` (Step 1),
- `prep ` (Step 2), `post ` (Step 14).
+ `prep ` (Step 2), `post ` (Step 14), or
+ `automated-signing` (Step A; 🪶 ASF-specific — if `project.md` does
+ not declare `organization: ASF`, block with *"automated release
+ signing is an ASF Infra offering; this project's organization does
+ not provide one"* and do not describe the flow further).
2. **Version argument parseable.** `` matches a semver-ish
pattern (`X.Y.Z`, `X.Y.Z.post0`, or similar).
3. **`release-management-config.md` readable.** Required keys present:
@@ -354,8 +385,8 @@ Return ONLY valid JSON with this structure:
```json
{
"verdict": "proceed" | "blocked",
- "sub_command": "plan" | "prep" | "post",
- "version": "",
+ "sub_command": "plan" | "prep" | "post" | "automated-signing",
+ "version": "",
"blockers": [""],
"release_branch_base": "",
"previous_tag": ""
@@ -577,7 +608,100 @@ Changelog coverage must be ≥ 90% of the merged-PR set. If fewer than
90% of PRs can be categorised, surface the uncategorised set and ask
the RM to classify before the PR is opened.
-### 2e — Compose the prep PR
+### 2e — Source-archive contents review (first release, or on drift)
+
+With `source_archive_method: git-archive` (the default in
+`release-build.md § Source archive`) the source artefact is an export
+of the tagged tree that honours `.gitattributes` `export-ignore`. The
+attributes are read from the tree being archived, so they have to be
+committed **before** the RC tag — which is why this review lands in
+the prep PR and why `release-rc-cut` blocks while it is outstanding.
+Full rationale and the classification buckets:
+[`docs/release-management/reproducibility.md` § The first-release `.gitattributes` review](../../../../docs/release-management/reproducibility.md#the-first-release-gitattributes-review).
+
+**When the full review runs:** `export_ignore_reviewed` is unset in
+`release-build.md`, or `--review-archive` was passed, or
+`source_archive_method` is `git-archive` and the file has no
+`§ Source archive` at all. **Otherwise** run only the drift check
+(below). With `source_archive_method: custom` skip the sub-step and
+say so (`archive_review: "skipped"`).
+
+This is an **education step**: the operator ends up knowing why every
+top-level path ships or does not. Do not guess; show, classify,
+explain, and ask.
+
+1. **List what would ship today** from the local clone at the release
+ branch tip, and what is tracked:
+
+ ```bash
+ git archive --format=tar HEAD | tar -tf - | sort > /tmp/would-ship.txt
+ git ls-files | cut -d/ -f1 | sort -u # top-level tracked entries
+ cat .gitattributes 2>/dev/null | grep export-ignore # what is already excluded
+ ```
+
+2. **Classify every top-level entry** into one bucket and say which —
+ *ship* (source, docs, build descriptors, lock files, `README*`),
+ *ship, never excludable* (`LICENSE`, `NOTICE`, `DISCLAIMER`,
+ `licenses/`), *ship, input to voter checks* (RAT excludes, in-tree
+ validators), *project's call* (`.asf.yaml`, `doap_*.rdf`,
+ `.gitignore`, large assets), *exclude: VCS metadata*
+ (`.gitattributes`, `.gitmodules`, `.mailmap`), *exclude: CI / bot
+ config* (`.github/workflows/`, `.github/dependabot.yml`,
+ `.gitlab-ci.yml`, `.travis.yml`, `.circleci/`,
+ `.pre-commit-config.yaml`), *exclude: editor / IDE* (`.idea/`,
+ `.vscode/`, `.devcontainer/`), *exclude: lint config not needed to
+ build* (`.lychee.toml`, `.markdownlint.json`, `.typos.toml`,
+ `.zizmor.yml`, `.yamllint`, `.codespellrc`), *agent-view dirs*
+ (`.claude/`, `.agents/`, `.kiro/`, `.cursor/` — exclude relay
+ symlink dirs, keep a single-hop canonical view if shipped files link
+ into it), *exclude: release-tooling scratch*
+ (`.apache-magpie.session-state.json`, `.apache-magpie.local.lock`).
+ Look inside `.github/` and the agent-view dirs; part of a directory
+ may ship (issue templates a shipped skill links to) while the rest
+ is excluded.
+
+3. **Check references before proposing any exclusion**:
+ `git grep -l -- ''` over tracked files. A path that a shipped
+ file links to must not be excluded or `release-verify-rc` Step 7
+ fails the RC on a dangling reference; name the referrers and offer
+ the alternative (keep it, or repoint the reference). Flag every
+ committed symlink whose target would be stripped, and every symlink
+ that points at another symlink (safe extractors reject chains).
+
+4. **Propose the entries** — root-anchored (`/.pre-commit-config.yaml`)
+ for root-only files, directory form (`.idea/`) for directories, one
+ rationale comment per entry — and show the before/after listing
+ diff:
+
+ ```bash
+ # "before": the attributes committed at HEAD
+ uv run --project /tools/reproducible-archive repro-archive build \
+ --ref HEAD --prefix p --format tar.gz -o "$TMPDIR/before.tar.gz"
+ # "after": the proposed .gitattributes as edited in the working tree
+ uv run --project /tools/reproducible-archive repro-archive build \
+ --ref HEAD --prefix p --format tar.gz --worktree-attributes -o "$TMPDIR/after.tar.gz"
+ uv run --project /tools/reproducible-archive repro-archive compare \
+ "$TMPDIR/before.tar.gz" "$TMPDIR/after.tar.gz" # 'removed' = exactly what the review strips
+ ```
+
+ Walk the RM through each proposed entry; each one is confirmed or
+ dropped individually. Never exclude `LICENSE`, `NOTICE`,
+ `DISCLAIMER`, a build descriptor, the RAT excludes, or a referenced
+ path, even if asked — say why and keep it.
+
+5. **Record the decision.** `.gitattributes` joins the prep PR file set
+ (2f), and the prep PR sets `export_ignore_reviewed: ` in
+ `release-build.md § Source archive` so the full review does not
+ repeat. If the RM confirms an existing `.gitattributes` unchanged,
+ still set the marker (`archive_review: "confirmed-existing"`).
+
+**Drift check (later releases).** Top-level entries added since the
+last reviewed tag — `git diff --name-only HEAD | cut -d/
+-f1 | sort -u` — that fall in an *exclude* bucket are surfaced as
+candidates with the same confirm-each flow; nothing new → `archive_review:
+"skipped"` with the note *"no new top-level paths since ``"*.
+
+### 2f — Compose the prep PR
The prep PR touches:
1. Each file in `version_manifest_files` — replace current dev
@@ -587,6 +711,9 @@ The prep PR touches:
3. `NOTICE` — apply the justified attribution changes (if any).
4. `LICENSE` — apply any required Category-B attribution additions
(if any).
+5. `.gitattributes` and `/release-build.md`
+ (`export_ignore_reviewed`) — only when 2e proposed or confirmed the
+ review.
Present the full set of file diffs to the RM for confirmation before
opening the PR.
@@ -614,11 +741,15 @@ Entry added for covering merged PRs since .
### NOTICE/LICENSE
+### Source archive contents
+". Otherwise omit this section.>
+
## Checklist (RM)
- [ ] Version bump is correct in all manifest files
- [ ] Changelog entry covers the intended scope
- [ ] NOTICE attribution changes are justified
- [ ] No Category-X dependency appears in the diff
+- [ ] (first release) every `export-ignore` entry was reviewed; LICENSE / NOTICE / build inputs still ship
Generated by `release-prepare` (magpie-release-prepare).
```
@@ -637,6 +768,7 @@ Return ONLY valid JSON with this structure:
"category_x_hit": false,
"notice_removal_unjustified": false,
"changelog_coverage_pct": ,
+ "archive_review": "proposed" | "confirmed-existing" | "skipped",
"proposed": true
}
```
@@ -644,6 +776,11 @@ Return ONLY valid JSON with this structure:
`proposed` is always `true` at the point this JSON is returned.
`category_x_hit` and `notice_removal_unjustified` are `false` because
the skill would have stopped in 2b or 2c if they were `true`.
+`archive_review` is `"proposed"` when 2e proposed `.gitattributes`
+entries (and `.gitattributes` appears in `files_in_scope`),
+`"confirmed-existing"` when the RM confirmed the existing entries
+unchanged (only `release-build.md` joins the file set), `"skipped"`
+when the review was not due or `source_archive_method` is `custom`.
---
@@ -712,6 +849,113 @@ Return ONLY valid JSON with this structure:
---
+## Step A — Automated release signing setup (sub-command: `automated-signing`, 🪶 ASF-specific)
+
+> **Scope.** Only for a project whose `project.md` declares
+> `organization: ASF`. The option is an ASF Infra offering
+> ([Infra § Automated release signing](https://infra.apache.org/release-signing.html#automated-release-signing))
+> and is resolved from
+> [`organizations/ASF/organization.md`](../../../../organizations/ASF/organization.md)
+> → `release_process.automated_signing`; for any other organization
+> the value is `null`, Step 0 blocks, and the flow is not described.
+> Non-ASF adopters keep the RM-key flow.
+
+A one-time, version-less **drafting** step. Under the policy an ASF
+project may let CI sign the artefacts it builds with an
+Infra-provisioned key **provided that** every signed artefact is built
+reproducibly, CI deploys to staging only, and a committer re-validates
+every artefact **bit-by-bit identical on trusted hardware** before
+publication; the Apache Security Team approves the workflow before use.
+Background:
+[`docs/release-management/reproducibility.md` § Automated release signing](../../../../docs/release-management/reproducibility.md#automated-release-signing--asf-specific-optional).
+
+### A1 — Eligibility gate
+
+All of the following, else stop and list what is missing:
+
+- `project.md` → `organization: ASF`.
+- `release-build.md` → `source_archive_method: git-archive`,
+ `reproducibility_source: on`, and `reproducibility_binaries:
+ byte-identical` for every convenience binary in `expected_artefacts`
+ (or none).
+- The most recent RC's `release-verify-rc` report on its planning issue
+ shows Step 9 `PASS` with every artefact `identical`. If no such report
+ exists the build is not *demonstrably* reproducible yet: tell the RM
+ to cut and verify one RC with the checks on first.
+- `release_vote_backend: atr` or `release_dist_backend: atr` — ATR
+ trusted publishing is the staging target the workflow template uses.
+
+### A2 — Draft the Infra Jira ticket
+
+Draft (never file) an `INFRA` ticket titled *"CI release signing key
+for Apache "* that: requests the key per the policy (4096-bit
+RSA, signing-only, private half held by infra-root, public block to
+`KEYS`, encrypted revocation certificate to the project's private
+repo); names the workflow (`ci_release_workflow`) and the staging
+target (ATR via `apache/tooling-actions/upload-to-atr`, pinned by
+commit SHA); **highlights the trusted-hardware validation step** —
+`release-verify-rc` Step 9 with `--trusted-hardware`, `repro-archive
+compare --require-identical` for every artefact, recorded on the
+planning issue, gating `release-promote`; and references the
+background ticket in
+`release_process.automated_signing.key_request_background`.
+
+### A3 — Draft the Security Team notification
+
+Draft (never send — [spec § Boundary 3](../../../../docs/release-management/spec.md#boundary-3-agent-never-sends-mail-to-dev-users-announce))
+a mail to `release_process.automated_signing.approval_body`
+(`security@apache.org`) from the RM, pointing at the ticket, the
+workflow PR and the validation step, asking for the approval the
+policy requires before the workflow is used. Plain text, real links,
+per the repository's email rules.
+
+### A4 — Propose the workflow PR
+
+From
+[`projects/_template/workflows/release-candidate.yml`](../../../../projects/_template/workflows/release-candidate.yml),
+rendered with the project's slug, artefact prefix, source format and
+`build_command`, placed at `ci_release_workflow`. The template builds
+the source archive with the embedded `repro-archive` script (copy
+`tools/reproducible-archive/src/reproducible_archive/__init__.py` to
+`release/reproducible_archive.py` in the upstream repo), builds
+binaries under `SOURCE_DATE_EPOCH`, builds twice and compares,
+checksums with sha512 only, and uploads to ATR with OIDC. It contains
+**no key material and no signing step**; the signing mechanism is what
+Infra agrees on the ticket. Pin every action to a commit SHA. Open as a
+draft PR via `gh pr create --web` after RM confirmation.
+
+### A5 — Propose the config diff
+
+`release-management-config.md § Signing`: `automated_release_signing:
+requested`, `ci_release_workflow`, `ci_signing_infra_ticket` (once the
+ticket exists). Tell the RM that `enabled` is set only after Infra has
+provisioned the key, its public block is in `KEYS`
+(`release-keys-sync`), the Security Team has approved, and the workflow
+PR is merged — and that from then on `release-rc-cut` emits the tag
+push instead of local signing, `release-verify-rc` Step 9 is mandatory,
+and `release-promote` blocks without the attestation.
+
+Return ONLY valid JSON with this structure:
+
+```json
+{
+ "organization": "ASF",
+ "eligible": true | false,
+ "missing_conditions": [""],
+ "infra_ticket_draft": "",
+ "security_notification_draft": "",
+ "workflow_pr": {"path": "", "title": "", "proposed": true} | null,
+ "config_diff": [""],
+ "filed_or_sent": false,
+ "proposed": true
+}
+```
+
+`filed_or_sent` is always `false`: the skill drafts the ticket and the
+mail and proposes the PR; the RM files, sends and marks ready.
+
+---
+
## Step N+1 — Hand-back artefact
The AI-driven part ends with a hand-back artefact containing:
@@ -733,6 +977,10 @@ The AI-driven part ends with a hand-back artefact containing:
- **NOTICE/LICENSE summary** — confirmed clean (or the removals that
required justification).
- **Changelog coverage** — percentage and any uncategorised PRs.
+- **Source-archive review** — the `export-ignore` entries proposed or
+ confirmed with their reasons, the paths kept because shipped files
+ reference them, and the `export_ignore_reviewed` marker; or the
+ one-line reason the review was skipped.
- **Label to apply** — `prep-pr-open` on the planning issue after the
RM merges the prep PR.
- **Next steps** — `release-keys-sync` (Step 3), then `release-rc-cut
@@ -744,6 +992,15 @@ The AI-driven part ends with a hand-back artefact containing:
- **Next development version** — restated for clarity.
- **Scope** — confirmed only `version_manifest_files` were modified.
+**For Step A (`automated-signing`, 🪶 ASF-specific):**
+
+- **Eligibility** — met, or the conditions still missing.
+- **Infra ticket draft** and **Security Team notification draft** —
+ for the RM to file and send.
+- **Workflow PR** — URL if opened as a draft, or the rendered file.
+- **Config diff** — the `release-management-config.md § Signing`
+ changes, and what has to happen before `enabled`.
+
---
## Hard rules
@@ -763,6 +1020,17 @@ The AI-driven part ends with a hand-back artefact containing:
- **Never emit signing commands.** `gpg`, `git tag -s`, and `svn`
commands belong to other skills (`release-keys-sync`,
`release-rc-cut`).
+- **Never edit `.gitattributes` without per-entry confirmation**, and
+ never propose excluding `LICENSE`, `NOTICE`, `DISCLAIMER`, a build
+ descriptor, the RAT excludes, or a path a shipped file references.
+- **Never mark the archive review done on the skill's own authority.**
+ `export_ignore_reviewed` is set only in a prep PR the RM confirmed.
+- **Never file the Infra ticket, never send the Security Team mail,
+ never add key material to the workflow.** Step A drafts; the RM
+ files and sends.
+- **Never offer automated release signing outside `organization:
+ ASF`.** The option is an ASF Infra offering; for other organizations
+ it does not exist in this skill.
---
@@ -779,6 +1047,11 @@ The AI-driven part ends with a hand-back artefact containing:
| Changelog coverage low | Many PRs lack standard labels | RM classifies uncategorised PRs before the prep PR opens |
| Scope violation (prep) | A proposed file is outside the expected set | Confirm the extra file explicitly or remove it from the diff |
| Scope violation (post) | A proposed file is outside `version_manifest_files` | Confirm the extra file explicitly or remove it |
+| 2e: proposed exclusion is referenced | A shipped file links to the path (`git grep` hit) | Keep the path, or repoint the reference; never exclude it as-is |
+| 2e: symlink chain | A committed symlink points at another symlink | Exclude the relay dir, keep the single-hop canonical link, or replace the relay with a real link |
+| 2e: no local clone | `user.md` names no `` clone | Set the clone path in `user.md`, or clone and rerun `prep` |
+| Step A blocked — not ASF | `project.md` organization is not `ASF` | No action; automated signing is an ASF Infra offering |
+| Step A blocked — not demonstrably reproducible | No `release-verify-rc` report with every artefact `identical`, or `reproducibility_*` not set as required | Enable the checks in `release-build.md`, cut and verify an RC, then rerun |
---
@@ -788,6 +1061,16 @@ The AI-driven part ends with a hand-back artefact containing:
Steps 1, 2, and 14 context.
- [`docs/release-management/spec.md`](../../../../docs/release-management/spec.md) —
`release-prepare` per-skill specification.
+- [`docs/release-management/reproducibility.md`](../../../../docs/release-management/reproducibility.md) —
+ the source-archive review (2e) buckets and rationale, and the 🪶
+ ASF-specific automated-signing setup (Step A).
+- [`/release-build.md`](../../../../projects/_template/release-build.md) —
+ `§ Source archive` (`source_archive_method`, `export_ignore_reviewed`)
+ and `§ Reproducibility checks`.
+- [`projects/_template/workflows/release-candidate.yml`](../../../../projects/_template/workflows/release-candidate.yml) —
+ the workflow template Step A renders.
+- [`tools/reproducible-archive`](../../../../tools/reproducible-archive/README.md) —
+ `repro-archive build` / `compare` used for the before/after listing.
- [`/release-management-config.md`](../../../../projects/_template/release-management-config.md) —
adopter keys this skill reads (`release_branch_base`,
`version_manifest_files`, `category_x_dependencies`,
diff --git a/plugins/magpie-release-management/skills/promote/SKILL.md b/plugins/magpie-release-management/skills/promote/SKILL.md
index 7ed7cc989..7b9ae319c 100644
--- a/plugins/magpie-release-management/skills/promote/SKILL.md
+++ b/plugins/magpie-release-management/skills/promote/SKILL.md
@@ -342,8 +342,21 @@ non-blocking.
the roster. If the RM is a committer but not a PMC member, set
`rm_is_pmc = false`; the skill continues to emit non-command outputs but
replaces the svn command set with a hand-off note.
-6. **Drift check** — see *Snapshot drift* above.
-7. **Override consultation** — see *Adopter overrides* above.
+6. **Trusted-hardware validation recorded** (🪶 ASF-specific; only when
+ `release-management-config.md` sets `automated_release_signing:
+ enabled` under `organization: ASF`). The planning issue must carry a
+ `release-verify-rc` comment with the **Reproducibility validated on
+ trusted hardware** attestation for *this* `-rc` (every
+ artefact `identical`, `--trusted-hardware` asserted by the committer).
+ Absent → hard blocker: *"automated release signing requires every
+ artefact to be rebuilt bit-by-bit identical on trusted hardware before
+ publication ([Infra § Automated release signing](https://infra.apache.org/release-signing.html#automated-release-signing));
+ run `release-verify-rc -rc --trusted-hardware --post-to
+ ` on your own machine first"*. Not applicable (and
+ never mentioned) when the key is `off`, `requested`, or the project
+ is not ASF.
+7. **Drift check** — see *Snapshot drift* above.
+8. **Override consultation** — see *Adopter overrides* above.
If any check fails (except the PMC gate, which downgrades to hand-off),
stop and surface what is missing.
@@ -388,6 +401,8 @@ Read the following from the planning issue and
| `rc_commit_sha` | git / planning issue body | commit the `-rc` tag points to; the final `` tag is cut on this SAME commit (no rebuild). `git rev-list -n1 -rc` |
| `rm_gpg_fingerprint` | RM `user.md` | `release_manager.gpg_fingerprint`; the release key the final `` tag is signed with |
| `git_upstream_remote` | `release-management-config.md` | `git_upstream_remote`; the remote the final `` tag is pushed to |
+| `convenience_artefacts` | `release-build.md § Convenience artefacts` | the project's optional, project-specific artefacts with their `publish_channel` / `publish_command`; empty for a source-only project |
+| `verify_rc_binaries` | planning issue body | the `release-verify-rc` Step 9 result for this RC: which convenience artefacts reproduced (`identical` / documented `WARN`) and which `differs` |
Surface the loaded metadata to the RM for a brief sanity check before
proceeding to Step 2.
@@ -484,6 +499,44 @@ stop.
---
+### Convenience artefacts (optional, project-specific)
+
+Only when `convenience_artefacts` is non-empty. The source promotion
+above is the release; this block publishes what the project ships
+*besides* the source, to wherever the project declared. Emit it
+**after** the dist promotion and the final tag, as its own section,
+one entry per artefact:
+
+- `publish_channel: dist-release` — nothing to emit: the artefact
+ moved with the source in the promotion above; say so.
+- any other channel — render the entry's `publish_command` verbatim,
+ with `` substituted (for example `twine upload
+ dist/apache_-*`, `mvn nexus-staging:release
+ -DstagingRepositoryId=`, `docker push
+ /:`, `helm push …`). These are the
+ project's own commands; the skill never invents a channel or a
+ command the config does not declare.
+
+**Reproducibility gate — the artefact must be good before it is
+published.** A convenience artefact is publishable only if the
+`release-verify-rc` run recorded on the planning issue rebuilt it from
+the voted tag and it reproduced: `identical`, or `WARN` with every
+difference matched by its `known_divergences`. For an artefact that
+reported `DIFFERS`, or that no verify-rc run covered, emit a **HOLD**
+note in place of its publish command:
+
+```text
+HOLD: — not published. release-verify-rc Step 9 did not
+reproduce it from -rc (). A binary that
+cannot be rebuilt from the voted source is not known to be what the vote
+approved. Fix the build or document the divergence in release-build.md,
+re-run `release-verify-rc -rc`, then re-run this skill.
+```
+
+The source promotion is not held back by a convenience artefact; the
+source is the release, the artefact is a courtesy, and a courtesy that
+cannot be verified is withheld, not shipped.
+
### Mirror note (required for all backends)
After the backend command block, always include:
@@ -514,13 +567,17 @@ Return ONLY valid JSON with this structure:
"rm_is_pmc": true | false,
"handoff_note": "",
"proposed_label": "promoted",
- "mirror_note_present": true
+ "mirror_note_present": true,
+ "convenience_publish_commands": [": "],
+ "convenience_held": [": "]
}
```
`handoff_note` is non-null only when `rm_is_pmc = false`; the command block
is still populated (a PMC member can copy and run it). `mirror_note_present`
is always `true` — the mirror and timing note is never omitted.
+`convenience_publish_commands` and `convenience_held` are both empty for a
+source-only project; every declared artefact appears in exactly one of them.
---
diff --git a/plugins/magpie-release-management/skills/rc-cut/SKILL.md b/plugins/magpie-release-management/skills/rc-cut/SKILL.md
index cae780c6c..9e3abfc44 100644
--- a/plugins/magpie-release-management/skills/rc-cut/SKILL.md
+++ b/plugins/magpie-release-management/skills/rc-cut/SKILL.md
@@ -9,12 +9,18 @@ requires_config:
- release-build.md
- release-management-config.md
description: |
- Emit the paste-ready command sequence to tag an RC, build artefacts,
- sign each artefact, generate checksums, and stage them to the adopter's
- distribution backend. Covers Steps 4–5 of the release-management
- lifecycle. Never runs any command locally — all sequences are emitted
- for the Release Manager to execute on their own machine with their own
- key and ASF credentials.
+ Emit the paste-ready command sequence to tag an RC, build artefacts
+ (the source archive reproducibly, via `git archive` + `.gitattributes`
+ `export-ignore` + the framework's `repro-archive` tool), optionally
+ self-check reproducibility, sign each artefact, generate checksums, and
+ stage them to the adopter's distribution backend. Covers Steps 4–5 of
+ the release-management lifecycle. Never runs any command locally — all
+ sequences are emitted for the Release Manager to execute on their own
+ machine with their own key and ASF credentials. Blocks while the
+ first-release `.gitattributes` review (`release-prepare prep`) is
+ outstanding. For ASF projects with `automated_release_signing: enabled`,
+ emits the tag push that triggers the CI build instead of local
+ sign/stage commands.
when_to_use: |
Invoke when a Release Manager says "cut rc1 for ", "prepare
rc for ", "tag the release candidate", "stage the RC to
@@ -257,6 +263,30 @@ Any path that includes `dist/release/` is on a hard denylist (when `release_dist
skill refuses to emit a command that stages to `dist/release/` (`release_dist_backend = svnpubsub`)
regardless of input. Promotion is `release-promote`'s responsibility.
+**Golden rule 6 — the source artefact is an export of the tag, never
+an archive of a working tree.**
+With `source_archive_method: git-archive` (the default) the source
+artefact is `repro-archive build --ref -` — `git archive`
+(tracked files at the tag only, `.gitattributes` `export-ignore`
+honoured) with every
+[reproducible-builds.org archive rule](https://reproducible-builds.org/docs/archives/)
+applied (one `SOURCE_DATE_EPOCH` mtime, sorted members, uid/gid 0,
+`a=rX,u+w`, no PAX `atime`/`ctime`, `gzip -n`, `zip -X`). The skill
+never emits `zip -r`, `tar czf `, or any command that packs a
+working directory; a working tree carries `__pycache__`, editor state
+and untracked files, and no voter can regenerate it. Rationale and the
+rule-by-rule mapping:
+[`docs/release-management/reproducibility.md`](../../../../docs/release-management/reproducibility.md).
+
+**Golden rule 7 — an unreviewed `.gitattributes` blocks the cut.**
+`git archive` reads `export-ignore` from the tree it archives, so the
+first-release review of what ships (`release-prepare prep` Step 2f)
+must have landed *before* the RC tag exists. While
+`export_ignore_reviewed` is unset in `release-build.md` and
+`source_archive_method` is `git-archive`, Step 0 blocks and points at
+`release-prepare prep `; `--allow-unreviewed-archive` is the
+explicit, logged override.
+
---
## Adopter overrides
@@ -295,10 +325,20 @@ non-blocking.
already exist on the remote; if it does, the skill blocks and the
RM decides whether to bump RC or delete the existing tag.
- **`/release-build.md` readable** — `build_command`,
- `expected_artefacts`, `digest_set`, optional `binary_exclude_list`.
+ `expected_artefacts`, `digest_set`, optional `binary_exclude_list`;
+ `§ Source archive` (`source_archive_method`, `source_archive_format`,
+ `source_archive_prefix`, `export_ignore_reviewed`) and
+ `§ Reproducibility checks` (`reproducibility_source`,
+ `reproducibility_binaries`, `binary_rebuild_command`).
- **`/release-management-config.md` readable** —
`release_dist_backend`, `release_dist_url_template`,
- optional `release_publish_command_template`.
+ optional `release_publish_command_template`; `§ Signing ›
+ automated_release_signing` (🪶 ASF-specific; read only when
+ `project.md` declares `organization: ASF`).
+- **`.gitattributes` reviewed** — when `source_archive_method` is
+ `git-archive`, `export_ignore_reviewed` is set (the first-release
+ review in `release-prepare prep` Step 2f has landed and is in the
+ tree the tag will point at).
---
@@ -311,6 +351,8 @@ non-blocking.
| `--planning-issue ` | Explicit planning issue URL (auto-detected if omitted) |
| `--release-branch ` | Override release branch (default from `release_branch_base` in config) |
| `--remote ` | Override the git remote name pointing at the upstream repo (default from `git_upstream_remote` in config, else `origin`) |
+| `--allow-unreviewed-archive` | Cut the RC although `export_ignore_reviewed` is unset; the override is recorded in the Step 4 planning-issue comment |
+| `--skip-repro-check` | Do not emit Step 2b's optional reproducibility self-check (has no effect when `automated_release_signing: enabled`, where the check is mandatory) |
---
@@ -333,8 +375,26 @@ non-blocking.
(`release_dist_backend`, `release_dist_url_template`) are present.
7. **Digest set valid.** `digest_set` in `release-build.md` contains at
least `sha512` and does not contain `md5` or `sha1`.
-8. **Drift check** — see *Snapshot drift* above.
-9. **Override consultation** — see *Adopter overrides* above.
+8. **Source-archive contents reviewed.** When `source_archive_method`
+ is `git-archive` (or unset — that is the default), `release-build.md
+ § Source archive` must set `export_ignore_reviewed`. If it is unset
+ and `--allow-unreviewed-archive` was not passed, block with
+ `archive_reviewed: false` and the remediation *"run
+ `release-prepare prep ` — its Step 2f walks you through
+ what ships in the source archive and lands `.gitattributes` in the
+ prep PR"*. With the override, proceed with `archive_reviewed: false`
+ and carry the override into Step 4. With `source_archive_method:
+ custom` the check does not apply (`archive_reviewed: true`).
+9. **Signing mode consistent** (🪶 ASF-specific). When
+ `automated_release_signing` is `enabled`, `project.md` must declare
+ `organization: ASF`, `reproducibility_source` must be `on`, and
+ `reproducibility_binaries` must be `byte-identical` for every
+ convenience binary in `expected_artefacts` — the policy conditions in
+ [Infra § Automated release signing](https://infra.apache.org/release-signing.html#automated-release-signing).
+ Any other combination blocks. For a non-ASF project the key is
+ ignored and never mentioned.
+10. **Drift check** — see *Snapshot drift* above.
+11. **Override consultation** — see *Adopter overrides* above.
If any check fails (and is not overridable), stop and surface what is
missing with the exact key name or API path that failed.
@@ -346,11 +406,15 @@ Return ONLY valid JSON with this structure:
"verdict": "proceed" | "blocked",
"blockers": [""],
"rc_tag_exists": true | false,
- "prep_pr_merged": true | false
+ "prep_pr_merged": true | false,
+ "archive_reviewed": true | false
}
```
`verdict` is `"proceed"` only when all hard blockers resolve.
+`archive_reviewed` is `true` when `export_ignore_reviewed` is set or the
+check does not apply; `false` when the review is outstanding (blocked,
+or overridden with `--allow-unreviewed-archive`).
---
@@ -370,6 +434,13 @@ Read the following from `/release-build.md` and
| `signing_key_fingerprint` | user.md or `release-management-config.md` | `rm_key_fingerprint` |
| `release_branch` | `release-management-config.md` | `release_branch_base` (or `--release-branch` override) |
| `git_upstream_remote` | `release-management-config.md` | `git_upstream_remote` — git remote name pointing at the upstream repo (default `origin`, or `--remote` override) |
+| `source_archive_method` | `release-build.md § Source archive` | `git-archive` (default) or `custom` |
+| `source_archive_format` | `release-build.md § Source archive` | `tar.gz` or `zip` |
+| `source_archive_prefix` | `release-build.md § Source archive` | top-level directory inside the archive, rendered with `` |
+| `reproducibility_source` | `release-build.md § Reproducibility checks` | `on` (default with `git-archive`) or `off` |
+| `reproducibility_binaries` | `release-build.md § Reproducibility checks` | `off` (default), `byte-identical`, `documented-divergence`; with `binary_rebuild_command` |
+| `signing_mode` | `release-management-config.md § Signing` | `rm-key` (default) or `ci-automated` when `automated_release_signing: enabled` **and** `project.md` → `organization: ASF`; non-ASF projects always resolve to `rm-key` |
+| `convenience_artefacts` | `release-build.md § Convenience artefacts` | the project's optional, project-specific artefacts besides the source — each with `build_command`, `staging` / `stage_command`, `reproducibility`, `vote_included`; empty for a source-only project |
Surface the loaded configuration to the RM for confirmation before
proceeding to Step 2.
@@ -388,7 +459,13 @@ Return ONLY valid JSON with this structure:
"staging_url": "",
"signing_key_fingerprint": "",
"release_branch": "",
- "git_upstream_remote": ""
+ "git_upstream_remote": "",
+ "source_archive_method": "git-archive" | "custom",
+ "source_archive_format": "tar.gz" | "zip",
+ "source_archive_prefix": "",
+ "reproducibility_source": "on" | "off",
+ "reproducibility_binaries": "off" | "byte-identical" | "documented-divergence",
+ "signing_mode": "rm-key" | "ci-automated"
}
```
@@ -414,16 +491,77 @@ git push -
**Section 2 — Build command.**
-The exact `build_command` from `release-build.md`, emitted verbatim (run at
-the tag). First **gitignore the RC artefacts** (`` + `.asc`/`.sha512`,
+First **gitignore the RC artefacts** (`` + `.asc`/`.sha512`,
e.g. a committed glob like `*-source.zip*`) so a stray `git add` never commits
-an RC build:
+an RC build. Then, depending on `source_archive_method`:
+
+*`git-archive` (default).* The source artefact is exported from the tag
+with the framework's
+[`reproducible-archive`](../../../../tools/reproducible-archive/README.md)
+tool (`` is `.apache-magpie` in an adopting project, `.` in
+the framework checkout; `python3 /tools/reproducible-archive/src/reproducible_archive/__init__.py`
+is the no-`uv` equivalent). It packs only tracked files at the tag,
+honours `.gitattributes` `export-ignore`, and applies every
+reproducible-builds.org archive rule, so the bytes are a function of
+the tag alone. It prints the **record** the RM pastes back for the
+Step 4 comment: the commit, the `SOURCE_DATE_EPOCH` it used (the tag's
+committer timestamp), the sha512, and the
+[Software Heritage identifiers](https://swhid.org/) — `swh:1:rev:` of
+the commit and `swh:1:dir:` of the archive's expanded content, both
+qualified with the repository URL (`--origin`, rendered from
+``) — plus a note saying whether the content SWHID equals
+the repository tree at the commit (nothing `export-ignore`d) or not.
+`build_command` (if any) follows, for convenience binaries only, with
+the same `SOURCE_DATE_EPOCH` exported so embedded timestamps are fixed:
+
+```text
+# Run at the release tag -
+uv run --project /tools/reproducible-archive repro-archive build \
+ --ref "-" --format \
+ --prefix "" \
+ --origin "https://github.com/" \
+ -o ""
+# → prints: commit , SOURCE_DATE_EPOCH , sha512 ,
+# swhid_rev swh:1:rev:;origin=…, swhid_dir swh:1:dir:;origin=…;anchor=…,
+# swhid_dir_note the repository tree
+
+# Convenience binaries (only when build_command is set):
+export SOURCE_DATE_EPOCH="$(uv run --project /tools/reproducible-archive repro-archive epoch --ref "-")"
+
+```
+
+`` is the canonical source artefact from
+`expected_artefacts`; its extension must match `source_archive_format`.
+
+*`custom`.* The exact `build_command` from `release-build.md`, emitted
+verbatim (run at the tag), with `SOURCE_DATE_EPOCH` exported first:
```text
# Run at the release tag -
+export SOURCE_DATE_EPOCH="$(git log -1 --format=%ct "-")"
```
+Under either method, never emit `zip -r`, `tar czf ` or any
+other command that packs a working directory (Golden rule 6).
+
+*Convenience artefacts (optional, project-specific).* When
+`convenience_artefacts` is non-empty, follow the source archive with
+one block per entry — the entry's own `build_command` verbatim, under
+the same `SOURCE_DATE_EPOCH`, so the artefact is a function of the tag
+and a voter can rebuild it in `release-verify-rc` Step 9 (the check
+that decides whether a binary is good). The framework does not know
+how a project builds its wheels, jars or images; the config does:
+
+```text
+# Convenience artefact: () — built from the tagged source
+export SOURCE_DATE_EPOCH="$(uv run --project /tools/reproducible-archive repro-archive epoch --ref "-")"
+
+```
+
+For a source-only project say *"no convenience artefacts declared"*
+rather than emitting a build block.
+
**Section 3 — Sign commands.**
For each artefact in `expected_artefacts`:
@@ -466,10 +604,149 @@ Return ONLY valid JSON with this structure:
or `sha1` digest command was emitted. `proposed` is always `true` at the
point this JSON is returned — the RM has not yet confirmed execution.
+When `signing_mode` is `ci-automated`, Sections 3 and 4 are **not**
+emitted (CI signs and checksums); return them as empty lists and
+continue with Step 2c instead of Step 3.
+
+---
+
+## Step 2b — Emit reproducibility self-check commands (optional)
+
+Skipped when `reproducibility_source` is `off` **and**
+`reproducibility_binaries` is `off`, or when `--skip-repro-check` was
+passed and `signing_mode` is `rm-key`. Mandatory (the flag is ignored)
+when `signing_mode` is `ci-automated`. Run **after** the build and
+**before** signing: a non-reproducible build found here costs a rebuild,
+found by a voter it costs an RC.
+
+**Source (`reproducibility_source: on`).** Lint the artefact against
+the reproducible-builds.org checklist, rebuild it into a scratch
+directory from the same tag, and compare:
+
+```text
+# 1. every archive rule holds (single SOURCE_DATE_EPOCH mtime, sorted, uid/gid 0, a=rX,u+w, no PAX atime/ctime, gzip -n / zip -X)
+uv run --project /tools/reproducible-archive repro-archive check \
+ "" --epoch ""
+# 2. rebuild from the tag and require byte-identical output
+mkdir -p rebuild
+uv run --project /tools/reproducible-archive repro-archive build \
+ --ref "-" --format \
+ --prefix "" -o "rebuild/"
+uv run --project /tools/reproducible-archive repro-archive compare --require-identical \
+ "" "rebuild/"
+```
+
+With `source_archive_method: custom` the `check` still runs (it lints
+any `.tar`, `.tar.gz` or `.zip`); the rebuild step re-runs
+`build_command` into `rebuild/` and compares with
+`repro-archive compare`. `content-identical` is then a warning to
+switch the build to `repro-archive build` or `repro-archive recipe`;
+`differs` is a stop.
+
+**Convenience artefacts.** One block per entry in
+`convenience_artefacts`, using the entry's `reproducibility` mode
+(default `reproducibility_binaries`). `byte-identical` — re-run the
+entry's `build_command` into `rebuild/` under the same
+`SOURCE_DATE_EPOCH` and compare bytes:
+
+```text
+export SOURCE_DATE_EPOCH=""
+( cd rebuild && )
+cmp "" "rebuild/" \
+ && echo "identical: " || echo "DIFFERS: "
+```
+
+`documented-divergence` — the same rebuild, then the entry's
+`verification_command` (for example `diffoscope
+rebuild/`); any difference not listed under the
+entry's `known_divergences` is a stop, listed ones are reported.
+`off` — state `SKIP` explicitly for that artefact. An artefact that
+does not reproduce here is not good to sign: it is not known to be
+what the tagged source produces, and `release-promote` will withhold
+its publication until a verify-rc run reproduces it.
+
+The RM runs the block and reports the outcome. Any `differs` /
+`DIFFERS` stops the cut: the RM fixes the build (or documents the
+divergence) and rebuilds before signing anything.
+
+Return ONLY valid JSON with this structure:
+
+```json
+{
+ "source_check_enabled": true | false,
+ "binary_check_mode": "off" | "byte-identical" | "documented-divergence",
+ "mandatory": true | false,
+ "source_check_commands": ["", "", ""],
+ "binary_check_commands": [""],
+ "stop_on": ["differs", "DIFFERS"],
+ "proposed": true
+}
+```
+
+`mandatory` is `true` only when `signing_mode` is `ci-automated`.
+`source_check_commands` is empty when `source_check_enabled` is
+`false`; `binary_check_commands` is empty when `binary_check_mode` is
+`off`. `stop_on` always lists the verdicts that halt the cut.
+`proposed` is always `true`.
+
+---
+
+## Step 2c — CI-signed flow (🪶 ASF-specific, `signing_mode: ci-automated`)
+
+Only for a project whose `project.md` declares `organization: ASF` and
+whose `release-management-config.md` sets `automated_release_signing:
+enabled` after the one-time setup in `release-prepare automated-signing`
+(Infra-provisioned key, Security Team approval, workflow merged). For
+every other project this step does not exist and is never mentioned.
+
+Under
+[Infra § Automated release signing](https://infra.apache.org/release-signing.html#automated-release-signing)
+CI builds, signs and **stages** the artefacts; a committer re-validates
+them bit-by-bit on trusted hardware before anything is published. The
+RM still signs the **tag** with their own key (Section 1). Instead of
+Sections 3–4 and Step 3, emit:
+
+```text
+# 1. Push the signed tag — this triggers
+git push -
+# 2. Watch the run; it builds reproducibly (repro-archive), self-compares,
+# checksums, and uploads to ATR (OIDC trusted publishing). It publishes nothing.
+gh run list --repo --workflow --branch -
+gh run watch --repo