Skip to content

fix(release): accept canonical workflow pagination links #1530

fix(release): accept canonical workflow pagination links

fix(release): accept canonical workflow pagination links #1530

Workflow file for this run

name: CI
on:
pull_request:
push:
branches:
- main
permissions:
contents: read
jobs:
metadata_scope:
name: metadata-scope
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
metadata_only: ${{ steps.classify.outputs.metadata_only }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.17.0
- name: Classify metadata-only change
id: classify
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -eu
METADATA_ONLY="$(node scripts/ci-scope.mjs --event pull_request --base "$BASE_SHA" --head "$HEAD_SHA")"
case "$METADATA_ONLY" in
true|false) ;;
*) echo "Malformed metadata-only scope output" >&2; exit 1 ;;
esac
printf 'metadata_only=%s\n' "$METADATA_ONLY" >> "$GITHUB_OUTPUT"
changesets:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Require changeset for user-facing changes
env:
BASE_REF: origin/${{ github.event.pull_request.base.ref }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: node scripts/check-changesets.mjs
validate:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.33.0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
# Node 24 — Dawn's floor is the active LTS line (node:sqlite
# unflagged, npm >= 11 bundled). Matches release.yml and engines.
node-version: 24.17.0
cache: pnpm
- name: Install
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm lint
- name: Build Cache Config
run: pnpm check:build-cache
- name: Build
run: pnpm build
- name: Typecheck
run: pnpm typecheck
- name: Source Tests
# `hono-node-roundtrip` needs a Docker daemon — the first suite in
# `pnpm test` ever to, since previously only the `*-docker` jobs did. It
# SKIPS without one so a contributor with no Docker can still run the
# suite, and this flag is the other half of that bargain: on a runner,
# which always has Docker, a skip means the coverage was lost silently,
# so the flag turns it into a hard failure instead.
env:
DAWN_REQUIRE_DOCKER: "1"
run: pnpm test
- name: Release Inventory
run: pnpm check:release-inventory
- name: Release Controller Tests
run: pnpm test:release-controller
- name: Docs Check
run: node scripts/check-docs.mjs
- name: Pack Check
run: pnpm pack:check
- name: TypeScript Tooling Pack Smoke
run: pnpm verify:typescript-tooling-pack
- name: Harness Coordinator Self-Test
run: pnpm verify:harness:self-test
- name: Framework Verification
run: pnpm verify:harness:framework
- name: Runtime Contract Verification
run: pnpm verify:harness:runtime
- name: Smoke Harness Verification
run: pnpm verify:harness:smoke
- name: Upload Harness Artifacts
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: harness-artifacts
path: artifacts/testing/
retention-days: 7
testing-windows:
runs-on: windows-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.33.0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.17.0
cache: pnpm
- name: Install
run: pnpm install --frozen-lockfile
- name: Build testing dependency closure
# The focused test starts the CLI from its built dist output. The
# trailing ellipsis selects @dawn-ai/testing and its dependencies.
run: pnpm --filter @dawn-ai/testing... build
- name: Native Windows subprocess shutdown tests
# Exercise the real win32 taskkill tree and the injected stale-dispatch
# guard without running broader Windows HMR coverage.
run: pnpm --filter @dawn-ai/testing exec vitest --run --config vitest.config.ts test/subprocess.test.ts --testNamePattern "Windows process tree|injected Windows tree kill"
- name: Dependency security regressions
run: pnpm exec vitest --run --config test/security-dependencies/vitest.config.ts test/security-dependencies/dependency-resolution.test.ts test/security-dependencies/hono-serve-static-windows.test.ts
dependency-security-browser:
permissions:
contents: read
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.33.0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.17.0
cache: pnpm
- name: Install
run: pnpm install --frozen-lockfile
- name: Install Chromium
run: pnpm exec playwright install --with-deps chromium
- name: Dependency security browser regressions
run: |
pnpm exec tsc -p test/security-dependencies/tsconfig.json --noEmit
pnpm exec playwright test --config test/security-dependencies/playwright.config.ts
sandbox-docker:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.33.0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
# Node 24 — Dawn's floor is the active LTS line (node:sqlite
# unflagged, npm >= 11 bundled). Matches release.yml and engines.
node-version: 24.17.0
cache: pnpm
- name: Install
run: pnpm install --frozen-lockfile
- name: Build sandbox package
run: pnpm --filter @dawn-ai/sandbox... build
- name: Pull sandbox workload image
run: |
SANDBOX_IMAGE="$(
pnpm exec tsx -e 'import { loadCompatibilityPolicy } from "./scripts/kubernetes-compat/policy.ts"; loadCompatibilityPolicy().then((policy) => process.stdout.write(policy.images.sandboxWorkload))'
)"
docker pull "$SANDBOX_IMAGE"
- name: Real-Docker sandbox conformance + e2e
run: DAWN_TEST_DOCKER=1 pnpm --filter @dawn-ai/sandbox test docker-sandbox.integration
pgvector-docker:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.33.0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
# Node 24 — Dawn's floor is the active LTS line (node:sqlite
# unflagged, npm >= 11 bundled). Matches release.yml and engines.
node-version: 24.17.0
cache: pnpm
- name: Install
run: pnpm install --frozen-lockfile
- name: Build packages
# Full topological build — @dawn-ai/memory-pgvector (and @dawn-ai/testing)
# compile from source against @dawn-ai/cli/@dawn-ai/workspace etc., so a
# narrow per-package filter fails to build those deps first (TS2307).
run: pnpm build
- name: Real-Postgres pgvector tests
run: DAWN_TEST_PGVECTOR=1 pnpm --filter @dawn-ai/memory-pgvector test
postgres-storage-docker:
runs-on: ubuntu-latest
# Five test files (four in @dawn-ai/postgres-storage, one in @dawn-ai/cli)
# each start their own postgres:16 container, and a cold runner also pulls
# the image. 30 minutes leaves headroom for a slow pull plus the
# per-container startup timeout without masking a hung suite.
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.33.0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
# Node 24 — Dawn's floor is the active LTS line (node:sqlite
# unflagged, npm >= 11 bundled). Matches release.yml and engines.
node-version: 24.17.0
cache: pnpm
- name: Install
run: pnpm install --frozen-lockfile
- name: Build packages
# Full topological build — @dawn-ai/postgres-storage (and @dawn-ai/testing)
# compile from source against @dawn-ai/cli/@dawn-ai/workspace etc., so a
# narrow per-package filter fails to build those deps first (TS2307).
run: pnpm build
- name: Real-Postgres storage tests
run: DAWN_TEST_PGSTORAGE=1 pnpm --filter @dawn-ai/postgres-storage test
- name: Real-Postgres Agent Protocol pending-interrupts tests
# GET /threads/:id/pending_interrupts is purely checkpoint-backed, so the
# spec asks for it against both checkpointer fixtures. This job already
# has Docker and a built workspace, so the gated @dawn-ai/cli lane rides
# here rather than paying for a fourth Postgres job.
#
# A filename filter passed to the package's OWN test script, not a
# spelled-out `exec vitest`: nobody runs this lane locally, so a config
# move or a new setupFiles in that script must not leave this one lane
# silently running a different vitest from every other lane.
#
# Green here means the Postgres assertions RAN, not merely that nothing
# failed. Two different mistakes are covered by two different
# mechanisms: a filter that matches no file exits 1 on its own
# (`passWithNoTests` is false by default in vitest 4), and a suite that
# matches but skips is caught by the "gated Postgres lane" self-check in
# the test file — vitest has no fail-on-skipped flag to do it from here.
run: DAWN_TEST_PGSTORAGE=1 pnpm --filter @dawn-ai/cli test pending-interrupts-endpoint
edge-workerd:
runs-on: ubuntu-latest
# Postgres + a wsproxy container, plus `wrangler dev` booting real workerd.
# 30 minutes leaves headroom for a cold image pull and the workerd binary
# download without masking a hung startup.
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.33.0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
# Node 24 — Dawn's floor is the active LTS line (node:sqlite
# unflagged, npm >= 11 bundled). Matches release.yml and engines.
node-version: 24.17.0
cache: pnpm
- name: Install
# The workerd binary arrives through wrangler's postinstall, which pnpm
# 10 blocks unless the package is in `onlyBuiltDependencies` — it is, in
# pnpm-workspace.yaml. Without that the lane fails with no binary to exec.
run: pnpm install --frozen-lockfile
- name: Build packages
# Full topological build, not a filtered one: the lane builds a fixture
# app with `dawn build` and then resolves @dawn-ai/* from dist exactly as
# a deployed bundle does, so every workspace package must be compiled
# first (same #320 lesson as inspector-e2e below).
run: pnpm build
- name: Real-workerd edge lane
# The JSON report is this job's receipt, read by the step below.
run: |
DAWN_TEST_WORKERD=1 pnpm --filter @dawn-ai/cli test workerd-lane \
--reporter=default --reporter=json \
--outputFile.json="${{ runner.temp }}/workerd-lane-report.json"
- name: Assert the workerd lane did not skip
# `workerd-lane.test.ts` gates itself on DAWN_TEST_WORKERD, and the step
# above is the ONLY place that flag is set. So any drift between the two
# strings — a rename on either side — turns this merge gate into a no-op
# THAT STILL REPORTS GREEN: every test skips and vitest exits 0.
# (Verified: with the flag unset, "1 skipped", exit 0, success: true.)
#
# Same bargain `DAWN_REQUIRE_DOCKER=1` strikes on validate's Source
# Tests: the escape hatch that lets a contributor run the suite without
# Docker and a workerd binary must not become a way to lose the coverage
# on a runner that has both.
#
# Deliberately NOT `if: always()` — a lane that genuinely FAILED has
# already reddened the job and reported a better message than this one.
# The case this exists for is the lane exiting 0 having run nothing.
run: |
node -e '
const fs = require("node:fs")
const path = process.argv[1]
if (!fs.existsSync(path)) {
throw new Error("no vitest report at " + path + " — the lane produced no run at all.")
}
const report = JSON.parse(fs.readFileSync(path, "utf8"))
const passed = report.numPassedTests ?? 0
const skipped = report.numPendingTests ?? 0
if (passed === 0 || skipped > 0) {
throw new Error(
"the workerd lane reported " + passed + " passed and " + skipped + " skipped. " +
"This job is the only place DAWN_TEST_WORKERD=1 is set, so a skip here means the " +
"merge gate ran nothing while still exiting 0. Check that the flag name in " +
"packages/cli/test/workerd-lane.test.ts still matches the one this job exports.",
)
}
console.log("workerd lane ran: " + passed + " passed, 0 skipped.")
' "${{ runner.temp }}/workerd-lane-report.json"
copilotkit-examples-e2e:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.33.0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.17.0
cache: pnpm
- name: Install
run: pnpm install --frozen-lockfile
- name: Install Chromium
run: pnpm --filter @dawn-example/chat-web exec playwright install --with-deps chromium
- name: Build example workspace dependencies
run: pnpm --filter @dawn-ai/ag-ui build
- name: Verify chat transport
run: pnpm --filter @dawn-example/chat-web test:e2e
- name: Verify research transport
run: pnpm --filter @dawn-example/research-web test:e2e
vercel-native:
if: >-
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository)
runs-on: ubuntu-latest
timeout-minutes: 45
environment: vercel-preview
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.33.0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.17.0
cache: pnpm
- name: Install
run: pnpm install --frozen-lockfile
- name: Build packages
run: pnpm build
- name: Prepare native Vercel artifact directory
run: install -d -m 0700 -- "${{ runner.temp }}/vercel-native"
- name: Run native Vercel previews
id: native-vercel
env:
DAWN_TEST_VERCEL: "1"
DAWN_VERCEL_TOKEN: ${{ secrets.DAWN_VERCEL_TOKEN }}
DAWN_VERCEL_ORG_ID: ${{ secrets.DAWN_VERCEL_ORG_ID }}
DAWN_VERCEL_PROJECT_ID: ${{ secrets.DAWN_VERCEL_PROJECT_ID }}
DAWN_VERCEL_DATABASE_URL: ${{ secrets.DAWN_VERCEL_DATABASE_URL }}
DAWN_VERCEL_ARTIFACT_DIR: ${{ runner.temp }}/vercel-native
run: >-
pnpm --filter @dawn-ai/cli test vercel-native-lane.test.ts
--reporter=json
--outputFile.json="${{ runner.temp }}/vercel-native/vitest.json"
- name: Reconcile and remove native Vercel resources
id: native-vercel-cleanup
if: always()
env:
DAWN_VERCEL_TOKEN: ${{ secrets.DAWN_VERCEL_TOKEN }}
DAWN_VERCEL_ORG_ID: ${{ secrets.DAWN_VERCEL_ORG_ID }}
DAWN_VERCEL_PROJECT_ID: ${{ secrets.DAWN_VERCEL_PROJECT_ID }}
DAWN_VERCEL_DATABASE_URL: ${{ secrets.DAWN_VERCEL_DATABASE_URL }}
DAWN_VERCEL_ARTIFACT_DIR: ${{ runner.temp }}/vercel-native
run: node packages/cli/test/helpers/vercel-native-cleanup.mjs --cleanup
- name: Assert closed native Vercel receipt
env:
DAWN_VERCEL_ARTIFACT_DIR: ${{ runner.temp }}/vercel-native
run: node packages/cli/test/helpers/vercel-native-cleanup.mjs --assert-receipt
- name: Prepare native Vercel diagnostics
id: native_vercel_diagnostics
if: always()
env:
DAWN_VERCEL_TOKEN: ${{ secrets.DAWN_VERCEL_TOKEN }}
DAWN_VERCEL_ORG_ID: ${{ secrets.DAWN_VERCEL_ORG_ID }}
DAWN_VERCEL_PROJECT_ID: ${{ secrets.DAWN_VERCEL_PROJECT_ID }}
DAWN_VERCEL_DATABASE_URL: ${{ secrets.DAWN_VERCEL_DATABASE_URL }}
DAWN_VERCEL_ARTIFACT_DIR: ${{ runner.temp }}/vercel-native
run: node packages/cli/test/helpers/vercel-native-cleanup.mjs --prepare-artifacts
- name: Upload native Vercel diagnostics
if: failure() && steps.native_vercel_diagnostics.outcome == 'success'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: vercel-native-diagnostics
path: ${{ runner.temp }}/vercel-native/upload/
if-no-files-found: error
retention-days: 3
inspector-e2e:
runs-on: ubuntu-latest
# 35, not 20: the browser lane below adds a chromium download, a second
# standalone boot and 19 single-worker spec files (44 tests) on top of the
# vitest lane.
timeout-minutes: 35
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.33.0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
# Node 24 — Dawn's floor is the active LTS line (node:sqlite
# unflagged, npm >= 11 bundled). Matches release.yml and engines.
node-version: 24.17.0
cache: pnpm
- name: Install
run: pnpm install --frozen-lockfile
- name: Build inspector + workspace deps
# The `...` suffix builds @dawn-ai/inspector AND its workspace deps in
# topological order (`next build` compiles against @dawn-ai/core and
# @dawn-ai/memory dist — the #320 pgvector-lane lesson: partial builds
# break). The e2e tests boot the built .next/standalone server.
run: pnpm turbo run build --filter=@dawn-ai/inspector...
- name: Inspector standalone e2e
run: DAWN_TEST_INSPECTOR=1 pnpm --filter @dawn-ai/inspector test
- name: Install Playwright browsers
# `--filter` is required, not stylistic: @playwright/test is a dependency of
# packages/inspector only, so a bare root `pnpm exec playwright` fails with
# ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL / Command "playwright" not found.
run: pnpm --filter @dawn-ai/inspector exec playwright install --with-deps chromium
- name: Inspector browser verification
# Boots the same .next/standalone artifact the vitest lane above uses (`Inspector
# standalone e2e`, not the browser-install step immediately above), against a
# seeded 1250-record store. `workers: 1` in playwright.config.ts is load-bearing:
# the specs share one store and several mutate it.
run: pnpm --filter @dawn-ai/inspector test:e2e
- name: Upload Playwright traces
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: inspector-playwright-traces
path: packages/inspector/test-results
retention-days: 7
chart-validate:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Helm
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
- name: Install kubeconform
# This step extracts a tarball from the internet and `sudo install`s it
# into /usr/local/bin — the same trust boundary as a GitHub Action, and
# every action in this repo is SHA-pinned. So pin the artifact too.
#
# The checksum is a literal on purpose. Fetching the release CHECKSUMS
# file over the same channel and trusting it verifies nothing: whoever
# can serve a bad tarball can serve a matching checksum. That is the
# mistake upstream kind.sh makes. This value was read from the v0.8.0
# release CHECKSUMS and confirmed against a locally computed sha256 of
# the downloaded artifact.
#
# --fail so a 429/5xx is an error rather than an HTML error page written
# into the tarball (which surfaced as "tar: Error is not recoverable"),
# and --retry so a transient blip is retried instead of failing CI.
run: |
set -euo pipefail
expected=9bc2bffbf71f261128533edaf912153948b7ff238f9a531ae6d34466ec287883
curl --fail --retry 5 --retry-all-errors --retry-delay 2 -sSL -o /tmp/kubeconform.tar.gz \
https://github.com/yannh/kubeconform/releases/download/v0.8.0/kubeconform-linux-amd64.tar.gz
actual="$(sha256sum /tmp/kubeconform.tar.gz | cut -d' ' -f1)"
if [ "$actual" != "$expected" ]; then
echo "kubeconform-linux-amd64.tar.gz checksum mismatch" >&2
echo " expected $expected" >&2
echo " actual $actual" >&2
exit 1
fi
tar -xzf /tmp/kubeconform.tar.gz -C /tmp kubeconform
sudo install /tmp/kubeconform /usr/local/bin/kubeconform
kubeconform -v
- name: Helm lint (strict) — dawn-sandbox-infra
run: helm lint --strict charts/dawn-sandbox-infra
- name: Reaper unit test
run: sh charts/dawn-sandbox-infra/test/reaper.test.sh
- name: Render assertions — dawn-sandbox-infra
run: sh charts/dawn-sandbox-infra/test/render.sh
- name: kubeconform (default values) — dawn-sandbox-infra
run: |
helm template test charts/dawn-sandbox-infra \
| kubeconform -strict -summary -ignore-missing-schemas
- name: kubeconform (restricted PSS, reaper disabled) — dawn-sandbox-infra
run: |
helm template test charts/dawn-sandbox-infra \
--set podSecurityStandard.enforce=restricted \
--set reaper.enabled=false \
| kubeconform -strict -summary -ignore-missing-schemas
- name: Helm lint (strict) — dawn-app
run: helm lint --strict charts/dawn-app
- name: Render assertions — dawn-app
run: sh charts/dawn-app/test/render.sh
- name: kubeconform (default values) — dawn-app
run: |
helm template test charts/dawn-app --set image.repository=example/app \
| kubeconform -strict -summary -ignore-missing-schemas
- name: kubeconform (full override) — dawn-app
run: |
helm template test charts/dawn-app \
--set image.repository=example/app \
--set ingress.enabled=true \
--set ingress.host=app.example.com \
--set autoscaling.enabled=true \
--set podDisruptionBudget.enabled=true \
| kubeconform -strict -summary -ignore-missing-schemas
sandbox-k8s:
needs: metadata_scope
if: ${{ always() && (github.event_name != 'pull_request' || needs.metadata_scope.result != 'success' || needs.metadata_scope.outputs.metadata_only != 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 35
env:
DAWN_TEST_K8S_CONTEXT: kind-dawn-k8s-canonical
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.33.0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
# Node 24 — Dawn's floor is the active LTS line (node:sqlite
# unflagged, npm >= 11 bundled). Matches release.yml and engines.
node-version: 24.17.0
cache: pnpm
- name: Install
run: pnpm install --frozen-lockfile
- name: Setup Helm
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
with:
version: v4.2.3
- name: Verify chart/provider permission parity
env:
DAWN_REQUIRE_HELM: "1"
run: pnpm exec vitest --run --config test/k8s-compat/vitest.config.ts chart-rbac
# helm/kind-action installs kind and kubectl with `curl -sSLo` and no
# `--fail`, so a transient upstream 429/5xx writes the error page into
# the checksum file and the step dies with "no properly formatted
# checksum lines found". Priming RUNNER_TOOL_CACHE with verified
# binaries makes the action's `[[ ! -x ... ]]` guards skip both
# downloads entirely.
#
# The versions here must match the `version:` and `kubectl_version:`
# inputs on the step below. If any of them ever drift, this step primes
# a path the action does not read, the action downloads that tool as it
# does today, and CI is degraded — not broken.
- name: Prime kind tool cache
run: scripts/prime-kind-cache.sh v0.32.0 v1.35.6
- name: Create kind cluster
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
with:
version: v0.32.0
kubectl_version: v1.35.6
node_image: kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95
config: .github/kind/kind-calico.yaml
cluster_name: dawn-k8s-canonical
- name: Prepare verified Calico manifest
env:
CALICO_MANIFEST: ${{ runner.temp }}/dawn-calico.yaml
run: pnpm exec tsx scripts/kubernetes-compat/workflow.ts prepare-calico --output "$CALICO_MANIFEST"
- name: Install verified Calico
env:
CALICO_MANIFEST: ${{ runner.temp }}/dawn-calico.yaml
run: |
set -eu
kubectl --context "$DAWN_TEST_K8S_CONTEXT" apply --filename "$CALICO_MANIFEST"
kubectl --context "$DAWN_TEST_K8S_CONTEXT" -n kube-system rollout status daemonset/calico-node --timeout=180s
kubectl --context "$DAWN_TEST_K8S_CONTEXT" wait --for=condition=Ready nodes --all --timeout=180s
- name: Run Kubernetes 1.35 compatibility harness
run: pnpm verify:k8s:compat -- --target 1.35 --context "$DAWN_TEST_K8S_CONTEXT"
- name: Upload Kubernetes compatibility artifacts
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: kubernetes-compat-canonical
path: artifacts/testing/kubernetes-compat/**
if-no-files-found: warn
retention-days: 7
sandbox-k8s-e2e:
# Full-arc smoke: a deployed Dawn app (built the user-facing way via
# `dawn build`'s node target, run as `node .dawn/build/server.mjs`) drives
# kubernetesSandbox to spawn a REAL sandbox Pod, then tears it down. Mirrors
# sandbox-k8s's kind+Calico setup; adds a Verdaccio-published image build and
# both Helm charts (cross-namespace: app in dawn-app, sandboxes in
# dawn-sandboxes). Determinism comes from a baked aimock (no model key).
needs: metadata_scope
if: ${{ always() && (github.event_name != 'pull_request' || needs.metadata_scope.result != 'success' || needs.metadata_scope.outputs.metadata_only != 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 30
env:
# Marks this as the gated full-arc e2e lane (distinct from sandbox-k8s).
DAWN_TEST_SMOKE_E2E: "1"
KIND_CLUSTER: dawn-smoke
DAWN_TEST_K8S_CONTEXT: kind-dawn-smoke
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.33.0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
# Node 24 — Dawn's floor is the active LTS line (node:sqlite
# unflagged, npm >= 11 bundled). Matches release.yml and engines.
node-version: 24.17.0
cache: pnpm
- name: Install
run: pnpm install --frozen-lockfile
- name: Build
# Full build so every published-to-Verdaccio @dawn-ai/* package has a
# dist/ (publishWorkspace packs dist as-is).
run: pnpm build
- name: Setup Helm
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
with:
version: v4.2.3
- name: Verify chart/provider permission parity
env:
DAWN_REQUIRE_HELM: "1"
run: pnpm exec vitest --run --config test/k8s-compat/vitest.config.ts chart-rbac
# See the sandbox-k8s lane: primes the tool cache so the action skips its
# own no-`--fail` kind and kubectl downloads. Keep these versions and
# the `version:`/`kubectl_version:` inputs below in lockstep.
- name: Prime kind tool cache
run: scripts/prime-kind-cache.sh v0.32.0 v1.35.6
- name: Create kind cluster
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
with:
version: v0.32.0
kubectl_version: v1.35.6
node_image: kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95
config: .github/kind/kind-calico.yaml
cluster_name: dawn-smoke
- name: Prepare verified Calico manifest
env:
CALICO_MANIFEST: ${{ runner.temp }}/dawn-calico.yaml
run: pnpm exec tsx scripts/kubernetes-compat/workflow.ts prepare-calico --output "$CALICO_MANIFEST"
- name: Install verified Calico
env:
CALICO_MANIFEST: ${{ runner.temp }}/dawn-calico.yaml
run: |
set -eu
kubectl --context "$DAWN_TEST_K8S_CONTEXT" apply --filename "$CALICO_MANIFEST"
kubectl --context "$DAWN_TEST_K8S_CONTEXT" -n kube-system rollout status daemonset/calico-node --timeout=180s
kubectl --context "$DAWN_TEST_K8S_CONTEXT" wait --for=condition=Ready nodes --all --timeout=180s
- name: Preload sandbox workload image into kind
# Pull through the node's CRI so containerd records the canonical digest
# reference that the sandbox Pod uses during the packaged-app proof.
run: |
SANDBOX_IMAGE="$(
pnpm exec tsx -e 'import { loadCompatibilityPolicy } from "./scripts/kubernetes-compat/policy.ts"; loadCompatibilityPolicy().then((policy) => process.stdout.write(policy.images.sandboxWorkload))'
)"
docker exec "${KIND_CLUSTER}-control-plane" crictl pull "$SANDBOX_IMAGE"
docker exec "${KIND_CLUSTER}-control-plane" crictl inspecti "$SANDBOX_IMAGE" >/dev/null
- name: Build and load smoke app image (Verdaccio)
# Start the local registry (bound 0.0.0.0 so the in-image npm install can
# reach it via host.docker.internal), publish the workspace, build the app
# image the user-facing way (build-image.sh → node-target Dockerfile), then
# load it into kind. The registry only needs to live for this step.
run: |
set -eu
URLFILE="$(mktemp)"
pnpm exec tsx test/k8s-smoke/serve-registry.ts "$URLFILE" &
REG_PID=$!
for _ in $(seq 1 180); do
[ -s "$URLFILE" ] && break
kill -0 "$REG_PID" 2>/dev/null || { echo "serve-registry exited before publishing"; exit 1; }
sleep 1
done
[ -s "$URLFILE" ] || { echo "registry URL never became ready"; kill "$REG_PID" 2>/dev/null || true; exit 1; }
REG_URL="$(cat "$URLFILE")"
echo "registry ready at $REG_URL"
sh test/k8s-smoke/build-image.sh k8s "$REG_URL"
kill "$REG_PID" 2>/dev/null || true
kind load docker-image dawn-smoke-app:k8s --name "$KIND_CLUSTER"
- name: Build and load aimock image
run: |
docker build -t dawn-smoke-aimock:latest -f test/k8s-smoke/aimock/Dockerfile test/k8s-smoke
kind load docker-image dawn-smoke-aimock:latest --name "$KIND_CLUSTER"
- name: Create app namespace
run: kubectl --context "$DAWN_TEST_K8S_CONTEXT" create namespace dawn-app
- name: Install sandbox-infra chart
run: |
helm --kube-context "$DAWN_TEST_K8S_CONTEXT" install dawn-sandbox-infra charts/dawn-sandbox-infra \
-n dawn-app \
-f test/k8s-smoke/values-sandbox-infra.yaml --wait
- name: Deploy aimock
run: |
kubectl --context "$DAWN_TEST_K8S_CONTEXT" apply -f test/k8s-smoke/aimock.k8s.yaml
kubectl --context "$DAWN_TEST_K8S_CONTEXT" -n dawn-app rollout status deploy/aimock --timeout=120s
- name: Install dawn-app chart
run: |
helm --kube-context "$DAWN_TEST_K8S_CONTEXT" install dawn-app charts/dawn-app \
-n dawn-app \
-f test/k8s-smoke/values-dawn-app.yaml --wait
- name: Run full-arc assertions
run: sh test/k8s-smoke/assert-k8s.sh
- name: Diagnostics + cleanup
if: always()
run: |
echo "----- app -----"
kubectl --context "$DAWN_TEST_K8S_CONTEXT" -n dawn-app get pods,svc -o wide || true
kubectl --context "$DAWN_TEST_K8S_CONTEXT" -n dawn-app logs deploy/dawn-app --tail=120 || true
echo "----- aimock -----"
kubectl --context "$DAWN_TEST_K8S_CONTEXT" -n dawn-app logs -l app.kubernetes.io/name=aimock --tail=120 --all-containers || true
kubectl --context "$DAWN_TEST_K8S_CONTEXT" -n dawn-app describe pods -l app.kubernetes.io/name=aimock || true
echo "----- sandboxes -----"
kubectl --context "$DAWN_TEST_K8S_CONTEXT" -n dawn-sandboxes get pods,pvc,networkpolicy -o wide || true
kubectl --context "$DAWN_TEST_K8S_CONTEXT" -n dawn-sandboxes describe pods -l app.kubernetes.io/managed-by=dawn || true
echo "----- teardown -----"
helm --kube-context "$DAWN_TEST_K8S_CONTEXT" uninstall dawn-app -n dawn-app || true
helm --kube-context "$DAWN_TEST_K8S_CONTEXT" uninstall dawn-sandbox-infra -n dawn-app || true
kubectl --context "$DAWN_TEST_K8S_CONTEXT" delete namespace dawn-app --ignore-not-found --wait=false || true
kubectl --context "$DAWN_TEST_K8S_CONTEXT" delete namespace dawn-sandboxes --ignore-not-found --wait=false || true
sandbox-docker-e2e:
# Full-arc smoke (docker-out-of-docker): a CONTAINERIZED Dawn app — built the
# user-facing way (`dawn build`'s node target, run as `node
# .dawn/build/server.mjs`) with the host Docker socket bind-mounted — drives
# dockerSandbox to spawn a REAL, isolated SIBLING sandbox container on the
# host daemon, then tears it down. Mirrors sandbox-docker's host-Docker setup;
# adds a Verdaccio-published image build (same mechanism as sandbox-k8s-e2e).
# Determinism comes from a baked aimock (no model key).
needs: metadata_scope
if: ${{ always() && (github.event_name != 'pull_request' || needs.metadata_scope.result != 'success' || needs.metadata_scope.outputs.metadata_only != 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 30
env:
# Marks this as the gated full-arc e2e lane (distinct from sandbox-docker).
DAWN_TEST_SMOKE_E2E: "1"
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.33.0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
# Node 24 — Dawn's floor is the active LTS line (node:sqlite
# unflagged, npm >= 11 bundled). Matches release.yml and engines.
node-version: 24.17.0
cache: pnpm
- name: Install
run: pnpm install --frozen-lockfile
- name: Build
# Full build so every published-to-Verdaccio @dawn-ai/* package has a
# dist/ (publishWorkspace packs dist as-is).
run: pnpm build
- name: Pull sandbox workload image
# Pull the digest-pinned policy image up front so sibling startup does
# not wait on an external registry.
run: |
SANDBOX_IMAGE="$(
pnpm exec tsx -e 'import { loadCompatibilityPolicy } from "./scripts/kubernetes-compat/policy.ts"; loadCompatibilityPolicy().then((policy) => process.stdout.write(policy.images.sandboxWorkload))'
)"
docker pull "$SANDBOX_IMAGE"
- name: Build app + aimock images (Verdaccio)
# Start the local registry (bound 0.0.0.0 so the in-image npm install can
# reach it via host.docker.internal), publish the workspace, then build
# the app image the user-facing way (build-image.sh docker → node-target
# Dockerfile + static docker CLI layer) and the baked aimock image. The
# registry only needs to live for the app build.
run: |
set -eu
URLFILE="$(mktemp)"
pnpm exec tsx test/k8s-smoke/serve-registry.ts "$URLFILE" &
REG_PID=$!
for _ in $(seq 1 180); do
[ -s "$URLFILE" ] && break
kill -0 "$REG_PID" 2>/dev/null || { echo "serve-registry exited before publishing"; exit 1; }
sleep 1
done
[ -s "$URLFILE" ] || { echo "registry URL never became ready"; kill "$REG_PID" 2>/dev/null || true; exit 1; }
REG_URL="$(cat "$URLFILE")"
echo "registry ready at $REG_URL"
sh test/k8s-smoke/build-image.sh docker "$REG_URL"
docker build -t dawn-smoke-aimock:latest -f test/k8s-smoke/aimock/Dockerfile test/k8s-smoke
kill "$REG_PID" 2>/dev/null || true
- name: Run full-arc assertions (DooD)
run: sh test/k8s-smoke/assert-docker.sh
- name: Diagnostics + cleanup
if: always()
run: |
echo "----- containers -----"
docker ps -a || true
echo "----- app logs -----"
docker logs dawn-smoke-app --tail=120 || true
echo "----- teardown -----"
docker rm -f dawn-smoke-app dawn-smoke-aimock || true
for c in $(docker ps -aq --filter name=dawn-sbx-); do docker rm -f "$c" || true; done
for v in $(docker volume ls -q --filter name=dawn-sbx-); do docker volume rm "$v" || true; done
docker network rm dawn-smoke-net || true
chart-apply-smoke:
needs: metadata_scope
if: ${{ always() && (github.event_name != 'pull_request' || needs.metadata_scope.result != 'success' || needs.metadata_scope.outputs.metadata_only != 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 15
env:
DAWN_TEST_K8S_CONTEXT: kind-dawn-chart-apply
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Helm
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
with:
version: v4.2.3
# See the sandbox-k8s lane: primes the tool cache so the action skips its
# own no-`--fail` kind and kubectl downloads. Keep these versions and
# the `version:`/`kubectl_version:` inputs below in lockstep.
- name: Prime kind tool cache
run: scripts/prime-kind-cache.sh v0.32.0 v1.35.6
- name: Create kind cluster
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
with:
version: v0.32.0
kubectl_version: v1.35.6
node_image: kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95
cluster_name: dawn-chart-apply
- name: Install dawn-app (placeholder image) and verify it serves
# nginx-unprivileged runs as non-root (UID 101, port 8080), so it satisfies
# the chart's hardened runAsNonRoot default — which is exactly what a real
# langgraph app image must also do. A root image (e.g. traefik/whoami) would
# be rejected by the kubelet, so this doubles as a proof of the non-root default.
run: |
set -eu
PLACEHOLDER_IMAGE="$(jq -er '.images.placeholderApp' .github/kubernetes-compatibility.json)"
REACHABILITY_IMAGE="$(jq -er '.images.reachabilityProbe' .github/kubernetes-compatibility.json)"
PLACEHOLDER_WITH_TAG="${PLACEHOLDER_IMAGE%@sha256:*}"
PLACEHOLDER_REPOSITORY="${PLACEHOLDER_WITH_TAG%:*}"
PLACEHOLDER_DIGEST="sha256:${PLACEHOLDER_IMAGE##*@sha256:}"
printf '%s\n' "$PLACEHOLDER_DIGEST" | grep -Eq '^sha256:[0-9a-f]{64}$'
printf '%s\n' "$REACHABILITY_IMAGE" | grep -Eq '@sha256:[0-9a-f]{64}$'
helm --kube-context "$DAWN_TEST_K8S_CONTEXT" install dawn-app charts/dawn-app \
--set-string image.repository="$PLACEHOLDER_REPOSITORY" \
--set-string image.digest="$PLACEHOLDER_DIGEST" \
--set containerPort=8080 --set healthPath=/ \
--wait --timeout 3m
kubectl --context "$DAWN_TEST_K8S_CONTEXT" rollout status deploy/dawn-app --timeout=120s
kubectl --context "$DAWN_TEST_K8S_CONTEXT" run curl --image="$REACHABILITY_IMAGE" --restart=Never --rm -i --quiet -- \
curl -sf http://dawn-app.default.svc.cluster.local/ >/dev/null && echo "app served OK"