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
12 changes: 7 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,18 @@ jobs:
run: |
cargo install just --version 1.57.0 --locked
cargo install wasm-tools --version 1.252.0 --locked
cargo install b3sum --version 1.8.3 --locked
- name: Build checksum-verified zstd 1.5.7
run: |
curl --proto '=https' --tlsv1.2 -fsSLO https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz
echo 'eb33e51f49a15e023950cd7825ca74a4a2b43db8354825ac24fc1b7ee09e6fa3 zstd-1.5.7.tar.gz' | sha256sum --check --strict
tar -xzf zstd-1.5.7.tar.gz
make -C zstd-1.5.7 -j2 zstd
echo "$PWD/zstd-1.5.7/programs" >> "$GITHUB_PATH"
- name: Build twice with identical canonical bytes
- name: Build twice with identical package and release identity
run: |
just dist
first="$(sha256sum dist/*.sigil-plugin.tar.zst)"
just dist
test "$first" = "$(sha256sum dist/*.sigil-plugin.tar.zst)"
source_commit=0000000000000000000000000000000000000000
just release-dist "$source_commit"
first="$(sha256sum dist/*.sigil-plugin.tar.zst dist/SHA256SUMS dist/release-manifest.json)"
just release-dist "$source_commit"
test "$first" = "$(sha256sum dist/*.sigil-plugin.tar.zst dist/SHA256SUMS dist/release-manifest.json)"
Original file line number Diff line number Diff line change
@@ -1,45 +1,40 @@
name: release
name: prepare-release

on:
push:
tags: ['v*']
workflow_dispatch:

permissions:
contents: read

jobs:
release:
assets:
runs-on: ubuntu-24.04
environment: release
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
fetch-depth: 0
persist-credentials: false
- name: Install pinned build tools
run: |
cargo install just --version 1.57.0 --locked
cargo install wasm-tools --version 1.252.0 --locked
cargo install b3sum --version 1.8.3 --locked
- name: Build checksum-verified zstd 1.5.7
run: |
curl --proto '=https' --tlsv1.2 -fsSLO https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz
echo 'eb33e51f49a15e023950cd7825ca74a4a2b43db8354825ac24fc1b7ee09e6fa3 zstd-1.5.7.tar.gz' | sha256sum --check --strict
tar -xzf zstd-1.5.7.tar.gz
make -C zstd-1.5.7 -j2 zstd
echo "$PWD/zstd-1.5.7/programs" >> "$GITHUB_PATH"
- name: Verify tag and build canonical assets
- name: Build exact package and canonical release identity
run: |
version="$(sed -n 's/^version = "\([^"]*\)"$/\1/p' plugin.toml)"
test "${GITHUB_REF_NAME#v}" = "$version"
just dist
- name: Publish immutable release
env:
GH_TOKEN: ${{ github.token }}
run: |
version="${GITHUB_REF_NAME#v}"
name="$(sed -n 's/^name = "\([^"]*\)"$/\1/p' plugin.toml)"
gh release create "$GITHUB_REF_NAME" \
"dist/$name-$version.sigil-plugin.tar.zst" \
dist/SHA256SUMS \
--verify-tag --title "$name $version" --generate-notes
set -euo pipefail
test "$GITHUB_REF" = refs/heads/main
test "$(sed -n 's/^source = "\([^"]*\)"$/\1/p' plugin.toml)" = "github:${GITHUB_REPOSITORY}"
just release-dist "$GITHUB_SHA"
test "$(find dist -mindepth 1 -maxdepth 1 -type f | wc -l)" -eq 3
test "$(find dist -mindepth 1 -maxdepth 1 | wc -l)" -eq 3
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: release-candidate-assets
path: dist/
if-no-files-found: error
254 changes: 254 additions & 0 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
name: publish-release

on:
workflow_dispatch:
inputs:
version:
description: Exact approved SemVer without v prefix
required: true
type: string
source_commit:
description: Exact approved 40-hex main commit
required: true
type: string
candidate_run_id:
description: Exact first-attempt prepare-release run
required: true
type: string
package_sha256:
description: Exact approved package SHA-256
required: true
type: string
checksums_sha256:
description: Exact approved SHA256SUMS SHA-256
required: true
type: string
release_manifest_sha256:
description: Exact approved release-manifest.json SHA-256
required: true
type: string

permissions: {}

jobs:
publish:
runs-on: ubuntu-24.04
environment: release
permissions:
actions: read
artifact-metadata: write
attestations: write
contents: write
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
persist-credentials: false
- name: Verify dispatch and candidate build identity
env:
GH_TOKEN: ${{ github.token }}
CANDIDATE_RUN_ID: ${{ inputs.candidate_run_id }}
SOURCE_COMMIT: ${{ inputs.source_commit }}
run: |
set -euo pipefail
[[ "$CANDIDATE_RUN_ID" =~ ^[1-9][0-9]*$ ]]
[[ "$SOURCE_COMMIT" =~ ^[0-9a-f]{40}$ ]]
test "$GITHUB_REF" = refs/heads/main
test "$GITHUB_SHA" = "$SOURCE_COMMIT"
test "$(git rev-parse HEAD)" = "$SOURCE_COMMIT"
run_json="$(mktemp)"
gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${CANDIDATE_RUN_ID}" > "$run_json"
jq -e --arg repository "$GITHUB_REPOSITORY" --arg commit "$SOURCE_COMMIT" '
.repository.full_name == $repository and
.path == ".github/workflows/prepare-release.yml" and
.event == "workflow_dispatch" and
.head_branch == "main" and
.head_sha == $commit and
.status == "completed" and
.conclusion == "success" and
.run_attempt == 1
' "$run_json" >/dev/null
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: release-candidate-assets
path: dist
github-token: ${{ github.token }}
run-id: ${{ inputs.candidate_run_id }}
- name: Verify exact approved unpublished bytes
id: release
env:
VERSION: ${{ inputs.version }}
SOURCE_COMMIT: ${{ inputs.source_commit }}
PACKAGE_SHA256: ${{ inputs.package_sha256 }}
CHECKSUMS_SHA256: ${{ inputs.checksums_sha256 }}
RELEASE_MANIFEST_SHA256: ${{ inputs.release_manifest_sha256 }}
run: |
set -euo pipefail
[[ "$VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]
[[ "$SOURCE_COMMIT" =~ ^[0-9a-f]{40}$ ]]
[[ "$PACKAGE_SHA256" =~ ^[0-9a-f]{64}$ ]]
[[ "$CHECKSUMS_SHA256" =~ ^[0-9a-f]{64}$ ]]
[[ "$RELEASE_MANIFEST_SHA256" =~ ^[0-9a-f]{64}$ ]]
name="$(sed -n 's/^name = "\([^"]*\)"$/\1/p' plugin.toml)"
source="$(sed -n 's/^source = "\([^"]*\)"$/\1/p' plugin.toml)"
test "$source" = "github:${GITHUB_REPOSITORY}"
test "$(sed -n 's/^version = "\([^"]*\)"$/\1/p' plugin.toml)" = "$VERSION"
package="${name}-${VERSION}.sigil-plugin.tar.zst"
test -f "dist/$package" && test ! -L "dist/$package"
test -f dist/SHA256SUMS && test ! -L dist/SHA256SUMS
test -f dist/release-manifest.json && test ! -L dist/release-manifest.json
test "$(find dist -mindepth 1 -maxdepth 1 -type f | wc -l)" -eq 3
test "$(find dist -mindepth 1 -maxdepth 1 | wc -l)" -eq 3
printf '%s %s\n' "$PACKAGE_SHA256" "dist/$package" | sha256sum --check --strict
printf '%s %s\n' "$CHECKSUMS_SHA256" dist/SHA256SUMS | sha256sum --check --strict
printf '%s %s\n' "$RELEASE_MANIFEST_SHA256" dist/release-manifest.json | sha256sum --check --strict
test "$(cat dist/SHA256SUMS)" = "$PACKAGE_SHA256 $package"
python3 -I - "$name" "$VERSION" "$SOURCE_COMMIT" "$source" "$package" "$PACKAGE_SHA256" <<'PY'
import json
from pathlib import Path
import re
import sys

name, version, commit, source, package, package_sha256 = sys.argv[1:]
path = Path("dist/release-manifest.json")
raw = path.read_bytes()
value = json.loads(raw)
assert set(value) == {
"schema_version", "source", "source_commit", "name", "version",
"asset_name", "package_sha256", "package_blake3",
"manifest_blake3", "component_blake3",
}
assert value["schema_version"] == 1
assert value["source"] == source
assert value["source_commit"] == commit
assert value["name"] == name
assert value["version"] == version
assert value["asset_name"] == package
assert value["package_sha256"] == f"sha256:{package_sha256}"
for field in ("package_blake3", "manifest_blake3", "component_blake3"):
assert re.fullmatch(r"blake3:[0-9a-f]{64}", value[field])
canonical = json.dumps(
value, allow_nan=False, ensure_ascii=True, sort_keys=True,
separators=(",", ":"),
).encode("ascii")
assert raw == canonical
PY
echo "package_name=$package" >> "$GITHUB_OUTPUT"
echo "subject_digest=sha256:$PACKAGE_SHA256" >> "$GITHUB_OUTPUT"
- name: Stage and read back the exact draft
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
SOURCE_COMMIT: ${{ inputs.source_commit }}
PACKAGE_SHA256: ${{ inputs.package_sha256 }}
CHECKSUMS_SHA256: ${{ inputs.checksums_sha256 }}
RELEASE_MANIFEST_SHA256: ${{ inputs.release_manifest_sha256 }}
PACKAGE_NAME: ${{ steps.release.outputs.package_name }}
run: |
set -euo pipefail
tag="v${VERSION}"
if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
echo "existing tag burns ${tag}; refusing publication" >&2
exit 1
fi
if gh release view "$tag" >/dev/null 2>&1; then
echo "existing release burns ${tag}; refusing publication" >&2
exit 1
fi
status="$(curl --proto '=https' --tlsv1.2 --silent --show-error \
--output "${RUNNER_TEMP}/preexisting-attestations.json" --write-out '%{http_code}' \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/attestations/sha256:${PACKAGE_SHA256}?predicate_type=provenance&per_page=30")"
test "$status" = 404
gh release create "$tag" \
--target "$SOURCE_COMMIT" \
--title "${PACKAGE_NAME%.sigil-plugin.tar.zst}" \
--notes "Immutable keyless-provenance Sigil plugin ${VERSION}." \
--draft \
"dist/$PACKAGE_NAME" dist/SHA256SUMS dist/release-manifest.json
release_json="$(mktemp)"
gh release view "$tag" --json isDraft,isPrerelease,targetCommitish,assets > "$release_json"
jq -e --arg commit "$SOURCE_COMMIT" --arg package "$PACKAGE_NAME" '
.isDraft == true and .isPrerelease == false and .targetCommitish == $commit and
([.assets[].name] | sort) == (["SHA256SUMS", "release-manifest.json", $package] | sort)
' "$release_json" >/dev/null
readback="$(mktemp -d)"
gh release download "$tag" --dir "$readback"
test "$(find "$readback" -mindepth 1 -maxdepth 1 -type f | wc -l)" -eq 3
test "$(find "$readback" -mindepth 1 -maxdepth 1 | wc -l)" -eq 3
test "$(sha256sum "$readback/$PACKAGE_NAME" | cut -d' ' -f1)" = "$PACKAGE_SHA256"
test "$(sha256sum "$readback/SHA256SUMS" | cut -d' ' -f1)" = "$CHECKSUMS_SHA256"
test "$(sha256sum "$readback/release-manifest.json" | cut -d' ' -f1)" = "$RELEASE_MANIFEST_SHA256"
test "$(cat "$readback/SHA256SUMS")" = "$PACKAGE_SHA256 $PACKAGE_NAME"
- name: Create GitHub OIDC build provenance for the exact package
id: attest
uses: actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d # v4.2.1
with:
subject-name: ${{ steps.release.outputs.package_name }}
subject-digest: ${{ steps.release.outputs.subject_digest }}
show-summary: false
- name: Publish once and verify immutable public state
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
SOURCE_COMMIT: ${{ inputs.source_commit }}
PACKAGE_SHA256: ${{ inputs.package_sha256 }}
CHECKSUMS_SHA256: ${{ inputs.checksums_sha256 }}
RELEASE_MANIFEST_SHA256: ${{ inputs.release_manifest_sha256 }}
PACKAGE_NAME: ${{ steps.release.outputs.package_name }}
ATTESTATION_ID: ${{ steps.attest.outputs.attestation-id }}
BUNDLE_PATH: ${{ steps.attest.outputs.bundle-path }}
run: |
set -euo pipefail
test -n "$ATTESTATION_ID"
test -f "$BUNDLE_PATH" && test ! -L "$BUNDLE_PATH"
attestation_found=false
for _ in 1 2 3 4 5 6; do
status="$(curl --proto '=https' --tlsv1.2 --silent --show-error \
--output "${RUNNER_TEMP}/attestations.json" --write-out '%{http_code}' \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/attestations/sha256:${PACKAGE_SHA256}?predicate_type=provenance&per_page=30")"
if test "$status" = 200 && jq -e --argjson repository_id "$GITHUB_REPOSITORY_ID" '
(.attestations | length) == 1 and
.attestations[0].repository_id == $repository_id and
.attestations[0].initiator == "user"
' "${RUNNER_TEMP}/attestations.json" >/dev/null; then
attestation_found=true
break
fi
sleep 5
done
test "$attestation_found" = true
tag="v${VERSION}"
test "$(gh release view "$tag" --json isDraft,targetCommitish --jq '.isDraft')" = true
test "$(gh release view "$tag" --json isDraft,targetCommitish --jq '.targetCommitish')" = "$SOURCE_COMMIT"
if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
echo "tag appeared after preflight; ${tag} is burned" >&2
exit 1
fi
gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" \
-f ref="refs/tags/${tag}" -f sha="$SOURCE_COMMIT" >/dev/null
git fetch --force origin "refs/tags/$tag:refs/tags/$tag"
test "$(git rev-list -n 1 "$tag")" = "$SOURCE_COMMIT"
gh release edit "$tag" --draft=false --latest=false
immutable=false
for _ in 1 2 3 4 5 6; do
if test "$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${tag}" --jq '.immutable')" = true; then
immutable=true
break
fi
sleep 5
done
test "$immutable" = true
test "$(gh release view "$tag" --json isDraft --jq '.isDraft')" = false
readback="$(mktemp -d)"
gh release download "$tag" --dir "$readback"
test "$(find "$readback" -mindepth 1 -maxdepth 1 -type f | wc -l)" -eq 3
test "$(find "$readback" -mindepth 1 -maxdepth 1 | wc -l)" -eq 3
test "$(sha256sum "$readback/$PACKAGE_NAME" | cut -d' ' -f1)" = "$PACKAGE_SHA256"
test "$(sha256sum "$readback/SHA256SUMS" | cut -d' ' -f1)" = "$CHECKSUMS_SHA256"
test "$(sha256sum "$readback/release-manifest.json" | cut -d' ' -f1)" = "$RELEASE_MANIFEST_SHA256"
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/build/
/dist/
/plugin.wasm

__pycache__/
4 changes: 4 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ sigil-check: check
dist: check
mkdir -p dist
{{python}} scripts/pack.py plugin.toml dist

release-dist source_commit: check
mkdir -p dist
{{python}} scripts/pack.py plugin.toml dist --source-commit "{{source_commit}}"
30 changes: 20 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ Before publishing a derived plugin:
contract and declares exactly the matching capabilities.
3. Run `just check` with `wasm-tools 1.252.0`, then run `just sigil-check`
with a Sigil release that provides the `plugin` command.
4. Review the exact tag, source commit, package, and `SHA256SUMS` bytes.
5. Configure the repository's protected `release` environment before pushing
a tag. Published versions are immutable; recovery always uses a new SemVer.
4. Configure the protected `release` environment to allow only `main` and to
require an explicit human reviewer.
5. Dispatch `prepare-release` from the reviewed `main` commit, reproduce its
package, `SHA256SUMS`, and canonical `release-manifest.json` locally, then
review the exact candidate run and digests.
6. Dispatch `publish-release` once with that approved tuple. Existing or
partial tags, releases, or attestations burn the SemVer; recovery always
prepares and approves a new version.

The component is built from checked-in WIT and core WAT, then validated and
packed into Sigil's canonical P3 archive:
Expand All @@ -26,11 +31,16 @@ just dist
just sigil-check
```

The bootstrap workflows pin `wasm-tools`, zstd 1.5.7 source, and every Action
commit. The small compatibility packer is byte-identical to Sigil P3 and
avoids depending on an unreleased Sigil command; installation still performs
Sigil's complete manifest, component, archive, and digest validation.
The workflows pin `wasm-tools`, zstd 1.5.7 source, and every Action commit. The
small compatibility packer is byte-identical to Sigil and avoids depending on
an unreleased Sigil command. The publisher uses only the ephemeral GitHub token
and GitHub OIDC: there is no long-lived signing secret. Its exact
`workflow_dispatch`/`main`/`release` identity is part of the Sigstore proof.

The release workflow produces only `NAME-VERSION.sigil-plugin.tar.zst` and
`SHA256SUMS`. A capability request is not a capability grant, and installation
is not a project evaluation lock.
The immutable release contains exactly `NAME-VERSION.sigil-plugin.tar.zst`,
`SHA256SUMS`, and `release-manifest.json`; the attestation is read through
GitHub's artifact-attestations API. Sigil's closed official provenance profile
applies only to reviewed `sigil-plugins/*` repositories. A derived third-party
repository remains third-party evidence even if it uses the same workflow. A
capability request is not a capability grant, and installation is not a
project evaluation lock.
Loading