Skip to content

ci: follow a copied producer back to the attempt that built its artifact #2792

ci: follow a copied producer back to the attempt that built its artifact

ci: follow a copied producer back to the attempt that built its artifact #2792

Workflow file for this run

name: CI
on:
push:
# train/** runs the same checks on a train branch before it is fast-forwarded
# onto master; required status checks on master are satisfied by that run.
branches: [master, main, "train/**"]
# The nightly run rechecks the Cargo graph pinned by Cargo.lock;
# workflow_dispatch remains available for an operator-triggered run.
schedule:
- cron: "17 3 * * *"
workflow_dispatch:
# A train branch gets pushed again with each merge, and every run is about twenty
# jobs against a shared runner limit, so a new push cancels the train's older run.
# master keeps every run, so a failure still names the commit that broke it.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' && github.ref != 'refs/heads/main' }}
permissions:
contents: read
actions: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
# Separate differential parity cases by name; when absent, lib shards cover the entire library.
MC_PARITY_FILTER: 'test(/^providers::step_transform::parity_tests::r2_review_720_pass_differential/)'
# Pipeline shape on every push to master/main:
#
# Consumers check out/install immediately, then wait for verified same-attempt artifacts.
# preflight (layout, manifest, fmt, typecheck, lint, complete dists)
# ├─ unit suites and packaging smokes
# ├─ Docker install smokes (independent of unit/host results)
# ├─ host behavior (OC1: four whole-file shards; OC2: two)
# ├─ Rust clippy, lib archive → lib partitions + parity
# └─ Rust integration/doctests/remaining crates and hermetic build → shards
# ci-summary always checks every required leg, including cancelled/skipped jobs.
#
# Two e2e layers cover different concerns:
# - Docker e2e: fresh-install smoke (plugin loads, doctor clean, one mock turn writes DB rows
# under cortexkit path with right harness). 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 an embedded mock provider.
# Catches cache-stability + correctness regressions that the smoke layer cannot see.
#
# Docker e2e was previously in a separate workflow (e2e-docker.yml). Folding it here means
# every master/main push exercises all unit, Docker and host proofs. The
# Rust jobs run on every push and scheduled run. Pushes use Cargo.lock-pinned crates;
# the scheduled hermetic build additionally probes the subconscious default branch:
#
# rust-crates legacy-named aggregate for clippy, lib/parity, integration,
# binary, doctest and remaining-workspace targets
# e2e-rust-hermetic-build → e2e-rust-hermetic (4 shards)
#
# The mc-module integration tests always build ck-subc from its Cargo.lock revision.
# Hermetic push builds use that same revision; the nightly build intentionally builds
# subconscious default-branch HEAD and reports it as drift in the job summary.
jobs:
preflight:
name: Preflight (checks and dists)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-node@v4
with:
node-version: "24"
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Layout, manifest and shard contracts
run: |
bash scripts/check-cortexkit-layout.sh
bash scripts/check-docs-layout.sh
bash scripts/check-docs-layout.test.sh
python3 scripts/cargo-lock-pins.py --self-test
python3 scripts/check-nextest-inventory.py --self-test
python3 scripts/wait-ci-artifact.py --self-test
timeout 180s python3 scripts/check-cargo-path-dependencies.py
timeout 180s python3 scripts/check-cargo-path-dependencies.py --self-test
bun packages/e2e-tests/scripts/validate-mode-manifest.ts
cd packages/e2e-tests
bun test scripts/validate-mode-manifest.test.ts scripts/select-rust-shard.test.ts scripts/select-host-shard.test.ts scripts/select-unit-shard.test.ts scripts/ci-workflow.test.ts
- name: Format check
if: ${{ !cancelled() }}
run: cargo fmt --check
- name: TypeScript typecheck
if: ${{ !cancelled() }}
run: |
status=0
for package in plugin cli retina-local-fs pi-plugin dashboard; do
bun run --cwd "packages/$package" typecheck || status=1
done
bun run --cwd packages/e2e-tests tsc --noEmit -p tsconfig.ci.json || status=1
exit "$status"
- name: Lint
if: ${{ !cancelled() }}
run: |
status=0
for package in plugin cli retina-local-fs pi-plugin dashboard; do
bun run --cwd "packages/$package" lint || status=1
done
exit "$status"
- name: Build complete dists
run: |
bun run build
bun run --cwd packages/pi-plugin build:e2e-argv
bun run --cwd packages/dashboard build
- name: Check compiled contracts
run: |
bun run --cwd packages/plugin check:tui-compiled
bun crates/mc-module/gen/gen-historian-prompt-golden.ts --check
bun test packages/e2e-tests/scripts/ci-dists.test.ts
- name: Package same-commit dists
run: |
tar -czf ci-dists.tar.gz packages/plugin/dist packages/pi-plugin/dist packages/cli/dist packages/retina-local-fs/dist packages/dashboard/dist tests/docker/.generated
sha256sum ci-dists.tar.gz > ci-dists.sha256
printf '%s\n' "$GITHUB_SHA" > ci-dists.commit
- uses: actions/upload-artifact@v4
with:
name: ci-dists-${{ github.sha }}-${{ github.run_attempt }}
path: |
ci-dists.tar.gz
ci-dists.sha256
ci-dists.commit
compression-level: 0
if-no-files-found: error
retention-days: 1
check-docs-layout:
name: Check (public documentation layout)
runs-on: ubuntu-latest
needs: [preflight]
steps:
- uses: actions/checkout@v5
- name: Reject tracked ignored CortexKit files
run: bash scripts/check-cortexkit-layout.sh
- name: Reject internal documentation in docs
run: bash scripts/check-docs-layout.sh
- name: Exercise planted layout violations
run: bash scripts/check-docs-layout.test.sh
check-plugin-compat:
name: Check (CLI, retina and packaging)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
# node:sqlite (the Pi / OpenCode-Desktop SQLite backend) is only reachable
# under Node, not Bun. Node 24 also strips the smoke script's inline TS.
- uses: actions/setup-node@v4
with:
node-version: "24"
- name: Install dependencies
run: bun install --frozen-lockfile
- uses: ./.github/actions/restore-ci-dists
id: dists
- name: Test (CLI and retina, normal and debug assertions)
# Bun has no --no-fail-fast flag. Collect package/mode statuses explicitly;
# each package wrapper still owns its preload, JSC flags and timeout.
run: |
status=0
for package in cli retina-local-fs; do
bun run --cwd "packages/$package" test || status=1
done
MAGIC_CONTEXT_DEBUG_ASSERTIONS=1 bun run --cwd packages/cli test || status=1
MAGIC_CONTEXT_DEBUG_ASSERTIONS=1 bun run --cwd packages/retina-local-fs test --timeout=60000 || status=1
exit "$status"
# Exercise the node:sqlite branch of shared/sqlite.ts under REAL Node —
# bun test only covers the bun:sqlite branch, so the transaction() shim,
# readonly→readOnly mapping, and the array-bind normalization (#151) would
# otherwise ship unverified on Pi/Desktop.
- name: Smoke (node:sqlite backend)
if: ${{ !cancelled() && steps.dists.outcome == 'success' }}
run: node packages/plugin/scripts/smoke-node-sqlite.ts
# The smart-note QuickJS sandbox loads a ~1MB WASM. `bun test` runs it from
# src (wasm resolves via node_modules), so it cannot catch a BUNDLING break
# where the wasm isn't embedded in dist. This bundles sandbox-runner exactly
# like the package build and runs a real check against the bundle.
- name: Smoke (smart-note wasm bundle)
if: ${{ !cancelled() && steps.dists.outcome == 'success' }}
run: bun packages/plugin/scripts/smoke-smartnote-wasm.ts
# The raw-TSX ./tui entry imports @opentui/solid's JSX runtime. `bun test`
# never imports it, so a missing/mismatched OpenTUI or Solid dep (as broke
# on OpenCode 1.17.10's OpenTUI 0.4.2 bump) ships a TUI that won't load.
# Import the entry the way OpenCode loads the ./tui export to catch it.
- name: Smoke (TUI entry import)
if: ${{ !cancelled() && steps.dists.outcome == 'success' }}
run: bun packages/plugin/scripts/smoke-tui-import.ts
# The dev-path import above cannot catch packaging breaks: OpenTUI's Solid
# transform skips node_modules sources, so only a packed PROD install
# exercises the resolution path OpenCode's plugin cache uses (v0.31.1
# shipped without runtime deps and passed every dev-path check).
- name: Smoke (TUI packaged install import)
if: ${{ !cancelled() && steps.dists.outcome == 'success' }}
run: bun packages/plugin/scripts/smoke-tui-pack-install.ts --skip-build
# Tokenizer loading is lazy, so an import-only smoke misses resolution
# failures. Pack + npm-install prod deps, then exercise an estimate from a
# compiled Bun host matching OpenCode's /$bunfs/root runtime.
- name: Smoke (tokenizer packaged install estimate)
if: ${{ !cancelled() && steps.dists.outcome == 'success' }}
run: bun packages/plugin/scripts/smoke-tokenizer-pack-install.ts --skip-build
unit-plugin:
name: Unit (plugin ${{ matrix.mode }} ${{ matrix.shard }}/4)
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
mode: [normal, assertions]
shard: [0, 1, 2, 3]
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- uses: ./.github/actions/restore-ci-dists
- name: Run whole-file plugin shard
env:
UNIT_MODE: ${{ matrix.mode }}
run: |
bun packages/e2e-tests/scripts/select-unit-shard.ts plugin "$UNIT_MODE" "${{ matrix.shard }}/4" > "$RUNNER_TEMP/unit-files.txt"
mapfile -t files < "$RUNNER_TEMP/unit-files.txt"
test "${#files[@]}" -gt 0
printf '%s\n' "${files[@]}"
if [[ "$UNIT_MODE" == assertions ]]; then export MAGIC_CONTEXT_DEBUG_ASSERTIONS=1; fi
bun run --cwd packages/plugin test --reporter=junit --reporter-outfile "$GITHUB_WORKSPACE/unit-junit.xml" "${files[@]}"
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: unit-plugin-${{ matrix.mode }}-${{ matrix.shard }}-${{ github.run_attempt }}
path: unit-junit.xml
if-no-files-found: error
unit-pi-plugin:
name: Unit (pi-plugin ${{ matrix.mode }} ${{ matrix.shard }}/4)
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
mode: [normal, assertions]
shard: [0, 1, 2, 3]
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- uses: ./.github/actions/restore-ci-dists
- name: Run whole-file Pi shard
env:
UNIT_MODE: ${{ matrix.mode }}
# Assertion shards keep one worker and the existing 60-second timeout.
run: |
bun packages/e2e-tests/scripts/select-unit-shard.ts pi-plugin "$UNIT_MODE" "${{ matrix.shard }}/4" > "$RUNNER_TEMP/unit-files.txt"
mapfile -t files < "$RUNNER_TEMP/unit-files.txt"
test "${#files[@]}" -gt 0
printf '%s\n' "${files[@]}"
if [[ "$UNIT_MODE" == assertions ]]; then
MAGIC_CONTEXT_DEBUG_ASSERTIONS=1 bun run --cwd packages/pi-plugin test --parallel=1 --timeout=60000 --reporter=junit --reporter-outfile "$GITHUB_WORKSPACE/unit-junit.xml" "${files[@]}"
else
bun run --cwd packages/pi-plugin test --reporter=junit --reporter-outfile "$GITHUB_WORKSPACE/unit-junit.xml" "${files[@]}"
fi
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: unit-pi-plugin-${{ matrix.mode }}-${{ matrix.shard }}-${{ github.run_attempt }}
path: unit-junit.xml
if-no-files-found: error
check-plugin:
name: Check (plugin)
runs-on: ubuntu-latest
if: ${{ always() }}
needs: [unit-plugin, check-plugin-compat]
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Require plugin unit and packaging legs
env:
CI_NEEDS: ${{ toJSON(needs) }}
run: bun packages/e2e-tests/scripts/check-ci-results.ts check-plugin
check-pi-plugin:
name: Check (pi-plugin)
runs-on: ubuntu-latest
if: ${{ always() }}
needs: [unit-pi-plugin]
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Require Pi unit legs
env:
CI_NEEDS: ${{ toJSON(needs) }}
run: bun packages/e2e-tests/scripts/check-ci-results.ts check-pi-plugin
check-dashboard:
name: Check (dashboard)
runs-on: ubuntu-latest
needs: [preflight]
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
# Frontend-only checks (no Rust/Tauri needed). The key gate is the test
# step, which runs config-parity.test.ts — it fails the build if the
# plugin config schema gains/renames/removes a field the dashboard's
# ConfigEditor coverage manifest doesn't account for, so the form can't
# silently drift out of sync with the schema again.
- name: Test
run: bun run --cwd packages/dashboard test
e2e-opencode:
name: E2E (OpenCode, Docker)
runs-on: ubuntu-latest
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
- uses: ./.github/actions/restore-ci-dists
- 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-opencode2:
name: E2E (OpenCode 2.0.22, Docker)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install workspace dependencies
run: bun install --frozen-lockfile
- uses: ./.github/actions/restore-ci-dists
# The wrapper is the single local/CI entrypoint. It builds the publishable
# plugin, fails if Docker or opencode2 is missing, and runs the exact-pinned
# GA host through a real transform, host fold, database check, and TUI boot.
- name: Run pinned OpenCode 2 container lane
run: tests/docker/opencode2/run.sh
e2e-pi:
name: E2E (Pi, Docker)
runs-on: ubuntu-latest
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
- uses: ./.github/actions/restore-ci-dists
- 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-omp:
name: E2E (Oh My Pi, real Docker)
runs-on: ubuntu-latest
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
- uses: ./.github/actions/restore-ci-dists
- name: Build real OMP E2E image
run: |
docker build \
--platform linux/amd64 \
-f tests/docker/Dockerfile.omp \
-t mc-e2e-omp \
.
- name: Run real OMP install and session smoke
run: docker run --rm --platform linux/amd64 mc-e2e-omp
e2e-host-opencode:
name: E2E (OpenCode, host behavior ${{ matrix.shard }}/4)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3]
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
# Float to LATEST (no --version) so CI exercises the opencode version
# users actually run and catches upstream breakage as it ships — not at
# our release time. (A fixed pin previously hid opencode 1.16's
# post-overflow change, commit 7e09660c3, until release; the
# overflow-recovery host test is now version-agnostic.) Trade-off: a
# known-bad upstream release can turn CI red for unrelated PRs — if that
# happens, temporarily re-pin with `--version X.Y.Z` here until it's
# resolved upstream.
run: |
OPENCODE_VERSION=latest bash tests/docker/install-opencode.sh
echo "$HOME/.opencode/bin" >> "$GITHUB_PATH"
- name: Verify opencode on PATH
run: opencode --version
- uses: ./.github/actions/restore-ci-dists
id: dists
# 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: ""
MC_E2E_MODE: ts
MC_E2E_HOST: opencode
run: |
cd packages/e2e-tests
# A validator failure must fail the job, never fall through to an empty
# list: `bun test` with no files runs every file in the package.
set -o pipefail
files=$(bun scripts/select-host-shard.ts opencode "${{ matrix.shard }}/4" | tr '\n' ' ')
if [ -z "${files// /}" ]; then echo 'mode manifest selected no files'; exit 1; fi
echo "Running OpenCode host tests from mode manifest: $files"
# shellcheck disable=SC2086 # The repository-controlled file list must expand into Bun arguments.
bun test --timeout 600000 $files
# This oracle is outside tests/**/*.test.ts and runs once even if a host suite fails.
- name: Cache analysis oracle
if: ${{ !cancelled() && matrix.shard == 0 && steps.dists.outcome == 'success' }}
env:
NODE_ENV: ""
MC_E2E_MODE: ts
MC_E2E_HOST: opencode
run: cd packages/e2e-tests && bun test --timeout 600000 src/cache-analysis.test.ts
e2e-host-pi:
name: E2E (Pi, host behavior)
runs-on: ubuntu-latest
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. `bun install` brings it in.
- 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. Float to LATEST like the OpenCode host job
# (see that step for the rationale + re-pin escape hatch).
- name: Install opencode
run: |
OPENCODE_VERSION=latest bash tests/docker/install-opencode.sh
echo "$HOME/.opencode/bin" >> "$GITHUB_PATH"
- name: Verify opencode on PATH
run: opencode --version
- uses: ./.github/actions/restore-ci-dists
# Per-test timeout bumped to 300s for the same cold-start reason as
# the OpenCode host job. Pi historian publish path also crosses an
# HTTP boundary into the mock provider, which is slower on shared
# runners than on local hardware.
- name: Run host e2e suite (Pi tests from mode manifest)
env:
NODE_ENV: ""
MC_E2E_MODE: ts
MC_E2E_HOST: pi
run: |
cd packages/e2e-tests
# A validator failure must fail the job, never fall through to an empty
# list: `bun test` with no files runs every file in the package.
set -o pipefail
files=$(bun scripts/validate-mode-manifest.ts --mode ts --harness pi | tr '\n' ' ')
if [ -z "${files// /}" ]; then echo 'mode manifest selected no files'; exit 1; fi
echo "Running Pi host tests from mode manifest: $files"
# shellcheck disable=SC2086 # The repository-controlled file list must expand into Bun arguments.
bun test --timeout 600000 $files
e2e-host-opencode2:
name: E2E (OpenCode 2.0.22, host behavior ${{ matrix.shard }}/2)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [0, 1]
timeout-minutes: 40
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
# The OpenCode 2 harness spawns the real GA host from @opencode/cli, a
# workspace dev dependency whose postinstall unpacks the platform binary
# (trustedDependencies at the workspace root lets bun run it).
- name: Install workspace deps
run: bun install --frozen-lockfile
# The conversion regression boots both generations and doctor probes the
# v1 CLI on PATH; the v2 binary comes from the pinned workspace dependency.
# The installer one-liner resolves "latest" through the rate-limited GitHub API
# and can exit 0 without installing; the repo's installer pins, retries and
# asserts the binary runs.
- name: Install OpenCode 1 for conversion coverage
run: |
OPENCODE_VERSION=1.18.31 bash tests/docker/install-opencode.sh
echo "$HOME/.opencode/bin" >> "$GITHUB_PATH"
- name: Verify the GA host binary
run: bun -e "const { CLI } = await import('./packages/e2e-tests/src/opencode2-runner/spawn.ts'); const { execFileSync } = await import('node:child_process'); const version = execFileSync(CLI, ['--version']).toString().trim(); console.log(CLI, version); if (version !== 'opencode v2.0.22') throw new Error('OpenCode 2 host lane requires 2.0.22')"
- uses: ./.github/actions/restore-ci-dists
id: dists
- name: Run host e2e suite (OpenCode 2 tests from mode manifest)
env:
NODE_ENV: ""
MC_E2E_MODE: ts
MC_E2E_HOST: opencode2
run: |
cd packages/e2e-tests
set -o pipefail
files=$(bun scripts/select-host-shard.ts opencode2 "${{ matrix.shard }}/2" | tr '\n' ' ')
if [ -z "${files// /}" ]; then echo 'mode manifest selected no files'; exit 1; fi
echo "Running OpenCode 2 host tests from mode manifest: $files"
# shellcheck disable=SC2086 # The repository-controlled file list must expand into Bun arguments.
bun test --timeout 600000 $files
# These long-lived dual-host fixtures manipulate separate throwaway stores,
# but run sequentially to avoid contending with one another's live hosts.
- name: Run OC2 conversion and marker regressions
if: ${{ !cancelled() && matrix.shard == 0 && steps.dists.outcome == 'success' }}
env:
NODE_ENV: ""
MC_E2E_MODE: ts
MC_E2E_HOST: opencode2
run: |
cd packages/e2e-tests
status=0
bun test --timeout 600000 tests/opencode2/store-generation-conversion.test.ts || status=1
bun test --timeout 600000 tests/opencode2/marker-s3-runtime.test.ts || status=1
exit "$status"
e2e-host-omp:
name: E2E (Oh My Pi, host behavior)
runs-on: ubuntu-latest
timeout-minutes: 40
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
# OMP runs the same Pi plugin through Pi's extension API; the harness
# resolves @oh-my-pi/pi-coding-agent (a workspace dev dependency) when
# MC_E2E_HOST=omp.
- name: Install workspace deps
run: bun install --frozen-lockfile
- name: Install opencode
run: |
OPENCODE_VERSION=latest bash tests/docker/install-opencode.sh
echo "$HOME/.opencode/bin" >> "$GITHUB_PATH"
- uses: ./.github/actions/restore-ci-dists
- name: Run host e2e suite (OMP tests from mode manifest)
env:
NODE_ENV: ""
MC_E2E_MODE: ts
MC_E2E_HOST: omp
run: |
cd packages/e2e-tests
set -o pipefail
files=$(bun scripts/validate-mode-manifest.ts --mode ts --harness omp | tr '\n' ' ')
if [ -z "${files// /}" ]; then echo 'mode manifest selected no files'; exit 1; fi
echo "Running OMP host tests from mode manifest: $files"
# shellcheck disable=SC2086 # The repository-controlled file list must expand into Bun arguments.
bun test --timeout 600000 $files
rust-mc-module-integration:
name: Rust (mc-module integration)
# mc-module integration tests build ck-subc from the locked source revision.
# Published commons and subconscious crates resolve from this repository's
# Cargo.lock, so there is no commons checkout or floating crate source here.
#
# Keep daemon tests under libtest: its process-local build locks must not be
# multiplied by nextest's process-per-test execution model.
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
# The hostless-store integration test provisions context.db through the CLI.
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install CLI dependencies
run: bun install --frozen-lockfile
- name: Resolve Cargo.lock crate pins
id: cargo-pins
shell: bash
run: |
set -euo pipefail
python3 scripts/cargo-lock-pins.py --self-test
pins="$(python3 scripts/cargo-lock-pins.py)"
printf '%s\n' "$pins" >> "$GITHUB_OUTPUT"
locked_crates="$(printf '%s\n' "$pins" | sed -n 's/^locked_crates=//p')"
echo "Cargo.lock pins: $locked_crates" >> "$GITHUB_STEP_SUMMARY"
# real_daemon.rs builds ck-subc and checks its launch-nonce history; this
# exact locked checkout is therefore needed only by this integration-test job.
- name: Check out locked subconscious source for daemon integration tests
uses: actions/checkout@v5
with:
repository: cortexkit/subconscious
path: .siblings/subconscious
ref: ${{ steps.cargo-pins.outputs.subc_revision }}
fetch-depth: 0
# These integration tests use ../subconscious to build ck-subc. Cargo now
# resolves commons crates from the registry/Git lock instead of ../commons.
- name: Link locked daemon source for integration tests
shell: bash
run: |
set -euo pipefail
ln -sfn "$GITHUB_WORKSPACE/.siblings/subconscious" "$GITHUB_WORKSPACE/../subconscious"
test -f "$GITHUB_WORKSPACE/../subconscious/Cargo.lock"
- uses: ./.github/actions/restore-ci-dists
- name: Test mc-module (integration, binary and doctest targets)
run: |
status=0
cargo test --locked -p mc-module --test '*' --bins --no-fail-fast || status=1
cargo test --locked -p mc-module --doc --no-fail-fast || status=1
exit "$status"
rust-clippy:
name: Rust (clippy)
runs-on: ubuntu-latest
needs: [preflight]
timeout-minutes: 45
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Clippy (including integration targets)
run: cargo clippy --locked --workspace --all-targets -- -D warnings
rust-workspace-crates:
name: Rust (remaining crates)
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install workspace dependencies for migration-oracle tests
run: bun install --frozen-lockfile
- uses: ./.github/actions/restore-ci-dists
- name: Test the remaining workspace crates
run: cargo test --locked --workspace --exclude mc-module --no-fail-fast
rust-lib-archive:
name: Rust (mc-module lib archive)
runs-on: ubuntu-latest
needs: [preflight]
timeout-minutes: 45
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@v2
with:
tool: cargo-nextest@0.9.138
- name: Compile the complete lib once and prove inventory equality
run: |
mkdir -p ci-rust-lib
cargo nextest --version
cargo nextest archive --locked -p mc-module --lib --archive-file ci-rust-lib/archive.tar.zst
cargo test --locked -p mc-module --lib -- --list > ci-rust-lib/cargo-list.txt
cargo test --locked -p mc-module --lib -- --list --ignored > ci-rust-lib/cargo-ignored.txt
cargo nextest list --archive-file ci-rust-lib/archive.tar.zst --run-ignored all --message-format json > ci-rust-lib/all.json
for shard in 1 2 3 4; do
cargo nextest list --archive-file ci-rust-lib/archive.tar.zst --run-ignored all --partition "hash:$shard/4" -E "not ($MC_PARITY_FILTER)" --message-format json > "ci-rust-lib/lib-$shard.json"
done
cargo nextest list --archive-file ci-rust-lib/archive.tar.zst --run-ignored all -E "$MC_PARITY_FILTER" --message-format json > ci-rust-lib/parity.json
python3 scripts/check-nextest-inventory.py ci-rust-lib
diff -u ci-rust-lib/cargo-names.txt ci-rust-lib/union-names.txt
rustc -vV > ci-rust-lib/rustc.txt
printf '%s\n' "$GITHUB_SHA" > ci-rust-lib/commit.txt
sha256sum ci-rust-lib/archive.tar.zst > ci-rust-lib/archive.sha256
- uses: actions/upload-artifact@v4
with:
name: rust-lib-${{ github.sha }}-${{ github.run_attempt }}
path: ci-rust-lib/
compression-level: 0
if-no-files-found: error
retention-days: 1
rust-mc-module-lib:
name: Rust (mc-module lib ${{ matrix.shard }}/4)
runs-on: ubuntu-latest
# The cap includes at most 45 minutes waiting plus the original execution allowance.
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@v2
with:
tool: cargo-nextest@0.9.138
- uses: ./.github/actions/wait-ci-artifact
id: archive
with:
artifact: rust-lib-${{ github.sha }}
producer: Rust (mc-module lib archive)
timeout-minutes: "45"
- uses: actions/download-artifact@v4
with:
artifact-ids: ${{ steps.archive.outputs.artifact-id }}
merge-multiple: true
path: ci-rust-lib
- name: Verify archive provenance
run: |
test "$(cat ci-rust-lib/commit.txt)" = "$GITHUB_SHA"
sha256sum --check ci-rust-lib/archive.sha256
rustc -vV | diff - ci-rust-lib/rustc.txt
- name: Run lib partition
run: cargo nextest run --archive-file ci-rust-lib/archive.tar.zst --workspace-remap "$GITHUB_WORKSPACE" --profile ci --no-fail-fast --failure-output immediate-final --partition "hash:${{ matrix.shard }}/4" -E "not ($MC_PARITY_FILTER)"
- name: Check every selected test executed
if: ${{ !cancelled() }}
run: python3 scripts/check-nextest-inventory.py --junit ci-rust-lib "lib-${{ matrix.shard }}" target/nextest/ci/junit.xml
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: rust-lib-results-${{ matrix.shard }}
path: target/nextest/ci/junit.xml
if-no-files-found: error
rust-mc-module-parity:
name: Rust (mc-module parity differential)
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@v2
with:
tool: cargo-nextest@0.9.138
- uses: ./.github/actions/wait-ci-artifact
id: archive
with:
artifact: rust-lib-${{ github.sha }}
producer: Rust (mc-module lib archive)
timeout-minutes: "45"
- uses: actions/download-artifact@v4
with:
artifact-ids: ${{ steps.archive.outputs.artifact-id }}
merge-multiple: true
path: ci-rust-lib
- name: Verify archive provenance
run: |
test "$(cat ci-rust-lib/commit.txt)" = "$GITHUB_SHA"
sha256sum --check ci-rust-lib/archive.sha256
rustc -vV | diff - ci-rust-lib/rustc.txt
- name: Run parity groups from the shared lib archive
# Only parity permits an empty selection. The lib archive job compares the
# disjoint lib/parity union to Cargo discovery, including ignored tests.
run: cargo nextest run --archive-file ci-rust-lib/archive.tar.zst --workspace-remap "$GITHUB_WORKSPACE" --profile ci --no-fail-fast --failure-output immediate-final --no-tests=pass -E "$MC_PARITY_FILTER"
- name: Check every selected parity test executed
if: ${{ !cancelled() }}
run: python3 scripts/check-nextest-inventory.py --junit ci-rust-lib parity target/nextest/ci/junit.xml
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: rust-parity-results
path: target/nextest/ci/junit.xml
if-no-files-found: error
rust-crates:
name: Rust (crates)
runs-on: ubuntu-latest
if: ${{ always() }}
needs: [preflight, rust-clippy, rust-lib-archive, rust-mc-module-lib, rust-mc-module-parity, rust-mc-module-integration, rust-workspace-crates]
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Require every Rust leg
env:
CI_NEEDS: ${{ toJSON(needs) }}
run: bun packages/e2e-tests/scripts/check-ci-results.ts rust-crates
e2e-rust-hermetic-build:
name: E2E (Rust hermetic build)
# Cargo.lock pins the published crate versions and both git dependency revisions.
# On schedule the daemon checkout below intentionally switches to upstream HEAD.
runs-on: ubuntu-latest
needs: [preflight]
timeout-minutes: 120
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Resolve Cargo.lock crate pins
id: cargo-pins
shell: bash
run: |
set -euo pipefail
python3 scripts/cargo-lock-pins.py --self-test
pins="$(python3 scripts/cargo-lock-pins.py)"
printf '%s\n' "$pins" >> "$GITHUB_OUTPUT"
locked_crates="$(printf '%s\n' "$pins" | sed -n 's/^locked_crates=//p')"
echo "Cargo.lock pins: $locked_crates" >> "$GITHUB_STEP_SUMMARY"
# Cargo can resolve subc-core as a library dependency but does not build its
# ck-subc binary target. Pushes use the root lockfile revision; only the scheduled
# run deliberately checks the subconscious default branch for upstream drift.
- name: Check out Cargo.lock-pinned subconscious source for ck-subc
if: ${{ github.event_name != 'schedule' }}
uses: actions/checkout@v5
with:
repository: cortexkit/subconscious
path: .siblings/subconscious
ref: ${{ steps.cargo-pins.outputs.subc_revision }}
- name: Check out subconscious default branch for scheduled drift
if: ${{ github.event_name == 'schedule' }}
uses: actions/checkout@v5
with:
repository: cortexkit/subconscious
path: .siblings/subconscious
- name: Record ck-subc source revision
id: daemon-source
shell: bash
run: |
set -euo pipefail
sha="$(git -C "$GITHUB_WORKSPACE/.siblings/subconscious" rev-parse HEAD)"
if [[ "$GITHUB_EVENT_NAME" == "schedule" ]]; then
summary="ck-subc source drift: subconscious default-branch HEAD $sha"
else
summary="ck-subc source pinned by Cargo.lock: $sha"
fi
echo "$summary"
echo "$summary" >> "$GITHUB_STEP_SUMMARY"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
- name: Derive hermetic Cargo cache key
id: cargo-cache-key
shell: bash
run: |
set -euo pipefail
# Key on the resolved Cargo graph plus the exact daemon source, including
# nightly HEAD, so pinned and drift builds never reuse one another's cache.
lock_digest="$(sha256sum Cargo.lock | awk '{print $1}')"
source_sha="${{ steps.daemon-source.outputs.sha }}"
digest="$(printf '%s\n%s\n' "$lock_digest" "$source_sha" | sha256sum | awk '{print $1}')"
echo "key=rust-hermetic-${RUNNER_OS}-${RUNNER_ARCH}-${digest}" >> "$GITHUB_OUTPUT"
- name: Restore ck-subc and ckdev-mc-e2e Cargo target cache
uses: actions/cache@v4
with:
path: packages/e2e-tests/.cache/rust-e2e-cargo-target
key: ${{ steps.cargo-cache-key.outputs.key }}
- name: Install workspace dependencies
run: bun install --frozen-lockfile
- name: Build hermetic binary pair (including fault-injection variant)
shell: bash
run: |
set -euo pipefail
target="$GITHUB_WORKSPACE/packages/e2e-tests/.cache/rust-e2e-cargo-target"
mkdir -p "$target/prebuilt"
CARGO_TARGET_DIR="$target" cargo build --release -p mc-module
cp "$target/release/ck-mc" "$target/prebuilt/ckdev-mc-e2e"
CARGO_TARGET_DIR="$target" cargo build --release -p mc-module --features drive-fault
cp "$target/release/ck-mc" "$target/prebuilt/ckdev-mc-e2e-drive-fault"
(cd "$GITHUB_WORKSPACE/.siblings/subconscious" && CARGO_TARGET_DIR="$target" cargo build --release -p subc-core --bins)
cp "$target/release/ck-subc" "$target/prebuilt/ckdev-subc"
printf '%s\n' "$GITHUB_SHA" > "$target/prebuilt/commit.txt"
tar -czf "$RUNNER_TEMP/rust-hermetic-binaries.tar.gz" -C "$target/prebuilt" .
(cd "$RUNNER_TEMP" && sha256sum rust-hermetic-binaries.tar.gz > rust-hermetic-binaries.sha256)
- uses: actions/upload-artifact@v4
with:
name: rust-hermetic-binaries-${{ github.sha }}-${{ github.run_attempt }}
path: |
${{ runner.temp }}/rust-hermetic-binaries.tar.gz
${{ runner.temp }}/rust-hermetic-binaries.sha256
compression-level: 0
if-no-files-found: error
retention-days: 1
e2e-rust-hermetic:
name: E2E (Rust hermetic ${{ matrix.shard }}/4)
runs-on: ubuntu-latest
# Waiting is capped at the producer's 120 minutes; tests retain their own timeouts.
timeout-minutes: 180
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3]
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install workspace dependencies
run: bun install --frozen-lockfile
- name: Install pinned OpenCode
run: |
OPENCODE_VERSION=1.18.32 bash tests/docker/install-opencode.sh
echo "$HOME/.opencode/bin" >> "$GITHUB_PATH"
- uses: ./.github/actions/restore-ci-dists
- uses: ./.github/actions/wait-ci-artifact
id: binaries
with:
artifact: rust-hermetic-binaries-${{ github.sha }}
producer: E2E (Rust hermetic build)
timeout-minutes: "120"
- uses: actions/download-artifact@v4
with:
artifact-ids: ${{ steps.binaries.outputs.artifact-id }}
merge-multiple: true
path: ${{ runner.temp }}/hermetic-artifact
- name: Restore executable and auxiliary binaries
run: |
(cd "$RUNNER_TEMP/hermetic-artifact" && sha256sum --check rust-hermetic-binaries.sha256)
mkdir -p packages/e2e-tests/.cache/prebuilt
tar -xzf "$RUNNER_TEMP/hermetic-artifact/rust-hermetic-binaries.tar.gz" -C packages/e2e-tests/.cache/prebuilt
test "$(cat packages/e2e-tests/.cache/prebuilt/commit.txt)" = "$GITHUB_SHA"
for binary in ckdev-mc-e2e ckdev-mc-e2e-drive-fault ckdev-subc; do
test -x "packages/e2e-tests/.cache/prebuilt/$binary"
done
- name: Run manifest shard
env:
MC_E2E_SHARD: ${{ matrix.shard }}/4
MC_E2E_CK_MC_PREBUILT_BIN: ${{ github.workspace }}/packages/e2e-tests/.cache/prebuilt/ckdev-mc-e2e
MC_E2E_CK_MC_DRIVE_FAULT_BIN: ${{ github.workspace }}/packages/e2e-tests/.cache/prebuilt/ckdev-mc-e2e-drive-fault
MC_E2E_CK_SUBC_BIN: ${{ github.workspace }}/packages/e2e-tests/.cache/prebuilt/ckdev-subc
run: scripts/run-rust-hermetic-e2e.sh
ci-summary:
name: CI (required)
runs-on: ubuntu-latest
if: ${{ always() }}
needs: [preflight, check-docs-layout, check-plugin, check-pi-plugin, check-plugin-compat, unit-plugin, unit-pi-plugin, check-dashboard, e2e-opencode, e2e-opencode2, e2e-pi, e2e-omp, e2e-host-opencode, e2e-host-opencode2, e2e-host-pi, e2e-host-omp, rust-clippy, rust-lib-archive, rust-mc-module-lib, rust-mc-module-parity, rust-mc-module-integration, rust-workspace-crates, rust-crates, e2e-rust-hermetic-build, e2e-rust-hermetic]
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Require every CI leg
env:
CI_NEEDS: ${{ toJSON(needs) }}
run: bun packages/e2e-tests/scripts/check-ci-results.ts ci-summary