mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-26 18:35:05 +00:00
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.
468 lines
17 KiB
YAML
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
|