# OpenLife — Release Process

> The pattern used for v1.2 → v1.6. Each milestone is a single branch
> opened from `main`, with one atomic commit per closed story, merged
> via PR and tagged on the merge commit.

## 1. Open the milestone branch

```bash
git checkout main
git pull --ff-only origin main
git checkout -b feat/v<X.Y>-<theme>
```

Naming: `feat/v1.6-sandbox-rollout`, `feat/v1.5-evaluation`,
`feat/v1.4-tenten`. The theme is one short hyphenated phrase, not a
sprint or epic number.

## 2. Execute stories as atomic commits

Each story = **one commit**. Commit message format:

```
<type>(<scope>): <short summary> (Story <N>, v<X.Y>)
```

Types in use: `feat`, `fix`, `chore`, `docs`, `test`, `refactor`.
Examples from the actual history:

```
feat(doctor): ProcessSandbox runtime check (Story 18.1, v1.6)
feat(executor): OPENLIFE_PROCESS_SANDBOX opt-in for runShellCommand (Story 18.2, v1.6)
chore(types): any 109 → 83 across Gatekeeper, TestHarness, VoiceManager, admin (Story 19.1, v1.6)
docs(v1.6): consolidated changelog with sprint/story/commit map (Cap)
```

Co-Author trailer is required:

```
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
```

## 3. Per-commit guardrails (locally)

Before each story commit:

```bash
npm run build               # tsc clean — strict, no any leak
npm run test:<story>        # the test added for the story
```

Before merging the milestone:

```bash
npm run test:all            # the full canonical suite (~5 min)
```

`prepublishOnly` runs `test:all` automatically, so publish is gated.

## 4. Write the changelog

One file per milestone: `docs/v<X.Y>-changelog.md`. Sections:

- **Header** — branch name, predecessor tag, one-paragraph thesis.
- **What landed by epic** — sprint × epic × commits table.
- **Per-epic stories** — table mapping story id → commit sha → surface
  touched → outcome.
- **Calendar-gated deferrals** — if any (e.g., Story 13.5).
- **Locked decisions** — explicit defaults that stay opt-in.
- **Tests added** — markers count, what each new `test_*.ts` covers.
- **Honest scorecard delta** — vs the previous milestone, no
  inflation.

The roadmap file (`docs/v<X.Y>-roadmap.md`) is **optional** and only
needed when a milestone is multi-sprint with non-obvious sequencing.

## 5. Open the PR

```bash
gh pr create --base main --title "<X.Y>: <theme>" \
  --body "$(cat <<'EOF'
## Summary
- <one bullet per epic>

## Verification
- [x] npm run test:all green (<N> markers)
- [x] docs/v<X.Y>-changelog.md committed
- [x] No new env vars introduced without docs

🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
```

## 6. Merge and tag

After PR approval:

```bash
gh pr merge <number> --merge      # not squash — we keep the per-story commits
git checkout main
git pull --ff-only origin main
git tag -a v<X.Y>.0 -m "v<X.Y>.0 — <theme>"
git push origin v<X.Y>.0
```

Tag the **merge commit**, not the last feature commit. That keeps the
tag pointing at a stable PR boundary.

## 7. Maintenance cuts (rare)

Used when a calendar-gate requires a second tag on a line that has
already moved on. Pattern (see `v1.4.1`):

```bash
git worktree add /tmp/openlife-v<X.Y>.<Z> v<X.Y>.0
cd /tmp/openlife-v<X.Y>.<Z>
git checkout -b maint/v<X.Y>.<Z>
# touch docs/v<X.Y>-changelog.md with the maintenance section
git commit -am "docs(maint): v<X.Y>.<Z> lineage marker — <reason>"
git tag -a v<X.Y>.<Z> -m "..."
git push origin maint/v<X.Y>.<Z> v<X.Y>.<Z>
cd -
git worktree remove /tmp/openlife-v<X.Y>.<Z>
```

The worktree avoids disrupting any in-progress work on the main
development branch.

## 8. npm publish (via GitHub Actions — Story R.2)

OpenLife publishes to npm via `.github/workflows/publish.yml`. The
workflow runs on every `v*.*.*` tag push and can also be fired
manually via `workflow_dispatch` for retro-publishing older tags.

### One-time setup (per repo)

The `NPM_TOKEN` GitHub secret must exist before any publish can
succeed.

1. **Create an npm account** if you don't have one
   (https://www.npmjs.com/signup). Enable 2FA.
2. **Generate a Granular Access Token:**
   - npm web → Account → Access Tokens → Generate New Token →
     **Granular Access Token**
   - Allowed Packages: `@openlife/*`
   - Permissions: `Read and Write`
   - Expiration: ≤365 days (90 days recommended; rotate quarterly)
   - **Copy the token immediately** — it's only shown once.
3. **Register the token as a GitHub secret:**
   - https://github.com/GOOODZ/openlife-core/settings/secrets/actions
   - New repository secret → Name: `NPM_TOKEN` → Value: the token
   - **Never paste tokens into chat, code, commits, or logs.** If
     a token leaks (committed, posted in chat, exposed in a build
     log), revoke it on npm immediately and generate a replacement.

### Per-release publish flow

For tags created **after** the workflow lands (v1.7.0 and beyond):

1. Cut and push the tag as documented in step 6 of this runbook.
2. The `Publish to npm` workflow fires automatically.
3. Track progress at
   https://github.com/GOOODZ/openlife-core/actions/workflows/publish.yml.
4. On success, verify with `npm view @openlife/cli@<version>`.

For tags created **before** the workflow existed (v1.4.0, v1.4.1,
v1.5.0, v1.6.0):

1. https://github.com/GOOODZ/openlife-core/actions/workflows/publish.yml
   → Run workflow → enter version (e.g. `1.6.0`) → Run.
2. The workflow checks out the corresponding `v<version>` tag,
   builds, runs `prepublishOnly` (= `test:all`), and publishes.

### Version contract

The version embedded in `package.json` on `main` intentionally stays
at `1.0.0`. The workflow uses `npm version --no-git-tag-version` at
runtime to set the tarball's version from the git tag. This avoids
the per-release `package.json` bump commit on `main`.

### Verification after a successful publish

```bash
npm view @openlife/cli versions                # list all published
npm view @openlife/cli@1.6.0                   # metadata for one
npm audit signatures @openlife/cli@1.6.0       # verify provenance
```

### If a publish fails

| Error | Cause | Remedy |
|---|---|---|
| 403 Forbidden | Token lacks scope `@openlife/*` | Re-issue token; update `NPM_TOKEN` secret |
| 402 Payment Required | Scoped pkg default-private | `--access=public` (already in workflow) |
| 409 Conflict | Version already published | Bump and re-tag |
| `prepublishOnly` fails | A test in `test:all` doesn't pass on CI runners | Reproduce locally on Node 20, fix the test, push a maintenance tag |
| ENEEDAUTH | `NPM_TOKEN` secret missing or wrong name | Recreate the GitHub secret with the exact name `NPM_TOKEN` |

## 9. Registry announce (post R.1)

Once `RemoteCatalogClient` and the canonical registry ship, this step
will write a manifest pointer for the new milestone. Today this is
NOT required to release — the milestone is fully usable from npm and
GitHub without it.

## Rollback

If a tagged release is broken:

1. **Do not delete the tag remotely.** A pushed tag is a public
   contract; deletion creates lineage confusion across consumers.
2. Cut a maintenance tag with the fix (`v<X.Y>.<Z+1>`) following
   step 7.
3. Update the milestone changelog with a "Known issue / superseded
   by" callout near the affected story.

For pre-tag failures (build broken on merge):

1. `git revert <merge-commit>` on `main` via a new PR.
2. Re-tag is allowed only if the original tag was **not** pushed yet
   (purely local). Otherwise, follow the cut-forward pattern above.

## Quick reference

| Step | Owner | Command |
|---|---|---|
| Branch | dev | `git checkout -b feat/v<X.Y>-<theme>` |
| Per-story commit | dev | one commit + matching `test_*.ts` |
| Full suite | dev | `npm run test:all` |
| Changelog | dev | edit `docs/v<X.Y>-changelog.md` |
| PR | devops | `gh pr create --base main` |
| Merge | devops | `gh pr merge --merge` |
| Tag | devops | `git tag -a v<X.Y>.0 && git push origin v<X.Y>.0` |
| Maintenance | devops | worktree + `maint/v<X.Y>.<Z>` |
| npm publish | human | `npm login` then `npm publish` |
