Files
ECC/.github/workflows/ci.yml
T
haelyra 5fd527cf66 fix(ci): verify pinned Windows ripgrep without Chocolatey
Prefer a preinstalled rg application. Otherwise download the official ripgrep 15.2.0 x64 MSVC ZIP with bounded HTTP retries/timeouts, verify its committed SHA256 before extraction and execution, validate rg.exe, and publish its directory through GITHUB_PATH. Preserve the existing version and PCRE2 checks and every test lane.
2026-10-29 17:41:29 -07:00

495 lines
19 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
timeout-minutes: 5
run: |
$ErrorActionPreference = 'Stop'
if (-not (Get-Command rg -CommandType Application -ErrorAction SilentlyContinue)) {
if ($env:RUNNER_ARCH -ne 'X64') { throw 'Pinned ripgrep archive requires an X64 runner' }
$ripgrepVersion = '15.2.0'
# Official BurntSushi/ripgrep release archive and published SHA256.
$archiveName = "ripgrep-$ripgrepVersion-x86_64-pc-windows-msvc.zip"
$expectedHash = '71b2fef860abe467217a538ff31de02f5258807c0129f771846f87bd029aafc5'
$downloadRoot = Join-Path $env:RUNNER_TEMP ("ecc-ripgrep-" + [Guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $downloadRoot | Out-Null
$archivePath = Join-Path $downloadRoot $archiveName
$archiveUrl = "https://github.com/BurntSushi/ripgrep/releases/download/$ripgrepVersion/$archiveName"
Invoke-WebRequest -Uri $archiveUrl -OutFile $archivePath -MaximumRetryCount 2 -RetryIntervalSec 2 -ConnectionTimeoutSeconds 15 -OperationTimeoutSeconds 60 -MaximumRedirection 5
$actualHash = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actualHash -ne $expectedHash) { throw 'ripgrep archive SHA256 verification failed' }
Expand-Archive -LiteralPath $archivePath -DestinationPath $downloadRoot
$ripgrepDir = Join-Path $downloadRoot "ripgrep-$ripgrepVersion-x86_64-pc-windows-msvc"
$ripgrepExe = Join-Path $ripgrepDir 'rg.exe'
if (-not (Test-Path -LiteralPath $ripgrepExe -PathType Leaf)) { throw 'Verified archive has no rg.exe' }
& $ripgrepExe --version
if ($LASTEXITCODE -ne 0) { throw 'Verified ripgrep executable failed' }
Add-Content -LiteralPath $env:GITHUB_PATH -Value $ripgrepDir -Encoding utf8
$env:PATH = "$ripgrepDir;$env:PATH"
}
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
- name: Check observer and revision identity
if: matrix.scope == 'full'
shell: bash
run: |
node tests/lib/observer-sessions.test.js
node tests/commands/issue-review.test.js
# 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