chore(release): v0.12.0 (#1327) #122
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| concurrency: | |
| group: release | |
| cancel-in-progress: false | |
| env: | |
| GH_REPO: AltimateAI/altimate-code | |
| jobs: | |
| test: | |
| name: Test | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2 | |
| with: | |
| bun-version: "1.3.14" | |
| - name: Cache Bun dependencies | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} | |
| restore-keys: | | |
| bun-${{ runner.os }}- | |
| - name: Configure git for tests | |
| run: | | |
| git config --global user.name "CI" | |
| git config --global user.email "ci@test.local" | |
| - name: Install dependencies | |
| run: bun install | |
| - name: Typecheck | |
| run: bun turbo typecheck | |
| - name: Run release-critical tests | |
| run: bun test --timeout 30000 test/branding/ test/install/ | |
| working-directory: packages/opencode | |
| build: | |
| name: Build (${{ matrix.name }}) | |
| # Runs in PARALLEL with test — publish waits for both | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # Each target builds ONE binary in parallel (~5 min each) | |
| # Index matches allTargets array in build.ts. Targets without a | |
| # @altimateai/altimate-core NAPI prebuild (linux-*-musl, win32-arm64) | |
| # are intentionally excluded — see allTargets in build.ts. | |
| - { index: 0, name: "linux-arm64" } | |
| - { index: 1, name: "linux-x64" } | |
| - { index: 2, name: "linux-x64-baseline" } | |
| - { index: 3, name: "darwin-arm64" } | |
| - { index: 4, name: "darwin-x64" } | |
| - { index: 5, name: "darwin-x64-baseline" } | |
| - { index: 6, name: "win32-x64" } | |
| - { index: 7, name: "win32-x64-baseline" } | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2 | |
| with: | |
| bun-version: "1.3.14" | |
| - name: Cache Bun dependencies | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} | |
| restore-keys: | | |
| bun-${{ runner.os }}- | |
| - name: Install dependencies | |
| run: bun install | |
| - name: Build ${{ matrix.name }} | |
| run: bun run packages/opencode/script/build.ts --target-index=${{ matrix.index }} | |
| env: | |
| OPENCODE_VERSION: ${{ github.ref_name }} | |
| # altimate_change — derive channel from the tag: a prerelease tag (e.g. v0.9.0-beta.1) | |
| # publishes to the npm `beta` dist-tag so existing `latest` users are NOT auto-upgraded; | |
| # a plain tag (v0.9.0) goes to `latest`. Prevents a beta tag from bricking the whole user base. | |
| OPENCODE_CHANNEL: ${{ contains(github.ref_name, '-') && 'beta' || 'latest' }} | |
| OPENCODE_RELEASE: "1" | |
| # altimate_change — embed the operator-controlled Base endpoint without publishing it in source | |
| ALTIMATE_BASE_GATEWAY_URL: ${{ vars.ALTIMATE_BASE_GATEWAY_URL }} | |
| GH_REPO: ${{ env.GH_REPO }} | |
| # altimate_change — MODELS_DEV_API_JSON is deliberately NOT set here. | |
| # Pointing it at test/tool/fixtures/models-api.json (as ci.yml does, where | |
| # a hermetic build is correct) made every shipped binary embed that fixture | |
| # as its bundled models.dev catalog — newest entry 2026-03-30. Release | |
| # builds fetch models.dev live, matching upstream's publish.yml. build.ts | |
| # validates the payload and fails the build if it is unusable. | |
| # Smoke-test: verify the compiled binary actually starts. | |
| # Only possible for native linux-x64 builds on the ubuntu runner. | |
| # This catches missing externals (e.g. @altimateai/altimate-core) | |
| # that compile fine but crash at runtime. | |
| - name: Smoke test binary | |
| if: matrix.name == 'linux-x64' | |
| id: smoke-test | |
| run: | | |
| # Resolve to an absolute path before we cd away from the workspace. | |
| # Test `altimate-code` — the binary the platform package actually ships | |
| # (the redundant `altimate` copy is dropped from the npm package after the | |
| # release archive is built, to stay under npm's tarball size limit). | |
| BINARY=$(find "$(pwd)/packages/opencode/dist" -name altimate-code -type f | head -1) | |
| if [ -z "$BINARY" ]; then | |
| echo "::error::No binary found in dist/" | |
| exit 1 | |
| fi | |
| chmod +x "$BINARY" | |
| # altimate_change — share the resolved path with the cold-start step below so a dist-layout | |
| # change can't make this step pass while the other fails on a stale hardcoded path. | |
| echo "binary=$BINARY" >> "$GITHUB_OUTPUT" | |
| # Run with NO pre-set NODE_PATH AND from a directory with no | |
| # node_modules anywhere upward. Bun's compiled binary would | |
| # otherwise walk the workspace tree for node_modules and resolve | |
| # altimate-core that way — the test would pass for the wrong | |
| # reason if the staged shim ever silently misses. | |
| cd "${RUNNER_TEMP:-/tmp}" | |
| env -u NODE_PATH "$BINARY" --version | |
| echo "Smoke test passed: standalone binary starts hermetically" | |
| # altimate_change start — --version and PURE-mode tests never exercise ordinary config installs. | |
| - name: Cold-start config regression (compiled, non-PURE) | |
| if: matrix.name == 'linux-x64' | |
| working-directory: packages/opencode | |
| env: | |
| # Reuse the path the smoke test just resolved via `find` instead of a second hardcoded | |
| # copy of it — a dist-layout change would otherwise break one of these two steps silently. | |
| OPENCODE_TEST_CLI: ${{ steps.smoke-test.outputs.binary }} | |
| run: bun test test/cli/serve/fresh-start.test.ts --timeout 90000 | |
| # altimate_change end | |
| - name: Upload build artifact | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: dist-${{ matrix.name }} | |
| path: packages/opencode/dist/ | |
| compression-level: 1 | |
| # --------------------------------------------------------------------------- | |
| # Verdaccio sanity suite — tests the real `npm install -g` flow BEFORE | |
| # publishing to npm. Catches broken symlinks, missing files, postinstall | |
| # failures, and dependency resolution issues that smoke tests miss. | |
| # --------------------------------------------------------------------------- | |
| sanity-verdaccio: | |
| name: Sanity (Verdaccio) | |
| needs: build | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2 | |
| with: | |
| bun-version: "1.3.14" | |
| - name: Cache Bun dependencies | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} | |
| restore-keys: | | |
| bun-${{ runner.os }}- | |
| - name: Install dependencies | |
| run: bun install | |
| - name: Download linux-x64 build artifact | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: dist-linux-x64 | |
| path: packages/opencode/dist/ | |
| - name: Build dbt-tools | |
| run: bun run build | |
| working-directory: packages/dbt-tools | |
| - name: Run Verdaccio sanity suite | |
| run: | | |
| docker compose -f test/sanity/docker-compose.verdaccio.yml up \ | |
| --build --abort-on-container-exit --exit-code-from sanity | |
| env: | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| publish-npm: | |
| name: Publish to npm | |
| # 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) | |
| [ -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 | |
| 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" | |
| - name: Cache Bun dependencies | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} | |
| restore-keys: | | |
| bun-${{ runner.os }}- | |
| - name: Install dependencies | |
| run: bun install | |
| - name: Build dbt-tools (bundled with CLI) | |
| run: bun run build | |
| working-directory: packages/dbt-tools | |
| - name: Smoke test dbt-tools bundle | |
| run: | | |
| # Verify node_python_bridge.py was copied into dist | |
| if [ ! -f packages/dbt-tools/dist/node_python_bridge.py ]; then | |
| echo "::error::node_python_bridge.py missing from dbt-tools dist" | |
| exit 1 | |
| fi | |
| # Verify no hardcoded absolute path in __dirname (catches any CI runner OS) | |
| if grep -qE 'var __dirname\s*=\s*"(/|[A-Za-z]:\\)' packages/dbt-tools/dist/index.js; then | |
| echo "::error::dbt-tools bundle contains hardcoded absolute path in __dirname" | |
| exit 1 | |
| fi | |
| # Verify __dirname was patched to runtime resolution | |
| if ! grep -q 'import.meta.dirname' packages/dbt-tools/dist/index.js; then | |
| echo "::error::dbt-tools bundle missing import.meta.dirname patch" | |
| exit 1 | |
| fi | |
| echo "dbt-tools smoke test passed" | |
| - name: Free disk space for artifact download + npm publish | |
| run: | | |
| sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/share/boost | |
| df -h / | |
| - name: Download all build artifacts | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| pattern: "dist-*" | |
| path: packages/opencode/dist/ | |
| merge-multiple: true | |
| - name: Configure npm auth | |
| run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > "$RUNNER_TEMP/.npmrc" | |
| env: | |
| NPM_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| # TODO: Uncomment when AUR publishing is enabled (see publish.ts for setup steps) | |
| # - name: Configure SSH for AUR | |
| # if: ${{ !contains(github.ref_name, '-') }} | |
| # run: | | |
| # mkdir -p ~/.ssh | |
| # echo "$AUR_SSH_PRIVATE_KEY" > ~/.ssh/aur | |
| # chmod 600 ~/.ssh/aur | |
| # ssh-keyscan -t ed25519 aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null | |
| # cat >> ~/.ssh/config << 'EOF' | |
| # Host aur.archlinux.org | |
| # IdentityFile ~/.ssh/aur | |
| # User aur | |
| # EOF | |
| # env: | |
| # AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} | |
| # Smoke-test a linux-x64 binary from the downloaded artifacts before publishing. | |
| # This is the last gate before npm publish — catches runtime crashes that | |
| # compile-time checks miss (e.g. missing NAPI externals like v0.5.10). | |
| - name: Pre-publish smoke test | |
| run: | | |
| # Resolve to an absolute path before we cd away from the workspace. | |
| BINARY=$(find "$(pwd)/packages/opencode/dist" -path '*altimate-code-linux-x64/bin/altimate-code' -type f | head -1) | |
| if [ -z "$BINARY" ]; then | |
| echo "::error::No linux-x64 binary found in artifacts — cannot verify release" | |
| exit 1 | |
| else | |
| chmod +x "$BINARY" | |
| # No NODE_PATH AND hermetic cwd: see the build-time smoke test | |
| # comment for why this matters. The binary must start without | |
| # walking the workspace for node_modules. | |
| cd "${RUNNER_TEMP:-/tmp}" | |
| # 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 | |
| env: | |
| OPENCODE_VERSION: ${{ github.ref_name }} | |
| # altimate_change — derive channel from the tag: a prerelease tag (e.g. v0.9.0-beta.1) | |
| # publishes to the npm `beta` dist-tag so existing `latest` users are NOT auto-upgraded; | |
| # a plain tag (v0.9.0) goes to `latest`. Prevents a beta tag from bricking the whole user base. | |
| OPENCODE_CHANNEL: ${{ contains(github.ref_name, '-') && 'beta' || 'latest' }} | |
| OPENCODE_RELEASE: "1" | |
| NPM_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| NPM_CONFIG_USERCONFIG: ${{ runner.temp }}/.npmrc | |
| GH_REPO: ${{ env.GH_REPO }} | |
| GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} | |
| # Python engine (publish-engine) job removed — engine eliminated. | |
| # All methods now run natively in TypeScript. | |
| github-release: | |
| name: Create GitHub Release | |
| needs: [build, publish-npm] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Generate release notes | |
| id: notes | |
| run: | | |
| # Validate tag format to prevent injection | |
| if ! echo "$CURRENT_TAG" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$'; then | |
| echo "::error::Invalid tag format: $CURRENT_TAG" | |
| exit 1 | |
| fi | |
| # Get the previous tag. | |
| # altimate_change — pick the newest STABLE tag that is an ANCESTOR of this | |
| # 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 | |
| echo "" >> notes.md | |
| if [ -n "$PREV_TAG" ]; then | |
| # Categorize commits | |
| echo "### Features" >> notes.md | |
| git log "${PREV_TAG}".."${CURRENT_TAG}" --pretty=format:"- %s (%h)" --grep="^feat" >> notes.md || true | |
| echo "" >> notes.md | |
| echo "" >> notes.md | |
| echo "### Bug Fixes" >> notes.md | |
| git log "${PREV_TAG}".."${CURRENT_TAG}" --pretty=format:"- %s (%h)" --grep="^fix" >> notes.md || true | |
| echo "" >> notes.md | |
| echo "" >> notes.md | |
| echo "### Other Changes" >> notes.md | |
| git log "${PREV_TAG}".."${CURRENT_TAG}" --pretty=format:"- %s (%h)" --invert-grep --grep="^feat" --grep="^fix" >> notes.md || true | |
| echo "" >> notes.md | |
| else | |
| echo "Initial release" >> notes.md | |
| fi | |
| echo "" >> notes.md | |
| echo "### Install" >> notes.md | |
| echo '```bash' >> notes.md | |
| echo "npm install -g @altimateai/altimate-code@${CURRENT_TAG#v}" >> notes.md | |
| echo "# or" >> notes.md | |
| echo "brew install AltimateAI/tap/altimate-code" >> notes.md | |
| echo '```' >> notes.md | |
| echo "" >> notes.md | |
| echo "**Full Changelog**: https://github.com/${GH_REPO}/compare/${PREV_TAG}...${CURRENT_TAG}" >> notes.md | |
| env: | |
| GH_REPO: ${{ env.GH_REPO }} | |
| CURRENT_TAG: ${{ github.ref_name }} | |
| - name: Download all build artifacts | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| pattern: "dist-*" | |
| path: packages/opencode/dist/ | |
| merge-multiple: true | |
| - name: Generate checksums | |
| # Single checksums.txt (sha256sum format: "<hash> <bare-filename>") shipped | |
| # as a release asset. The curl and PowerShell installers fetch it and verify | |
| # the downloaded archive before extracting. | |
| working-directory: packages/opencode/dist | |
| run: sha256sum *.tar.gz *.zip > checksums.txt | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2 | |
| with: | |
| body_path: notes.md | |
| draft: false | |
| prerelease: ${{ contains(github.ref_name, '-') }} | |
| files: | | |
| packages/opencode/dist/*.tar.gz | |
| packages/opencode/dist/*.zip | |
| packages/opencode/dist/checksums.txt | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |