Bump JsonSchema.Net from 9.2.2 to 9.3.0 #4508
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: 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@6599ee8b7a49aef6a770f63d261d214911a7ce02 # v0.6.0 | |
| 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@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 | |
| 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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| 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@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 | |
| 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@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 | |
| 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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| 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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 | |
| 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@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 | |
| 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; } |