Files
ECC/.github/workflows/ci.yml
T
Affaan Mustafa 07971588f8 ci: replace the 33-way test matrix with full and package-manager lanes
The test job ran the entire suite on 3 OSes x 3 Node lines x 4 package
managers, 33 full runs per push. Windows lanes took 28 to 39 minutes and
macOS 14 to 19, so one push queued about 1.5 hours of wall clock behind
runner concurrency, and timing-sensitive tests got 33 chances to flake.
The package-manager axis only ever changed one environment variable that
nine suites read.

Now there are 13 lanes. Five run the full suite: Linux on Node 18, 20
and 22, plus Windows and macOS on Node 22. Eight are package-manager
lanes on Node 22 that run only the suites which read
CLAUDE_CODE_PACKAGE_MANAGER or CLAUDE_PACKAGE_MANAGER; the file list is
grepped at run time so it cannot drift as tests are added. Full lanes
use the runner's 4-process pool on Linux and macOS; Windows stays serial
because its process spawn and temp-dir cleanup are the flaky part.
2026-10-20 10:47:58 -04:00

468 lines
17 KiB
YAML

name: CI
on:
push:
branches: [main, 'release/**']
tags: ['v*']
pull_request:
branches: [main]
# Prevent duplicate runs
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Minimal permissions
permissions:
contents: read
jobs:
test:
name: Test (${{ matrix.os }}, Node ${{ matrix.node }}, ${{ matrix.pm }}, ${{ matrix.scope }})
runs-on: ${{ matrix.os }}
# The serial Windows full lane reaches its final installer cases at about
# 30 minutes; retain a bounded completion window.
timeout-minutes: 45
strategy:
fail-fast: false
# Two kinds of lane. `full` runs the whole suite; `pm` runs only the suites
# that read CLAUDE_CODE_PACKAGE_MANAGER, which is all the package-manager
# axis ever changed. The old 33-way cross product ran the full suite 33
# times per push, with Windows lanes at 30 to 40 minutes each.
matrix:
include:
# Full suite: Linux on every supported Node line, current LTS on Windows and macOS.
- { os: ubuntu-latest, node: '18.x', pm: npm, scope: full }
- { os: ubuntu-latest, node: '20.x', pm: npm, scope: full }
- { os: ubuntu-latest, node: '22.x', pm: npm, scope: full }
- { os: windows-latest, node: '22.x', pm: npm, scope: full }
- { os: macos-latest, node: '22.x', pm: npm, scope: full }
# Package-manager lanes on current LTS. Bun has limited Windows support.
- { os: ubuntu-latest, node: '22.x', pm: pnpm, scope: pm }
- { os: ubuntu-latest, node: '22.x', pm: yarn, scope: pm }
- { os: ubuntu-latest, node: '22.x', pm: bun, scope: pm }
- { os: windows-latest, node: '22.x', pm: pnpm, scope: pm }
- { os: windows-latest, node: '22.x', pm: yarn, scope: pm }
- { os: macos-latest, node: '22.x', pm: pnpm, scope: pm }
- { os: macos-latest, node: '22.x', pm: yarn, scope: pm }
- { os: macos-latest, node: '22.x', pm: bun, scope: pm }
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Node.js ${{ matrix.node }}
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ matrix.node }}
- name: Install and verify ripgrep (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends -y ripgrep
rg --version
printf 'ecc-pcre2-probe\n' | rg --pcre2 -- '^ecc-pcre2-probe$'
- name: Install and verify ripgrep (macOS)
if: runner.os == 'macOS'
shell: bash
run: |
if ! command -v rg >/dev/null 2>&1; then brew install ripgrep; fi
rg --version
printf 'ecc-pcre2-probe\n' | rg --pcre2 -- '^ecc-pcre2-probe$'
- name: Install and verify ripgrep (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
if (-not (Get-Command rg -ErrorAction SilentlyContinue)) {
choco install ripgrep --yes --no-progress
if ($LASTEXITCODE -ne 0) { throw 'ripgrep installation failed' }
}
rg --version
if ($LASTEXITCODE -ne 0) { throw 'ripgrep executable is unavailable' }
# PowerShell writes CRLF to native stdin; retain anchored matching.
'ecc-pcre2-probe' | rg --pcre2 --crlf -- '^ecc-pcre2-probe$'
if ($LASTEXITCODE -ne 0) { throw 'ripgrep PCRE2 support is required' }
# Package manager setup
- name: Setup pnpm
if: matrix.pm == 'pnpm' && matrix.node != '18.x'
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
# Keep an explicit pnpm major because this repo's packageManager is Yarn.
version: 10
- name: Setup pnpm (via Corepack)
if: matrix.pm == 'pnpm' && matrix.node == '18.x'
shell: bash
run: |
corepack enable
corepack prepare pnpm@9 --activate
- name: Setup Yarn (via Corepack)
if: matrix.pm == 'yarn'
shell: bash
run: |
corepack enable
corepack prepare yarn@stable --activate
- name: Setup Bun
if: matrix.pm == 'bun'
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
# Install dependencies
# COREPACK_ENABLE_STRICT=0 allows pnpm to install even though
# package.json declares "packageManager": "yarn@..."
- name: Install dependencies
shell: bash
env:
COREPACK_ENABLE_STRICT: '0'
npm_config_ignore_scripts: 'true'
YARN_ENABLE_SCRIPTS: 'false'
run: |
case "${{ matrix.pm }}" in
npm) npm ci --ignore-scripts ;;
# pnpm v10 can fail CI on ignored native build scripts
# (for example msgpackr-extract) even though this repo is Yarn-native
# and pnpm is only exercised here as a compatibility lane.
pnpm) pnpm install --ignore-scripts --config.strict-dep-builds=false --no-frozen-lockfile ;;
# Yarn Berry (v4+) removed --ignore-engines; engine checking is no longer a core feature
yarn) yarn install --mode=skip-build ;;
bun) bun install --ignore-scripts ;;
*) echo "Unsupported package manager: ${{ matrix.pm }}" && exit 1 ;;
esac
# Run tests. Full lanes run every file through a 4-process pool on Linux
# and macOS; Windows stays serial because its process spawn and temp-dir
# cleanup are the flaky part. pm lanes derive their file list from the
# suites that actually read the package-manager variable, so the lane
# cannot drift out of date as tests are added.
- name: Run tests
shell: bash
run: |
if [ "${{ matrix.scope }}" = "pm" ]; then
ECC_TEST_ONLY=$(grep -rlE --include='*.test.js' 'CLAUDE(_CODE)?_PACKAGE_MANAGER' tests | sed 's#^tests/##' | sort | paste -sd, -)
export ECC_TEST_ONLY
echo "pm lane files: $ECC_TEST_ONLY"
fi
# Suites that run scripts/build-opencode.js, or rename a checkout path
# to a .backup-<pid> copy and restore it, mutate the shared checkout;
# suites that assert a wall-clock budget cannot share cores with the
# pool. Both groups run last, one at a time.
ECC_TEST_SERIAL=$(grep -rlE --include='*.test.js' 'build-opencode|\.backup-|dist\.backup|time budget' tests | sed 's#^tests/##' | sort | paste -sd, -)
export ECC_TEST_SERIAL
echo "serial after the pool: $ECC_TEST_SERIAL"
node tests/run-all.js
env:
CLAUDE_CODE_PACKAGE_MANAGER: ${{ matrix.pm }}
ECC_TEST_JOBS: ${{ runner.os == 'Windows' && '1' || '4' }}
# Upload test artifacts on failure
- name: Upload test artifacts
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results-${{ matrix.os }}-node${{ matrix.node }}-${{ matrix.pm }}-${{ matrix.scope }}
path: |
tests/
!tests/node_modules/
pack-installer:
name: Pack Installer Artifact
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
package_file: ${{ steps.pack.outputs.package_file }}
package_sha256: ${{ steps.pack.outputs.package_sha256 }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20.x'
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Pack exact installer artifact
id: pack
run: |
npm pack --json > npm-pack.json
node -e "const crypto = require('crypto'); const fs = require('fs'); const data = JSON.parse(fs.readFileSync('npm-pack.json', 'utf8')); const file = data[0]?.filename; if (!/^ecc-universal-[0-9A-Za-z.+-]+\.tgz$/.test(file || '')) throw new Error('Unexpected packed filename'); const archives = fs.readdirSync('.').filter(name => name.endsWith('.tgz')); if (archives.length !== 1 || archives[0] !== file) throw new Error('Expected exactly one packed archive'); const digest = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); fs.appendFileSync(process.env.GITHUB_OUTPUT, 'package_file=' + file + '\npackage_sha256=' + digest + '\n')"
- name: Upload exact installer artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ecc-ci-installer-artifact
path: ${{ steps.pack.outputs.package_file }}
if-no-files-found: error
packed-install-lifecycle:
name: Packed Install (${{ matrix.os }})
needs: pack-installer
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- name: Checkout lifecycle test
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20.x'
- name: Download exact installer artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ecc-ci-installer-artifact
path: release-artifacts
- name: Verify packed install lifecycle
env:
ECC_RELEASE_PACKAGE: release-artifacts/${{ needs.pack-installer.outputs.package_file }}
ECC_RELEASE_SHA256: ${{ needs.pack-installer.outputs.package_sha256 }}
run: node tests/ci/packed-artifact-lifecycle.js
validate:
name: Validate Components
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20.x'
- name: Install validation dependencies
run: npm ci --ignore-scripts
- name: Validate agents
run: node scripts/ci/validate-agents.js
continue-on-error: false
- name: Validate hooks
run: node scripts/ci/validate-hooks.js
continue-on-error: false
- name: Validate commands
run: node scripts/ci/validate-commands.js
continue-on-error: false
- name: Validate skills
run: node scripts/ci/validate-skills.js
continue-on-error: false
- name: Validate skill host compatibility
run: node scripts/ci/validate-skill-host-compat.js
continue-on-error: false
- name: Validate install manifests
run: node scripts/ci/validate-install-manifests.js
continue-on-error: false
- name: Validate workflow security
run: node scripts/ci/validate-workflow-security.js
continue-on-error: false
- name: Validate rules
run: node scripts/ci/validate-rules.js
continue-on-error: false
- name: Validate catalog counts
run: node scripts/ci/catalog.js --text
continue-on-error: false
- name: Validate command registry
run: npm run command-registry:check
continue-on-error: false
- name: Check unicode safety
run: node scripts/ci/check-unicode-safety.js
continue-on-error: false
- name: Validate no personal paths
run: node scripts/ci/validate-no-personal-paths.js
continue-on-error: false
pi-core:
name: Pi Core Profile
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20.x'
# Rebuild runs frontmatter validation and the pi/core safety checks
# (no callable endpoints, no runtime downloads, no secrets, no absolute
# home paths, no symlinks, no duplicate skill names); the diff check
# proves the committed profile is up to date.
- name: Rebuild pi/core and verify it is up to date
run: |
node scripts/build-pi-core.js
git diff --exit-code pi/core
- name: Install Pi coding agent CLI
run: npm install --global --ignore-scripts --no-audit --no-fund @mariozechner/pi-coding-agent@0.73.1
# --extension only loads a package's pi.extensions entries; pi/core is a
# skills+prompts-only package, so the equivalent --skill/--prompt-template
# resource flags are used. The load-test script additionally asserts via
# RPC that every curated command actually loaded.
- name: Offline load test with the Pi CLI
run: |
PI_OFFLINE=1 pi --offline --mode rpc --no-session --no-context-files --no-extensions \
--skill pi/core/skills --prompt-template pi/core/commands </dev/null >/dev/null
node scripts/ci/pi-core-load-test.js
python-tests:
name: Python Lint, Type Check & Test
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.11'
- name: Install Python dependencies
run: python -m pip install --upgrade pip && python -m pip install -e '.[dev]'
- name: Run ruff (lint)
run: python -m ruff check src tests
- name: Run mypy (type check)
run: python -m mypy src
- name: Run Python tests
run: python -m pytest tests/test_*.py -m "not integration"
- name: Test minimum supported OpenAI SDK
run: |
python -m pip install 'openai==2.34.0'
python -m pytest tests/test_provider_tools.py tests/test_atlas_provider.py tests/test_astraflow_provider.py tests/test_resolver.py
security:
name: Security Scan
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20.x'
- name: Install audit dependencies
run: npm ci --ignore-scripts
- name: Run npm audit
run: |
npm audit signatures
# Runtime/package advisories are release blockers. Development-only
# lint tooling remains covered by signature and IOC verification.
npm audit --omit=dev --audit-level=high
- name: Run supply-chain IOC scan
run: npm run security:ioc-scan
coverage:
name: Coverage
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20.x'
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Install and verify ripgrep
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends -y ripgrep
rg --version
printf 'ecc-pcre2-probe\n' | rg --pcre2 -- '^ecc-pcre2-probe$'
- name: Run coverage
run: npm run coverage
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-ubuntu-node20-npm
path: coverage/
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20.x'
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Run lint
run: npm run lint