Skip to content

Bump Microsoft.EntityFrameworkCore and Microsoft.EntityFrameworkCore.InMemory #4486

Bump Microsoft.EntityFrameworkCore and Microsoft.EntityFrameworkCore.InMemory

Bump Microsoft.EntityFrameworkCore and Microsoft.EntityFrameworkCore.InMemory #4486

Workflow file for this run

name: CI
on:
pull_request:
branches: [main]
merge_group:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
jobs:
# Detect which file groups changed so downstream jobs can skip when irrelevant.
# Runs on pull_request and merge_group (diff against base ref) and on push
# (diff against previous commit). When the event doesn't carry a meaningful
# diff (manual workflow_dispatch), dorny/paths-filter defaults every filter
# to true so nothing is skipped.
changes:
name: Detect changed paths
runs-on: ubuntu-latest
timeout-minutes: 2
outputs:
dotnet: ${{ steps.filter.outputs.dotnet }}
openapi: ${{ steps.filter.outputs.openapi }}
agents: ${{ steps.filter.outputs.agents }}
python_agents: ${{ steps.filter.outputs.python_agents }}
connectors_web: ${{ steps.filter.outputs.connectors_web }}
web: ${{ steps.filter.outputs.web }}
ci_workflow: ${{ steps.filter.outputs.ci_workflow }}
workflow_files: ${{ steps.filter.outputs.workflow_files }}
dispatcher_host: ${{ steps.filter.outputs.dispatcher_host }}
agent_images: ${{ steps.filter.outputs.agent_images }}
docs: ${{ steps.filter.outputs.docs }}
api_docs: ${{ steps.filter.outputs.api_docs }}
efcore_model: ${{ steps.filter.outputs.efcore_model }}
packages: ${{ steps.filter.outputs.packages }}
runtime_catalog: ${{ steps.filter.outputs.runtime_catalog }}
sample: ${{ steps.filter.outputs.sample }}
platform_dockerfile: ${{ steps.filter.outputs.platform_dockerfile }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4
id: filter
with:
filters: |
dotnet:
- '**/*.cs'
- '**/*.csproj'
- '**/*.slnx'
- 'Directory.Packages.props'
- 'Directory.Build.props'
- 'global.json'
- 'NuGet.config'
- '.config/dotnet-tools.json'
openapi:
- 'src/Cvoya.Spring.Host.Api/**'
- 'src/Cvoya.Spring.Cli/**'
- 'src/Cvoya.Spring.Web/**'
- '.config/dotnet-tools.json'
agents:
- 'packages/**/agents/**'
python_agents:
- 'agents/spring-voyage-agent/**'
- 'agents/spring-voyage-agent-sdk/**'
- 'agents/magazine-langgraph-orchestrator/**'
connectors_web:
- 'src/Cvoya.Spring.Connector.*/web/**'
- 'src/Cvoya.Spring.Web/src/connectors/**'
- 'eng/ci/validate-connector-web.sh'
- 'package.json'
- 'package-lock.json'
web:
- 'src/Cvoya.Spring.Web/**'
- 'src/Cvoya.Spring.Connector.*/web/**'
- 'src/Cvoya.Spring.Host.Api/openapi.json'
- 'eslint.config.mjs'
- 'package.json'
- 'package-lock.json'
ci_workflow:
# Editing ci.yml can alter any required job below, so fan out
# the full CI matrix on CI workflow changes.
- '.github/workflows/ci.yml'
workflow_files:
# Non-CI workflow edits only need the lightweight YAML guard
# below; running the full product test matrix for release or
# scheduled workflow edits adds cost without validating those
# workflows meaningfully.
- '.github/workflows/**'
dispatcher_host:
- 'eng/deploy/spring-voyage-host.sh'
- 'eng/deploy/scripts/**'
- 'eng/deploy/deploy.sh'
- 'eng/deploy/docker-compose.yml'
- 'eng/config/spring.env.example'
- 'src/Cvoya.Spring.Dispatcher/**'
agent_images:
# build-agent-images.sh builds every Dockerfile.agent.* in
# `eng/build/` (agent-base + claude-code + dapr + the four
# OSS role images). Globbing the family keeps the smoke
# job in sync with what the script actually builds, so a
# change to e.g. `Dockerfile.agent.oss-software-engineering`
# is exercised end-to-end on its own PR rather than
# slipping in unvalidated and only surfacing on a later
# workflow-touching PR.
- 'eng/build/Dockerfile.agent-base'
- 'eng/build/Dockerfile.agent.*'
- 'eng/build/build-agent-images.sh'
- 'src/Cvoya.Spring.AgentSidecar/**'
- 'agents/spring-voyage-agent/**'
- 'agents/magazine-langgraph-orchestrator/**'
- 'tests/smoke/smoke-agent-images.sh'
- 'tests/smoke/smoke-1087.sh'
# #1120: BYOI conformance path 2 fixture (npm-installed
# bridge). The smoke driver builds an image from this
# Dockerfile + a `npm pack` tarball of the in-tree sidecar,
# so the job needs to re-run when either side changes.
- 'tests/fixtures/byoi-path2/**'
docs:
- 'docs/**'
- '!docs/decisions/**'
- '!docs/plan/**'
- '!docs/roadmap/**'
api_docs:
# C2.2 (#1251): re-render the consumer-facing v1 API
# reference whenever the spec, the renderer wiring, or the
# docs/api/ index changes. The output (docs/api/v1.html) is
# gitignored — the job uploads it as a workflow artifact so
# consumers can fetch the rendered HTML without a local build.
- 'src/Cvoya.Spring.Host.Api/openapi.json'
- 'src/Cvoya.Spring.Web/package.json'
- 'docs/api/**'
- 'package.json'
- 'package-lock.json'
efcore_model:
# #1430: trigger the efcore-model-drift guard on any input
# that can move the live SpringDbContext model relative to
# SpringDbContextModelSnapshot.cs — entities, configurations,
# the context itself, the migrations directory (snapshot lives
# there), or a dotnet-ef tooling bump.
- 'src/Cvoya.Spring.Dapr/Data/**'
- '.config/dotnet-tools.json'
packages:
# #1680: re-run `spring package validate --strict` against
# every in-tree package whenever the package YAML, the
# validator, or the CLI change. Keeps the in-repo packages
# installable on every PR.
- 'packages/**'
- 'src/Cvoya.Spring.Manifest/**'
- 'src/Cvoya.Spring.Cli/**'
runtime_catalog:
# ADR-0038: re-run the schema lint on any change to the
# platform runtime catalogue or its schema.
- 'eng/runtime-catalog/runtime-catalog.yaml'
- 'eng/runtime-catalog/runtime-catalog.schema.json'
sample:
# #2245: keep the workflow-agent sample buildable on every
# PR that could affect it (the sample, the SDK it depends
# on, the slnx, or this workflow itself). The sample is
# listed in SpringVoyage.slnx so the regular build/test
# jobs already cover it; this dedicated job guards against
# standalone-build regressions.
- 'samples/workflow-agent-image/**'
- 'src/Cvoya.Spring.AgentSdk/**'
- 'SpringVoyage.slnx'
- '.github/workflows/ci.yml'
platform_dockerfile:
# #2783: the platform image's web-build stage
# (eng/build/Dockerfile) was previously only built in
# release.yml, so a missing connector COPY (e.g. the
# WebSearch gap fixed by #2784) surfaced at release
# time rather than on the PR that introduced it. The
# `docker-platform-web` job below builds that target on
# every PR that touches the Dockerfile, the npm-ci
# enforcement script it bakes in, or any web workspace
# input (web filter, gated on jointly below).
- 'eng/build/Dockerfile'
- 'eng/ci/enforce-npm.cjs'
workflow-yaml:
name: Workflow YAML syntax
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.workflow_files == 'true'
timeout-minutes: 2
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Parse workflow YAML files
shell: bash
run: |
ruby -e '
require "yaml"
paths = Dir[".github/workflows/*.yml"] + Dir[".github/workflows/*.yaml"]
paths.sort.each do |path|
YAML.load_file(path)
puts "ok #{path}"
rescue Psych::SyntaxError => e
warn "::error file=#{path},line=#{e.line},col=#{e.column}::#{e.message}"
exit 1
end
'
- name: Audit workflow security
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
with:
version: 1.26.1
inputs: .github/workflows
persona: regular
advanced-security: false
annotations: true
dotnet:
name: Build, format, and test
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.dotnet == 'true' || needs.changes.outputs.ci_workflow == 'true'
timeout-minutes: 30
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5
with:
dotnet-version: "10.0.x"
cache: true
cache-dependency-path: |
**/*.csproj
Directory.Packages.props
global.json
NuGet.config
- name: Install Dapr CLI
shell: bash
run: |
wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash -s 1.14.1
dapr --version
# Cache ~/.dapr so we skip the CDN round-trip entirely once we've
# successfully inited at least once. 'dapr init --slim' pulls three
# binaries from the GitHub release asset CDN (daprd, placement,
# dashboard) and that CDN has had sustained 504 windows; a warm cache
# means the test job doesn't depend on CDN health.
- name: Cache Dapr runtime (~/.dapr)
id: dapr-cache
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ~/.dapr
key: dapr-slim-${{ runner.os }}-1.14.1-v1
- name: Initialize Dapr
if: steps.dapr-cache.outputs.cache-hit != 'true'
shell: bash
run: |
# Retry with a ~4-minute window so transient CDN 504s don't bounce
# the PR out of the merge queue. Uninstall between attempts so
# re-init starts clean.
for attempt in 1 2 3 4 5 6; do
if dapr init --slim; then
echo "dapr init succeeded on attempt $attempt"
exit 0
fi
echo "dapr init attempt $attempt failed; sleeping 30s before retry"
sleep 30
dapr uninstall --all || true
done
echo "::error::dapr init failed after 6 attempts over ~4 minutes - Dapr release CDN may be down"
exit 1
- name: Restore dependencies
run: dotnet restore SpringVoyage.slnx
- name: Restore dotnet tools (Kiota)
run: dotnet tool restore
- name: Build
run: dotnet build SpringVoyage.slnx --no-restore --configuration Release
# Formatting is a static-input check, so the pull-request result remains valid in the
# merge queue. Reuse the build above instead of restoring and compiling a third time.
- name: Check formatting
if: github.event_name != 'merge_group'
run: dotnet format SpringVoyage.slnx --no-restore --verify-no-changes
- name: Test
# Wrapped in a wall-clock watchdog (#2604): on rare hangs in
# Cvoya.Spring.Dapr.Tests the suite would sit silent until
# the 30-min job timeout killed the run with no diagnostics.
# The watchdog bounds the run, dumps thread stacks for live
# testhost processes when the budget is exceeded, and fails
# the step with a clear error message instead.
run: eng/ci/dotnet-test-with-watchdog.sh --solution SpringVoyage.slnx --no-restore --no-build --configuration Release
package-validate:
name: Validate in-tree packages
runs-on: ubuntu-latest
needs: changes
# #1680: offline pre-publish gate. Runs `spring package validate --strict
# --format json` against every package directory under packages/ and
# converts each diagnostic into a GitHub file annotation. Static-input
# check, so it's safe to skip in the merge queue — the validator's
# output is a function of the PR's own sources.
if: >
github.event_name != 'merge_group' &&
(needs.changes.outputs.packages == 'true' || needs.changes.outputs.ci_workflow == 'true')
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5
with:
dotnet-version: "10.0.x"
cache: true
cache-dependency-path: |
**/*.csproj
Directory.Packages.props
global.json
NuGet.config
- name: Restore dependencies
run: dotnet restore SpringVoyage.slnx
- name: Restore dotnet tools (Kiota)
run: dotnet tool restore
- name: Build CLI
run: dotnet build src/Cvoya.Spring.Cli/Cvoya.Spring.Cli.csproj --no-restore --configuration Release
- name: Validate every in-tree package (--strict, JSON, file annotations)
shell: bash
run: |
set -euo pipefail
# Discover every package directory (one level deep under packages/).
# Skip directories that don't carry package.yaml — those are
# ancillary trees (e.g. snapshots) and not installable packages.
mapfile -t pkgs < <(
find packages -maxdepth 2 -mindepth 2 -name package.yaml -printf '%h\n' \
| sort
)
if [ ${#pkgs[@]} -eq 0 ]; then
echo "No package.yaml found under packages/ — nothing to validate."
exit 0
fi
failed=0
for pkg in "${pkgs[@]}"; do
echo
echo "── Validating $pkg ──"
# Capture JSON to a tmp file so we can both display table output
# AND parse JSON for annotations without re-running the validator.
tmp_json="$(mktemp)"
set +e
dotnet run --project src/Cvoya.Spring.Cli --no-build --configuration Release -- \
package validate "$pkg" --strict --format json >"$tmp_json"
rc=$?
set -e
# Re-render the table form for readable logs.
dotnet run --project src/Cvoya.Spring.Cli --no-build --configuration Release -- \
package validate "$pkg" --strict || true
# Emit one GitHub annotation per diagnostic. The CLI's JSON shape
# is documented on the ValidateCommand class — `diagnostics[]`
# carries `{file, severity, code, message}`. jq is preinstalled
# on ubuntu-latest runners.
pkg_root="$pkg" jq -r '
.diagnostics[] |
"::" +
(if .severity == "error" then "error" else "warning" end) +
" file=" + env.pkg_root + "/" + .file +
",title=" + .code +
"::" + (.message | gsub("\n"; " ") | gsub("%"; "%25"))
' "$tmp_json"
rm -f "$tmp_json"
if [ "$rc" -ne 0 ]; then
failed=1
fi
done
if [ "$failed" -ne 0 ]; then
echo
echo "::error::One or more packages failed validation. See annotations above."
exit 1
fi
echo
echo "All packages validated cleanly."
agent-definitions-lint:
name: Lint agent definitions
runs-on: ubuntu-latest
needs: changes
# Skipped in the merge queue: this validates static intra-file references
# that don't depend on any integration. Another PR merging ahead can't
# silently break these - either its own queue run would have failed lint,
# or it didn't touch agent definitions.
if: >
github.event_name != 'merge_group' &&
(needs.changes.outputs.agents == 'true' || needs.changes.outputs.ci_workflow == 'true')
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Validate agent definition references
shell: bash
run: |
set -euo pipefail
failed=0
while IFS= read -r -d '' def_file; do
echo "Checking $def_file ..."
while IFS=: read -r _ ref_path; do
ref_path="$(echo "$ref_path" | xargs)"
base_dir="$(dirname "$def_file")"
resolved="$base_dir/$ref_path"
if [ ! -e "$resolved" ]; then
echo "::error file=$def_file::Referenced path '$ref_path' does not exist (resolved to '$resolved')"
failed=1
fi
done < <(grep -oE '(skills|workflows|connectors|execution|units)_dir:\s*\S+' "$def_file" 2>/dev/null || true)
done < <(find packages -path "*/agents/*.md" -not -name ".gitkeep" -print0 2>/dev/null)
while IFS= read -r -d '' def_file; do
echo "Checking $def_file ..."
while IFS=: read -r _ ref_path; do
ref_path="$(echo "$ref_path" | xargs)"
if [[ "$ref_path" != /* ]]; then
base_dir="$(dirname "$def_file")"
resolved="$base_dir/$ref_path"
else
resolved="$ref_path"
fi
if [ ! -e "$resolved" ]; then
echo "::error file=$def_file::Referenced path '$ref_path' does not exist (resolved to '$resolved')"
failed=1
fi
done < <(grep -oE 'path:\s*\S+' "$def_file" 2>/dev/null || true)
done < <(find packages \( -path "*/agents/*.yaml" -o -path "*/agents/*.yml" \) -print0 2>/dev/null)
if [ "$failed" -ne 0 ]; then
echo
echo "Agent definition lint failed: some referenced paths do not exist."
exit 1
fi
echo "All agent definition references are valid (or no definitions found yet)."
runtime-catalog-lint:
name: Lint runtime catalogue (ADR-0038)
runs-on: ubuntu-latest
needs: changes
# Skipped in the merge queue: this is a static check of the in-repo
# YAML against its sibling JSON Schema; another PR merging ahead can't
# silently break it.
if: >
github.event_name != 'merge_group' &&
(needs.changes.outputs.runtime_catalog == 'true' || needs.changes.outputs.ci_workflow == 'true')
timeout-minutes: 5
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.13"
cache: pip
cache-dependency-path: eng/ci/runtime-catalog-requirements.txt
- name: Install locked schema validator
run: python -m pip install --require-hashes -r eng/ci/runtime-catalog-requirements.txt
- name: Validate runtime-catalog.yaml against schema
run: >
check-jsonschema
--schemafile eng/runtime-catalog/runtime-catalog.schema.json
eng/runtime-catalog/runtime-catalog.yaml
sample-build:
name: Build workflow-agent sample (#2245)
runs-on: ubuntu-latest
needs: changes
# The sample is also listed in SpringVoyage.slnx so the regular
# `build` / `test` jobs already exercise it. This dedicated job
# gives PRs touching the sample (or anything it depends on)
# explicit signal that the standalone build still works, even
# when the regular dotnet filter does not trip.
if: >
needs.changes.outputs.sample == 'true' ||
needs.changes.outputs.ci_workflow == 'true'
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5
with:
dotnet-version: "10.0.x"
cache: true
cache-dependency-path: |
**/*.csproj
Directory.Packages.props
global.json
NuGet.config
- name: Restore sample
run: |
dotnet restore samples/workflow-agent-image/Cvoya.Spring.Sample.WorkflowAgent/Cvoya.Spring.Sample.WorkflowAgent.csproj
dotnet restore samples/workflow-agent-image/Cvoya.Spring.Sample.WorkflowAgent.Tests/Cvoya.Spring.Sample.WorkflowAgent.Tests.csproj
- name: Build sample
run: |
dotnet build samples/workflow-agent-image/Cvoya.Spring.Sample.WorkflowAgent/Cvoya.Spring.Sample.WorkflowAgent.csproj --no-restore --configuration Release
dotnet build samples/workflow-agent-image/Cvoya.Spring.Sample.WorkflowAgent.Tests/Cvoya.Spring.Sample.WorkflowAgent.Tests.csproj --no-restore --configuration Release
- name: Test sample
run: dotnet test samples/workflow-agent-image/Cvoya.Spring.Sample.WorkflowAgent.Tests/Cvoya.Spring.Sample.WorkflowAgent.Tests.csproj --no-restore --no-build --configuration Release
connector-web-lint:
name: Lint connector web submodules
runs-on: ubuntu-latest
needs: changes
# Skipped in the merge queue: like agent-definitions-lint this
# validates static intra-repo references that don't depend on any
# integration with other merged PRs.
if: >
github.event_name != 'merge_group' &&
(needs.changes.outputs.connectors_web == 'true' || needs.changes.outputs.ci_workflow == 'true')
timeout-minutes: 5
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Validate connector web submodules
shell: bash
run: bash eng/ci/validate-connector-web.sh
docs-evergreen-framing:
name: Lint docs (evergreen framing)
runs-on: ubuntu-latest
needs: changes
# Skipped in the merge queue — pure intra-repo grep with no external deps.
if: >
github.event_name != 'merge_group' &&
(needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci_workflow == 'true')
timeout-minutes: 2
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Reject pinned-version framing in user-facing docs
shell: bash
run: |
set -euo pipefail
# Docs are evergreen — they should reflect the system in the repo,
# not a pinned release tag. Version pins (V2, V2.1, etc.) belong
# in ADRs (docs/decisions/) and release plans (docs/plan/), and
# only there. The path filter excludes those directories upstream;
# this grep is a belt-and-braces check.
set +e
hits=$(grep -rEn --include='*.md' --exclude-dir=decisions --exclude-dir=plan --exclude-dir=roadmap '\bV2\b|\bV2\.1\b' docs/)
set -e
if [ -n "$hits" ]; then
echo "::error::Pinned-version framing (V2 / V2.1) found in user-facing docs."
echo "Move the version reference into docs/decisions/ or docs/plan/, or rephrase to be evergreen."
echo
echo "$hits"
exit 1
fi
web-lint:
name: Lint web (ESLint)
runs-on: ubuntu-latest
needs: changes
# Skipped in the merge queue: ESLint is a static-input check of the
# PR's own code. A pass on the pull_request run stays valid through
# the queue.
if: >
github.event_name != 'merge_group' &&
(needs.changes.outputs.web == 'true' || needs.changes.outputs.ci_workflow == 'true')
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "20"
cache: "npm"
- name: Install workspace dependencies
run: npm ci
- name: Lint
run: npm run lint
web-knip:
name: Dead-code check (knip)
runs-on: ubuntu-latest
needs: changes
# Skipped in the merge queue: knip is a static-input check of the
# PR's own sources. Catches unused files, unlisted dependencies, and
# unresolved imports that ESLint and tsc don't surface (see
# src/Cvoya.Spring.Web/knip.json for the rule scope - we deliberately
# gate only on `files`, `unlisted`, `unresolved`, `duplicates`; unused
# exports/types are reported but don't fail because the API surface
# is intentionally broad).
if: >
github.event_name != 'merge_group' &&
(needs.changes.outputs.web == 'true' || needs.changes.outputs.ci_workflow == 'true')
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "20"
cache: "npm"
- name: Install workspace dependencies
run: npm ci
- name: Knip (dead code, unlisted deps, unresolved imports)
run: npm --workspace=spring-voyage-dashboard run knip
web-typecheck:
name: Typecheck web (tsc)
runs-on: ubuntu-latest
needs: changes
# Skipped in the merge queue: tsc --noEmit is a static-input check of
# the PR's own sources. Catches type regressions in test files
# (`*.test.ts(x)`) and other code paths Next.js's build excludes from
# type-checking.
if: >
github.event_name != 'merge_group' &&
(needs.changes.outputs.web == 'true' || needs.changes.outputs.ci_workflow == 'true')
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "20"
cache: "npm"
- name: Install workspace dependencies
run: npm ci
# pretypecheck runs openapi-typescript against the committed
# src/Cvoya.Spring.Host.Api/openapi.json - no dotnet build needed
# here; openapi-drift covers contract freshness separately.
- name: Typecheck
run: npm --workspace=spring-voyage-dashboard run typecheck
web-build:
name: Build web (Next.js)
runs-on: ubuntu-latest
needs: changes
# Skipped in the merge queue: a successful production build of the
# Next.js app is a static-input check of the PR's own sources - it
# doesn't depend on the state of other merged PRs. Catches regressions
# like Turbopack workspace-root config drift (#297) that ESLint won't
# see.
if: >
github.event_name != 'merge_group' &&
(needs.changes.outputs.web == 'true' || needs.changes.outputs.ci_workflow == 'true')
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "20"
cache: "npm"
- name: Install workspace dependencies
run: npm ci
# prebuild runs openapi-typescript against the committed
# src/Cvoya.Spring.Host.Api/openapi.json - no dotnet build needed
# here; openapi-drift covers contract freshness separately.
- name: Build Next.js app
working-directory: src/Cvoya.Spring.Web
run: npm run build
# Enforce bundle-size budget against the freshly built artifact.
# Script reads `.next/static/chunks/*.js`, sums raw + gzipped, and
# picks out the largest single chunk. Budgets and rationale live
# in src/Cvoya.Spring.Web/scripts/check-bundle-size.mjs.
- name: Bundle-size budget
working-directory: src/Cvoya.Spring.Web
run: npm run check-bundle-size
# Persist the build artifact so downstream e2e + lighthouse jobs
# don't have to repeat `npm run build`. Saves ~30s per dependent
# job on cold cache.
- name: Upload Next.js build artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: web-next-build
path: |
src/Cvoya.Spring.Web/.next
!src/Cvoya.Spring.Web/.next/cache
retention-days: 1
if-no-files-found: error
# `.next` starts with a dot, so without this flag v4 silently
# excludes the entire directory tree (`include-hidden-files`
# defaults to false). The artifact would upload as empty and the
# downstream e2e / lighthouse jobs would fail at extract time.
include-hidden-files: true
web-test:
name: Test web (vitest)
runs-on: ubuntu-latest
needs: changes
# Skipped in the merge queue: vitest runs the portal's own unit tests
# against committed sources, so a pass on the pull_request run stays
# valid through the queue. #663 slipped through because this check
# didn't exist.
if: >
github.event_name != 'merge_group' &&
(needs.changes.outputs.web == 'true' || needs.changes.outputs.ci_workflow == 'true')
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "20"
cache: "npm"
- name: Install workspace dependencies
run: npm ci
# pretest runs openapi-typescript against the committed
# src/Cvoya.Spring.Host.Api/openapi.json - no dotnet build needed
# here; openapi-drift covers contract freshness separately.
# Use `test:coverage` (vitest --coverage) so the configured
# thresholds in vitest.config.ts gate the run; see that file for
# the rationale on threshold floors.
- name: Run vitest with coverage
run: npm --workspace=spring-voyage-dashboard run test:coverage
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: web-coverage
path: src/Cvoya.Spring.Web/coverage
retention-days: 7
if-no-files-found: ignore
web-api-docs:
name: Render web API docs (Redoc)
runs-on: ubuntu-latest
needs: changes
# C2.2 (#1251): re-render the consumer-facing v1 API reference from
# the committed openapi.json on every change to the spec or the
# renderer wiring. Skipped in the merge queue — the output is a
# static-input function of the PR's own sources, so a pass on the
# pull_request run stays valid through the queue.
if: >
github.event_name != 'merge_group' &&
(needs.changes.outputs.api_docs == 'true' || needs.changes.outputs.ci_workflow == 'true')
timeout-minutes: 5
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "20"
cache: "npm"
# Renders src/Cvoya.Spring.Host.Api/openapi.json into the
# gitignored docs/api/v1.html via @redocly/cli build-docs. The
# script in src/Cvoya.Spring.Web/package.json fetches the renderer
# transiently via `npx --yes` so we do not pay the cost of a full
# `npm ci` install just to render the docs. See docs/api/README.md
# for the consumer-facing index that points at this artefact.
- name: Generate API docs (docs/api/v1.html)
run: npm --workspace=spring-voyage-dashboard run generate-api-docs
- name: Verify rendered HTML exists
shell: bash
run: |
if [ ! -s docs/api/v1.html ]; then
echo "::error::docs/api/v1.html was not produced or is empty - check the generate-api-docs script wiring."
exit 1
fi
echo "Rendered $(wc -c < docs/api/v1.html) bytes."
- name: Upload rendered HTML as workflow artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: web-api-docs
path: docs/api/v1.html
retention-days: 30
if-no-files-found: error
web-e2e:
name: E2E web (Playwright smoke)
runs-on: ubuntu-latest
needs: [changes, web-build]
# Skipped in the merge queue: smoke tests run against the artifact
# produced by web-build. Their pass/fail is a function of the PR's
# own sources, so it stays valid through the queue.
if: >
github.event_name != 'merge_group' &&
(needs.changes.outputs.web == 'true' || needs.changes.outputs.ci_workflow == 'true')
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "20"
cache: "npm"
- name: Install workspace dependencies
run: npm ci
# Reuse the artifact from web-build to avoid rebuilding (~30s).
- name: Download Next.js build artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: web-next-build
path: src/Cvoya.Spring.Web/.next
# Cache Playwright browsers so cold-cache cost is bounded - the
# cache key is pinned to the Playwright version so a bump
# invalidates and re-downloads cleanly.
- name: Resolve Playwright version
id: playwright-version
working-directory: src/Cvoya.Spring.Web
run: |
version="$(node -p "require('@playwright/test/package.json').version")"
echo "version=${version}" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers (~/.cache/ms-playwright)
id: playwright-cache
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
- name: Install Playwright browser (Chromium + system deps)
if: steps.playwright-cache.outputs.cache-hit != 'true'
working-directory: src/Cvoya.Spring.Web
run: npx playwright install --with-deps chromium
# On a cache hit we still need OS-level deps that aren't cached
# by the browser cache key (libnss3, libatk1.0-0, etc.).
- name: Install Playwright system deps (cache-hit path)
if: steps.playwright-cache.outputs.cache-hit == 'true'
working-directory: src/Cvoya.Spring.Web
run: npx playwright install-deps chromium
- name: Run Playwright smoke tests
working-directory: src/Cvoya.Spring.Web
run: npm run test:e2e
- name: Upload Playwright report on failure
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: playwright-report
path: |
src/Cvoya.Spring.Web/playwright-report
src/Cvoya.Spring.Web/test-results
retention-days: 7
if-no-files-found: ignore
web-lighthouse:
name: Lighthouse CI (web)
runs-on: ubuntu-latest
needs: [changes, web-build]
# Skipped in the merge queue: Lighthouse runs against the same
# PR-built artifact and is bounded by the assertion thresholds in
# src/Cvoya.Spring.Web/lighthouserc.json. Performance runs are
# noisy, so the perf threshold is set as a `warn` (informational)
# while accessibility stays an `error` gate.
if: >
github.event_name != 'merge_group' &&
(needs.changes.outputs.web == 'true' || needs.changes.outputs.ci_workflow == 'true')
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "20"
cache: "npm"
- name: Install workspace dependencies
run: npm ci
- name: Download Next.js build artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: web-next-build
path: src/Cvoya.Spring.Web/.next
# Lighthouse spins up `next start` itself (configured in
# lighthouserc.json's `startServerCommand`). Pin the API target
# to a non-routable host so the dashboard renders shell + skeletons
# without depending on a backend (matches the Playwright smoke
# contract).
- name: Run Lighthouse CI
id: lighthouse
uses: treosh/lighthouse-ci-action@3e7e23fb74242897f95c0ba9cabad3d0227b9b18 # 12.6.2
env:
PORT: "3100"
SPRING_API_URL: "http://127.0.0.1:65535"
NEXT_PUBLIC_API_URL: "http://127.0.0.1:65535"
with:
configPath: src/Cvoya.Spring.Web/lighthouserc.json
uploadArtifacts: true
artifactName: lighthouse-report
temporaryPublicStorage: true
- name: Enforce Lighthouse error assertions
env:
ASSERTIONS: ${{ steps.lighthouse.outputs.assertionResults }}
shell: bash
run: |
set -euo pipefail
[[ -n "$ASSERTIONS" ]] || { echo "::error::Lighthouse produced no assertion results"; exit 1; }
jq -e 'all(.[]; .level != "error" or .passed == true)' <<<"$ASSERTIONS" >/dev/null || {
echo "::error::Lighthouse error-level assertions failed"
jq -r '.[] | select(.level == "error" and .passed != true) | "\(.url): \(.auditId) expected \(.operator) \(.expected), got \(.actual)"' <<<"$ASSERTIONS"
exit 1
}
python-lint:
name: Lint Python agents
runs-on: ubuntu-latest
needs: changes
# Skipped in the merge queue: linting is a static-input check of the
# PR's own code, so a pass on the pull_request run stays valid through
# the queue.
if: >
github.event_name != 'merge_group' &&
(needs.changes.outputs.python_agents == 'true' || needs.changes.outputs.ci_workflow == 'true')
timeout-minutes: 5
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.12'
- name: Install ruff
run: pip install ruff
- name: Lint
run: |
ruff check agents/spring-voyage-agent/
ruff check agents/spring-voyage-agent-sdk/
ruff check agents/magazine-langgraph-orchestrator/
- name: Check formatting
run: |
ruff format --check agents/spring-voyage-agent/
ruff format --check agents/spring-voyage-agent-sdk/
ruff format --check agents/magazine-langgraph-orchestrator/
python-test:
name: Test Python agents
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.python_agents == 'true' || needs.changes.outputs.ci_workflow == 'true'
timeout-minutes: 5
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.12'
- name: Install spring-voyage-agent-sdk
run: pip install -e agents/spring-voyage-agent-sdk[dev]
- name: Install spring-voyage-agent dependencies
run: pip install -r agents/spring-voyage-agent/requirements.txt -r agents/spring-voyage-agent/requirements-dev.txt
- name: Test spring-voyage-agent-sdk
run: pytest agents/spring-voyage-agent-sdk/tests/ -v
- name: Lint spring-voyage-agent-sdk
run: ruff check agents/spring-voyage-agent-sdk/
- name: Run spring-voyage-agent tests
run: pytest agents/spring-voyage-agent/tests/ -v
- name: Install magazine-langgraph-orchestrator (ADR-0066)
run: pip install -e agents/magazine-langgraph-orchestrator pytest-asyncio
- name: Run magazine-langgraph-orchestrator tests
run: pytest agents/magazine-langgraph-orchestrator/tests/ -v
openapi-drift:
name: OpenAPI contract drift
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.openapi == 'true' || needs.changes.outputs.ci_workflow == 'true'
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5
with:
dotnet-version: "10.0.x"
cache: true
cache-dependency-path: |
**/*.csproj
Directory.Packages.props
global.json
NuGet.config
- name: Restore dependencies
run: dotnet restore SpringVoyage.slnx
- name: Restore dotnet tools (Kiota)
run: dotnet tool restore
# Microsoft.Extensions.ApiDescription.Server regenerates
# src/Cvoya.Spring.Host.Api/openapi.json after Build when configured
# on Release. If a PR touched an endpoint without committing the
# updated contract, the working tree is dirty after this step.
# The CLI's GenerateKiotaClient target re-emits the gitignored
# Generated/ tree from this contract during build, so any contract
# change that breaks the typed client surfaces as a build failure.
- name: Build (regenerates openapi.json)
run: dotnet build SpringVoyage.slnx --no-restore --configuration Release
- name: Verify openapi.json matches committed contract
shell: bash
run: |
if ! git diff --exit-code -- src/Cvoya.Spring.Host.Api/openapi.json; then
echo "::error file=src/Cvoya.Spring.Host.Api/openapi.json::openapi.json is out of date. Run 'dotnet build SpringVoyage.slnx --configuration Release' locally and commit the regenerated file."
exit 1
fi
# #186: explicit drift check for the CLI's Kiota client. The earlier
# build step regenerates Generated/ on first compile (Inputs/Outputs
# gating); here we delete it and force a clean regeneration so a contract
# change that produces a tree the hand-written SpringApiClient wrapper
# can't compile against (Kiota tool bump, reshaped schema, renamed
# property the wrapper still references) fails CI here rather than at
# the next contributor's local build.
- name: Verify Kiota CLI client regenerates cleanly from openapi.json
shell: bash
run: |
rm -rf src/Cvoya.Spring.Cli/Generated
dotnet build src/Cvoya.Spring.Cli/Cvoya.Spring.Cli.csproj --no-restore --configuration Release
if [ ! -f src/Cvoya.Spring.Cli/Generated/kiota-lock.json ]; then
echo "::error::Kiota did not regenerate src/Cvoya.Spring.Cli/Generated/ - check the GenerateKiotaClient MSBuild target and the dotnet-tools manifest."
exit 1
fi
efcore-model-drift:
# #1430: catches the regression that crashed spring-worker in #1429.
# EF's MigrateAsync validates the live SpringDbContext model against
# SpringDbContextModelSnapshot.cs before any SQL runs and throws
# PendingModelChangesWarning when they diverge. PR #1263 added a
# migration without updating the snapshot, which only surfaced as a
# local-Podman crash loop. This job fails the PR instead.
name: EF Core model drift
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.efcore_model == 'true' || needs.changes.outputs.ci_workflow == 'true'
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5
with:
dotnet-version: "10.0.x"
cache: true
cache-dependency-path: |
**/*.csproj
Directory.Packages.props
global.json
NuGet.config
- name: Restore dependencies
run: dotnet restore SpringVoyage.slnx
- name: Restore dotnet tools (dotnet-ef, Kiota)
run: dotnet tool restore
# The EF tooling needs the latest compiled SpringDbContext so the
# model it loads matches the source on disk. has-pending-model-changes
# then diffs that loaded model against SpringDbContextModelSnapshot.cs
# — no DB connection is opened (SpringDbContextDesignTimeFactory uses
# a placeholder connection string).
- name: Build Cvoya.Spring.Dapr
run: dotnet build src/Cvoya.Spring.Dapr/Cvoya.Spring.Dapr.csproj --no-restore --configuration Debug
- name: Verify SpringDbContext snapshot matches model
shell: bash
run: |
if ! dotnet ef migrations has-pending-model-changes \
--project src/Cvoya.Spring.Dapr \
--startup-project src/Cvoya.Spring.Dapr \
--no-build; then
echo "::error file=src/Cvoya.Spring.Dapr/Data/Migrations/SpringDbContextModelSnapshot.cs::EF Core model has drifted from the snapshot. Run 'dotnet ef migrations add <Name> --project src/Cvoya.Spring.Dapr --startup-project src/Cvoya.Spring.Dapr' locally and commit the new migration (or, if intentional, regenerate the snapshot)."
exit 1
fi
host-script-idempotence:
name: Host script idempotence
runs-on: ubuntu-latest
needs: changes
# Skipped in the merge queue: the host script is a static-input
# check (drives spring-voyage-host.sh through start/stop/restart
# against the PR's own sources). A pass on the pull_request run
# stays valid through the queue. Path-filtered to the dispatcher
# surface so unrelated PRs don't pay the build cost.
if: >
github.event_name != 'merge_group' &&
(needs.changes.outputs.dispatcher_host == 'true' || needs.changes.outputs.ci_workflow == 'true')
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
# ubuntu-latest runners ship podman preinstalled; assert it
# so a runner-image regression is loud rather than mistaken
# for a host-script bug.
- name: Verify container runtime is on PATH
shell: bash
run: |
if ! command -v podman >/dev/null 2>&1 && ! command -v docker >/dev/null 2>&1; then
echo "::error::neither podman nor docker is on PATH on this runner image"
exit 1
fi
command -v podman && podman --version || true
command -v docker && docker --version || true
- name: Run idempotence test driver
shell: bash
run: bash eng/deploy/scripts/test-spring-voyage-host.sh
- name: Verify deploy.sh forces dispatcher rebuild
shell: bash
run: bash eng/deploy/scripts/test-deploy-dispatcher-rebuild.sh
- name: Verify deploy.sh up preflight guards
shell: bash
run: bash eng/deploy/scripts/test-deploy-preflight.sh
dispatcher-smoke:
name: Dispatcher smoke (Tier 1)
runs-on: ubuntu-latest
needs: changes
# Skipped in the merge queue: same reasoning as host-script-idempotence
# — the smoke driver runs against the PR's own dispatcher sources and
# a Tier-1 alpine echo container, so a pass stays valid through the
# queue. Path-filtered to the dispatcher surface for the same cost
# reason. Tier-2 (full deploy.sh round-trip) lives in
# dispatcher-smoke-full.yml and is workflow_dispatch-only.
if: >
github.event_name != 'merge_group' &&
(needs.changes.outputs.dispatcher_host == 'true' || needs.changes.outputs.ci_workflow == 'true')
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
# ubuntu-latest runners do not ship podman preinstalled, so install
# it from the distro apt repo before the smoke driver runs. The
# dispatcher's PodmanRuntime always invokes `podman` literally, so
# the smoke test cannot run without it. Installing on the runner
# (rather than skipping when absent — the original c0a932bc
# workaround for #1697) ensures the job actually validates the
# PodmanRuntime / dispatcher dispatch path on every PR; a vacuous
# pass would let a broken dispatcher merge undetected.
- name: Install podman
shell: bash
run: |
set -euo pipefail
sudo apt-get update -qq
sudo apt-get install -y podman
podman --version
- name: Verify podman is on PATH
shell: bash
run: |
if ! command -v podman >/dev/null 2>&1; then
echo "::error::podman is not on PATH; the dispatcher's PodmanRuntime always invokes 'podman' literally so the smoke test cannot run."
exit 1
fi
podman --version
- name: Run dispatcher smoke driver
shell: bash
run: bash eng/deploy/scripts/dispatcher-smoke.sh
audit-no-container-cli:
name: Audit (no worker-side podman/docker)
runs-on: ubuntu-latest
needs: changes
# Stage 2 of #522 / #1063: the worker container holds no
# podman/docker binding any more — every container operation routes
# through the host-process spring-dispatcher. This job greps src/
# for the worker-side CLI patterns we just removed so a future PR
# can't silently re-introduce one. Static-input check, so it stays
# valid through the merge queue; we only path-filter on dotnet/
# workflows changes since the audit is over .cs sources.
if: >
github.event_name != 'merge_group' &&
(needs.changes.outputs.dotnet == 'true' || needs.changes.outputs.ci_workflow == 'true')
timeout-minutes: 5
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Run worker-side container CLI audit
shell: bash
run: bash eng/ci/audit-no-container-cli.sh
agent-images-smoke:
name: Agent images build + smoke
runs-on: ubuntu-latest
needs: changes
# PR 3b of #1087 (#1096): build the three tool-bearing agent images
# (`agent-base`, `agent-claude-code`, `spring-voyage-agent`) the dispatcher
# launches today and smoke-test that each one exposes
# /.well-known/agent.json with the BYOI conformance shape
# (`docs/architecture/agent-runtime.md` § 7).
#
# Static-input check (it bakes the PR's own Dockerfiles), so it is
# safe to skip in the merge queue — a pass on the pull_request run
# stays valid through the queue. Path-filtered to the eng/build +
# python-agent surface so unrelated PRs don't pay the build cost.
if: >
github.event_name != 'merge_group' &&
(needs.changes.outputs.agent_images == 'true' || needs.changes.outputs.ci_workflow == 'true')
timeout-minutes: 25
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Verify docker is on PATH
shell: bash
run: |
if ! command -v docker >/dev/null 2>&1; then
echo "::error::docker is not on PATH; ubuntu-latest images normally ship docker pre-installed"
exit 1
fi
docker version
- name: Build agent images (offline-safe — agent-base from local sources)
shell: bash
# `--skip-agent-base=0` (the default) builds ghcr.io/cvoya-com/spring-voyage-agent-base:dev
# locally first so the claude-code FROM resolves without a GHCR pull.
# That way a PR can be tested before the corresponding agent-base
# tag is published.
run: bash eng/build/build-agent-images.sh --tag dev
- name: Smoke each image (curl /.well-known/agent.json)
shell: bash
env:
SMOKE_IMAGE_TAG: dev
run: bash tests/smoke/smoke-agent-images.sh
# PR 6 of #1087 (#1099): the unified-dispatch end-to-end smoke fires
# an A2A `message/send` against the agent-base bridge (path 1) and
# asserts a real response (`status.state == "completed"`, artifact
# echoes the prompt). Path 3 (spring-voyage-agent) is gated behind SMOKE_DAPR=1
# pending #1110.
#
# `--path all` (added in #1120) covers BYOI conformance path 1
# (the agent-base bridge baked into the claude-code image) AND
# path 2. The path-2 fixture exercises the historical npm-install
# shape (`npm i -g` from a `npm pack` tarball of the in-tree
# sidecar source). The @cvoya/spring-voyage-agent-sidecar npm
# package is no longer published with releases as of 2026-05-13,
# but the in-tree fixture builds its tarball locally so the
# install path stays exercised in CI. The supported path-2 install
# method going forward is the SEA binary attached to each GitHub
# Release; see tests/fixtures/byoi-path2/Dockerfile and the BYOI
# guide for the canonical recipe.
- name: End-to-end smoke (PR 6 of #1087 acceptance, paths 1 + 2)
shell: bash
env:
SMOKE_IMAGE_TAG: dev
run: bash tests/smoke/smoke-1087.sh --path all
docker-platform-web:
name: Build platform Dockerfile (web-build stage)
runs-on: ubuntu-latest
needs: changes
# #2783: build the platform image's `web-build` target on every PR
# that touches the Dockerfile or any input it consumes for the web
# layer. Catches missing connector COPYs (e.g. the WebSearch gap
# fixed by #2784) on the PR that introduces them rather than at
# release time. The runtime stage isn't built here — release.yml's
# publish-platform-image job covers the full multi-arch build —
# so this job stays bounded (~3 min) and skips the .NET hosts.
#
# Skipped in the merge queue: static-input check of the PR's own
# Dockerfile + workspace sources. A pass on the pull_request run
# stays valid through the queue.
if: >
github.event_name != 'merge_group' &&
(needs.changes.outputs.web == 'true' ||
needs.changes.outputs.platform_dockerfile == 'true' ||
needs.changes.outputs.ci_workflow == 'true')
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
- name: Build platform Dockerfile (web-build stage)
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: .
file: eng/build/Dockerfile
target: web-build
push: false
load: false
cache-from: type=gha,scope=docker-platform-web
cache-to: type=gha,mode=max,scope=docker-platform-web
required-checks:
name: Required checks
runs-on: ubuntu-latest
needs:
- changes
- workflow-yaml
- dotnet
- agent-definitions-lint
- runtime-catalog-lint
- sample-build
- connector-web-lint
- web-lint
- web-knip
- web-typecheck
- web-build
- web-test
- web-api-docs
- web-e2e
- web-lighthouse
- openapi-drift
- efcore-model-drift
- python-lint
- python-test
- host-script-idempotence
- dispatcher-smoke
- audit-no-container-cli
- agent-images-smoke
- docs-evergreen-framing
- package-validate
- docker-platform-web
# Always runs - branch protection depends on a single "Required checks"
# context, so this job must report a status even when upstream jobs were
# path-filtered out.
if: ${{ always() }}
timeout-minutes: 5
steps:
- name: Verify all required jobs passed (or were skipped by path filter)
shell: bash
env:
DOTNET_RESULT: ${{ needs.dotnet.result }}
LINT_RESULT: ${{ needs.agent-definitions-lint.result }}
RUNTIME_CATALOG_RESULT: ${{ needs.runtime-catalog-lint.result }}
SAMPLE_BUILD_RESULT: ${{ needs.sample-build.result }}
CONNECTOR_WEB_RESULT: ${{ needs.connector-web-lint.result }}
WEB_LINT_RESULT: ${{ needs.web-lint.result }}
WEB_KNIP_RESULT: ${{ needs.web-knip.result }}
WEB_TYPECHECK_RESULT: ${{ needs.web-typecheck.result }}
WEB_BUILD_RESULT: ${{ needs.web-build.result }}
WEB_TEST_RESULT: ${{ needs.web-test.result }}
WEB_API_DOCS_RESULT: ${{ needs.web-api-docs.result }}
WEB_E2E_RESULT: ${{ needs.web-e2e.result }}
WEB_LIGHTHOUSE_RESULT: ${{ needs.web-lighthouse.result }}
OPENAPI_RESULT: ${{ needs.openapi-drift.result }}
EFCORE_DRIFT_RESULT: ${{ needs.efcore-model-drift.result }}
PYTHON_LINT_RESULT: ${{ needs.python-lint.result }}
PYTHON_TEST_RESULT: ${{ needs.python-test.result }}
HOST_IDEMPOTENCE_RESULT: ${{ needs.host-script-idempotence.result }}
DISPATCHER_SMOKE_RESULT: ${{ needs.dispatcher-smoke.result }}
AUDIT_RESULT: ${{ needs.audit-no-container-cli.result }}
AGENT_IMAGES_RESULT: ${{ needs.agent-images-smoke.result }}
DOCS_FRAMING_RESULT: ${{ needs.docs-evergreen-framing.result }}
PACKAGE_VALIDATE_RESULT: ${{ needs.package-validate.result }}
DOCKER_PLATFORM_WEB_RESULT: ${{ needs.docker-platform-web.result }}
WORKFLOW_YAML_RESULT: ${{ needs.workflow-yaml.result }}
CHANGES_RESULT: ${{ needs.changes.result }}
run: |
echo "changes=$CHANGES_RESULT"
echo "workflow-yaml=$WORKFLOW_YAML_RESULT"
echo "dotnet=$DOTNET_RESULT"
echo "agent-definitions-lint=$LINT_RESULT"
echo "runtime-catalog-lint=$RUNTIME_CATALOG_RESULT"
echo "sample-build=$SAMPLE_BUILD_RESULT"
echo "connector-web-lint=$CONNECTOR_WEB_RESULT"
echo "web-lint=$WEB_LINT_RESULT"
echo "web-knip=$WEB_KNIP_RESULT"
echo "web-typecheck=$WEB_TYPECHECK_RESULT"
echo "web-build=$WEB_BUILD_RESULT"
echo "web-test=$WEB_TEST_RESULT"
echo "web-api-docs=$WEB_API_DOCS_RESULT"
echo "web-e2e=$WEB_E2E_RESULT"
echo "web-lighthouse=$WEB_LIGHTHOUSE_RESULT"
echo "openapi-drift=$OPENAPI_RESULT"
echo "efcore-model-drift=$EFCORE_DRIFT_RESULT"
echo "python-lint=$PYTHON_LINT_RESULT"
echo "python-test=$PYTHON_TEST_RESULT"
echo "host-script-idempotence=$HOST_IDEMPOTENCE_RESULT"
echo "dispatcher-smoke=$DISPATCHER_SMOKE_RESULT"
echo "audit-no-container-cli=$AUDIT_RESULT"
echo "agent-images-smoke=$AGENT_IMAGES_RESULT"
echo "docs-evergreen-framing=$DOCS_FRAMING_RESULT"
echo "package-validate=$PACKAGE_VALIDATE_RESULT"
echo "docker-platform-web=$DOCKER_PLATFORM_WEB_RESULT"
# The 'changes' gate itself must have run cleanly - if path detection
# failed we can't trust the skip decisions downstream.
if [[ "$CHANGES_RESULT" != "success" ]]; then
echo "::error::changes detection job failed"
exit 1
fi
# Each real job must be either success (ran and passed) or skipped
# (path filter decided it wasn't needed). Anything else - failure,
# cancellation, timeout - fails the PR.
ok() { [[ "$1" == "success" || "$1" == "skipped" ]]; }
ok "$DOTNET_RESULT" || { echo "::error::build, format, or tests failed"; exit 1; }
ok "$LINT_RESULT" || { echo "::error::agent-definitions-lint failed"; exit 1; }
ok "$RUNTIME_CATALOG_RESULT" || { echo "::error::runtime-catalog-lint failed"; exit 1; }
ok "$SAMPLE_BUILD_RESULT" || { echo "::error::sample-build failed"; exit 1; }
ok "$CONNECTOR_WEB_RESULT" || { echo "::error::connector-web-lint failed"; exit 1; }
ok "$WEB_LINT_RESULT" || { echo "::error::web-lint failed"; exit 1; }
ok "$WEB_KNIP_RESULT" || { echo "::error::web-knip failed"; exit 1; }
ok "$WEB_TYPECHECK_RESULT" || { echo "::error::web-typecheck failed"; exit 1; }
ok "$WEB_BUILD_RESULT" || { echo "::error::web-build failed"; exit 1; }
ok "$WEB_TEST_RESULT" || { echo "::error::web-test failed"; exit 1; }
ok "$WEB_API_DOCS_RESULT" || { echo "::error::web-api-docs failed"; exit 1; }
ok "$WEB_E2E_RESULT" || { echo "::error::web-e2e failed"; exit 1; }
ok "$WEB_LIGHTHOUSE_RESULT" || { echo "::error::web-lighthouse failed"; exit 1; }
ok "$OPENAPI_RESULT" || { echo "::error::openapi-drift failed"; exit 1; }
ok "$EFCORE_DRIFT_RESULT" || { echo "::error::efcore-model-drift failed"; exit 1; }
ok "$PYTHON_LINT_RESULT" || { echo "::error::python-lint failed"; exit 1; }
ok "$PYTHON_TEST_RESULT" || { echo "::error::python-test failed"; exit 1; }
ok "$HOST_IDEMPOTENCE_RESULT" || { echo "::error::host-script-idempotence failed"; exit 1; }
ok "$DISPATCHER_SMOKE_RESULT" || { echo "::error::dispatcher-smoke failed"; exit 1; }
ok "$AUDIT_RESULT" || { echo "::error::audit-no-container-cli failed"; exit 1; }
ok "$AGENT_IMAGES_RESULT" || { echo "::error::agent-images-smoke failed"; exit 1; }
ok "$DOCS_FRAMING_RESULT" || { echo "::error::docs-evergreen-framing failed"; exit 1; }
ok "$PACKAGE_VALIDATE_RESULT" || { echo "::error::package-validate failed"; exit 1; }
ok "$DOCKER_PLATFORM_WEB_RESULT" || { echo "::error::docker-platform-web failed"; exit 1; }
ok "$WORKFLOW_YAML_RESULT" || { echo "::error::workflow-yaml failed"; exit 1; }