feat: retire the runtime release-channel switch in favor of the build… #2407
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: CI | |
| on: | |
| push: | |
| branches: [main] | |
| tags: ['v*'] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| permissions: | |
| actions: read | |
| contents: read | |
| concurrency: | |
| group: ci-oss-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| build: | |
| if: "${{ github.event_name != 'push' || github.ref_type != 'branch' || !startsWith(github.event.head_commit.message || '', 'chore: bump version to v') }}" | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| node-version: [22] | |
| env: | |
| ADHDEV_SKIP_GHOSTTY_VT_BUILD: '1' | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Use Node.js ${{ matrix.node-version }} | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ matrix.node-version }} | |
| cache: 'npm' | |
| - name: Compute ghostty triplet | |
| run: echo "GHOSTTY_TRIPLET=linux-x64-node$(node -p 'process.versions.modules')" >> "$GITHUB_ENV" | |
| - name: Download latest ghostty prebuilt artifact | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "packages/ghostty-vt-node/prebuilt/${GHOSTTY_TRIPLET}" | |
| run_id="$(gh api "repos/${GITHUB_REPOSITORY}/actions/workflows/ghostty-vt-node.yml/runs?branch=main&event=push&per_page=20" --jq '.workflow_runs[] | select(.conclusion == "success") | .id' | head -n 1 || true)" | |
| if [ -z "${run_id}" ]; then | |
| echo "No successful Ghostty VT Native run found on main; continuing without prebuilt artifact." | |
| exit 0 | |
| fi | |
| gh run download "${run_id}" \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --name "ghostty-vt-node-${GHOSTTY_TRIPLET}" \ | |
| --dir "packages/ghostty-vt-node/prebuilt/${GHOSTTY_TRIPLET}" || { | |
| echo "Ghostty prebuilt artifact download failed; continuing without it." | |
| exit 0 | |
| } | |
| find "packages/ghostty-vt-node/prebuilt/${GHOSTTY_TRIPLET}" -mindepth 2 -maxdepth 2 -type f -exec mv {} "packages/ghostty-vt-node/prebuilt/${GHOSTTY_TRIPLET}/" \; | |
| find "packages/ghostty-vt-node/prebuilt/${GHOSTTY_TRIPLET}" -mindepth 1 -type d -empty -delete | |
| - name: Verify package lock is in sync | |
| run: | | |
| npm install --package-lock-only --ignore-scripts | |
| git diff --exit-code -- package-lock.json | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build all packages | |
| run: npm run build | |
| - name: Verify shebang | |
| run: head -1 packages/daemon-standalone/dist/index.js | grep '#!/usr/bin/env node' | |
| - name: Upload publish artifacts | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: publish-artifacts | |
| path: | | |
| packages/session-host-core/dist | |
| packages/session-host-daemon/dist | |
| packages/mcp-server/dist | |
| packages/daemon-core/dist | |
| packages/daemon-standalone/dist | |
| packages/web-standalone/dist | |
| if-no-files-found: error | |
| test: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| env: | |
| ADHDEV_SKIP_GHOSTTY_VT_BUILD: '1' | |
| steps: | |
| - uses: actions/checkout@v5 | |
| # Provider spec fixtures live in the sibling adhdev-providers repo (a | |
| # submodule of the private cloud monorepo, unknown to this OSS repo). The | |
| # spec-driven tests resolve them at repoRoot/adhdev-providers, where | |
| # repoRoot = $GITHUB_WORKSPACE/.. — a path outside the workspace that | |
| # actions/checkout cannot target directly. Check it out inside the | |
| # workspace and symlink it to the expected sibling location. | |
| - name: Checkout provider specs | |
| # Non-fatal: fork PRs get a read-only token scoped to the head repo and | |
| # may lack access to adhdev-providers. On push/tag (the release path) | |
| # the token has access and this succeeds. | |
| continue-on-error: true | |
| uses: actions/checkout@v5 | |
| with: | |
| repository: vilmire/adhdev-providers | |
| path: .adhdev-providers-src | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Link provider specs into expected sibling path | |
| run: | | |
| set -euo pipefail | |
| if [ -d "$GITHUB_WORKSPACE/.adhdev-providers-src" ]; then | |
| ln -sfn "$GITHUB_WORKSPACE/.adhdev-providers-src" "$GITHUB_WORKSPACE/../adhdev-providers" | |
| echo "Linked adhdev-providers → $GITHUB_WORKSPACE/../adhdev-providers" | |
| ls "$GITHUB_WORKSPACE/../adhdev-providers/cli/claude-cli/specs/4.0.json" | |
| else | |
| echo "⚠ adhdev-providers checkout absent (external fork without access); spec tests will resolve via fallback or fail." | |
| fi | |
| # Real-git fixture tests (mesh-fast-forward-node, git-status, refine-*) | |
| # shell out to git init/commit/rev-parse. Without an identity and default | |
| # branch, those spawns emit hints and misbehave on the runner. | |
| - name: Configure git identity for test fixtures | |
| run: | | |
| git config --global user.email "ci@adhf.dev" | |
| git config --global user.name "ADHDev CI" | |
| git config --global init.defaultBranch main | |
| - name: Use Node.js 22 | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| cache: 'npm' | |
| - name: Compute ghostty triplet | |
| run: echo "GHOSTTY_TRIPLET=linux-x64-node$(node -p 'process.versions.modules')" >> "$GITHUB_ENV" | |
| - name: Download latest ghostty prebuilt artifact | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "packages/ghostty-vt-node/prebuilt/${GHOSTTY_TRIPLET}" | |
| run_id="$(gh api "repos/${GITHUB_REPOSITORY}/actions/workflows/ghostty-vt-node.yml/runs?branch=main&event=push&per_page=20" --jq '.workflow_runs[] | select(.conclusion == "success") | .id' | head -n 1 || true)" | |
| if [ -z "${run_id}" ]; then | |
| echo "No successful Ghostty VT Native run found on main; continuing without prebuilt artifact." | |
| exit 0 | |
| fi | |
| gh run download "${run_id}" \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --name "ghostty-vt-node-${GHOSTTY_TRIPLET}" \ | |
| --dir "packages/ghostty-vt-node/prebuilt/${GHOSTTY_TRIPLET}" || { | |
| echo "Ghostty prebuilt artifact download failed; continuing without it." | |
| exit 0 | |
| } | |
| find "packages/ghostty-vt-node/prebuilt/${GHOSTTY_TRIPLET}" -mindepth 2 -maxdepth 2 -type f -exec mv {} "packages/ghostty-vt-node/prebuilt/${GHOSTTY_TRIPLET}/" \; | |
| find "packages/ghostty-vt-node/prebuilt/${GHOSTTY_TRIPLET}" -mindepth 1 -type d -empty -delete | |
| - name: Install dependencies | |
| run: npm ci | |
| # Build order follows the @adhdev dependency graph, because the typecheck | |
| # steps below resolve workspace imports through each package's published | |
| # `types`/`exports` — i.e. through its dist/, not its src/. A package whose | |
| # dependency was never built fails with TS2307 "Cannot find module". | |
| # | |
| # terminal-mux-{core,control} are built here for exactly that reason: | |
| # without them "Typecheck @adhdev/terminal-mux-cli" and | |
| # "Typecheck @adhdev/daemon-standalone" both fail on TS2307. This is | |
| # invisible locally (and to Refinery, which runs from the root worktree) | |
| # because dist/ already exists there — it only reproduces in a clean | |
| # checkout, which is what CI is. | |
| # | |
| # Order: session-host-core → terminal-mux-core → terminal-mux-control | |
| # (mux-core needs session-host-core; mux-control needs both). | |
| - name: Build packages required for tests | |
| run: | | |
| npm run build -w packages/session-host-core | |
| npm run build -w packages/session-host-daemon | |
| npm run build -w packages/mesh-shared | |
| npm run build -w packages/daemon-core | |
| npm run build -w packages/terminal-mux-core | |
| npm run build -w packages/terminal-mux-control | |
| - name: Test @adhdev/mesh-shared | |
| run: npm run test -w packages/mesh-shared | |
| - name: Test @adhdev/daemon-core | |
| run: npm run test -w packages/daemon-core | |
| - name: Test @adhdev/mcp-server | |
| run: npm run test -w packages/mcp-server | |
| - name: Test @adhdev/web-core | |
| run: npm run test -w packages/web-core | |
| - name: Test @adhdev/session-host-daemon | |
| run: npm run test -w packages/session-host-daemon | |
| - name: Test @adhdev/session-host-core | |
| run: npm run test -w packages/session-host-core | |
| # terminal-mux-{core,cli} tests import leaf modules directly rather than | |
| # the package barrel, so they never load the @adhdev/ghostty-vt-node | |
| # native binding and stay green on runners without a prebuilt. | |
| - name: Test @adhdev/terminal-mux-core | |
| run: npm run test -w packages/terminal-mux-core | |
| - name: Test @adhdev/terminal-mux-cli | |
| run: npm run test -w packages/terminal-mux-cli | |
| - name: Test @adhdev/terminal-render-web | |
| run: npm run test -w packages/terminal-render-web | |
| - name: Test @adhdev/web-standalone | |
| run: npm run test -w packages/web-standalone | |
| # Dual runner: `node --import tsx --test test/*.test.ts && vitest run`. | |
| # Both halves must pass — the node:test half covers session-host stop and | |
| # CLI arg handling, the vitest half covers the HTTP surfaces. | |
| - name: Test @adhdev/daemon-standalone | |
| run: npm run test -w packages/daemon-standalone | |
| - name: Typecheck @adhdev/daemon-core | |
| run: npm run typecheck -w packages/daemon-core | |
| - name: Typecheck @adhdev/session-host-daemon | |
| run: npm run typecheck -w packages/session-host-daemon | |
| - name: Typecheck @adhdev/session-host-core | |
| run: npm run typecheck -w packages/session-host-core | |
| - name: Typecheck @adhdev/terminal-mux-core | |
| run: npm run typecheck -w packages/terminal-mux-core | |
| - name: Typecheck @adhdev/terminal-mux-cli | |
| run: npm run typecheck -w packages/terminal-mux-cli | |
| - name: Typecheck @adhdev/terminal-render-web | |
| run: npm run typecheck -w packages/terminal-render-web | |
| - name: Typecheck @adhdev/daemon-standalone | |
| run: npm run typecheck -w packages/daemon-standalone | |
| publish: | |
| needs: [build, test] | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| runs-on: ubuntu-latest | |
| env: | |
| ADHDEV_SKIP_GHOSTTY_VT_BUILD: '1' | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Use Node.js 22 | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| registry-url: 'https://registry.npmjs.org' | |
| cache: 'npm' | |
| - name: Verify package lock is in sync | |
| run: | | |
| npm install --package-lock-only --ignore-scripts | |
| git diff --exit-code -- package-lock.json | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Check ghostty release need | |
| id: ghostty_release | |
| run: | | |
| set -euo pipefail | |
| package_version="$(node -p "require('./packages/ghostty-vt-node/package.json').version")" | |
| published_version="$(npm view @adhdev/ghostty-vt-node version 2>/dev/null || true)" | |
| if [ "${package_version}" = "${published_version}" ]; then | |
| echo "publish=false" >> "$GITHUB_OUTPUT" | |
| echo "Ghostty package version ${package_version} is already published; skipping ghostty release work." | |
| else | |
| echo "publish=true" >> "$GITHUB_OUTPUT" | |
| echo "Ghostty package version ${package_version} is unpublished; running ghostty release work." | |
| fi | |
| - name: Download release ghostty prebuilt artifacts | |
| if: steps.ghostty_release.outputs.publish == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| run_id="" | |
| for attempt in $(seq 1 30); do | |
| run_id="$(gh api "repos/${GITHUB_REPOSITORY}/actions/workflows/ghostty-vt-node.yml/runs?per_page=30" --jq '.workflow_runs[] | select(.head_sha == "'"${GITHUB_SHA}"'" and .conclusion == "success") | .id' | head -n 1 || true)" | |
| if [ -n "${run_id}" ]; then | |
| break | |
| fi | |
| pending_run="$(gh api "repos/${GITHUB_REPOSITORY}/actions/workflows/ghostty-vt-node.yml/runs?per_page=30" --jq '.workflow_runs[] | select(.head_sha == "'"${GITHUB_SHA}"'" and (.status == "queued" or .status == "in_progress" or .status == "pending" or .status == "waiting")) | .id' | head -n 1 || true)" | |
| if [ -z "${pending_run}" ]; then | |
| echo "No successful Ghostty VT Native run found for ${GITHUB_SHA}." | |
| exit 1 | |
| fi | |
| echo "Waiting for Ghostty VT Native run ${pending_run} to finish..." | |
| sleep 10 | |
| done | |
| if [ -z "${run_id}" ]; then | |
| echo "Timed out waiting for Ghostty VT Native run on ${GITHUB_SHA}." | |
| exit 1 | |
| fi | |
| artifact_names="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/artifacts?per_page=100" --jq '.artifacts[] | select(.expired == false and (.name | startswith("ghostty-vt-node-"))) | .name')" | |
| if [ -z "${artifact_names}" ]; then | |
| echo "No ghostty native artifacts found for run ${run_id}." | |
| exit 1 | |
| fi | |
| while IFS= read -r artifact_name; do | |
| [ -n "${artifact_name}" ] || continue | |
| triplet="${artifact_name#ghostty-vt-node-}" | |
| mkdir -p "packages/ghostty-vt-node/prebuilt/${triplet}" | |
| gh run download "${run_id}" \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --name "${artifact_name}" \ | |
| --dir "packages/ghostty-vt-node/prebuilt/${triplet}" | |
| find "packages/ghostty-vt-node/prebuilt/${triplet}" -mindepth 2 -maxdepth 2 -type f -exec mv {} "packages/ghostty-vt-node/prebuilt/${triplet}/" \; | |
| find "packages/ghostty-vt-node/prebuilt/${triplet}" -mindepth 1 -type d -empty -delete | |
| done <<< "${artifact_names}" | |
| find packages/ghostty-vt-node/prebuilt -maxdepth 2 -type f -print | sort | |
| - name: Download publish artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: publish-artifacts | |
| path: packages | |
| - name: Bundle web into daemon-standalone | |
| run: npm run bundle:web -w packages/daemon-standalone | |
| - name: Bundle vendored runtime deps into daemon-standalone | |
| run: npm run bundle:vendor -w packages/daemon-standalone | |
| - name: Build @adhdev/mesh-shared | |
| run: npm run build -w packages/mesh-shared | |
| - name: Publish @adhdev/mesh-shared | |
| if: env.NODE_AUTH_TOKEN != '' | |
| run: cd packages/mesh-shared && npm publish --access public --ignore-scripts || echo '⚠️ mesh-shared publish skipped (version may already exist)' | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Publish @adhdev/session-host-core | |
| if: env.NODE_AUTH_TOKEN != '' | |
| run: cd packages/session-host-core && npm publish --access public --ignore-scripts || echo '⚠️ session-host-core publish skipped (version may already exist)' | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Publish @adhdev/ghostty-vt-node | |
| if: env.NODE_AUTH_TOKEN != '' && steps.ghostty_release.outputs.publish == 'true' | |
| run: cd packages/ghostty-vt-node && npm publish --access public --ignore-scripts || echo '⚠️ ghostty-vt-node publish skipped (version may already exist)' | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Publish @adhdev/mcp-server | |
| if: env.NODE_AUTH_TOKEN != '' | |
| run: cd packages/mcp-server && npm publish --access public --ignore-scripts || echo '⚠️ mcp-server publish skipped (version may already exist)' | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Vendor session-host-core into daemon-core | |
| run: | | |
| set -euo pipefail | |
| target="packages/daemon-core/node_modules/@adhdev/session-host-core" | |
| rm -rf "${target}" | |
| mkdir -p "${target}" | |
| cp packages/session-host-core/package.json "${target}/package.json" | |
| cp -R packages/session-host-core/dist "${target}/dist" | |
| - name: Publish @adhdev/daemon-core | |
| if: env.NODE_AUTH_TOKEN != '' | |
| run: cd packages/daemon-core && npm publish --access public --ignore-scripts || echo '⚠️ daemon-core publish skipped (version may already exist)' | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Publish @adhdev/daemon-standalone | |
| if: env.NODE_AUTH_TOKEN != '' | |
| run: cd packages/daemon-standalone && npm publish --access public --ignore-scripts || echo '⚠️ daemon-standalone publish skipped (version may already exist)' | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} |