Ctrl K
ComunidadGuías…

RELEASING.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Releasing

This is the canonical procedure for cutting a NimbleBrain release. Follow it literally — commands are copy-pasteable, verification steps have expected outputs. Deviating means you are not following this runbook.

## 1. Versioning rules

Tags use SemVer 2.0.0 with a `v` prefix: `vMAJOR.MINOR.PATCH[-PRERELEASE]`.

| Tag shape | Example | Stability | `:latest` on GHCR? | GitHub Release marked pre-release? |
|---|---|---|---|---|
| No hyphen | `v0.4.0`, `v1.2.3` | Stable / GA | Yes, moves forward | No (gets "Latest" banner) |
| With hyphen | `v0.4.0-beta.1`, `v0.4.0-rc.1`, `v0.4.0-alpha.1` | Pre-release | No | Yes |

The workflow distinguishes using `contains(github.ref_name, '-')`. Do not introduce tag shapes that break this rule (never `v0.4.0.beta1` with a dot, never a GA tag with a hyphen).

**Pre-release sequence.** Use `-beta.N` for validation, `-rc.N` for final sign-off. Increment `N` monotonically within a base version. Do not reset or reuse numbers:

```
v0.4.0-beta.1 → v0.4.0-beta.2 → v0.4.0-rc.1 → v0.4.0
```

If a release is botched, bump to the next number rather than re-tagging — §6.

**`package.json` is a sentinel — do not bump.** It is pinned to `"version": "0.0.0-dev"` and stays there forever. The image is version-agnostic — the version is injected at **deploy time** via the `NB_VERSION` env (the platform chart sets it from the image tag), which is what lets a release promote the staging-verified image by retag instead of rebuilding. Builds without `NB_VERSION` (local dev, non-deployed) fall through to `pkg.version` and self-report `0.0.0-dev`, which is intentionally clearly-not-a-release. Bumping this field per release would re-introduce the drift bug PR #73 fixed.

## 2. Prerequisites

Before cutting a release, verify:

- You are in a clean clone of `NimbleBrainInc/nimblebrain`
- Local `main` fast-forwards cleanly from origin
- The CI workflow on the commit you're about to tag has succeeded (check `gh run list --workflow=ci.yml --branch=main --limit=1`)
- `gh auth status` reports authenticated
- No tag with your target version already exists

Do not cut a release if any of these fail. Fix the underlying issue first.

## 3. Cut the release

Replace `TAG` with your target (e.g. `v0.4.0-beta.3` or `v0.4.0`).

```bash
TAG=v0.4.0-beta.3

# Sync main and sanity-check
git checkout main
git pull origin main --ff-only
git log --oneline -3
git tag -l "$TAG"   # EXPECTED: empty output. Non-empty = tag exists, abort.

# Create an annotated tag — always -a, never lightweight
git tag -a "$TAG" -m "$TAG

One-line summary of what this release delivers or validates.
Optional second paragraph with more detail."

# Push the tag — this triggers .github/workflows/release.yml
git push origin "$TAG"

# Find the run and watch to completion
sleep 5
gh run list --workflow=release.yml --limit 1
# Copy the run ID from the first column, then:
gh run watch <run-id> --exit-status --interval 20
```

Typical workflow duration: 3–5 minutes. If the run fails, go to §6.

## 4. Verify outputs

After the workflow reports success, confirm four things. Run the commands; compare against the expected outputs.

### 4a. GitHub Release

```bash
gh release view "$TAG" --json tagName,isPrerelease,url
```

**Expected:**
- `tagName` matches `$TAG`
- `isPrerelease` is `true` for hyphenated tags, `false` for GA
- `url` is the release page

### 4b. GHCR platform image

The canonical runtime image is `nimblebrain-runtime`; `nimblebrain` is a
transitional alias carrying the same digest (verify it too until the alias is
retired).

```bash
gh api "/orgs/NimbleBrainInc/packages/container/nimblebrain-runtime/versions" \
  --jq ".[] | select(.metadata.container.tags | contains([\"$TAG\"])) | {tags: .metadata.container.tags, created: .created_at}"
```

**Expected:**
- Output contains exactly one entry
- `tags` includes `$TAG` and a 7-char git SHA
- For **stable** releases: `tags` also includes `"latest"`
- For **pre-release** tags: `tags` does NOT include `"latest"`

### 4c. GHCR web image

```bash
gh api "/orgs/NimbleBrainInc/packages/container/nimblebrain-web/versions" \
  --jq ".[] | select(.metadata.container.tags | contains([\"$TAG\"])) | {tags: .metadata.container.tags, created: .created_at}"
```

**Expected:** same pattern as platform image.

### 4d. Anonymous pull works (public OSS contract)

From a shell where you are NOT logged in to GHCR:

```bash
docker logout ghcr.io 2>/dev/null
docker pull "ghcr.io/nimblebraininc/nimblebrain-runtime:$TAG"
docker pull "ghcr.io/nimblebraininc/nimblebrain:$TAG"   # transitional alias
docker pull "ghcr.io/nimblebraininc/nimblebrain-web:$TAG"
```

**Expected:** both pulls succeed. A failure here with "not found" or "denied" for a newly-created image name means §5 is needed.

If any of 4a–4d fail, go to §6.

## 5. One-time visibility flip for new image names

When a *new* image name is first published to GHCR (not a new version of an existing name), GitHub creates the package as **private** by default. The public `docker pull` in §4d will fail until the package is flipped to public.

This is needed exactly once per image name, then never again.

Current image names:
- https://github.com/orgs/NimbleBrainInc/packages/container/nimblebrain-runtime/settings — **NEW** (canonical runtime name); flip to public on its first release
- https://github.com/orgs/NimbleBrainInc/packages/container/nimblebrain/settings — already public (transitional alias)
- https://github.com/orgs/NimbleBrainInc/packages/container/nimblebrain-web/settings — already public

To flip: visit the settings URL → "Danger Zone" section → "Change visibility" → Public → confirm.

**Skip this section** unless §4d fails with an access/not-found error for a newly-introduced image name.

## 6. Rolling back a botched release

If verification fails or the release is broken, tear down everything associated with the tag and cut a new one with the next number. Do not try to fix in place.

```bash
TAG=v0.4.0-beta.3   # the botched tag

# 1. Delete local and remote git tag
git tag -d "$TAG"
git push origin ":refs/tags/$TAG"

# 2. Delete the GitHub Release (also deletes the remote tag if still present)
gh release delete "$TAG" --yes --cleanup-tag

# 3. Delete the GHCR package versions for every image.
#    First, list IDs of versions that carry this tag:
gh api "/orgs/NimbleBrainInc/packages/container/nimblebrain-runtime/versions" \
  --jq ".[] | select(.metadata.container.tags | contains([\"$TAG\"])) | .id"
# Then for each ID:
gh api -X DELETE "/orgs/NimbleBrainInc/packages/container/nimblebrain-runtime/versions/<id>"
# Repeat the two commands above for `nimblebrain` (the alias) and `nimblebrain-web`.

# 4. Delete ECR images for the botched tag (requires AWS CLI + credentials):
aws ecr batch-delete-image \
  --repository-name nimblebrain/nimblebrain-runtime \
  --image-ids "imageTag=$TAG" \
  --region us-east-1
aws ecr batch-delete-image \
  --repository-name nimblebrain/nimblebrain-web \
  --image-ids "imageTag=$TAG" \
  --region us-east-1
```

Then fix the underlying issue on `main` and cut the **next** pre-release number (e.g. if `v0.4.0-beta.3` was botched, next is `v0.4.0-beta.4`). Do not reuse the deleted tag number; issue references and CHANGELOG entries remain readable.

## 7. Workflow reference

`.github/workflows/release.yml` fires on any `v*` tag push (and `workflow_dispatch` with a `tag` input + an EMERGENCY `rebuild` toggle) and runs three jobs:

1. **Promote release images** — **promote-by-retag, not rebuild.** `ci.yml` already built and pushed the staging-verified runtime + web `:<sha>` images to ECR on merge; this re-tags those exact manifests to `:vX.Y.Z` via `aws ecr put-image` (guarded: target repo must be IMMUTABLE, the `:<sha>` source must exist, and a re-run is idempotent — same digest skips, a different digest fails). It then copies the same `:<sha>` manifests to the GHCR tags with `docker buildx imagetools create` (edge never pushes GHCR), and for stable releases moves `:latest`. There is no Verify or rebuild step — `ci.yml` already gated the commit, and the source-image-exists guard proves it. The `workflow_dispatch` `rebuild=true` escape hatch rebuilds from source instead (loud warning: prod won't be byte-for-byte) — only for a tag off a commit that never rode staging.
2. **GitHub Release** — creates a release with auto-generated notes; `prerelease` flag mirrors the hyphen rule
3. **Promote to prod (stable channel)** — GA only; dispatches the deploy-config receiver to record the new prod stable image tag, which the auto-sync per-tenant ApplicationSet then rolls. No-ops when the prod-promote App isn't configured (forks/pre-setup CI).

Artifact map:

| Registry | Repo | Tags on every release | Extra tags for stable only |
|---|---|---|---|
| GHCR (public) | `ghcr.io/nimblebraininc/nimblebrain-runtime` | `$TAG`, short-sha | `latest` |
| GHCR (public) | `ghcr.io/nimblebraininc/nimblebrain` (transitional alias) | `$TAG`, short-sha | `latest` |
| GHCR (public) | `ghcr.io/nimblebraininc/nimblebrain-web` | `$TAG`, short-sha | `latest` |
| ECR (private) | `nimblebrain/nimblebrain-runtime` | `$TAG` | — |
| ECR (private) | `nimblebrain/nimblebrain-web` | `$TAG` | — |

ECR repositories have immutable tags enabled — `:latest` is deliberately not pushed there; deploys pin to version or sha. ECR and GHCR repo names now match (`nimblebrain-runtime`, `nimblebrain-web`). `ghcr.io/nimblebraininc/nimblebrain` is a transitional alias for the runtime image kept so existing OSS pulls don't break; drop it once consumers have moved.

## 8. Hotfixing a released version

Use this when one fix must reach a deployment running `vX.Y.Z` without the rest of what `main` has gained since. The hotfix is that release plus cherry-picks of merged commits, and nothing else.

**The fix lands on `main` first**, by the normal PR, review and merge queue. The hotfix branch only ever carries cherry-picks of commits already on `main`, so the next release from `main` contains every hotfix and nothing has to be merged back.

**A hotfix tag is a pre-release: `vX.Y.(Z+1)-hotfix.N`.** A GA tag would promote the image to every stable deployment and move `:latest`, and this image never rode staging. The hyphen keeps it out of both (§1, §7). It sorts below the `vX.Y.(Z+1)` that `main` cuts later.

```bash
BASE=v0.52.0                 # the release the deployment runs
LINE=hotfix/v0.52            # one branch per minor line
TAG=v0.52.1-hotfix.1         # bump N for each further hotfix on the line
FIX=<merge commit on main>

# 1. Branch from the release (or check out the existing line) and cherry-pick
git fetch origin --tags
git worktree add -b "$LINE" ../hotfix "$BASE"     # existing line: git worktree add -B "$LINE" ../hotfix "origin/$LINE"
cd ../hotfix
git cherry-pick -x "$FIX"

# 2. Verify locally. CI runs only on main and on PRs into it, so nothing else checks this tree.
bun install --frozen-lockfile && (cd web && bun install --frozen-lockfile) && bun run install:platform-apps
bun run verify && bun run test:integration

# 3. Push the branch and an annotated tag
git push -u origin "$LINE"
git tag -a "$TAG" -m "$TAG: $BASE plus <what the fix does> (#<PR>)"
git push origin "$TAG"
```

The tag push starts `release.yml`, and that run **fails by design** at `No staging-built image … it never rode staging-edge`. The guard runs before any image is written, so nothing is half-published. Then build from source:

```bash
# 4. Release with the emergency rebuild
gh workflow run release.yml --ref "$TAG" -f tag="$TAG" -f rebuild=true
gh run list --workflow=release.yml --limit 1        # then: gh run watch <id> --exit-status

# 5. Confirm both images exist in ECR
for repo in nimblebrain-runtime nimblebrain-web; do
  aws ecr describe-images --repository-name "nimblebrain/$repo" --image-ids "imageTag=$TAG" \
    --region us-east-1 --query 'imageDetails[0].imagePushedAt'
done
```

Releasing still deploys nothing (§9). The deployment that needs the fix is pinned to `$TAG` in its deploy configuration, and the pin comes out once a GA release containing the fix reaches that deployment's channel. A pinned deployment does not move when stable does, so an unremoved pin silently holds it back.

## 9. Out of scope for this runbook

These are things you might be tempted to do during a release but should NOT unless explicitly requested:

- **Do not** bump `package.json` version per release. The git tag is the authoritative version; bumping `package.json` adds PR churn without value.
- **Do not** edit CHANGELOG during a release — GitHub auto-generates release notes from merged PR titles.
- **Do not** deploy to production as part of cutting a release. Releasing publishes artifacts; deploying consumes them. Separate concerns, separate runbooks.
- **Do not** push `:latest` manually. The workflow gates this on stable releases; bypassing it defeats the gate.