cpm-planner 0.2.2

Critical Path Method planner as an MCP server: schedules task graphs (slack, critical path, resource levelling, Monte Carlo), leases lock-aware parallel cohorts to agents, versions plan-as-code variants, and tracks earned value.
Documentation
# Releasing

This is the maintainer runbook for cutting a cpm-planner release. A tag
`vX.Y.Z` on `main` runs [`.github/workflows/release.yml`](../.github/workflows/release.yml),
which builds and publishes the GitHub release, the container image and the MCP
registry entry, and publishes the npm launcher after you approve it. crates.io
is published by hand afterwards.

Throughout, `X.Y.Z` is the new version and `vX.Y.Z` its tag.

## 1. Bump the version

On a branch off `dev`, set the same version in the four files that carry it:

- `Cargo.toml`: `[package] version`.
- `Cargo.lock`: run `cargo check` (or `cargo update -p cpm-planner`) so the
  lock file records the new version.
- `server.json`: the top-level `version` and the tag of the OCI identifier
  (`ghcr.io/praxec/cpm-planner:X.Y.Z`). The OCI package has no `version`
  field: the MCP registry rejects one.
- `npm/package.json`: `version` (the npm launcher downloads the release with
  this version, so it must match).

Then check them:

```sh
scripts/check-version-sync.sh
```

It fails when any `server.json` version or OCI tag, or the `npm/package.json`
version, differs from `Cargo.toml`.
The release workflow repeats the check against the tag and refuses to build on
a mismatch. Nothing else needs a version bump: the README writes `<version>`
for a release tag instead of pinning one.

## 2. Update the CHANGELOG

In [CHANGELOG.md](../CHANGELOG.md), rename `## [Unreleased]` to
`## [X.Y.Z] - YYYY-MM-DD` and add a fresh empty `## [Unreleased]` above it.
Follow Keep a Changelog: group entries under Added, Changed, Fixed and so on.
The date here is the planned release date; step 3 corrects it on the day.

Open the bump as a pull request into `dev` and merge it once CI is green.

## 3. Release PR from `dev` to `main`

On release day, first set the `## [X.Y.Z] - YYYY-MM-DD` date in
`CHANGELOG.md` to today's date (a one-line pull request into `dev`) if it
differs from the planned one.

Then open a pull request with base `main` and head `dev`. The gitflow guard
(`gitflow-guard.yml`) rejects any other head branch. Wait for every required
check, then merge it with a **merge commit**, not squash or rebase, so `main`
and `dev` keep a shared history.

## 4. Tag `main`

```sh
git fetch origin
git checkout main
git pull --ff-only origin main
git tag -a vX.Y.Z -m "vX.Y.Z"
git push origin vX.Y.Z
```

Tag only commits on `main`: the workflow triggers on any `v*` tag. The tag must
match `vMAJOR.MINOR.PATCH` (an optional pre-release or build suffix is
allowed) and equal the `Cargo.toml`, `server.json` and `npm/package.json`
versions.

The workflow then runs these jobs:

1. **validate + draft release**: checks the tag against the versions and
   creates a draft GitHub release with generated notes.
2. **build**: six targets (Linux, macOS and Windows on x86_64 and ARM64). Each
   archive is smoke-tested with `scripts/mcp-smoke.mjs` (MCP `initialize` and
   `tools/list`) and uploaded to the draft.
3. **aggregate metadata + publish**: writes `checksums.sha256` and
   `release-manifest.json` (it fails if the matrix is incomplete), uploads them
   with `install.sh` and `install.ps1`, runs `npm pack` in `npm/` and attaches
   `matthew-cochran-cpm-X.Y.Z.tgz` with its digest in `npm-package.sha256`,
   and publishes the release.
4. **container image** (`.github/workflows/image.yml`): downloads the two
   Linux release archives, checks them against `checksums.sha256` and builds
   the image from those binaries with `Dockerfile.release`, so nothing is
   compiled under emulation. It smoke-tests the amd64 image and checks its
   `--version`, then pushes
   `ghcr.io/praxec/cpm-planner` for linux/amd64 and linux/arm64 with the tags
   `X.Y.Z`, `X.Y` and `latest`.
5. **npm publish**: waits for approval in the `npm` environment, then
   publishes that same tarball as `@matthew-cochran/cpm` (see step 5).
6. **MCP registry**: publishes `server.json` as `io.github.praxec/cpm-planner`
   with a pinned, checksum-verified `mcp-publisher` over GitHub OIDC. This job
   is best-effort (`continue-on-error`), so a failure does not fail the
   release; check its log.

## 5. Approve the npm publish

The `npm publish` job publishes the npm launcher (`npm/`, package
`@matthew-cochran/cpm`) with npm trusted publishing: npm authenticates the job
over GitHub OIDC, so the repository holds no npm token, and the package gets a
provenance attestation. The job runs in the `npm` environment, which waits for
a maintainer's approval. Approve it from the run page (**Review deployments**)
as soon as the release is public. The launcher downloads the GitHub release
whose version equals its own, and until the publish the documented
`npx -y @matthew-cochran/cpm` and `npm install -g @matthew-cochran/cpm`
commands fail with a 404 from the registry.

The job publishes the tarball attached to the release after checking it against
`npm-package.sha256`, so npm gets exactly that file. A version that is already
on npm is skipped, so re-running the job is safe. Pre-release versions
(`X.Y.Z-rc.N`) go to the `next` dist-tag. The job ends by running
`npx -y @matthew-cochran/cpm@X.Y.Z --version`.

One-time setup: on npmjs.com, the package's
**Settings → Trusted publishing** names GitHub Actions, the repository
`praxec/cpm-planner`, the workflow `release.yml` and the environment `npm`; on
GitHub, the `npm` environment has required reviewers and allows only `v*` tags.
npm can only set up trusted publishing for a package that already exists, so
the first version was published by hand.

If the job cannot publish (for example, trusted publishing is misconfigured),
publish the same tarball by hand. You need to be logged in to npm
(`npm login`) as an owner of the `@matthew-cochran` scope, with 2FA:

```sh
mkdir -p /tmp/cpm-npm && cd /tmp/cpm-npm
gh release download vX.Y.Z --repo praxec/cpm-planner \
  --pattern 'matthew-cochran-cpm-X.Y.Z.tgz' --pattern npm-package.sha256
sha256sum -c npm-package.sha256          # must print OK; stop if it does not
tar -tzf matthew-cochran-cpm-X.Y.Z.tgz   # package/{LICENSE,NOTICE,README.md,package.json,bin/,lib/} only, no tests
npm publish ./matthew-cochran-cpm-X.Y.Z.tgz --access public   # prompts for the 2FA code
```

Verify from a clean cache, outside the repository:

```sh
npx -y @matthew-cochran/cpm@X.Y.Z --version
node scripts/mcp-smoke.mjs npx -y @matthew-cochran/cpm@X.Y.Z   # from a checkout of vX.Y.Z
```

The first command prints `cpm-planner X.Y.Z` on stdout after a one-time
download whose progress goes to stderr. The smoke test checks that the launcher
keeps stdout clean for MCP (`initialize` and `tools/list`).

## 6. Verify the release

Assets: the release page lists six archives, `checksums.sha256`,
`release-manifest.json`, `install.sh`, `install.ps1`,
`matthew-cochran-cpm-X.Y.Z.tgz` and `npm-package.sha256`.

```sh
gh release view vX.Y.Z --json assets --jq '.assets[].name'
gh release download vX.Y.Z --pattern checksums.sha256 --pattern '*linux-gnu.tar.gz'
sha256sum -c --ignore-missing checksums.sha256
```

Installer, on a clean shell (repeat with `install.ps1` on Windows):

```sh
curl -fsSL https://github.com/praxec/cpm-planner/releases/latest/download/install.sh | sh -s -- --version vX.Y.Z
node scripts/mcp-smoke.mjs ~/.local/bin/cpm-planner
```

Container image:

```sh
docker pull ghcr.io/praxec/cpm-planner:X.Y.Z
node scripts/mcp-smoke.mjs docker run -i --rm ghcr.io/praxec/cpm-planner:X.Y.Z
```

MCP registry: confirm the registry job succeeded, or look the server up in the
registry and check that its version is `X.Y.Z`.

## 7. Publish to crates.io (manual)

The workflow does not publish the crate. From the tagged commit, with a
crates.io token configured (`cargo login`):

```sh
git checkout vX.Y.Z
cargo publish --dry-run --locked
cargo package --list --locked   # no docs/superpowers, .github or scripts
cargo publish --locked
```

Until this step runs, crates.io keeps serving the previous version, which is why
the README installs from the git tag. docs.rs builds the new version on its own
a few minutes after publishing.

## Rollback

- **A build failed before publishing.** The release is still a draft. Fix the
  problem on `dev`, then delete the draft and the tag and re-release with the
  next patch version (or re-push the same tag if nothing was published):

  ```sh
  gh release delete vX.Y.Z --yes
  git push origin :refs/tags/vX.Y.Z
  git tag -d vX.Y.Z
  ```

  Re-running the workflow on an existing draft is also safe: assets are
  uploaded with `--clobber`, and an existing release is left as is.
- **A bad release was published.** Do not reuse its version. Mark the GitHub
  release as a pre-release or edit its notes to warn users, then ship a fixed
  `X.Y.(Z+1)`. The installers resolve `releases/latest`, so they pick up the
  fix as soon as it is published.
- **A bad crate was published.** crates.io versions cannot be deleted or
  overwritten. Yank it so new lock files do not pick it up, then publish a fix:

  ```sh
  cargo yank --version X.Y.Z cpm-planner
  # undo: cargo yank --version X.Y.Z --undo cpm-planner
  ```

- **The image job failed or timed out.** The GitHub release and npm are not
  affected. Rebuild the image for the existing release with
  `gh workflow run image.yml --ref main -f tag=vX.Y.Z`, which also runs the
  MCP registry step.
- **A bad image was pushed.** Push the fixed release, which moves `latest` and
  `X.Y`. Delete the bad `X.Y.Z` tag from the GHCR package settings only if it is
  harmful to keep.
- **A bad npm version was published.** npm versions cannot be reused.
  Deprecate it and publish the fixed version:

  ```sh
  npm deprecate @matthew-cochran/cpm@X.Y.Z "broken release, use X.Y.(Z+1)"
  ```

  `npm unpublish` works only within 72 hours and blocks the version number
  for good, so prefer deprecation.
- **A bad MCP registry entry.** Publish the fixed version; the registry lists
  the newest version.