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
49 changes: 48 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -491,9 +491,56 @@ jobs:
- name: Build native MSI and NSIS installers
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
Remove-Item 'target/release/bundle/msi' -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item 'target/release/bundle/nsis' -Recurse -Force -ErrorAction SilentlyContinue
& .\web\node_modules\.bin\tauri.cmd build --ci --bundles msi,nsis
if ([string]::IsNullOrWhiteSpace($env:RUNNER_TEMP)) { throw 'RUNNER_TEMP is required' }
$runnerTemp = [System.IO.Path]::GetFullPath($env:RUNNER_TEMP)
$configPath = Join-Path $env:RUNNER_TEMP "opentake-windows-ci-$env:GITHUB_RUN_ID-$env:GITHUB_RUN_ATTEMPT.json"
$configPath = [System.IO.Path]::GetFullPath($configPath)
if (-not [System.IO.Path]::IsPathFullyQualified($configPath)) { throw 'Tauri config path is not absolute' }
if (-not [string]::Equals([System.IO.Path]::GetDirectoryName($configPath), $runnerTemp, [System.StringComparison]::OrdinalIgnoreCase)) {
throw 'Tauri config path escaped RUNNER_TEMP'
}
if (Test-Path -LiteralPath $configPath) { throw 'Tauri config path already exists' }
$configJson = '{"bundle":{"createUpdaterArtifacts":false}}'
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
[System.IO.File]::WriteAllText($configPath, $configJson, $utf8NoBom)
$configItem = Get-Item -LiteralPath $configPath -Force
if ($configItem.PSIsContainer -or (($configItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0)) {
throw 'Tauri config must be a regular non-link file'
}
$configBytes = [System.IO.File]::ReadAllBytes($configPath)
if ($configBytes.Length -ne $utf8NoBom.GetByteCount($configJson)) { throw 'Tauri config is not exact UTF-8 without BOM' }
$configText = [System.IO.File]::ReadAllText($configPath, [System.Text.Encoding]::UTF8)
if ($configText -cne $configJson) { throw 'Tauri config content changed' }
$parsedConfig = [System.IO.File]::ReadAllText($configPath, [System.Text.Encoding]::UTF8) | ConvertFrom-Json -AsHashtable
if (@($parsedConfig.Keys).Count -ne 1 -or -not ($parsedConfig.Keys -ccontains 'bundle')) { throw 'Tauri config root is not exact' }
$bundleConfig = $parsedConfig['bundle']
if (-not ($bundleConfig -is [System.Collections.IDictionary]) -or @($bundleConfig.Keys).Count -ne 1 -or -not ($bundleConfig.Keys -ccontains 'createUpdaterArtifacts')) {
throw 'Tauri bundle config is not exact'
}
if (-not ($bundleConfig['createUpdaterArtifacts'] -is [bool]) -or $bundleConfig['createUpdaterArtifacts'] -ne $false) {
throw 'Tauri updater artifact config must be false in product CI'
}
$tauriArguments = @(
'build'
'--ci'
'--bundles'
'msi,nsis'
'--config'
$configPath
)
$tauriExitCode = $null
try {
& .\web\node_modules\.bin\tauri.cmd @tauriArguments
$tauriExitCode = $LASTEXITCODE
} finally {
if (Test-Path -LiteralPath $configPath) {
Remove-Item -LiteralPath $configPath -Force
}
}
if ($tauriExitCode -ne 0) { exit $tauriExitCode }

- name: Install NSIS package and execute installed product without PATH
shell: pwsh
Expand Down
101 changes: 91 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ on:
required: true
type: string
failed_run_id:
description: Failed tag-push Release run ID whose exact source is being recovered
description: Root failed tag-push Release run ID (31412976593) for the immutable source
required: true
type: string
failed_recovery_run_id:
description: Previous failed workflow_dispatch recovery Release run ID (31427093503) chained to the same immutable source
required: true
type: string

Expand All @@ -25,9 +29,13 @@ jobs:
validate:
name: Validate immutable release source
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
env:
RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
FAILED_RUN_ID: ${{ github.event_name == 'workflow_dispatch' && inputs.failed_run_id || '' }}
FAILED_RECOVERY_RUN_ID: ${{ github.event_name == 'workflow_dispatch' && inputs.failed_recovery_run_id || '' }}
RELEASE_TOOLING_SHA: ${{ github.workflow_sha }}
outputs:
source_sha: ${{ steps.bind.outputs.source_sha }}
Expand Down Expand Up @@ -63,15 +71,20 @@ jobs:
read -r remote_main remote_ref < <(git ls-remote --exit-code origin refs/heads/main)
remote_main="$(printf '%s' "$remote_main" | tr '[:upper:]' '[:lower:]')"
test "$remote_ref" = "refs/heads/main"
predecessor_tooling_sha="6162466834bbabb8a16a2c08808e03a53c2b22b6"
if [[ "$GITHUB_EVENT_NAME" = "push" ]]; then
test -z "$FAILED_RUN_ID"
test -z "$FAILED_RECOVERY_RUN_ID"
test "$tooling_sha" = "$source_sha"
if [[ "$source_sha" != "$remote_main" ]]; then
echo "tag commit does not equal current remote main HEAD" >&2
exit 1
fi
elif [[ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]]; then
[[ "$FAILED_RUN_ID" =~ ^[1-9][0-9]*$ ]]
[[ "$FAILED_RECOVERY_RUN_ID" =~ ^[1-9][0-9]*$ ]]
test "$FAILED_RUN_ID" = "31412976593"
test "$FAILED_RECOVERY_RUN_ID" = "31427093503"
test "$tooling_sha" = "$remote_main"
recovery_root="$RUNNER_TEMP/opentake-release-recovery-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
mkdir -p "$recovery_root/tooling"
Expand All @@ -86,20 +99,41 @@ jobs:
test -s "$recovery_root/tooling/check_release_workflow.py"
test -s "$recovery_root/tooling/workflow_yaml.py"
gh api "repos/$GITHUB_REPOSITORY/actions/runs/$FAILED_RUN_ID" \
> "$recovery_root/run.json"
> "$recovery_root/root-run.json"
gh api "repos/$GITHUB_REPOSITORY/actions/runs/$FAILED_RUN_ID/jobs?per_page=100" \
> "$recovery_root/jobs.json"
gh api "repos/$GITHUB_REPOSITORY/compare/$source_sha...$remote_main" \
> "$recovery_root/comparison.json"
> "$recovery_root/root-jobs.json"
gh api "repos/$GITHUB_REPOSITORY/compare/$source_sha...$predecessor_tooling_sha" \
> "$recovery_root/source-to-predecessor.json"
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH="$recovery_root/tooling" \
python3 "$recovery_root/tooling/check_release_workflow.py" \
validate-recovery-run \
--run "$recovery_root/run.json" \
--jobs "$recovery_root/jobs.json" \
--comparison "$recovery_root/comparison.json" \
--run "$recovery_root/root-run.json" \
--jobs "$recovery_root/root-jobs.json" \
--comparison "$recovery_root/source-to-predecessor.json" \
--run-id "$FAILED_RUN_ID" \
--tag "$RELEASE_TAG" \
--sha "$source_sha"
--sha "$source_sha" \
--comparison-head-sha "$predecessor_tooling_sha"
gh api "repos/$GITHUB_REPOSITORY/actions/runs/$FAILED_RECOVERY_RUN_ID" \
> "$recovery_root/predecessor-run.json"
gh api "repos/$GITHUB_REPOSITORY/actions/runs/$FAILED_RECOVERY_RUN_ID/jobs?per_page=100" \
> "$recovery_root/predecessor-jobs.json"
gh api "repos/$GITHUB_REPOSITORY/actions/runs/$FAILED_RECOVERY_RUN_ID/artifacts?per_page=100" \
> "$recovery_root/predecessor-artifacts.json"
gh api "repos/$GITHUB_REPOSITORY/compare/$predecessor_tooling_sha...$remote_main" \
> "$recovery_root/predecessor-to-current.json"
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH="$recovery_root/tooling" \
python3 "$recovery_root/tooling/check_release_workflow.py" \
validate-failed-recovery-run \
--run "$recovery_root/predecessor-run.json" \
--jobs "$recovery_root/predecessor-jobs.json" \
--artifacts "$recovery_root/predecessor-artifacts.json" \
--tooling-comparison "$recovery_root/predecessor-to-current.json" \
--run-id "$FAILED_RECOVERY_RUN_ID" \
--tag "$RELEASE_TAG" \
--source-sha "$source_sha" \
--tooling-sha "$predecessor_tooling_sha" \
--current-tooling-sha "$tooling_sha"
else
echo "unsupported release event: $GITHUB_EVENT_NAME" >&2
exit 1
Expand Down Expand Up @@ -750,9 +784,56 @@ jobs:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
$ErrorActionPreference = 'Stop'
Remove-Item 'target/release/bundle/msi' -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item 'target/release/bundle/nsis' -Recurse -Force -ErrorAction SilentlyContinue
& .\web\node_modules\.bin\tauri.cmd build --ci --bundles msi,nsis --config '{"bundle":{"createUpdaterArtifacts":true}}'
if ([string]::IsNullOrWhiteSpace($env:RUNNER_TEMP)) { throw 'RUNNER_TEMP is required' }
$runnerTemp = [System.IO.Path]::GetFullPath($env:RUNNER_TEMP)
$configPath = Join-Path $env:RUNNER_TEMP "opentake-windows-updater-$env:GITHUB_RUN_ID-$env:GITHUB_RUN_ATTEMPT.json"
$configPath = [System.IO.Path]::GetFullPath($configPath)
if (-not [System.IO.Path]::IsPathFullyQualified($configPath)) { throw 'Tauri config path is not absolute' }
if (-not [string]::Equals([System.IO.Path]::GetDirectoryName($configPath), $runnerTemp, [System.StringComparison]::OrdinalIgnoreCase)) {
throw 'Tauri config path escaped RUNNER_TEMP'
}
if (Test-Path -LiteralPath $configPath) { throw 'Tauri config path already exists' }
$configJson = '{"bundle":{"createUpdaterArtifacts":true}}'
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
[System.IO.File]::WriteAllText($configPath, $configJson, $utf8NoBom)
$configItem = Get-Item -LiteralPath $configPath -Force
if ($configItem.PSIsContainer -or (($configItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0)) {
throw 'Tauri config must be a regular non-link file'
}
$configBytes = [System.IO.File]::ReadAllBytes($configPath)
if ($configBytes.Length -ne $utf8NoBom.GetByteCount($configJson)) { throw 'Tauri config is not exact UTF-8 without BOM' }
$configText = [System.IO.File]::ReadAllText($configPath, [System.Text.Encoding]::UTF8)
if ($configText -cne $configJson) { throw 'Tauri config content changed' }
$parsedConfig = [System.IO.File]::ReadAllText($configPath, [System.Text.Encoding]::UTF8) | ConvertFrom-Json -AsHashtable
if (@($parsedConfig.Keys).Count -ne 1 -or -not ($parsedConfig.Keys -ccontains 'bundle')) { throw 'Tauri config root is not exact' }
$bundleConfig = $parsedConfig['bundle']
if (-not ($bundleConfig -is [System.Collections.IDictionary]) -or @($bundleConfig.Keys).Count -ne 1 -or -not ($bundleConfig.Keys -ccontains 'createUpdaterArtifacts')) {
throw 'Tauri bundle config is not exact'
}
if (-not ($bundleConfig['createUpdaterArtifacts'] -is [bool]) -or $bundleConfig['createUpdaterArtifacts'] -ne $true) {
throw 'Tauri updater artifact config must be true'
}
$tauriArguments = @(
'build'
'--ci'
'--bundles'
'msi,nsis'
'--config'
$configPath
)
$tauriExitCode = $null
try {
& .\web\node_modules\.bin\tauri.cmd @tauriArguments
$tauriExitCode = $LASTEXITCODE
} finally {
if (Test-Path -LiteralPath $configPath) {
Remove-Item -LiteralPath $configPath -Force
}
}
if ($tauriExitCode -ne 0) { exit $tauriExitCode }

- name: Install NSIS and smoke installed app, sidecars, and updater artifacts
shell: pwsh
Expand Down
23 changes: 16 additions & 7 deletions docs/releases/1.0.0-beta.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,29 @@
`.github/workflows/release.yml` 的正常 tag push 只接受 `v1.0.0-beta.4` 这类 `v<semver>` tag;
此时 product source SHA 与 release tooling SHA 必须相同,并等于当前远端 `main` HEAD。若这个
tag push 已通过 source、质量与 macOS 门禁,却仅在 Windows 的 checksum-pinned FFmpeg sidecar
provision 步骤失败,则允许用显式 `tag` 与 `failed_run_id` 发起一次 `workflow_dispatch` 恢复:
product source 仍是原不可变 tag SHA,release tooling 则固定为当前 `main` 的
provision 步骤失败,则该 `workflow_dispatch` 恢复链以 root `failed_run_id`(固定为
`failed_run_id=31412976593`)绑定原 tag-push run。首次恢复
run `31427093503` 又仅在 Windows 原生构建的 PowerShell → `.cmd` → Tauri config argv 边界失败;
后续一次性恢复必须同时显式提供 `failed_run_id=31412976593` 与
`failed_recovery_run_id=31427093503`,缺少或替换任一 ID 都 fail closed。product source 仍是原不可变
tag SHA `2c4efdff9d2587c90cbcac0919f9d1d333d67d6a`;上一恢复的 release tooling 固定为
`6162466834bbabb8a16a2c08808e03a53c2b22b6`,当前 release tooling 则固定为当前远端 `main` 的
`github.workflow_sha`,并从该 Git commit 的 blob 精确提取,绝不从 raw HTTP URL 下载执行。
两条路径都不创建、移动或删除 tag。Cargo、Tauri 与 Web 版本均为 `1.0.0-beta.4`,Windows WiX
安装器版本为 `1.0.0.4`,并由独立发布合约 fail closed 校验。

1. validate job 解析 immutable source SHA;恢复路径还会逐项核对原 run 的五个 job 与 Windows
失败 step,并证明 source 是当前 `main` 的祖先。随后验证版本、WiX、本文档、prerelease 语义
和洁净 checkout;质量门禁执行依赖锁定安装、audit、格式、clippy、workspace/Web 测试及
workflow/tooling commit 合同。
1. validate job 解析 immutable source SHA;恢复路径先逐项核对 root run 的五个 job 与 Windows
sidecar 失败 step,再独立核对上一恢复 run 的五个 job、Windows config argv 失败 step 以及唯一
macOS artifact 的 ID、大小、digest、未过期状态与 run/repository/source 绑定。两个 GitHub compare
响应必须分别证明 source → `61624668…` 与 `61624668…` → 当前远端 `main`,不接受互换、跳段或
任意失败 run。随后验证版本、WiX、本文档、prerelease 语义和洁净 checkout;质量门禁执行依赖
锁定安装、audit、格式、clippy、workspace/Web 测试及 workflow/tooling commit 合同。
2. macOS ARM64 和 Windows x64 都要求非空 `TAURI_SIGNING_PRIVATE_KEY` 与
`TAURI_SIGNING_PRIVATE_KEY_PASSWORD`。构建产生平台安装包、Tauri updater package、每个
companion signature 与签名 attestation;macOS app 仍是 ad-hoc 签名,Windows 安装器仍不是
Authenticode 签名。
Authenticode 签名。Windows release 与 full-product CI 都在 `RUNNER_TEMP` 写入 UTF-8 无 BOM、
非链接 regular-file Tauri config,并把绝对路径作为单独 `--config` argv;release 精确开启 updater
artifacts,CI 精确关闭它以避免依赖发布签名 secrets。
3. publish 从 exact SHA 的配置读取内置 updater 公钥,用独立 Minisign 工具验证 package 与
attestation;manifest 将版本、tag、source SHA、平台、资产名称、大小和 SHA-256 绑定。只有
draft 的十七项精确资产上传、下载回读、名称/大小、签名和 `SHA256SUMS` 全部相符后才公开。
Expand Down
Loading
Loading