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
149 changes: 142 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ on:
description: Existing v<semver> tag from a failed run; this workflow never creates or moves tags
required: true
type: string
failed_run_id:
description: Failed tag-push Release run ID whose exact source is being recovered
required: true
type: string

permissions:
contents: read
Expand All @@ -23,8 +27,11 @@ jobs:
runs-on: ubuntu-latest
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 || '' }}
RELEASE_TOOLING_SHA: ${{ github.workflow_sha }}
outputs:
source_sha: ${{ steps.bind.outputs.source_sha }}
tooling_sha: ${{ steps.bind.outputs.tooling_sha }}
tag: ${{ steps.bind.outputs.tag }}
version: ${{ steps.bind.outputs.version }}
notes_path: ${{ steps.bind.outputs.notes_path }}
Expand All @@ -38,6 +45,8 @@ jobs:

- name: Validate tag, source SHA, versions, and notes
id: bind
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
Expand All @@ -49,15 +58,55 @@ jobs:
git cat-file -e "${source_sha}^{commit}"
test -z "$(git status --porcelain=v1 --untracked-files=all)"

tooling_sha="$(printf '%s' "$RELEASE_TOOLING_SHA" | tr '[:upper:]' '[:lower:]')"
[[ "$tooling_sha" =~ ^[0-9a-f]{40}$ ]]
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"
if [[ "$source_sha" != "$remote_main" ]]; then
echo "tag commit does not equal current remote main HEAD" >&2
if [[ "$GITHUB_EVENT_NAME" = "push" ]]; then
test -z "$FAILED_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]*$ ]]
test "$tooling_sha" = "$remote_main"
recovery_root="$RUNNER_TEMP/opentake-release-recovery-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
mkdir -p "$recovery_root/tooling"
if ! git cat-file -e "${tooling_sha}^{commit}" 2>/dev/null; then
git fetch --no-tags --depth=1 origin "$tooling_sha"
fi
test "$(git rev-parse "${tooling_sha}^{commit}")" = "$tooling_sha"
git cat-file blob "$tooling_sha:scripts/check_release_workflow.py" \
> "$recovery_root/tooling/check_release_workflow.py"
git cat-file blob "$tooling_sha:scripts/workflow_yaml.py" \
> "$recovery_root/tooling/workflow_yaml.py"
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"
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"
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-id "$FAILED_RUN_ID" \
--tag "$RELEASE_TAG" \
--sha "$source_sha"
else
echo "unsupported release event: $GITHUB_EVENT_NAME" >&2
exit 1
fi

printf 'source_sha=%s\n' "$source_sha" >> "$GITHUB_OUTPUT"
printf 'tooling_sha=%s\n' "$tooling_sha" >> "$GITHUB_OUTPUT"
RELEASE_TAG="$RELEASE_TAG" python3 - <<'PY'
import json
import os
Expand Down Expand Up @@ -131,6 +180,7 @@ jobs:
timeout-minutes: 120
env:
TARGET_SHA: ${{ needs.validate.outputs.source_sha }}
RELEASE_TOOLING_SHA: ${{ needs.validate.outputs.tooling_sha }}
CI: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
Expand Down Expand Up @@ -214,15 +264,60 @@ jobs:

- name: Validate Windows and release workflow contracts
run: |
set -euo pipefail
python3 -B scripts/check_windows_product_ci.py
python3 -B -m unittest discover -s scripts -p 'test_check_windows_product_ci.py'
python3 -B scripts/check_release_workflow.py
python3 -B -m unittest discover -s scripts -p 'test_check_release_workflow.py'
python3 -B -m unittest discover -s scripts -p 'test_write_updater_attestation.py'
python3 -B -m unittest discover -s scripts -p 'test_write_updater_manifest.py'
tooling_root="$RUNNER_TEMP/opentake-release-tooling-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
mkdir -p "$tooling_root"
if ! git cat-file -e "${RELEASE_TOOLING_SHA}^{commit}" 2>/dev/null; then
git fetch --no-tags --depth=1 origin "$RELEASE_TOOLING_SHA"
fi
test "$(git rev-parse "${RELEASE_TOOLING_SHA}^{commit}")" = "$RELEASE_TOOLING_SHA"
git cat-file blob "$RELEASE_TOOLING_SHA:scripts/check_release_workflow.py" \
> "$tooling_root/check_release_workflow.py"
git cat-file blob "$RELEASE_TOOLING_SHA:scripts/test_check_release_workflow.py" \
> "$tooling_root/test_check_release_workflow.py"
git cat-file blob "$RELEASE_TOOLING_SHA:scripts/workflow_yaml.py" \
> "$tooling_root/workflow_yaml.py"
git cat-file blob "$RELEASE_TOOLING_SHA:scripts/provision_ffmpeg_sidecars.py" \
> "$tooling_root/provision_ffmpeg_sidecars.py"
git cat-file blob "$RELEASE_TOOLING_SHA:scripts/tests/test_provision_ffmpeg_sidecars.py" \
> "$tooling_root/test_provision_ffmpeg_sidecars.py"
git cat-file blob "$RELEASE_TOOLING_SHA:.github/workflows/release.yml" \
> "$tooling_root/release.yml"
git cat-file blob "$RELEASE_TOOLING_SHA:docs/releases/1.0.0-beta.4.md" \
> "$tooling_root/release-notes.md"
test -s "$tooling_root/check_release_workflow.py"
test -s "$tooling_root/test_check_release_workflow.py"
test -s "$tooling_root/workflow_yaml.py"
test -s "$tooling_root/provision_ffmpeg_sidecars.py"
test -s "$tooling_root/test_provision_ffmpeg_sidecars.py"
test -s "$tooling_root/release.yml"
test -s "$tooling_root/release-notes.md"
OPENTAKE_REPOSITORY_ROOT="$GITHUB_WORKSPACE" \
OPENTAKE_RELEASE_WORKFLOW_PATH="$tooling_root/release.yml" \
OPENTAKE_RELEASE_NOTES_PATH="$tooling_root/release-notes.md" \
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH="$tooling_root" \
python3 -B "$tooling_root/check_release_workflow.py"
OPENTAKE_REPOSITORY_ROOT="$GITHUB_WORKSPACE" \
OPENTAKE_RELEASE_WORKFLOW_PATH="$tooling_root/release.yml" \
OPENTAKE_RELEASE_NOTES_PATH="$tooling_root/release-notes.md" \
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH="$tooling_root" \
python3 -B -m unittest discover -s "$tooling_root" \
-p 'test_check_release_workflow.py'

- name: Provisioner unit tests
run: python3 -B -m unittest discover -s scripts/tests -p 'test_*.py'
run: |
set -euo pipefail
python3 -B -m unittest discover -s scripts/tests -p 'test_*.py'
tooling_root="$RUNNER_TEMP/opentake-release-tooling-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
OPENTAKE_REPOSITORY_ROOT="$GITHUB_WORKSPACE" \
OPENTAKE_PROVISIONER_PATH="$tooling_root/provision_ffmpeg_sidecars.py" \
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH="$tooling_root" \
python3 -B -m unittest discover \
-s "$tooling_root" -p 'test_provision_ffmpeg_sidecars.py'

- name: Install locked Web dependencies
run: pnpm -C web install --frozen-lockfile
Expand Down Expand Up @@ -527,6 +622,7 @@ jobs:
timeout-minutes: 120
env:
TARGET_SHA: ${{ needs.validate.outputs.source_sha }}
RELEASE_TOOLING_SHA: ${{ needs.validate.outputs.tooling_sha }}
RELEASE_TAG: ${{ needs.validate.outputs.tag }}
RELEASE_VERSION: ${{ needs.validate.outputs.version }}
CI: true
Expand Down Expand Up @@ -575,7 +671,30 @@ jobs:
ruby-version: '3.3'

- name: Provision checksum-pinned Windows FFmpeg sidecars
run: python scripts/provision_ffmpeg_sidecars.py --target x86_64-pc-windows-msvc
shell: pwsh
run: |
if ($env:RELEASE_TOOLING_SHA -notmatch '^[0-9a-f]{40}$') {
throw 'release tooling SHA must be lowercase 40-hex'
}
$toolingRoot = Join-Path $env:RUNNER_TEMP "opentake-sidecar-tooling-$env:GITHUB_RUN_ID-$env:GITHUB_RUN_ATTEMPT"
New-Item -ItemType Directory -Force -Path $toolingRoot | Out-Null
$provisioner = Join-Path $toolingRoot 'provision_ffmpeg_sidecars.py'
git cat-file -e "$($env:RELEASE_TOOLING_SHA)^{commit}" 2>$null
if ($LASTEXITCODE -ne 0) {
git fetch --no-tags --depth=1 origin $env:RELEASE_TOOLING_SHA
if ($LASTEXITCODE -ne 0) { throw 'failed to fetch exact release tooling commit' }
}
$resolvedTooling = (git rev-parse "$($env:RELEASE_TOOLING_SHA)^{commit}").Trim().ToLowerInvariant()
if ($LASTEXITCODE -ne 0 -or $resolvedTooling -ne $env:RELEASE_TOOLING_SHA) {
throw 'release tooling commit did not resolve exactly'
}
git cat-file blob "$($env:RELEASE_TOOLING_SHA):scripts/provision_ffmpeg_sidecars.py" > $provisioner
if ($LASTEXITCODE -ne 0) { throw 'failed to extract exact sidecar provisioner blob' }
if (-not (Test-Path $provisioner -PathType Leaf) -or (Get-Item $provisioner).Length -le 0) {
throw 'release sidecar provisioner blob is missing or empty'
}
$env:OPENTAKE_REPOSITORY_ROOT = $env:GITHUB_WORKSPACE
python $provisioner --target x86_64-pc-windows-msvc

- name: Verify pinned sidecar supply
shell: bash
Expand Down Expand Up @@ -806,6 +925,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.validate.outputs.tag }}
RELEASE_SHA: ${{ needs.validate.outputs.source_sha }}
RELEASE_TOOLING_SHA: ${{ needs.validate.outputs.tooling_sha }}
RELEASE_VERSION: ${{ needs.validate.outputs.version }}
NOTES_PATH: ${{ needs.validate.outputs.notes_path }}
PYTHONDONTWRITEBYTECODE: '1'
Expand Down Expand Up @@ -1317,12 +1437,27 @@ jobs:
shell: bash
run: |
set -euo pipefail
cp "$NOTES_PATH" "$PUBLISH_ROOT/release-body.md"
notes_sha="$RELEASE_SHA"
if [[ "$RELEASE_TOOLING_SHA" = "$RELEASE_SHA" ]]; then
cp "$NOTES_PATH" "$PUBLISH_ROOT/release-body.md"
else
[[ "$RELEASE_TOOLING_SHA" =~ ^[0-9a-f]{40}$ ]]
if ! git cat-file -e "${RELEASE_TOOLING_SHA}^{commit}" 2>/dev/null; then
git fetch --no-tags --depth=1 origin "$RELEASE_TOOLING_SHA"
fi
test "$(git rev-parse "${RELEASE_TOOLING_SHA}^{commit}")" = "$RELEASE_TOOLING_SHA"
git cat-file blob "$RELEASE_TOOLING_SHA:$NOTES_PATH" \
> "$PUBLISH_ROOT/release-body.md"
notes_sha="$RELEASE_TOOLING_SHA"
fi
test -s "$PUBLISH_ROOT/release-body.md"
cat >> "$PUBLISH_ROOT/release-body.md" <<EOF

## Automated release provenance

- Source commit: \`$RELEASE_SHA\`
- Release tooling commit: \`$RELEASE_TOOLING_SHA\`
- Release notes commit: \`$notes_sha\`
- GitHub Actions run: [$GITHUB_RUN_ID/$GITHUB_RUN_ATTEMPT]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)
- Updater trust: updater packages are signed with the dedicated Tauri updater key; the private key is supplied only from GitHub Actions secrets and is never published.
- Platform signing limits: the macOS app uses ad-hoc signing only; it is not Developer ID signed or notarized. Windows installers are not Authenticode-signed.
Expand Down
19 changes: 14 additions & 5 deletions docs/releases/1.0.0-beta.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,28 @@

## GitHub 自动发布流程

`.github/workflows/release.yml` 只接受 `v1.0.0-beta.4` 这类现有 `v<semver>` tag;tag 必须指向
远端 `main` 的当前 HEAD,workflow 不创建、移动或复用 tag。Cargo、Tauri 与 Web 版本均为
`1.0.0-beta.4`,Windows WiX 安装器版本为 `1.0.0.4`,并由独立发布合约 fail closed 校验。
`.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` 的
`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,验证版本、WiX、本文档、prerelease 语义和洁净
checkout;质量门禁执行依赖锁定安装、audit、格式、clippy、workspace/Web 测试及工作流合同。
1. validate job 解析 immutable source SHA;恢复路径还会逐项核对原 run 的五个 job 与 Windows
失败 step,并证明 source 是当前 `main` 的祖先。随后验证版本、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 签名。
3. publish 从 exact SHA 的配置读取内置 updater 公钥,用独立 Minisign 工具验证 package 与
attestation;manifest 将版本、tag、source SHA、平台、资产名称、大小和 SHA-256 绑定。只有
draft 的十七项精确资产上传、下载回读、名称/大小、签名和 `SHA256SUMS` 全部相符后才公开。
恢复发布的公开 release notes 从精确 release tooling commit 读取,并同时记录 source、tooling、
notes commit 与 Actions run,避免把旧不可变 tag 误写成当前 `main` HEAD。

Updater 私钥不写入 checkout、receipt、manifest、日志或发布 artifact;缺少任一 secret 时流程
失败,绝不退化为未签名 updater。
Expand Down
Loading
Loading