Skip to content

fix: move 0.875rem to html root — rem-based bootstrap components (h4/… #41

fix: move 0.875rem to html root — rem-based bootstrap components (h4/…

fix: move 0.875rem to html root — rem-based bootstrap components (h4/… #41

Workflow file for this run

name: CI
# 所有 push / PR 只经过同一个验证关;验证通过的 main/master 才继续打包与滚动发布。
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
# 所有 ref 统一:新提交到达即取消同 ref 在跑的旧 run。
# main 的 publish 步骤有回滚 trap 兜底中断态,且发布头检查会拒绝过期 run。
concurrency:
group: ${{ format('ci-{0}', github.ref) }}
cancel-in-progress: true
jobs:
verify:
name: Verify
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx biome ci .
- run: npx tsc
- run: node scripts/check-syntax.mjs
- run: node scripts/validate-manifest.mjs
- run: npm test
- name: Verify purged CSS is in sync
run: |
node scripts/purge-css.mjs
git diff --exit-code bootstrap.purged.css
package:
name: Build extension zip
needs: verify
if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- name: Build extension zip
run: |
set -euo pipefail
VERSION=$(jq -r .version manifest.json)
STAGE="MultiLangSwitcher-v${VERSION}"
mkdir "$STAGE"
cp manifest.json LICENSE \
background.js popup.html popup.js \
debug.html debug-ui.js debug-headers.js \
detect.html detect.js detect-ai.js \
toggle.js toggle.css \
domain-rules-manager.js domain-rules.json \
bootstrap.purged.css \
"$STAGE/"
cp -r background popup debug detect _locales shared i18n images fonts "$STAGE/"
find "$STAGE/shared/vendor" -name "*.map" -delete 2>/dev/null || true
zip -r "${STAGE}.zip" "$STAGE"
sha256sum "${STAGE}.zip" | awk '{print $1}' > digest.txt
printf '%s\n' "$GITHUB_SHA" > source-sha.txt
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: extension-zip
path: |
MultiLangSwitcher-v*.zip
digest.txt
source-sha.txt
if-no-files-found: error
retention-days: 7
publish-latest:
name: Publish rolling prerelease
needs: package
if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') }}
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: extension-zip
path: release
- name: Publish asset with rollback, then move tag and verify
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
VERSION=$(jq -r .version manifest.json)
FIXED_NAME="MultiLangSwitcher-latest.zip"
zip_file="$(ls release/MultiLangSwitcher-v*.zip)"
expected_digest="$(cat release/digest.txt)"
source_sha="$(cat release/source-sha.txt)"
test "$source_sha" = "$GITHUB_SHA"
test "$(sha256sum "$zip_file" | awk '{print $1}')" = "$expected_digest"
current_branch_sha() {
gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/$GITHUB_REF_NAME" --jq '.object.sha'
}
# concurrency 只能约束运行顺序;显式头检查同时拦截旧 run 的 Re-run。
assert_current_head() {
local current
current="$(current_branch_sha)"
if [ "$current" != "$GITHUB_SHA" ]; then
echo "::error::$GITHUB_REF_NAME 已前进到 $current,拒绝用旧提交 $GITHUB_SHA 更新滚动 Release。"
return 1
fi
}
assert_current_head
release_api="repos/$GITHUB_REPOSITORY/releases/tags/latest"
metadata_before="$RUNNER_TEMP/release-metadata-before.json"
old_tag_sha=''
release_id=''
if gh api "$release_api" > "$RUNNER_TEMP/release-before.json" 2>/dev/null; then
release_id="$(jq -r '.id' "$RUNNER_TEMP/release-before.json")"
jq '{name, body, draft, prerelease}' "$RUNNER_TEMP/release-before.json" > "$metadata_before"
old_tag_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/latest" --jq '.object.sha' 2>/dev/null || true)"
else
jq -n '{name: "Latest (continuous)", body: "", draft: false, prerelease: true}' > "$metadata_before"
fi
staged_name="staging-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${GITHUB_SHA}.zip"
rollback_name="rollback-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.zip"
staged_zip="release/$staged_name"
cp "$zip_file" "$staged_zip"
asset_id_by_name() {
test -n "$release_id" || return 0
gh api "repos/$GITHUB_REPOSITORY/releases/$release_id" \
| jq -r --arg name "$1" '[.assets[] | select(.name == $name) | .id][0] // empty'
}
wait_for_asset_digest() {
local name="$1" expected="$2" expected_id="${3:-}"
local attempt asset_id digest
for attempt in {1..15}; do
asset_id="$(asset_id_by_name "$name" 2>/dev/null || true)"
if [ -n "$asset_id" ] && { [ -z "$expected_id" ] || [ "$asset_id" = "$expected_id" ]; }; then
digest="$(gh api "repos/$GITHUB_REPOSITORY/releases/assets/$asset_id" --jq '.digest // empty' 2>/dev/null || true)"
if [ "$digest" = "$expected" ]; then
printf '%s\n' "$asset_id"
return 0
fi
fi
sleep 2
done
echo "::error::等待 Release 资产 $name 的摘要就绪超时。" >&2
return 1
}
staged_id=''
old_asset_id="$(asset_id_by_name "$FIXED_NAME")"
test -z "$(asset_id_by_name "$staged_name")"
test -z "$(asset_id_by_name "$rollback_name")"
rollback_publish() {
local status="$1"
local cleanup_id fixed_id old_name current_tag
trap - ERR INT TERM
set +e
echo '::warning::滚动 Release 发布失败,正在恢复上一个已验证状态。'
cleanup_id="$staged_id"
if [ -z "$cleanup_id" ]; then
cleanup_id="$(asset_id_by_name "$staged_name")"
fi
if [ -n "$cleanup_id" ] && [ "$cleanup_id" != "$old_asset_id" ]; then
gh api --method DELETE "repos/$GITHUB_REPOSITORY/releases/assets/$cleanup_id" >/dev/null
fi
if [ -n "$old_asset_id" ]; then
old_name="$(gh api "repos/$GITHUB_REPOSITORY/releases/assets/$old_asset_id" --jq '.name' 2>/dev/null)"
if [ -n "$old_name" ] && [ "$old_name" != "$FIXED_NAME" ]; then
fixed_id="$(asset_id_by_name "$FIXED_NAME")"
if [ -n "$fixed_id" ] && [ "$fixed_id" != "$old_asset_id" ]; then
gh api --method DELETE "repos/$GITHUB_REPOSITORY/releases/assets/$fixed_id" >/dev/null
fi
gh api --method PATCH "repos/$GITHUB_REPOSITORY/releases/assets/$old_asset_id" \
-f name="$FIXED_NAME" >/dev/null
fi
fi
if [ -n "$release_id" ]; then
gh api --method PATCH "repos/$GITHUB_REPOSITORY/releases/$release_id" \
--input "$metadata_before" >/dev/null
fi
if [ -n "$old_tag_sha" ]; then
gh api --method PATCH "repos/$GITHUB_REPOSITORY/git/refs/tags/latest" \
-f sha="$old_tag_sha" -F force=true >/dev/null
current_tag="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/latest" --jq '.object.sha' 2>/dev/null)"
if [ "$current_tag" != "$old_tag_sha" ]; then
echo '::error::tag 回滚未恢复,需要人工检查。'
fi
fi
exit "$status"
}
trap 'rollback_publish $?' ERR
trap 'rollback_publish 130' INT
trap 'rollback_publish 143' TERM
# 失败定位辅助:每个关键步骤出错时打印步骤名(此前裸失败时无报错行可查)
step() {
echo "::group::$*"
local rc=0
"$@" || rc=$?
echo "::endgroup::"
if [ "$rc" -ne 0 ]; then echo "::error::FAILED AT STEP: $* (exit $rc)"; fi
return "$rc"
}
# 首次发布:创建 prerelease;后续:上传暂存资产并校验摘要。
if [ -z "$release_id" ]; then
gh release create latest --repo "$GITHUB_REPOSITORY" \
--title "Latest (continuous)" --prerelease --notes "滚动最新构建渠道。"
release_id="$(gh api "$release_api" --jq '.id')"
test -n "$release_id"
fi
step gh release upload latest "$staged_zip" --repo "$GITHUB_REPOSITORY"
# GitHub 异步计算资产 digest,上传后留出沉降时间再轮询
sleep 5
staged_id="$(wait_for_asset_digest "$staged_name" "sha256:$expected_digest")" || {
echo "::error::FAILED AT STEP: wait_for_asset_digest staged"
exit 1
}
# 上传暂存资产可能耗时;在触碰旧固定资产前再核对一次分支头。
assert_current_head
# 暂存资产完整后,旧固定资产改回滚名,新资产切换为固定名。
if [ -n "$old_asset_id" ]; then
step gh api --method PATCH "repos/$GITHUB_REPOSITORY/releases/assets/$old_asset_id" \
-f name="$rollback_name" >/dev/null
fi
step gh api --method PATCH "repos/$GITHUB_REPOSITORY/releases/assets/$staged_id" \
-f name="$FIXED_NAME" >/dev/null
notes="$RUNNER_TEMP/release-notes.md"
{
echo "自动构建的最新扩展包(滚动测试渠道,与正式版 Release 互不冲突)。"
echo "- 版本:v${VERSION}"
echo "- 提交:${GITHUB_SHA}"
echo "- 分支:${GITHUB_REF_NAME}"
} > "$notes"
gh release edit latest --repo "$GITHUB_REPOSITORY" \
--title "Latest v${VERSION} (${GITHUB_SHA::7})" \
--notes-file "$notes" \
--prerelease
# 新资产和发布说明就绪后才移动 tag。
assert_current_head
gh api --method PATCH "repos/$GITHUB_REPOSITORY/git/refs/tags/latest" \
-f sha="$GITHUB_SHA" -F force=true >/dev/null || \
gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \
-f ref="refs/tags/latest" -f sha="$GITHUB_SHA" >/dev/null
actual_tag="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/latest" --jq '.object.sha')"
test "$actual_tag" = "$GITHUB_SHA"
asset_id="$(wait_for_asset_digest "$FIXED_NAME" "sha256:$expected_digest" "$staged_id")"
test "$asset_id" = "$staged_id"
gh api -H 'Accept: application/octet-stream' \
"repos/$GITHUB_REPOSITORY/releases/assets/$asset_id" > "$RUNNER_TEMP/published.zip"
test "$(sha256sum "$RUNNER_TEMP/published.zip" | awk '{print $1}')" = "$expected_digest"
# 若发布期间分支已前进,就在旧资产仍可恢复时主动回滚本次结果。
assert_current_head
# 新状态已完整核验,之后只清理旧副本。
trap - ERR INT TERM
if [ -n "$old_asset_id" ]; then
gh api --method DELETE "repos/$GITHUB_REPOSITORY/releases/assets/$old_asset_id" >/dev/null
fi
mapfile -t orphan_asset_ids < <(
gh api "repos/$GITHUB_REPOSITORY/releases/$release_id" \
| jq -r '.assets[] | select(.name | test("^(staging|rollback)-.*\\.zip$")) | .id'
)
for orphan_id in "${orphan_asset_ids[@]}"; do
gh api --method DELETE "repos/$GITHUB_REPOSITORY/releases/assets/$orphan_id" >/dev/null
done
final_count="$(
gh api "repos/$GITHUB_REPOSITORY/releases/$release_id" \
| jq --arg name "$FIXED_NAME" '[.assets[] | select(.name == $name)] | length'
)"
test "$final_count" = '1'
release:
name: Publish tagged release
needs: package
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Guard tag/version match
run: |
set -euo pipefail
TAG=${GITHUB_REF_NAME#v}
VERSION=$(jq -r .version manifest.json)
test "$TAG" = "$VERSION" || { echo "tag $TAG != manifest version $VERSION"; exit 1; }
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: extension-zip
path: release
- name: Verify digest before publishing
run: |
set -euo pipefail
zip_file="$(ls release/MultiLangSwitcher-v*.zip)"
test "$(sha256sum "$zip_file" | awk '{print $1}')" = "$(cat release/digest.txt)"
test "$(cat release/source-sha.txt)" = "$GITHUB_SHA"
- uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
files: release/MultiLangSwitcher-v*.zip
generate_release_notes: true