@@ -96,216 +96,33 @@ jobs:
9696 with :
9797 name : extension-zip
9898 path : release
99- - name : Publish asset with rollback, then move tag and verify
99+ - name : Verify artifact integrity and branch head
100100 env :
101101 GH_TOKEN : ${{ github.token }}
102- shell : bash
103102 run : |
104103 set -euo pipefail
105- VERSION=$(jq -r .version manifest.json)
106- FIXED_NAME="MultiLangSwitcher-latest.zip"
107104 zip_file="$(ls release/MultiLangSwitcher-v*.zip)"
108- expected_digest="$(cat release/digest.txt)"
109- source_sha="$(cat release/source-sha.txt)"
110- test "$source_sha" = "$GITHUB_SHA"
111- test "$(sha256sum "$zip_file" | awk '{print $1}')" = "$expected_digest"
112-
113- current_branch_sha() {
114- gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/$GITHUB_REF_NAME" --jq '.object.sha'
115- }
116-
117- # concurrency 只能约束运行顺序;显式头检查同时拦截旧 run 的 Re-run。
118- assert_current_head() {
119- local current
120- current="$(current_branch_sha)"
121- if [ "$current" != "$GITHUB_SHA" ]; then
122- echo "::error::$GITHUB_REF_NAME 已前进到 $current,拒绝用旧提交 $GITHUB_SHA 更新滚动 Release。"
123- return 1
124- fi
125- }
126- assert_current_head
127-
128- release_api="repos/$GITHUB_REPOSITORY/releases/tags/latest"
129- metadata_before="$RUNNER_TEMP/release-metadata-before.json"
130- old_tag_sha=''
131- release_id=''
132- if gh api "$release_api" > "$RUNNER_TEMP/release-before.json" 2>/dev/null; then
133- release_id="$(jq -r '.id' "$RUNNER_TEMP/release-before.json")"
134- jq '{name, body, draft, prerelease}' "$RUNNER_TEMP/release-before.json" > "$metadata_before"
135- old_tag_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/latest" --jq '.object.sha' 2>/dev/null || true)"
136- else
137- jq -n '{name: "Latest (continuous)", body: "", draft: false, prerelease: true}' > "$metadata_before"
138- fi
139-
140- staged_name="staging-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${GITHUB_SHA}.zip"
141- rollback_name="rollback-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.zip"
142- staged_zip="release/$staged_name"
143- cp "$zip_file" "$staged_zip"
144-
145- asset_id_by_name() {
146- test -n "$release_id" || return 0
147- gh api "repos/$GITHUB_REPOSITORY/releases/$release_id" \
148- | jq -r --arg name "$1" '[.assets[] | select(.name == $name) | .id][0] // empty'
149- }
150-
151- # 下载资产回本地做 sha256 比对(不依赖 GitHub 异步计算的 digest 字段,消除时序抖动)
152- wait_for_asset_bytes() {
153- local name="$1" expected_hex="$2" expected_id="${3:-}"
154- local attempt asset_id
155- for attempt in {1..15}; do
156- asset_id="$(asset_id_by_name "$name" 2>/dev/null || true)"
157- if [ -n "$asset_id" ] && { [ -z "$expected_id" ] || [ "$asset_id" = "$expected_id" ]; }; then
158- rm -f "$RUNNER_TEMP/asset-check.bin"
159- gh api -H 'Accept: application/octet-stream' \
160- "repos/$GITHUB_REPOSITORY/releases/assets/$asset_id" > "$RUNNER_TEMP/asset-check.bin" 2>/dev/null || true
161- if [ -s "$RUNNER_TEMP/asset-check.bin" ] && \
162- [ "$(sha256sum "$RUNNER_TEMP/asset-check.bin" | awk '{print $1}')" = "$expected_hex" ]; then
163- printf '%s\n' "$asset_id"
164- return 0
165- fi
166- fi
167- sleep 2
168- done
169- echo "::error::等待 Release 资产 $name 下载校验超时。" >&2
170- return 1
171- }
172-
173- staged_id=''
174- old_asset_id="$(asset_id_by_name "$FIXED_NAME")"
175- test -z "$(asset_id_by_name "$staged_name")"
176- test -z "$(asset_id_by_name "$rollback_name")"
177-
178- rollback_publish() {
179- local status="$1"
180- local cleanup_id fixed_id old_name current_tag
181- trap - ERR INT TERM
182- set +e
183- echo '::warning::滚动 Release 发布失败,正在恢复上一个已验证状态。'
184-
185- cleanup_id="$staged_id"
186- if [ -z "$cleanup_id" ]; then
187- cleanup_id="$(asset_id_by_name "$staged_name")"
188- fi
189- if [ -n "$cleanup_id" ] && [ "$cleanup_id" != "$old_asset_id" ]; then
190- gh api --method DELETE "repos/$GITHUB_REPOSITORY/releases/assets/$cleanup_id" >/dev/null
191- fi
192-
193- if [ -n "$old_asset_id" ]; then
194- old_name="$(gh api "repos/$GITHUB_REPOSITORY/releases/assets/$old_asset_id" --jq '.name' 2>/dev/null)"
195- if [ -n "$old_name" ] && [ "$old_name" != "$FIXED_NAME" ]; then
196- fixed_id="$(asset_id_by_name "$FIXED_NAME")"
197- if [ -n "$fixed_id" ] && [ "$fixed_id" != "$old_asset_id" ]; then
198- gh api --method DELETE "repos/$GITHUB_REPOSITORY/releases/assets/$fixed_id" >/dev/null
199- fi
200- gh api --method PATCH "repos/$GITHUB_REPOSITORY/releases/assets/$old_asset_id" \
201- -f name="$FIXED_NAME" >/dev/null
202- fi
203- fi
204-
205- if [ -n "$release_id" ]; then
206- gh api --method PATCH "repos/$GITHUB_REPOSITORY/releases/$release_id" \
207- --input "$metadata_before" >/dev/null
208- fi
209- if [ -n "$old_tag_sha" ]; then
210- gh api --method PATCH "repos/$GITHUB_REPOSITORY/git/refs/tags/latest" \
211- -f sha="$old_tag_sha" -F force=true >/dev/null
212- current_tag="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/latest" --jq '.object.sha' 2>/dev/null)"
213- if [ "$current_tag" != "$old_tag_sha" ]; then
214- echo '::error::tag 回滚未恢复,需要人工检查。'
215- fi
216- fi
217- exit "$status"
218- }
219- trap 'rollback_publish $?' ERR
220- trap 'rollback_publish 130' INT
221- trap 'rollback_publish 143' TERM
222-
223- # 失败定位辅助:每个关键步骤出错时打印步骤名(此前裸失败时无报错行可查)
224- step() {
225- echo "::group::$*"
226- local rc=0
227- "$@" || rc=$?
228- echo "::endgroup::"
229- if [ "$rc" -ne 0 ]; then echo "::error::FAILED AT STEP: $* (exit $rc)"; fi
230- return "$rc"
231- }
232-
233- # 首次发布:创建 prerelease;后续:上传暂存资产并校验摘要。
234- if [ -z "$release_id" ]; then
235- gh release create latest --repo "$GITHUB_REPOSITORY" \
236- --title "Latest (continuous)" --prerelease --notes "滚动最新构建渠道。"
237- release_id="$(gh api "$release_api" --jq '.id')"
238- test -n "$release_id"
239- fi
240-
241- step gh release upload latest "$staged_zip" --repo "$GITHUB_REPOSITORY"
242- # 上传后短暂沉降,降低资产列表/下载端点的可见性延迟
243- sleep 5
244- staged_id="$(wait_for_asset_bytes "$staged_name" "$expected_digest")" || {
245- echo "::error::FAILED AT STEP: wait_for_asset_bytes staged"
246- exit 1
247- }
248-
249- # 上传暂存资产可能耗时;在触碰旧固定资产前再核对一次分支头。
250- assert_current_head
251-
252- # 暂存资产完整后,旧固定资产改回滚名,新资产切换为固定名。
253- if [ -n "$old_asset_id" ]; then
254- step gh api --method PATCH "repos/$GITHUB_REPOSITORY/releases/assets/$old_asset_id" \
255- -f name="$rollback_name" >/dev/null
256- fi
257- step gh api --method PATCH "repos/$GITHUB_REPOSITORY/releases/assets/$staged_id" \
258- -f name="$FIXED_NAME" >/dev/null
259-
260- notes="$RUNNER_TEMP/release-notes.md"
261- {
262- echo "自动构建的最新扩展包(滚动测试渠道,与正式版 Release 互不冲突)。"
263- echo "- 版本:v${VERSION}"
264- echo "- 提交:${GITHUB_SHA}"
265- echo "- 分支:${GITHUB_REF_NAME}"
266- } > "$notes"
267- gh release edit latest --repo "$GITHUB_REPOSITORY" \
268- --title "Latest v${VERSION} (${GITHUB_SHA::7})" \
269- --notes-file "$notes" \
270- --prerelease
271-
272- # 新资产和发布说明就绪后才移动 tag。
273- assert_current_head
274- gh api --method PATCH "repos/$GITHUB_REPOSITORY/git/refs/tags/latest" \
275- -f sha="$GITHUB_SHA" -F force=true >/dev/null || \
276- gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \
277- -f ref="refs/tags/latest" -f sha="$GITHUB_SHA" >/dev/null
278-
279- actual_tag="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/latest" --jq '.object.sha')"
280- test "$actual_tag" = "$GITHUB_SHA"
281-
282- asset_id="$(wait_for_asset_bytes "$FIXED_NAME" "$expected_digest" "$staged_id")"
283- test "$asset_id" = "$staged_id"
284- gh api -H 'Accept: application/octet-stream' \
285- "repos/$GITHUB_REPOSITORY/releases/assets/$asset_id" > "$RUNNER_TEMP/published.zip"
286- test "$(sha256sum "$RUNNER_TEMP/published.zip" | awk '{print $1}')" = "$expected_digest"
287-
288- # 若发布期间分支已前进,就在旧资产仍可恢复时主动回滚本次结果。
289- assert_current_head
290-
291- # 新状态已完整核验,之后只清理旧副本。
292- trap - ERR INT TERM
293- if [ -n "$old_asset_id" ]; then
294- gh api --method DELETE "repos/$GITHUB_REPOSITORY/releases/assets/$old_asset_id" >/dev/null
295- fi
296- mapfile -t orphan_asset_ids < <(
297- gh api "repos/$GITHUB_REPOSITORY/releases/$release_id" \
298- | jq -r '.assets[] | select(.name | test("^(staging|rollback)-.*\\.zip$")) | .id'
299- )
300- for orphan_id in "${orphan_asset_ids[@]}"; do
301- gh api --method DELETE "repos/$GITHUB_REPOSITORY/releases/assets/$orphan_id" >/dev/null
302- done
303-
304- final_count="$(
305- gh api "repos/$GITHUB_REPOSITORY/releases/$release_id" \
306- | jq --arg name "$FIXED_NAME" '[.assets[] | select(.name == $name)] | length'
307- )"
308- test "$final_count" = '1'
105+ test "$(cat release/digest.txt)" = "$(sha256sum "$zip_file" | awk '{print $1}')"
106+ test "$(cat release/source-sha.txt)" = "$GITHUB_SHA"
107+ # 过期 run(re-run 或并发)不接触线上 Release
108+ current="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/$GITHUB_REF_NAME" --jq '.object.sha')"
109+ test "$current" = "$GITHUB_SHA" || { echo "::error::$GITHUB_REF_NAME 已前进,拒绝用旧提交发布。"; exit 1; }
110+ cp "$zip_file" release/MultiLangSwitcher-latest.zip
111+ - name : Move latest tag to HEAD
112+ run : |
113+ git config user.name "github-actions[bot]"
114+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
115+ git tag -fa latest -m "Rolling latest build"
116+ git push -f origin latest
117+ - uses : softprops/action-gh-release@c12583777ecdfd3be55c69cf75464299dc01057e # v3
118+ with :
119+ tag_name : latest
120+ name : Latest (continuous)
121+ body : |
122+ 自动构建的最新扩展包(滚动测试渠道,与正式版 Release 互不冲突)。
123+ 提交:${{ github.sha }}
124+ prerelease : true
125+ files : release/MultiLangSwitcher-latest.zip
309126
310127 release :
311128 name : Publish tagged release
@@ -333,7 +150,7 @@ jobs:
333150 zip_file="$(ls release/MultiLangSwitcher-v*.zip)"
334151 test "$(sha256sum "$zip_file" | awk '{print $1}')" = "$(cat release/digest.txt)"
335152 test "$(cat release/source-sha.txt)" = "$GITHUB_SHA"
336- - uses : softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
153+ - uses : softprops/action-gh-release@c12583777ecdfd3be55c69cf75464299dc01057e # v3
337154 with :
338155 files : release/MultiLangSwitcher-v*.zip
339156 generate_release_notes : true
0 commit comments