Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 149 additions & 77 deletions .github/workflows/ci.yml

Large diffs are not rendered by default.

60 changes: 38 additions & 22 deletions .github/workflows/release-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ on:
- finalize
pull_request:
paths:
# Product/dependency changes are checked by central CI. PR packaging
# validates changes to the release machinery and distribution inputs.
- '.github/workflows/release-desktop.yml'
- 'cmd/packagede2e/**'
- 'cmd/producte2e/**'
Expand All @@ -27,6 +29,7 @@ on:
- 'internal/producte2e/**'
- 'internal/releasegate/**'
- 'scripts/build-desktop-darwin.sh'
- 'scripts/build-embedded-wasi.sh'
- 'scripts/check-macos-compat.sh'
- 'scripts/release-desktop-darwin.sh'
- 'scripts/macos-release.py'
Expand All @@ -43,25 +46,12 @@ on:
- 'scripts/smoke-desktop-operator-preview.ps1'
- 'scripts/standard-code-packaged-e2e.ps1'
- 'scripts/standard-code-product-e2e.ps1'
- 'scripts/test-standard-code-packaged-readiness.ps1'
- 'scripts/verify-portable-zip.ps1'
- 'scripts/verify-msix.ps1'
- 'packaging/windows/**'
- 'packaging/macos/**'
- 'assets/branding/**'
- 'web/public/**'
- 'web/src/assets/fonts/**'
- 'web/src/assets/traverse-board-mark.png'
- 'packaging/windows/RELEASE-NOTES.md'
- 'protocols/registry.json'
- 'go.mod'
- 'go.sum'
- 'web/package.json'
- 'web/package-lock.json'
- 'analyzers/Cargo.toml'
- 'analyzers/Cargo.lock'
- 'analyzers/rust-toolchain.toml'
- 'internal/analyzer/embedded/**'
- 'README.md'
push:
tags:
- 'v*'
Expand Down Expand Up @@ -150,7 +140,7 @@ jobs:
"phase=$phase" >> $env:GITHUB_OUTPUT
"signing_prepare=$($signingPrepare.ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT

- name: Require successful central CI for the exact release revision
- name: Require successful full central CI for the exact release revision
if: ${{ github.event_name != 'pull_request' }}
shell: pwsh
env:
Expand All @@ -161,18 +151,44 @@ jobs:
throw "Release CI revision is invalid"
}
$endpoint = "repos/$env:GITHUB_REPOSITORY/actions/workflows/ci.yml/runs" +
"?head_sha=$revision&event=push&status=completed&per_page=100"
$response = gh api $endpoint
"?head_sha=$revision&status=completed&per_page=100"
$response = gh api $endpoint --paginate --slurp
if ($LASTEXITCODE -ne 0) { throw "Central CI provenance lookup failed" }
$runs = ($response | Out-String) | ConvertFrom-Json
$successful = @($runs.workflow_runs | Where-Object {
$pages = ($response | Out-String) | ConvertFrom-Json
$successful = @($pages | ForEach-Object { $_.workflow_runs } | Where-Object {
[string]$_.head_sha -ceq $revision -and
[string]$_.event -ceq "push" -and
[string]$_.event -cin @("push", "workflow_dispatch", "schedule") -and
[string]$_.status -ceq "completed" -and
[string]$_.conclusion -ceq "success"
})
if ($successful.Count -lt 1) {
throw "The exact release revision has no successful central CI push run"
$fullVerified = $false
foreach ($run in $successful) {
$runID = [string]$run.id
$attempt = [string]$run.run_attempt
if ($runID -notmatch '^[1-9][0-9]*$' -or $attempt -notmatch '^[1-9][0-9]*$') {
throw "Central CI run or attempt identity is invalid"
}
# Partial runs can succeed. The full-only aggregate is successful
# only after every full check passed in this same run attempt.
$jobsEndpoint = "repos/$env:GITHUB_REPOSITORY/actions/runs/$runID" +
"/attempts/$attempt/jobs?per_page=100"
$jobResponse = gh api $jobsEndpoint --paginate --slurp
if ($LASTEXITCODE -ne 0) { throw "Full central CI job lookup failed" }
$jobPages = ($jobResponse | Out-String) | ConvertFrom-Json
$fullJobs = @($jobPages | ForEach-Object { $_.jobs } | Where-Object {
[string]$_.name -ceq "Full CI verification"
})
if ($fullJobs.Count -eq 1 -and
[string]$fullJobs[0].run_id -ceq $runID -and
[string]$fullJobs[0].head_sha -ceq $revision -and
[string]$fullJobs[0].status -ceq "completed" -and
[string]$fullJobs[0].conclusion -ceq "success") {
$fullVerified = $true
break
}
}
if (-not $fullVerified) {
throw "The exact release revision has no successful full central CI; run CI manually for this revision before releasing"
}

- name: Set up pinned Go 1.25.12
Expand Down
85 changes: 71 additions & 14 deletions docs/ci.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,79 @@
# CI checks and execution time

The Go result keeps the name **Go control plane**. It aggregates the
general Go checks and all applicable Store shards. A failed classifier, failed
shard, cancellation, or unexpected skip cannot produce a successful Go result.
PRs and main pushes select checks from their changed inputs. **Go control
plane** aggregates the selected Go checks, Store shards, and authority race
checks. **Full CI verification** is a separate result produced only by a complete
run; releases require that result for their exact source commit.

## What runs

| Change | Central CI | Desktop release workflow |
| --- | --- | --- |
| Push to main | Full checks | Runs for version tags or manual release requests |
| PR containing code, build scripts, workflows, generated contracts, or unknown paths | Full checks | Runs when its existing product-input paths match |
| PR changing only the six explicitly listed documentation files | Module, protocol, Surface and documentation release checks | README.md still triggers archive validation because it is packaged |

The exact documentation allowlist is in `scripts/ci/classify_changes.py`.
An empty, invalid, or unreadable diff is an error, never permission to skip tests.
Other documentation, including migration history and generated protocol files,
continues through full central CI. Main pushes never use the documentation shortcut.
| Documentation | Module, protocol, Surface and documentation contract checks | No PR package build |
| Frontend | Frontend types, API drift, tests, build, dependency audit, and Go tests loading the actual embedded production bundle | No PR package build |
| OpenAPI snapshot | Frontend checks plus the Go DTO-to-OpenAPI golden check | No PR package build |
| Go source or resources | Direct package tests, affected consumer tests or compilation, and relevant integration tests | No PR package build |
| Store or its dependencies | The applicable Go checks plus all eight Store shards | No PR package build |
| LSP, analyzer, browser or native execution inputs | Their corresponding real runtime / platform checks | No package build unless packaging inputs change |
| Packaging / release tooling | Native boundaries and packaging checks | Existing PR package validation |
| Go module dependencies, CI machinery or unclassified inputs | Full checks | Only if release tooling also changes |
| Nightly or manual CI | Full checks, platform matrix, packaging and Go vulnerability audit | Version tags / manual releases still use the release workflow |

The selection rules live in `scripts/ci/classify_changes.py`. Main uses the push
before/after commits, so merging a frontend change does not rerun the entire
backend matrix. Nightly CI runs at 19:23 UTC (03:23 Asia/Hong_Kong). The Actions
page's **CI → Run workflow** starts a full run for a selected branch.

An unreadable or empty change set fails selection. Failed selection, a failed
required job, cancellation, or an unexpected required-job skip cannot become a
successful aggregate result. Unclassified build inputs select full checks.

## Go impact selection

`scripts/ci/run_go_checks.py` uses `go list -json ./...` to identify package
ownership, embedded resources and the real import graph. Production imports
propagate impact; test imports add the corresponding test consumer without
turning a test-only dependency into a production dependency. A change confined
to test files or local testdata stays with its package. Shared fixture paths in
`TEST_INPUT_CONSUMERS` explicitly include their other test consumers; the agent-package launch-handoff
fixture therefore runs both the agent-package and MCP suites. Upstream skill
fixtures also select their snapshot, import, CLI or Store consumers.

Directly changed packages run their complete tests. Indirect consumers also run
their tests except the four large integration packages: `application`, `app`,
`httpapi` and `desktop`. Those compile their tests and run explicitly selected
existing provider, MCP and fixture integration tests where relevant. An affected `httpapi`
package also runs its Go DTO-to-OpenAPI golden check, including when only
`docs/openapi.json` changes. Store runs separately through its existing shards.
Vet covers the entire affected set; subsequent tests disable duplicate automatic vet.

This deliberately reserves the entire large integration suites for direct
changes and full CI. A direct `application` change can still take significantly
longer than a frontend or leaf-package change. A removed package or an input
with no current Go owner selects full checks, including Store and all platforms.

The native jobs retain their real platform checks. On a runtime-only change they
run desktop boundary and tagged adapter tests without producing release archives
or running the second reproducibility build. These adapter tests still build the
renderer because they consume the embedded production assets.

The frontend lane also runs the desktop-tagged `web` package tests on Linux.
These pass the built assets through the same `LoadEmbeddedFS` loader used at
Desktop startup, so invalid production assets fail without a native package
build. Native and packaging checks run that same test alongside the desktop
entry tests. The Rust analyzer lane also runs the Go consumers of the shared
analyzer protocol and archive inventory golden vectors.

## Release verification

Ordinary Go, frontend, documentation and dependency changes no longer launch the
separate Desktop release workflow on every PR. Changes to actual release tools,
packaging scripts, packaging assets and branding still do.

Before a tag or manual release, run full CI for that exact commit (or use a
successful nightly run of the same commit). The release workflow checks a
successful **Full CI verification** job from the same run attempt and commit.
A successful partial run, a PR run, or a skipped full gate is not release evidence.

## Store tests

Expand Down Expand Up @@ -60,6 +118,5 @@ Central CI and PR-only release validation cancel superseded runs. Heavy jobs use
`!cancelled()` so cancellation does not start further work. Tag and manually
requested release runs retain their non-cancelling behavior.

Desktop release PR triggers exclude documentation that is not an archive input.
This is not cross-workflow artifact reuse: product-input changes still run the
existing real archive and reproducibility checks.
PR/push, nightly and manual runs use separate concurrency groups, so a new PR
commit does not cancel a full nightly or manually requested run.
2 changes: 0 additions & 2 deletions internal/releasegate/brand_assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,6 @@ func TestApprovedBrandMasterAndPlatformAssetsStaySynchronized(t *testing.T) {
"scripts/windows-visual-assets.ps1",
"assets/branding/**",
"packaging/macos/**",
"web/public/**",
"web/src/assets/traverse-board-mark.png",
} {
if !strings.Contains(workflow, requiredPath) {
t.Fatalf("Desktop release workflow does not watch %q", requiredPath)
Expand Down
4 changes: 0 additions & 4 deletions internal/releasegate/font_assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,6 @@ func TestApprovedHarmonyOSSansAssetsStayByteIdentical(t *testing.T) {
if !strings.Contains(attributes, "web/src/assets/fonts/*.ttf -text") {
t.Fatal("font binaries are not explicitly protected from Git text conversion")
}
workflow := string(readTestFile(t, filepath.Join(root, ".github", "workflows", "release-desktop.yml")))
if !strings.Contains(workflow, "web/src/assets/fonts/**") || !strings.Contains(workflow, "web/public/**") {
t.Fatal("Desktop release workflow does not watch fonts and their public notices")
}
readme := string(readTestFile(t, filepath.Join(root, "README.md")))
settings := string(readTestFile(t, filepath.Join(root, "web", "src", "v2", "components", "settings.tsx")))
if !strings.Contains(readme, "第三方字体声明") ||
Expand Down
5 changes: 3 additions & 2 deletions internal/releasegate/workflow_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ func TestDesktopReleaseWorkflowWiresEvidenceAndReverifiesBeforePublish(t *testin
"scripts/finalize-windows-release.ps1",
"packaging/windows/RELEASE-NOTES.md",
"Validate Draft Release evidence intake",
"Require successful central CI for the exact release revision",
"The exact release revision has no successful central CI push run",
"Require successful full central CI for the exact release revision",
"Full CI verification",
"The exact release revision has no successful full central CI",
"standard-code-product-e2e.json",
"standard-code-security-evidence.json",
"standard-code-release-gate.json",
Expand Down
2 changes: 1 addition & 1 deletion scripts/build-desktop-darwin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ if [ "$SkipFrontend" != true ]; then
fi

go test ./internal/desktop ./internal/webui -count=1
go test -tags "desktop" ./cmd/cyberagent-desktop -count=1
go test -count=1 -tags "desktop" ./cmd/cyberagent-desktop ./web

revision="$(git rev-parse HEAD)"
if [ -z "$revision" ] || ! printf '%s' "$revision" | grep -Eq '^[0-9a-f]{40}$'; then
Expand Down
2 changes: 1 addition & 1 deletion scripts/build-desktop.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ try {
go test ./internal/desktop ./internal/webui -count=1 `
-skip '^TestWindowsControlPlaneConsumesValidatedLocalSandboxReadiness$'
} "Desktop Go boundary tests failed"
Invoke-Checked { go test -tags "desktop,wv2runtime.error" ./cmd/cyberagent-desktop -count=1 } "Wails adapter tests failed"
Invoke-Checked { go test -count=1 -tags "desktop,wv2runtime.error" ./cmd/cyberagent-desktop ./web } "Wails adapter tests failed"

$revision = (& git rev-parse HEAD).Trim()
if ($LASTEXITCODE -ne 0 -or $revision -notmatch '^[0-9a-f]{40}$') {
Expand Down
Loading
Loading