Skip to content

release: unblock v0.21.5 pipeline + harden supply chain #86

release: unblock v0.21.5 pipeline + harden supply chain

release: unblock v0.21.5 pipeline + harden supply chain #86

Workflow file for this run

name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
id-token: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
# Pipeline shape (each stage gates the next via `needs:`):
#
# test, test-pi, test-cli (unit tests, parallel)
#
# e2e-opencode, e2e-pi (Docker install + smoke, parallel — gated by unit tests)
#
# e2e-host-opencode, e2e-host-pi (host behavior suite from packages/e2e-tests, parallel — gated by Docker)
#
# publish-npm, publish-npm-pi, publish-npm-cli (npm publishes, parallel — gated by host e2e)
#
# github-release (GH release tag — gated by all publishes)
#
# discord-announce (Discord post — gated by github-release)
#
# Two e2e layers cover different concerns:
# - Docker e2e: fresh-install smoke (plugin loads, doctor clean, one mock turn writes DB
# rows under the cortexkit path). Catches packaging / install-flow regressions.
# - Host e2e: behavior suite with byte-level wire assertions, multi-turn cache stability,
# historian publish behavior, tag-owner collision, synthetic todowrite, cross-harness
# memory, etc. Spawns real `opencode serve` / Pi subprocesses against the embedded
# mock provider. Catches cache-stability + correctness regressions that the smoke
# layer cannot see. Adding it to the release gate means a tag push that breaks any of
# the ~74 host tests will block npm publishes.
#
# Prior to v0.21.x the Docker e2e ran in a separate workflow (e2e-docker.yml) on the
# same tag push, so publishes could complete even if e2e failed. Folding both e2e
# layers inline closes that gap.
#
# Discord announcement also lives inline: GitHub Actions suppresses
# workflow cascades from `GITHUB_TOKEN`-authored events, so a separate
# `on: release: published` workflow never fires when `softprops/action-gh-release`
# publishes the release. discord-release.yml is kept as a manual override
# (workflow_dispatch only) for re-announcements and external releases.
jobs:
test:
name: Test (plugin)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: TypeScript typecheck
run: bun run typecheck
- name: Lint
run: bun run lint
- name: Build
run: bun run build
- name: Test
run: bun run test
test-pi:
name: Test (pi-plugin)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: TypeScript typecheck
run: bun run --cwd packages/pi-plugin typecheck
- name: Lint
run: bun run --cwd packages/pi-plugin lint
- name: Build
run: bun run --cwd packages/pi-plugin build
- name: Test
run: bun run --cwd packages/pi-plugin test
test-cli:
name: Test (cli)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: TypeScript typecheck
run: bun run --cwd packages/cli typecheck
- name: Lint
run: bun run --cwd packages/cli lint
- name: Build
run: bun run --cwd packages/cli build
- name: Test
run: bun run --cwd packages/cli test
e2e-opencode:
name: E2E (OpenCode, Docker)
runs-on: ubuntu-latest
needs: [test, test-cli]
timeout-minutes: 25
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install workspace deps
run: bun install --frozen-lockfile
- name: Build OpenCode plugin
run: bun run --cwd packages/plugin build
- name: Build CLI
# The CLI is its own package (@cortexkit/magic-context) since
# v0.16.1; Dockerfile.opencode COPYs packages/cli/dist/ in.
run: bun run --cwd packages/cli build
- name: Build E2E image
run: |
docker build \
--platform linux/amd64 \
-f tests/docker/Dockerfile.opencode \
-t mc-e2e-opencode \
.
- name: Run E2E
run: docker run --rm --platform linux/amd64 mc-e2e-opencode
e2e-pi:
name: E2E (Pi, Docker)
runs-on: ubuntu-latest
needs: [test-pi, test-cli]
timeout-minutes: 25
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install workspace deps
run: bun install --frozen-lockfile
- name: Build Pi plugin
run: bun run --cwd packages/pi-plugin build
- name: Build CLI
# The CLI moved to its own package (@cortexkit/magic-context) in
# v0.16.1. Dockerfile.pi COPYs packages/cli/dist/ in for the
# `magic-context doctor --harness pi` test invocation.
run: bun run --cwd packages/cli build
- name: Build E2E image
# The Pi Dockerfile installs runtime deps fresh inside the image
# (better-sqlite3 builds against linux/amd64), so no host-side
# `npm install` is needed.
run: |
docker build \
--platform linux/amd64 \
-f tests/docker/Dockerfile.pi \
-t mc-e2e-pi \
.
- name: Run E2E
run: docker run --rm --platform linux/amd64 mc-e2e-pi
e2e-host-opencode:
name: E2E (OpenCode, host behavior)
runs-on: ubuntu-latest
# Gated on Docker e2e: no point exercising the deep behavior suite if
# the simpler install+smoke path is broken.
needs: [e2e-opencode]
timeout-minutes: 40
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install workspace deps
run: bun install --frozen-lockfile
# Install opencode the same way the Docker image does — the host
# suite spawns `opencode serve` from PATH.
- name: Install opencode
# NOTE: pinned to 1.15.4 because opencode 1.15.5 on linux-amd64
# never responds to HTTP /doc after stdout reports "server listening".
# See ci.yml for the same pin and details.
run: |
curl -fsSL https://opencode.ai/install | bash -s -- --version 1.15.4
echo "$HOME/.opencode/bin" >> "$GITHUB_PATH"
- name: Verify opencode on PATH
run: opencode --version
- name: Build OpenCode plugin
# Host tests spawn `opencode serve` with a file:// plugin
# specifier pointing at packages/plugin/, so dist must exist.
run: bun run --cwd packages/plugin build
# Strip inherited NODE_ENV=test so the spawned opencode subprocess
# gets the same logging + runtime behavior as a normal local run
# (documented in CONTRIBUTING / project memory).
- name: Run host e2e suite (OpenCode tests only)
env:
NODE_ENV: ""
run: |
cd packages/e2e-tests
# Match every test file except pi-*.test.ts.
files=$(ls tests/*.test.ts | grep -v "/pi-" | tr '\n' ' ')
echo "Running OpenCode host tests: $files"
bun test --timeout 600000 $files
e2e-host-pi:
name: E2E (Pi, host behavior)
runs-on: ubuntu-latest
needs: [e2e-pi]
timeout-minutes: 40
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
# Pi tests resolve the Pi binary via createRequire against
# @earendil-works/pi-coding-agent, which is a workspace dep of
# packages/pi-plugin.
- name: Install workspace deps
run: bun install --frozen-lockfile
# pi-cross-harness.test.ts spawns BOTH a Pi runner and an OpenCode
# serve to verify cross-harness memory sharing, so this job needs
# opencode on PATH too — same install path the OpenCode host job uses.
- name: Install opencode
run: |
curl -fsSL https://opencode.ai/install | bash
echo "$HOME/.opencode/bin" >> "$GITHUB_PATH"
- name: Verify opencode on PATH
run: opencode --version
- name: Build Pi plugin
run: bun run --cwd packages/pi-plugin build
# pi-cross-harness also instantiates the OpenCode harness, which
# spawns `opencode serve` with a file:// plugin specifier pointing
# at packages/plugin/. That dist must exist.
- name: Build OpenCode plugin
run: bun run --cwd packages/plugin build
- name: Run host e2e suite (Pi tests only)
env:
NODE_ENV: ""
run: |
cd packages/e2e-tests
bun test --timeout 600000 tests/pi-*.test.ts
publish-npm:
name: Publish plugin to npm
runs-on: ubuntu-latest
# Publishes are gated on the full e2e suite (OpenCode + Pi) so a tag
# push can never produce a published npm package whose runtime fails
# the install + first-turn smoke test.
needs: [test, test-pi, test-cli, e2e-opencode, e2e-pi, e2e-host-opencode, e2e-host-pi]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Ensure latest npm (for trusted publishing)
run: npm install -g npm@latest
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Sync version from tag
run: node scripts/version-sync.mjs --from-tag
- name: Build plugin
run: bun run --cwd packages/plugin build
- name: Generate schema
run: bun packages/plugin/scripts/build-schema.ts
- name: Copy README for npm package
run: cp README.md packages/plugin/README.md
# Uses npm Trusted Publishing (OIDC) — configured on npmjs.com
- name: Publish to npm
run: npm publish --access public --provenance
working-directory: packages/plugin
publish-npm-pi:
name: Publish pi-plugin to npm
runs-on: ubuntu-latest
needs: [test, test-pi, test-cli, e2e-opencode, e2e-pi, e2e-host-opencode, e2e-host-pi]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Ensure latest npm (for trusted publishing)
run: npm install -g npm@latest
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Sync version from tag
run: node scripts/version-sync.mjs --from-tag
- name: Build pi-plugin
run: bun run --cwd packages/pi-plugin build
# Uses npm Trusted Publishing (OIDC) — must be configured on npmjs.com
# for @cortexkit/pi-magic-context before first release.
- name: Publish to npm
run: npm publish --access public --provenance
working-directory: packages/pi-plugin
publish-npm-cli:
name: Publish unified CLI to npm
runs-on: ubuntu-latest
needs: [test, test-pi, test-cli, e2e-opencode, e2e-pi, e2e-host-opencode, e2e-host-pi]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Ensure latest npm (for trusted publishing)
run: npm install -g npm@latest
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Sync version from tag
run: node scripts/version-sync.mjs --from-tag
- name: Build cli
run: bun run --cwd packages/cli build
# Uses npm Trusted Publishing (OIDC) — must be configured on npmjs.com
# for @cortexkit/magic-context before first release.
- name: Publish to npm
run: npm publish --access public --provenance
working-directory: packages/cli
github-release:
name: Create GitHub Release
runs-on: ubuntu-latest
# Wait for ALL three npm publishes to succeed before tagging the
# GitHub release. Previously this depended only on test jobs, so a
# publish timeout or registry failure could leave a published release
# page on GitHub while one of the @cortexkit/* packages was missing
# from npm.
needs: [test, test-pi, test-cli, e2e-opencode, e2e-pi, e2e-host-opencode, e2e-host-pi, publish-npm, publish-npm-pi, publish-npm-cli]
steps:
- uses: actions/checkout@v5
- name: Verify curated release notes exist
# `.alfonso/release-notes/<tag>.md` is the source of truth for the
# release body AND the Discord post. .alfonso/ is gitignored except
# for the release-notes/ subdirectory — see .gitignore. Fail loudly
# if the file is missing so we don't ship a release with the bare
# `**Full Changelog**: ...` GitHub auto-generated body.
run: |
notes_file=".alfonso/release-notes/${GITHUB_REF_NAME}.md"
if [ ! -f "$notes_file" ]; then
echo "::error::Curated release notes missing: $notes_file"
echo "Draft them under .alfonso/release-notes/ before tagging the release."
exit 1
fi
echo "Using $notes_file as the release body."
wc -l "$notes_file"
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
body_path: .alfonso/release-notes/${{ github.ref_name }}.md
# Announce on Discord after the GitHub release is created.
#
# Why this lives here (not in discord-release.yml as `on: release: published`):
# GitHub Actions intentionally suppresses event-triggered workflow cascades
# from `GITHUB_TOKEN`-authored events. `softprops/action-gh-release@v2` uses
# the default `GITHUB_TOKEN`, so the `release: published` event it emits does
# not trigger downstream workflows. Inlining the Discord post here is the
# cleanest fix (vs rotating PATs for release creation).
#
# `continue-on-error: true` keeps Discord failures from rolling back a
# successful release — if the webhook is down, the announcement can be
# retried via `gh workflow run discord-release.yml ...`.
discord-announce:
name: Announce on Discord
runs-on: ubuntu-latest
needs: github-release
if: success()
continue-on-error: true
steps:
- uses: actions/checkout@v5
- name: Read curated release notes
id: notes
run: |
notes_file=".alfonso/release-notes/${GITHUB_REF_NAME}.md"
if [ ! -f "$notes_file" ]; then
echo "::error::Curated release notes missing: $notes_file"
exit 1
fi
{
echo "body<<DISCORD_EOF"
cat "$notes_file"
echo "DISCORD_EOF"
} >> "$GITHUB_OUTPUT"
- name: Post to Discord
uses: SethCohen/github-releases-to-discord@v1
with:
webhook_url: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }}
# Cortexkit teal — decimal value of #2C4A7C.
color: "2902140"
username: "Magic Context Releases"
# Reduce h1/h2 headings to fit Discord embed limits comfortably.
reduce_headings: true
# Keep PR/issue links — useful context in Discord.
remove_github_reference_links: false
footer_title: "Changelog"
footer_timestamp: true
release_name: ${{ github.ref_name }}
release_body: ${{ steps.notes.outputs.body }}
release_html_url: https://github.com/${{ github.repository }}/releases/tag/${{ github.ref_name }}