fix(tasks): hard-break preservation, error decoupling, tag deduplication #1711
Workflow file for this run
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: Arch Smoke | |
| # Builds and boots the Docker image on both production architectures on | |
| # every PR — native runners, no QEMU. This is the only PR-time check that | |
| # executes an arm64 artifact (trivy-pr builds amd64 only), so it catches | |
| # cases where a native prebuild requires a newer glibc than the base image | |
| # provides. Each leg: | |
| # 1. build --target local — natively executes the Dockerfile deps-stage | |
| # native-binding assertion for its arch | |
| # 2. boot smoke — run the image against a small fixture vault; /healthz | |
| # returning 200 proves better-sqlite3 + sqlite-vec loaded and the | |
| # FTS index built (server.ts exits 1 on any startup failure), then | |
| # an authenticated MCP initialize round-trip is asserted | |
| # 3. build --target remote, then the remote-boot vitest suite boots | |
| # the image with the Sync CLI stubbed (src/__tests__/docker/fixtures/ob), | |
| # so the s6 init chain runs end-to-end without Sync credentials | |
| # | |
| # EMBEDDING_ENABLED=false at runtime is fine: onnxruntime-node's dynamic | |
| # import is never reached, but the deps-stage assertion already executed | |
| # require('onnxruntime-node') for this arch at build time. | |
| # | |
| # The rendered job names — "arch-smoke (amd64)" / "arch-smoke (arm64)" — | |
| # are the required-status-check contexts in the branch ruleset; friendly | |
| # arch names keep those contexts stable if runner labels change. | |
| on: | |
| pull_request: | |
| push: | |
| branches: [main] # seeds the per-arch build cache all PRs can restore | |
| concurrency: | |
| group: arch-smoke-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| arch-smoke: | |
| name: arch-smoke (${{ matrix.arch }}) | |
| runs-on: ${{ matrix.runner }} | |
| timeout-minutes: 30 | |
| strategy: | |
| # Arch failures are independent signals — never cancel the other leg. | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - arch: amd64 | |
| runner: ubuntu-latest | |
| - arch: arm64 | |
| runner: ubuntu-24.04-arm | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 | |
| # Same daily cache-bust as trivy-pr — an APT_UPGRADE_DATE that | |
| # differs between the two workflows would change the base layer | |
| # hash and stop the amd64 leg sharing trivy-pr's cache. | |
| - name: Cache-bust date for apt-get upgrade | |
| id: date | |
| run: echo "date=$(date +'%Y-%m-%d')" >> "$GITHUB_OUTPUT" | |
| # Per-arch, per-target cache scope: a GHA cache scope keeps only its | |
| # last mode=max export, so every build sharing one scope — the two | |
| # legs here, trivy-pr (default scope), or the arm64 leg's local and | |
| # remote builds — would evict each other's layers. The bare type=gha | |
| # cache-from lets the amd64 leg still read trivy-pr's layers; it is | |
| # a no-op on arm64 (no matching blobs). | |
| - name: Build image from branch (local target) | |
| uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 | |
| with: | |
| context: . | |
| target: local | |
| # Make the built image visible to the local Docker daemon so the | |
| # boot step can run it by tag. | |
| load: true | |
| tags: vault-cortex:smoke | |
| build-args: APT_UPGRADE_DATE=${{ steps.date.outputs.date }} | |
| cache-from: | | |
| type=gha,scope=arch-smoke-${{ matrix.arch }}-local | |
| type=gha | |
| cache-to: type=gha,mode=max,scope=arch-smoke-${{ matrix.arch }}-local | |
| # Loaded on both legs so the remote-boot test suite below can run | |
| # it by tag. Both targets share the base/deps/build stages, so this | |
| # step reads the local cache scope too. The amd64 remote scope is | |
| # seeded by the main-branch push like the others. | |
| - name: Build image from branch (remote target) | |
| uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 | |
| with: | |
| context: . | |
| target: remote | |
| load: true | |
| tags: vault-cortex:remote-ci | |
| build-args: APT_UPGRADE_DATE=${{ steps.date.outputs.date }} | |
| cache-from: | | |
| type=gha,scope=arch-smoke-${{ matrix.arch }}-remote | |
| type=gha,scope=arch-smoke-${{ matrix.arch }}-local | |
| cache-to: type=gha,mode=max,scope=arch-smoke-${{ matrix.arch }}-remote | |
| # Two notes across two folders exercise the boot-time index build | |
| # for real: frontmatter properties, tags, a wikilink pair (link | |
| # extraction + backlinks), and a task line — all inserted into FTS | |
| # by the blocking rebuildFromVault before /healthz can return 200. | |
| - name: Create fixture vault | |
| run: | | |
| mkdir -p "$RUNNER_TEMP/smoke-vault/Projects" "$RUNNER_TEMP/smoke-vault/Daily" | |
| cat > "$RUNNER_TEMP/smoke-vault/Projects/Arch Smoke.md" << 'EOF' | |
| --- | |
| tags: [ci, smoke] | |
| status: active | |
| --- | |
| # Arch Smoke | |
| Fixture note for the CI boot smoke — indexed at container start. | |
| Links to [[2026-07-30]]. | |
| - [ ] boot the image on both architectures | |
| EOF | |
| cat > "$RUNNER_TEMP/smoke-vault/Daily/2026-07-30.md" << 'EOF' | |
| # 2026-07-30 | |
| Daily fixture note linking back to [[Arch Smoke]]. #smoke | |
| EOF | |
| # MEMORY_ENABLED=false keeps the About Me/ bootstrap from writing | |
| # into the mount, so the vault can be read-only (the container's | |
| # UID-1000 'node' user couldn't write the runner-owned dir anyway). | |
| # The token is generated per run — nothing token-shaped is committed. | |
| # No --rm: the container must survive a failure so the diagnostics | |
| # step can read its logs. | |
| - name: Boot smoke (healthz + MCP initialize) | |
| run: | | |
| MCP_AUTH_TOKEN="$(openssl rand -hex 24)" | |
| docker run -d --name smoke --pull=never -p 8000:8000 \ | |
| -v "$RUNNER_TEMP/smoke-vault:/vault:ro" \ | |
| -e MCP_AUTH_TOKEN="$MCP_AUTH_TOKEN" \ | |
| -e PUBLIC_URL=http://localhost:8000 \ | |
| -e EMBEDDING_ENABLED=false \ | |
| -e MEMORY_ENABLED=false \ | |
| vault-cortex:smoke | |
| for i in $(seq 1 30); do | |
| if [ "$(docker inspect -f '{{.State.Running}}' smoke)" != "true" ]; then | |
| echo "::error::container exited during startup (poll attempt $i)" | |
| exit 1 | |
| fi | |
| if curl -fsS http://localhost:8000/healthz > /dev/null 2>&1; then | |
| echo "healthz ok (attempt $i)" | |
| break | |
| fi | |
| if [ "$i" -eq 30 ]; then | |
| echo "::error::/healthz not ready within 60s" | |
| exit 1 | |
| fi | |
| sleep 2 | |
| done | |
| curl -fsS --max-time 30 -D "$RUNNER_TEMP/mcp-headers.txt" -o "$RUNNER_TEMP/mcp-body.txt" \ | |
| http://localhost:8000/mcp \ | |
| -H "Authorization: Bearer $MCP_AUTH_TOKEN" \ | |
| -H "Content-Type: application/json" \ | |
| -H "Accept: application/json, text/event-stream" \ | |
| --data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci-smoke","version":"0.0.1"}}}' | |
| if ! grep -qi '^mcp-session-id:' "$RUNNER_TEMP/mcp-headers.txt"; then | |
| echo "::error::initialize response has no mcp-session-id header" | |
| exit 1 | |
| fi | |
| # A JSON-RPC *error* still returns HTTP 200 — serverInfo only | |
| # appears in a successful initialize result. | |
| if ! grep -q '"serverInfo"' "$RUNNER_TEMP/mcp-body.txt"; then | |
| echo "::error::initialize response body has no serverInfo" | |
| cat "$RUNNER_TEMP/mcp-body.txt" | |
| exit 1 | |
| fi | |
| echo "MCP initialize ok" | |
| # The remote-boot tests are a vitest suite, so they need the repo's | |
| # Node toolchain. They run after the local smoke on purpose: a | |
| # native-binding regression still fails fast, before the install. | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: npm | |
| - run: npm ci | |
| # Without this, onnxruntime-node's postinstall downloads CUDA binaries on linux/x64 | |
| env: | |
| ONNXRUNTIME_NODE_INSTALL: skip | |
| # Runs the vitest config directly: `npm run test:remote-boot` would | |
| # rebuild the image, and the buildx step above already built it. | |
| - name: Remote image boot tests (stubbed Sync client) | |
| run: npx vitest run --config vitest.remote-boot.config.ts | |
| env: | |
| REMOTE_IMAGE: vault-cortex:remote-ci | |
| # Fires on any prior step's failure (build included, so the list may | |
| # be empty) so a red run is diagnosable from the Actions log alone. | |
| # Two name filters OR together: the boot smoke's fixed `smoke` name | |
| # and the remote-boot-* containers the test suite creates. The suite | |
| # removes its own containers on success; anything still present is a | |
| # failure artifact. | |
| - name: Dump container diagnostics | |
| if: failure() | |
| run: | | |
| docker ps -a | |
| for container in $(docker ps -aq --filter name=smoke --filter name=remote-boot-); do | |
| docker inspect -f '{{.Name}} {{json .State}}' "$container" | |
| docker logs "$container" | |
| done | |
| - name: Remove smoke containers | |
| if: always() | |
| run: | | |
| docker ps -aq --filter name=smoke --filter name=remote-boot- | xargs -r docker rm -f -v | |
| docker volume ls -q --filter name=remote-boot- | xargs -r docker volume rm |