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
72 changes: 59 additions & 13 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ on:
required: true
type: string
failed_recovery_run_id:
description: Previous failed workflow_dispatch recovery Release run ID (31427093503) chained to the same immutable source
description: Direct predecessor workflow_dispatch Release run ID (31441693191) whose publish job failed for the same immutable source
required: true
type: string

Expand Down Expand Up @@ -71,7 +71,7 @@ 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"
predecessor_tooling_sha="924bc1102a9343e14c3beea2a3622b5d92ebff13"
if [[ "$GITHUB_EVENT_NAME" = "push" ]]; then
test -z "$FAILED_RUN_ID"
test -z "$FAILED_RECOVERY_RUN_ID"
Expand All @@ -84,7 +84,7 @@ jobs:
[[ "$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 "$FAILED_RECOVERY_RUN_ID" = "31441693191"
test "$tooling_sha" = "$remote_main"
recovery_root="$RUNNER_TEMP/opentake-release-recovery-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
mkdir -p "$recovery_root/tooling"
Expand Down Expand Up @@ -1009,6 +1009,8 @@ jobs:
RELEASE_TOOLING_SHA: ${{ needs.validate.outputs.tooling_sha }}
RELEASE_VERSION: ${{ needs.validate.outputs.version }}
NOTES_PATH: ${{ needs.validate.outputs.notes_path }}
ROOT_FAILED_RUN_ID: ${{ github.event_name == 'workflow_dispatch' && inputs.failed_run_id || '' }}
PREDECESSOR_RUN_ID: ${{ github.event_name == 'workflow_dispatch' && inputs.failed_recovery_run_id || '' }}
PYTHONDONTWRITEBYTECODE: '1'
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
Expand Down Expand Up @@ -1037,6 +1039,17 @@ jobs:
"$RUNNER_TEMP"/opentake-release-*) ;;
*) exit 1 ;;
esac
mkdir -p "$publish_root/tooling"
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" \
> "$publish_root/tooling/check_release_workflow.py"
git cat-file blob "$RELEASE_TOOLING_SHA:scripts/workflow_yaml.py" \
> "$publish_root/tooling/workflow_yaml.py"
test -s "$publish_root/tooling/check_release_workflow.py"
test -s "$publish_root/tooling/workflow_yaml.py"
printf 'PUBLISH_ROOT=%s\n' "$publish_root" >> "$GITHUB_ENV"

- name: Install Minisign verifier
Expand Down Expand Up @@ -1532,13 +1545,23 @@ jobs:
notes_sha="$RELEASE_TOOLING_SHA"
fi
test -s "$PUBLISH_ROOT/release-body.md"
recovery_chain="normal tag push"
if [[ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]]; then
test "$ROOT_FAILED_RUN_ID" = "31412976593"
test "$PREDECESSOR_RUN_ID" = "31441693191"
recovery_chain="root run $ROOT_FAILED_RUN_ID; direct predecessor run $PREDECESSOR_RUN_ID"
else
test -z "$ROOT_FAILED_RUN_ID"
test -z "$PREDECESSOR_RUN_ID"
fi
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\`
- Recovery chain: $recovery_chain
- 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 Expand Up @@ -1566,7 +1589,7 @@ jobs:
git ls-remote --exit-code origin \
"refs/tags/$RELEASE_TAG" "refs/tags/$RELEASE_TAG^{}" \
> "$PUBLISH_ROOT/remote-tag-before-draft.txt"
python3 scripts/check_release_workflow.py resolve-remote-tag \
python3 -B "$PUBLISH_ROOT/tooling/check_release_workflow.py" resolve-remote-tag \
--input "$PUBLISH_ROOT/remote-tag-before-draft.txt" \
--tag "$RELEASE_TAG" --sha "$RELEASE_SHA"

Expand All @@ -1580,17 +1603,19 @@ jobs:
gh api graphql \
-f query="$query" -f owner="$owner" -f name="$repository" -f tag="$RELEASE_TAG" \
> "$PUBLISH_ROOT/existing-release-graphql.json"
python3 scripts/check_release_workflow.py resolve-release-state \
python3 -B "$PUBLISH_ROOT/tooling/check_release_workflow.py" resolve-release-state \
--input "$PUBLISH_ROOT/existing-release-graphql.json" \
--tag "$RELEASE_TAG" \
--sha "$RELEASE_SHA" \
--output "$PUBLISH_ROOT/release-state.json"
action="$(jq -r '.action' "$PUBLISH_ROOT/release-state.json")"
case "$action" in
create)
# The existing, independently verified tag binds the source commit.
# Passing target_commitish would require a Workflows-write token when
# release tooling changed workflow files; GITHUB_TOKEN cannot have it.
gh release create "$RELEASE_TAG" \
--verify-tag \
--target "$RELEASE_SHA" \
--title "OpenTake $RELEASE_VERSION" \
--notes-file "$PUBLISH_ROOT/release-body.md" \
--draft --prerelease --latest=false
Expand All @@ -1605,13 +1630,12 @@ jobs:
fi
test "$(jq -r '.prerelease' "$PUBLISH_ROOT/existing-draft-rest.json")" = "true"
test "$(jq -r '.tag_name' "$PUBLISH_ROOT/existing-draft-rest.json")" = "$RELEASE_TAG"
test "$(jq -r '.target_commitish' "$PUBLISH_ROOT/existing-draft-rest.json" | tr '[:upper:]' '[:lower:]')" = "$RELEASE_SHA"
jq -e '.target_commitish | (type == "string" and length > 0)' "$PUBLISH_ROOT/existing-draft-rest.json" >/dev/null
jq -e 'all(.assets[]; (.id | type) == "number")' "$PUBLISH_ROOT/existing-draft-rest.json" >/dev/null
while read -r asset_id; do
gh api --method DELETE "repos/$GITHUB_REPOSITORY/releases/assets/$asset_id"
done < <(jq -r '.assets[].id' "$PUBLISH_ROOT/existing-draft-rest.json")
gh release edit "$RELEASE_TAG" \
--target "$RELEASE_SHA" \
--title "OpenTake $RELEASE_VERSION" \
--notes-file "$PUBLISH_ROOT/release-body.md" \
--draft --prerelease --latest=false
Expand All @@ -1633,7 +1657,7 @@ jobs:
gh api graphql \
-f query="$query" -f owner="$owner" -f name="$repository" -f tag="$RELEASE_TAG" \
> "$PUBLISH_ROOT/draft-release-graphql.json"
python3 scripts/check_release_workflow.py resolve-release-state \
python3 -B "$PUBLISH_ROOT/tooling/check_release_workflow.py" resolve-release-state \
--input "$PUBLISH_ROOT/draft-release-graphql.json" \
--tag "$RELEASE_TAG" \
--sha "$RELEASE_SHA" \
Expand Down Expand Up @@ -1681,7 +1705,7 @@ jobs:
git ls-remote --exit-code origin \
"refs/tags/$RELEASE_TAG" "refs/tags/$RELEASE_TAG^{}" \
> "$PUBLISH_ROOT/remote-tag-before-publication.txt"
python3 scripts/check_release_workflow.py resolve-remote-tag \
python3 -B "$PUBLISH_ROOT/tooling/check_release_workflow.py" resolve-remote-tag \
--input "$PUBLISH_ROOT/remote-tag-before-publication.txt" \
--tag "$RELEASE_TAG" --sha "$RELEASE_SHA"

Expand All @@ -1706,6 +1730,28 @@ jobs:
- name: Verify public release through API and checksums
run: |
set -euo pipefail
git ls-remote --exit-code origin \
"refs/tags/$RELEASE_TAG" "refs/tags/$RELEASE_TAG^{}" \
> "$PUBLISH_ROOT/remote-tag-after-publication.txt"
python3 -B "$PUBLISH_ROOT/tooling/check_release_workflow.py" resolve-remote-tag \
--input "$PUBLISH_ROOT/remote-tag-after-publication.txt" \
--tag "$RELEASE_TAG" --sha "$RELEASE_SHA"

owner="${GITHUB_REPOSITORY%%/*}"
repository="${GITHUB_REPOSITORY#*/}"
query='query($owner:String!,$name:String!,$tag:String!){repository(owner:$owner,name:$name){release(tagName:$tag){databaseId tagName tagCommit{oid} isDraft isPrerelease releaseAssets(first:100){nodes{id name size} pageInfo{hasNextPage}}}}}'
gh api graphql \
-f query="$query" -f owner="$owner" -f name="$repository" -f tag="$RELEASE_TAG" \
> "$PUBLISH_ROOT/public-release-graphql.json"
python3 -B "$PUBLISH_ROOT/tooling/check_release_workflow.py" resolve-public-release-state \
--input "$PUBLISH_ROOT/public-release-graphql.json" \
--tag "$RELEASE_TAG" \
--sha "$RELEASE_SHA" \
--output "$PUBLISH_ROOT/public-release-state.json"
jq -r '.asset_names[]' "$PUBLISH_ROOT/public-release-state.json" | LC_ALL=C sort > "$PUBLISH_ROOT/public-graphql-assets.txt"
cmp "$PUBLISH_ROOT/expected-assets.txt" "$PUBLISH_ROOT/public-graphql-assets.txt"
jq -e '.asset_sizes | length == 17 and all(. > 0)' "$PUBLISH_ROOT/public-release-state.json" >/dev/null

gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG" > "$PUBLISH_ROOT/public-release.json"
python3 - <<'PY'
import json
Expand All @@ -1714,15 +1760,15 @@ jobs:

root = Path(os.environ["PUBLISH_ROOT"])
release = json.loads((root / "public-release.json").read_text(encoding="utf-8"))
expected_sha = os.environ["RELEASE_SHA"]
expected_names = set((root / "expected-assets.txt").read_text(encoding="utf-8").splitlines())
actual_names = {asset["name"] for asset in release.get("assets", [])}
if release.get("draft") is not False or release.get("prerelease") is not True:
raise SystemExit("release was not published as a prerelease")
if release.get("tag_name") != os.environ["RELEASE_TAG"]:
raise SystemExit("public release tag mismatch")
if release.get("target_commitish") != expected_sha:
raise SystemExit("public release target SHA mismatch")
target_commitish = release.get("target_commitish")
if not isinstance(target_commitish, str) or not target_commitish:
raise SystemExit("public release target_commitish schema is malformed")
if expected_names != actual_names:
raise SystemExit(f"public release assets mismatch: {sorted(actual_names)}")
PY
Expand Down
30 changes: 18 additions & 12 deletions docs/releases/1.0.0-beta.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,23 @@
此时 product source SHA 与 release tooling SHA 必须相同,并等于当前远端 `main` HEAD。若这个
tag push 已通过 source、质量与 macOS 门禁,却仅在 Windows 的 checksum-pinned FFmpeg sidecar
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` 的
`failed_run_id=31412976593`)绑定原 tag-push run。历史恢复 run `31427093503` 仅在 Windows
PowerShell → `.cmd` → Tauri config argv 边界失败;其后的直接前驱 run `31441693191`
已通过 validate、质量、macOS 与 Windows 门禁,只在 publish 的 `Create or refresh draft prerelease`
步骤失败。下一次恢复必须同时显式提供 `failed_run_id=31412976593` 与
`failed_recovery_run_id=31441693191`,缺少或替换任一 ID 都 fail closed。product source 仍是原不可变
tag SHA `2c4efdff9d2587c90cbcac0919f9d1d333d67d6a`;直接前驱的 release tooling 固定为
`924bc1102a9343e14c3beea2a3622b5d92ebff13`,当前 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;恢复路径先逐项核对 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;质量门禁执行依赖
sidecar 失败 step,再独立核对直接前驱 run 的五个 exact job(包括成功的 validate job)、
publish 第 15 步权限失败以及 macOS/Windows 两个 artifact 的 ID、大小、digest、未过期状态与
run/repository/source 绑定。两个 GitHub compare 响应必须分别证明 source → `924bc110…` 与
`924bc110…` → 当前远端 `main`,不接受互换、跳段或任意失败 run。直接前驱的 validate 成功
也是它已核验上一恢复链的证据。随后验证版本、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、每个
Expand All @@ -61,8 +63,12 @@ tag SHA `2c4efdff9d2587c90cbcac0919f9d1d333d67d6a`;上一恢复的 release too
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。
前驱的 403 不是 `contents: write` 缺失:当 `target_commitish` 指向与默认分支之间修改过
workflow 文件的旧提交时,GitHub 要求 Actions `GITHUB_TOKEN` 无法获得的 Workflows write 权限。
因 tag 已存在且经独立验证,create/refresh 都不再传 `--target`;REST `target_commitish` 只做非空
schema 校验,不作为 source oracle。发布前后的 remote tag peel 与 GraphQL `tagCommit.oid` 才必须始终
精确等于 source SHA。恢复发布的公开 release notes 从精确 release tooling commit 读取,并同时记录
root/direct-predecessor run IDs、source、tooling、notes commit 与当前 Actions run。

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