Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .claude/commands/release-beta.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,13 @@ DIST=$(find packages/opencode/dist -type d -name '*'"$(uname -m | sed s/arm64/ar
# 4. marker guard
bun run script/upstream/analyze.ts --markers --base main --strict

# 5. Local Verdaccio sanity IF docker + a native-platform build are available
# 5. Deterministic preflight (tag collisions, version sanity vs npm, prerelease
# ancestry, release-blocker PRs). For a beta cut from main use it as-is; for a
# branch beta the `base` check will FAIL by design — then rely on the manual
# tag verification in Step 4 instead.
bun script/release-preflight.ts --version X.Y.Z-beta.N --stage tag --allow-prerelease
Comment on lines +94 to +98

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Allow preflight to validate branch beta candidates

When a beta is cut from a supported release branch that does not contain the latest origin/main, this --stage tag invocation always fails because the preflight requires origin/main to be an ancestor of HEAD. The surrounding instructions simultaneously say branch betas are supported, suggest disregarding this failure, and require stopping on any red gate, leaving the operator to either block a valid beta or bypass the supposedly mandatory preflight; add a branch-beta base mode that can pass while retaining the other gates.

Useful? React with 👍 / 👎.


# 6. Local Verdaccio sanity IF docker + a native-platform build are available
# (the docker image is linux; on a mac you cannot cross-build the linux NAPI dist,
# so this validates the current platform only — CI covers the rest):
# (cd packages/dbt-tools && bun run build) && docker compose \
Expand All @@ -108,7 +114,12 @@ The `-beta.N` suffix is what routes to the beta channel. Do NOT omit it.

```bash
BETA_TAG="vX.Y.Z-beta.N"
# Fail closed on collisions — a stale local tag makes `git tag` no-op and the
# push would publish whatever the old tag points at. Never delete-and-recreate.
git rev-parse -q --verify "refs/tags/$BETA_TAG" && { echo "LOCAL TAG EXISTS — STOP"; exit 1; }
git ls-remote origin "refs/tags/$BETA_TAG" | grep -q . && { echo "REMOTE TAG EXISTS — STOP"; exit 1; }
git tag "$BETA_TAG"
test "$(git rev-parse "$BETA_TAG")" = "$(git rev-parse HEAD)" || { echo "TAG MISMATCH — STOP"; exit 1; }
git push origin "$BETA_TAG" # push the TAG (not necessarily main)
```

Expand Down
159 changes: 108 additions & 51 deletions .claude/commands/release.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,12 @@ jobs:
run: bun test
working-directory: script/upstream

- name: Run release preflight tests
# Root bunfig.toml pins `bun test` away from the repo root, so run from
# script/ — same pattern as the marker parser tests above.
run: bun test release-preflight.test.ts
working-directory: script

- name: Check for missing altimate_change markers
run: |
if [[ "${{ github.event_name }}" == "push" ]]; then
Expand Down
81 changes: 69 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,14 +181,58 @@ jobs:

publish-npm:
name: Publish to npm
needs: [build, sanity-verdaccio]
# altimate_change — gate publishing on the test job. Previously publish-npm only
# needed [build, sanity-verdaccio], so npm could publish while typecheck/tests
# were red (found in the 2026-07-22 release retro).
needs: [test, build, sanity-verdaccio]
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: read
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

# altimate_change — validate the tag BEFORE any publish work. Tag-format
# validation previously lived in github-release, which runs AFTER publish-npm,
# so a malformed tag could publish to npm and only fail afterwards.
- name: Validate release tag
run: |
# Strict SemVer: no leading zeros in numeric identifiers, no empty
# prerelease identifiers (rejects v01.2.3, v1.2.3-01, v1.2.3-a..b).
SEMVER_ID='(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)'
if ! echo "$CURRENT_TAG" | grep -qE "^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-${SEMVER_ID}(\.${SEMVER_ID})*)?$"; then
echo "::error::Invalid tag format: $CURRENT_TAG — refusing to publish"
exit 1
fi
# Ask origin directly what the tag points to — the event payload and
# the checkout's local tag ref are both stale if the tag was force-moved
# between the push event and this job. An unforced `git fetch` would
# refuse to clobber the local tag, so ls-remote is the authority.
LS=$(git ls-remote origin "refs/tags/$CURRENT_TAG" "refs/tags/$CURRENT_TAG^{}") || {
echo "::error::Could not query origin for tag $CURRENT_TAG"
exit 1
}
# Annotated tags list a peeled ^{} line pointing at the commit; prefer it.
TAG_SHA=$(echo "$LS" | grep '\^{}' | cut -f1 | head -1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle lightweight tags before the SHA fallback

For the lightweight tags created by git tag v... in both release guides, git ls-remote returns no peeled ^{} row. GitHub Actions runs this step with Bash's -e -o pipefail, so grep exits 1 and makes this assignment terminate the step before the fallback on the next line can run; consequently the new validation blocks every normally created release before npm publishing. Make the absent peeled row nonfatal or select the peeled/base row inside one conditional.

Useful? React with 👍 / 👎.

[ -z "$TAG_SHA" ] && TAG_SHA=$(echo "$LS" | cut -f1 | head -1)
if [ -z "$TAG_SHA" ]; then
echo "::error::Tag $CURRENT_TAG no longer exists on origin — refusing to publish"
exit 1
fi
HEAD_SHA=$(git rev-parse HEAD)
if [ "$TAG_SHA" != "$HEAD_SHA" ]; then
echo "::error::Tag $CURRENT_TAG points at $TAG_SHA on origin but workflow checked out $HEAD_SHA (tag moved since the push event?)"
exit 1
fi
VERSION="${CURRENT_TAG#v}"
if ! grep -q "\[$VERSION\]" CHANGELOG.md && [ "${CURRENT_TAG#*-}" = "$CURRENT_TAG" ]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require a real changelog heading

For a stable release whose version merely appears elsewhere in CHANGELOG.md—for example in prose or a link while the new release heading was omitted—this search succeeds and allows publishing without the required entry. The unescaped dots in $VERSION are also regex wildcards, so even a malformed heading such as [0-9-3] satisfies a 0.9.3 release; match the exact ## [$VERSION] heading instead.

Useful? React with 👍 / 👎.

echo "::error::CHANGELOG.md has no entry for $VERSION — stable releases require a changelog entry"
exit 1
fi
echo "Tag validation passed: $CURRENT_TAG @ $TAG_SHA"
env:
CURRENT_TAG: ${{ github.ref_name }}

- uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2
with:
bun-version: "1.3.14"
Expand Down Expand Up @@ -276,9 +320,19 @@ jobs:
# comment for why this matters. The binary must start without
# walking the workspace for node_modules.
cd "${RUNNER_TEMP:-/tmp}"
env -u NODE_PATH "$BINARY" --version
echo "Pre-publish smoke test passed"
# altimate_change — assert the EXACT version, not just "it starts".
# The 2026-07-22 release retro found a smoke test once validated a
# stale binary (0.7.3) while releasing 0.9.2.
REPORTED=$(env -u NODE_PATH "$BINARY" --version)
EXPECTED="${CURRENT_TAG#v}"
if [ "$REPORTED" != "$EXPECTED" ]; then
echo "::error::Binary reports version '$REPORTED' but tag says '$EXPECTED'"
exit 1
fi
echo "Pre-publish smoke test passed ($REPORTED)"
fi
env:
CURRENT_TAG: ${{ github.ref_name }}

- name: Publish to npm
run: bun run packages/opencode/script/publish.ts
Expand Down Expand Up @@ -321,15 +375,18 @@ jobs:

# Get the previous tag.
# altimate_change — pick the newest STABLE tag that is an ANCESTOR of this
# commit, excluding the current tag and any prerelease (-beta etc). Plain
# `--sort=-version:refname | head -2 | tail -1` picked the 2nd-highest tag by
# name, which for a stable release lands on a prerelease or a stray/divergent
# tag (e.g. a dangling v0.9.0), producing a bogus compare range. Restricting to
# `--merged HEAD` non-prerelease tags yields the real previous release.
PREV_TAG=$(git tag --merged HEAD --sort=-version:refname \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| grep -vx "$CURRENT_TAG" \
| head -1)
# commit AND strictly LOWER than the current tag. Excluding only equality is
# not enough: if upstream history is ever merged, fork-inherited tags (e.g.
# v1.18.3) would beat the real previous release for a v0.9.x target and the
# compare range would silently omit history.
PREV_TAG=""
for t in $(git tag --merged HEAD --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$'); do
if [ "$t" != "$CURRENT_TAG" ] && \
[ "$(printf '%s\n%s\n' "$t" "$CURRENT_TAG" | sort -V | head -1)" = "$t" ]; then
PREV_TAG="$t"
break
fi
done

# Generate changelog from commits between tags
echo "## What's Changed" > notes.md
Expand Down
60 changes: 45 additions & 15 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,22 @@ The version is injected into the binary via esbuild defines at compile time.

## Release Process

### 1. Update CHANGELOG.md
### 1. Run preflight (before touching anything)

**MANDATORY** — the deterministic gate. Run it while the tree is still clean;
it fails on a dirty worktree by design:

```bash
# Checks: clean tree, HEAD == fresh origin/main, no tag collisions (local OR
# remote), version sanity vs npm, prerelease-line ancestry, release-blocker
# PRs, PREV_TAG dry-run, marker guard.
bun script/release-preflight.ts --version 0.5.0 --stage pre
```

Do NOT proceed if any check fails. If it reports a tag collision, resolve it
explicitly — never delete-and-recreate a tag blind.

### 2. Update CHANGELOG.md

Add a new section at the top of `CHANGELOG.md`:

Expand All @@ -48,42 +63,57 @@ Add a new section at the top of `CHANGELOG.md`:
- ...
```

### 2. Run pre-release sanity check
### 3. Run pre-release sanity check

**MANDATORY** — this catches broken binaries before they reach users:

```bash
cd packages/opencode
bun run pre-release
(cd packages/opencode && OPENCODE_VERSION=0.5.0 bun run pre-release)
```

This verifies:
`pre-release` verifies:
- All required NAPI externals are in `package.json` dependencies
- They're installed in `node_modules`
- A local build produces a binary that actually starts

Do NOT proceed if any check fails.

### 3. Commit and tag
### 4. Commit, re-preflight, tag, push

Stage files **individually** — never `git add -A` (release worktrees carry
scratch files that must not ship in the release commit). Releasing does not
require being literally on the `main` branch: the invariant is that a clean
HEAD equals freshly fetched `origin/main` (worktrees push via `HEAD:main`).

```bash
git add -A
git add CHANGELOG.md <other release files>
git commit -m "release: v0.5.0"

# Re-run preflight now that the release commit exists. Stage `tag` allows
# local commits ahead of origin/main (the tree must be clean again).
bun script/release-preflight.ts --version 0.5.0 --stage tag || exit 1

git tag v0.5.0
git push origin main v0.5.0
# Verify the tag points at HEAD BEFORE pushing — a pre-existing tag makes
# `git tag` silently fail, and pushing a stale tag publishes old code.
test "$(git rev-parse v0.5.0)" = "$(git rev-parse HEAD)" || exit 1
# Atomic: branch and tag land together or not at all.
git push --atomic origin HEAD:main v0.5.0
```

### 4. What happens automatically
### 5. What happens automatically

The `v*` tag triggers `.github/workflows/release.yml` which:

1. **Builds** all platform binaries (linux/darwin/windows, x64/arm64)
2. **Publishes to npm** — platform-specific binary packages + wrapper package
3. **Creates GitHub Release** — with auto-generated release notes and binary attachments
4. **Updates AUR** — pushes PKGBUILD update to `altimate-code-bin`
5. **Publishes Docker image** — to `ghcr.io/altimateai/altimate-code`
1. **Runs release-critical tests** — typecheck + branding/install tests (gates npm publish)
2. **Builds** all platform binaries (linux/darwin/windows, x64/arm64)
3. **Validates the tag** — format, tag-SHA == checked-out SHA, CHANGELOG entry present (before any publish)
4. **Publishes to npm** — platform-specific binary packages + wrapper package
5. **Creates GitHub Release** — with auto-generated release notes and binary attachments
6. **Publishes Docker image (best-effort)** — to `ghcr.io/altimateai/altimate-code`; failures are logged but do NOT fail the release, so verify manually if you need the image
7. ~~Updates AUR~~ — currently **disabled** (the workflow step is commented out; see `publish.ts` for setup steps to re-enable)

### 5. Verify
### 6. Verify

After the workflow completes:

Expand Down
Loading
Loading