Skip to content

chore(sync): main → develop tras los bumps de Dependabot #1544

chore(sync): main → develop tras los bumps de Dependabot

chore(sync): main → develop tras los bumps de Dependabot #1544

Workflow file for this run

name: CI/CD
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
# GT-574: `develop` is where every change actually lands, and until now this
# pipeline did not run on a push to it — so the engine suites and the
# boundary guard only ever saw a change once it was already on `main`, or in
# a PR that nothing required. The deploy/docker/publish jobs stay ref-guarded
# to main/tags below, so adding develop here runs tests and gates only.
branches: [main, develop]
tags: ['v*']
# GT-324/W-Enforce: run the engine test + boundary-guard jobs on every PR to
# main/develop. The deploy/docker/publish jobs below stay ref-guarded
# (if: github.ref == main || tags), so PRs exercise tests + the
# STATELESS_CORE_REPOSITORY_BAN guard without triggering any deploy.
pull_request:
branches: [main, develop]
workflow_dispatch:
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-tags: true
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '20'
- name: Install dependencies
run: npm ci
# sdk/cli imports @beyondnet/evolith-* subpaths that resolve to dist, and its
# integration tests spawn the built dist/main.js — build the chain AND the
# CLI before running the tests.
- name: Build workspace dependencies + CLI
run: |
npm run build --workspace @beyondnet/evolith-contracts
npm run build --workspace @beyondnet/evolith-core-domain
npm run build --workspace @beyondnet/evolith-agent-runtime
npm run build --workspace @beyondnet/evolith-sdk
npm run build --workspace @beyondnet/evolith-core
npm run build --workspace @beyondnet/evolith-infra-providers
npm run build --workspace @beyondnet/evolith-mcp
npm run build --workspace src/sdk/cli
# CD gate = the evolith-cli unit suite (fast, deterministic). The full e2e
# (env-sensitive: spawns servers, loads rulesets) is covered by the
# dedicated sdk-cli-ci.yml e2e job + the per-flow E2E playbooks, not here.
- name: Run unit tests
run: npm run test:unit
working-directory: src/sdk/cli
# GT-592 — retrieval quality is a MEASUREMENT, gated here so a ranking
# change cannot be settled by anecdote.
#
# The harness replays a dense baseline recorded once against the real
# Qwen3-Embedding-0.6B sidecar (rag-eval-record.mjs), because a runner
# cannot host a 0.6B-parameter model — but everything that lives in this
# repository (tokenizer, BM25, RRF) is executed for real, from the built
# agent-runtime dist. The recorded baseline is fingerprinted against the
# frozen corpus, so a corpus edit without a re-record fails loudly rather
# than reporting a stale comparison.
#
# The self-tests run FIRST and on purpose: they deliberately damage the
# ranking three ways and assert the gate goes red, so a gate nobody can
# breach is itself a build failure.
- name: Retrieval eval self-tests (the gate must be able to fail)
run: node --test .harness/scripts/ci/rag-eval.test.mjs
- name: Retrieval eval gate (hybrid vs dense-only, fixed query set)
run: node .harness/scripts/ci/rag-eval.mjs
test-core-domain:
name: Test core-domain
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
# Architecture boundary guard: enforces the core-domain inner-layer
# hierarchy on VALUE imports AND the stateless-Core *Repository ban
# (GT-377/ADR-0101 — product/initiative/evidence/decision are context, not
# entities). Parses the import graph only (no type-aware services / built
# deps needed).
- name: Run core-domain architecture boundary guard
run: npm run lint:boundaries --workspace @beyondnet/evolith-core-domain
# GT-382 follow-up: the core-domain OPA integration test loads the real
# compiled policy.wasm. Build it (pinned opa toolchain) so the test runs
# end-to-end; it self-skips if the artifact is absent.
- name: Compile OPA policy to WASM
run: npm run build:policy
- name: Run core-domain tests with coverage
run: npm run test:cov --workspace @beyondnet/evolith-core-domain
# GT-326: end-to-end governance flow (phase → gate → artifact → verdict)
# against a real tmpdir satellite, exercising Core's Tracker/agent
# integration contract (events, webhook dispatch, audit) in CI.
- name: Run core-domain governance E2E (GT-326)
run: npm run test:e2e --workspace @beyondnet/evolith-core-domain
test-core:
name: Test core
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
- name: Run core barrel contract tests
run: npm test --workspace @beyondnet/evolith-core
test-mcp-server:
name: Test mcp-server
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
# Architecture boundary guard: enforces the mcp-server layer hierarchy
# (domain → application → adapters) on VALUE imports. Parses the import
# graph only (no type-aware services), so it needs no built workspace deps.
- name: Run mcp-server architecture boundary guard
run: npm run lint:boundaries --workspace @beyondnet/evolith-mcp
- name: Build workspace dependencies
run: |
npm run build --workspace @beyondnet/evolith-contracts
npm run build --workspace @beyondnet/evolith-core-domain
npm run build --workspace @beyondnet/evolith-agent-runtime
npm run build --workspace @beyondnet/evolith-sdk
npm run build --workspace @beyondnet/evolith-core
npm run build --workspace @beyondnet/evolith-infra-providers
# GT-602 criterion 2: the three rego tool sets are DERIVED from
# AbacEvaluator.toolProjection(), not hand-maintained beside it. Two copies
# of one fact is what let fifteen tools stay FORBIDDEN in production. The
# projection is the runtime's own classifyTool, so this cannot drift from
# what actually decides.
- name: ABAC rego tool sets match the runtime projection
run: node .harness/scripts/generate-abac-tool-sets.mjs --check
# GT-583 criteria 2 and 3: the capability manifest's per-operation
# `inputSchema`/`outputSchema`, and the CLI's `TOOL_SCHEMAS`, are DERIVED
# from ToolRegistryService.operationProjection() — the registry that
# answers `tools/list` describing itself. Three hand-maintained copies of
# one contract is what shipped a THREE-entry CLI catalog for a fifty-tool
# MCP surface, under names no tool has ever had.
- name: Capability operation schemas match the runtime projection
run: node .harness/scripts/generate-capability-operations.mjs --check
# GT-602: compile the rego to the `policy.wasm` the dispatcher actually loads.
# `abac-rego-parity.spec.ts` now compiles it ITSELF when it is missing or stale,
# so this step is no longer what makes the parity check run. It stays because it
# fails fast and unambiguously on a toolchain problem (no opa binary, no network)
# instead of surfacing it as one red test inside the whole jest suite.
- name: Compile OPA policy bundle (policy.wasm)
run: npm run build:policy
# GT-602 criteria 1 and 3 are enforced HERE. `abac-rego-parity.spec.ts` loads the
# COMPILED policy.wasm — the artifact that decides at dispatch — evaluates it over
# every tool name the DI graph registers, and requires ALLOW for an `architect` in
# `production`. A tool that exists in the TypeScript registry and not in the
# compiled policy comes back ABAC-03 and turns this step red. That block never
# skips: a missing bundle is compiled, and a bundle that cannot be compiled is a
# failure. `Test mcp-server` is a required check on `main`.
- name: Run mcp-server tests with coverage
run: npm run test:cov --workspace @beyondnet/evolith-mcp
# GT-330: MCP server dedicated E2E (spawns the live MCP HTTP server)
- name: Build mcp-server + run E2E
run: |
npm run build --workspace @beyondnet/evolith-mcp
npm run --workspace src/packages/mcp-server test:e2e
test-core-api:
name: Test core-api
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
# Architecture boundary guard: enforces the core-api layer hierarchy
# (application → infrastructure → presentation; openapi cross-cutting) on
# VALUE imports. Parses the import graph only (no type-aware services), so
# it needs no built workspace deps.
- name: Run core-api architecture boundary guard
run: npm run lint:boundaries --workspace core-api
- name: Build workspace dependencies
run: |
npm run build --workspace @beyondnet/evolith-contracts
npm run build --workspace @beyondnet/evolith-core-domain
npm run build --workspace @beyondnet/evolith-infra-providers
- name: Run core-api tests
run: npm test --workspace core-api
# GT-330: core-api dedicated E2E (real HTTP surface — health/metrics/v1)
- name: Run core-api E2E
run: npm run --workspace core-api test:e2e
# W-Coverage: the SDK REST/MCP clients ship to consumers but ran in NO CI job.
test-sdk-client:
name: Test sdk-client
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
- name: Build workspace dependencies
run: |
npm run build --workspace @beyondnet/evolith-contracts
npm run build --workspace @beyondnet/evolith-core-domain
npm run build --workspace @beyondnet/evolith-agent-runtime
- name: Run sdk-client tests with coverage
run: npm run test:cov --workspace @beyondnet/evolith-sdk
# GT-565: this suite existed, passed, and ran in NO workflow. A contract test CI
# never executes is the same false-signal class it exists to catch -- it is the
# mechanism that would have caught the sdk-client type drift, and it was
# disconnected the whole time the drift went unnoticed.
test-contract:
name: Test contract (SDK wire fidelity)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
# The runtime layer boots core-api, and the compile-time layer resolves the
# SDK against the BUILT core-domain, so both must exist before the run.
- name: Build workspace dependencies
run: |
# ORDER MATTERS. The contract suite compiles core-api's module chain and
# the CLI's commands, so it needs every package those import, built in
# dependency order: core must precede mcp (mcp imports it), and
# core-domain must precede everything (its subpath exports, e.g.
# `common/request-context`, only resolve once built).
#
# Derived empirically, not guessed: every dist was moved aside to
# reproduce a clean CI checkout, then packages were added one at a time
# until `npm run test:contract` went 43/43. The previous list stopped at
# core-domain + agent-runtime, which is why this job had never once
# passed since it was wired.
npm run build --workspace @beyondnet/evolith-contracts
npm run build --workspace @beyondnet/evolith-contracts
npm run build --workspace @beyondnet/evolith-core-domain
npm run build --workspace @beyondnet/evolith-infra-providers
npm run build --workspace @beyondnet/evolith-core
npm run build --workspace @beyondnet/evolith-agent-runtime
# sdk BEFORE mcp: mcp's agent.tools.ts imports @beyondnet/evolith-sdk.
npm run build --workspace @beyondnet/evolith-sdk
npm run build --workspace @beyondnet/evolith-mcp
- name: Run contract tests
run: npm run test:contract
# W-Coverage: the filesystem/logger/config/github adapters ran in NO CI job.
test-infra-providers:
name: Test infra-providers
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
# infra-providers depends on @beyondnet/evolith-core-domain (resolved from dist).
- name: Build workspace dependencies
run: |
npm run build --workspace @beyondnet/evolith-contracts
npm run build --workspace @beyondnet/evolith-core-domain
- name: Run infra-providers tests with coverage
run: npm run test:cov --workspace @beyondnet/evolith-infra-providers
# GT-578 follow-up: three workspaces had specs and no job, so ~340 tests ran
# nowhere. That is not hypothetical — `@beyondnet/evolith-contracts` carried a
# spec asserting two deliberately different path conventions were identical, so
# it could never pass, and it sat red and unseen because nothing executed it.
# Two of the three ship to npm; agent-runtime's suite includes the guard that
# freezes the published package's public surface.
test-agent-runtime:
name: Test agent-runtime
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
- name: Build workspace dependencies
run: |
npm run build --workspace @beyondnet/evolith-contracts
npm run build --workspace @beyondnet/evolith-core-domain
- name: Run agent-runtime tests
run: npm run test --workspace @beyondnet/evolith-agent-runtime
test-agent-runtime-api:
name: Test agent-runtime-api
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
- name: Build workspace dependencies
run: |
npm run build --workspace @beyondnet/evolith-contracts
npm run build --workspace @beyondnet/evolith-core-domain
npm run build --workspace @beyondnet/evolith-agent-runtime
- name: Run agent-runtime-api tests
run: npm run test --workspace agent-runtime-api
test-contracts:
name: Test contracts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
# `capability-contract.parity.spec.ts` imports
# `@beyondnet/evolith-core-domain/capabilities/capabilities-manifest`, a
# subpath that resolves through dist. Without this the suite passes locally
# (where a dist already exists from an earlier build) and fails in CI on a
# clean checkout — which is exactly what it did the first time this job ran.
- name: Build workspace dependencies
run: |
npm run build --workspace @beyondnet/evolith-contracts
npm run build --workspace @beyondnet/evolith-core-domain
- name: Run contracts tests
run: npm run test --workspace @beyondnet/evolith-contracts
# GT-578 — the two anti-false-green mechanisms, run on every push and PR.
#
# They live here rather than in docs.yml's `Validate documentation` job on
# purpose: that job is a required check on main, and wiring a brand-new
# reporting-mode guard into a required check is how a reporting-mode guard
# gets deleted. This job is not required yet; promoting it is a branch-
# protection change, made once the evidence corpus is migrated (below).
governance-guards:
name: Governance guards (GT-578)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
- name: Install evidence-command shell dependencies
run: sudo apt-get update && sudo apt-get install -y ripgrep
# FIRST, and deliberately. Every step in this job runs under `bash -e`, so the
# first red step skips every step after it. On run 30629887881 a network-
# dependent guard went red and this block — the layer that checks the guards
# THEMSELVES — never ran, silently, on the one run where a guard's behaviour
# was in question. A self-test that only executes once every guard has already
# passed can confirm a green run and nothing else.
#
# It runs first rather than merely earlier because it needs nothing: no build,
# no registry, no `gh`. Its fixtures are in-repo and its subjects are the guard
# modules. Anything it depends on would be another way for it not to run.
- name: Self-tests for the governance guards
run: |
node --test .harness/scripts/ci/42-validate-guard-denominators.test.mjs
node --test .harness/scripts/ci/48-validate-security-publish-lag.test.mjs
node --test .harness/scripts/ci/49-validate-gap-id-allocation.test.mjs
node --test .harness/scripts/ci/50-validate-gap-claim.test.mjs
node --test .harness/scripts/ci/41-validate-evidence-commands.test.mjs
node --test .harness/scripts/ci/43-validate-guard-negative-fixtures.test.mjs
node --test .harness/scripts/ci/44-validate-adr-implementation-status.test.mjs
node --test .harness/scripts/ci/34-check-skill-registry-parity.test.mjs
node --test .harness/scripts/ci/45-validate-port-inventory-honesty.test.mjs
# Everything below was referenced by NO workflow. Enumerating `.harness` for
# `*.test.mjs` found 51 self-tests and 32 of them ran nowhere — the same defect
# PR #322 closed for one file, at twenty-six times the size. Five were red and
# had been red silently: two audit scripts whose path literals lost the `src/`
# prefix in the refactor, a report writer that died on an unhandled ENOENT off
# the real tree, a fixture whose diff header predated a move, and a guard test
# that GT-556/557 had quietly detached from its own fixture.
#
# A note on what these prove. Most of the GUARDS below also run in no workflow;
# wiring the self-test asserts the guard still behaves, not that anything calls
# it. That second hole is real and deliberately left open here — switching on
# twenty-odd unrun guards at once is a different change with a different blast
# radius, and it should not ride in on a test-wiring commit.
node --test .harness/scripts/ci/07-generate-inventories.test.mjs
node --test .harness/scripts/ci/19-validate-rest-versioning.test.mjs
node --test .harness/scripts/ci/40-validate-path-literals.test.mjs
node --test .harness/scripts/ci/drift-audit.test.mjs
node --test .harness/scripts/ci/parity-gate.test.mjs
node --test .harness/scripts/lib/guard-classification.test.mjs
node --test .harness/scripts/lib/scope.test.mjs
node --test .harness/scripts/ci/agentic/review-input.test.mjs
node --test .harness/scripts/ci/agentic/review-provider.test.mjs
node --test .harness/scripts/ci/agentic/review-result.test.mjs
node --test .harness/scripts/cleanup-temp-files.test.mjs
node --test .harness/scripts/validate-contract-conformance.test.mjs
node --test .harness/scripts/board/atdm-principal.test.mjs
node --test .harness/scripts/board/debt-economics.test.mjs
node --test .harness/scripts/board/new-row-economics-guard.test.mjs
node --test .harness/scripts/run-evolith-intelligent-data-audit.test.mjs
node --test .harness/scripts/run-evolith-sdlc-interface-audit.test.mjs
# The four `rag-*` suites, which are NOT the special case they look like. Each
# file states in its own header that the DB client and the sidecar `fetch` are
# stubbed and "the network is never touched", and all four pass offline with no
# skips — no model endpoint, no pgvector, nothing to provision. The live
# integration against a real endpoint is a separate thing that does not exist
# here; these are unit tests over the adapter and the chunker.
node --test .harness/scripts/ci/rag-sync.test.mjs
node --test .harness/scripts/ci/rag-pgvector.test.mjs
node --test .harness/scripts/ci/rag-embed-qwen3.test.mjs
node --test .harness/scripts/ci/rag-embed-integration.test.mjs
# The dead-reference budget below was measured on a BUILT tree. A handful of
# recorded commands legitimately name build outputs (`dist/main.js`), which
# a clean checkout reports as dead — so without this the ratchet drifts by
# the number of dist references and fails for a reason that has nothing to
# do with the corpus. Building first makes the count reproducible.
- name: Build workspace dependencies
run: |
npm run build --workspace @beyondnet/evolith-contracts
npm run build --workspace @beyondnet/evolith-core-domain
npm run build --workspace @beyondnet/evolith-infra-providers
npm run build --workspace @beyondnet/evolith-agent-runtime
# GT-642: the cycle guard RUNS GT-589's extractor rather than owning a second
# one, so the extractor has to be built or the guard has nothing to run. It
# fails loudly in that case instead of reporting "no cycles found".
npm run build --workspace @beyondnet/evolith-repo-facts
# STRICT. Every script under .harness/scripts/ci must either refuse a
# zero-element scan or be registered, with a reason, as something that is
# not a corpus scan. A new guard that can pass vacuously fails here, which
# is the whole point: the sweep is worthless if the 34th guard starts
# unprotected.
- name: Every CI guard publishes a denominator
run: node .harness/scripts/ci/42-validate-guard-denominators.mjs --verbose
# OBSERVED, not declared. 42 checks that each guard CAN refuse an empty
# scan; this one points every scanning guard at a deliberately empty
# repo-shaped tree and watches it go red. A guard nobody has ever seen fail
# is the defect this whole sweep is about, and the distinction between
# "has a vacuity check" and "the vacuity check fires" is where the last
# four false greens in this repository came from.
- name: Every scanning guard has been SEEN failing
run: node .harness/scripts/ci/43-validate-guard-negative-fixtures.mjs
# GT-607: seven ADRs were Accepted with no implementing code. The guard
# falsifies an author DECLARATION rather than guessing at implementing
# code — a grep-based signal was tried against this corpus and rejected,
# with the reasoning in the guard header. Report mode: 212 Accepted ADRs
# still declare nothing and are baselined; `--strict` is the switch when
# that baseline reaches zero.
- name: ADR implementation-status declarations resolve
run: node .harness/scripts/ci/44-validate-adr-implementation-status.mjs
# GT-424: the three skill registries. R3 measures the EFFECTIVE routing
# surface and audits GT-608's derivation rather than trusting it — a
# synthesis narrowed by one extra conjunct silently unreaches capabilities
# and would otherwise keep this green.
- name: Skill registries agree in both directions
run: node .harness/scripts/ci/34-check-skill-registry-parity.mjs
# GT-621: a diagram published "12 hexagonal ports · 30 adapters". The
# numbers were stale AND the sentence read as delivered capability when it
# described a declared surface — 11 of 19 ports are reachable from the
# runtime; the rest are seams with adapters and no consumer. The hot path
# is DERIVED from `AgentRuntimeDeps`, so it cannot rot the way the
# hand-typed count did.
- name: A port inventory does not read as a capability claim
run: node .harness/scripts/ci/45-validate-port-inventory-honesty.mjs --verbose
# GT-644: a policy calling an OPA builtin the shipped wasm runtime cannot
# execute passes `opa test` and `opa build`, then throws "not implemented:
# built-in function N" at evaluation time — which OpaEvaluator turns into
# "OPA engine error — enforcement blocked" for EVERY rule in the run. One
# `time.parse_rfc3339_ns` therefore fails the whole OPA half of the gate with
# a message that reads like an infrastructure hiccup. The authority is the
# COMPILED bundle's `builtins()` table, so the bundle is built first;
# `--require-wasm` makes a bundle that could not be built fatal rather than a
# source scan wearing a green tick.
#
# Placed HERE, ahead of the three checks below, on purpose: those three ask
# the network (npm, the base branch, the pull-request list) and this one is a
# deterministic read of this checkout. `bash -e` stops the job at the first
# failure, so a live-GitHub answer standing in front of a repository check
# means the repository check never runs at all — which is what happened on
# run 30629887881, where an unrelated contested claim skipped both steps.
- name: Compile the OPA bundle the builtin check reads
run: npm run build:policy
- name: Policies only call builtins the wasm runtime implements
run: |
node .harness/scripts/ci/55-validate-wasm-builtin-support.mjs --require-wasm --verbose
node --test .harness/scripts/ci/55-validate-wasm-builtin-support.test.mjs
# GT-624: the 2026-07-23 security wave sat unpublished until 2026-07-27
# while SECURITY.md declared the 1.1.x line "actively patched". Nothing
# detected it; an audit did. This asks the registry what is published, finds
# the commit where each package's published version was SET, and fails on any
# commit after it whose type or scope marks it as security. It needs the
# network — a registry answer is the only honest source for "published", as
# the newest v* tag here is v1.1.0 while npm serves 1.2.2.
- name: No security fix is committed but unpublished
run: node .harness/scripts/ci/48-validate-security-publish-lag.mjs --verbose
# GT-642: two runtime import cycles reached a 346-module package unseen and were
# found by hand (GT-641), because nothing looks — lint:boundaries enforces layer
# DIRECTION and not acyclicity, and tsc compiles a require cycle silently. This
# runs GT-589's extractor over EVERY workspace package and prints the per-package
# table on every run, zeros included: a guard whose green run reports nothing is
# indistinguishable from one that cannot run. Cycles that close only through an
# `import type` edge are listed and are deliberately NOT fatal.
- name: No runtime import cycle in any workspace package
run: node .harness/scripts/ci/54-validate-import-cycles.mjs --verbose
# GT-647: a runner stage lists the workspace packages it copies BY HAND, and
# node_modules/@beyondnet/* are symlinks into src/packages/* — so a package
# left off the list dangles and the container dies at its first require. On
# 2026-07-31 GT-604 added one import to infra-providers and silently broke
# three images (core-api, mcp-server, cli), which nothing detected: the build
# is green, the push is green, and the only symptom is a chaos-drill timeout.
# The closure is COMPUTED transitively here, because at direct-dependency
# depth every one of those Dockerfiles was already complete.
- name: Every image ships its full workspace closure
run: |
node .harness/scripts/ci/56-validate-docker-workspace-closure.mjs --verbose
node --test .harness/scripts/ci/56-validate-docker-workspace-closure.test.mjs
# GT-435 — nothing has ever run in production, so nothing has ever pulled these images and
# a green CI proves nothing about them. On 2026-08-02 all three charts named a tag no
# workflow produces; one of them (agent-runtime 0.1.0) had no matching git tag at all, so
# it was unproducible by any path. The day the server exists is not the day to find out.
- name: Charts request images this repository can produce
run: node .harness/scripts/ci/58-validate-deployable-images.mjs
# GT-650 / ADR-0125 — the artifact registry is the accepted single declaration, and the gate
# corpus is still hand-maintained while the migration lands. For as long as both exist, the
# only thing making that intermediate state safe is that they are checked to agree. This
# guard dies with `57` when the gates derive from the registry.
- name: The artifact registry agrees with the gate corpus
run: node .harness/scripts/ci/59-validate-artifact-registry.mjs
# GT-650 — the served gate corpus is now GENERATED from the registry and the gates, so the
# two copies cannot disagree by construction. `57-validate-gate-corpora-parity` compared them
# and is deleted with this change: a guard kept past its cause becomes noise, and noise
# trains people to skip red.
- name: The served gate corpus is current
run: node .harness/scripts/generate-phase-gates-rules.mjs --check
# GT-591: the OPA pin sat at 0.65.0 for two years and nothing said so, while a
# SECOND spelling of it lived in compile-opa-wasm.mjs's download URL — so the
# policies could be tested by one compiler and the enforcing wasm built by
# another, both green. Now on the v1 line, where `if`/`contains` are mandatory,
# so the pin and the Rego dialect are one fact: the guard also fails on any
# `.rego` that stops declaring `import rego.v1`.
- name: One OPA pin, on the v1 line, and no second spelling of it
run: node .harness/scripts/ci/53-validate-opa-pin.mjs --verbose
# GT-638: a gap id is chosen by reading the highest GT-* on whichever branch
# you are on, so two parallel sessions allocate the same number and find out
# at merge time. `8449af3d` had to renumber GT-634 -> GT-637 for exactly this.
# The comparison needs the BASE branch, which a shallow checkout does not
# have — fetch it explicitly rather than letting the guard fail for the wrong
# reason.
- name: One gap id, one gap
run: |
git fetch --no-tags --depth=1 origin "${{ github.base_ref || github.event.repository.default_branch }}"
node .harness/scripts/ci/49-validate-gap-id-allocation.mjs --verbose
# GT-639: a board row carries a STATUS but not who is working it or where, so
# two sessions can both read `PENDING` and both start. On 2026-07-30 the same
# work was done twice, three times over. The claim set is DERIVED from open
# pull requests — never hand-written, which would go stale in the direction
# that matters — and an id claimed by two open PRs fails here, naming both.
#
# A claim is DECLARED: an id in the title, in the branch name, or in the body
# after a `Closes`/`Advances`/`Claims` marker. Reading it from any `GT-*` in
# the prose made every cross-reference a claim, and the bodies here cite their
# neighbours constantly — run 30630343658 failed three times over, none of them
# real, and the fix each author reached for was to delete the sentence.
#
# A claim is ALSO what the DIFF did (GT-645): a board row whose status it
# moves, or a closure-evidence record it writes. Prose alone missed the other
# direction — run 30633517559 read ONE claim off #326 and exited green on a
# pull request that had flipped eight rows. `GH_TOKEN` now covers a `gh pr
# diff` per open pull request as well as the list; a diff it cannot read is a
# hard failure, never a run that quietly checked the prose alone.
- name: One gap, one claim
env:
GH_TOKEN: ${{ github.token }}
run: node .harness/scripts/ci/50-validate-gap-claim.mjs
# GT-578 AC3: this is now a governed execution gate, not a report. It
# resolves every validationCommand recorded in the board's closure
# evidence and executes the read-only subset. Strict mode fails on dead
# referents, non-zero executions, inconclusive search exits, vacuous scans,
# or any command that dirties the checkout.
#
# The remaining budget of 17 is not live debt: it is the clean-checkout
# basis for generated/gitignored referents (`dist/`, `node_modules/`,
# `.harness/bin/`) that are materialized by build/download steps. Lower it
# only from the number this job prints; at 0, delete `--max-dead`.
- name: Execute the board's recorded validationCommands (strict)
run: node .harness/scripts/ci/41-validate-evidence-commands.mjs --execute --strict --max-dead 17 --verbose
# RETIRED 2026-07-27 (GT-570): the `publish-npm` job lived here and published
# ONLY `src/sdk/cli`, with `--tag beta` and no provenance, writing the token
# into a .npmrc via `npm config set`. It could not publish the other seven
# public packages at all — including @beyondnet/evolith-mcp, which carries the
# 2026-07-23 security wave — which is why every version on the registry has no
# dist.attestations. Publishing now lives in `npm-release.yml`: all eight
# workspaces, in dependency order, with --provenance, idempotent against the
# registry, and rehearsable via a dry run that defaults to on.
docker-services:
name: Build & Push Services (GHCR)
needs: [test, test-core-domain, test-core, test-mcp-server, test-core-api, test-sdk-client, test-infra-providers]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
# context is the repo root — both Dockerfiles COPY repo-root-relative paths
- service: core-api
context: .
dockerfile: ./src/apps/core-api/Dockerfile
image: evolith-core-api
- service: mcp-server
context: .
dockerfile: ./src/packages/mcp-server/Dockerfile
image: evolith-mcp
# GT-437: agent-runtime-api had a Dockerfile but was never built/pushed
# on main by this workflow (only by docker-images.yml on tags/dispatch).
- service: agent-runtime
context: .
dockerfile: ./src/apps/agent-runtime-api/Dockerfile
image: evolith-agent-runtime
steps:
- uses: actions/checkout@v7
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push ${{ matrix.service }}
uses: docker/build-push-action@v5
with:
context: ${{ matrix.context }}
file: ${{ matrix.dockerfile }}
push: true
tags: |
ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}:latest
ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}:${{ github.sha }}
# GT-324: deploy the freshly-pushed images to the Coolify runtime.
#
# El fail-soft cubre UN solo modo de fallo: secrets ausentes. Sin
# COOLIFY_API_TOKEN o sin el deploy-hook del servicio, el step avisa y sale 0,
# asi que el workflow es seguro de mergear antes de configurar CD.
#
# Con los secrets PUESTOS, este job falla duro y debe hacerlo: `curl --fail`
# sale distinto de cero si el hook no responde, y eso significa que el
# despliegue no ocurrio. El comentario anterior decia "never fails CD", que era
# falso y ademas indeseable -- un step de despliegue que no puede fallar no
# informa de nada. Si esto esta rojo, mira el runtime antes que el workflow.
#
# GT-567: APAGADO A PROPOSITO via `vars.VPS_DEPLOY_ENABLED`. El entorno
# objetivo es hoy Docker + kind local (cluster `evolith-cluster`); el VPS se
# retoma mas adelante. Sin este flag el job fallaba en CADA push a `main`
# desde 2026-07-19 contra un host fuera de alcance, y como no es check
# requerido nadie leia el rojo.
#
# Se apaga con una VARIABLE y no borrando los secrets a proposito: borrarlos
# deja el apagado invisible --dentro de un mes el job simplemente saltaria y
# nadie sabria si fue deliberado-- y sus valores solo se pueden reponer desde
# el panel de Coolify, o sea con el VPS levantado. Aqui queda escrito el
# porque, y reactivar es UN paso: `gh variable set VPS_DEPLOY_ENABLED --body true`.
deploy:
name: Deploy services (Coolify)
needs: [docker-services]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && vars.VPS_DEPLOY_ENABLED == 'true'
steps:
# GT-324 — HARDENED 2026-08-02. Each service used to check its own hook and, when unset,
# warn and `exit 0`. That is right for "not configured yet" and WRONG for "half configured":
# set the token, typo one hook, and CD reports success while that service never deploys —
# and the only symptom is an old image serving traffic nobody is looking at.
#
# The two states are now separated. Nothing configured is a skip; PARTIALLY configured is a
# failure that names how many of the four values are present. Same contract as the Tracker's
# `images.yml`, so both repositories fail the same way.
- name: Classify the deploy configuration
id: cfg
env:
TOKEN: ${{ secrets.COOLIFY_API_TOKEN }}
HOOK_CORE: ${{ secrets.COOLIFY_COREAPI_DEPLOY_HOOK }}
HOOK_MCP: ${{ secrets.COOLIFY_MCP_DEPLOY_HOOK }}
HOOK_AGENT: ${{ secrets.COOLIFY_AGENTRUNTIME_DEPLOY_HOOK }}
run: |
set=0; unset=0
for v in "$TOKEN" "$HOOK_CORE" "$HOOK_MCP" "$HOOK_AGENT"; do
if [ -n "$v" ]; then set=$((set+1)); else unset=$((unset+1)); fi
done
if [ "$set" -eq 0 ]; then
echo "::warning::Coolify is not configured (no token, no hooks). Skipping the deploy. Set COOLIFY_API_TOKEN and the three hooks to enable CD (GT-324)."
echo "state=absent" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$unset" -gt 0 ]; then
echo "::error::Coolify is PARTIALLY configured: $set of 4 values present. A half-wired deploy reports success while some services never ship, so this fails instead of warning. Set all of COOLIFY_API_TOKEN, COOLIFY_COREAPI_DEPLOY_HOOK, COOLIFY_MCP_DEPLOY_HOOK and COOLIFY_AGENTRUNTIME_DEPLOY_HOOK, or unset them all."
exit 1
fi
echo "state=complete" >> "$GITHUB_OUTPUT"
- name: Trigger the deploys
if: steps.cfg.outputs.state == 'complete'
env:
TOKEN: ${{ secrets.COOLIFY_API_TOKEN }}
HOOK_CORE: ${{ secrets.COOLIFY_COREAPI_DEPLOY_HOOK }}
HOOK_MCP: ${{ secrets.COOLIFY_MCP_DEPLOY_HOOK }}
HOOK_AGENT: ${{ secrets.COOLIFY_AGENTRUNTIME_DEPLOY_HOOK }}
run: |
failed=0
trigger() {
# `--fail` matters: without it curl exits 0 on a 4xx and a REJECTED hook reads as a
# successful deploy.
if curl --fail --silent --show-error -X GET "$2" -H "Authorization: Bearer $TOKEN" -o /dev/null; then
echo "$1 deploy triggered"
else
echo "::error::$1 deploy hook did not accept the request"
failed=1
fi
}
# All three are attempted before failing, so one bad hook does not hide the state of the
# other two.
trigger core-api "$HOOK_CORE"
trigger mcp-server "$HOOK_MCP"
trigger agent-runtime "$HOOK_AGENT"
exit $failed
# Triggering is not deploying: Coolify works asynchronously, so a green step means the
# REQUEST was accepted, never that the new image is serving. GT-448 asks for a deploy
# evidenced by a recorded run against the live service, and this job cannot provide it.
- name: What this job did and did not prove
if: steps.cfg.outputs.state == 'complete'
run: |
{
echo '### Deploy triggered'
echo ''
echo 'Coolify accepted the webhook for all three services at `${{ github.sha }}`.'
echo ''
echo '> This proves the REQUEST was accepted. It does not prove the new image is'
echo '> serving traffic. GT-448 asks for a recorded run against the live service.'
} >> "$GITHUB_STEP_SUMMARY"