Skip to content

Merge pull request #288 from CWNU-Open-Source-Community/codex/permiss… #697

Merge pull request #288 from CWNU-Open-Source-Community/codex/permiss…

Merge pull request #288 from CWNU-Open-Source-Community/codex/permiss… #697

Workflow file for this run

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