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
163 changes: 152 additions & 11 deletions .github/workflows/release-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,28 @@ on:
paths:
- '.github/workflows/release-desktop.yml'
- 'cmd/packagede2e/**'
- 'cmd/producte2e/**'
- 'cmd/releasegate/**'
- 'cmd/releasegen/**'
- 'internal/packagede2e/**'
- 'internal/producte2e/**'
- 'internal/releasegate/**'
- 'scripts/build-desktop.ps1'
- 'scripts/check-windows-compat.ps1'
- 'scripts/package-portable-zip.ps1'
- 'scripts/release-desktop.ps1'
- 'scripts/smoke-desktop-operator-preview.ps1'
- 'scripts/standard-code-packaged-e2e.ps1'
- 'scripts/standard-code-product-e2e.ps1'
- 'scripts/verify-portable-zip.ps1'
- 'docs/standard-code-packaged-e2e.md'
- 'docs/standard-code-product-e2e.md'
- 'docs/standard-code-release-gate.md'
- 'docs/adr/0138-standard-code-packaged-e2e-foundation.md'
- 'docs/adr/0144-standard-code-release-gate-aggregation.md'
- 'docs/convergence/protocol-registry.md'
- 'packaging/windows/**'
- 'protocols/registry.json'
- 'go.mod'
- 'go.sum'
- 'web/package.json'
Expand All @@ -33,11 +43,13 @@ on:
- 'analyzers/rust-toolchain.toml'
- 'internal/analyzer/embedded/**'
- 'README.md'
- 'README.en.md'
push:
tags:
- 'v*'

permissions:
actions: read
contents: read

concurrency:
Expand All @@ -54,6 +66,31 @@ jobs:
with:
fetch-depth: 0

- name: Require successful central CI for the exact release revision
if: ${{ github.event_name != 'pull_request' }}
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
$revision = (git rev-parse HEAD).Trim()
if ($LASTEXITCODE -ne 0 -or $revision -notmatch '^[0-9a-f]{40}$') {
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
if ($LASTEXITCODE -ne 0) { throw "Central CI provenance lookup failed" }
$runs = ($response | Out-String) | ConvertFrom-Json
$successful = @($runs.workflow_runs | Where-Object {
[string]$_.head_sha -ceq $revision -and
[string]$_.event -ceq "push" -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"
}

- name: Set up pinned Go 1.25.12
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
Expand Down Expand Up @@ -131,14 +168,17 @@ jobs:
foreach ($part in @($Matches.major, $Matches.minor, $Matches.patch)) {
if ([int64]$part -gt 65535) { throw "MSIX release version component is too large" }
}
if ($env:EVENT_NAME -ne "pull_request" -and -not $version.Contains("-")) {
throw "Standard Code Beta releases must use a prerelease version"
}
$msixVersion = "$($Matches.major).$($Matches.minor).$($Matches.patch).0"
$revision = (git rev-parse HEAD).Trim()
if ($LASTEXITCODE -ne 0 -or $revision -notmatch '^[0-9a-f]{40}$') {
throw "Release revision is invalid"
}
if ($env:EVENT_NAME -eq "push") {
if ($env:EVENT_NAME -ne "pull_request") {
git merge-base --is-ancestor $revision origin/main
if ($LASTEXITCODE -ne 0) { throw "Release tag is not reachable from origin/main" }
if ($LASTEXITCODE -ne 0) { throw "Release revision is not reachable from origin/main" }
}
$artifactVersion = $version -replace '[^0-9A-Za-z._-]', '_'
"version=$version" >> $env:GITHUB_OUTPUT
Expand All @@ -163,6 +203,9 @@ jobs:
shell: pwsh
run: rustup toolchain install 1.97.1 --profile minimal

- name: Verify fail-closed Standard Code release-gate contract
run: go test -count=1 ./cmd/releasegate ./internal/releasegate ./internal/producte2e ./internal/packagede2e

- name: Build Desktop twice and compare bytes
shell: pwsh
run: ./scripts/build-desktop.ps1 -Version '${{ steps.release.outputs.version }}' -VerifyReproducible
Expand All @@ -179,13 +222,72 @@ jobs:
-ExpectedRevision '${{ steps.release.outputs.revision }}' `
-SmokeTest

- name: Exercise packaged Standard Code bootstrap and prepare attack matrix
- name: Validate Draft Release evidence intake
if: ${{ github.event_name != 'pull_request' }}
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
RELEASE_VERSION: ${{ steps.release.outputs.version }}
run: |
$release = gh release view $env:RELEASE_VERSION `
--repo $env:GITHUB_REPOSITORY `
--json isDraft,tagName,assets | ConvertFrom-Json
if ($LASTEXITCODE -ne 0 -or -not [bool]$release.isDraft -or
[string]$release.tagName -cne $env:RELEASE_VERSION) {
throw "A matching Draft Release is required for candidate-bound evidence intake"
}
$required = @(
"standard-code-product-e2e.json",
"standard-code-security-evidence.json"
)
$assets = @($release.assets)
foreach ($name in $required) {
$matches = @($assets | Where-Object { [string]$_.name -ceq $name })
if ($matches.Count -ne 1 -or [int64]$matches[0].size -le 0 -or
[int64]$matches[0].size -gt 4194304) {
throw "Draft Release evidence asset is missing, duplicated, empty, or oversized: $name"
}
}
foreach ($name in $required) {
$target = Join-Path "build/desktop" $name
if (Test-Path -LiteralPath $target) {
throw "Evidence intake target already exists: $name"
}
}
gh release download $env:RELEASE_VERSION `
--repo $env:GITHUB_REPOSITORY `
--pattern 'standard-code-product-e2e.json' `
--pattern 'standard-code-security-evidence.json' `
--dir build/desktop
if ($LASTEXITCODE -ne 0) { throw "Draft Release evidence download failed" }
foreach ($name in $required) {
$item = Get-Item -LiteralPath (Join-Path "build/desktop" $name) -Force
if (-not $item.PSIsContainer -and
-not ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -and
$item.Length -gt 0 -and $item.Length -le 4194304) { continue }
throw "Downloaded evidence asset is unsafe: $name"
}

- name: Exercise packaged Standard Code bootstrap
if: ${{ github.event_name == 'pull_request' }}
shell: pwsh
run: |
./scripts/standard-code-packaged-e2e.ps1 `
-ExpectedVersion '${{ steps.release.outputs.version }}' `
-ExpectedRevision '${{ steps.release.outputs.revision }}'

- name: Aggregate candidate-bound Standard Code release evidence
if: ${{ github.event_name != 'pull_request' }}
shell: pwsh
run: |
./scripts/standard-code-packaged-e2e.ps1 `
-ExpectedVersion '${{ steps.release.outputs.version }}' `
-ExpectedRevision '${{ steps.release.outputs.revision }}' `
-ProductReportPath 'build/desktop/standard-code-product-e2e.json' `
-SecurityReportPath 'build/desktop/standard-code-security-evidence.json' `
-AggregateOutputPath 'build/desktop/standard-code-release-gate.json' `
-RequireReleaseGate

- name: Package unsigned per-user MSIX candidate
shell: pwsh
run: ./scripts/package-msix.ps1 -Version '${{ steps.release.outputs.msix_version }}'
Expand All @@ -206,6 +308,9 @@ jobs:
build/desktop/release-metadata.json
build/desktop/windows-compatibility.json
build/desktop/standard-code-packaged-e2e.json
build/desktop/standard-code-product-e2e.json
build/desktop/standard-code-security-evidence.json
build/desktop/standard-code-release-gate.json
build/desktop/sbom.json
build/desktop/NOTICE

Expand Down Expand Up @@ -236,13 +341,37 @@ jobs:
--name '${{ needs.portable-zip.outputs.artifact_name }}' `
--dir build/desktop

- name: Set up pinned Go 1.25.12
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: '1.25.12'
cache: true

- name: Reverify downloaded candidate
shell: pwsh
run: |
./scripts/verify-portable-zip.ps1 `
-ExpectedVersion '${{ needs.portable-zip.outputs.version }}' `
-ExpectedRevision '${{ needs.portable-zip.outputs.revision }}'

- name: Reverify aggregate Standard Code release gate
shell: pwsh
run: |
$manifest = Get-Content -LiteralPath build/desktop/portable-zip-manifest.json `
-Raw | ConvertFrom-Json
$archive = Join-Path build/desktop ([string]$manifest.zip_name)
go run ./cmd/releasegate `
--binary build/desktop/TraverseBoard.exe `
--archive $archive `
--portable-manifest build/desktop/portable-zip-manifest.json `
--release-metadata build/desktop/release-metadata.json `
--bootstrap build/desktop/standard-code-packaged-e2e.json `
--product build/desktop/standard-code-product-e2e.json `
--security build/desktop/standard-code-security-evidence.json `
--expected-revision '${{ needs.portable-zip.outputs.revision }}' `
--verify-report build/desktop/standard-code-release-gate.json
if ($LASTEXITCODE -ne 0) { throw "Aggregate Standard Code release gate revalidation failed" }

- name: Sign MSIX when the protected certificate is configured
if: ${{ env.MSIX_CERTIFICATE_BASE64 != '' }}
shell: pwsh
Expand Down Expand Up @@ -276,6 +405,9 @@ jobs:
"build/desktop/release-metadata.json",
"build/desktop/windows-compatibility.json",
"build/desktop/standard-code-packaged-e2e.json",
"build/desktop/standard-code-product-e2e.json",
"build/desktop/standard-code-security-evidence.json",
"build/desktop/standard-code-release-gate.json",
"build/desktop/sbom.json",
"build/desktop/NOTICE"
)
Expand All @@ -289,14 +421,23 @@ jobs:
throw "Release asset is missing: $asset"
}
}
$arguments = @(
"release", "create", $env:RELEASE_VERSION
) + $assets + @(
$release = gh release view $env:RELEASE_VERSION `
--repo $env:GITHUB_REPOSITORY --json isDraft,tagName | ConvertFrom-Json
if ($LASTEXITCODE -ne 0 -or -not [bool]$release.isDraft -or
[string]$release.tagName -cne $env:RELEASE_VERSION) {
throw "The candidate evidence Draft Release is no longer available"
}
$upload = @("release", "upload", $env:RELEASE_VERSION) + $assets + @(
"--repo", $env:GITHUB_REPOSITORY, "--clobber"
)
& gh @upload
if ($LASTEXITCODE -ne 0) { throw "Verified release asset upload failed" }
$publish = @(
"release", "edit", $env:RELEASE_VERSION,
"--repo", $env:GITHUB_REPOSITORY,
"--verify-tag",
"--generate-notes",
"--verify-tag", "--draft=false",
"--prerelease",
"--title", "Traverse Board Desktop $env:RELEASE_VERSION"
)
if ($env:RELEASE_VERSION.Contains("-")) { $arguments += "--prerelease" }
& gh @arguments
if ($LASTEXITCODE -ne 0) { throw "GitHub Release creation failed" }
& gh @publish
if ($LASTEXITCODE -ne 0) { throw "Verified Draft Release publication failed" }
2 changes: 2 additions & 0 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,8 @@ Local Ollama is the only keyless Provider and enables only when `CYBERAGENT_OLLA

Use the operator-preview launcher. Opening the bare `TraverseBoard.exe` intentionally starts with the most conservative permissions. See [`packaging/windows/LOCAL-TEST-GUIDE.txt`](packaging/windows/LOCAL-TEST-GUIDE.txt) for the full manual test flow.

The `Desktop release` workflow keeps pull-request evidence honest: its packaged bootstrap remains `needs_full_matrix`. A non-PR Beta candidate must obtain the independently produced #182 product report and #181 security report from the matching Draft Release, bind both reports to the current EXE, ZIP, source revision, fixture, and attack matrix in `standard_code_release_gate.v1`, and reverify that aggregate before publication. Missing, tampered, replayed, skipped, or waived evidence fails closed. Only versions with a prerelease identifier (`-`) can be published, and signing/Microsoft Store distribution remain separate. See the [Beta release gate](docs/standard-code-release-gate.md) and [ADR 0144](docs/adr/0144-standard-code-release-gate-aggregation.md).

> [!NOTE]
> Portable means install-free application files, not directory-local data. The default data home remains `%USERPROFILE%\.cyberagent-workbench`, so extracting another ZIP reuses the same database. `v0.1.0-rc.2` cannot upgrade one exact pre-final Windows preview v97 history; schema v125 in later builds accepts that exact history and transactionally restores the canonical trigger without deleting or falsifying old data. See [ADR 0126](docs/adr/0126-legacy-v97-docker-trigger-compatibility.md).

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,9 +356,9 @@ pwsh -NoProfile -File scripts/release-desktop.ps1 -Version v0.1.0-rc.2

Canonical release CI pins Go 1.25.12, Node 24.16.0 (including its bundled npm), and Rust 1.97.1; `release-metadata.json` additionally binds those observed versions plus the Go, npm, Cargo, and embedded-analyzer hashes. 本地命令会记录实际工具链,正式 GitHub Release 只采用上述 CI 固定版本。

`Desktop release` 工作流会在 PR 中重跑依赖边界、可复现构建、ZIP 完整性验证,以及四个固定 Go/Node.js/Python/Rust 仓库的 fail/repair/pass oracle;它再从确切 ZIP 启动默认模式与安全 operator preview,验证强杀重开、无宿主监听、固件不变和合成 credential sentinel 不落盘。这个 bootstrap 成功后仍如实报告 `needs_full_matrix`;40 项 Local/Docker/Approval/恢复攻击未全部取得不可变证据前,不能作为 #140 Beta gate 通过。完整合同见 [Standard Code packaged E2E](docs/standard-code-packaged-e2e.md) 与 [ADR 0138](docs/adr/0138-standard-code-packaged-e2e-foundation.md)。`workflow_dispatch` 只保留 Actions artifact,只有可追溯到 `main` 的 `v*` tag 才会创建 GitHub Release。带 `-` 的版本会发布为 prerelease。
`Desktop release` 工作流会在 PR 中重跑依赖边界、可复现构建、ZIP 完整性验证,以及四个固定 Go/Node.js/Python/Rust 仓库的 fail/repair/pass oracle;它再从确切 ZIP 启动默认模式与安全 operator preview,验证强杀重开、无宿主监听、固件不变和合成 credential sentinel 不落盘。这个 bootstrap 成功后仍如实报告 `needs_full_matrix`。非 PR 的 release run 还必须从同版本 Draft Release 取得 #182 产品报告与 #181 安全报告,并把两者的 self-hash、40 case/75 backend run、四语言交付、Windows 10/11 UX 证据与当前 EXE/ZIP/revision 聚合为 `standard_code_release_gate.v1`;缺失、篡改、跨候选 replay、skip 或 waiver 都会阻断发布。完整合同见 [Standard Code packaged E2E](docs/standard-code-packaged-e2e.md)、[Beta release gate](docs/standard-code-release-gate.md) 与 [ADR 0144](docs/adr/0144-standard-code-release-gate-aggregation.md)。`workflow_dispatch` 只保留 Actions artifact;只有通过 aggregate 且版本包含 prerelease 标识(`-`)的 `v*` tag 才会把原 Draft 发布为 GitHub Pre-release。

The `Desktop release` workflow reruns dependency boundaries, the reproducible build, ZIP integrity verification, and the fail/repair/pass oracle for four fixed Go, Node.js, Python, and Rust repositories. It then launches conservative default mode and safe operator preview from the exact ZIP and checks kill/reopen, absence of a host listener, fixture immutability, and synthetic credential-sentinel non-persistence. A successful bootstrap still reports `needs_full_matrix`; it is not the Issue #140 Beta gate until all 40 Local, Docker, Approval, and recovery attacks have immutable evidence. See [Standard Code packaged E2E](docs/standard-code-packaged-e2e.md) and [ADR 0138](docs/adr/0138-standard-code-packaged-e2e-foundation.md). `workflow_dispatch` keeps only an Actions artifact; only a `v*` tag reachable from `main` creates a GitHub Release, and versions containing `-` are prereleases.
The `Desktop release` workflow reruns dependency boundaries, the reproducible build, ZIP integrity verification, and the fail/repair/pass oracle for four fixed Go, Node.js, Python, and Rust repositories. It then launches conservative default mode and safe operator preview from the exact ZIP and checks kill/reopen, absence of a host listener, fixture immutability, and synthetic credential-sentinel non-persistence. A successful bootstrap still reports `needs_full_matrix`. A non-PR release run must additionally obtain the #182 product report and #181 security report from the matching Draft Release, then bind their self-hashes, 40 cases/75 backend runs, four-language delivery, Windows 10/11 UX evidence, and the current EXE/ZIP/revision into `standard_code_release_gate.v1`. Missing, tampered, replayed, skipped, or waived evidence blocks publication. See [Standard Code packaged E2E](docs/standard-code-packaged-e2e.md), the [Beta release gate](docs/standard-code-release-gate.md), and [ADR 0144](docs/adr/0144-standard-code-release-gate-aggregation.md). `workflow_dispatch` keeps an Actions artifact only; only a `v*` tag containing a prerelease identifier (`-`) can publish the existing Draft as a GitHub Pre-release after the aggregate passes.

**SmartScreen 预期 / SmartScreen expectations**:便携 ZIP 与 EXE 均未签名,Windows SmartScreen 可能提示“未知发布者”。这是未签名候选的预期限制,不是构建缺陷;正式签名发行(MSIX)在另一条发布线完成。The ZIP and EXE are unsigned, so Windows SmartScreen may warn about an unknown publisher. This is the expected limitation of an unsigned candidate, not a build defect; the signed MSIX release is tracked separately.

Expand Down
Loading