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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions .github/workflows/provenance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# SPDX-FileCopyrightText: 2026 Sudo Apt Holdings LLC
# SPDX-License-Identifier: Apache-2.0
#
# Build provenance for the Hex tarball, as beam_mcp's provenance workflow does it: CI builds
# the tarball the canonical way (tools/release_tarball.sh: `git archive` at the ref, every
# file 644), attests its digest (SLSA build provenance, Sigstore-signed, GitHub's attestation
# store), and verifies the attestation against the tarball it built and against the bytes
# hex.pm serves for that version. The tarball's sha256 is hex.pm's package checksum, so a
# match binds the attestation to the published package.
#
# THE OWNER TAGS AND PUBLISHES; THIS WORKFLOW ATTESTS. It never runs `mix hex.publish`. Order:
# publish, then push the tag; on the tag's run a version hex.pm does not serve, or serves as
# other bytes, is a failure.
#
# A manual run takes `ref`, a tag already published (for example v0.2.1, released before this
# workflow existed): it builds that tag's tree with this workflow and holds the result to
# hex.pm's bytes the same way, so a failure to fetch or match fails the run. With no `ref`,
# a manual run attests the branch head, which is nobody's release, and its hex.pm half reads
# NOT MEASURED.

name: provenance

on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
ref:
description: "A published release tag to attest (e.g. v0.2.1); empty attests the branch head"
required: false
default: ""

permissions:
contents: read

jobs:
attest:
name: Provenance - build, attest, verify
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
attestations: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
# The pinned pair, a copy of .tool-versions' line.
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
with:
otp-version: "28"
elixir-version: "1.18"
- name: the tarball, built the canonical way, its version and its sha256 (hex.pm's package checksum)
id: build
env:
INPUT_REF: ${{ inputs.ref }}
run: |
set -euo pipefail
ref="${INPUT_REF:-${GITHUB_SHA}}"
case "$ref" in
"${GITHUB_SHA}") claimed="${{ github.ref_type == 'tag' }}" ;;
v*) git rev-parse --verify --quiet "refs/tags/${ref}" >/dev/null || { echo "no tag ${ref}"; exit 1; }; claimed=true ;;
*) echo "ref must be a release tag (v...) or empty"; exit 1 ;;
esac
version=$(git show "${ref}:mix.exs" | sed -nE 's/^ @version "([^"]+)"$/\1/p' | head -1)
[ -n "$version" ] || { echo "no @version in ${ref}'s mix.exs"; exit 1; }
tools/release_tarball.sh "${ref}" "beam_mcp_signer-${version}.tar" | tee build.out
sha=$(sha256sum "beam_mcp_signer-${version}.tar" | cut -d' ' -f1)
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "sha256=${sha}" >> "$GITHUB_OUTPUT"
echo "claimed=${claimed}" >> "$GITHUB_OUTPUT"
echo "tarball beam_mcp_signer-${version}.tar from ${ref}: sha256 ${sha}"
# Pinned by commit (v4.2.2), as in beam_mcp.
- uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: beam_mcp_signer-${{ steps.build.outputs.version }}.tar
- name: verify the attestation against the tarball this run built
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh attestation verify "beam_mcp_signer-${{ steps.build.outputs.version }}.tar" --repo "${GITHUB_REPOSITORY}" \
--signer-workflow "${GITHUB_REPOSITORY}/.github/workflows/provenance.yml"
- name: verify the attestation against the bytes hex.pm serves for this version
env:
GH_TOKEN: ${{ github.token }}
CLAIMED: ${{ steps.build.outputs.claimed }}
run: |
set -uo pipefail
v="${{ steps.build.outputs.version }}"
code=$(curl -sSL -o "published-${v}.tar" -w '%{http_code}' "https://repo.hex.pm/tarballs/beam_mcp_signer-${v}.tar") || code="000"
if [ "${code}" != "200" ]; then
if [ "${CLAIMED}" = "true" ]; then
echo "FAIL: hex.pm answered ${code} for beam_mcp_signer ${v} -- publish first, then push the tag (404: not published; anything else: the fetch failed)"
exit 1
fi
echo "NOT MEASURED: hex.pm answered ${code} for beam_mcp_signer ${v}; a run on a branch head attests nobody's release"
exit 0
fi
pub=$(sha256sum "published-${v}.tar" | cut -d' ' -f1)
echo "published sha256 ${pub}; built ${{ steps.build.outputs.sha256 }}"
if [ "${pub}" != "${{ steps.build.outputs.sha256 }}" ]; then
if [ "${CLAIMED}" = "true" ]; then
echo "FAIL: the published tarball is not the bytes this tag builds canonically; it was not published with tools/release_tarball.sh"
exit 1
fi
echo "NOT MEASURED: hex.pm serves ${v} but not this commit's bytes; a run on a branch head attests nobody's release"
exit 0
fi
gh attestation verify "published-${v}.tar" --repo "${GITHUB_REPOSITORY}" \
--signer-workflow "${GITHUB_REPOSITORY}/.github/workflows/provenance.yml"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: tarball-${{ steps.build.outputs.version }}
path: beam_mcp_signer-${{ steps.build.outputs.version }}.tar
if-no-files-found: error
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ SPDX-License-Identifier: Apache-2.0

## [Unreleased]

### Added: build provenance for each release

- `.github/workflows/provenance.yml`: on a release tag, CI builds the tarball with
`tools/release_tarball.sh`, attests its digest (SLSA build provenance), and verifies the
attestation against what hex.pm serves. A manual run takes a published tag, so `0.2.1` is
attested after its release. `docs/verifying-releases.md` gives the command.

### Changed: `main` is protected by a ruleset (no code change)

- `GOVERNANCE.md`: how a change lands is now enforced on `main` (pull requests only, rebase
merges, linear history, the five checks required, no bypass), where it had been practice.

## [0.2.1] - 2026-09-26

A security fix: two more ways the private key could reach an exception report are closed.
Expand Down
7 changes: 6 additions & 1 deletion GOVERNANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@ date, so the bus factor is two, and his reviews are the public record of it.
## How a change lands

1. A branch and a pull request; nothing is pushed to `main` directly.
2. CI on three OTP/Elixir pairs (the floor, the pinned line, the newest) and the DCO check.
2. CI on three OTP/Elixir pairs (the floor, the pinned line, the newest), the REUSE lint and
the DCO check.
3. The maintainer's review, then a rebase merge.

From 2026-09-26 a repository ruleset on `main` holds steps 1 to 3 rather than leaving them to
habit, as `beam_mcp`'s does: no deletion or force-push, changes by pull request only, a rebase
merge and linear history, and the five checks above required and up to date, with no bypass.
4. A release is a signed tag and a hex.pm publish by the maintainer, recorded in the
CHANGELOG; `docs/verifying-releases.md` says how to check one.

Expand Down
17 changes: 17 additions & 0 deletions docs/verifying-releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,20 @@ The script builds from `git archive` of the tag (tracked files only, every file
two machines produce the same bytes. A release published with the script (`--publish`) has
the checksum hex.pm shows; `0.1.0` and `0.1.1` were built from a working tree before the
script existed, and are verified by their signed tags only.

## Build provenance

From `0.2.1` each release's tarball carries a SLSA build-provenance attestation, made by
`.github/workflows/provenance.yml`: CI builds the tarball from the tag with the same script,
attests its digest (Sigstore-signed, in GitHub's attestation store), and checks that the
bytes hex.pm serves are the bytes it attested. `0.2.1` was attested after its release, by a
manual run of the workflow on its tag; later releases are attested by the tag's own run.

```sh
v=0.2.1
curl -fsSLO "https://repo.hex.pm/tarballs/beam_mcp_signer-${v}.tar"
gh attestation verify "beam_mcp_signer-${v}.tar" --repo ScriptKittyOS/beam_mcp_signer \
--signer-workflow ScriptKittyOS/beam_mcp_signer/.github/workflows/provenance.yml
```

`gh attestation` needs GitHub CLI 2.49 or newer.