diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b011396..799ccb2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 @@ -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`. diff --git a/Makefile b/Makefile index 0f89db5..383f88c 100644 --- a/Makefile +++ b/Makefile @@ -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 ?= @@ -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 @@ -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)"; \ @@ -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) # @@ -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) @@ -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 diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md index 5492d68..673b977 100644 --- a/RELEASE_CHECKLIST.md +++ b/RELEASE_CHECKLIST.md @@ -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 @@ -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}: " + : "${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: " ``` - [ ] 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 @@ -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. @@ -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** @@ -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 @${VERSION}` + `cargo info --registry crates-io @` 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 @@ -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** @@ -272,13 +305,14 @@ 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** @@ -286,7 +320,8 @@ export WAITPRIMS_GPG_HOMEDIR=/path/to/gpg/homedir # optional 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** @@ -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. @@ -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 @@ -336,7 +371,7 @@ 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@`, 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). @@ -344,11 +379,12 @@ gh release edit v$(cat VERSION) --draft=false ### 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 @@ -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" diff --git a/docs/decisions/PDR-0001-crates-io-after-tag.md b/docs/decisions/PDR-0001-crates-io-after-tag.md index 59a5083..89bd3f3 100644 --- a/docs/decisions/PDR-0001-crates-io-after-tag.md +++ b/docs/decisions/PDR-0001-crates-io-after-tag.md @@ -3,7 +3,7 @@ id: "PDR-0001" title: "crates.io after the git tag, before the GitHub Release is green" status: "accepted" date: "2026-08-18" -last_updated: "2026-08-18" +last_updated: "2026-09-01" deciders: - "@3leapsdave" scope: "waitprims release process" @@ -25,10 +25,10 @@ relates-to: ## Context -waitprims is a library-first workspace. Three crates opt in to -crates.io (`waitprims-core`, `waitprims-async`, `waitprims-testkit`). -The diagnostic CLI stays unpublished. Dependents use `version` + -`path` so `cargo publish` rewrites them to registry versions. +waitprims is a library-first workspace. Four crates opt in to +crates.io (`waitprims-core`, `waitprims-async`, `waitprims-testkit`, +`waitprims-fs`). The diagnostic CLI stays unpublished. Dependents use +`version` + `path` so `cargo publish` rewrites them to registry versions. MSRV is 1.88. On that Cargo, `cargo package --workspace` looks up path dependents on crates.io when preparing the next crate. It @@ -37,7 +37,7 @@ So the tag Release workflow's Package Check fails for a **new** version until `waitprims-core@VERSION` exists on the index. v0.1.3 hit that: the git tag CI was green; Release Package Check -failed; there was no draft to sign until the three libraries were +failed; there was no draft to sign until the libraries were published and the workflow re-run. The checklist used to put crates.io **after** MFA sign and undraft. @@ -46,7 +46,7 @@ That order cannot produce a signable GitHub draft on first upload ## Decision -1. **Order.** `cargo publish` the three library crates **after** +1. **Order.** `cargo publish` the four library crates **after** the annotated git tag is on `origin`, **before** treating the GitHub Release workflow as green. Then MFA-sign and undraft. Later cuts may see Package Check pass without a new publish @@ -57,7 +57,7 @@ That order cannot produce a signable GitHub draft on first upload cut that enabled `publish = true`. Older git tags stay off crates.io. -3. **Tokens (OOB).** Use the crates.io account that will own the +3. **Tokens.** Use the crates.io account that will own the 3leaps crates. Do not reuse another org's token. | Token role | Scopes | Use | @@ -65,11 +65,12 @@ That order cannot produce a signable GitHub draft on first upload | new + update | `publish-new`, `publish-update` | first upload of a crate name | | update only | `publish-update` | later versions of existing crates | - Restrict both tokens to the three library crate names (add other + Restrict both tokens to the four library crate names (add other 3leaps crate names when those cuts are scheduled). No crate-name wildcard exists. No `yank` unless a separate playbook says so. - Expiry 30–90 days. Store OOB as `CARGO_REGISTRY_TOKEN_3LEAPS` - (and a `_NEW` sibling if you keep both). Never commit a token. + Expiry 30–90 days. Store as `CARGO_REGISTRY_TOKEN_3LEAPS` + (and a `_NEW` sibling if you keep both) in a secure external + secret store. Never commit a token. 4. **Index proof.** After each upload, wait on `cargo info --registry crates-io @VERSION`. Bare @@ -91,6 +92,7 @@ This is a **PDR** (process), not an ADR. Record types follow ## Revision History -| Date | Status Change | Summary | Updated By | -| ---------- | ------------- | -------------------------------------------- | ---------- | -| 2026-08-18 | → accepted | Tag, then crates.io, then GitHub sign/undraft | echo-devlead | +| Date | Status Change | Summary | Updated By | +| ---------- | ------------- | --------------------------------------------------- | ------------ | +| 2026-08-18 | → accepted | Tag, then crates.io, then GitHub sign/undraft | devlead | +| 2026-09-01 | accepted | Add `waitprims-fs` as the fourth published library | devlead | diff --git a/scripts/check-version.sh b/scripts/check-version.sh index b8c7cdb..b1942bb 100755 --- a/scripts/check-version.sh +++ b/scripts/check-version.sh @@ -149,7 +149,7 @@ while IFS= read -r line; do error " $line" DEP_MISMATCH=1 fi -done < <(grep -E '^waitprims-(core|async|testkit) = \{ version = "' "$CARGO_TOML" || true) +done < <(grep -E '^waitprims-(core|async|testkit|fs) = \{ version = "' "$CARGO_TOML" || true) if [[ "$DEP_MISMATCH" -ne 0 ]]; then error "Run 'make version-sync' to sync path-dependency versions" diff --git a/scripts/export-release-keys.sh b/scripts/export-release-keys.sh index 47b219a..8283260 100755 --- a/scripts/export-release-keys.sh +++ b/scripts/export-release-keys.sh @@ -33,7 +33,7 @@ if [ -n "$MINISIGN_PUB" ] && [ -f "$MINISIGN_PUB" ]; then cat "$DIR/waitprims-minisign.pub" else echo "[!!] Minisign public key not found" - echo "Set WAITPRIMS_MINISIGN_PUB or ensure .pub file exists alongside .key" + echo "Load the secure release-signing environment and retry" fi if [ -n "${WAITPRIMS_PGP_KEY_ID:-}" ]; then diff --git a/scripts/generate-checksums.sh b/scripts/generate-checksums.sh index 20e3186..8e95b45 100755 --- a/scripts/generate-checksums.sh +++ b/scripts/generate-checksums.sh @@ -8,7 +8,7 @@ set -euo pipefail DIR=${1:-dist/release} -TAG=${2:-${WAITPRIMS_RELEASE_TAG:-}} +TAG=${2:-${WAITPRIMS_RELEASE_TAG:-${WAITPRIMS_RELEASE_KEY:-}}} if [ ! -d "$DIR" ]; then echo "Error: Directory $DIR does not exist" @@ -16,11 +16,11 @@ if [ ! -d "$DIR" ]; then fi if [ -z "$TAG" ] || [ "$TAG" = "v" ]; then - echo "Error: No release tag. Pass the tag or set WAITPRIMS_RELEASE_TAG=vX.Y.Z" + echo "Error: No release tag. Pass the tag or load the release environment" exit 1 fi -VERSION="${TAG#v}" +RELEASE_VERSION="${TAG#v}" NOTES="release-notes-${TAG}.md" cd "$DIR" @@ -37,9 +37,9 @@ echo "Generating checksums in $DIR for $TAG..." CHECKSUM_FILES=() for f in LICENSE-* \ "$NOTES" \ - "sbom-${VERSION}.cdx.json" \ - "waitprims-${VERSION}-"*.tar.gz \ - "waitprims-${VERSION}-"*.zip; do + "sbom-${RELEASE_VERSION}.cdx.json" \ + "waitprims-${RELEASE_VERSION}-"*.tar.gz \ + "waitprims-${RELEASE_VERSION}-"*.zip; do if [ -f "$f" ]; then CHECKSUM_FILES+=("$f") fi diff --git a/scripts/release-guard-tag-version.sh b/scripts/release-guard-tag-version.sh index 4dae6da..e1fcfda 100755 --- a/scripts/release-guard-tag-version.sh +++ b/scripts/release-guard-tag-version.sh @@ -4,6 +4,7 @@ # Use in CI to ensure version consistency, or before signing locally. # # Environment variables: +# WAITPRIMS_RELEASE_KEY - Operator-provided release key (v-prefixed tag) # WAITPRIMS_RELEASE_TAG - Override tag to check # WAITPRIMS_REQUIRE_TAG - Set to 1 to fail if no tag found (for CI) @@ -26,6 +27,10 @@ detect_tag() { printf '%s' "${WAITPRIMS_RELEASE_TAG}" return 0 fi + if [ -n "${WAITPRIMS_RELEASE_KEY:-}" ]; then + printf '%s' "${WAITPRIMS_RELEASE_KEY}" + return 0 + fi if [ -n "${RELEASE_TAG:-}" ]; then printf '%s' "${RELEASE_TAG}" return 0 @@ -50,13 +55,21 @@ main() { version="$(read_version)" local expected="v${version}" + if [ -n "${WAITPRIMS_RELEASE_TAG:-}" ] && + [ -n "${WAITPRIMS_RELEASE_KEY:-}" ] && + [ "${WAITPRIMS_RELEASE_TAG}" != "${WAITPRIMS_RELEASE_KEY}" ]; then + echo "error: conflicting release tag inputs" >&2 + echo " WAITPRIMS_RELEASE_TAG and WAITPRIMS_RELEASE_KEY differ" >&2 + exit 1 + fi + local tag tag="$(detect_tag)" if [ -z "$tag" ]; then local require_tag="${WAITPRIMS_REQUIRE_TAG:-}" if [ "${require_tag}" = "1" ]; then - echo "error: no exact tag found for HEAD and no WAITPRIMS_RELEASE_TAG provided" >&2 + echo "error: no exact tag found and no explicit release key/tag provided" >&2 exit 1 fi echo "[--] release guard: no tag detected (set WAITPRIMS_REQUIRE_TAG=1 to enforce in CI)" @@ -70,6 +83,18 @@ main() { exit 1 fi + if [ "${WAITPRIMS_REQUIRE_TAG:-}" = "1" ]; then + if ! git tag --points-at HEAD --format='%(refname:short)' | + grep -Fqx "$tag"; then + echo "error: required release tag is not on HEAD: $tag" >&2 + exit 1 + fi + if [ "$(git cat-file -t "refs/tags/$tag" 2>/dev/null || true)" != "tag" ]; then + echo "error: required release tag is not annotated: $tag" >&2 + exit 1 + fi + fi + echo "[ok] release guard: tag matches VERSION ($tag)" } diff --git a/scripts/release-guard-tag-version.test.sh b/scripts/release-guard-tag-version.test.sh new file mode 100755 index 0000000..e6af1f0 --- /dev/null +++ b/scripts/release-guard-tag-version.test.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GUARD="$SCRIPT_DIR/release-guard-tag-version.sh" +TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/waitprims-release-guard.XXXXXX")" +trap 'rm -rf "$TEST_ROOT"' EXIT + +cp "$GUARD" "$TEST_ROOT/release-guard-tag-version.sh" +chmod +x "$TEST_ROOT/release-guard-tag-version.sh" +printf '1.2.3\n' >"$TEST_ROOT/VERSION" + +git -C "$TEST_ROOT" init -q +git -C "$TEST_ROOT" config user.name "waitprims release guard test" +git -C "$TEST_ROOT" config user.email "noreply@example.invalid" +git -C "$TEST_ROOT" add VERSION release-guard-tag-version.sh +git -C "$TEST_ROOT" commit -qm "test fixture" +git -C "$TEST_ROOT" tag -a v1.2.3 -m "test fixture" + +run_guard() { + ( + cd "$TEST_ROOT" + env -u WAITPRIMS_RELEASE_KEY \ + -u WAITPRIMS_RELEASE_TAG \ + -u WAITPRIMS_REQUIRE_TAG \ + -u RELEASE_TAG \ + "$@" + ) +} + +expect_fail() { + if "$@" >/dev/null 2>&1; then + echo "expected command to fail: $*" >&2 + exit 1 + fi +} + +run_guard "$TEST_ROOT/release-guard-tag-version.sh" >/dev/null +run_guard WAITPRIMS_RELEASE_KEY=v1.2.3 \ + "$TEST_ROOT/release-guard-tag-version.sh" >/dev/null +run_guard WAITPRIMS_RELEASE_KEY=v1.2.3 WAITPRIMS_REQUIRE_TAG=1 \ + "$TEST_ROOT/release-guard-tag-version.sh" >/dev/null +run_guard VERSION=v9.9.9 WAITPRIMS_RELEASE_KEY=v1.2.3 \ + "$TEST_ROOT/release-guard-tag-version.sh" >/dev/null + +expect_fail run_guard WAITPRIMS_RELEASE_KEY=1.2.3 \ + "$TEST_ROOT/release-guard-tag-version.sh" +expect_fail run_guard WAITPRIMS_RELEASE_KEY=vv1.2.3 \ + "$TEST_ROOT/release-guard-tag-version.sh" +expect_fail run_guard WAITPRIMS_RELEASE_KEY=v1.2.3 \ + WAITPRIMS_RELEASE_TAG=vv1.2.3 \ + "$TEST_ROOT/release-guard-tag-version.sh" + +printf 'untagged\n' >"$TEST_ROOT/marker" +git -C "$TEST_ROOT" add marker +git -C "$TEST_ROOT" commit -qm "untagged fixture" +run_guard "$TEST_ROOT/release-guard-tag-version.sh" >/dev/null +expect_fail run_guard WAITPRIMS_RELEASE_KEY=v1.2.3 \ + WAITPRIMS_REQUIRE_TAG=1 \ + "$TEST_ROOT/release-guard-tag-version.sh" + +printf '1.2.4\n' >"$TEST_ROOT/VERSION" +git -C "$TEST_ROOT" add VERSION +git -C "$TEST_ROOT" commit -qm "lightweight tag fixture" +git -C "$TEST_ROOT" tag v1.2.4 +expect_fail run_guard WAITPRIMS_RELEASE_KEY=v1.2.4 \ + WAITPRIMS_REQUIRE_TAG=1 \ + "$TEST_ROOT/release-guard-tag-version.sh" + +echo "[ok] release tag/version guard tests passed" diff --git a/scripts/sign-release-assets.sh b/scripts/sign-release-assets.sh index 9282df6..0b06172 100755 --- a/scripts/sign-release-assets.sh +++ b/scripts/sign-release-assets.sh @@ -20,14 +20,12 @@ fi if [ -z "${WAITPRIMS_MINISIGN_KEY:-}" ]; then echo "Error: WAITPRIMS_MINISIGN_KEY environment variable not set" - echo "" - echo "Set to path of 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 if [ ! -f "$WAITPRIMS_MINISIGN_KEY" ]; then - echo "Error: Minisign key not found: $WAITPRIMS_MINISIGN_KEY" + echo "Error: Configured minisign key is not a readable file" exit 1 fi