Repository navigation
Merge remote-tracking branch 'upstream/main' #17
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: | |
| push: | |
| branches: | |
| - main | |
| - 'release/**' | |
| pull_request: | |
| types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] | |
| merge_group: | |
| types: [checks_requested] | |
| permissions: | |
| contents: read | |
| # PR runs coalesce by PR number. Merge-group candidates plus main/release pushes use | |
| # the exact candidate SHA as the group key so each integration candidate keeps its evidence. | |
| concurrency: | |
| group: ci-${{ github.event.pull_request.number || github.sha }} | |
| cancel-in-progress: true | |
| env: | |
| CARGO_TERM_COLOR: always | |
| jobs: | |
| changes: | |
| # Classify only trusted event metadata plus base...head Git data. For PRs and | |
| # merge-group candidates the classifier is loaded from the trusted base commit, | |
| # so candidate changes cannot weaken their own native gates. If the trusted base | |
| # predates the classifier, fail safe to | |
| # the full daily native matrix instead of executing classifier code from the PR head. | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| outputs: | |
| needs_windows: ${{ steps.classify.outputs.needs_windows }} | |
| needs_windows_core: ${{ steps.classify.outputs.needs_windows_core }} | |
| needs_windows_runner: ${{ steps.classify.outputs.needs_windows_runner }} | |
| needs_windows_package: ${{ steps.classify.outputs.needs_windows_package }} | |
| needs_windows_desktop: ${{ steps.classify.outputs.needs_windows_desktop }} | |
| needs_macos: ${{ steps.classify.outputs.needs_macos }} | |
| needs_macos_desktop: ${{ steps.classify.outputs.needs_macos_desktop }} | |
| # Older trusted PR bases do not expose the newer frontend/container/Plugin SDK outputs. | |
| # A historical full-native result is the safe rollout fallback for those bases. | |
| needs_frontend: ${{ steps.classify.outputs.needs_frontend == 'true' || (steps.classify.outputs.needs_frontend == '' && steps.classify.outputs.needs_full_native == 'true') }} | |
| needs_desktop_frontend: ${{ steps.classify.outputs.needs_desktop_frontend == 'true' || (steps.classify.outputs.needs_desktop_frontend == '' && steps.classify.outputs.needs_full_native == 'true') }} | |
| needs_plugin_sdk: ${{ steps.classify.outputs.needs_plugin_sdk == 'true' || (steps.classify.outputs.needs_plugin_sdk == '' && steps.classify.outputs.needs_full_native == 'true') }} | |
| needs_docker: ${{ steps.classify.outputs.needs_docker == 'true' || (steps.classify.outputs.needs_docker == '' && steps.classify.outputs.needs_full_native == 'true') }} | |
| needs_desktop_package: ${{ steps.classify.outputs.needs_desktop_package }} | |
| needs_full_native: ${{ steps.classify.outputs.needs_full_native }} | |
| categories: ${{ steps.classify.outputs.categories }} | |
| reason: ${{ steps.classify.outputs.reason }} | |
| steps: | |
| - name: Checkout trusted classifier source | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'merge_group' && github.event.merge_group.base_sha || github.sha }} | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Classify native CI risk | |
| id: classify | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| EXTERNAL_CONTRIBUTOR: ${{ github.event_name == 'pull_request' && github.event.pull_request.user.login != github.repository_owner }} | |
| RUN_CI: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-ci') }} | |
| BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before || '' }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha || github.sha }} | |
| PR_NUMBER: ${{ github.event.pull_request.number || '' }} | |
| MERGE_GROUP_HEAD_REF: ${{ github.event.merge_group.head_ref || '' }} | |
| run: | | |
| set -euo pipefail | |
| classifier="scripts/ci_path_risk.py" | |
| if [ ! -f "$classifier" ]; then | |
| { | |
| for key in \ | |
| needs_windows \ | |
| needs_windows_core \ | |
| needs_windows_runner \ | |
| needs_windows_package \ | |
| needs_windows_desktop \ | |
| needs_macos \ | |
| needs_macos_desktop \ | |
| needs_frontend \ | |
| needs_desktop_frontend \ | |
| needs_plugin_sdk \ | |
| needs_docker \ | |
| needs_desktop_package \ | |
| needs_full_native; do | |
| echo "$key=true" | |
| done | |
| echo "categories=bootstrap-full-native" | |
| echo "reason=full-native:bootstrap-base-missing" | |
| } >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [ "$EVENT_NAME" = pull_request ] && \ | |
| [ "$EXTERNAL_CONTRIBUTOR" != true ] && \ | |
| [ "$RUN_CI" != true ] && \ | |
| ! git cat-file -e "$HEAD_SHA^{commit}" 2>/dev/null; then | |
| git fetch --no-tags origin "refs/pull/$PR_NUMBER/head" | |
| fetched_head="$(git rev-parse FETCH_HEAD)" | |
| if [ "$fetched_head" != "$HEAD_SHA" ]; then | |
| echo "fetched PR head does not match event head SHA" >&2 | |
| exit 1 | |
| fi | |
| fi | |
| if [ "$EVENT_NAME" = merge_group ] && \ | |
| ! git cat-file -e "$HEAD_SHA^{commit}" 2>/dev/null; then | |
| if [ -z "$MERGE_GROUP_HEAD_REF" ]; then | |
| echo "merge-group head ref is missing" >&2 | |
| exit 1 | |
| fi | |
| git fetch --no-tags origin "$MERGE_GROUP_HEAD_REF" | |
| fetched_head="$(git rev-parse FETCH_HEAD)" | |
| if [ "$fetched_head" != "$HEAD_SHA" ]; then | |
| echo "fetched merge-group head does not match event head SHA" >&2 | |
| exit 1 | |
| fi | |
| fi | |
| python3 "$classifier" \ | |
| --event-name "$EVENT_NAME" \ | |
| --external-contributor "$EXTERNAL_CONTRIBUTOR" \ | |
| --run-ci "$RUN_CI" \ | |
| --base "$BASE_SHA" \ | |
| --head "$HEAD_SHA" \ | |
| --github-output "$GITHUB_OUTPUT" | |
| contract: | |
| # Mandatory contract lane for every workflow invocation. It waits only for the | |
| # cheap path classifier so unrelated frontend dependency/install work can be | |
| # skipped; Rust/schema/workspace contracts remain unconditional. | |
| needs: changes | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable | |
| with: | |
| components: rustfmt | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 | |
| with: | |
| shared-key: linux-ci | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| if: needs.changes.outputs.needs_frontend == 'true' || needs.changes.outputs.needs_desktop_frontend == 'true' || needs.changes.outputs.needs_plugin_sdk == 'true' | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: | | |
| frontend/package-lock.json | |
| apps/desktop/package-lock.json | |
| npm/plugin-sdk/package-lock.json | |
| plugins/safe-delete/package-lock.json | |
| plugins/repo-info/package-lock.json | |
| plugins/repo-context/package-lock.json | |
| plugins/campus-application/package-lock.json | |
| plugins/agent-browser/package-lock.json | |
| - name: Install frontend dependencies | |
| if: needs.changes.outputs.needs_frontend == 'true' || needs.changes.outputs.needs_desktop_frontend == 'true' | |
| run: npm ci --prefix frontend | |
| - name: Check frontend contracts | |
| if: needs.changes.outputs.needs_frontend == 'true' | |
| run: | | |
| npm --prefix frontend run typecheck | |
| npm --prefix frontend test | |
| npm --prefix frontend run check:dist | |
| - name: Install Desktop frontend dependencies | |
| if: needs.changes.outputs.needs_desktop_frontend == 'true' | |
| run: npm ci --prefix apps/desktop | |
| - name: Check Desktop frontend contracts | |
| if: needs.changes.outputs.needs_desktop_frontend == 'true' | |
| run: | | |
| npm --prefix apps/desktop run typecheck | |
| npm --prefix apps/desktop test | |
| npm --prefix apps/desktop run build | |
| - name: Install Plugin SDK dependencies | |
| if: needs.changes.outputs.needs_plugin_sdk == 'true' | |
| run: | | |
| npm ci --prefix npm/plugin-sdk | |
| npm ci --prefix plugins/safe-delete | |
| npm ci --prefix plugins/repo-info | |
| npm ci --prefix plugins/repo-context | |
| npm ci --prefix plugins/campus-application | |
| npm ci --prefix plugins/agent-browser | |
| - name: Check Plugin SDK contracts and first-party dogfood | |
| if: needs.changes.outputs.needs_plugin_sdk == 'true' | |
| run: | | |
| npm --prefix npm/plugin-sdk run typecheck | |
| npm --prefix npm/plugin-sdk run build | |
| npm --prefix npm/plugin-sdk test | |
| npm --prefix npm/plugin-sdk run pack:dry-run | |
| npm --prefix plugins/safe-delete run typecheck | |
| npm --prefix plugins/safe-delete test | |
| npm --prefix plugins/repo-info run typecheck | |
| npm --prefix plugins/repo-info test | |
| npm --prefix plugins/repo-context run typecheck | |
| npm --prefix plugins/repo-context test | |
| npm --prefix plugins/campus-application run typecheck | |
| npm --prefix plugins/campus-application test | |
| npm --prefix plugins/agent-browser run typecheck | |
| npm --prefix plugins/agent-browser test | |
| - name: Verify workspace boundaries | |
| run: | | |
| bash scripts/workspace_boundary_check.sh --self-test | |
| bash scripts/workspace_boundary_check.sh | |
| - name: Check build identity and incremental boundary | |
| run: PYTHONDONTWRITEBYTECODE=1 python3 -m unittest scripts.tests.test_build_identity | |
| - name: Check formatting | |
| run: cargo fmt --all -- --check | |
| - name: Ensure test inventory dependency | |
| run: | | |
| if ! command -v rg >/dev/null 2>&1; then | |
| sudo apt-get update | |
| sudo apt-get install --no-install-recommends -y ripgrep | |
| fi | |
| - name: Check test inventory contract | |
| run: | | |
| bash scripts/test_inventory.sh --self-test | |
| PYTHONDONTWRITEBYTECODE=1 python3 -m unittest scripts.tests.test_ci_rust_test | |
| bash scripts/test_inventory.sh | |
| - name: Check macOS signing setup and release evidence contracts | |
| # Fake signing commands and synthetic artifacts only; no Apple credentials. | |
| run: PYTHONDONTWRITEBYTECODE=1 python3 -m unittest scripts.tests.test_macos_signing_setup scripts.tests.test_macos_release_evidence | |
| - name: Check schema and metadata contracts | |
| run: | | |
| python3 scripts/tests/test_runner_observability_contract.py | |
| cargo test --locked -p webcodex --lib mcp_tools_list | |
| cargo test --locked -p webcodex --lib tool_definition_surface_counts_stay_canonical | |
| cargo test --locked -p webcodex --lib external_provider_discovery_cannot_change_public_tool_surface | |
| mcp-conformance: | |
| # Dated MCP wire evidence is independent from ordinary unit/integration tests. | |
| # The script pins the external referee and rejects missing or silently skipped | |
| # coverage; known failures require exact check-level classification. | |
| needs: contract | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 | |
| with: | |
| shared-key: linux-ci | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: 22 | |
| - name: Check conformance report gate | |
| run: python3 scripts/tests/test_mcp_conformance_report.py | |
| - name: Run pinned MCP dated-revision baseline | |
| run: bash scripts/mcp_conformance.sh | |
| - name: Upload MCP conformance evidence | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: mcp-conformance-${{ github.sha }} | |
| path: target/mcp-conformance/reports | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| test-linux-rust: | |
| # Complete Linux Rust coverage is mandatory before merge, including owner PRs. | |
| # Start it immediately in parallel with contract/frontend work; the stable | |
| # `test` aggregate still requires both surfaces to succeed. | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - shard: server | |
| packages: "-p webcodex" | |
| - shard: runner | |
| packages: "-p webcodex-runner -p webcodex-lsp --features runner-real-process-tests" | |
| - shard: workspace-crates | |
| packages: >- | |
| --workspace | |
| --exclude webcodex | |
| --exclude webcodex-runner | |
| --exclude webcodex-lsp | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 | |
| with: | |
| shared-key: linux-ci | |
| - name: Run ${{ matrix.shard }} Rust tests | |
| env: | |
| CI_RUST_SHARD: ${{ matrix.shard }} | |
| run: python3 scripts/ci_rust_test.py --report target/ci-test-reports/${{ matrix.shard }}.json -- cargo test --locked ${{ matrix.packages }} | |
| - name: Upload bounded Rust test evidence | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: rust-tests-${{ matrix.shard }}-${{ github.sha }}-${{ github.run_attempt }} | |
| path: target/ci-test-reports/*.json | |
| if-no-files-found: warn | |
| retention-days: 7 | |
| test-linux-tooling: | |
| # Release/tooling checks are mandatory on every PR, but do not need contract | |
| # output. Start them in parallel; the stable `test` aggregate joins the results. | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 | |
| with: | |
| shared-key: linux-ci | |
| # The retained npm release-smoke tooling below invokes node/npm directly. | |
| # Frontend dependency installation and frontend-specific npm caching remain | |
| # contract-lane responsibilities and are intentionally not repeated here. | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: 22 | |
| - name: Check MCP App DOM and message-order contracts | |
| run: node --test src/mcp_tests/*.test.mjs | |
| - name: Ensure Python tooling test dependencies | |
| run: | | |
| if ! command -v rg >/dev/null 2>&1; then | |
| sudo apt-get update | |
| sudo apt-get install --no-install-recommends -y ripgrep | |
| fi | |
| - name: Test optional Codex observation adapter | |
| run: python3 -m unittest discover -s integrations/codex -p 'test_*.py' -v | |
| - name: Test configured model API ACP adapter | |
| run: python3 -m unittest discover -s integrations/model_gateway/tests -p 'test_*.py' -v | |
| - name: Check complete workspace production/test targets | |
| if: github.event_name == 'pull_request' && (github.event.pull_request.user.login != github.repository_owner || contains(github.event.pull_request.labels.*.name, 'run-ci')) | |
| run: cargo check --locked --workspace --all-targets | |
| - name: Test deterministic release contract and tooling | |
| run: bash scripts/release_check.sh --static-only | |
| test: | |
| needs: [contract, changes, test-linux-rust, test-linux-tooling] | |
| # Preserve the historical aggregate status check and always evaluate upstream | |
| # results so a failed or unexpectedly skipped Linux lane cannot disappear. | |
| if: always() | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Require successful Linux lanes | |
| env: | |
| CONTRACT_RESULT: ${{ needs.contract.result }} | |
| CHANGES_RESULT: ${{ needs.changes.result }} | |
| RUST_RESULT: ${{ needs.test-linux-rust.result }} | |
| TOOLING_RESULT: ${{ needs.test-linux-tooling.result }} | |
| run: | | |
| set -euo pipefail | |
| echo "contract=$CONTRACT_RESULT changes=$CHANGES_RESULT rust=$RUST_RESULT tooling=$TOOLING_RESULT" | |
| if [ "$CONTRACT_RESULT" != success ] || \ | |
| [ "$CHANGES_RESULT" != success ] || \ | |
| [ "$RUST_RESULT" != success ] || \ | |
| [ "$TOOLING_RESULT" != success ]; then | |
| echo "required Linux CI lane did not succeed" >&2 | |
| exit 1 | |
| fi | |
| test-docker-server: | |
| needs: changes | |
| if: needs.changes.outputs.needs_docker == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 35 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 | |
| - name: Resolve exact Server image build identity | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| version="$(node -p "require('./npm/webcodex/package.json').version")" | |
| { | |
| echo "WEBCODEX_CI_VERSION=$version" | |
| echo "WEBCODEX_CI_SOURCE_SHORT=$(git rev-parse --short=12 HEAD)" | |
| echo "WEBCODEX_CI_BUILT_AT=$(git show -s --format=%ct HEAD)" | |
| echo "WEBCODEX_CI_IMAGE=webcodex-server-ci:${GITHUB_SHA}" | |
| } >> "$GITHUB_ENV" | |
| git diff --check | |
| test -z "$(git status --porcelain --untracked-files=all)" | |
| - name: Build amd64 Server image | |
| uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 | |
| with: | |
| context: . | |
| file: ./Dockerfile | |
| platforms: linux/amd64 | |
| load: true | |
| push: false | |
| tags: ${{ env.WEBCODEX_CI_IMAGE }} | |
| build-args: | | |
| WEBCODEX_GIT_COMMIT=${{ env.WEBCODEX_CI_SOURCE_SHORT }} | |
| WEBCODEX_GIT_DIRTY=false | |
| WEBCODEX_BUILT_AT=${{ env.WEBCODEX_CI_BUILT_AT }} | |
| cache-from: type=gha,scope=server-image-ci-amd64 | |
| # GHA cache export is a performance optimization, not image evidence. | |
| # BuildKit can transiently return layer-blob not_found after the image | |
| # itself has built; keep build/load/smoke authoritative in that case. | |
| cache-to: type=gha,mode=max,scope=server-image-ci-amd64,ignore-error=true | |
| provenance: false | |
| sbom: false | |
| - name: Smoke amd64 Server image | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| image="$WEBCODEX_CI_IMAGE" | |
| test "$(docker image inspect --format '{{.Architecture}}' "$image")" = amd64 | |
| test "$(docker image inspect --format '{{.Config.User}}' "$image")" = "webcodex:webcodex" | |
| docker run --rm --entrypoint sh "$image" -ec ' | |
| test "$(id -u)" = 10001 | |
| test "$(id -g)" = 10001 | |
| test ! -e /usr/local/bin/webcodex-runner | |
| ' | |
| expected_server="webcodex-server $WEBCODEX_CI_VERSION (commit $WEBCODEX_CI_SOURCE_SHORT, dirty=false, built_at=$WEBCODEX_CI_BUILT_AT)" | |
| expected_cli="webcodex $WEBCODEX_CI_VERSION (commit $WEBCODEX_CI_SOURCE_SHORT, dirty=false, built_at=$WEBCODEX_CI_BUILT_AT)" | |
| test "$(docker run --rm "$image" --version)" = "$expected_server" | |
| test "$(docker run --rm --entrypoint /usr/local/bin/webcodex "$image" --version)" = "$expected_cli" | |
| name="webcodex-server-ci-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" | |
| cleanup() { docker rm -f "$name" >/dev/null 2>&1 || true; } | |
| trap cleanup EXIT | |
| docker run -d --name "$name" \ | |
| -e WEBCODEX_TOKEN=0000000000000000000000000000000000000000000000000000000000000000 \ | |
| -e WEBCODEX_PUBLIC_URL=https://webcodex.invalid \ | |
| "$image" >/dev/null | |
| for _ in $(seq 1 40); do | |
| health="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}missing{{end}}' "$name")" | |
| if [ "$health" = healthy ]; then break; fi | |
| if [ "$health" = unhealthy ] || [ "$health" = missing ]; then | |
| docker logs --tail 80 "$name" >&2 || true | |
| exit 1 | |
| fi | |
| sleep 2 | |
| done | |
| test "$(docker inspect --format '{{.State.Health.Status}}' "$name")" = healthy | |
| test-macos-core: | |
| needs: changes | |
| if: needs.changes.outputs.needs_macos == 'true' | |
| # Daily macOS coverage uses Apple Silicon. Intel is intentionally reserved for | |
| # explicit/release-time extended-native validation. | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: darwin-arm64 | |
| runner: macos-15 | |
| host_arch: arm64 | |
| rust_host: aarch64-apple-darwin | |
| runs-on: ${{ matrix.runner }} | |
| timeout-minutes: 75 | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 | |
| with: | |
| shared-key: macos-core-ci-${{ matrix.platform }} | |
| - name: Verify native macOS host | |
| env: | |
| EXPECTED_HOST_ARCH: ${{ matrix.host_arch }} | |
| EXPECTED_RUST_HOST: ${{ matrix.rust_host }} | |
| run: | | |
| test "$(uname -m)" = "$EXPECTED_HOST_ARCH" | |
| rust_host="$(rustc -vV | sed -n 's/^host: //p')" | |
| test "$rust_host" = "$EXPECTED_RUST_HOST" | |
| - name: Check macOS workspace production surfaces | |
| run: cargo check --locked --workspace | |
| - name: Run native macOS Runner and Computer tests | |
| run: cargo test --locked -p webcodex-runner -p webcodex-lsp -p webcodex-computer --features runner-real-process-tests | |
| test-macos-desktop: | |
| needs: changes | |
| if: needs.changes.outputs.needs_macos_desktop == 'true' | |
| # Keep the common Apple-Silicon Desktop package validation path-aware. The | |
| # corresponding Intel DMG build/smoke lives in extended-native CI. | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: darwin-arm64 | |
| runner: macos-15 | |
| host_arch: arm64 | |
| rust_host: aarch64-apple-darwin | |
| runs-on: ${{ matrix.runner }} | |
| timeout-minutes: 75 | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| fetch-depth: 0 | |
| - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 | |
| with: | |
| shared-key: macos-desktop-ci-${{ matrix.platform }} | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: | | |
| frontend/package-lock.json | |
| apps/desktop/package-lock.json | |
| - name: Verify native macOS host | |
| env: | |
| EXPECTED_HOST_ARCH: ${{ matrix.host_arch }} | |
| EXPECTED_RUST_HOST: ${{ matrix.rust_host }} | |
| run: | | |
| test "$(uname -m)" = "$EXPECTED_HOST_ARCH" | |
| rust_host="$(rustc -vV | sed -n 's/^host: //p')" | |
| test "$rust_host" = "$EXPECTED_RUST_HOST" | |
| - name: Install shared frontend dependencies | |
| run: npm ci --prefix frontend | |
| - name: Install Desktop dependencies | |
| run: npm ci --prefix apps/desktop | |
| - name: Check Desktop frontend | |
| run: | | |
| npm --prefix apps/desktop run typecheck | |
| npm --prefix apps/desktop test | |
| npm --prefix apps/desktop run build | |
| - name: Check Desktop Rust | |
| run: | | |
| cargo check --locked --manifest-path apps/desktop/src-tauri/Cargo.toml --all-targets | |
| cargo test --locked --manifest-path apps/desktop/src-tauri/Cargo.toml | |
| - name: Build exact native WebCodex runtime for Desktop | |
| id: desktop_runtime | |
| env: | |
| EXPECTED_FILE_ARCH: ${{ matrix.host_arch }} | |
| run: | | |
| set -euo pipefail | |
| source_sha="$(git rev-parse HEAD)" | |
| short_source="$(git rev-parse --short=12 HEAD)" | |
| version="$(node -p "require('./npm/webcodex/package.json').version")" | |
| built_at="$(git show -s --format=%ct HEAD)" | |
| export WEBCODEX_BUILT_AT="$built_at" | |
| cargo build --locked --profile dogfood -p webcodex -p webcodex-cli -p webcodex-runner | |
| for name in webcodex webcodex-server webcodex-runner; do | |
| binary="target/dogfood/$name" | |
| actual="$($binary --version)" | |
| expected="$name $version (commit $short_source, dirty=false, built_at=$built_at)" | |
| test "$actual" = "$expected" | |
| test "$(/usr/bin/lipo -archs "$binary")" = "$EXPECTED_FILE_ARCH" | |
| done | |
| bash scripts/macos_sign_runner.sh target/dogfood/webcodex-runner - | |
| { | |
| echo "source=$source_sha" | |
| echo "short=$short_source" | |
| echo "version=$version" | |
| echo "built_at=$built_at" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Stage exact runtime for native macOS Desktop | |
| id: desktop_bundle | |
| run: | | |
| set -euo pipefail | |
| stage="$RUNNER_TEMP/webcodex-desktop-${{ matrix.platform }}-bundle" | |
| python3 scripts/prepare_desktop_bundle_macos.py \ | |
| --bin-dir target/dogfood \ | |
| --version "${{ steps.desktop_runtime.outputs.version }}" \ | |
| --source-sha "${{ steps.desktop_runtime.outputs.source }}" \ | |
| --built-at "${{ steps.desktop_runtime.outputs.built_at }}" \ | |
| --platform "${{ matrix.platform }}" \ | |
| --signing-mode adhoc \ | |
| --output-dir "$stage" | |
| { | |
| echo "config=$stage/tauri.bundle.conf.json" | |
| echo "metadata=$stage/desktop-bundle.json" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Build native ad-hoc macOS Desktop DMG | |
| id: desktop_dmg | |
| working-directory: apps/desktop | |
| env: | |
| CI: "true" | |
| APPLE_SIGNING_IDENTITY: "-" | |
| run: | | |
| set -euo pipefail | |
| export CARGO_TARGET_DIR="$RUNNER_TEMP/webcodex-desktop-${{ matrix.platform }}-tauri" | |
| npm exec tauri -- build \ | |
| --bundles dmg \ | |
| --config "${{ steps.desktop_bundle.outputs.config }}" \ | |
| --ci -- --locked | |
| bundle_dir="$CARGO_TARGET_DIR/release/bundle/dmg" | |
| shopt -s nullglob | |
| candidates=("$bundle_dir"/*.dmg) | |
| test "${#candidates[@]}" -eq 1 | |
| dmg="$RUNNER_TEMP/webcodex-desktop-ci-${{ steps.desktop_runtime.outputs.short }}-v${{ steps.desktop_runtime.outputs.version }}-${{ matrix.platform }}.dmg" | |
| cp "${candidates[0]}" "$dmg" | |
| echo "path=$dmg" >> "$GITHUB_OUTPUT" | |
| - name: Smoke native macOS Desktop DMG | |
| run: | | |
| set -euo pipefail | |
| bash scripts/desktop_install_macos_smoke.sh \ | |
| --dmg "${{ steps.desktop_dmg.outputs.path }}" \ | |
| --version "${{ steps.desktop_runtime.outputs.version }}" \ | |
| --source-sha "${{ steps.desktop_runtime.outputs.source }}" \ | |
| --built-at "${{ steps.desktop_runtime.outputs.built_at }}" \ | |
| --platform "${{ matrix.platform }}" \ | |
| --stage-metadata "${{ steps.desktop_bundle.outputs.metadata }}" \ | |
| --signing-mode adhoc | |
| test-macos: | |
| needs: [contract, changes, test-macos-core, test-macos-desktop] | |
| # Stable aggregate context: every configured macOS sublane must either succeed | |
| # when required by policy or be intentionally skipped when not required. | |
| if: always() | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Validate macOS lane policy | |
| env: | |
| CONTRACT_RESULT: ${{ needs.contract.result }} | |
| CHANGES_RESULT: ${{ needs.changes.result }} | |
| NEEDS_CORE: ${{ needs.changes.outputs.needs_macos }} | |
| NEEDS_DESKTOP: ${{ needs.changes.outputs.needs_macos_desktop }} | |
| CORE_RESULT: ${{ needs.test-macos-core.result }} | |
| DESKTOP_RESULT: ${{ needs.test-macos-desktop.result }} | |
| run: | | |
| set -euo pipefail | |
| echo "changes=$CHANGES_RESULT contract=$CONTRACT_RESULT core=$CORE_RESULT desktop=$DESKTOP_RESULT needs_core=$NEEDS_CORE needs_desktop=$NEEDS_DESKTOP" | |
| if [ "$CHANGES_RESULT" != success ] || [ "$CONTRACT_RESULT" != success ]; then | |
| echo "CI policy or contract classification did not succeed" >&2 | |
| exit 1 | |
| fi | |
| require_lane() { | |
| label="$1" | |
| required="$2" | |
| actual="$3" | |
| expected=skipped | |
| if [ "$required" = true ]; then expected=success; fi | |
| if [ "$actual" != "$expected" ]; then | |
| echo "$label macOS lane expected $expected, got $actual" >&2 | |
| exit 1 | |
| fi | |
| } | |
| require_lane core "$NEEDS_CORE" "$CORE_RESULT" | |
| require_lane desktop "$NEEDS_DESKTOP" "$DESKTOP_RESULT" | |
| # Ordinary Runner/Computer fixtures must tolerate default libtest parallelism. | |
| # Timing-sensitive lifecycle/process-tree fixtures remain ignored here and run only | |
| # by explicit local commands. Core/library, Runner, and packaging coverage are independent. | |
| test-windows-core: | |
| needs: changes | |
| if: needs.changes.outputs.needs_windows_core == 'true' | |
| runs-on: windows-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | |
| with: | |
| python-version: '3.12' | |
| - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 | |
| with: | |
| shared-key: windows-core-ci | |
| - name: Configure PowerShell 7 persistent-shell tests | |
| shell: pwsh | |
| run: | | |
| $pwsh = (Get-Command pwsh -ErrorAction Stop).Source | |
| "WEBCODEX_TEST_PWSH=$pwsh" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append | |
| - name: Run Windows persistent-shell tests (PowerShell 5.1 + PowerShell 7) | |
| run: cargo test --locked -p webcodex-persistent-shell | |
| - name: Check Windows workspace production surfaces | |
| run: cargo check --locked --workspace | |
| - name: Run Windows library and CLI tests | |
| run: cargo test --locked -p webcodex-runner-config -p webcodex-process -p webcodex-cli | |
| - name: Run Windows unified upgrade transactions including native v0.4.3 fixture | |
| run: | | |
| cargo test --locked -p webcodex-environment --lib upgrade:: | |
| - name: Test Windows unified installer routing and Desktop build freshness | |
| run: python -m unittest scripts.tests.test_prepare_windows_unified_installer scripts.tests.test_desktop_runtime_manifest scripts.tests.test_desktop_frontend_build | |
| - name: Test optional Codex observation adapter on Windows | |
| run: python -m unittest discover -s integrations/codex -p 'test_*.py' -v | |
| - name: Run Windows search fallback tests | |
| run: | | |
| cargo test --locked -p webcodex --lib search_project_text_grep | |
| if ($LASTEXITCODE -ne 0) { throw "Windows grep fallback tests failed" } | |
| cargo test --locked -p webcodex --lib search_requires_ripgrep_based_on_effective_features_not_field_presence | |
| if ($LASTEXITCODE -ne 0) { throw "Windows search capability test failed" } | |
| test-windows-runner: | |
| needs: changes | |
| if: needs.changes.outputs.needs_windows_runner == 'true' | |
| runs-on: windows-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 | |
| with: | |
| shared-key: windows-runner-ci | |
| - name: Run Windows Runner and Computer tests | |
| # This lane starts real PowerShell/MCP child processes. Keep the Rust | |
| # harness bounded so hosted-runner scheduling does not consume their | |
| # production startup/request deadlines. | |
| env: | |
| RUST_TEST_THREADS: "2" | |
| run: cargo test --locked -p webcodex-runner -p webcodex-lsp -p webcodex-computer --features runner-real-process-tests | |
| test-windows-package: | |
| needs: changes | |
| if: needs.changes.outputs.needs_windows_package == 'true' | |
| runs-on: windows-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 | |
| with: | |
| shared-key: windows-package-ci | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: 22 | |
| - name: Test npm installer and release manifest natively | |
| run: npm --prefix npm/webcodex test | |
| - name: Windows artifact to npm install smoke | |
| # Builds the three Windows binaries, packages the win32-x64 artifact | |
| # with the native PowerShell script, installs it through the npm | |
| # installer into a temporary prefix, and verifies the wrapper plus | |
| # --version/--help and the failure-preserves-old-binaries path. Never | |
| # touches the npm registry. | |
| shell: pwsh | |
| run: powershell -NoProfile -ExecutionPolicy Bypass -File scripts/npm_install_windows_smoke.ps1 | |
| test-windows-desktop: | |
| needs: changes | |
| if: needs.changes.outputs.needs_windows_desktop == 'true' | |
| runs-on: windows-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| fetch-depth: 0 | |
| - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 | |
| with: | |
| shared-key: windows-desktop-ci | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: | | |
| frontend/package-lock.json | |
| apps/desktop/package-lock.json | |
| - name: Install shared frontend dependencies | |
| run: npm ci --prefix frontend | |
| - name: Install Desktop dependencies | |
| run: npm ci --prefix apps/desktop | |
| - name: Check Desktop frontend | |
| run: | | |
| npm --prefix apps/desktop run typecheck | |
| npm --prefix apps/desktop test | |
| npm --prefix apps/desktop run build | |
| - name: Check Desktop Rust | |
| shell: pwsh | |
| run: | | |
| cargo check --locked --manifest-path apps/desktop/src-tauri/Cargo.toml --all-targets | |
| if ($LASTEXITCODE -ne 0) { throw "Desktop cargo check failed" } | |
| cargo test --locked --manifest-path apps/desktop/src-tauri/Cargo.toml | |
| if ($LASTEXITCODE -ne 0) { throw "Desktop cargo test failed" } | |
| - name: Build exact local WebCodex runtime | |
| id: runtime | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $source = (git rev-parse HEAD).Trim() | |
| $short = (git rev-parse --short=12 HEAD).Trim() | |
| $version = (node -e "console.log(require('./npm/webcodex/package.json').version)").Trim() | |
| $builtAt = [Int64]((git show -s --format=%ct HEAD).Trim()) | |
| $env:WEBCODEX_BUILT_AT = "$builtAt" | |
| cargo build --locked --profile dogfood -p webcodex -p webcodex-cli -p webcodex-runner | |
| if ($LASTEXITCODE -ne 0) { throw "WebCodex runtime build failed" } | |
| foreach ($name in @("webcodex", "webcodex-server", "webcodex-runner")) { | |
| $binary = Join-Path "target\dogfood" "$name.exe" | |
| $output = @(& $binary --version) | |
| $exitCode = $LASTEXITCODE | |
| if ($exitCode -ne 0 -or $output.Count -eq 0 -or -not $output[0]) { | |
| throw "$name --version failed with exit code $exitCode" | |
| } | |
| $actual = $output[0].TrimEnd() | |
| $expected = "$name $version (commit $short, dirty=false, built_at=$builtAt)" | |
| if ($actual -ne $expected) { throw "Unexpected $name identity: $actual" } | |
| } | |
| "source=$source" >> $env:GITHUB_OUTPUT | |
| "short=$short" >> $env:GITHUB_OUTPUT | |
| "version=$version" >> $env:GITHUB_OUTPUT | |
| "built_at=$builtAt" >> $env:GITHUB_OUTPUT | |
| - name: Stage exact runtime for Desktop bundle | |
| id: desktop_bundle | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $stage = Join-Path $env:RUNNER_TEMP "webcodex-desktop-ci-bundle" | |
| & .\scripts\prepare_desktop_bundle.ps1 ` | |
| -BinDir "target\dogfood" ` | |
| -Version "${{ steps.runtime.outputs.version }}" ` | |
| -SourceSha "${{ steps.runtime.outputs.source }}" ` | |
| -BuiltAt ([Int64]"${{ steps.runtime.outputs.built_at }}") ` | |
| -OutputDir $stage | |
| "config=$(Join-Path $stage 'tauri.bundle.conf.json')" >> $env:GITHUB_OUTPUT | |
| - name: Build unsigned NSIS installer | |
| working-directory: apps/desktop | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $env:CARGO_TARGET_DIR = Join-Path $env:GITHUB_WORKSPACE "target\desktop-ci-tauri" | |
| npm exec tauri -- build --bundles nsis --config "${{ steps.desktop_bundle.outputs.config }}" --ci --no-sign | |
| if ($LASTEXITCODE -ne 0) { throw "Tauri NSIS build failed" } | |
| $bundleDir = Join-Path $env:CARGO_TARGET_DIR "release\bundle\nsis" | |
| $built = @(Get-ChildItem -LiteralPath $bundleDir -Filter "*-setup.exe" -File) | |
| if ($built.Count -ne 1) { throw "Expected exactly one NSIS installer, found $($built.Count)" } | |
| New-Item -ItemType Directory -Force -Path "target\desktop-ci-dist" | Out-Null | |
| $installer = Join-Path "target\desktop-ci-dist" "webcodex-desktop-ci-${{ steps.runtime.outputs.short }}-v${{ steps.runtime.outputs.version }}-win32-x64-setup.exe" | |
| Copy-Item -LiteralPath $built[0].FullName -Destination $installer | |
| "DESKTOP_CI_INSTALLER=$([System.IO.Path]::GetFullPath($installer))" >> $env:GITHUB_ENV | |
| - name: Smoke newly-built Desktop installer | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| & .\scripts\desktop_install_windows_smoke.ps1 ` | |
| -Installer $env:DESKTOP_CI_INSTALLER ` | |
| -Version "${{ steps.runtime.outputs.version }}" ` | |
| -SourceSha "${{ steps.runtime.outputs.source }}" ` | |
| -BuiltAt ([Int64]"${{ steps.runtime.outputs.built_at }}") ` | |
| -Platform win32-x64 | |
| $customInstallDir = Join-Path $env:RUNNER_TEMP "WebCodex Custom Install" | |
| & .\scripts\desktop_install_windows_smoke.ps1 ` | |
| -Installer $env:DESKTOP_CI_INSTALLER ` | |
| -Version "${{ steps.runtime.outputs.version }}" ` | |
| -SourceSha "${{ steps.runtime.outputs.source }}" ` | |
| -BuiltAt ([Int64]"${{ steps.runtime.outputs.built_at }}") ` | |
| -Platform win32-x64 ` | |
| -InstallDir $customInstallDir | |
| test-windows: | |
| needs: [contract, changes, test-windows-core, test-windows-runner, test-windows-package, test-windows-desktop] | |
| # Preserve the stable Windows aggregate while allowing each heavy lane to be | |
| # selected independently by the deterministic path-risk classifier. | |
| if: always() | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Validate Windows lane policy | |
| env: | |
| CONTRACT_RESULT: ${{ needs.contract.result }} | |
| CHANGES_RESULT: ${{ needs.changes.result }} | |
| NEEDS_CORE: ${{ needs.changes.outputs.needs_windows_core }} | |
| NEEDS_RUNNER: ${{ needs.changes.outputs.needs_windows_runner }} | |
| NEEDS_PACKAGE: ${{ needs.changes.outputs.needs_windows_package }} | |
| NEEDS_DESKTOP: ${{ needs.changes.outputs.needs_windows_desktop }} | |
| CORE_RESULT: ${{ needs.test-windows-core.result }} | |
| RUNNER_RESULT: ${{ needs.test-windows-runner.result }} | |
| PACKAGE_RESULT: ${{ needs.test-windows-package.result }} | |
| DESKTOP_RESULT: ${{ needs.test-windows-desktop.result }} | |
| run: | | |
| set -euo pipefail | |
| echo "changes=$CHANGES_RESULT contract=$CONTRACT_RESULT core=$CORE_RESULT runner=$RUNNER_RESULT package=$PACKAGE_RESULT desktop=$DESKTOP_RESULT" | |
| if [ "$CHANGES_RESULT" != success ] || [ "$CONTRACT_RESULT" != success ]; then | |
| echo "CI policy or contract classification did not succeed" >&2 | |
| exit 1 | |
| fi | |
| require_lane() { | |
| label="$1" | |
| required="$2" | |
| actual="$3" | |
| expected=skipped | |
| if [ "$required" = true ]; then expected=success; fi | |
| if [ "$actual" != "$expected" ]; then | |
| echo "$label Windows lane expected $expected, got $actual" >&2 | |
| exit 1 | |
| fi | |
| } | |
| require_lane core "$NEEDS_CORE" "$CORE_RESULT" | |
| require_lane runner "$NEEDS_RUNNER" "$RUNNER_RESULT" | |
| require_lane package "$NEEDS_PACKAGE" "$PACKAGE_RESULT" | |
| require_lane desktop "$NEEDS_DESKTOP" "$DESKTOP_RESULT" | |
| test-native: | |
| needs: [contract, changes, mcp-conformance, test-docker-server, test-macos, test-windows] | |
| # Stable branch-protection context: daily native aggregates and the path-aware | |
| # Server image smoke must resolve according to classifier policy. | |
| if: always() | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Validate native lane policy | |
| env: | |
| CONTRACT_RESULT: ${{ needs.contract.result }} | |
| CHANGES_RESULT: ${{ needs.changes.result }} | |
| NEEDS_DOCKER: ${{ needs.changes.outputs.needs_docker }} | |
| MCP_CONFORMANCE_RESULT: ${{ needs.mcp-conformance.result }} | |
| DOCKER_RESULT: ${{ needs.test-docker-server.result }} | |
| MACOS_RESULT: ${{ needs.test-macos.result }} | |
| WINDOWS_RESULT: ${{ needs.test-windows.result }} | |
| run: | | |
| set -euo pipefail | |
| echo "changes=$CHANGES_RESULT contract=$CONTRACT_RESULT mcp_conformance=$MCP_CONFORMANCE_RESULT docker=$DOCKER_RESULT macos=$MACOS_RESULT windows=$WINDOWS_RESULT needs_docker=$NEEDS_DOCKER" | |
| if [ "$CHANGES_RESULT" != success ] || [ "$CONTRACT_RESULT" != success ] || [ "$MCP_CONFORMANCE_RESULT" != success ]; then | |
| echo "CI policy or contract classification did not succeed" >&2 | |
| exit 1 | |
| fi | |
| if [ "$MACOS_RESULT" != success ] || [ "$WINDOWS_RESULT" != success ]; then | |
| echo "native platform aggregate failed" >&2 | |
| exit 1 | |
| fi | |
| expected_docker=skipped | |
| if [ "$NEEDS_DOCKER" = true ]; then expected_docker=success; fi | |
| if [ "$DOCKER_RESULT" != "$expected_docker" ]; then | |
| echo "Server image lane expected $expected_docker, got $DOCKER_RESULT" >&2 | |
| exit 1 | |
| fi |