Skip to content

Merge branch 'dev' into feat/epic-open-code-engine-in-keiko-2473 #4309

Merge branch 'dev' into feat/epic-open-code-engine-in-keiko-2473

Merge branch 'dev' into feat/epic-open-code-engine-in-keiko-2473 #4309

Workflow file for this run

name: CI
on:
push:
branches:
- dev
- feat/dev
- feat/epic-open-code-engine-in-keiko-2473
- feat/keiko-editor
- feat/keiko-agent-native-editor-foundation-and-runtime
- feat/prompt-enhancer-1307
- feat/keiko-establish-governed-end-to-end-git-delivery
- feat/keiko-voice-digital-twin
- feat/keiko-isolated-task-workspaces
- feat/keiko-colleague-like-voice-dialogue-mode
- feat/keiko-repository-centered-desktop-workflow
- feat/epic-memoria-viva-2538
- epic/coding-workbench-opencode-codex
- "release/**"
pull_request:
branches:
- dev
- feat/dev
- feat/epic-open-code-engine-in-keiko-2473
- feat/keiko-editor
- feat/keiko-agent-native-editor-foundation-and-runtime
- feat/prompt-enhancer-1307
- feat/keiko-establish-governed-end-to-end-git-delivery
- feat/keiko-voice-digital-twin
- feat/keiko-isolated-task-workspaces
- feat/keiko-colleague-like-voice-dialogue-mode
- feat/keiko-repository-centered-desktop-workflow
- feat/epic-memoria-viva-2538
- epic/coding-workbench-opencode-codex
- "release/**"
# ADR-0139 D7: every required workflow also validates merge-queue groups so a human can
# enable GitHub's merge queue without agents re-racing a moving dev branch.
merge_group:
branches:
- dev
workflow_dispatch:
permissions: {}
jobs:
protected-branch-gate:
name: Protected branch gate
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Confirm dev branch gate
run: |
case "${GITHUB_REF}:${GITHUB_BASE_REF}" in
refs/heads/dev: | refs/heads/feat/dev: | refs/heads/feat/epic-open-code-engine-in-keiko-2473: | refs/heads/feat/keiko-editor: | refs/heads/feat/keiko-agent-native-editor-foundation-and-runtime: | refs/heads/feat/prompt-enhancer-1307: | refs/heads/feat/keiko-establish-governed-end-to-end-git-delivery: | refs/heads/feat/keiko-voice-digital-twin: | refs/heads/feat/keiko-isolated-task-workspaces: | refs/heads/feat/keiko-colleague-like-voice-dialogue-mode: | refs/heads/feat/keiko-repository-centered-desktop-workflow: | refs/heads/feat/epic-memoria-viva-2538: | refs/heads/epic/coding-workbench-opencode-codex: | refs/heads/release/*: | *:dev | *:feat/dev | *:feat/epic-open-code-engine-in-keiko-2473 | *:feat/keiko-editor | *:feat/keiko-agent-native-editor-foundation-and-runtime | *:feat/prompt-enhancer-1307 | *:feat/keiko-establish-governed-end-to-end-git-delivery | *:feat/keiko-voice-digital-twin | *:feat/keiko-isolated-task-workspaces | *:feat/keiko-colleague-like-voice-dialogue-mode | *:feat/keiko-repository-centered-desktop-workflow | *:feat/epic-memoria-viva-2538 | *:epic/coding-workbench-opencode-codex | *:release/*)
echo "Protected or integration branch gate satisfied."
;;
*)
echo "This gate is only valid for protected dev, integration, and release branches."
exit 1
;;
esac
core-quality:
name: Core quality
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && 'dev' || github.ref }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24.18.0"
package-manager-cache: false
- name: Verify governed Node.js and npm toolchain
run: node scripts/check-runtime-toolchain.mjs --exact
- run: npm ci --ignore-scripts
- name: Install sandbox isolation backend (bubblewrap)
run: |
sudo apt-get update
sudo apt-get install -y bubblewrap
# Ubuntu 24.04 restricts unprivileged user namespaces via AppArmor; bubblewrap (and the
# unshare fallback) need them to create the network namespace. Relax it for the runner so
# the enforced-egress proof below actually exercises the isolation boundary (ADR-0043).
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
- name: Enforced sandbox egress proof (ADR-0043 / #1202)
# NOT gated by the feat/keiko-editor branch skip: the assured pre-filter executes untrusted
# model-generated code, so the deny-by-default egress boundary must be proven on every run.
# The egress test fails the job if an outbound connection from inside network:"none" succeeds.
run: npm --workspace @oscharko-dev/keiko-sandbox test
- name: Verify Keiko Editor package
run: |
npm --workspace @oscharko-dev/keiko-editor run build
npm --workspace @oscharko-dev/keiko-editor run typecheck
npm --workspace @oscharko-dev/keiko-editor test
- run: npm run typecheck
- run: npm run check:version-consistency
- run: npm run check:dependency-hygiene
- run: npm run check:knip
- run: npm run check:qodo-config
- run: npm run check:release-required-workflows
- run: npm run check:portable-approvals
- run: npm run check:git-client-evidence
- run: npm run lint
env:
NODE_OPTIONS: --max-old-space-size=6144
- run: npm run format:check
- name: UI i18n guard
env:
KEIKO_I18N_GUARD_BASE_REF: ${{ github.base_ref }}
KEIKO_I18N_GUARD_BASE_SHA: ${{ github.event.before }}
run: |
if [ -n "${KEIKO_I18N_GUARD_BASE_REF:-}" ]; then
git fetch --no-tags --depth=100 origin "${KEIKO_I18N_GUARD_BASE_REF}:refs/remotes/origin/${KEIKO_I18N_GUARD_BASE_REF}"
elif [ "${GITHUB_EVENT_NAME:-}" = "push" ] && [ "${GITHUB_REF_NAME:-}" != "dev" ]; then
git fetch --no-tags --depth=100 origin "dev:refs/remotes/origin/dev"
fi
npm run check:ui-i18n
- run: npm run check:adr-index
- run: npm run arch:check
- run: npm run arch:check:negative
- name: Quality Intelligence supply-chain gate
run: npm run check:qi-supply-chain
- name: Retrieval latency gate (enterprise retrieval M3)
# Runs after `npm run typecheck` (which builds packages), so the compiled keiko-workspace
# dist the gate imports is present. Deterministic fixture + generous p95 ceiling.
run: npm run check:retrieval-latency
- name: Retrieval quality gate (enterprise retrieval M4)
# Same compiled workspace surface as the latency gate; evaluates top-k quality, line-level
# evidence, and generated-artifact leakage over fixed synthetic enterprise fixtures.
run: npm run check:retrieval-quality
- name: Grounded retrieval quality gate (RB-5, GEN-AI-RELEASE-GATE-001)
# Drives the REAL production semantic-retrieval + RRF fusion + model-reranker path (not the
# lexical toy corpus) over distractor-dense fixtures with non-tautological (<1) floors, and
# PROVES the gate is non-vacuous by asserting an injected ranking/reranker regression drops
# below the floors. Uses the compiled keiko-server dist built by `npm run typecheck` above.
run: npm run check:grounded-retrieval-quality
- name: Grounded faithfulness gate (RB-4, GEN-AI-EVAL-003)
# Scores the real citation-reconciliation + abstention logic over scripted answerer variants
# (faithful / hallucinated-citation / confident-over-empty / refusal). Fails closed if a
# fabricated citation stops being flagged or empty evidence stops abstaining.
run: npm run check:grounded-faithfulness
- name: Grounded entailment gate (Knowledge M1.2, Issue #2563)
# Scores the real claim-segmentation + entailment-reconciliation + marker logic over
# distractor-dense fixtures with a deterministic scripted judge (same port as the gateway
# judge). Fails closed if an in-pack-but-unsupported claim stops being flagged, a supported
# claim is falsely flagged, an unavailable judge stops degrading to WARN, or a pass-through
# judge still "detects" (the checker must be load-bearing — non-tautology proven in CI).
run: npm run check:grounded-entailment
- name: Server error observability gate (RB-6, GEN-OBS-DIAGNOSTICS-901)
# Drives the REAL built BFF: forces a handler throw and asserts the top-level catch returns an
# opaque 500 that carries a correlation id (body + X-Keiko-Correlation-Id header), routes the
# redacted cause to the operator diagnostic sink keyed by that id, never leaks the raw cause,
# and honours a UI-supplied id. Goes RED against a bare `.catch(() => {})` that drops the id or
# the diagnostic. Uses the compiled keiko-server dist built by `npm run typecheck` above.
run: npm run check:error-observability
- name: Context quality gate (ADR-0052, context-engineering PR1)
# Runs after `npm run typecheck` (which builds packages), so the compiled keiko-contracts,
# keiko-workflows, keiko-workspace, and keiko-security dist the gate imports are present —
# the same build dependency the retrieval gates above rely on. Drives the deterministic lane
# allocator over a fixed offline corpus and enforces context-quality invariants.
run: npm run check:context-quality
- name: Keiko Editor static-export bundle gate
run: |
npm run build:ui
npm run check:editor-bundle-size -- --require-static-export
npm run check:editor-release-evidence
- name: Package surface and installable smoke
run: |
npm run build
npm run prepare:bin
npm run prune:package-build-artifacts
npm run prune:package-native-optionals
npm run check:package-surface
KEIKO_SMOKE_PACK_IGNORE_SCRIPTS=1 npm run smoke:install
coverage-sonar:
name: Coverage and SonarCloud
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && 'dev' || github.ref }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24.18.0"
package-manager-cache: false
- name: Verify governed Node.js and npm toolchain
run: node scripts/check-runtime-toolchain.mjs --exact
- run: npm ci --ignore-scripts
- name: Install sandbox isolation backend (bubblewrap)
run: |
sudo apt-get update
sudo apt-get install -y bubblewrap
# Coverage executes the same live isolation proofs as the core test job. Ubuntu 24.04
# otherwise exposes bubblewrap while AppArmor prevents it from creating the namespace.
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
- name: Coverage quality gates
if: ${{ github.event_name != 'pull_request' || github.base_ref != 'feat/keiko-editor' }}
run: npm run test:coverage:quality
- name: Verify governed Sonar analysis scope
run: npm run check:sonar-scope
- name: Verify changed production sources are mapped into LCOV
if: ${{ github.event_name == 'pull_request' && github.base_ref == 'dev' }}
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: node scripts/check-lcov-source-mapping.mjs --base "$BASE_SHA" --head "$HEAD_SHA"
- name: Resolve project version for SonarCloud
if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.base_ref == 'dev') || (github.event_name == 'push' && github.ref == 'refs/heads/dev') }}
id: sonar-version
run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
- name: Verify manual analysis is bound to remote dev
if: ${{ github.event_name == 'workflow_dispatch' }}
id: sonar-head
run: |
actual="$(git rev-parse HEAD)"
expected="$(git rev-parse refs/remotes/origin/dev)"
if [ "$actual" != "$expected" ]; then
echo "Manual Sonar analysis checkout is not the fetched remote dev revision."
exit 1
fi
echo "sha=$actual" >> "$GITHUB_OUTPUT"
- name: Download Sonar Scanner CLI
# Downloaded and checksum-verified directly rather than through a marketplace action whose
# license is denied by the repository dependency-review policy.
if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.base_ref == 'dev') || (github.event_name == 'push' && github.ref == 'refs/heads/dev') }}
run: |
scanner_zip="$RUNNER_TEMP/sonar-scanner-cli.zip"
curl --proto '=https' -sSfL -o "$scanner_zip" "https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-8.1.0.6389-linux-x64.zip"
echo "bb8f709f9cb73352f8d1260a3b3c506c0f41146754bc630762c126d795499d0b $scanner_zip" | sha256sum --check
unzip -q "$scanner_zip" -d "$RUNNER_TEMP"
echo "SONAR_SCANNER_HOME=$RUNNER_TEMP/sonar-scanner-8.1.0.6389-linux-x64" >> "$GITHUB_ENV"
- name: SonarCloud CI-based analysis (ADR-0131 / ADR-0134)
if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.base_ref == 'dev') || (github.event_name == 'push' && github.ref == 'refs/heads/dev') }}
id: sonar-scan
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
STEPS_SONAR_VERSION_OUTPUTS_VERSION: ${{ steps.sonar-version.outputs.version }}
# Pull-request runs analyze the GitHub-provided merge ref (refs/pull/N/merge), whose commit
# SHA differs from the pull-request head. Pin the analysis revision to the head commit so the
# SonarCloud SCM sensor attributes changed lines to the real head instead of warning "detected
# as changed but without having changed lines". Push/dispatch runs analyze their own head.
SONAR_HEAD_SHA: ${{ github.event.pull_request.head.sha || steps.sonar-head.outputs.sha || github.sha }}
run: |
# ADR-0139 D6: transient SonarCloud-side provisioning or network failures retry with
# backoff before failing closed; genuine quality-gate failures are never retried.
for attempt in 1 2 3; do
set +e
"$SONAR_SCANNER_HOME/bin/sonar-scanner" \
-Dsonar.projectBaseDir="$GITHUB_WORKSPACE" \
-Dsonar.projectVersion="${STEPS_SONAR_VERSION_OUTPUTS_VERSION}" \
-Dsonar.scm.revision="${SONAR_HEAD_SHA}" \
-Dsonar.qualitygate.wait=true \
-Dsonar.working.directory="$RUNNER_TEMP/sonar-work" \
2>&1 | tee "$RUNNER_TEMP/sonar-scanner.log"
scanner_status=${PIPESTATUS[0]}
set -e
if [ "$scanner_status" -eq 0 ]; then
break
fi
if ! grep -qE "Gateway Timeout|Bad Gateway|Service Unavailable|Failed to query JRE metadata|connect timed out|Connection reset" "$RUNNER_TEMP/sonar-scanner.log"; then
break
fi
if [ "$attempt" -lt 3 ]; then
echo "Transient SonarCloud failure (attempt $attempt); retrying after backoff."
sleep $((attempt * 30))
fi
done
node scripts/check-sonar-analysis-log.mjs --log "$RUNNER_TEMP/sonar-scanner.log"
echo "status=$scanner_status" >> "$GITHUB_OUTPUT"
if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then
exit 0
fi
exit "$scanner_status"
- name: Verify SonarCloud Banking Grade PR evidence
if: ${{ github.event_name == 'pull_request' && github.base_ref == 'dev' }}
env:
SONAR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
SONAR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
SONAR_PULL_REQUEST: ${{ github.event.pull_request.number }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: node scripts/check-sonar-pr-quality-gate.mjs
- name: Verify SonarCloud Banking Grade dev evidence
if: ${{ always() && github.event_name == 'workflow_dispatch' }}
env:
SONAR_HEAD_SHA: ${{ steps.sonar-head.outputs.sha }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: node scripts/check-sonar-main-quality-gate.mjs
ci:
name: ci
if: ${{ always() }}
needs:
- protected-branch-gate
- core-quality
- coverage-sonar
- cross-platform-smoke
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Aggregate required CI results fail closed
env:
CORE_QUALITY_RESULT: ${{ needs.core-quality.result }}
COVERAGE_SONAR_RESULT: ${{ needs.coverage-sonar.result }}
CROSS_PLATFORM_RESULT: ${{ needs.cross-platform-smoke.result }}
PROTECTED_BRANCH_RESULT: ${{ needs.protected-branch-gate.result }}
run: |
for result in "$PROTECTED_BRANCH_RESULT" "$CORE_QUALITY_RESULT" "$COVERAGE_SONAR_RESULT" "$CROSS_PLATFORM_RESULT"; do
if [ "$result" != "success" ]; then
echo "Required CI dependency did not succeed: $result"
exit 1
fi
done
echo "All required CI dependencies succeeded."
actionlint:
name: actionlint
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Download and verify actionlint
run: |
curl --proto '=https' -sSfL "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz" -o actionlint.tar.gz
echo "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 actionlint.tar.gz" | sha256sum --check
tar -xzf actionlint.tar.gz actionlint
chmod +x actionlint
- name: Run actionlint
run: ./actionlint -color .github/workflows/*.yml
verify-pinned-shas:
name: Verify pinned action SHAs
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Assert all action references are pinned to 40-hex SHAs
run: |
set -euo pipefail
violations=$(grep -rnE '^\s*-?\s*uses:' .github/workflows/ \
| grep -vE 'uses:\s*\./' \
| grep -vE 'uses:\s*docker://' \
| grep -vE 'uses:\s*[A-Za-z0-9._/-]+@[0-9a-f]{40}([[:space:]]|$|#)' || true)
if [ -n "$violations" ]; then
echo "Unpinned action references (must pin to a full 40-hex commit SHA):"
echo "$violations"
exit 1
fi
echo "All action references are pinned to full commit SHAs."
zizmor:
name: zizmor
if: ${{ (github.event_name == 'pull_request' && github.base_ref == 'dev') || (github.event_name == 'push' && github.ref == 'refs/heads/dev') || github.event_name == 'merge_group' }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
with:
version: "1.26.1"
config: .github/zizmor.yml
advanced-security: false
annotations: true
build-scan-sbom-smoke:
name: Build, scan, SBOM, smoke
# Keep early feature-branch PRs fast. These release/package gates still run on merges to
# feat/keiko-editor, on dev/release PRs, and on manual workflow_dispatch runs.
if: ${{ github.event_name != 'pull_request' || github.base_ref != 'feat/keiko-editor' }}
runs-on: ubuntu-latest
permissions:
attestations: write
contents: read
id-token: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24.18.0"
package-manager-cache: false
- name: Verify governed Node.js and npm toolchain
run: node scripts/check-runtime-toolchain.mjs --exact
- run: npm ci --ignore-scripts
- name: Build
run: npm run build
- name: Prepare packaged CLI and UI assets
run: |
npm run prepare:bin
npm run build:ui
npm run check:editor-release-evidence
- name: Installable-package smoke (Issue #169 AC2)
run: npm run smoke:install
- name: Installable-package smoke with optional dependencies
run: npm run smoke:install:optional
- name: Installable memory smoke (Issue #216)
run: npm run smoke:install:memory
- name: Smoke - offline evaluate exits 0
run: KEIKO_EVIDENCE_DIR="$(mktemp -d)" node dist/cli/index.js evaluate --suite all
- name: Security audit (high and above)
run: npm audit --audit-level=high
- name: Generate CycloneDX SBOM
run: npm sbom --sbom-format cyclonedx --omit dev > sbom.cdx.json
- name: Workspace SBOM and license gate (Issue #169 AC4)
run: npm run check:workspace-supply-chain
- name: Smoke - help exits 0
run: node dist/cli/index.js --help
- name: Smoke - version exits 0
run: node dist/cli/index.js --version
- name: Smoke - unknown command exits 2
run: |
set +e
node dist/cli/index.js definitely-not-a-command
code=$?
if [ "$code" -ne 2 ]; then echo "expected exit 2, got $code"; exit 1; fi
echo "unknown-command exit code correct: $code"
- name: Upload SBOM
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sbom-cyclonedx
path: sbom.cdx.json
retention-days: 90
# Build-provenance (not SBOM-predicate) attestation: this job has no single packed artifact
# to bind an SBOM-attestation subject to (no `npm pack` runs here), so this proves the SBOM
# document itself was produced by this exact workflow run/commit, not tampered or fabricated.
# Scoped to `push` only (dev/release branches) so PR and workflow_dispatch runs stay
# unverified-staging and do not add attestation noise for commits that never ship.
- name: Attest build provenance for root SBOM
if: github.event_name == 'push'
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
with:
subject-path: sbom.cdx.json
- name: Upload workspace SBOMs (Issue #169 AC4)
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: workspace-sboms-cyclonedx
path: sbom/
retention-days: 90
- name: Attest build provenance for workspace SBOMs
if: github.event_name == 'push'
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
with:
subject-path: sbom/workspace-*.cdx.json
cross-platform-smoke:
name: Cross-platform smoke (${{ matrix.os }})
# The Node.js 24 baseline must execute, typecheck, build, and install on every supported desktop
# OS. The complete hermetic suite remains in the Linux `ci` job because many security fixtures
# intentionally model POSIX paths, Bash, and symlinks. Keep fail-fast:false so one platform
# result never masks another.
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24.18.0"
package-manager-cache: false
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
if: runner.os == 'Windows'
with:
dotnet-version: "8.0.x"
- name: Verify governed Node.js and npm toolchain
run: node scripts/check-runtime-toolchain.mjs --exact
- run: npm ci --ignore-scripts
- name: Configure MSVC for native quality analysis
if: runner.os == 'Windows'
shell: pwsh
run: |
$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio/Installer/vswhere.exe"
$installation = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
if (-not $installation) { throw "Visual Studio C++ tools were not found" }
$vcvars = Join-Path $installation "VC/Auxiliary/Build/vcvars64.bat"
$vcvarsWrapper = Join-Path $env:RUNNER_TEMP "keiko-vcvars-env.cmd"
Set-Content -LiteralPath $vcvarsWrapper -Encoding ascii -Value "@call `"$vcvars`" >nul`r`n@set"
$environment = & cmd.exe /d /c $vcvarsWrapper
if ($LASTEXITCODE -ne 0) { throw "MSVC environment initialization failed" }
foreach ($name in @("PATH", "INCLUDE", "LIB", "LIBPATH")) {
$line = $environment | Where-Object { $_ -match "^$name=" } | Select-Object -First 1
if (-not $line) { throw "MSVC environment did not define $name" }
$line | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
}
- name: Verify productive native sources on macOS
if: runner.os == 'macOS'
run: npm run check:native:macos
- name: Verify productive native sources on Windows
if: runner.os == 'Windows'
run: npm run check:native:windows
- name: Install Linux sandbox isolation backend
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y bubblewrap
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
- name: Typecheck the complete package graph
run: npm run typecheck
- name: Build
run: npm run build
# Two separate steps (not one multi-line run): on the Windows runner the default shell is
# PowerShell, which only surfaces the exit code of the LAST line of a multi-line `run`; as
# discrete steps each command's failure fails the job on every OS.
- name: Prepare packaged CLI
run: npm run prepare:bin
- name: Build UI (static export)
run: npm run build:ui
- name: Installable-package smoke (Issue #169 AC2 / #284 AC4 cross-platform)
env:
# Pack the dist already built by the steps above instead of re-running the Linux-oriented
# `prepack` publish gate (arch-check / package-surface / supply-chain shell out to npx/npm
# in ways that are not Windows-portable — a separate concern from artifact portability).
# The gating `build-scan-sbom-smoke` job keeps the full prepack pack on Linux.
KEIKO_SMOKE_PACK_IGNORE_SCRIPTS: "1"
run: npm run smoke:install
- name: Installable-package smoke with native optional dependencies
env:
KEIKO_SMOKE_PACK_IGNORE_SCRIPTS: "1"
run: npm run smoke:install:optional
ui:
name: ui
runs-on: ubuntu-latest
permissions:
attestations: write
contents: read
id-token: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# The immutable D12 evidence gate recomputes the subject for its pinned baseline commit.
# Keep that commit reachable instead of relying on the default single-commit checkout.
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24.18.0"
package-manager-cache: false
- name: Verify governed Node.js and npm toolchain
run: node scripts/check-runtime-toolchain.mjs --exact
- name: Install workspaces (single root lockfile after #167)
run: npm ci --ignore-scripts
- name: Install sandbox isolation backend (bubblewrap)
run: |
sudo apt-get update
sudo apt-get install -y bubblewrap
# The #2215 real-BFF E2E executes verification commands through the same mandatory
# network:none boundary as production. Ubuntu 24.04 restricts unprivileged user
# namespaces via AppArmor, so enable the proven Bubblewrap path exactly as the required
# ci job does; never weaken the verification runner to inherited networking for tests.
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
- name: Build internal packages so contracts dist resolves for UI tsc
run: npm run build:packages
- name: Security audit UI dependencies (moderate and above)
run: npm audit --audit-level=moderate --workspace @oscharko-dev/keiko-ui
- name: Lint UI
run: npm run lint --workspace @oscharko-dev/keiko-ui
- name: Typecheck UI
run: npm run typecheck --workspace @oscharko-dev/keiko-ui
- name: Verify UI source compatibility with TypeScript 7
run: npm run typecheck:native --workspace @oscharko-dev/keiko-ui
- name: Build UI (static export)
run: npm run build --workspace @oscharko-dev/keiko-ui
- name: Generate UI CycloneDX SBOM
if: ${{ github.event_name != 'pull_request' || github.base_ref != 'feat/keiko-editor' }}
run: npm sbom --sbom-format cyclonedx --omit dev --workspace @oscharko-dev/keiko-ui > ui-sbom.cdx.json
- name: Test UI with coverage (jsdom + axe a11y)
run: npm run test:coverage:ui
- name: UI coverage ratchet
run: npm run check:coverage:ui
- name: Install Playwright browser
if: ${{ github.event_name != 'pull_request' || github.base_ref != 'feat/keiko-editor' }}
# chromium is the reference browser for the smoke + perf-evidence suites. The
# workspace-performance webkit project is a local-only cross-browser aid: headless WebKit on CI
# software-renders and mis-batches events, so both its timing and write-coalescing budgets are
# environment artifacts (all CI perf-evidence runs are scoped to --project=chromium).
run: npx --ignore-scripts playwright@1.61.1 install --with-deps chromium
- name: Release smoke E2E
if: ${{ github.event_name != 'pull_request' || github.base_ref != 'feat/keiko-editor' }}
run: npm run test:e2e:smoke
- name: Editor run, verify, problems E2E (#2215)
if: ${{ github.event_name != 'pull_request' || github.base_ref != 'feat/keiko-editor' }}
run: npm run test:e2e:editor-run-verification-2215
- name: Governed editor debugging E2E (#2348)
if: ${{ github.event_name != 'pull_request' || github.base_ref != 'feat/keiko-editor' }}
run: npm run test:e2e:editor-debugging-2348
- name: Refresh workspace performance evidence
if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
run: |
rm -f docs/release/1580-workspace-perf-evidence.json
npm run test:e2e:workspace-perf
# The editor document is the immutable D12 baseline/candidate comparison measured on the PR
# candidate. Its source-tree and toolchain bindings (ADR-0139 D2: measured surfaces only)
# make it valid across the signed squash and across merge-queue integration.
- name: Validate immutable editor D12 performance evidence
if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
run: npm run check:perf-evidence:editor
- name: Performance evidence freshness + budget gate
if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
run: npm run check:perf-evidence
# Upload only metric documents that passed the redaction-closing schema and canonical-byte
# gate above. Raw traces can contain workspace fixture text and remain outside this boundary.
- name: Upload redacted performance evidence
if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: performance-evidence-${{ github.run_id }}-${{ github.run_attempt }}
path: |
docs/release/1209-perf-evidence.json
docs/release/1580-workspace-perf-evidence.json
if-no-files-found: warn
retention-days: 30
- name: Build package and UI assets
if: ${{ github.event_name != 'pull_request' || github.base_ref != 'feat/keiko-editor' }}
run: |
npm run build
npm run prepare:bin
npm run build:ui
npm run check:editor-release-evidence
npm run prune:package-build-artifacts
npm run prune:package-native-optionals
- name: Verify package surface
if: ${{ github.event_name != 'pull_request' || github.base_ref != 'feat/keiko-editor' }}
run: npm run check:package-surface
- name: Smoke - keiko ui serves /api/health
if: ${{ github.event_name != 'pull_request' || github.base_ref != 'feat/keiko-editor' }}
run: |
set -euo pipefail
node dist/cli/index.js ui --port 4399 &
ui_pid=$!
trap 'kill "$ui_pid" 2>/dev/null || true' EXIT
body=$(curl --retry 10 --retry-connrefused --retry-delay 1 -fsS http://127.0.0.1:4399/api/health)
echo "health: $body"
echo "$body" | grep -q '"status":"ok"'
- name: Upload UI SBOM
if: ${{ github.event_name != 'pull_request' || github.base_ref != 'feat/keiko-editor' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ui-sbom-cyclonedx
path: ui-sbom.cdx.json
retention-days: 90
# Same build-provenance-of-the-document rationale as the root/workspace SBOM attestations in
# the build-scan-sbom-smoke job: no single packed UI artifact exists to bind an SBOM-predicate
# attestation subject to at this point, so this attests the SBOM document's own provenance.
- name: Attest build provenance for UI SBOM
if: github.event_name == 'push'
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
with:
subject-path: ui-sbom.cdx.json