mirror of
https://github.com/open-multi-agent/open-multi-agent.git
synced 2026-10-19 01:31:07 -05:00
A library that gets installed into someone else's backend is judged partly on what its own install pulls down. Three changes tighten that, and two questions turned out to need no change at all. The line this draws is what ships. Every dependency that stays inside the repository is now pinned to the exact version the lockfile already resolved: all 27 devDependencies across the four workspaces, plus the private release bot's own zod. The lockfile change is 28 spec lines with no resolution moved, no entry added or removed, and all 61 platform-specific entries intact. A new root .npmrc sets save-exact=true so the next one arrives pinned rather than as a range nobody notices. What a consumer installs keeps its range: `dependencies` in packages/core, the only published runtime block here, every `peerDependencies`, and otel's core compatibility range. An exact version in those blocks overrides the consumer's own resolution and stops their `npm audit fix` from reaching a patched build until we cut a release, which is a worse trade than the install-time drift it prevents. save-exact will still pin a new dependency added to one of them, so the package job now asserts all three published manifests and fails when one is left exact, next to the assertion that already covers otel's range. min-release-age is deliberately not set. It first shipped in npm 11.10.0, and the CI matrix runs Node 20 and 22, which bundle npm 10.8.2 and 10.9.8. Those two accept the key, echo it back from `npm config get`, and never act on it, so a quarantine window would hold on one leg of the matrix while silently doing nothing on the other two. It also has no effect on `npm ci`, which installs the lockfile verbatim. Every `npm ci` that installs this repository's own tree now passes --ignore-scripts, so a compromised dependency cannot run an install hook on a runner that holds release credentials. Only three dependencies declare a postinstall at all, and the full lint, test, build, scaffold, and example suites pass on a tree installed that way. The `npm install` calls that stand in for a real consumer are deliberately left alone: their job is to reproduce what a user's install actually does, and hardening them would make the smoke tests prove less than they claim. A new scheduled Supply chain audit workflow asks two questions about the dependencies that reach a consumer, an hour before the weekly release bot so a finding lands before the release PR is drafted. --omit=dev is the point of running it separately: auditing the full tree reports eighteen advisories, every one of them in test and fixture tooling that never ships, and a permanently red job is one nobody reads. The two steps fail differently. `npm audit` reports into the run summary, because a new upstream advisory is not this repository misbehaving and may have no fixed version yet. `npm audit signatures` fails, because it compares served bytes against the registry's own signature, so a mismatch is tampering rather than churn; all 27 production packages verify today. Two things needed no change. npm provenance is already live: publish.yml publishes through npm trusted publishing with id-token: write, which attaches SLSA attestations on its own, and every version since core 1.16.0, otel 0.1.2, and create-oma-app 0.8.1 carries one. And .npmrc left .gitignore in #198 because it only restated the default registry; it comes back tracked, with the ignore rule narrowed so a contributor's own nested .npmrc stays out.
260 lines
11 KiB
YAML
260 lines
11 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
lint:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
cache: npm
|
|
- run: npm ci --ignore-scripts
|
|
- run: npm run lint
|
|
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
strategy:
|
|
matrix:
|
|
node-version: [20, 22, 24]
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: ${{ matrix.node-version }}
|
|
cache: npm
|
|
- run: npm ci --ignore-scripts
|
|
- run: npm test
|
|
|
|
coverage:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
permissions:
|
|
contents: read
|
|
id-token: write
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
cache: npm
|
|
- run: npm ci --ignore-scripts
|
|
- run: npm run test:coverage
|
|
- uses: codecov/codecov-action@v5
|
|
with:
|
|
use_oidc: true
|
|
files: ./packages/core/coverage/lcov.info
|
|
fail_ci_if_error: true
|
|
|
|
package:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
cache: npm
|
|
- run: npm ci --ignore-scripts
|
|
- run: npm run build
|
|
- name: Smoke-test that the built entry points import and the CLI runs
|
|
working-directory: packages/core
|
|
run: |
|
|
node -e "Promise.all([import('./dist/index.js'), import('./dist/observability/index.js'), import('./dist/observability/file.js'), import('./dist/acp.js'), import('./dist/process.js'), import('./dist/mcp.js'), import('./dist/ai-sdk.js'), import('./dist/eval/index.js'), import('./dist/eval/file.js')]).then(() => console.log('entry points import OK')).catch((e) => { console.error(e); process.exit(1); })"
|
|
node dist/cli/oma.js help | grep -q "oma eval run"
|
|
node dist/cli/oma.js help | grep -q "oma eval gate"
|
|
echo "CLI help runs OK"
|
|
- name: Run the no-network evaluation gate pass and fail paths
|
|
working-directory: packages/core
|
|
run: |
|
|
eval_gate_out=$(mktemp -d)
|
|
node dist/cli/oma.js eval run \
|
|
--set tests/fixtures/eval/set.json \
|
|
--target tests/fixtures/eval/target.mjs \
|
|
--gate tests/fixtures/eval/gate.json \
|
|
--baseline tests/fixtures/eval/baseline.json \
|
|
--out "$eval_gate_out/pass" > "$eval_gate_out/pass-summary.json"
|
|
jq -e '.verdict.pass == true' "$eval_gate_out/pass-summary.json"
|
|
|
|
set +e
|
|
node dist/cli/oma.js eval run \
|
|
--set tests/fixtures/eval/set.json \
|
|
--target tests/fixtures/eval/target-gate-fail.mjs \
|
|
--gate tests/fixtures/eval/gate.json \
|
|
--baseline tests/fixtures/eval/baseline.json \
|
|
--out "$eval_gate_out/fail" > "$eval_gate_out/fail-summary.json"
|
|
eval_gate_status=$?
|
|
set -e
|
|
test "$eval_gate_status" -eq 1
|
|
jq -e '.verdict.pass == false' "$eval_gate_out/fail-summary.json"
|
|
- name: Assert the create-oma-app template pins the current core version
|
|
run: |
|
|
core_version=$(jq -r .version packages/core/package.json)
|
|
for manifest in packages/create-oma-app/templates/*/package.json; do
|
|
pinned=$(jq -r '.dependencies["@open-multi-agent/core"]' "$manifest")
|
|
if [ "$pinned" != "$core_version" ]; then
|
|
echo "$manifest pins @open-multi-agent/core@$pinned but this repo's core is $core_version."
|
|
exit 1
|
|
fi
|
|
done
|
|
- name: Assert the published dependency blocks stay semver ranges
|
|
run: |
|
|
set -euo pipefail
|
|
# save-exact=true in the root .npmrc pins whatever `npm install`
|
|
# saves. That is right for the tree that never leaves this
|
|
# repository and wrong for the blocks a consumer installs: an exact
|
|
# version there overrides their own resolution and stops their
|
|
# `npm audit fix` from reaching a patched build until we cut a
|
|
# release. A new dependency added to one of these has to be widened
|
|
# by hand, so assert it rather than trusting the habit.
|
|
fail=0
|
|
for manifest in packages/core/package.json packages/otel/package.json packages/create-oma-app/package.json; do
|
|
for block in dependencies peerDependencies; do
|
|
pinned=$(jq -r --arg b "$block" '(.[$b] // {}) | to_entries[] | select(.value | test("^[0-9]")) | "\(.key)@\(.value)"' "$manifest" | tr '\n' ' ')
|
|
if [ -n "$pinned" ]; then
|
|
echo "::error::$manifest .$block ships to consumers and must stay semver ranges, but these are exact: $pinned"
|
|
fail=1
|
|
fi
|
|
done
|
|
done
|
|
test "$fail" -eq 0
|
|
|
|
- name: Typecheck the create-oma-app template against core
|
|
run: npm run typecheck:template -w create-oma-app
|
|
- name: Assert the npm tarball ships exactly the expected files
|
|
working-directory: packages/core
|
|
run: |
|
|
paths=$(npm pack --dry-run --json | jq -r '.[0].files[].path')
|
|
unexpected=$(echo "$paths" | grep -vE '^(dist/|README\.md$|LICENSE$|package\.json$)' || true)
|
|
if [ -n "$unexpected" ]; then
|
|
echo "Unexpected files staged for the npm tarball:"
|
|
echo "$unexpected"
|
|
echo "Only dist/, README.md, LICENSE, package.json may ship."
|
|
exit 1
|
|
fi
|
|
missing=""
|
|
for f in dist/index.js dist/index.d.ts dist/observability/index.js dist/observability/index.d.ts dist/observability/file.js dist/observability/file.d.ts dist/cli/oma.js dist/acp.js dist/acp.d.ts dist/process.js dist/process.d.ts dist/mcp.js dist/mcp.d.ts dist/ai-sdk.js dist/ai-sdk.d.ts dist/eval/index.js dist/eval/index.d.ts dist/eval/file.js dist/eval/file.d.ts; do
|
|
echo "$paths" | grep -qxF "$f" || missing="$missing $f"
|
|
done
|
|
if [ -n "$missing" ]; then
|
|
echo "Required entry points missing from the npm tarball:$missing"
|
|
echo "Either the build did not run or an entry was dropped from package.json \"files\"."
|
|
exit 1
|
|
fi
|
|
|
|
- name: Smoke-test the installed core npm bin
|
|
working-directory: packages/core
|
|
run: |
|
|
pack_dir=$(mktemp -d)
|
|
consumer_dir=$(mktemp -d)
|
|
pack_json=$(npm pack --json --pack-destination "$pack_dir")
|
|
tarball=$(echo "$pack_json" | jq -r '.[0].filename')
|
|
npm install --prefix "$consumer_dir" "$pack_dir/$tarball"
|
|
help_output=$("$consumer_dir/node_modules/.bin/oma" help)
|
|
echo "$help_output" | grep -q "oma eval run"
|
|
echo "$help_output" | grep -q "oma eval gate"
|
|
echo "Installed CLI help runs OK"
|
|
|
|
- name: Assert the create-oma-app tarball ships dist + template
|
|
working-directory: packages/create-oma-app
|
|
run: |
|
|
paths=$(npm pack --dry-run --json | jq -r '.[0].files[].path')
|
|
unexpected=$(echo "$paths" | grep -vE '^(dist/|template/|templates/|README\.md$|LICENSE$|package\.json$)' || true)
|
|
if [ -n "$unexpected" ]; then
|
|
echo "Unexpected files staged for the create-oma-app tarball:"
|
|
echo "$unexpected"
|
|
echo "Only dist/, template/, README.md, LICENSE, package.json may ship."
|
|
exit 1
|
|
fi
|
|
missing=""
|
|
for f in dist/index.js dist/scaffold.js templates/demo/package.json templates/pr-review/src/index.ts templates/security/src/index.ts template/src/runtime.ts template/src/report.ts template/_env.example template/_env.ollama template/_gitignore; do
|
|
echo "$paths" | grep -qxF "$f" || missing="$missing $f"
|
|
done
|
|
if [ -n "$missing" ]; then
|
|
echo "Required files missing from the create-oma-app tarball:$missing"
|
|
echo "The scaffolder needs its template/ to function once published."
|
|
exit 1
|
|
fi
|
|
|
|
- name: Smoke-test that the otel entry point imports
|
|
working-directory: packages/otel
|
|
run: node -e "import('./dist/index.js').then(() => console.log('otel entry point imports OK')).catch((e) => { console.error(e); process.exit(1); })"
|
|
|
|
- name: Assert the otel tarball ships exactly the expected files
|
|
working-directory: packages/otel
|
|
run: |
|
|
paths=$(npm pack --dry-run --json | jq -r '.[0].files[].path')
|
|
unexpected=$(echo "$paths" | grep -vE '^(dist/|README\.md$|LICENSE$|package\.json$)' || true)
|
|
if [ -n "$unexpected" ]; then
|
|
echo "Unexpected files staged for the @open-multi-agent/otel tarball:"
|
|
echo "$unexpected"
|
|
echo "Only dist/, README.md, LICENSE, package.json may ship."
|
|
exit 1
|
|
fi
|
|
missing=""
|
|
for f in dist/index.js dist/index.d.ts; do
|
|
echo "$paths" | grep -qxF "$f" || missing="$missing $f"
|
|
done
|
|
if [ -n "$missing" ]; then
|
|
echo "Required entry points missing from the @open-multi-agent/otel tarball:$missing"
|
|
echo "Either the build did not run or an entry was dropped from package.json \"files\"."
|
|
exit 1
|
|
fi
|
|
|
|
- name: Run public observability examples without network or API keys
|
|
run: npm run test:observability-examples
|
|
|
|
- name: Run public evaluation example without network or API keys
|
|
run: npm run test:eval-examples
|
|
|
|
- name: Run tolerant same-host observability regression gate
|
|
run: npm run bench:observability:ci
|
|
|
|
- name: Pack the minimum core version supported by OTel
|
|
id: minimum-otel-core
|
|
run: |
|
|
set -euo pipefail
|
|
range=$(jq -r '.dependencies["@open-multi-agent/core"]' packages/otel/package.json)
|
|
if [[ ! "$range" =~ ^\^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
echo "::error::expected an exact caret range for the OTel core dependency; got $range"
|
|
exit 1
|
|
fi
|
|
minimum="${range#^}"
|
|
filename=$(npm pack "@open-multi-agent/core@$minimum" --pack-destination "$RUNNER_TEMP" --json | jq -er '.[0].filename')
|
|
tarball="$RUNNER_TEMP/$filename"
|
|
test -f "$tarball"
|
|
echo "tarball=$tarball" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Install real core and OTel tarballs in clean consumers
|
|
env:
|
|
OMA_OBSERVABILITY_MIN_CORE_TARBALL: ${{ steps.minimum-otel-core.outputs.tarball }}
|
|
run: npm run test:observability-pack
|
|
|
|
scaffold-e2e:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
cache: npm
|
|
- run: npm ci --ignore-scripts
|
|
- name: End-to-end scaffold smoke test (pack -> generate -> install -> run gate)
|
|
run: npm run test:scaffold
|