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: 6 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,10 @@ jobs:
id: version
run: |
set -euo pipefail
WAITPRIMS_RELEASE_TAG="$GITHUB_REF_NAME" \
WAITPRIMS_REQUIRE_TAG=1 \
./scripts/release-guard-tag-version.sh
FILE_VERSION=$(tr -d ' \t\r\n' < VERSION)
TAG_VERSION=${GITHUB_REF_NAME#v}
if [ "$FILE_VERSION" != "$TAG_VERSION" ]; then
echo "VERSION mismatch: file=$FILE_VERSION tag=$TAG_VERSION"
exit 1
fi
echo "version=$FILE_VERSION" >> "$GITHUB_OUTPUT"

- name: Check version consistency
Expand Down Expand Up @@ -235,6 +233,8 @@ jobs:
| Windows x64 | `waitprims-*-windows-amd64.zip` |
| Windows arm64 | `waitprims-*-windows-arm64.zip` |

Workspace `publish` is false; three library crates opt in.
Workspace `publish` is false; four library crates opt in:
`waitprims-core`, `waitprims-async`, `waitprims-testkit`,
and `waitprims-fs`.
The diagnostic CLI is unpublished. This workflow does not
run `cargo publish`.
37 changes: 21 additions & 16 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ VERSION := $(shell tr -d ' \t\r\n' < $(VERSION_FILE) 2>/dev/null || echo dev)
CARGO = cargo

DIST_RELEASE := dist/release
# In-tree VERSION is the default tag, not the nearest older git tag.
WAITPRIMS_RELEASE_TAG ?= v$(VERSION)
# The operator-provided release key is already a canonical v-prefixed tag. Fall back to
# the in-tree VERSION when the secure release environment is not loaded.
WAITPRIMS_RELEASE_KEY ?=
WAITPRIMS_RELEASE_TAG ?= $(if $(strip $(WAITPRIMS_RELEASE_KEY)),$(strip $(WAITPRIMS_RELEASE_KEY)),v$(VERSION))
export WAITPRIMS_RELEASE_KEY
export WAITPRIMS_RELEASE_TAG

WAITPRIMS_MINISIGN_KEY ?=
Expand Down Expand Up @@ -100,6 +103,7 @@ check: fmt-check lint test ## Run quality checks
test: ## Run locked test suite
@echo "Running tests..."
$(CARGO) test --workspace --locked
./scripts/release-guard-tag-version.test.sh
@echo "[ok] Tests passed"

fmt: ## Format Rust
Expand Down Expand Up @@ -212,7 +216,7 @@ text = p.read_text(); \
text, n = re.subn(r'(?m)^version = \"[^\"]*\"', 'version = \"%s\"' % ver, text, count=1); \
if n != 1: \
raise SystemExit('failed to update [workspace.package] version'); \
text = re.sub(r'(waitprims-(?:core|async|testkit) = \{ version = )\"[^\"]*\"', r'\1\"%s\"' % ver, text); \
text = re.sub(r'(waitprims-(?:core|async|testkit|fs) = \{ version = )\"[^\"]*\"', r'\1\"%s\"' % ver, text); \
p.write_text(text); \
" "$$ver"; \
echo "[ok] Synced Cargo.toml to $$ver (python fallback)"; \
Expand All @@ -238,8 +242,10 @@ version-check: ## Validate version consistency across files
# `make release-export-keys` must not re-clean or re-download.
#
# Environment variables:
# WAITPRIMS_MINISIGN_KEY - Path to minisign secret key (required for sign)
# WAITPRIMS_MINISIGN_PUB - Path to minisign public key (optional)
# WAITPRIMS_RELEASE_KEY - Operator-provided release key (v-prefixed tag)
# WAITPRIMS_RELEASE_TAG - Explicit canonical tag override
# WAITPRIMS_MINISIGN_KEY - Approved minisign secret-key locator (required)
# WAITPRIMS_MINISIGN_PUB - Approved minisign public-key locator
# WAITPRIMS_PGP_KEY_ID - PGP key ID for GPG signing (optional)
# WAITPRIMS_GPG_HOMEDIR - Custom GPG home directory (optional)
#
Expand Down Expand Up @@ -341,7 +347,8 @@ release-clean: ## Remove dist/release contents

release-download: ## Download release assets from GitHub
@if [ -z "$(WAITPRIMS_RELEASE_TAG)" ] || [ "$(WAITPRIMS_RELEASE_TAG)" = "v" ]; then \
echo "Error: No release tag found. Set WAITPRIMS_RELEASE_TAG=vX.Y.Z"; \
echo "Error: No release tag found. Load the release environment"; \
echo "or set WAITPRIMS_RELEASE_TAG to the canonical v-prefixed tag."; \
exit 1; \
fi
./scripts/download-release-assets.sh $(WAITPRIMS_RELEASE_TAG) $(DIST_RELEASE)
Expand All @@ -363,21 +370,19 @@ release-checksums: ## Generate SHA256SUMS and SHA512SUMS
release-sign: ## Sign checksum manifests (requires WAITPRIMS_MINISIGN_KEY)
@if [ -z "$(WAITPRIMS_MINISIGN_KEY)" ]; then \
echo "Error: WAITPRIMS_MINISIGN_KEY not set"; \
echo ""; \
echo "Set the path to your minisign secret key:"; \
echo " export WAITPRIMS_MINISIGN_KEY=/path/to/signing.key"; \
echo "Load the secure release-signing environment and retry."; \
exit 1; \
fi
WAITPRIMS_MINISIGN_KEY=$(WAITPRIMS_MINISIGN_KEY) \
WAITPRIMS_PGP_KEY_ID=$(WAITPRIMS_PGP_KEY_ID) \
WAITPRIMS_GPG_HOMEDIR=$(WAITPRIMS_GPG_HOMEDIR) \
@WAITPRIMS_MINISIGN_KEY="$(WAITPRIMS_MINISIGN_KEY)" \
WAITPRIMS_PGP_KEY_ID="$(WAITPRIMS_PGP_KEY_ID)" \
WAITPRIMS_GPG_HOMEDIR="$(WAITPRIMS_GPG_HOMEDIR)" \
./scripts/sign-release-assets.sh $(WAITPRIMS_RELEASE_TAG) $(DIST_RELEASE)

release-export-keys: ## Export public signing keys
WAITPRIMS_MINISIGN_KEY=$(WAITPRIMS_MINISIGN_KEY) \
WAITPRIMS_MINISIGN_PUB=$(WAITPRIMS_MINISIGN_PUB) \
WAITPRIMS_PGP_KEY_ID=$(WAITPRIMS_PGP_KEY_ID) \
WAITPRIMS_GPG_HOMEDIR=$(WAITPRIMS_GPG_HOMEDIR) \
@WAITPRIMS_MINISIGN_KEY="$(WAITPRIMS_MINISIGN_KEY)" \
WAITPRIMS_MINISIGN_PUB="$(WAITPRIMS_MINISIGN_PUB)" \
WAITPRIMS_PGP_KEY_ID="$(WAITPRIMS_PGP_KEY_ID)" \
WAITPRIMS_GPG_HOMEDIR="$(WAITPRIMS_GPG_HOMEDIR)" \
./scripts/export-release-keys.sh $(DIST_RELEASE)

release-verify-checksums: ## Verify checksums match artifacts
Expand Down
103 changes: 68 additions & 35 deletions RELEASE_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ committed `.a`. CI never holds signing keys.

- GPG and minisign installed
- Signing keys configured (shared 3leaps release signing keys)
- `WAITPRIMS_*` environment variables set (see step 2)
- Secure release environment loaded (see section 3). Required:
`WAITPRIMS_RELEASE_KEY`, `WAITPRIMS_MINISIGN_KEY`, and
`WAITPRIMS_MINISIGN_PUB`; PGP variables are optional
- `gh` CLI authenticated with push access

## 1. Write / prep
Expand Down Expand Up @@ -89,20 +91,26 @@ One annotated `v*` tag. Do not add a path-prefixed module tag.
- [ ] Create the annotated tag:

```bash
VERSION=$(cat VERSION)
git tag -a "v${VERSION}" -m "v${VERSION}: <brief description of release>"
: "${WAITPRIMS_RELEASE_KEY:?load the release environment}"
WAITPRIMS_RELEASE_TAG="$WAITPRIMS_RELEASE_KEY" make release-guard-tag-version
git tag -a "$WAITPRIMS_RELEASE_KEY" \
-m "$WAITPRIMS_RELEASE_KEY: <brief description of release>"
```

- [ ] Push the tag (triggers the release workflow):

```bash
git push origin "v${VERSION}"
git push origin "$WAITPRIMS_RELEASE_KEY"
```

`WAITPRIMS_RELEASE_KEY` is already the canonical `vX.Y.Z` tag. Do not
copy it into a generic `VERSION` environment variable and do not prepend
another `v`.

### CI verification on the tag

- [ ] Required **CI** workflow on the tag is green
(`gh run list --branch "v${VERSION}"`)
(`gh run list --branch "$WAITPRIMS_RELEASE_KEY"`)
- [ ] The **Release** workflow drafts the GitHub release. On MSRV,
`cargo package --workspace` cannot prepare dependents until
this `VERSION` of `waitprims-core` is on crates.io. If Package
Expand Down Expand Up @@ -145,7 +153,7 @@ Workspace `publish` stays `false`. The four libraries opt in.
Still publish **after the tag**, so the registry version matches
the git tag.

### Tokens (OOB)
### Tokens

Use a crates.io token scoped to the four library crate names. Do
not reuse a Fulmen / other-org token.
Expand All @@ -156,32 +164,39 @@ not reuse a Fulmen / other-org token.
| update only | `publish-update` | later versions of crates that already exist |

No `yank` unless a separate playbook says so. Expiry 30–90 days.
Store as `CARGO_REGISTRY_TOKEN_3LEAPS` (or a `_NEW` sibling) in
the org OOB secret store — not in this repo.
Store as `CARGO_REGISTRY_TOKEN_3LEAPS` (or a `_NEW` sibling) in a
secure external secret store — not in this repo.

### Publish steps (cued)

From a clean checkout of the **tag** (not a dirty worktree):

```bash
VERSION=$(cat VERSION)
git checkout "v${VERSION}"
: "${WAITPRIMS_RELEASE_KEY:?load the release environment}"
git checkout "$WAITPRIMS_RELEASE_KEY"
release_version=$(tr -d ' \t\r\n' < VERSION)
WAITPRIMS_REQUIRE_TAG=1 make release-guard-tag-version
cargo publish --dry-run -p waitprims-core
cargo publish -p waitprims-core
cargo info --registry crates-io "waitprims-core@${VERSION}"
cargo info --registry crates-io "waitprims-core@${release_version}"
cargo publish --dry-run -p waitprims-async
cargo publish -p waitprims-async
cargo info --registry crates-io "waitprims-async@${VERSION}"
cargo info --registry crates-io "waitprims-async@${release_version}"
cargo publish --dry-run -p waitprims-testkit
cargo publish -p waitprims-testkit
cargo info --registry crates-io "waitprims-testkit@${VERSION}"
cargo info --registry crates-io "waitprims-testkit@${release_version}"
cargo info --registry crates-io waitprims-fs
# For the first waitprims-fs upload, confirm the name is still unclaimed.
cargo publish --dry-run -p waitprims-fs
cargo publish -p waitprims-fs
cargo info --registry crates-io "waitprims-fs@${VERSION}"
cargo info --registry crates-io "waitprims-fs@${release_version}"
```

Each `cargo publish` is a separate irreversible gate. Reconfirm the current
authorization immediately before every upload. A later stop or hold supersedes
an earlier cue; do not continue merely because the whole sequence was
previously authorized.

- [ ] Dry-run then publish **core**, wait for the index, then **async**,
wait for the index, then **testkit**, wait for the index, then
name-slot check and publish **fs**
Expand All @@ -191,7 +206,7 @@ cargo info --registry crates-io "waitprims-fs@${VERSION}"
- [ ] Do **not** `cargo publish -p waitprims-cli` (must fail closed:
`cannot be published`)
- [ ] Confirm each predecessor with
`cargo info --registry crates-io <crate>@${VERSION}`
`cargo info --registry crates-io <crate>@<version>`
before the next publish. Bare `cargo info` can hit the local
workspace and is not an index proof.
- [ ] If the tag Release workflow failed Package Check, re-run it
Expand Down Expand Up @@ -226,14 +241,32 @@ text becomes true only after this step.

### Set environment variables

Load the operator's secure release environment. This repository intentionally
does not prescribe host-local secret paths. From a clean worktree, fetch and
check out the exact release tag before running the strict guard. Confirm
environment presence without printing values:

```bash
export WAITPRIMS_RELEASE_TAG=v$(cat VERSION)
export WAITPRIMS_MINISIGN_KEY=/path/to/signing.key
export WAITPRIMS_MINISIGN_PUB=/path/to/signing.pub
export WAITPRIMS_PGP_KEY_ID="keyid!"
export WAITPRIMS_GPG_HOMEDIR=/path/to/gpg/homedir # optional
: "${WAITPRIMS_RELEASE_KEY:?missing approved release key}"
: "${WAITPRIMS_MINISIGN_KEY:?missing approved minisign secret key}"
: "${WAITPRIMS_MINISIGN_PUB:?missing approved minisign public key}"
test -z "$(git status --porcelain)" || {
echo "error: release signing requires a clean worktree" >&2
exit 1
}
git fetch origin \
"refs/tags/${WAITPRIMS_RELEASE_KEY}:refs/tags/${WAITPRIMS_RELEASE_KEY}"
git checkout --detach "$WAITPRIMS_RELEASE_KEY"
WAITPRIMS_REQUIRE_TAG=1 make release-guard-tag-version
```

`WAITPRIMS_RELEASE_KEY` is the canonical `vX.Y.Z` tag and is consumed directly
by the Makefile. The strict guard confirms that the tag is annotated, matches
`VERSION`, and points at `HEAD`; the signing steps therefore source per-cut
notes from the tagged tree. `WAITPRIMS_PGP_KEY_ID` and
`WAITPRIMS_GPG_HOMEDIR` are optional. Never paste environment values or
signing-command transcripts into issues, pull requests, or chat.

### Signing steps

1. **Clean previous release artifacts**
Expand Down Expand Up @@ -272,21 +305,23 @@ export WAITPRIMS_GPG_HOMEDIR=/path/to/gpg/homedir # optional
SBOM, licenses, and `release-notes-vX.Y.Z.md`. Leftover files from
an earlier cut are omitted and reported.

5. **Sign checksum manifests** (minisign + PGP)
5. **Sign checksum manifests** (minisign, plus PGP when configured)

```bash
make release-sign
```

Produces: `.minisig` and `.asc` signatures for both checksum files
Produces `.minisig` signatures for both checksum files. When
`WAITPRIMS_PGP_KEY_ID` is configured, also produces `.asc` signatures.

6. **Export public keys**

```bash
make release-export-keys
```

Produces: `waitprims-minisign.pub`, `waitprims-release-signing-key.asc`
Produces `waitprims-minisign.pub` and, when PGP is configured,
`waitprims-release-signing-key.asc`.

7. **Verify everything before upload**

Expand All @@ -312,7 +347,7 @@ export WAITPRIMS_GPG_HOMEDIR=/path/to/gpg/homedir # optional
9. **Publish the release** (promotes draft → public):

```bash
gh release edit v$(cat VERSION) --draft=false
gh release edit "$WAITPRIMS_RELEASE_KEY" --draft=false
```

The release is a draft until this step. Do not announce until after this.
Expand All @@ -327,7 +362,7 @@ Or run the full signing + upload workflow in one command:
```bash
make release
# Then manually publish the draft:
gh release edit v$(cat VERSION) --draft=false
gh release edit "$WAITPRIMS_RELEASE_KEY" --draft=false
```

## 4. Post-release verification
Expand All @@ -336,19 +371,20 @@ gh release edit v$(cat VERSION) --draft=false
- [ ] Verify checksums match: download and verify locally
- [ ] Verify signatures with public keys
- [ ] After a crates.io cue: each library crate has this VERSION
(`cargo info --registry crates-io waitprims-core@${VERSION}`,
(`cargo info --registry crates-io waitprims-core@<version>`,
same for async, testkit, and fs). Bare `cargo info` can resolve the
workspace and is not an index proof. Search is not a
version-history proof; no-backfill is policy (section 2).

### Verification example

```bash
VERSION=$(cat VERSION)
: "${WAITPRIMS_RELEASE_KEY:?load the release environment}"
release_version=${WAITPRIMS_RELEASE_KEY#v}

curl -LO "https://github.com/3leaps/waitprims/releases/download/v${VERSION}/SHA256SUMS"
curl -LO "https://github.com/3leaps/waitprims/releases/download/v${VERSION}/SHA256SUMS.minisig"
curl -LO "https://github.com/3leaps/waitprims/releases/download/v${VERSION}/waitprims-minisign.pub"
curl -LO "https://github.com/3leaps/waitprims/releases/download/${WAITPRIMS_RELEASE_KEY}/SHA256SUMS"
curl -LO "https://github.com/3leaps/waitprims/releases/download/${WAITPRIMS_RELEASE_KEY}/SHA256SUMS.minisig"
curl -LO "https://github.com/3leaps/waitprims/releases/download/${WAITPRIMS_RELEASE_KEY}/waitprims-minisign.pub"

shasum -a 256 -c SHA256SUMS --ignore-missing
minisign -Vm SHA256SUMS -p waitprims-minisign.pub
Expand Down Expand Up @@ -391,11 +427,8 @@ the release documentation updates required by the pre-tag gate.

### "WAITPRIMS_MINISIGN_KEY not set"

Set the environment variable:

```bash
export WAITPRIMS_MINISIGN_KEY=/path/to/signing.key
```
Load the operator's secure release-signing environment. Do not invent or
publish a host-local key path.

### "No release notes found"

Expand Down
Loading
Loading