release: v1.135.0 + docs daily update 2026-08-22 #3007
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Merge Gate | |
| on: | |
| pull_request: | |
| branches: | |
| - main | |
| # Run the same gate when GitHub's merge queue forms a merge group, so the | |
| # "Merge Gate" required check reports a status for queued PRs. Without this, | |
| # a merge-queue-required check would never run and PRs would be stuck. | |
| # https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue#triggering-merge-group-checks-with-github-actions | |
| merge_group: | |
| jobs: | |
| detect-changes: | |
| name: Detect Changes | |
| runs-on: ubuntu-latest | |
| # Checkout only; GITHUB_TOKEN needs nothing else. | |
| permissions: | |
| contents: read | |
| outputs: | |
| lint: ${{ steps.check.outputs.lint }} | |
| test: ${{ steps.check.outputs.test }} | |
| pi_skills: ${{ steps.check.outputs.pi_skills }} | |
| seed_skills: ${{ steps.check.outputs.seed_skills }} | |
| script_types: ${{ steps.check.outputs.script_types }} | |
| openapi: ${{ steps.check.outputs.openapi }} | |
| vendored_openapi: ${{ steps.check.outputs.vendored_openapi }} | |
| docker: ${{ steps.check.outputs.docker }} | |
| ui: ${{ steps.check.outputs.ui }} | |
| templates_workflows: ${{ steps.check.outputs.templates_workflows }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| - name: Determine base for diff | |
| id: base | |
| run: | | |
| set -euo pipefail | |
| if [ "${{ github.event_name }}" = "merge_group" ]; then | |
| # merge_group events have no github.base_ref; diff against the | |
| # merge group's base SHA (already reachable via fetch-depth: 0). | |
| echo "ref=${{ github.event.merge_group.base_sha }}" >> "$GITHUB_OUTPUT" | |
| else | |
| git fetch origin "${{ github.base_ref }}" | |
| echo "ref=origin/${{ github.base_ref }}" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Compute per-domain change flags | |
| id: check | |
| run: | | |
| set -euo pipefail | |
| CHANGED=$(git diff --name-only ${{ steps.base.outputs.ref }}...HEAD) | |
| echo "Changed files:" | |
| echo "$CHANGED" | |
| echo "---" | |
| # Always re-run everything if the merge-gate workflow itself changed. | |
| GATE_CHANGED=$(echo "$CHANGED" | grep -E '^\.github/workflows/merge-gate\.yml$' || true) | |
| match() { | |
| local pattern="$1" | |
| if [ -n "$GATE_CHANGED" ]; then | |
| echo "true"; return | |
| fi | |
| if echo "$CHANGED" | grep -E "$pattern" > /dev/null; then | |
| echo "true" | |
| else | |
| echo "false" | |
| fi | |
| } | |
| # lint + tsc:check: biome runs on src/, tsc covers src/ (scripts excluded in tsconfig). | |
| # bunfig.toml (linker/test config), turbo.json, member manifests (root frozen | |
| # install), and the dep-graph inputs (.dependency-cruiser.cjs, plugin/) are all | |
| # load-bearing for this job since the workspace migration. The Dockerfiles | |
| # feed the Bun version-pin check (check:bun-version). | |
| LINT=$(match '^(src/|apps/evals/|package\.json$|bun\.lock|bunfig\.toml$|turbo\.json$|konsistent\.json$|tsconfig\.json$|biome\.json$|\.dependency-cruiser\.cjs$|plugin/opencode-plugins/|apps/(ui|templates-ui)/package\.json$|Dockerfile$|Dockerfile\.worker$|scripts/check-bun-version\.ts$)') | |
| # tests: src/ + scripts that tests import + lockfile/tsconfig + bunfig ([test] | |
| # preload + pathIgnorePatterns) + member manifests (root frozen install). | |
| TEST=$(match '^(src/|scripts/|package\.json$|bun\.lock|bunfig\.toml$|tsconfig\.json$|apps/(ui|templates-ui|evals)/package\.json$)') | |
| # pi-skills freshness: generator inputs + outputs. | |
| PI_SKILLS=$(match '^(plugin/commands/|plugin/pi-skills/|plugin/build-pi-skills\.ts$)') | |
| # seeded-skill invariants + manifest freshness + the skill seeder tests. | |
| # NOTE: neither LINT nor TEST matches templates/ or plugin/skills/, so | |
| # without this flag a PR touching only those would run nothing but the | |
| # Docker build and could merge a stale bundled-files.generated.json — or a | |
| # baked skill colliding with a seeded one, which is the exact bug | |
| # check-skill-sources.ts exists to prevent. | |
| # | |
| # This must list every INPUT the checks read, not just the seeded content: | |
| # templates/skills/ seeded skills + bundled files | |
| # plugin/skills/ baked skills (duplicate-delivery-path rule) | |
| # integrations-catalog.ts templatePath entries (missing-remote-skill rule) | |
| # Dockerfile.worker npx --skill entries (duplicate-delivery-path rule) | |
| # the four scripts the checkers, generators, and vendor sync | |
| # src/be/seed-skills/ seeder + generated manifest | |
| # skill-fs-writer.ts the FS reconcile the tests cover | |
| SEED_SKILLS=$(match '^(templates/skills/|templates/ai-toolbox\.manifest\.json$|plugin/skills/|plugin/pi-skills/|plugin/commands/|Dockerfile\.worker$|src/be/seed-skills/|src/utils/skill-fs-writer\.ts$|scripts/(build-seed-skill-files|build-skill-md|check-skill-sources|sync-ai-toolbox-skills)\.ts$|apps/ui/src/lib/integrations-catalog\.ts$)') | |
| # script SDK types freshness: source constants + generator + outputs + | |
| # allowlist/registry (generation validates SDK_ALLOWLIST against server.ts). | |
| SCRIPT_TYPES=$(match '^(src/be/scripts/typecheck\.ts$|src/scripts-runtime/types/|src/scripts-runtime/sdk-allowlist\.ts$|scripts/bundle-script-types\.ts$|src/server\.ts$)') | |
| # openapi freshness: routes, types, generator, version, generated artifacts. | |
| OPENAPI=$(match '^(src/http/|src/types\.ts$|scripts/generate-openapi\.ts$|openapi\.json$|docs-site/content/docs/api-reference/|package\.json$)') | |
| VENDORED_OPENAPI=$(match '^(vendored-openapi/|src/be/vendored-openapi\.ts$|src/be/script-connections\.ts$|src/http/script-connections\.ts$|src/tools/script-connections/|scripts/(check|refresh)-vendored-openapi\.ts$|scripts/vendored-openapi-utils\.ts$|package\.json$)') | |
| # docker build test: mirrors docker-and-deploy.yml paths. All Dockerfiles now | |
| # COPY bunfig.toml + the member manifests, .dockerignore shapes the context, | |
| # and apps/evals/ feeds the evals-service image. | |
| DOCKER=$(match '^(src/|vendored-openapi/|Dockerfile$|Dockerfile\.worker$|apps/evals/|\.dockerignore$|docker-entrypoint\.sh$|api-entrypoint\.sh$|package\.json$|bun\.lock|bunfig\.toml$|tsconfig\.json$|scripts/|templates/|plugin/|deploy/|apps/(ui|templates-ui)/package\.json$)') | |
| # ui app lint + typecheck. ui's dependency tree is resolved by the ROOT | |
| # bun.lock + root overrides since the workspace migration. | |
| UI=$(match '^(apps/ui/|bun\.lock|package\.json$|bunfig\.toml$)') | |
| # workflow template validation: content.md changes in templates/workflows/. | |
| TEMPLATES_WORKFLOWS=$(match '^(templates/workflows/|scripts/validate-template-workflows\.ts$|src/workflows/definition\.ts$|src/types\.ts$)') | |
| for k in LINT TEST PI_SKILLS SEED_SKILLS SCRIPT_TYPES OPENAPI VENDORED_OPENAPI DOCKER UI TEMPLATES_WORKFLOWS; do | |
| v=$(eval echo \$$k) | |
| key=$(echo "$k" | tr '[:upper:]' '[:lower:]') | |
| echo "${key}=${v}" >> $GITHUB_OUTPUT | |
| echo "${key}=${v}" | |
| done | |
| lint-and-typecheck: | |
| name: Lint and Type Check | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.lint == 'true' | |
| runs-on: ubuntu-latest | |
| # Checkout only; GITHUB_TOKEN needs nothing else. | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| with: | |
| bun-version-file: package.json | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| # bun.lock must stay deduped. A removable duplicate ships two copies of the | |
| # same dep (Biome, zod and openai were the last offenders), so the version a | |
| # given import resolves to depends on where in the tree it sits. Runs in | |
| # milliseconds. Like the Bun version pin check it needs no timing series. | |
| - name: Check for duplicate lockfile versions | |
| run: bun dedupe --check | |
| - name: Run Biome linter | |
| id: lint | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| bun run lint | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| - name: Run TypeScript type check | |
| id: tsc | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| bun run tsc:check | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| - name: Run structural consistency lint | |
| id: structural-lint | |
| continue-on-error: true | |
| env: | |
| KONSISTENT_NO_UPDATE_CHECK: "true" | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| bun run lint:structure | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| # evals is now a Bun workspace member, so the root `bun install --frozen-lockfile` | |
| # above already installs its deps into the shared hoisted node_modules. No separate | |
| # `cd apps/evals && bun install` is needed. | |
| - name: Type check evals | |
| id: evals-tsc | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| bun run tsc:check | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| working-directory: apps/evals | |
| - name: Test evals | |
| id: evals-test | |
| # evals is its own bun package; these are pure unit tests (scoring, | |
| # registry, normalize, scenario structure) — no E2B/network, ~0.5s. | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| bun test | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| working-directory: apps/evals | |
| - name: Build evals UI | |
| id: evals-ui-build | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| bun run ui:build | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| working-directory: apps/evals | |
| - name: Check worker/API DB boundary | |
| id: db-boundary | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| bash scripts/check-db-boundary.sh | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| - name: Check test spawnSync boundary | |
| id: test-spawn-sync-boundary | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| bash scripts/check-test-spawn-sync.sh | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| - name: Check async DB seam | |
| id: async-db-seam | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| bash scripts/check-async-db-seam.sh | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| - name: Check floating promises | |
| id: floating-promises | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| bun scripts/check-floating-promises.ts | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| - name: Check promise sinks | |
| id: promise-sinks | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| bun scripts/check-promise-sinks.ts | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| - name: Check API_KEY env-access boundary | |
| id: api-key-boundary | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| bash scripts/check-api-key-boundary.sh | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| - name: Check RBAC authorization boundary | |
| id: rbac-boundary | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| bash scripts/check-rbac-boundary.sh | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| - name: Check dependency graph (worker/API boundary) | |
| id: dependency-graph | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| bun run check:dep-graph | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| - name: Check DB audit columns | |
| id: audit-columns | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| bash scripts/check-audit-columns.sh | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| - name: Check SDK tool registration | |
| id: sdk-tool-registration | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| bun scripts/check-sdk-tool-registration.ts | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| - name: Check RBAC coverage (tools, verbs, routes) | |
| id: rbac-coverage | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| bun scripts/check-rbac-coverage.ts | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| - name: Check OpenAPI response-schema coverage | |
| run: bun scripts/check-openapi-response-coverage.ts | |
| # Dockerfile `FROM oven/bun:<tag>` pins must equal package.json packageManager | |
| # (the version setup-bun installs in every job above). | |
| - name: Check Bun version pins | |
| run: bun run check:bun-version | |
| # Same-run calls merge server-side. See agent-fs docs/ci-timings.md. | |
| # Skipped on merge_group events and forks (no secret); never blocks the gate. | |
| - name: Report check timings to ci-timings | |
| if: always() && github.event_name == 'pull_request' | |
| continue-on-error: true | |
| env: | |
| TOKEN: ${{ secrets.SWARM_CI_TIMINGS_TOKEN }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| PR_NUMBER: ${{ github.event.pull_request.number || 'null' }} | |
| LINT_DURATION: ${{ steps.lint.outputs.duration_ms || '0' }} | |
| LINT_STATUS: ${{ steps.lint.outputs.status || 'skipped' }} | |
| TSC_DURATION: ${{ steps.tsc.outputs.duration_ms || '0' }} | |
| TSC_STATUS: ${{ steps.tsc.outputs.status || 'skipped' }} | |
| EVALS_TSC_DURATION: ${{ steps.evals-tsc.outputs.duration_ms || '0' }} | |
| EVALS_TSC_STATUS: ${{ steps.evals-tsc.outputs.status || 'skipped' }} | |
| EVALS_TEST_DURATION: ${{ steps.evals-test.outputs.duration_ms || '0' }} | |
| EVALS_TEST_STATUS: ${{ steps.evals-test.outputs.status || 'skipped' }} | |
| EVALS_UI_BUILD_DURATION: ${{ steps.evals-ui-build.outputs.duration_ms || '0' }} | |
| EVALS_UI_BUILD_STATUS: ${{ steps.evals-ui-build.outputs.status || 'skipped' }} | |
| STRUCTURAL_LINT_DURATION: ${{ steps.structural-lint.outputs.duration_ms || '0' }} | |
| STRUCTURAL_LINT_STATUS: ${{ steps.structural-lint.outputs.status || 'skipped' }} | |
| DB_BOUNDARY_DURATION: ${{ steps.db-boundary.outputs.duration_ms || '0' }} | |
| DB_BOUNDARY_STATUS: ${{ steps.db-boundary.outputs.status || 'skipped' }} | |
| TEST_SPAWN_SYNC_BOUNDARY_DURATION: ${{ steps.test-spawn-sync-boundary.outputs.duration_ms || '0' }} | |
| TEST_SPAWN_SYNC_BOUNDARY_STATUS: ${{ steps.test-spawn-sync-boundary.outputs.status || 'skipped' }} | |
| API_KEY_BOUNDARY_DURATION: ${{ steps.api-key-boundary.outputs.duration_ms || '0' }} | |
| API_KEY_BOUNDARY_STATUS: ${{ steps.api-key-boundary.outputs.status || 'skipped' }} | |
| RBAC_BOUNDARY_DURATION: ${{ steps.rbac-boundary.outputs.duration_ms || '0' }} | |
| RBAC_BOUNDARY_STATUS: ${{ steps.rbac-boundary.outputs.status || 'skipped' }} | |
| DEPENDENCY_GRAPH_DURATION: ${{ steps.dependency-graph.outputs.duration_ms || '0' }} | |
| DEPENDENCY_GRAPH_STATUS: ${{ steps.dependency-graph.outputs.status || 'skipped' }} | |
| AUDIT_COLUMNS_DURATION: ${{ steps.audit-columns.outputs.duration_ms || '0' }} | |
| AUDIT_COLUMNS_STATUS: ${{ steps.audit-columns.outputs.status || 'skipped' }} | |
| SDK_TOOL_REGISTRATION_DURATION: ${{ steps.sdk-tool-registration.outputs.duration_ms || '0' }} | |
| SDK_TOOL_REGISTRATION_STATUS: ${{ steps.sdk-tool-registration.outputs.status || 'skipped' }} | |
| RBAC_COVERAGE_DURATION: ${{ steps.rbac-coverage.outputs.duration_ms || '0' }} | |
| RBAC_COVERAGE_STATUS: ${{ steps.rbac-coverage.outputs.status || 'skipped' }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$TOKEN" ]; then echo "SWARM_CI_TIMINGS_TOKEN not available; skipping"; exit 0; fi | |
| BOUNDARIES_DURATION=$((STRUCTURAL_LINT_DURATION + DB_BOUNDARY_DURATION + TEST_SPAWN_SYNC_BOUNDARY_DURATION + API_KEY_BOUNDARY_DURATION + RBAC_BOUNDARY_DURATION + DEPENDENCY_GRAPH_DURATION + AUDIT_COLUMNS_DURATION + SDK_TOOL_REGISTRATION_DURATION + RBAC_COVERAGE_DURATION)) | |
| BOUNDARIES_STATUS=success | |
| for status in "$STRUCTURAL_LINT_STATUS" "$DB_BOUNDARY_STATUS" "$TEST_SPAWN_SYNC_BOUNDARY_STATUS" "$API_KEY_BOUNDARY_STATUS" "$RBAC_BOUNDARY_STATUS" "$DEPENDENCY_GRAPH_STATUS" "$AUDIT_COLUMNS_STATUS" "$SDK_TOOL_REGISTRATION_STATUS" "$RBAC_COVERAGE_STATUS"; do | |
| if [ "$status" = failure ]; then BOUNDARIES_STATUS=failure; break; fi | |
| if [ "$status" = skipped ]; then BOUNDARIES_STATUS=skipped; fi | |
| done | |
| BODY=$(jq -n \ | |
| --arg repo "$GITHUB_REPOSITORY" \ | |
| --arg branch "${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}" \ | |
| --arg sha "$HEAD_SHA" \ | |
| --arg base "$GITHUB_BASE_REF" \ | |
| --argjson pr "$PR_NUMBER" \ | |
| --arg runId "$GITHUB_RUN_ID" \ | |
| --arg attempt "$GITHUB_RUN_ATTEMPT" \ | |
| --arg url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ | |
| --arg wf "$GITHUB_WORKFLOW" \ | |
| --arg event "$GITHUB_EVENT_NAME" \ | |
| --arg actor "$GITHUB_ACTOR" \ | |
| --argjson lintDur "$LINT_DURATION" --arg lintStatus "$LINT_STATUS" \ | |
| --argjson tscDur "$TSC_DURATION" --arg tscStatus "$TSC_STATUS" \ | |
| --argjson evalsTscDur "$EVALS_TSC_DURATION" --arg evalsTscStatus "$EVALS_TSC_STATUS" \ | |
| --argjson evalsTestDur "$EVALS_TEST_DURATION" --arg evalsTestStatus "$EVALS_TEST_STATUS" \ | |
| --argjson evalsUiBuildDur "$EVALS_UI_BUILD_DURATION" --arg evalsUiBuildStatus "$EVALS_UI_BUILD_STATUS" \ | |
| --argjson boundariesDur "$BOUNDARIES_DURATION" --arg boundariesStatus "$BOUNDARIES_STATUS" \ | |
| '{repo:$repo, branch:$branch, sha:$sha, baseBranch:$base, prNumber:$pr, | |
| runId:$runId, runAttempt:$attempt, runUrl:$url, workflow:$wf, | |
| event:$event, actor:$actor, | |
| actions:[ | |
| {name:"lint", group:"checks", durationMs:$lintDur, status:$lintStatus}, | |
| {name:"tsc:check", group:"checks", durationMs:$tscDur, status:$tscStatus}, | |
| {name:"evals:tsc", group:"checks", durationMs:$evalsTscDur, status:$evalsTscStatus}, | |
| {name:"evals:test", group:"checks", durationMs:$evalsTestDur, status:$evalsTestStatus}, | |
| {name:"evals:ui-build", group:"checks", durationMs:$evalsUiBuildDur, status:$evalsUiBuildStatus}, | |
| {name:"checks:boundaries", group:"checks", durationMs:$boundariesDur, status:$boundariesStatus} | |
| ]}') | |
| curl -sSf --max-time 60 -X POST https://api.desplega.agent-swarm.dev/api/x/script/MJlLzJEoRxWG \ | |
| -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d "$BODY" | |
| # Resolve "the latest timings snapshot" ONCE per run and hand the same files | |
| # to both shards as an artifact. The primary key never exists at restore time | |
| # (save-timings creates it at the end of this run), so every restore goes | |
| # through the prefix fallback, and that resolves to the newest matching cache | |
| # at the moment of the call. If each shard restored on its own, a save landing | |
| # between the two calls (main's ci.yml, another PR run) would give them | |
| # different snapshots; --shard assigns files from the durations it is given, | |
| # so the two shards would split two different lists (one file in both, | |
| # another in neither) and the gate would stay green. Runs alongside | |
| # detect-changes (~10 s, no checkout) so it adds nothing to the critical path. | |
| # PR runs read main's entry via restore-keys; nothing on the very first run, | |
| # when --shard splits by count. Listed in the gate's needs so a failure here | |
| # cannot skip the test matrix silently. | |
| restore-timings: | |
| name: Restore test timings | |
| runs-on: ubuntu-latest | |
| # No checkout. Cache restore and artifact upload use the runner token. | |
| permissions: {} | |
| steps: | |
| - name: Restore test timings | |
| id: restore | |
| uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: .bun-test-timings | |
| key: bun-test-timings-${{ github.run_id }} | |
| restore-keys: bun-test-timings- | |
| # The artifact must exist even on a cache miss (download-artifact fails on | |
| # a missing name). The marker also records which snapshot this run used. | |
| - name: Record the resolved snapshot | |
| run: | | |
| mkdir -p .bun-test-timings | |
| echo "${{ steps.restore.outputs.cache-matched-key || 'none' }}" > .bun-test-timings/snapshot-key.txt | |
| ls -la .bun-test-timings | |
| # include-hidden-files: upload-artifact treats every path under a | |
| # dot-directory as hidden and would upload nothing from .bun-test-timings. | |
| - name: Hand the snapshot to the shards | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: timings-snapshot | |
| path: .bun-test-timings | |
| include-hidden-files: true | |
| retention-days: 1 | |
| test: | |
| name: Run Tests (${{ matrix.shard }}/2) | |
| needs: [detect-changes, restore-timings] | |
| if: needs.detect-changes.outputs.test == 'true' | |
| runs-on: ubuntu-latest | |
| # Checkout only. Cache + artifact steps use the runner token, and the | |
| # ci-timings report uses its own secret, so GITHUB_TOKEN needs nothing else. | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2] | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| with: | |
| bun-version-file: package.json | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| # The per-file durations resolved once by restore-timings (see that job for | |
| # why both shards must read the SAME files). Each shard writes only its own | |
| # durations to next/N.json; save-timings merges them into the next cache. | |
| - name: Download test timings | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: timings-snapshot | |
| path: .bun-test-timings | |
| - name: Run tests | |
| id: tests | |
| # apps/evals/ is excluded from the root run via bunfig.toml pathIgnorePatterns | |
| # (it's a separate bun package with its own deps + its own CI test step in | |
| # the lint-and-typecheck job). --parallel=4 runs each test file in its own | |
| # worker process (implies --isolate), 4 at a time per runner; the restored | |
| # per-file durations balance files across the 2 shards by total time. | |
| # --timeout 10000 matches ci.yml (main): 4 workers saturate the runner and | |
| # bunfig.toml no longer sets a global retry. | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| mkdir -p .bun-test-timings/next | |
| echo "timings snapshot: $(cat .bun-test-timings/snapshot-key.txt 2>/dev/null || echo none)" | |
| # shellcheck disable=SC2046 | |
| bun run test:root -- --timeout 10000 --parallel=4 --shard=${{ matrix.shard }}/2 \ | |
| --update-timings --timings=.bun-test-timings/next/${{ matrix.shard }}.json \ | |
| $(ls .bun-test-timings/*.json 2>/dev/null | sed 's/^/--timings=/') | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| - name: Upload this shard's test timings | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: test-timings-${{ matrix.shard }} | |
| path: .bun-test-timings/next/${{ matrix.shard }}.json | |
| retention-days: 1 | |
| if-no-files-found: ignore | |
| # Each shard reports independently. See agent-fs docs/ci-timings.md. | |
| # Skipped on merge_group events and forks (no secret); never blocks the gate. | |
| - name: Report test timing to ci-timings | |
| if: always() && github.event_name == 'pull_request' | |
| continue-on-error: true | |
| env: | |
| TOKEN: ${{ secrets.SWARM_CI_TIMINGS_TOKEN }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| PR_NUMBER: ${{ github.event.pull_request.number || 'null' }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$TOKEN" ]; then echo "SWARM_CI_TIMINGS_TOKEN not available; skipping"; exit 0; fi | |
| BODY=$(jq -n \ | |
| --arg repo "$GITHUB_REPOSITORY" \ | |
| --arg branch "${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}" \ | |
| --arg sha "$HEAD_SHA" \ | |
| --arg base "$GITHUB_BASE_REF" \ | |
| --argjson pr "$PR_NUMBER" \ | |
| --arg runId "$GITHUB_RUN_ID" \ | |
| --arg attempt "$GITHUB_RUN_ATTEMPT" \ | |
| --arg url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ | |
| --arg wf "$GITHUB_WORKFLOW" \ | |
| --arg event "$GITHUB_EVENT_NAME" \ | |
| --arg actor "$GITHUB_ACTOR" \ | |
| --argjson dur "${{ steps.tests.outputs.duration_ms || '0' }}" \ | |
| --arg status "${{ steps.tests.outputs.status || 'skipped' }}" \ | |
| --argjson idx "${{ matrix.shard }}" \ | |
| '{repo:$repo, branch:$branch, sha:$sha, baseBranch:$base, prNumber:$pr, | |
| runId:$runId, runAttempt:$attempt, runUrl:$url, workflow:$wf, | |
| event:$event, actor:$actor, | |
| actions:[{name:"test:root", group:"tests", durationMs:$dur, status:$status, | |
| shard:{index:$idx, total:2}, job:("test (\($idx)/2)")}]}') | |
| curl -sSf --max-time 60 -X POST https://api.desplega.agent-swarm.dev/api/x/script/MJlLzJEoRxWG \ | |
| -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d "$BODY" | |
| # Merge the per-shard durations into one cache entry for the next run. Only | |
| # after a fully green test matrix, so a crashed shard never poisons the split. | |
| save-timings: | |
| name: Save test timings | |
| needs: test | |
| if: needs.test.result == 'success' | |
| runs-on: ubuntu-latest | |
| # No checkout. Same-run artifact download and cache save use the runner | |
| # token, not GITHUB_TOKEN. | |
| permissions: {} | |
| steps: | |
| - name: Download per-shard timings | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: test-timings-* | |
| path: .bun-test-timings | |
| merge-multiple: true | |
| - name: Save merged timings | |
| uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: .bun-test-timings | |
| key: bun-test-timings-${{ github.run_id }} | |
| pi-skills-freshness: | |
| name: Pi-Skills Freshness Check | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.pi_skills == 'true' | |
| runs-on: ubuntu-latest | |
| # Checkout only; GITHUB_TOKEN needs nothing else. | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| with: | |
| bun-version-file: package.json | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Rebuild pi-skills and check for drift | |
| run: | | |
| bun run build:pi-skills | |
| if [ -n "$(git diff --name-only plugin/pi-skills/)" ]; then | |
| echo "::error::pi-skills are out of date! Run 'bun run build:pi-skills' and commit the changes." | |
| git diff --stat plugin/pi-skills/ | |
| exit 1 | |
| fi | |
| echo "pi-skills are up to date." | |
| seed-skills: | |
| name: Seeded Skills Check | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.seed_skills == 'true' | |
| runs-on: ubuntu-latest | |
| # Checkout only; GITHUB_TOKEN needs nothing else. | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version-file: package.json | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Check skill source-of-truth invariants | |
| run: bun run check:skill-sources | |
| - name: Check vendored ai-toolbox skills | |
| run: bun run check:ai-toolbox-skills | |
| - name: Check generated SKILL.md freshness | |
| run: bun run check:skill-md | |
| - name: Check bundled-file manifest freshness | |
| run: bun run check:seed-skill-files | |
| - name: Run skill seeder tests | |
| # LINT/TEST do not match templates/, so these would not otherwise run for | |
| # a templates/skills-only change. | |
| run: bun run test:root -- src/tests/seed-skills-bundled-files.test.ts src/tests/system-default-skills.test.ts src/tests/skill-fs-writer.test.ts src/tests/skill-sync.test.ts src/tests/sync-ai-toolbox-skills.test.ts src/tests/skill-parser.test.ts | |
| script-types-freshness: | |
| name: Script SDK Types Freshness Check | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.script_types == 'true' | |
| runs-on: ubuntu-latest | |
| # Checkout only; GITHUB_TOKEN needs nothing else. | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| with: | |
| bun-version-file: package.json | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Rebuild script SDK types and check for drift | |
| run: bun run check:script-types | |
| openapi-freshness: | |
| name: OpenAPI Spec Freshness Check | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.openapi == 'true' | |
| runs-on: ubuntu-latest | |
| # Checkout only; GITHUB_TOKEN needs nothing else. | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| with: | |
| bun-version-file: package.json | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Rebuild OpenAPI spec and check for drift | |
| run: | | |
| bun run docs:openapi | |
| if [ -n "$(git diff --name-only openapi.json)" ]; then | |
| echo "::error::openapi.json is out of date! Run 'bun run docs:openapi' and commit the changes." | |
| git diff --stat openapi.json | |
| exit 1 | |
| fi | |
| echo "OpenAPI spec is up to date." | |
| - name: Check generated API docs for drift | |
| run: | | |
| if [ -n "$(git diff --name-only docs-site/content/docs/api-reference/ docs-site/public/openapi.d.ts)" ]; then | |
| echo "::error::API reference docs are out of date! Run 'bun run docs:openapi' and commit the changes." | |
| git diff --stat docs-site/content/docs/api-reference/ docs-site/public/openapi.d.ts | |
| exit 1 | |
| fi | |
| echo "API reference docs are up to date." | |
| - name: Check for raw matchRoute usage | |
| run: | | |
| UNREGISTERED=$(grep -rn "matchRoute(" src/http/ --include="*.ts" | grep -v "route-def.ts" | grep -v "utils.ts" || true) | |
| if [ -n "$UNREGISTERED" ]; then | |
| echo "::error::Found raw matchRoute() calls. Convert to route() definitions:" | |
| echo "$UNREGISTERED" | |
| exit 1 | |
| fi | |
| echo "No raw matchRoute() calls found." | |
| vendored-openapi: | |
| name: Vendored OpenAPI Check | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.vendored_openapi == 'true' | |
| runs-on: ubuntu-latest | |
| # Checkout only; GITHUB_TOKEN needs nothing else. | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| with: | |
| bun-version-file: package.json | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Check vendored OpenAPI determinism | |
| run: bun run check:vendored-openapi | |
| docker-build: | |
| name: Docker Build Test | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.docker == 'true' | |
| runs-on: ubuntu-latest | |
| # Checkout only; GITHUB_TOKEN needs nothing else. | |
| permissions: | |
| contents: read | |
| strategy: | |
| matrix: | |
| # apps/evals/Dockerfile is the standalone evals-service image (Dokploy); it installs | |
| # from the ROOT workspace lockfile, so workspace changes can break it silently | |
| # unless it is built here too. | |
| # The worker builds only the worker-slim target here (fast PR gate — it still | |
| # exercises the shared worker-base layers and the leaf block); the full target | |
| # is built and published on merge by docker-and-deploy.yml. | |
| # `metric` legs load the built image into the local daemon and report its | |
| # uncompressed size to the ci-metrics swarm script (sticky PR diff comment). | |
| # See agent-fs docs/ci-metrics.md for the script contract. | |
| include: | |
| - dockerfile: Dockerfile | |
| target: "" | |
| metric: image.api | |
| timing: docker.api | |
| - dockerfile: Dockerfile.worker | |
| target: worker-slim | |
| metric: image.worker-slim | |
| timing: docker.worker-slim | |
| - dockerfile: apps/evals/Dockerfile | |
| target: "" | |
| metric: "" | |
| timing: docker.evals | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 | |
| - name: Start Docker build timer | |
| id: docker-timer | |
| run: echo "started_at=$(date +%s%3N)" >> "$GITHUB_OUTPUT" | |
| - name: Build Docker image | |
| uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 | |
| with: | |
| context: . | |
| file: ./${{ matrix.dockerfile }} | |
| target: ${{ matrix.target }} | |
| push: false | |
| load: ${{ matrix.metric != '' }} | |
| tags: test-image:latest | |
| # Each matrix leg reports independently. See agent-fs docs/ci-timings.md. | |
| # Skipped on merge_group events and forks (no secret); never blocks the gate. | |
| - name: Report Docker build timing to ci-timings | |
| if: always() && github.event_name == 'pull_request' | |
| continue-on-error: true | |
| env: | |
| TOKEN: ${{ secrets.SWARM_CI_TIMINGS_TOKEN }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| PR_NUMBER: ${{ github.event.pull_request.number || 'null' }} | |
| STARTED_AT: ${{ steps.docker-timer.outputs.started_at || '0' }} | |
| BUILD_STATUS: ${{ job.status }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$TOKEN" ]; then echo "SWARM_CI_TIMINGS_TOKEN not available; skipping"; exit 0; fi | |
| if [ "$STARTED_AT" -eq 0 ]; then DURATION=0; BUILD_STATUS=skipped; else DURATION=$(( $(date +%s%3N) - STARTED_AT )); fi | |
| BODY=$(jq -n \ | |
| --arg repo "$GITHUB_REPOSITORY" \ | |
| --arg branch "${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}" \ | |
| --arg sha "$HEAD_SHA" \ | |
| --arg base "$GITHUB_BASE_REF" \ | |
| --argjson pr "$PR_NUMBER" \ | |
| --arg runId "$GITHUB_RUN_ID" \ | |
| --arg attempt "$GITHUB_RUN_ATTEMPT" \ | |
| --arg url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ | |
| --arg wf "$GITHUB_WORKFLOW" \ | |
| --arg event "$GITHUB_EVENT_NAME" \ | |
| --arg actor "$GITHUB_ACTOR" \ | |
| --arg name "${{ matrix.timing }}" \ | |
| --argjson dur "$DURATION" \ | |
| --arg status "$BUILD_STATUS" \ | |
| '{repo:$repo, branch:$branch, sha:$sha, baseBranch:$base, prNumber:$pr, | |
| runId:$runId, runAttempt:$attempt, runUrl:$url, workflow:$wf, | |
| event:$event, actor:$actor, | |
| actions:[{name:$name, group:"build", durationMs:$dur, status:$status}]}') | |
| curl -sSf --max-time 60 -X POST https://api.desplega.agent-swarm.dev/api/x/script/MJlLzJEoRxWG \ | |
| -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d "$BODY" | |
| # Same-sha calls merge server-side, so each matrix leg posts its own metric | |
| # and they all land in ONE sticky "Docker image sizes" PR comment. | |
| # Skipped on merge_group events and forks (no secret); never blocks the gate. | |
| - name: Report image size to ci-metrics | |
| if: matrix.metric != '' && github.event_name == 'pull_request' | |
| continue-on-error: true | |
| env: | |
| TOKEN: ${{ secrets.SWARM_CI_METRICS_TOKEN }} | |
| METRIC: ${{ matrix.metric }} | |
| PR: ${{ github.event.pull_request.number }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$TOKEN" ]; then echo "SWARM_CI_METRICS_TOKEN not available; skipping"; exit 0; fi | |
| SIZE=$(docker image inspect test-image:latest --format '{{.Size}}') | |
| BODY=$(jq -n \ | |
| --arg sha "$HEAD_SHA" \ | |
| --arg branch "$GITHUB_HEAD_REF" \ | |
| --arg runUrl "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ | |
| --argjson pr "$PR" \ | |
| --arg name "$METRIC" \ | |
| --argjson value "$SIZE" \ | |
| '{repo: "desplega-ai/agent-swarm", sha: $sha, branch: $branch, prNumber: $pr, | |
| runUrl: $runUrl, commentKey: "docker", title: "Docker image sizes (uncompressed, amd64)", | |
| metrics: [{name: $name, value: $value, unit: "bytes", group: "docker"}]}') | |
| curl -sSf --max-time 60 -X POST https://api.desplega.agent-swarm.dev/api/x/script/JWCeFHIQUAQQ \ | |
| -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ | |
| -d "$BODY" | jq -r '.result.markdown // empty' >> "$GITHUB_STEP_SUMMARY" | |
| ui-lint: | |
| name: UI Lint and Type Check | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.ui == 'true' | |
| runs-on: ubuntu-latest | |
| # Checkout only; GITHUB_TOKEN needs nothing else. | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| with: | |
| bun-version-file: package.json | |
| # ui is now a Bun workspace member; the root install hoists its deps. Run the | |
| # ui-scoped scripts from the apps/ui/ directory against that shared node_modules. | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Check design tokens (no raw color literals) | |
| id: ui-tokens | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| bun run check:tokens | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| working-directory: apps/ui | |
| - name: Run Biome linter | |
| id: ui-lint | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| bun run lint | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| working-directory: apps/ui | |
| - name: Run TypeScript type check | |
| id: ui-tsc | |
| run: | | |
| start=$(date +%s%3N) | |
| set +e | |
| bunx tsc -b | |
| code=$? | |
| set -e | |
| echo "duration_ms=$(( $(date +%s%3N) - start ))" >> "$GITHUB_OUTPUT" | |
| echo "status=$([ $code -eq 0 ] && echo success || echo failure)" >> "$GITHUB_OUTPUT" | |
| exit $code | |
| working-directory: apps/ui | |
| # See agent-fs docs/ci-timings.md. Skipped on merge_group events and forks | |
| # (no secret); reporting is best-effort and never blocks the gate. | |
| - name: Report UI check timings to ci-timings | |
| if: always() && github.event_name == 'pull_request' | |
| continue-on-error: true | |
| env: | |
| TOKEN: ${{ secrets.SWARM_CI_TIMINGS_TOKEN }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| PR_NUMBER: ${{ github.event.pull_request.number || 'null' }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$TOKEN" ]; then echo "SWARM_CI_TIMINGS_TOKEN not available; skipping"; exit 0; fi | |
| BODY=$(jq -n \ | |
| --arg repo "$GITHUB_REPOSITORY" \ | |
| --arg branch "${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}" \ | |
| --arg sha "$HEAD_SHA" \ | |
| --arg base "$GITHUB_BASE_REF" \ | |
| --argjson pr "$PR_NUMBER" \ | |
| --arg runId "$GITHUB_RUN_ID" \ | |
| --arg attempt "$GITHUB_RUN_ATTEMPT" \ | |
| --arg url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ | |
| --arg wf "$GITHUB_WORKFLOW" \ | |
| --arg event "$GITHUB_EVENT_NAME" \ | |
| --arg actor "$GITHUB_ACTOR" \ | |
| --argjson lintDur "${{ steps.ui-lint.outputs.duration_ms || '0' }}" \ | |
| --arg lintStatus "${{ steps.ui-lint.outputs.status || 'skipped' }}" \ | |
| --argjson tscDur "${{ steps.ui-tsc.outputs.duration_ms || '0' }}" \ | |
| --arg tscStatus "${{ steps.ui-tsc.outputs.status || 'skipped' }}" \ | |
| --argjson tokensDur "${{ steps.ui-tokens.outputs.duration_ms || '0' }}" \ | |
| --arg tokensStatus "${{ steps.ui-tokens.outputs.status || 'skipped' }}" \ | |
| '{repo:$repo, branch:$branch, sha:$sha, baseBranch:$base, prNumber:$pr, | |
| runId:$runId, runAttempt:$attempt, runUrl:$url, workflow:$wf, | |
| event:$event, actor:$actor, | |
| actions:[ | |
| {name:"ui:lint", group:"checks", durationMs:$lintDur, status:$lintStatus}, | |
| {name:"ui:tsc", group:"checks", durationMs:$tscDur, status:$tscStatus}, | |
| {name:"ui:tokens", group:"checks", durationMs:$tokensDur, status:$tokensStatus} | |
| ]}') | |
| curl -sSf --max-time 60 -X POST https://api.desplega.agent-swarm.dev/api/x/script/MJlLzJEoRxWG \ | |
| -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d "$BODY" | |
| template-workflow-validation: | |
| name: Validate Workflow Templates | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.templates_workflows == 'true' | |
| runs-on: ubuntu-latest | |
| # Checkout only; GITHUB_TOKEN needs nothing else. | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| with: | |
| bun-version-file: package.json | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Validate workflow templates | |
| run: bun run scripts/validate-template-workflows.ts | |
| gate: | |
| name: Merge Gate | |
| needs: [detect-changes, restore-timings, lint-and-typecheck, test, pi-skills-freshness, seed-skills, script-types-freshness, openapi-freshness, vendored-openapi, docker-build, ui-lint, template-workflow-validation] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| # No checkout, no API calls; the job only inspects `needs` results. | |
| permissions: {} | |
| steps: | |
| - name: Check results | |
| run: | | |
| results='${{ toJSON(needs.*.result) }}' | |
| echo "Job results: $results" | |
| if echo "$results" | jq -e 'map(select(. != "success" and . != "skipped")) | length > 0' > /dev/null 2>&1; then | |
| echo "::error::Some required jobs failed!" | |
| exit 1 | |
| fi | |
| echo "All jobs passed or were skipped." |