From a368051c49c68c1027f52e06825e8e4317204dbf Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 27 Jun 2026 02:25:03 -0700 Subject: [PATCH 01/15] docs(release): adapt lifecycle runbook --- .continuum/release.yml | 120 ++++++++++++++ .github/workflows/release-crates.yml | 2 +- CHANGELOG.md | 7 + docs/CRATES_IO_RELEASE.md | 6 +- docs/governance/RELEASE_CHECKLIST.md | 8 +- docs/governance/RELEASE_POLICY.md | 16 ++ docs/method/release-runbook.md | 27 +++- docs/method/release.md | 233 +++++++++++++++++++++++++-- docs/topics/README.md | 2 +- docs/topics/releases.md | 16 ++ docs/truth-manifest.json | 15 ++ test/ci-workflows.bats | 7 + test/release-governance.bats | 24 +++ 13 files changed, 464 insertions(+), 19 deletions(-) create mode 100644 .continuum/release.yml diff --git a/.continuum/release.yml b/.continuum/release.yml new file mode 100644 index 00000000..1c88bdc6 --- /dev/null +++ b/.continuum/release.yml @@ -0,0 +1,120 @@ +schema: 1 +repo: + name: wesley + owner: flyingrobots + primary_artifact: rust-crates + public_binary: wesley + product_boundary: domain-free-graphql-to-ir + +versioning: + strategy: semver + tag_format: 'v{version}' + release_branch_format: 'release/v{version}' + release_milestone_format: 'Release: v{version}' + release_lane_label_format: 'v{version}' + goalpost_milestone_format: 'Goalpost: {name}' + +version_sources: + - path: crates/wesley-core/Cargo.toml + name: wesley-core + type: cargo-manifest + field: package.version + required: true + published: true + - path: crates/wesley-emit-codec/Cargo.toml + name: wesley-emit-codec + type: cargo-manifest + field: package.version + required: true + published: true + - path: crates/wesley-emit-rust/Cargo.toml + name: wesley-emit-rust + type: cargo-manifest + field: package.version + required: true + published: true + - path: crates/wesley-emit-typescript/Cargo.toml + name: wesley-emit-typescript + type: cargo-manifest + field: package.version + required: true + published: true + - path: crates/wesley-cli/Cargo.toml + name: wesley-cli + type: cargo-manifest + field: package.version + required: true + published: true + - path: package.json + name: root-private-package + type: json + field: version + required: true + published: false + +docs: + changelog: CHANGELOG.md + front_door: + - README.md + - docs/README.md + - docs/GUIDE.md + - docs/ENTRYPOINTS.md + architecture: + - docs/ARCHITECTURE.md + - docs/TECHNICAL_TEARDOWN.md + user_docs: + - docs/topics/ + - docs/reference/ + - docs/releases/ + operator_docs: + - docs/method/release.md + - docs/method/release-runbook.md + - docs/CRATES_IO_RELEASE.md + - docs/governance/RELEASE_POLICY.md + - docs/governance/RELEASE_CHECKLIST.md + contributor_docs: + - AGENTS.md + - CONTRIBUTING.md + - docs/METHOD.md + - docs/topics/contributing/triage.md + +validation: + docs_check: cargo xtask docs-check + prep: cargo xtask release-prep-guard --version {version} + preflight: cargo xtask preflight + release_check: cargo xtask release-check + package: cargo xtask package-crates --version {version} + tagged_guard: cargo xtask release-guard --tag v{version} + legacy_when_needed: cargo xtask legacy-preflight + +workflows: + publish: .github/workflows/release-crates.yml + publish_trigger: tag-push + autotag: none + +publish: + manual_dispatch_required: false + github_release: true + source_ref: 'v{version}' + registries: + - name: crates.io + packages: + - wesley-core + - wesley-emit-codec + - wesley-emit-rust + - wesley-emit-typescript + - wesley-cli + verify: cargo info {package}@{version} + +issue_model: + live_tracker: github + implementation_bucket: 'Goalpost: ... milestone' + release_gate_bucket: 'Release: v{version} milestone' + release_scheduling_axis: 'v{version} label' + triage_axis: 'triage:* label' + required_state_label: 'exactly one of triage:* or v{version}' + +release_evidence: + internal_packet: docs/method/releases/v{version}/release.md + verification: docs/method/releases/v{version}/verification.md + user_notes: docs/releases/v{version}.md diff --git a/.github/workflows/release-crates.yml b/.github/workflows/release-crates.yml index 066a7ac3..24758abc 100644 --- a/.github/workflows/release-crates.yml +++ b/.github/workflows/release-crates.yml @@ -223,7 +223,7 @@ jobs: run: | set -euo pipefail version="${GITHUB_REF_NAME#v}" - for crate in wesley-core wesley-emit-rust wesley-emit-typescript wesley-cli; do + for crate in wesley-core wesley-emit-codec wesley-emit-rust wesley-emit-typescript wesley-cli; do cargo info "${crate}@${version}" >/dev/null done diff --git a/CHANGELOG.md b/CHANGELOG.md index 6572f6cb..e9e609e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,10 @@ hash`, and `schema operations`. ### Changed +- **Release lifecycle profile**: Added a repo-local `.continuum/release.yml` + release profile and expanded the Wesley release doctrine/runbook around + thesis, scope, goalposts, immutable tagged-main publication, verification, + and retrospective evidence. - **Release documentation gate**: The release runbook, release policy, and human sign-off checklist now require a `docs/topics/` accuracy and coverage audit before tagging, with minimum 90% accuracy and 90% coverage floors. @@ -52,6 +56,9 @@ changed-schemas`, runs schema-scoped matrix jobs, and keeps per-schema report ### Fixed +- **Release crate visibility check**: The tag-triggered Release Crates workflow + now verifies crates.io visibility for `wesley-emit-codec` along with the rest + of the published Rust crate set before finalizing the GitHub Release. - **Docs CLI checker determinism**: The docs command checker now reads the native command list from the Rust CLI source help text instead of invoking `cargo run`, so Node-only repository hygiene does not depend on Cargo diff --git a/docs/CRATES_IO_RELEASE.md b/docs/CRATES_IO_RELEASE.md index 25963d12..58fe4735 100644 --- a/docs/CRATES_IO_RELEASE.md +++ b/docs/CRATES_IO_RELEASE.md @@ -1,12 +1,16 @@ # CRATES.IO RELEASE - + This is Wesley's official Rust release procedure. The distribution target is crates.io. The release authority is GitHub Actions. Humans prepare commits and tags; GitHub Actions performs the publish. +The repo-local release profile is [`.continuum/release.yml`](../.continuum/release.yml). +It declares the publish crate set, version sources, signposts, and verification +commands that this procedure must match. + ## Non-Negotiable Policy 1. Releases must only be performed by GitHub Actions. diff --git a/docs/governance/RELEASE_CHECKLIST.md b/docs/governance/RELEASE_CHECKLIST.md index b009a36f..4b551d91 100644 --- a/docs/governance/RELEASE_CHECKLIST.md +++ b/docs/governance/RELEASE_CHECKLIST.md @@ -10,7 +10,7 @@ Automated checks are not listed here — those run inside creating a tag, run `cargo xtask release-check` locally; it runs the same strict preflight gate used by `release-guard`, then builds and packages the native release artifacts without publishing anything. This checklist covers -checks 7, 10, 13, 18, 22, and 23 from the enforcement matrix, which require +checks 7, 10, 13, 18, 22, 23, and 24 from the enforcement matrix, which require human judgment. See [`RELEASE_POLICY.md`](RELEASE_POLICY.md) for the full enforcement matrix @@ -48,6 +48,12 @@ and rationale. workflows are met. Any stale topic claim, obsolete instruction, missing topic, or missing authoritative link was corrected before tagging. +- [ ] **Release thesis and scope are honest** + I confirmed the release packet records the thesis, must-ship work, + may-slip work, explicitly-not-included work, selected goalposts, acceptance + evidence, and retrospective/evidence location. Any planned work that did + not ship was moved, cut, or acknowledged before tagging. + - [ ] **No known issues being silently shipped** I reviewed the open GitHub Issues for known defects or outstanding decisions that affect this release's correctness or safety, whether or not they are diff --git a/docs/governance/RELEASE_POLICY.md b/docs/governance/RELEASE_POLICY.md index 7f3af310..9438a9ee 100644 --- a/docs/governance/RELEASE_POLICY.md +++ b/docs/governance/RELEASE_POLICY.md @@ -6,6 +6,12 @@ This document is the canonical gate policy for Wesley releases. Every release must clear all automated checks and all human sign-offs before a tag is considered valid and the publish workflow is permitted to run. +The release doctrine lives in [`docs/method/release.md`](../method/release.md). +The repo-local release profile lives in +[`../../.continuum/release.yml`](../../.continuum/release.yml) and declares the +version sources, publish crate set, signposts, workflows, and verification +commands this policy protects. + ## Enforcement Release gates are split between automated machinery and human review. Neither @@ -50,6 +56,7 @@ lacks the human sign-off is not a valid release, and vice versa. | 21 | `cargo doc --workspace` builds with zero warnings | cargo doc | | | 22 | No known issues silently shipped | | reviewer | | 23 | `docs/topics/` accuracy and coverage gate | | reviewer | +| 24 | Release thesis, scope, and retrospective path exist | | reviewer | ## Automated Checks — Details @@ -226,6 +233,15 @@ enough before tagging. Post-publish facts may live in the GitHub Release, workflow logs, and crates.io registry; they should not require a manual post-release merge to make the released commit truthful. +### Check 24: Release Thesis, Scope, And Retrospective Path + +A human reviewer must confirm planned releases have a current release thesis, +must-ship/may-slip/not-included scope, two to five goalposts with acceptance +evidence, and an explicit retrospective/evidence location under +`docs/method/releases/vX.Y.Z/`. Patch and emergency releases may use a shorter +thesis, but they still need a recorded reason, validation evidence, +post-publication verification, and fallout issue path. + ## Policy Violations If a release is discovered to have shipped in violation of this policy: diff --git a/docs/method/release-runbook.md b/docs/method/release-runbook.md index 421b4fcc..4391a4ea 100644 --- a/docs/method/release-runbook.md +++ b/docs/method/release-runbook.md @@ -1,11 +1,17 @@ # Release Runbook + + Use this runbook when a release has already been shaped in `docs/method/releases/vX.Y.Z/release.md` and is ready for pre-flight. This is intentionally the execution layer, not the doctrine layer. The release doctrine lives in `docs/method/release.md`. +The repo-local release profile lives in [`.continuum/release.yml`](../../.continuum/release.yml). +Use it as the source for version sources, publish crates, signposts, workflow +names, and verification commands. + ## Abort Conditions - Never guess. Never claim success for anything you did not directly verify. @@ -26,6 +32,7 @@ Before changing anything, determine and record: - package manager and lockfile authority - all version-bearing manifests - all publishable units +- release profile presence and obvious agreement with the current repo - latest reachable semver tag matching `v*` - current branch - exact sync state versus `origin/main` @@ -114,9 +121,26 @@ CI state. 8. Run `cargo xtask release-guard --tag vX.Y.Z` after the tag exists locally. 9. Push the exact release tag only, for example: `git push origin vX.Y.Z`. 10. Create the GitHub Release or equivalent forge release using the versioned - release notes. + release notes. 11. Monitor triggered workflows to completion. 12. Verify registries directly before claiming publication succeeded. +13. Record release evidence and retrospective before starting the next planned + release train. + +## Phase 5: Retrospective And Closeout + +After the tag workflow publishes and public verification passes: + +1. Update `docs/method/releases/vX.Y.Z/verification.md` with tag, commit, + workflow, GitHub Release, registry, smoke, and warning evidence. +2. Record plan-versus-actual scope: shipped, slipped, cut, expanded, and why. +3. Identify repeatable wins and concrete process improvements. +4. File fallout issues with a definition of done and exactly one scheduling + state label. +5. Close or move every scoped issue. +6. Close the release milestone after release-gate work is complete. +7. Write or refresh the next release thesis before making the next planned + train active. ## Evidence @@ -131,3 +155,4 @@ minimum include: - registry URLs - `docs/topics/` accuracy and coverage scores, with links to any corrections - any non-blocking warnings +- retrospective summary and fallout issue links diff --git a/docs/method/release.md b/docs/method/release.md index e307795a..63726ac6 100644 --- a/docs/method/release.md +++ b/docs/method/release.md @@ -1,12 +1,163 @@ # Release + + Releases happen when externally meaningful behavior changes. +Wesley follows the Continuum release spine, adapted for this repository's +actual shape: a domain-free Rust compiler/toolchain that publishes crates from +signed tags on synced `main`. A release is not a version bump. A release is a +promise made visible. + +The repo-local mechanics live in [`.continuum/release.yml`](../../.continuum/release.yml). +This doctrine defines the standard; the profile defines the boring facts that +automation and reviewers should enforce. + +## Doctrine + +A valid Wesley release has all of the following: + +1. **A reason**: planned releases have a release thesis before implementation + scope becomes active. +2. **A bucket**: Wesley uses GitHub for live work state. Implementation issues + stay in `Goalpost: ...` milestones; release-gate issues stay in + `Release: vX.Y.Z` milestones; concrete `vX.Y.Z` labels are the version + scheduling axis because GitHub issues can carry only one milestone. +3. **Honest scope**: must-ship, may-slip, and explicitly-not-included work is + recorded before release prep. +4. **A reviewed source commit**: the release tag points at the exact `main` + commit that passed release prep. +5. **An immutable public tag**: signed public tags are not moved. Bad releases + are fixed by patching forward. +6. **Synchronized metadata**: every version source declared in the release + profile agrees. +7. **Updated signposts**: changelog, README, guide, architecture, topics, + release notes, operator docs, and maintainer docs change when their truth + changes. +8. **Executable gates**: release law lives in `cargo xtask` checks and GitHub + Actions, not only in prose. +9. **Publication from the tag**: crates, GitHub Release notes, and registry + evidence are produced from the tagged source. +10. **Post-publication verification**: a release is done when consumers can see + and use the crates, not when the upload command returns. +11. **Evidence**: tag, commit, workflow, artifact, verification, and + retrospective evidence remain inspectable. +12. **Learning**: planned releases end with a retrospective and fallout issues. + +## Lifecycle + +Wesley releases move through this lifecycle: + +```text +planned + -> active + -> release-prep + -> merged + -> tagged + -> published + -> verified + -> retrospectived + -> closed +``` + +### planned + +A release is planned when a `Release: vX.Y.Z` milestone exists, a release-gate +issue exists, the release thesis exists, must-ship/may-slip/not-included scope +is recorded, two to five goalposts are named, and acceptance evidence is clear. + +### active + +A release is active when it is the current version train, at least one selected +goalpost issue is in progress, priority labels reflect the real queue, and +exactly one active slice or tracking issue is marked as active for the train. + +### release-prep + +A release enters prep when implementation scope is reconciled against the +previous public tag, slipped work is moved or cut, version metadata is updated, +release signposts are updated, local release-prep validation passes, and a +`release/vX.Y.Z` branch exists. + +### merged + +A release is merged when the release-prep PR has approval or explicit maintainer +admin authorization, CI is green, release-prep validation has passed, and the +release branch has landed on `main`. The merge commit is the candidate release +commit. + +### tagged + +A release is tagged when final preflight passes from synced `main`, the expected +tag does not already exist, and an annotated signed tag is created at the exact +candidate release commit. + +```bash +git tag -s vX.Y.Z -m "release: vX.Y.Z" +``` + +### published + +A release is published when `.github/workflows/release-crates.yml` checks out +the tag, verifies tag/metadata/main reachability, builds artifacts from the tag, +publishes every configured crate, and creates/finalizes the GitHub Release. + +### verified + +A release is verified when public crates.io visibility is confirmed for every +published crate, the installed `wesley` CLI launches, the GitHub Release is +visible, and release evidence is captured. + +### retrospectived + +A release is retrospectived when released work, unreleased work, +plan-versus-actual scope, repeatable wins, concrete improvements, fallout +issues, and the next release recommendation are recorded. + +### closed + +A release is closed when scoped work is closed, moved, or explicitly cut; +fallout issues are triaged; the release milestone is closed; the next thesis +exists; and the next active slice is selected. + +## Release Types + +- **Planned release**: normal minor, major, and meaningful patch trains. Requires + thesis, scoped issues, goalposts, release-prep PR, full validation, + publication evidence, and retrospective. +- **Patch release**: compatible bug fixes, packaging fixes, docs corrections + tied to current behavior, and narrow operator workflow improvements. Requires + a short patch thesis, changelog entry, validation, publication evidence, and a + lightweight retrospective. +- **Emergency or security release**: urgent production, security, data-loss, + broken package, or severe operator-impacting fixes. Planning may be + abbreviated, but immutable tags, proportional validation, verification, + retrospective, and fallout issues remain mandatory. +- **Prerelease**: alpha, beta, preview, or release-candidate crates. Prereleases + must not be treated as stable signposts unless promoted through a release + decision. +- **Docs-only release**: allowed only when the public release surface includes + docs or release notes consumers rely on. Runtime behavior changes must be + explicitly marked as absent. + +## Version Selection + +Wesley uses SemVer. + +- **Patch**: compatible fixes, dependency updates without public behavior + change, documentation corrections that describe existing behavior, and narrow + release-tooling fixes. +- **Minor**: new compatible CLI commands, public workflows, emitter behavior, + configuration options, or additive APIs. +- **Major**: incompatible CLI flag/default/output changes, public API removal, + package entrypoint changes, storage/IR schema changes consumers must handle, + or support-boundary removals. +- **Prerelease**: early artifacts without stable guarantees. + ## Shaped Release -A shaped release is not just a tag plus a changelog edit. It is a deliberate -packet that says what is shipping, why this version number is correct, which -users benefit, and what they need to do next. +A shaped release is a deliberate packet that says what is shipping, why this +version number is correct, which users benefit, and what they need to do next. Required release artifacts: @@ -28,19 +179,53 @@ Required release artifacts: `CHANGELOG.md` remains the ledger. The user-facing guided release surface lives in `docs/releases/`. -## Scope +## Scope Model -Releases aggregate shipped work. They do not create version-scoped filesystem -backlog directories, and implementation issues stay in goalpost milestones. -Release membership is tracked with concrete release lane labels such as -`v0.2.0`; release milestones hold release-gate issues. +Every planned release records three scope buckets: + +- **Must-ship**: work that defines the release. +- **May-slip**: valuable work that may move without invalidating the thesis. +- **Explicitly not included**: work people might assume is included but is not. The release design names and justifies the intended version before tagging. Commit history, diff inspection, and validation can support or challenge that judgment during pre-flight, but they do not silently own the decision by themselves. -## Default +## Goalposts And Evidence + +A planned release should have two to five goalposts. Each goalpost names an +outcome, issue set, and observable acceptance evidence. Good evidence includes +command output, test results, workflow runs, registry lookups, documentation +links, smoke tests, closed issues, and merged PRs. + +## Signposts + +Update every signpost whose truth changed since the previous public tag: + +- `CHANGELOG.md` +- `README.md` +- `docs/README.md` +- `docs/GUIDE.md` +- `docs/ENTRYPOINTS.md` +- `docs/ARCHITECTURE.md` +- `docs/TECHNICAL_TEARDOWN.md` +- `docs/topics/` +- `docs/releases/vX.Y.Z.md` +- `docs/method/releases/vX.Y.Z/` +- `docs/method/release.md` +- `docs/method/release-runbook.md` +- `docs/CRATES_IO_RELEASE.md` +- `docs/governance/RELEASE_POLICY.md` +- `docs/governance/RELEASE_CHECKLIST.md` +- `docs/METHOD.md`, `CONTRIBUTING.md`, and `AGENTS.md` when process truth + changes + +The `docs/topics/` audit is mandatory before tagging. It must reach at least +90% accuracy and 90% coverage for release-relevant contributor and operator +workflows. + +## Defaults - Not every cycle is a release. - Every cycle still updates the living docs honestly. @@ -51,11 +236,17 @@ themselves. ## Sequence 1. Shape the release in `docs/method/releases/vX.Y.Z/release.md`. -2. Accept the release scope and version justification before tagging. -3. Draft the user-facing release notes in `docs/releases/vX.Y.Z.md`. -4. Run the sequential pre-flight in `docs/method/release-runbook.md`. -5. Tag, publish, and verify delivery directly. -6. Ship sync repo-level surfaces that the release changed. +2. Confirm the release profile still matches the repo. +3. Accept the release thesis, scope, goalposts, and version justification. +4. Draft the user-facing release notes in `docs/releases/vX.Y.Z.md`. +5. Reconcile scope against the previous public tag. +6. Run the sequential pre-flight in `docs/method/release-runbook.md`. +7. Land the release-prep PR on `main`. +8. Create the signed tag on synced `main`. +9. Publish from the tag. +10. Verify delivery directly. +11. Record the release witness and retrospective. +12. Close the release and plan the next thesis. ## User-Facing Release Notes @@ -76,3 +267,17 @@ If no migration is required, say `No migration required.` explicitly. The doctrine lives here. The command-by-command, abort-fast release procedure lives in `docs/method/release-runbook.md` so it can become more explicit or automated later without bloating the core doctrine. + +## Non-Negotiables + +```text +No planned release without a thesis. +No release-prep PR without scope reconciliation. +No tag that does not point at the reviewed synced main commit. +No moving public tags. +No publishing from untagged moving source. +No silent version/profile mismatch. +No release without post-publication verification. +No planned release train after publication without a retrospective. +No domain-specific behavior in Wesley core release scope. +``` diff --git a/docs/topics/README.md b/docs/topics/README.md index 1f607434..fbdfd04a 100644 --- a/docs/topics/README.md +++ b/docs/topics/README.md @@ -40,7 +40,7 @@ short path to the authoritative surface. | Interpret GitHub Actions checks. | [CI Workflows](./ci-workflows.md) | `docs/ci.md`, `.github/workflows/` | | Understand HOLMES CI and PR comments. | [HOLMES CI](./holmes-ci.md) | `.github/workflows/wesley-holmes.yml`, `docs/architecture/` | | Work with assurance evidence and policies. | [Assurance Evidence](./assurance-evidence.md) | `docs/holmes-policy-spec.md` | -| Prepare or judge a release. | [Releases](./releases.md) | `docs/method/release-runbook.md`, `docs/governance/` | +| Prepare or judge a release. | [Releases](./releases.md) | `.continuum/release.yml`, `docs/method/release.md`, `docs/governance/` | | Refresh docs before a release tag. | [Docs Maintenance](./docs-maintenance.md) | `docs/governance/RELEASE_POLICY.md#check-23-docstopics-accuracy-and-coverage-gate` | ### Contributor Process diff --git a/docs/topics/releases.md b/docs/topics/releases.md index 8a954933..297f5d4c 100644 --- a/docs/topics/releases.md +++ b/docs/topics/releases.md @@ -17,6 +17,7 @@ Release state is split intentionally: | GitHub milestone `Release: vX.Y.Z` | Release-gate issue only | | GitHub goalpost milestones | Implementation issues | | `CHANGELOG.md` | Historical ledger of merged behavior | +| `.continuum/release.yml` | Repo-local release profile and publish facts | | `docs/method/releases/vX.Y.Z/release.md` | Internal release design and scope | | `docs/method/releases/vX.Y.Z/verification.md` | Release witness after validation and publish | | `docs/releases/vX.Y.Z.md` | User-facing release notes | @@ -40,6 +41,20 @@ That includes: The `docs/topics/` gate means this directory must cover release-relevant contributor and operator workflows or link clearly to the current authority. +## Release Lifecycle + +Wesley uses the lifecycle defined in +[`docs/method/release.md`](../method/release.md): + +```text +planned -> active -> release-prep -> merged -> tagged -> published -> verified -> retrospectived -> closed +``` + +The live tracker shape is Wesley-specific: goalpost milestones own +implementation slices, release milestones own release-gate issues, and concrete +`vX.Y.Z` labels are the version scheduling axis. This is intentional because a +GitHub issue can carry only one milestone. + ## Pre-Tag Launch Pass After the release-prep PR lands on `main` but before creating the signed tag, @@ -85,6 +100,7 @@ synced `main` release commit. ## Related Authority +- [`.continuum/release.yml`](../../.continuum/release.yml) - [`docs/method/release.md`](../method/release.md) - [`docs/method/release-runbook.md`](../method/release-runbook.md) - [`docs/governance/RELEASE_POLICY.md`](../governance/RELEASE_POLICY.md) diff --git a/docs/truth-manifest.json b/docs/truth-manifest.json index 6df27e85..70a875d8 100644 --- a/docs/truth-manifest.json +++ b/docs/truth-manifest.json @@ -226,6 +226,21 @@ "status": "current", "owner": "@flyingrobots" }, + { + "path": "docs/method/release.md", + "status": "current", + "owner": "@flyingrobots" + }, + { + "path": "docs/method/release-runbook.md", + "status": "current", + "owner": "@flyingrobots" + }, + { + "path": "docs/CRATES_IO_RELEASE.md", + "status": "current", + "owner": "@flyingrobots" + }, { "path": "docs/governance/RELEASE_POLICY.md", "status": "current", diff --git a/test/ci-workflows.bats b/test/ci-workflows.bats index e394916a..58002a35 100644 --- a/test/ci-workflows.bats +++ b/test/ci-workflows.bats @@ -461,6 +461,13 @@ load 'bats-plugins/bats-assert/load' [ "$verify_line" -lt "$finalize_line" ] } +@test "release crates workflow verifies every published crate" { + for crate in wesley-core wesley-emit-codec wesley-emit-rust wesley-emit-typescript wesley-cli; do + run grep -F "$crate" .github/workflows/release-crates.yml + assert_success + done +} + @test "release crates workflow keeps release scratch files outside repository" { run bash -lc "grep -F '\${RUNNER_TEMP}/release-notes.md' .github/workflows/release-crates.yml | wc -l" assert_success diff --git a/test/release-governance.bats b/test/release-governance.bats index 14d378ed..4181d09b 100644 --- a/test/release-governance.bats +++ b/test/release-governance.bats @@ -48,6 +48,30 @@ load 'bats-plugins/bats-assert/load' assert_success } +@test "release profile names every published Wesley crate" { + run test -f .continuum/release.yml + assert_success + + for crate in wesley-core wesley-emit-codec wesley-emit-rust wesley-emit-typescript wesley-cli; do + run grep -F " - $crate" .continuum/release.yml + assert_success + + run grep -F "name: $crate" .continuum/release.yml + assert_success + done +} + +@test "release doctrine requires thesis scope and retrospective evidence" { + run grep -F "No planned release without a thesis." docs/method/release.md + assert_success + + run grep -F "must-ship, may-slip, and explicitly-not-included" docs/method/release.md + assert_success + + run grep -F "retrospective and fallout issues" docs/method/release.md + assert_success +} + @test "entrypoints command map lists Rust LE binary emitter" { run grep -F "wesley emit le-binary-rust --schema --out " docs/ENTRYPOINTS.md assert_success From 1559702bfb81b83aba4abd78eb4484750fe508f0 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 27 Jun 2026 07:42:35 -0700 Subject: [PATCH 02/15] Fix: enforce release version sources --- CHANGELOG.md | 4 ++ docs/CRATES_IO_RELEASE.md | 1 + docs/governance/RELEASE_POLICY.md | 8 ++- xtask/src/main.rs | 95 +++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9e609e6..3ec63dbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,10 @@ changed-schemas`, runs schema-scoped matrix jobs, and keeps per-schema report - **Release crate visibility check**: The tag-triggered Release Crates workflow now verifies crates.io visibility for `wesley-emit-codec` along with the rest of the published Rust crate set before finalizing the GitHub Release. +- **Release version-source enforcement**: `cargo xtask release-prep-guard`, + `cargo xtask release-guard`, `cargo xtask package-crates`, and + `cargo xtask publish-crates` now reject root `package.json` version drift in + addition to Rust crate manifest drift. - **Docs CLI checker determinism**: The docs command checker now reads the native command list from the Rust CLI source help text instead of invoking `cargo run`, so Node-only repository hygiene does not depend on Cargo diff --git a/docs/CRATES_IO_RELEASE.md b/docs/CRATES_IO_RELEASE.md index 58fe4735..6a5c1775 100644 --- a/docs/CRATES_IO_RELEASE.md +++ b/docs/CRATES_IO_RELEASE.md @@ -90,6 +90,7 @@ The `release-gauntlet` job must verify: - tag commit is reachable from `origin/main` - every published `Cargo.toml` version matches the tag - every internal Wesley dependency version matches the tag +- root `package.json` version matches the tag - every publishable crate has the minimum package file set - root `README.md` exists - root `CHANGELOG.md` contains release notes for the exact version diff --git a/docs/governance/RELEASE_POLICY.md b/docs/governance/RELEASE_POLICY.md index 9438a9ee..f0bb216b 100644 --- a/docs/governance/RELEASE_POLICY.md +++ b/docs/governance/RELEASE_POLICY.md @@ -37,7 +37,7 @@ lacks the human sign-off is not a valid release, and vice versa. | 2 | Zero open exact-version tracker references | `xtask` + `gh` | | | 3 | Strict preflight gate | `xtask` | | | 4 | Zero open issues from prior-version lanes | `gh` | | -| 5 | Version lockstep across all `Cargo.toml` manifests | parse | | +| 5 | Version lockstep across release version sources | parse | | | 6 | `CHANGELOG.md` has a dated entry for this version | parse | | | 7 | `CHANGELOG.md` reflects actual diff vs. prior tag | | reviewer | | 8 | `README.md` version headline matches tag | grep | | @@ -95,8 +95,10 @@ artifacts. ### Check 5: Version Lockstep -All `Cargo.toml` manifests for published crates must declare the same version -as the release tag. Workspace members are not permitted to drift independently. +All release version sources declared in `.continuum/release.yml` must declare +the same version as the release tag. Today that means every published crate +`Cargo.toml` manifest plus the private root `package.json`. Workspace members +are not permitted to drift independently. ### Check 6: Changelog diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 02f7fd84..3dba31fb 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -661,12 +661,18 @@ fn check_release_tag_is_on_main(tag: &str) -> Result<(), Error> { fn check_publish_manifest_versions(version: &str) -> Result<(), Error> { let root = env::current_dir() .map_err(|source| Error::Usage(format!("failed to resolve current directory: {source}")))?; + check_publish_manifest_versions_at(&root, version) +} + +fn check_publish_manifest_versions_at(root: &Path, version: &str) -> Result<(), Error> { let publish_crate_names = PUBLISH_CRATES .iter() .map(|publish_crate| publish_crate.name) .collect::>(); let mut failures = Vec::new(); + check_root_package_version(root, version, &mut failures); + for publish_crate in PUBLISH_CRATES { let manifest_path = root.join(publish_crate.path).join("Cargo.toml"); let manifest = match read_toml_manifest(&manifest_path) { @@ -724,6 +730,35 @@ fn check_publish_manifest_versions(version: &str) -> Result<(), Error> { finish_check("release manifest versions", failures) } +fn check_root_package_version(root: &Path, version: &str, failures: &mut Vec) { + let path = root.join("package.json"); + let content = match fs::read_to_string(&path) { + Ok(content) => content, + Err(source) => { + failures.push(format!("package.json is missing or unreadable: {source}")); + return; + } + }; + + let manifest: serde_json::Value = match serde_json::from_str(&content) { + Ok(manifest) => manifest, + Err(source) => { + failures.push(format!("package.json is malformed JSON: {source}")); + return; + } + }; + + let manifest_version = manifest + .get("version") + .and_then(serde_json::Value::as_str) + .unwrap_or_default(); + if manifest_version != version { + failures.push(format!( + "package.json version is `{manifest_version}`, expected `{version}`" + )); + } +} + fn check_dependency_hygiene( publish_crate: &PublishCrate, manifest: &toml::Value, @@ -3572,6 +3607,66 @@ mod tests { } } + #[test] + fn root_package_json_version_mismatch_blocks_release_manifest_check() { + let root = env::temp_dir().join(format!( + "wesley-xtask-release-version-root-{}", + std::process::id() + )); + if root.exists() { + fs::remove_dir_all(&root).expect("stale temp root should be removed"); + } + fs::create_dir_all(&root).expect("temp root should be created"); + fs::write( + root.join("package.json"), + serde_json::json!({ + "name": "wesley", + "version": "9.9.9", + "private": true + }) + .to_string(), + ) + .expect("root package json should be written"); + + for publish_crate in PUBLISH_CRATES { + let crate_root = root.join(publish_crate.path); + fs::create_dir_all(crate_root.join("src")).expect("crate src should be created"); + fs::write(crate_root.join("README.md"), "# Test crate\n") + .expect("crate readme should be written"); + fs::write(crate_root.join("src/lib.rs"), "").expect("crate source should be written"); + + let mut dependency_lines = String::new(); + for dependency in publish_crate.dependencies { + dependency_lines.push_str(&format!( + "{dependency} = {{ path = \"../{dependency}\", version = \"1.2.3\" }}\n" + )); + } + fs::write( + crate_root.join("Cargo.toml"), + format!( + "[package]\nname = \"{}\"\nversion = \"1.2.3\"\nedition = \"2021\"\nreadme = \"README.md\"\n\n[dependencies]\n{}", + publish_crate.name, dependency_lines + ), + ) + .expect("crate manifest should be written"); + } + + let result = check_publish_manifest_versions_at(&root, "1.2.3"); + + match result { + Err(Error::CheckFailed { check, failures }) => { + assert_eq!(check, "release manifest versions"); + assert_eq!( + failures, + vec!["package.json version is `9.9.9`, expected `1.2.3`"] + ); + } + other => panic!("expected root package version failure, got {other:?}"), + } + + fs::remove_dir_all(root).expect("temp root should be removed"); + } + // --- looks_like_file_path --- #[test] From 431d2decab9547e469dfd1eadb8e97e5a10a7ea1 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 27 Jun 2026 07:50:28 -0700 Subject: [PATCH 03/15] Fix: enforce unpublished release version sources --- .continuum/release.yml | 6 ++ CHANGELOG.md | 5 +- test/release-governance.bats | 13 ++++ xtask/src/main.rs | 146 +++++++++++++++++++++++++++++++++++ 4 files changed, 168 insertions(+), 2 deletions(-) diff --git a/.continuum/release.yml b/.continuum/release.yml index 1c88bdc6..024c4fd3 100644 --- a/.continuum/release.yml +++ b/.continuum/release.yml @@ -45,6 +45,12 @@ version_sources: field: package.version required: true published: true + - path: crates/wesley-holmes/Cargo.toml + name: wesley-holmes + type: cargo-manifest + field: package.version + required: true + published: false - path: package.json name: root-private-package type: json diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ec63dbd..9e250e6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,8 +61,9 @@ changed-schemas`, runs schema-scoped matrix jobs, and keeps per-schema report of the published Rust crate set before finalizing the GitHub Release. - **Release version-source enforcement**: `cargo xtask release-prep-guard`, `cargo xtask release-guard`, `cargo xtask package-crates`, and - `cargo xtask publish-crates` now reject root `package.json` version drift in - addition to Rust crate manifest drift. + `cargo xtask publish-crates` now reject root `package.json` and unpublished + required Cargo manifest version drift in addition to published Rust crate + manifest drift. - **Docs CLI checker determinism**: The docs command checker now reads the native command list from the Rust CLI source help text instead of invoking `cargo run`, so Node-only repository hygiene does not depend on Cargo diff --git a/test/release-governance.bats b/test/release-governance.bats index 4181d09b..91bec852 100644 --- a/test/release-governance.bats +++ b/test/release-governance.bats @@ -61,6 +61,19 @@ load 'bats-plugins/bats-assert/load' done } +@test "release profile names unpublished Holmes as a required version source" { + run grep -F "path: crates/wesley-holmes/Cargo.toml" .continuum/release.yml + assert_success + + run grep -F "name: wesley-holmes" .continuum/release.yml + assert_success + + run grep -A5 "path: crates/wesley-holmes/Cargo.toml" .continuum/release.yml + assert_success + assert_output --partial "required: true" + assert_output --partial "published: false" +} + @test "release doctrine requires thesis scope and retrospective evidence" { run grep -F "No planned release without a thesis." docs/method/release.md assert_success diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 3dba31fb..3c3aaa2b 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -53,6 +53,11 @@ const PUBLISH_CRATES: &[PublishCrate] = &[ dependencies: &["wesley-core", "wesley-emit-rust", "wesley-emit-typescript"], }, ]; +const UNPUBLISHED_CARGO_VERSION_SOURCES: &[CargoVersionSource] = &[CargoVersionSource { + name: "wesley-holmes", + path: "crates/wesley-holmes", + publish: false, +}]; fn main() -> ExitCode { match run(env::args_os().skip(1).collect()) { @@ -673,6 +678,10 @@ fn check_publish_manifest_versions_at(root: &Path, version: &str) -> Result<(), check_root_package_version(root, version, &mut failures); + for source in UNPUBLISHED_CARGO_VERSION_SOURCES { + check_cargo_version_source(root, source, version, &mut failures); + } + for publish_crate in PUBLISH_CRATES { let manifest_path = root.join(publish_crate.path).join("Cargo.toml"); let manifest = match read_toml_manifest(&manifest_path) { @@ -730,6 +739,61 @@ fn check_publish_manifest_versions_at(root: &Path, version: &str) -> Result<(), finish_check("release manifest versions", failures) } +fn check_cargo_version_source( + root: &Path, + source: &CargoVersionSource, + version: &str, + failures: &mut Vec, +) { + let manifest_path = root.join(source.path).join("Cargo.toml"); + let manifest = match read_toml_manifest(&manifest_path) { + Ok(manifest) => manifest, + Err(failure) => { + failures.push(failure); + return; + } + }; + + let Some(package) = manifest.get("package").and_then(toml::Value::as_table) else { + failures.push(format!("{} is missing [package]", source.path)); + return; + }; + + let name = package + .get("name") + .and_then(toml::Value::as_str) + .unwrap_or_default(); + if name != source.name { + failures.push(format!( + "{} package.name is `{name}`, expected `{}`", + source.path, source.name + )); + } + + let manifest_version = package + .get("version") + .and_then(toml::Value::as_str) + .unwrap_or_default(); + if manifest_version != version { + failures.push(format!( + "{} version is `{manifest_version}`, expected `{version}`", + source.path + )); + } + + let publish = package.get("publish").and_then(toml::Value::as_bool); + if publish != Some(source.publish) { + failures.push(format!( + "{} publish is `{}`, expected `{}`", + source.path, + publish + .map(|value| value.to_string()) + .unwrap_or_else(|| "unset".to_string()), + source.publish + )); + } +} + fn check_root_package_version(root: &Path, version: &str, failures: &mut Vec) { let path = root.join("package.json"); let content = match fs::read_to_string(&path) { @@ -2768,6 +2832,12 @@ struct PublishCrate { dependencies: &'static [&'static str], } +struct CargoVersionSource { + name: &'static str, + path: &'static str, + publish: bool, +} + #[derive(Default)] struct PublishOptions { execute: bool, @@ -3651,6 +3721,14 @@ mod tests { .expect("crate manifest should be written"); } + let holmes_root = root.join("crates/wesley-holmes"); + fs::create_dir_all(holmes_root.join("src")).expect("holmes src should be created"); + fs::write( + holmes_root.join("Cargo.toml"), + "[package]\nname = \"wesley-holmes\"\nversion = \"1.2.3\"\nedition = \"2021\"\npublish = false\n", + ) + .expect("holmes manifest should be written"); + let result = check_publish_manifest_versions_at(&root, "1.2.3"); match result { @@ -3667,6 +3745,74 @@ mod tests { fs::remove_dir_all(root).expect("temp root should be removed"); } + #[test] + fn unpublished_holmes_version_mismatch_blocks_release_manifest_check() { + let root = env::temp_dir().join(format!( + "wesley-xtask-release-holmes-version-{}", + std::process::id() + )); + if root.exists() { + fs::remove_dir_all(&root).expect("stale temp root should be removed"); + } + fs::create_dir_all(&root).expect("temp root should be created"); + fs::write( + root.join("package.json"), + serde_json::json!({ + "name": "wesley", + "version": "1.2.3", + "private": true + }) + .to_string(), + ) + .expect("root package json should be written"); + + for publish_crate in PUBLISH_CRATES { + let crate_root = root.join(publish_crate.path); + fs::create_dir_all(crate_root.join("src")).expect("crate src should be created"); + fs::write(crate_root.join("README.md"), "# Test crate\n") + .expect("crate readme should be written"); + fs::write(crate_root.join("src/lib.rs"), "").expect("crate source should be written"); + + let mut dependency_lines = String::new(); + for dependency in publish_crate.dependencies { + dependency_lines.push_str(&format!( + "{dependency} = {{ path = \"../{dependency}\", version = \"1.2.3\" }}\n" + )); + } + fs::write( + crate_root.join("Cargo.toml"), + format!( + "[package]\nname = \"{}\"\nversion = \"1.2.3\"\nedition = \"2021\"\nreadme = \"README.md\"\n\n[dependencies]\n{}", + publish_crate.name, dependency_lines + ), + ) + .expect("crate manifest should be written"); + } + + let holmes_root = root.join("crates/wesley-holmes"); + fs::create_dir_all(holmes_root.join("src")).expect("holmes src should be created"); + fs::write( + holmes_root.join("Cargo.toml"), + "[package]\nname = \"wesley-holmes\"\nversion = \"9.9.9\"\nedition = \"2021\"\npublish = false\n", + ) + .expect("holmes manifest should be written"); + + let result = check_publish_manifest_versions_at(&root, "1.2.3"); + + match result { + Err(Error::CheckFailed { check, failures }) => { + assert_eq!(check, "release manifest versions"); + assert_eq!( + failures, + vec!["crates/wesley-holmes version is `9.9.9`, expected `1.2.3`"] + ); + } + other => panic!("expected Holmes package version failure, got {other:?}"), + } + + fs::remove_dir_all(root).expect("temp root should be removed"); + } + // --- looks_like_file_path --- #[test] From c3be0a74ec6cbf20b34d2fdf40db31efb3efd6d6 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 27 Jun 2026 07:51:31 -0700 Subject: [PATCH 04/15] Fix: query release gate milestones --- CHANGELOG.md | 3 +++ xtask/src/main.rs | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e250e6d..a99c01f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,9 @@ changed-schemas`, runs schema-scoped matrix jobs, and keeps per-schema report `cargo xtask publish-crates` now reject root `package.json` and unpublished required Cargo manifest version drift in addition to published Rust crate manifest drift. +- **Release milestone gate enforcement**: Release guards now query the declared + `Release: vX.Y.Z` milestone bucket so release-gate issues cannot survive + tagging merely because they lack a version label or exact title/body token. - **Docs CLI checker determinism**: The docs command checker now reads the native command list from the Rust CLI source help text instead of invoking `cargo run`, so Node-only repository hygiene does not depend on Cargo diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 3c3aaa2b..32d83642 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1609,6 +1609,7 @@ fn release_issue_queries(tag: &str, version: &str, repo: &str) -> Vec Vec { + let release_milestone = format!("Release: {tag}"); vec![ ReleaseIssueQuery { args: release_issue_selector_query("--label", tag), @@ -1625,6 +1626,11 @@ fn release_issue_query_specs(tag: &str, version: &str, _repo: &str) -> Vec Date: Sat, 27 Jun 2026 07:52:17 -0700 Subject: [PATCH 05/15] Fix: declare release audit validation --- .continuum/release.yml | 1 + CHANGELOG.md | 3 +++ test/release-governance.bats | 5 +++++ 3 files changed, 9 insertions(+) diff --git a/.continuum/release.yml b/.continuum/release.yml index 024c4fd3..085f47b1 100644 --- a/.continuum/release.yml +++ b/.continuum/release.yml @@ -88,6 +88,7 @@ validation: docs_check: cargo xtask docs-check prep: cargo xtask release-prep-guard --version {version} preflight: cargo xtask preflight + rust_advisory_audit: cargo audit release_check: cargo xtask release-check package: cargo xtask package-crates --version {version} tagged_guard: cargo xtask release-guard --tag v{version} diff --git a/CHANGELOG.md b/CHANGELOG.md index a99c01f1..7952fb0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,9 @@ changed-schemas`, runs schema-scoped matrix jobs, and keeps per-schema report - **Release milestone gate enforcement**: Release guards now query the declared `Release: vX.Y.Z` milestone bucket so release-gate issues cannot survive tagging merely because they lack a version label or exact title/body token. +- **Release advisory-audit profile**: The repo-local release profile now + declares the Rust advisory audit command alongside the other release + validation gates. - **Docs CLI checker determinism**: The docs command checker now reads the native command list from the Rust CLI source help text instead of invoking `cargo run`, so Node-only repository hygiene does not depend on Cargo diff --git a/test/release-governance.bats b/test/release-governance.bats index 91bec852..479a3111 100644 --- a/test/release-governance.bats +++ b/test/release-governance.bats @@ -74,6 +74,11 @@ load 'bats-plugins/bats-assert/load' assert_output --partial "published: false" } +@test "release profile declares Rust advisory audit validation" { + run grep -F "rust_advisory_audit: cargo audit" .continuum/release.yml + assert_success +} + @test "release doctrine requires thesis scope and retrospective evidence" { run grep -F "No planned release without a thesis." docs/method/release.md assert_success From 409c595f4d5a50c27025ea31042aa3fe894cbc67 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 27 Jun 2026 07:53:17 -0700 Subject: [PATCH 06/15] Fix: cover release signpost docs --- .continuum/release.yml | 2 ++ CHANGELOG.md | 3 +++ test/release-governance.bats | 8 ++++++++ 3 files changed, 13 insertions(+) diff --git a/.continuum/release.yml b/.continuum/release.yml index 085f47b1..9740c6d3 100644 --- a/.continuum/release.yml +++ b/.continuum/release.yml @@ -69,6 +69,8 @@ docs: - docs/ARCHITECTURE.md - docs/TECHNICAL_TEARDOWN.md user_docs: + - docs/GUIDE.md + - docs/site/ - docs/topics/ - docs/reference/ - docs/releases/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 7952fb0f..612c8988 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,9 @@ changed-schemas`, runs schema-scoped matrix jobs, and keeps per-schema report - **Release advisory-audit profile**: The repo-local release profile now declares the Rust advisory audit command alongside the other release validation gates. +- **Release signpost profile coverage**: The repo-local release profile now + includes the public MkDocs source and guide page in user-doc signposts so + profile-driven audits cover public release wording. - **Docs CLI checker determinism**: The docs command checker now reads the native command list from the Rust CLI source help text instead of invoking `cargo run`, so Node-only repository hygiene does not depend on Cargo diff --git a/test/release-governance.bats b/test/release-governance.bats index 479a3111..19ac124a 100644 --- a/test/release-governance.bats +++ b/test/release-governance.bats @@ -79,6 +79,14 @@ load 'bats-plugins/bats-assert/load' assert_success } +@test "release profile includes public site and guide signposts" { + run grep -F " - docs/site/" .continuum/release.yml + assert_success + + run grep -F " - docs/GUIDE.md" .continuum/release.yml + assert_success +} + @test "release doctrine requires thesis scope and retrospective evidence" { run grep -F "No planned release without a thesis." docs/method/release.md assert_success From c2dd64aea83b65d0f1d589a337e99c633b759702 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 27 Jun 2026 10:07:26 -0700 Subject: [PATCH 07/15] Fix: certify SHIPME after merge --- .github/workflows/cert-shipme.yml | 121 +----------------------------- CHANGELOG.md | 3 + docs/topics/assurance-evidence.md | 3 + docs/topics/ci-workflows.md | 3 + test/ci-workflows.bats | 68 ++++------------- 5 files changed, 26 insertions(+), 172 deletions(-) diff --git a/.github/workflows/cert-shipme.yml b/.github/workflows/cert-shipme.yml index d0414df5..88cf1b50 100644 --- a/.github/workflows/cert-shipme.yml +++ b/.github/workflows/cert-shipme.yml @@ -2,13 +2,7 @@ name: SHIPME Certificate on: push: - branches: [main, dev, 'feat/*', 'fix/*', 'milestone/*'] - paths: - - 'packages/**' - - 'scripts/prepare-shipme-cert-fixture.mjs' - - '.github/**' - - '.github/workflows/cert-shipme.yml' - pull_request: + branches: [main] paths: - 'packages/**' - 'scripts/prepare-shipme-cert-fixture.mjs' @@ -21,11 +15,6 @@ permissions: jobs: cert: runs-on: ubuntu-latest - permissions: - actions: read - contents: read - issues: write - pull-requests: write steps: - name: Harden runner uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 @@ -65,111 +54,3 @@ jobs: with: name: SHIPME path: SHIPME.md - - - name: Post SHIPME badge to PR - if: github.event_name == 'pull_request' - id: badge - run: | - echo "BADGE=[SHIPME] PASS · HOLMES fixture · sha ${GITHUB_SHA::7}" >> $GITHUB_OUTPUT - - - name: Wait for HOLMES suite comment - if: github.event_name == 'pull_request' - id: holmes_comment_wait - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - script: | - const marker = ``; - const started = Date.now(); - const workflowId = 'wesley-holmes.yml'; - const timeoutMs = 2 * 60 * 60 * 1000; - const pollIntervalMs = 15 * 1000; - const maxNoRunFound = 20; - let noRunFoundCount = 0; - - while (Date.now() - started < timeoutMs) { - const comments = await github.paginate(github.rest.issues.listComments, { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - per_page: 100, - }); - - const holmesComment = comments.find( - (comment) => comment.user?.login === 'github-actions[bot]' && comment.body?.includes(marker) - ); - - if (holmesComment) { - core.setOutput('ready', 'true'); - return; - } - - const workflowRunsResponse = await github.request( - 'GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs', - { - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: workflowId, - event: 'pull_request', - head_sha: context.sha, - per_page: 20 - } - ); - - const currentRun = workflowRunsResponse.data.workflow_runs - .sort((left, right) => new Date(right.created_at) - new Date(left.created_at))[0]; - - if (!currentRun) { - noRunFoundCount += 1; - if (noRunFoundCount > maxNoRunFound) { - core.setOutput('ready', 'false'); - core.setFailed(`No wesley-holmes.yml run found for ${context.sha} after ${maxNoRunFound} polls.`); - return; - } - - await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); - continue; - } - - noRunFoundCount = 0; - - if (currentRun?.status === 'completed') { - core.setOutput('ready', 'false'); - core.setFailed( - `HOLMES suite workflow ${currentRun.id} for ${context.sha} finished with conclusion ${currentRun.conclusion || 'unknown'} before posting the Holmes PR comment.` - ); - return; - } - - await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); - } - - core.setOutput('ready', 'false'); - core.setFailed(`HOLMES suite comment for ${context.sha} did not appear within the 2 hour coordination window.`); - - - name: Create/Update PR Comment - if: github.event_name == 'pull_request' && github.actor != 'dependabot[bot]' && steps.holmes_comment_wait.outputs.ready == 'true' - continue-on-error: true - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - script: | - const badge = process.env.BADGE || '${{ steps.badge.outputs.BADGE }}'; - const marker = ''; - const body = `${marker}\n### 🚢 SHIPME\n\n${badge}\n`; - try { - const comments = await github.paginate(github.rest.issues.listComments, { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - per_page: 100, - }); - const botComment = comments.find( - (c) => c.user?.login === 'github-actions[bot]' && c.body?.includes(marker) - ); - if (botComment) { - await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: botComment.id, body }); - } else { - await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body }); - } - } catch (error) { - core.warning(`Could not post SHIPME badge to PR: ${error.message}`); - } diff --git a/CHANGELOG.md b/CHANGELOG.md index 612c8988..1cb4c469 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,9 @@ changed-schemas`, runs schema-scoped matrix jobs, and keeps per-schema report - **Release signpost profile coverage**: The repo-local release profile now includes the public MkDocs source and guide page in user-doc signposts so profile-driven audits cover public release wording. +- **Post-merge SHIPME certification**: `cert-shipme.yml` now runs only on + `main` pushes, so SHIPME certificates bind to the landed target-branch SHA + instead of racing PR-time HOLMES comments for a temporary merge SHA. - **Docs CLI checker determinism**: The docs command checker now reads the native command list from the Rust CLI source help text instead of invoking `cargo run`, so Node-only repository hygiene does not depend on Cargo diff --git a/docs/topics/assurance-evidence.md b/docs/topics/assurance-evidence.md index 6117569e..37422b34 100644 --- a/docs/topics/assurance-evidence.md +++ b/docs/topics/assurance-evidence.md @@ -18,12 +18,15 @@ and it does not create product semantics for GraphQL. | `docs/templates/holmes-policy/` | Policy templates for host contexts. | | `docs/architecture/holmes-*` | Architecture and integration notes. | | `.github/workflows/wesley-holmes.yml` | Pull request assurance workflow. | +| `.github/workflows/cert-shipme.yml` | Post-merge SHIPME certificate workflow. | ## Rules Of Thumb - Reports should expose unavailable or invalid evidence honestly. - Missing artifacts should not be hidden behind a passing workflow. - Policy and report quality are evidence questions, not compiler semantics. +- PR-time HOLMES evidence and post-merge SHIPME certification are distinct + gates. SHIPME records the landed `main` SHA, not the temporary PR merge SHA. - Domain-specific target facts should be produced by the owning target module. ## Useful Checks diff --git a/docs/topics/ci-workflows.md b/docs/topics/ci-workflows.md index 51e771e2..4e83ea5c 100644 --- a/docs/topics/ci-workflows.md +++ b/docs/topics/ci-workflows.md @@ -18,6 +18,7 @@ diff or running focused checks before a PR. | CodeQL / analysis | Static analysis for supported languages. | | Dependency review | Dependency risk in PRs. | | HOLMES workflow | Schema-selected assurance reports and PR comments. | +| SHIPME certificate | Post-merge evidence for the landed `main` SHA. | ## Local Mirrors @@ -43,6 +44,8 @@ BATS_LIB_PATH=test/vendor bats -t test/ci-workflows.bats ## Rules Of Thumb - A skipped HOLMES matrix can be correct when no schema set is selected. +- SHIPME certification is post-merge only. PR checks evaluate the proposed + integration; SHIPME certifies the commit that actually lands on `main`. - Browser, Bun, and Deno host experiment workflows are retired from Wesley. - Required checks should name the Rust product or repository hygiene surface they protect. diff --git a/test/ci-workflows.bats b/test/ci-workflows.bats index 58002a35..60a0a2ab 100644 --- a/test/ci-workflows.bats +++ b/test/ci-workflows.bats @@ -278,42 +278,38 @@ load 'bats-plugins/bats-assert/load' [ "$output" -ge 3 ] } -@test "cert-shipme anchors and paginates bot comments" { - run bash -lc "grep -F '' .github/workflows/cert-shipme.yml | wc -l" +@test "cert-shipme certifies only landed target-branch commits" { + run bash -lc "grep -F 'pull_request:' .github/workflows/cert-shipme.yml | wc -l" assert_success - [ "$output" -eq 1 ] + [ "$output" -eq 0 ] - run bash -lc "grep -F \"github-actions[bot]\" .github/workflows/cert-shipme.yml | wc -l" + run bash -lc "grep -F 'branches: [main]' .github/workflows/cert-shipme.yml | wc -l" assert_success - [ "$output" -ge 1 ] + [ "$output" -eq 1 ] - run bash -lc "grep -F 'per_page: 100' .github/workflows/cert-shipme.yml | wc -l" + run bash -lc "grep -F 'Commit: \${GITHUB_SHA}' .github/workflows/cert-shipme.yml | wc -l" assert_success - [ "$output" -ge 1 ] + [ "$output" -eq 1 ] - run bash -lc "grep -F 'pull-requests: write' .github/workflows/cert-shipme.yml | wc -l" + run bash -lc "grep -F '' .github/workflows/cert-shipme.yml | wc -l" assert_success - [ "$output" -ge 1 ] + [ "$output" -eq 0 ] - run bash -lc "grep -F 'actions: read' .github/workflows/cert-shipme.yml | wc -l" + run bash -lc "grep -F 'Wait for HOLMES suite comment' .github/workflows/cert-shipme.yml | wc -l" assert_success - [ "$output" -ge 1 ] + [ "$output" -eq 0 ] - run bash -lc "grep -F \"github.actor != 'dependabot[bot]'\" .github/workflows/cert-shipme.yml | wc -l" + run bash -lc "grep -F 'HOLMES_SUITE_SHA:' .github/workflows/cert-shipme.yml | wc -l" assert_success - [ "$output" -eq 1 ] + [ "$output" -eq 0 ] run bash -lc "grep -F 'github.rest.issues.updateComment' .github/workflows/cert-shipme.yml | wc -l" assert_success - [ "$output" -eq 1 ] + [ "$output" -eq 0 ] run bash -lc "grep -F 'github.rest.issues.createComment' .github/workflows/cert-shipme.yml | wc -l" assert_success - [ "$output" -eq 1 ] - - run bash -lc "grep -F 'comment_id: botComment.id' .github/workflows/cert-shipme.yml | wc -l" - assert_success - [ "$output" -eq 1 ] + [ "$output" -eq 0 ] run bash -lc "grep -F 'Run HOLMES investigation' .github/workflows/cert-shipme.yml | wc -l" assert_success @@ -333,7 +329,7 @@ load 'bats-plugins/bats-assert/load' run bash -lc "grep -F \"'scripts/prepare-shipme-cert-fixture.mjs'\" .github/workflows/cert-shipme.yml | wc -l" assert_success - [ "$output" -eq 2 ] + [ "$output" -eq 1 ] run bash -lc "grep -F 'rehearse --schema test/fixtures/blade/schema-v1.graphql' .github/workflows/cert-shipme.yml | wc -l" assert_success @@ -342,38 +338,6 @@ load 'bats-plugins/bats-assert/load' run bash -lc "grep -F '.wesley-cache/holmes-report.json' .github/workflows/cert-shipme.yml | wc -l" assert_success [ "$output" -ge 1 ] - - run bash -lc "grep -F 'Wait for HOLMES suite comment' .github/workflows/cert-shipme.yml | wc -l" - assert_success - [ "$output" -eq 1 ] - - run bash -lc "grep -F 'HOLMES_SUITE_SHA:' .github/workflows/cert-shipme.yml | wc -l" - assert_success - [ "$output" -eq 1 ] - - run bash -lc "grep -F \"steps.holmes_comment_wait.outputs.ready == 'true'\" .github/workflows/cert-shipme.yml | wc -l" - assert_success - [ "$output" -eq 1 ] - - run bash -lc "grep -F 'actions/workflows/{workflow_id}/runs' .github/workflows/cert-shipme.yml | wc -l" - assert_success - [ "$output" -eq 1 ] - - run bash -lc "grep -F '2 * 60 * 60 * 1000' .github/workflows/cert-shipme.yml | wc -l" - assert_success - [ "$output" -eq 1 ] - - run bash -lc "grep -F 'const maxNoRunFound = 20' .github/workflows/cert-shipme.yml | wc -l" - assert_success - [ "$output" -eq 1 ] - - run bash -lc "grep -F 'No wesley-holmes.yml run found for' .github/workflows/cert-shipme.yml | wc -l" - assert_success - [ "$output" -eq 1 ] - - run bash -lc "grep -F 'core.setFailed(' .github/workflows/cert-shipme.yml | wc -l" - assert_success - [ "$output" -ge 1 ] } @test "retired progress workflow and README updater do not return" { From 375062713ac5715fed849b22f5c6421928610d4b Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 27 Jun 2026 13:22:56 -0700 Subject: [PATCH 08/15] docs(holmes): lock reusable workflow direction --- CHANGELOG.md | 3 +++ docs/architecture/holmes-integration.md | 14 ++++++++++++++ docs/topics/holmes-ci.md | 16 ++++++++++++++++ test/docs-planning-boundary.bats | 14 ++++++++++++++ 4 files changed, 47 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cb4c469..d6af1b76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,9 @@ hash`, and `schema operations`. manifest first, computes changed schema sets with `wesley config changed-schemas`, runs schema-scoped matrix jobs, and keeps per-schema report artifacts grouped for one aggregate PR comment. +- **HOLMES distribution direction**: Documented tagged reusable GitHub Actions + workflows plus copy/paste templates as the user-facing HOLMES install path, + with GitHub App delivery deferred to future identity or Checks API needs. - **Extension documentation**: Added current project-manifest and module authoring references, and clarified that `wesley.config.mjs` and the dynamic JavaScript module loader are retired from generic Wesley core. diff --git a/docs/architecture/holmes-integration.md b/docs/architecture/holmes-integration.md index 42041440..596860a0 100644 --- a/docs/architecture/holmes-integration.md +++ b/docs/architecture/holmes-integration.md @@ -113,6 +113,20 @@ Manifest `commentMode` controls the final PR comment behavior: | `append` | Create a new PR comment for each run. | | `silent` | Run analysis but do not write a PR comment. | +## Distribution Boundary + +HOLMES should be packaged for external users as a tagged reusable workflow with +documented workflow templates. That keeps execution inside the consumer +repository's GitHub Actions environment, where checkout state, permissions, +artifacts, and logs are inspectable by the repository owner. + +A GitHub App is not the first-class delivery mechanism. It remains deferred +unless HOLMES needs a durable certifying identity, Checks API ownership, +cross-repo dashboards, or organization-level policy orchestration. If such an +app exists later, it should consume evidence produced by repository-local +Actions runs rather than becoming a hidden compiler or source of target +semantics. + ## Dashboard Artifact The workflow uploads `docs/holmes-dashboard` as a dashboard template and diff --git a/docs/topics/holmes-ci.md b/docs/topics/holmes-ci.md index 473cb4ea..08efdec5 100644 --- a/docs/topics/holmes-ci.md +++ b/docs/topics/holmes-ci.md @@ -21,6 +21,22 @@ If no changed files are available, every schema set is selected. If no schema set matches the changed files, the HOLMES matrix is skipped rather than inventing work. +## Distribution Direction + +The user-facing HOLMES install path should be a tagged reusable GitHub Actions +workflow, backed by documented copy/paste workflow templates for repositories +that need full local ownership. Templates are examples and escape hatches; the +reusable workflow is the normal distribution surface because HOLMES is a CI +lane, not a single shell step. + +Consumers should pin released tags, not `main`, when they call a shared HOLMES +workflow or action. + +Do not make a GitHub App the first-class HOLMES install path. A future app may +be justified for dedicated `SHA-lock HOLMES` identity, Checks API ownership, +cross-repo dashboards, or organization policy orchestration. Until that need is +active, GitHub Actions should produce the evidence and PR comments directly. + ## Operator Notes - CodeRabbit and HOLMES are separate review surfaces. diff --git a/test/docs-planning-boundary.bats b/test/docs-planning-boundary.bats index f5162a4e..13be2d6a 100644 --- a/test/docs-planning-boundary.bats +++ b/test/docs-planning-boundary.bats @@ -103,3 +103,17 @@ load 'bats-plugins/bats-assert/load' run rg -n "cargo install wesley-cli --version 0\\.1\\.0" README.md docs/GUIDE.md docs/ENTRYPOINTS.md CONTRIBUTING.md assert_failure } + +@test "HOLMES distribution favors tagged workflows over a GitHub App install path" { + run grep -F "tagged reusable GitHub Actions" docs/topics/holmes-ci.md + assert_success + + run grep -F "GitHub App the first-class HOLMES install path" docs/topics/holmes-ci.md + assert_success + + run grep -F "GitHub App is not the first-class delivery mechanism" docs/architecture/holmes-integration.md + assert_success + + run grep -F "Consumers should pin released tags, not \`main\`" docs/topics/holmes-ci.md + assert_success +} From ec83ae49f8ff691f797d5a9302c3ed373f547286 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 27 Jun 2026 14:35:13 -0700 Subject: [PATCH 09/15] Fix: use release milestone gate only --- xtask/src/main.rs | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 32d83642..b57cb706 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1616,16 +1616,6 @@ fn release_issue_query_specs(tag: &str, version: &str, _repo: &str) -> Vec Date: Sat, 27 Jun 2026 14:35:54 -0700 Subject: [PATCH 10/15] Fix: retry crate visibility checks --- .github/workflows/release-crates.yml | 11 ++++++++++- CHANGELOG.md | 3 ++- test/ci-workflows.bats | 11 +++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-crates.yml b/.github/workflows/release-crates.yml index 24758abc..cf55485c 100644 --- a/.github/workflows/release-crates.yml +++ b/.github/workflows/release-crates.yml @@ -224,7 +224,16 @@ jobs: set -euo pipefail version="${GITHUB_REF_NAME#v}" for crate in wesley-core wesley-emit-codec wesley-emit-rust wesley-emit-typescript wesley-cli; do - cargo info "${crate}@${version}" >/dev/null + for attempt in $(seq 1 30); do + if cargo info "${crate}@${version}" >/dev/null 2>&1; then + break + fi + if [ "$attempt" -eq 30 ]; then + echo "${crate}@${version} did not become visible on crates.io in time" >&2 + exit 1 + fi + sleep 10 + done done - name: Finalize GitHub Release diff --git a/CHANGELOG.md b/CHANGELOG.md index d6af1b76..ffc35d5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,7 +61,8 @@ changed-schemas`, runs schema-scoped matrix jobs, and keeps per-schema report - **Release crate visibility check**: The tag-triggered Release Crates workflow now verifies crates.io visibility for `wesley-emit-codec` along with the rest - of the published Rust crate set before finalizing the GitHub Release. + of the published Rust crate set, with bounded registry-index retries before + finalizing the GitHub Release. - **Release version-source enforcement**: `cargo xtask release-prep-guard`, `cargo xtask release-guard`, `cargo xtask package-crates`, and `cargo xtask publish-crates` now reject root `package.json` and unpublished diff --git a/test/ci-workflows.bats b/test/ci-workflows.bats index 60a0a2ab..e9a9ccc5 100644 --- a/test/ci-workflows.bats +++ b/test/ci-workflows.bats @@ -432,6 +432,17 @@ load 'bats-plugins/bats-assert/load' done } +@test "release crates workflow retries crates.io visibility checks" { + run grep -F "for attempt in \$(seq 1 30)" .github/workflows/release-crates.yml + assert_success + + run grep -F "did not become visible on crates.io in time" .github/workflows/release-crates.yml + assert_success + + run grep -F "sleep 10" .github/workflows/release-crates.yml + assert_success +} + @test "release crates workflow keeps release scratch files outside repository" { run bash -lc "grep -F '\${RUNNER_TEMP}/release-notes.md' .github/workflows/release-crates.yml | wc -l" assert_success From 92289999ca730a736f66fc1181080a4764299202 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 27 Jun 2026 14:36:49 -0700 Subject: [PATCH 11/15] Fix: name Holmes version source in policy --- docs/governance/RELEASE_POLICY.md | 5 +++-- test/release-governance.bats | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/governance/RELEASE_POLICY.md b/docs/governance/RELEASE_POLICY.md index f0bb216b..166d5cbe 100644 --- a/docs/governance/RELEASE_POLICY.md +++ b/docs/governance/RELEASE_POLICY.md @@ -97,8 +97,9 @@ artifacts. All release version sources declared in `.continuum/release.yml` must declare the same version as the release tag. Today that means every published crate -`Cargo.toml` manifest plus the private root `package.json`. Workspace members -are not permitted to drift independently. +`Cargo.toml` manifest, the unpublished `crates/wesley-holmes/Cargo.toml` +manifest, and the private root `package.json`. Workspace members are not +permitted to drift independently. ### Check 6: Changelog diff --git a/test/release-governance.bats b/test/release-governance.bats index 19ac124a..4154c5de 100644 --- a/test/release-governance.bats +++ b/test/release-governance.bats @@ -74,6 +74,11 @@ load 'bats-plugins/bats-assert/load' assert_output --partial "published: false" } +@test "release policy names unpublished Holmes as a version source" { + run grep -F "crates/wesley-holmes/Cargo.toml" docs/governance/RELEASE_POLICY.md + assert_success +} + @test "release profile declares Rust advisory audit validation" { run grep -F "rust_advisory_audit: cargo audit" .continuum/release.yml assert_success From eed4048f7f37c9cc4ba68f2db0440023ad6d436d Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 27 Jun 2026 14:37:43 -0700 Subject: [PATCH 12/15] Fix: list release user-doc signposts --- docs/method/release.md | 2 ++ test/release-governance.bats | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/docs/method/release.md b/docs/method/release.md index 63726ac6..23016102 100644 --- a/docs/method/release.md +++ b/docs/method/release.md @@ -210,7 +210,9 @@ Update every signpost whose truth changed since the previous public tag: - `docs/ENTRYPOINTS.md` - `docs/ARCHITECTURE.md` - `docs/TECHNICAL_TEARDOWN.md` +- `docs/site/` - `docs/topics/` +- `docs/reference/` - `docs/releases/vX.Y.Z.md` - `docs/method/releases/vX.Y.Z/` - `docs/method/release.md` diff --git a/test/release-governance.bats b/test/release-governance.bats index 4154c5de..92d5a92e 100644 --- a/test/release-governance.bats +++ b/test/release-governance.bats @@ -92,6 +92,14 @@ load 'bats-plugins/bats-assert/load' assert_success } +@test "release doctrine lists profile user-doc signposts" { + run grep -F '`docs/site/`' docs/method/release.md + assert_success + + run grep -F '`docs/reference/`' docs/method/release.md + assert_success +} + @test "release doctrine requires thesis scope and retrospective evidence" { run grep -F "No planned release without a thesis." docs/method/release.md assert_success From 8af6b5f9e65cfde1e5852f3aef75354f413bcd53 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 27 Jun 2026 14:38:29 -0700 Subject: [PATCH 13/15] Fix: name release retrospective state --- docs/method/release.md | 6 +++--- docs/topics/releases.md | 2 +- test/release-governance.bats | 8 ++++++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/method/release.md b/docs/method/release.md index 23016102..8a7b4e64 100644 --- a/docs/method/release.md +++ b/docs/method/release.md @@ -56,7 +56,7 @@ planned -> tagged -> published -> verified - -> retrospectived + -> retrospected -> closed ``` @@ -108,9 +108,9 @@ A release is verified when public crates.io visibility is confirmed for every published crate, the installed `wesley` CLI launches, the GitHub Release is visible, and release evidence is captured. -### retrospectived +### retrospected -A release is retrospectived when released work, unreleased work, +A release is retrospected when released work, unreleased work, plan-versus-actual scope, repeatable wins, concrete improvements, fallout issues, and the next release recommendation are recorded. diff --git a/docs/topics/releases.md b/docs/topics/releases.md index 297f5d4c..6d54d245 100644 --- a/docs/topics/releases.md +++ b/docs/topics/releases.md @@ -47,7 +47,7 @@ Wesley uses the lifecycle defined in [`docs/method/release.md`](../method/release.md): ```text -planned -> active -> release-prep -> merged -> tagged -> published -> verified -> retrospectived -> closed +planned -> active -> release-prep -> merged -> tagged -> published -> verified -> retrospected -> closed ``` The live tracker shape is Wesley-specific: goalpost milestones own diff --git a/test/release-governance.bats b/test/release-governance.bats index 92d5a92e..3ed7c6ea 100644 --- a/test/release-governance.bats +++ b/test/release-governance.bats @@ -111,6 +111,14 @@ load 'bats-plugins/bats-assert/load' assert_success } +@test "release lifecycle uses retrospected state name" { + run rg -n "retrospectived" docs/method/release.md docs/topics/releases.md + assert_failure + + run rg -n "retrospected" docs/method/release.md docs/topics/releases.md + assert_success +} + @test "entrypoints command map lists Rust LE binary emitter" { run grep -F "wesley emit le-binary-rust --schema --out " docs/ENTRYPOINTS.md assert_success From 9038bf1d999eedd53e3e08ccb366bc32f24c593c Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 27 Jun 2026 14:39:37 -0700 Subject: [PATCH 14/15] Fix: scope crate visibility test --- test/ci-workflows.bats | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/ci-workflows.bats b/test/ci-workflows.bats index e9a9ccc5..1dd70352 100644 --- a/test/ci-workflows.bats +++ b/test/ci-workflows.bats @@ -427,7 +427,7 @@ load 'bats-plugins/bats-assert/load' @test "release crates workflow verifies every published crate" { for crate in wesley-core wesley-emit-codec wesley-emit-rust wesley-emit-typescript wesley-cli; do - run grep -F "$crate" .github/workflows/release-crates.yml + run bash -lc "awk '/name: Verify crates.io visibility/{in_step=1} in_step && /^ - name: Finalize GitHub Release/{exit} in_step {print}' .github/workflows/release-crates.yml | grep -F '$crate'" assert_success done } @@ -443,6 +443,11 @@ load 'bats-plugins/bats-assert/load' assert_success } +@test "release crates visibility assertion is scoped to visibility step" { + run bash -lc "awk '/@test \"release crates workflow verifies every published crate\"/{in_test=1} in_test && /^@test / && !/release crates workflow verifies every published crate/{exit} in_test {print}' test/ci-workflows.bats | grep -F 'Verify crates.io visibility'" + assert_success +} + @test "release crates workflow keeps release scratch files outside repository" { run bash -lc "grep -F '\${RUNNER_TEMP}/release-notes.md' .github/workflows/release-crates.yml | wc -l" assert_success From a6b4a1681cb46a9789eecb82400b1d2f4dd703cc Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 27 Jun 2026 14:40:40 -0700 Subject: [PATCH 15/15] Fix: harden release profile tests --- test/release-governance.bats | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/test/release-governance.bats b/test/release-governance.bats index 3ed7c6ea..622a66a2 100644 --- a/test/release-governance.bats +++ b/test/release-governance.bats @@ -53,7 +53,7 @@ load 'bats-plugins/bats-assert/load' assert_success for crate in wesley-core wesley-emit-codec wesley-emit-rust wesley-emit-typescript wesley-cli; do - run grep -F " - $crate" .continuum/release.yml + run grep -Eq "^[[:space:]]*-[[:space:]]*$crate$" .continuum/release.yml assert_success run grep -F "name: $crate" .continuum/release.yml @@ -68,10 +68,14 @@ load 'bats-plugins/bats-assert/load' run grep -F "name: wesley-holmes" .continuum/release.yml assert_success - run grep -A5 "path: crates/wesley-holmes/Cargo.toml" .continuum/release.yml + run bash -lc "awk ' + /path: crates\\/wesley-holmes\\/Cargo.toml/ { in_block=1 } + in_block && /^[[:space:]]*required:[[:space:]]*true$/ { required=1 } + in_block && /^[[:space:]]*published:[[:space:]]*false$/ { published=1 } + in_block && /^[[:space:]]*-[[:space:]]*path:/ && \$0 !~ /wesley-holmes/ { in_block=0 } + END { exit !(required && published) } + ' .continuum/release.yml" assert_success - assert_output --partial "required: true" - assert_output --partial "published: false" } @test "release policy names unpublished Holmes as a version source" { @@ -79,16 +83,24 @@ load 'bats-plugins/bats-assert/load' assert_success } +@test "release profile assertions are YAML spacing tolerant" { + run bash -lc "awk '/@test \"release profile names every published Wesley crate\"/{in_test=1} in_test && /^@test / && !/release profile names every published Wesley crate/{exit} in_test {print}' test/release-governance.bats | grep -F 'grep -Eq'" + assert_success + + run bash -lc "awk '/@test \"release profile names unpublished Holmes as a required version source\"/{in_test=1} in_test && /^@test / && !/release profile names unpublished Holmes as a required version source/{exit} in_test {print}' test/release-governance.bats | grep -F 'required=1'" + assert_success +} + @test "release profile declares Rust advisory audit validation" { run grep -F "rust_advisory_audit: cargo audit" .continuum/release.yml assert_success } @test "release profile includes public site and guide signposts" { - run grep -F " - docs/site/" .continuum/release.yml + run grep -Eq "^[[:space:]]*-[[:space:]]*docs/site/$" .continuum/release.yml assert_success - run grep -F " - docs/GUIDE.md" .continuum/release.yml + run grep -Eq "^[[:space:]]*-[[:space:]]*docs/GUIDE.md$" .continuum/release.yml assert_success }