Repository navigation
Merge pull request #288 from CWNU-Open-Source-Community/codex/permiss… #697
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] | |
| pull_request: | |
| workflow_dispatch: | |
| schedule: | |
| - cron: '23 19 * * *' # 03:23 Asia/Hong_Kong | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| classify: | |
| name: Select affected checks | |
| runs-on: ubuntu-latest | |
| outputs: | |
| full: ${{ steps.changes.outputs.full }} | |
| backend: ${{ steps.changes.outputs.backend }} | |
| store: ${{ steps.changes.outputs.store }} | |
| web: ${{ steps.changes.outputs.web }} | |
| lsp: ${{ steps.changes.outputs.lsp }} | |
| rust: ${{ steps.changes.outputs.rust }} | |
| native: ${{ steps.changes.outputs.native }} | |
| ui: ${{ steps.changes.outputs.ui }} | |
| race: ${{ steps.changes.outputs.race }} | |
| audit: ${{ steps.changes.outputs.audit }} | |
| packaging: ${{ steps.changes.outputs.packaging }} | |
| checks: ${{ steps.changes.outputs.checks }} | |
| go_plan: ${{ steps.changes.outputs.go_plan }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up repository-pinned Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: Test CI helpers | |
| run: python -B -m unittest discover -s scripts/ci -p 'test_*.py' -v | |
| - name: Classify the exact change set | |
| id: changes | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before || '' }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| run: >- | |
| python -B scripts/ci/classify_changes.py | |
| --event "$EVENT_NAME" | |
| --repo "$GITHUB_WORKSPACE" | |
| --base "$BASE_SHA" | |
| --head "$HEAD_SHA" | |
| >> "$GITHUB_OUTPUT" | |
| go-checks: | |
| name: Go checks | |
| needs: classify | |
| if: ${{ !cancelled() && needs.classify.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| # Protocol and Surface history are compared with the exact PR/push | |
| # baseline. The first Surface registry PR is its explicit bootstrap. | |
| fetch-depth: 0 | |
| - name: Set up repository-pinned Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: Report Go toolchain | |
| run: go version | |
| - run: go mod verify | |
| - name: Verify Go module graph is tidy | |
| run: go mod tidy -diff | |
| - name: Verify classified protocol registry and reader history | |
| env: | |
| PROTOCOL_REGISTRY_BASELINE: ${{ github.event.pull_request.base.sha || github.event.before || github.sha }} | |
| run: >- | |
| go run ./cmd/protocolregistry -check | |
| -baseline "$PROTOCOL_REGISTRY_BASELINE" | |
| - name: Materialize reviewed base Surface registry | |
| shell: bash | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before || github.sha }} | |
| run: | | |
| if [[ ! "$BASE_SHA" =~ ^[0-9a-f]{40}$ ]]; then | |
| echo "GitHub base commit identity is invalid" | |
| exit 1 | |
| fi | |
| registry_path="docs/convergence/surface-registry.json" | |
| if git cat-file -e "${BASE_SHA}:${registry_path}" 2>/dev/null; then | |
| base_registry="$RUNNER_TEMP/surface-registry-base.json" | |
| git show "${BASE_SHA}:${registry_path}" > "$base_registry" | |
| echo "SURFACE_REGISTRY_BASE=$base_registry" >> "$GITHUB_ENV" | |
| else | |
| echo "Base Surface registry is absent; treating this PR as the registry bootstrap." | |
| fi | |
| - name: Verify Surface tier registry and generated inventory | |
| run: go run ./cmd/surfacecheck | |
| - name: Verify documentation and release contracts | |
| env: | |
| GO_PLAN: ${{ needs.classify.outputs.go_plan }} | |
| run: | | |
| python - <<'PY' | |
| import json, os, subprocess | |
| plan = json.loads(os.environ["GO_PLAN"]) | |
| packages = [name for name in ( | |
| "cyberagent-workbench/internal/releasegate", | |
| "cyberagent-workbench/internal/producte2e", | |
| ) if name not in plan.get("test", [])] | |
| if packages: | |
| subprocess.run(["go", "test", "-count=1", *packages], check=True) | |
| PY | |
| - name: Test and vet selected Go packages | |
| if: ${{ needs.classify.outputs.backend == 'true' }} | |
| env: | |
| GO_PLAN: ${{ needs.classify.outputs.go_plan }} | |
| run: | | |
| printf '%s' "$GO_PLAN" > "$RUNNER_TEMP/go-plan.json" | |
| python -B scripts/ci/run_go_checks.py --plan "$RUNNER_TEMP/go-plan.json" | |
| go-race: | |
| name: Go authority race checks | |
| needs: classify | |
| if: ${{ !cancelled() && needs.classify.result == 'success' && needs.classify.outputs.race == 'true' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - name: Set up repository-pinned Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: Race-check Workspace Access permission and authority fencing | |
| run: >- | |
| go test -race -timeout 15m -count=1 | |
| ./internal/domain ./internal/executionauth ./internal/application | |
| ./internal/store ./internal/httpapi | |
| -run 'WorkspaceAccess|RunExecutionPermission|ExecutionPermission|PermissionDrift|SchemaV126|AgentCodeExecutorCreatesReviewedFileAndFailsClosedOnCASConflict|CommandOperationApprovalRealNativeThreeModes' | |
| go-audit: | |
| name: Go dependency audit | |
| needs: classify | |
| if: ${{ !cancelled() && needs.classify.result == 'success' && needs.classify.outputs.audit == 'true' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - name: Set up repository-pinned Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: Install pinned govulncheck | |
| env: | |
| GOBIN: ${{ runner.temp }} | |
| run: go install golang.org/x/vuln/cmd/govulncheck@v1.6.0 | |
| - name: Scan for reachable Go vulnerabilities | |
| env: | |
| GOVULNCHECK: ${{ runner.temp }}/govulncheck | |
| run: | | |
| "$GOVULNCHECK" -version | |
| "$GOVULNCHECK" ./... | |
| store-tests: | |
| name: Store package tests (shard ${{ matrix.shard }}) | |
| needs: classify | |
| if: ${{ !cancelled() && needs.classify.result == 'success' && needs.classify.outputs.store == 'true' }} | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [0, 1, 2, 3, 4, 5, 6, 7] | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - name: Set up repository-pinned Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: Run exact Store shard | |
| run: >- | |
| python -B scripts/ci/run_store_shard.py | |
| --index ${{ matrix.shard }} | |
| --count 8 | |
| --output-dir "$RUNNER_TEMP/store-shard-${{ matrix.shard }}" | |
| - name: Upload Store shard evidence | |
| if: ${{ always() }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: store-shard-${{ matrix.shard }} | |
| path: ${{ runner.temp }}/store-shard-${{ matrix.shard }}/ | |
| if-no-files-found: error | |
| retention-days: 5 | |
| go: | |
| name: Go control plane | |
| needs: [classify, go-checks, store-tests, go-race, go-audit] | |
| if: ${{ always() }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Require every selected Go gate | |
| env: | |
| NEEDS_JSON: ${{ toJSON(needs) }} | |
| run: | | |
| python - <<'PY' | |
| import json, os | |
| needs = json.loads(os.environ["NEEDS_JSON"]) | |
| if needs["classify"]["result"] != "success": | |
| raise SystemExit("Change selection failed") | |
| checks = json.loads(needs["classify"]["outputs"]["checks"]) | |
| required = ["go-checks"] | |
| required += [job for job, flag in ( | |
| ("store-tests", "store"), ("go-race", "race"), ("go-audit", "audit") | |
| ) if checks[flag]] | |
| failed = [job for job in required if needs[job]["result"] != "success"] | |
| if failed: | |
| raise SystemExit("Required Go checks did not succeed: " + ", ".join(failed)) | |
| print("All selected Go checks succeeded") | |
| PY | |
| code-intel-lsp: | |
| name: Real LSP (${{ matrix.os }}) | |
| needs: classify | |
| if: ${{ !cancelled() && needs.classify.result == 'success' && needs.classify.outputs.lsp == 'true' }} | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - name: Set up repository-pinned Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 24 | |
| - name: Install reviewed real language servers | |
| shell: pwsh | |
| run: | | |
| $goBin = Join-Path $env:RUNNER_TEMP "prayu-gopls" | |
| $tsRoot = Join-Path $env:RUNNER_TEMP "prayu-typescript-lsp" | |
| New-Item -ItemType Directory -Path $goBin -Force | Out-Null | |
| $env:GOBIN = $goBin | |
| go install golang.org/x/tools/gopls@v0.21.1 | |
| npm install --prefix $tsRoot --ignore-scripts --no-audit --no-fund ` | |
| --package-lock=false typescript-language-server@6.0.0 typescript@5.9.3 | |
| $suffix = if ($IsWindows) { ".exe" } else { "" } | |
| $gopls = Join-Path $goBin "gopls$suffix" | |
| $node = (Get-Command node).Source | |
| $typescriptServer = Join-Path $tsRoot ` | |
| "node_modules/typescript-language-server/lib/cli.mjs" | |
| foreach ($path in @($gopls, $node, $typescriptServer)) { | |
| if (-not [System.IO.Path]::IsPathFullyQualified($path) -or | |
| -not (Test-Path -LiteralPath $path -PathType Leaf)) { | |
| throw "reviewed real LSP dependency is unavailable: $path" | |
| } | |
| } | |
| "CYBERAGENT_GOPLS_SMOKE_BINARY=$gopls" | Out-File ` | |
| -FilePath $env:GITHUB_ENV -Encoding utf8 -Append | |
| "CYBERAGENT_TYPESCRIPT_LSP_SMOKE_NODE=$node" | Out-File ` | |
| -FilePath $env:GITHUB_ENV -Encoding utf8 -Append | |
| "CYBERAGENT_TYPESCRIPT_LSP_SMOKE_CLI=$typescriptServer" | Out-File ` | |
| -FilePath $env:GITHUB_ENV -Encoding utf8 -Append | |
| - name: Exercise reviewed gopls and TypeScript LSP tool sets | |
| run: >- | |
| go test -v -count=1 -timeout 5m ./internal/codeintel | |
| -run '^TestReal(Gopls|TypeScriptLanguageServer)ReadOnlySemanticToolSmoke$' | |
| - name: Run code-intel race and fault suite | |
| if: runner.os == 'Linux' | |
| run: go test -race -count=1 -timeout 5m ./internal/codeintel | |
| web: | |
| name: TypeScript console | |
| needs: classify | |
| if: ${{ !cancelled() && needs.classify.result == 'success' && needs.classify.outputs.web == 'true' }} | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: web | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 24 | |
| cache: npm | |
| cache-dependency-path: web/package-lock.json | |
| - run: npm ci | |
| - run: npm run check:api | |
| - run: npm test | |
| - run: npm run build | |
| - name: Set up repository-pinned Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: Verify Go bundle loader and embedded production UI bundle | |
| working-directory: . | |
| run: | | |
| go test -count=1 ./internal/webui | |
| go test -count=1 -tags desktop ./web | |
| - name: Audit dependencies | |
| run: | | |
| for attempt in 1 2 3; do | |
| if npm audit --audit-level=high; then | |
| exit 0 | |
| fi | |
| if [ "$attempt" -lt 3 ]; then | |
| sleep $((attempt * 5)) | |
| fi | |
| done | |
| exit 1 | |
| rust: | |
| name: Rust analyzer fixtures | |
| needs: classify | |
| if: ${{ !cancelled() && needs.classify.result == 'success' && needs.classify.outputs.rust == 'true' }} | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: analyzers | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - name: Set up repository-pinned Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: Report Go toolchain | |
| run: go version | |
| - name: Install pinned Rust toolchain | |
| run: rustup toolchain install 1.97.1 --profile minimal --component rustfmt --component clippy | |
| - run: cargo fmt --all -- --check | |
| - run: cargo test --locked | |
| - run: cargo clippy --locked --all-targets -- -D warnings | |
| - name: Audit locked Rust dependencies | |
| env: | |
| CARGO_AUDIT_VERSION: 0.22.2 | |
| CARGO_AUDIT_SHA256: ab28a1bdb54db4d5d8ad5981cf1f959410370b3d28250dbd35f6a44248620e39 | |
| run: | | |
| archive="$RUNNER_TEMP/cargo-audit.tgz" | |
| audit_root="$RUNNER_TEMP/cargo-audit" | |
| advisory_db="$RUNNER_TEMP/rustsec-advisory-db" | |
| curl --fail --location --retry 3 --retry-all-errors \ | |
| --output "$archive" \ | |
| "https://github.com/RustSec/rustsec/releases/download/cargo-audit/v${CARGO_AUDIT_VERSION}/cargo-audit-x86_64-unknown-linux-gnu-v${CARGO_AUDIT_VERSION}.tgz" | |
| echo "${CARGO_AUDIT_SHA256} ${archive}" | sha256sum --check | |
| mkdir -p "$audit_root" | |
| tar --extract --gzip --file "$archive" --directory "$audit_root" | |
| git clone --depth 1 https://github.com/RustSec/advisory-db.git "$advisory_db" | |
| git -C "$advisory_db" log -1 --format='RustSec advisory DB: %H (%cI)' | |
| "$audit_root/cargo-audit-x86_64-unknown-linux-gnu-v${CARGO_AUDIT_VERSION}/cargo-audit" \ | |
| audit --no-fetch --no-yanked --db "$advisory_db" --file Cargo.lock | |
| - name: Build deterministic Rust fixture | |
| run: cargo build --locked --bin cyberagent-analyzer-fixture | |
| - name: Build embedded WASI analyzer fixture | |
| working-directory: . | |
| run: ./scripts/build-embedded-wasi.sh | |
| - name: Verify rebuilt WASI matches the embedded product module | |
| id: verify-wasi | |
| working-directory: . | |
| env: | |
| REBUILT_WASI_FIXTURE: ${{ github.workspace }}/analyzers/target/wasm32-wasip1/release/cyberagent-analyzer-fixture.wasm | |
| EMBEDDED_WASI_FIXTURE: ${{ github.workspace }}/internal/analyzer/embedded/cyberagent-analyzer-fixture.wasm | |
| run: | | |
| if ! cmp --silent "$REBUILT_WASI_FIXTURE" "$EMBEDDED_WASI_FIXTURE"; then | |
| echo "The Rust fixture and the product-embedded WASI module differ." | |
| sha256sum "$REBUILT_WASI_FIXTURE" "$EMBEDDED_WASI_FIXTURE" | |
| echo "Run ./scripts/build-embedded-wasi.sh and replace internal/analyzer/embedded/cyberagent-analyzer-fixture.wasm." | |
| exit 1 | |
| fi | |
| sha256sum "$EMBEDDED_WASI_FIXTURE" | |
| - name: Preserve a mismatched rebuilt WASI module | |
| if: failure() && steps.verify-wasi.outcome == 'failure' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: rebuilt-wasi-fixture | |
| path: analyzers/target/wasm32-wasip1/release/cyberagent-analyzer-fixture.wasm | |
| if-no-files-found: error | |
| retention-days: 1 | |
| - name: Verify embedded WASI boundaries and shared protocol fixtures | |
| working-directory: . | |
| env: | |
| CYBERAGENT_WASI_FIXTURE: ${{ github.workspace }}/analyzers/target/wasm32-wasip1/release/cyberagent-analyzer-fixture.wasm | |
| run: go test -count=1 ./internal/analyzer -run '^Test(AnalyzerEmbeddedWASIProductionFixtureAssessment|AnalyzerProtocolSharedGoldenVectors|ArchiveInventorySharedGoldenVectors)$' | |
| - name: Verify Linux analyzer conformance boundaries | |
| working-directory: . | |
| env: | |
| CYBERAGENT_ANALYZER_CONFORMANCE_BINARY: ${{ github.workspace }}/analyzers/target/debug/cyberagent-analyzer-fixture | |
| run: go test -count=1 ./internal/analyzer -run '^Test(SubprocessConformance|AnalyzerResultStaging|Analyzer(ImmutableHandleHandoff|DedicatedLowPrivilegeIdentity|ReadOnlyFilesystemPrivateStaging)Conformance)' | |
| desktop-macos: | |
| name: macOS Desktop shell | |
| needs: classify | |
| if: ${{ !cancelled() && needs.classify.result == 'success' && needs.classify.outputs.native == 'true' }} | |
| runs-on: macos-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - name: Set up repository-pinned Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: Report Go toolchain | |
| run: go version | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 24 | |
| cache: npm | |
| cache-dependency-path: web/package-lock.json | |
| - name: Build embedded renderer | |
| working-directory: web | |
| run: | | |
| npm ci | |
| npm run build | |
| - name: Verify isolated macOS Keychain persistence | |
| env: | |
| CGO_ENABLED: '1' | |
| TRAVERSE_TEST_KEYCHAIN: '1' | |
| run: go test -v -tags keychainintegration -count=1 -timeout=90s ./internal/credential | |
| - name: Verify POSIX command runtime transport | |
| run: go test -v -count=1 ./internal/runner -run '^TestCommandRuntimePlatform(ShellSmoke|CancelReapsOwnedProcessTree)$' | |
| - name: Verify desktop boundaries without packaging | |
| if: ${{ needs.classify.outputs.packaging != 'true' }} | |
| run: | | |
| go test -count=1 ./internal/desktop ./internal/webui | |
| go test -count=1 -tags desktop ./cmd/cyberagent-desktop ./web | |
| - name: Build and verify downloadable macOS preview | |
| if: ${{ needs.classify.outputs.packaging == 'true' }} | |
| run: bash scripts/release-desktop-darwin.sh "v1.0.0-ci.$GITHUB_RUN_ID" "$GITHUB_SHA" "$(go env GOARCH)" | |
| - name: Upload downloadable macOS preview | |
| if: ${{ needs.classify.outputs.packaging == 'true' }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: TraverseBoard-macos-preview-arm64 | |
| path: build/macos-public/* | |
| if-no-files-found: error | |
| retention-days: 30 | |
| desktop: | |
| name: Windows Desktop shell | |
| needs: classify | |
| if: ${{ !cancelled() && needs.classify.result == 'success' && needs.classify.outputs.native == 'true' }} | |
| runs-on: windows-latest | |
| env: | |
| TRAVERSE_BOARD_LOCAL_SANDBOX_TEST_HOST_PREP: '1' | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - name: Set up repository-pinned Go | |
| id: windows-go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: Report Go toolchain | |
| run: go version | |
| - name: Prepare official Go at the fixed native installation path | |
| shell: pwsh | |
| env: | |
| TRAVERSE_CI_GO_VERSION: ${{ steps.windows-go.outputs.go-version }} | |
| run: | | |
| ./scripts/tests/prepare-windows-fixed-go.tests.ps1 | |
| ./scripts/tests/desktop-toolchain.tests.ps1 | |
| ./scripts/prepare-windows-fixed-go.ps1 -ExpectedGoVersion $env:TRAVERSE_CI_GO_VERSION | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 24 | |
| cache: npm | |
| cache-dependency-path: web/package-lock.json | |
| - name: Install pinned Rust toolchain | |
| run: rustup toolchain install 1.97.1 --profile minimal | |
| - name: Build deterministic Rust fixture | |
| working-directory: analyzers | |
| run: cargo +1.97.1 build --locked --bin cyberagent-analyzer-fixture | |
| - name: Build embedded renderer | |
| working-directory: web | |
| run: | | |
| npm ci | |
| npm run build | |
| - name: Verify Windows Local Sandbox isolation and product gates | |
| shell: pwsh | |
| run: | | |
| go test -v -count=1 -timeout 10m ./internal/app -run '^TestWindowsAPIServe' | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| $cliTests = @( | |
| 'TestRunCommandExecuteIsConfirmedAuditedAndExactlyOnce' | |
| 'TestCLIExecutionEntrypointsWireOrdinaryAgentBrowser' | |
| 'TestCLIWakeConsumeWiresOrdinaryAgentBrowser' | |
| 'TestCLIApprovalExecutionHandoffWiresOrdinaryAgentBrowser' | |
| 'TestCLIExecutionRuntimeClosesAgentBrowserWithoutGrantingPersistedFullAccess' | |
| 'TestCLIApprovalContinuationRequiresInvocationFullConfirmation' | |
| 'TestCLIFileReviewKeepsExactDecisionSeparateFromContinuationFull' | |
| 'TestCLIApprovalCompletedContinuationReplaysWithoutModelOrTool' | |
| 'TestCLIApprovalDenyContinuesOnceAndReplaysAcrossApps' | |
| 'TestCLIApprovalDoesNotWakePausedOrCancelledRun' | |
| ) | |
| go test -v -count=1 -timeout 10m ./internal/app -run ('^(' + ($cliTests -join '|') + ')$') | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| # Preserve the operator-first launch order and retain both results. | |
| go test -v -count=1 -timeout 10m ./internal/application -run '^TestOperatorFixedCommandUsesSharedJobLedger$' | |
| $fixedOperatorExitCode = $LASTEXITCODE | |
| go test -v -count=1 -timeout 10m ./internal/runner -run '^TestWindowsFixedCommandRuntimeUsesRestrictedNativeProcess$' | |
| if ($fixedOperatorExitCode -ne 0) { exit $fixedOperatorExitCode } | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| go test -v -count=1 -timeout 10m ./internal/sandbox -run '^TestWindowsLocalSandbox' | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| # Local command manifests bind Windows drive roots and LPAC policy. | |
| # Run their launch freshness/capability tests on the actual platform. | |
| go test -v -count=1 -timeout 10m ./internal/application -run '^TestLocalCommand' | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| - name: Verify cross-platform Runner receipt vectors | |
| run: go test -count=1 ./internal/runner -run 'TestEvidenceSetReceipt(Golden|Compatibility).*Vectors' | |
| - name: Verify command operation authority and cross-turn ownership | |
| run: >- | |
| go test -v -count=1 -timeout 10m ./internal/application ./internal/workspace | |
| -run '^(TestCommandOperationApproval.*|TestCommandOperationRechecksAfterPrepareAndAtNativeLaunch|TestCommandOperationInitialInputOutlivesRequestButRechecksAuthority|TestCommandRuntimeForegroundCancellationReapsProcessTree|TestCommandRuntimeUIEvidenceCleanupReapsExactJobAfterLeaseRelease|TestThreadBackgroundCommandWaitRetainsJobUntilExplicitPause|TestWorkspaceInit.*|TestWorkspaceImport.*DirectoryAlias)$' | |
| - name: Verify Windows command runtime transports | |
| run: go test -v -count=1 ./internal/runner -run '^TestCommandRuntime(Platform(ShellSmoke|CancelReapsOwnedProcessTree)|WindowsPowerShell(5PowerShell7AndGitBashSmoke|5BindsWorkspaceProfileBeforeLaunch|5ProfileWatchdogReapsProcess|7KeepsExistingProfileEnvironment))$' | |
| - name: Verify Windows analyzer conformance boundaries | |
| shell: pwsh | |
| run: | | |
| $env:CYBERAGENT_ANALYZER_CONFORMANCE_BINARY = Join-Path $env:GITHUB_WORKSPACE "analyzers\target\debug\cyberagent-analyzer-fixture.exe" | |
| go test -v -count=1 ./internal/analyzer -run '^Test(SubprocessConformance|AnalyzerResultStaging|AnalyzerSandboxEnforcementConformance|Analyzer(ImmutableHandleHandoff|DedicatedLowPrivilegeIdentity|ReadOnlyFilesystemPrivateStaging)Conformance)' | |
| - name: Verify desktop boundaries without packaging | |
| if: ${{ needs.classify.outputs.packaging != 'true' }} | |
| shell: pwsh | |
| run: | | |
| go test -count=1 ./internal/desktop ./internal/webui | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| go test -count=1 -tags "desktop,wv2runtime.error" ./cmd/cyberagent-desktop ./web | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| - name: Build and verify portable Desktop shell | |
| if: ${{ needs.classify.outputs.packaging == 'true' }} | |
| shell: pwsh | |
| run: ./scripts/build-desktop.ps1 -SkipFrontend -VerifyReproducible | |
| ui-evidence-windows: | |
| name: Real Edge UI evidence (standard user, Windows 2022) | |
| needs: classify | |
| if: ${{ !cancelled() && needs.classify.result == 'success' && needs.classify.outputs.ui == 'true' }} | |
| runs-on: windows-2022 | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - name: Set up repository-pinned Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: Verify Local Sandbox on the Windows 2022 compatibility host | |
| env: | |
| TRAVERSE_BOARD_LOCAL_SANDBOX_TEST_HOST_PREP: '1' | |
| run: go test -v -count=1 -timeout 10m ./internal/sandbox -run '^TestWindowsLocalSandbox' | |
| - name: Verify real Edge UI evidence matrix and regression detection | |
| shell: pwsh | |
| run: | | |
| $artifactDirectory = Join-Path $env:RUNNER_TEMP "ui-evidence-smoke" | |
| New-Item -ItemType Directory -Path $artifactDirectory -Force | Out-Null | |
| if ($env:GITHUB_RUN_ID -notmatch '^\d+$' -or $env:GITHUB_RUN_ATTEMPT -notmatch '^\d+$') { | |
| throw "GitHub run identity is unavailable" | |
| } | |
| # Hosted Windows RUNNER_TEMP is reparse-backed, and runneradmin's | |
| # LOCALAPPDATA is not traversable by the disposable standard user. | |
| # Use one exact child of the direct system volume root, then grant | |
| # that child (never the root) only to the temporary SID below. | |
| if ([string]::IsNullOrWhiteSpace($env:SystemRoot) -or | |
| -not [System.IO.Path]::IsPathFullyQualified($env:SystemRoot)) { | |
| throw "Windows system root is unavailable" | |
| } | |
| $directTempParent = [System.IO.Path]::GetPathRoot( | |
| [System.IO.Path]::GetFullPath($env:SystemRoot)) | |
| $directTempParentItem = Get-Item -LiteralPath $directTempParent -Force | |
| if (-not $directTempParentItem.PSIsContainer -or | |
| ($directTempParentItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { | |
| throw "Windows system volume root is indirect" | |
| } | |
| $directTempName = "cyberagent-ui-evidence-$($env:GITHUB_RUN_ID)-$($env:GITHUB_RUN_ATTEMPT)" | |
| $directTempRoot = Join-Path $directTempParent $directTempName | |
| $resolvedTempParent = [System.IO.Path]::GetFullPath((Split-Path -Parent $directTempRoot)) | |
| if (-not [string]::Equals( | |
| $resolvedTempParent.TrimEnd('\'), | |
| $directTempParent.TrimEnd('\'), | |
| [System.StringComparison]::OrdinalIgnoreCase)) { | |
| throw "UI evidence direct temp root escaped the system volume root" | |
| } | |
| if (Test-Path -LiteralPath $directTempRoot) { | |
| throw "UI evidence direct temp root already exists" | |
| } | |
| $directTempItem = New-Item -ItemType Directory -Path $directTempRoot | |
| if (($directTempItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { | |
| throw "UI evidence direct temp root is indirect" | |
| } | |
| $directArtifactDirectory = Join-Path $directTempRoot "artifacts" | |
| $testBinary = Join-Path $directTempRoot "browserruntime.test.exe" | |
| $userHome = Join-Path $directTempRoot "home" | |
| $userLocalAppData = Join-Path $userHome "AppData\Local" | |
| $userRoamingAppData = Join-Path $userHome "AppData\Roaming" | |
| foreach ($directory in @($directArtifactDirectory, $userLocalAppData, $userRoamingAppData)) { | |
| New-Item -ItemType Directory -Path $directory -Force | Out-Null | |
| } | |
| $runSuffix = $env:GITHUB_RUN_ID.Substring( | |
| [Math]::Max(0, $env:GITHUB_RUN_ID.Length - 8)) | |
| $standardUserName = "cyberui$runSuffix$($env:GITHUB_RUN_ATTEMPT)" | |
| if ($standardUserName -notmatch '^cyberui\d{1,12}$' -or | |
| $standardUserName.Length -gt 20) { | |
| throw "derived UI evidence standard-user name is invalid" | |
| } | |
| if (Get-LocalUser -Name $standardUserName -ErrorAction SilentlyContinue) { | |
| throw "UI evidence standard user already exists" | |
| } | |
| $standardUserCreated = $false | |
| $workspaceGrantAdded = $false | |
| $standardUserSID = $null | |
| $process = $null | |
| $securePassword = $null | |
| $cleanupFailure = $null | |
| try { | |
| go test -c -o $testBinary ./internal/browserruntime | |
| if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $testBinary)) { | |
| throw "compile real Edge UI evidence test binary failed" | |
| } | |
| $plainPassword = "Aa1!$([guid]::NewGuid().ToString('N'))zZ9!" | |
| $securePassword = ConvertTo-SecureString $plainPassword -AsPlainText -Force | |
| New-LocalUser -Name $standardUserName -Password $securePassword ` | |
| -AccountNeverExpires -PasswordNeverExpires -UserMayNotChangePassword | Out-Null | |
| $standardUserCreated = $true | |
| $usersGroup = Get-LocalGroup -SID "S-1-5-32-545" | |
| Add-LocalGroupMember -Group $usersGroup -Member $standardUserName | |
| $standardUserSID = (Get-LocalUser -Name $standardUserName).SID.Value | |
| if ($standardUserSID -notmatch '^S-1-5-21-(\d+-){3}\d+$') { | |
| throw "UI evidence standard-user SID is invalid" | |
| } | |
| & icacls.exe $env:GITHUB_WORKSPACE /grant:r ` | |
| ("*{0}:(OI)(CI)RX" -f $standardUserSID) /Q | Out-Host | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "grant standard-user workspace read access failed" | |
| } | |
| $workspaceGrantAdded = $true | |
| & icacls.exe $directTempRoot /grant:r ` | |
| ("*{0}:(OI)(CI)M" -f $standardUserSID) /Q | Out-Host | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "grant standard-user temporary-root access failed" | |
| } | |
| $start = [System.Diagnostics.ProcessStartInfo]::new() | |
| $start.FileName = $testBinary | |
| $start.WorkingDirectory = $env:GITHUB_WORKSPACE | |
| $start.UseShellExecute = $false | |
| $start.CreateNoWindow = $true | |
| $start.RedirectStandardOutput = $true | |
| $start.RedirectStandardError = $true | |
| $start.Domain = $env:COMPUTERNAME | |
| $start.UserName = $standardUserName | |
| $start.Password = $securePassword | |
| # Edge requires the disposable account's HKCU hive during browser | |
| # initialization. The exact profile is removed by SID below. | |
| $start.LoadUserProfile = $true | |
| $start.ArgumentList.Add("-test.v") | |
| $start.ArgumentList.Add("-test.timeout=3m") | |
| $start.ArgumentList.Add( | |
| "-test.run=^TestInstalledEdgeUIEvidenceHeadlessMatrixAndRegression$") | |
| $start.Environment.Clear() | |
| $processEnvironment = [ordered]@{ | |
| APPDATA = $userRoamingAppData | |
| CYBERAGENT_UI_EVIDENCE_ARTIFACT_DIR = $directArtifactDirectory | |
| CYBERAGENT_UI_EVIDENCE_SMOKE = "1" | |
| GITHUB_ACTIONS = "true" | |
| GIT_CONFIG_COUNT = "1" | |
| # Git for Windows 2.56/UCRT requires lowercase nul here. | |
| GIT_CONFIG_GLOBAL = "nul" | |
| GIT_CONFIG_KEY_0 = "safe.directory" | |
| GIT_CONFIG_NOSYSTEM = "1" | |
| GIT_CONFIG_VALUE_0 = $env:GITHUB_WORKSPACE | |
| GIT_OPTIONAL_LOCKS = "0" | |
| GIT_TERMINAL_PROMPT = "0" | |
| HOME = $userHome | |
| LOCALAPPDATA = $userLocalAppData | |
| PATH = "C:\Program Files\Git\cmd;$env:SystemRoot\System32;$env:SystemRoot" | |
| PATHEXT = ".COM;.EXE;.BAT;.CMD" | |
| SystemRoot = $env:SystemRoot | |
| TEMP = $directTempRoot | |
| TMP = $directTempRoot | |
| USERPROFILE = $userHome | |
| WINDIR = $env:WINDIR | |
| } | |
| foreach ($entry in $processEnvironment.GetEnumerator()) { | |
| $start.Environment[$entry.Key] = $entry.Value | |
| } | |
| $process = [System.Diagnostics.Process]::new() | |
| $process.StartInfo = $start | |
| if (-not $process.Start()) { | |
| throw "start real Edge UI evidence as a standard user failed" | |
| } | |
| $plainPassword = $null | |
| $stdout = $process.StandardOutput.ReadToEndAsync() | |
| $stderr = $process.StandardError.ReadToEndAsync() | |
| if (-not $process.WaitForExit(240000)) { | |
| $process.Kill($true) | |
| $process.WaitForExit() | |
| throw "real Edge UI evidence standard-user process timed out" | |
| } | |
| $stdout.Result | Write-Host | |
| $stderr.Result | Write-Host | |
| if (Test-Path -LiteralPath $directArtifactDirectory) { | |
| Get-ChildItem -LiteralPath $directArtifactDirectory -File | ForEach-Object { | |
| Copy-Item -LiteralPath $_.FullName -Destination $artifactDirectory -Force | |
| } | |
| } | |
| if ($process.ExitCode -ne 0) { | |
| throw "real Edge UI evidence verification failed with exit code $($process.ExitCode)" | |
| } | |
| if (-not (Test-Path -LiteralPath (Join-Path $artifactDirectory "receipt.json"))) { | |
| throw "real Edge UI evidence receipt was not produced" | |
| } | |
| } | |
| finally { | |
| if ($null -ne $process) { | |
| $process.Dispose() | |
| } | |
| if ($null -ne $securePassword) { | |
| $securePassword.Dispose() | |
| } | |
| if ($workspaceGrantAdded -and $null -ne $standardUserSID) { | |
| & icacls.exe $env:GITHUB_WORKSPACE /remove:g ` | |
| ("*{0}" -f $standardUserSID) /Q | Out-Host | |
| if ($LASTEXITCODE -ne 0) { | |
| $cleanupFailure = "remove standard-user workspace grant failed" | |
| } | |
| } | |
| if ($standardUserCreated) { | |
| $profileDeadline = (Get-Date).AddSeconds(5) | |
| do { | |
| $loadedProfile = Get-CimInstance -ClassName Win32_UserProfile | | |
| Where-Object { $_.SID -eq $standardUserSID } | |
| if ($null -eq $loadedProfile -or -not $loadedProfile.Loaded) { | |
| break | |
| } | |
| Start-Sleep -Milliseconds 250 | |
| } while ((Get-Date) -lt $profileDeadline) | |
| if ($null -ne $loadedProfile) { | |
| if ($loadedProfile.Special -or $loadedProfile.Loaded) { | |
| $cleanupFailure = "UI evidence standard-user profile is not removable" | |
| } | |
| else { | |
| $loadedProfile | Remove-CimInstance | |
| } | |
| } | |
| Remove-LocalUser -Name $standardUserName | |
| } | |
| if (Test-Path -LiteralPath $directTempRoot) { | |
| Remove-Item -LiteralPath $directTempRoot -Recurse -Force | |
| } | |
| if (Test-Path -LiteralPath $directTempRoot) { | |
| throw "UI evidence direct temp root was not cleaned" | |
| } | |
| if ($null -ne $cleanupFailure) { | |
| throw $cleanupFailure | |
| } | |
| } | |
| - name: Upload real Edge UI evidence receipt | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ui-evidence-edge-smoke | |
| path: ${{ runner.temp }}/ui-evidence-smoke | |
| if-no-files-found: warn | |
| retention-days: 5 | |
| full-ci: | |
| name: Full CI verification | |
| needs: [classify, go, code-intel-lsp, web, rust, desktop-macos, desktop, ui-evidence-windows] | |
| if: ${{ always() && needs.classify.outputs.full == 'true' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Require the complete integration and platform matrix | |
| env: | |
| NEEDS_JSON: ${{ toJSON(needs) }} | |
| run: | | |
| python - <<'PY' | |
| import json, os | |
| needs = json.loads(os.environ["NEEDS_JSON"]) | |
| failed = [name for name, job in needs.items() if job["result"] != "success"] | |
| if failed: | |
| raise SystemExit("Full CI did not succeed: " + ", ".join(failed)) | |
| print("Full CI verification succeeded") | |
| PY |