Files

240 lines
11 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: deploy-demo
on:
# 网页手工发布的正式版;Actions 自己发布的版本由 workflow_call 接入。
release:
types: [published]
workflow_call:
inputs:
tag:
required: true
type: string
commit:
required: true
type: string
secrets:
VERCEL_TOKEN:
required: true
workflow_dispatch:
inputs:
tag:
description: "已发布的最新正式版标签;留空自动选择最新正式版"
required: false
type: string
permissions:
contents: read
# 不中断正在上传的部署;旧版本在构建前和上线前都会检查是否仍为 latest。
concurrency:
group: demo-production
cancel-in-progress: false
jobs:
deploy:
if: github.event_name != 'release' || github.event.release.prerelease == false
runs-on: ubuntu-latest
timeout-minutes: 25
env:
VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID }}
DEMO_URL: ${{ vars.DEMO_URL }}
NEXT_PUBLIC_MOCK: "1"
NEXT_TELEMETRY_DISABLED: "1"
steps:
- name: Select a published, complete, latest release
id: release
uses: actions/github-script@v7
env:
REQUESTED_TAG: ${{ inputs.tag || github.event.release.tag_name }}
with:
script: |
const { owner, repo } = context.repo;
const { data: latest } = await github.rest.repos.getLatestRelease({ owner, repo });
const tag = process.env.REQUESTED_TAG || latest.tag_name;
if (!/^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(tag)) {
throw new Error('只允许 v主版本.次版本.修订号 的正式标签');
}
const { data: release } = await github.rest.repos.getReleaseByTag({ owner, repo, tag });
if (release.draft || release.prerelease) throw new Error('草稿和预发布版本不可部署');
const version = tag.slice(1);
const assets = new Set(release.assets.map(asset => asset.name));
for (const platform of ['linux-amd64', 'linux-arm64', 'darwin-amd64', 'darwin-arm64', 'windows-amd64']) {
if (!assets.has(`artex-${version}-${platform}.zip`)) throw new Error(`缺少 ${platform} 发布包`);
}
if (!assets.has('SHA256SUMS')) throw new Error('缺少发布校验文件');
core.setOutput('tag', tag);
core.setOutput('current', String(tag === latest.tag_name));
if (tag !== latest.tag_name) {
core.notice(`${tag} 已被 ${latest.tag_name} 替代,跳过以免覆盖新版 Demo`);
}
- name: Check deployment configuration
if: steps.release.outputs.current == 'true'
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: |
for name in VERCEL_ORG_ID VERCEL_PROJECT_ID VERCEL_TOKEN DEMO_URL; do
if [ -z "${!name}" ]; then
echo "::error::请先在仓库 Actions 中配置 $name,参见 README 的在线 Demo 部署说明"
exit 1
fi
done
if [[ ! "$DEMO_URL" =~ ^https://[a-zA-Z0-9][a-zA-Z0-9.-]+/?$ ]]; then
echo "::error::DEMO_URL 必须是 HTTPS 生产域名,不能含路径、查询参数或凭据"
exit 1
fi
- uses: actions/checkout@v4
if: steps.release.outputs.current == 'true'
with:
ref: refs/tags/${{ steps.release.outputs.tag }}
persist-credentials: false
- name: Verify the released commit and write deployment metadata
if: steps.release.outputs.current == 'true'
env:
EXPECTED_COMMIT: ${{ inputs.commit }}
RELEASE_TAG: ${{ steps.release.outputs.tag }}
run: |
commit=$(git rev-parse HEAD)
if [ -n "$EXPECTED_COMMIT" ] && [ "$commit" != "$EXPECTED_COMMIT" ]; then
echo "::error::发布标签已移动,拒绝部署与 Release 构建不同的提交"
exit 1
fi
mkdir -p web/public
jq -n --arg tag "$RELEASE_TAG" --arg commit "$commit" \
--arg release "https://github.com/$GITHUB_REPOSITORY/releases/tag/$RELEASE_TAG" \
'{tag: $tag, commit: $commit, release: $release, mock: true}' > web/public/demo-version.json
# 兼容首次部署尚未含此配置的历史正式版;CLI 部署不受此开关影响。
jq -n '{"$schema":"https://openapi.vercel.sh/vercel.json", framework:"nextjs", installCommand:"npm ci", buildCommand:"npm run build", git:{deploymentEnabled:false}}' > web/vercel.json
- uses: actions/setup-node@v4
if: steps.release.outputs.current == 'true'
with:
node-version: "22"
cache: npm
cache-dependency-path: web/package-lock.json
- name: Install Vercel CLI
if: steps.release.outputs.current == 'true'
run: npm install --global [email protected]
- name: Configure the dedicated demo project
if: steps.release.outputs.current == 'true'
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: |
# CLI 以 web/ 为项目根;设置与 runner 一致的 Node 主版本。
curl --fail-with-body --silent --show-error --request PATCH \
--header "Authorization: Bearer $VERCEL_TOKEN" --header 'Content-Type: application/json' \
--data '{"framework":"nextjs","nodeVersion":"22.x","rootDirectory":null,"installCommand":"npm ci","buildCommand":"npm run build"}' \
"https://api.vercel.com/v9/projects/$VERCEL_PROJECT_ID?teamId=$VERCEL_ORG_ID" \
--output /tmp/demo-project.json
curl --fail-with-body --silent --show-error --request POST \
--header "Authorization: Bearer $VERCEL_TOKEN" --header 'Content-Type: application/json' \
--data '{"key":"NEXT_PUBLIC_MOCK","value":"1","type":"plain","target":["production"]}' \
"https://api.vercel.com/v10/projects/$VERCEL_PROJECT_ID/env?teamId=$VERCEL_ORG_ID&upsert=true" \
--output /tmp/demo-env.json
jq -e '(.failed // []) | length == 0' /tmp/demo-env.json
# 项目令牌无法调用 CLI alias 依赖的用户信息接口,直接配置项目域名。
# 将域名关联到项目后,--prod 会随每次发布自动更新该域名。
domain="${DEMO_URL#https://}"
domain="${domain%/}"
status=$(curl --silent --show-error --write-out '%{http_code}' \
--header "Authorization: Bearer $VERCEL_TOKEN" \
"https://api.vercel.com/v9/projects/$VERCEL_PROJECT_ID/domains/$domain?teamId=$VERCEL_ORG_ID" \
--output /tmp/demo-domain.json)
if [ "$status" = "404" ]; then
jq -n --arg name "$domain" '{name:$name}' > /tmp/demo-domain-input.json
curl --fail-with-body --silent --show-error --request POST \
--header "Authorization: Bearer $VERCEL_TOKEN" --header 'Content-Type: application/json' \
--data-binary @/tmp/demo-domain-input.json \
"https://api.vercel.com/v10/projects/$VERCEL_PROJECT_ID/domains?teamId=$VERCEL_ORG_ID" \
--output /tmp/demo-domain.json
elif [ "$status" != "200" ]; then
jq '.error' /tmp/demo-domain.json
exit 1
fi
jq -e --arg domain "$domain" '.name == $domain and .verified == true' /tmp/demo-domain.json
- name: Pull production project settings
if: steps.release.outputs.current == 'true'
working-directory: web
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: vercel pull --yes --environment=production --token="$VERCEL_TOKEN"
- name: Build the mock demo
if: steps.release.outputs.current == 'true'
working-directory: web
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: vercel build --prod --token="$VERCEL_TOKEN"
- name: Check latest release again before publishing
if: steps.release.outputs.current == 'true'
id: publish
uses: actions/github-script@v7
env:
RELEASE_TAG: ${{ steps.release.outputs.tag }}
with:
script: |
const { data: latest } = await github.rest.repos.getLatestRelease(context.repo);
const current = latest.tag_name === process.env.RELEASE_TAG;
core.setOutput('current', String(current));
if (!current) core.notice('构建期间已有更新的正式版发布,保留线上版本,等待新版部署');
- name: Deploy the prebuilt demo to production
if: steps.publish.outputs.current == 'true'
id: deploy
working-directory: web
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: |
url=$(vercel deploy --prebuilt --prod --yes --token="$VERCEL_TOKEN")
echo "url=$url" >> "$GITHUB_OUTPUT"
echo "### Demo 已部署" >> "$GITHUB_STEP_SUMMARY"
echo "- 正式版本:${{ steps.release.outputs.tag }}" >> "$GITHUB_STEP_SUMMARY"
echo "- 固定网址:$DEMO_URL" >> "$GITHUB_STEP_SUMMARY"
echo "- 部署地址:$url" >> "$GITHUB_STEP_SUMMARY"
- name: Verify production version metadata
if: steps.publish.outputs.current == 'true'
env:
RELEASE_TAG: ${{ steps.release.outputs.tag }}
run: |
# 检查公开的生产域名;Vercel 可单独保护部署专属的随机域名。
# HTTP 成功时也可能暂时读到旧版本,因此请求和内容校验必须一起重试。
expected_commit=$(git rev-parse HEAD)
response=$(mktemp)
trap 'rm -f "$response"' EXIT
deadline=$((SECONDS + 120))
for attempt in $(seq 1 24); do
remaining=$((deadline - SECONDS))
if [ "$remaining" -le 0 ]; then
break
fi
request_timeout=$((remaining < 10 ? remaining : 10))
rm -f "$response"
if curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time "$request_timeout" \
--header 'Cache-Control: no-cache' \
"${DEMO_URL%/}/demo-version.json?verify=${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${attempt}" \
-o "$response" && \
jq -e --arg tag "$RELEASE_TAG" --arg commit "$expected_commit" \
'.tag == $tag and .commit == $commit and .mock == true' "$response" > /dev/null; then
echo "Demo 版本校验通过:$RELEASE_TAG ($expected_commit)"
exit 0
fi
echo "::notice::Demo 版本尚未就绪(第 $attempt/24 次),等待生产域名同步"
remaining=$((deadline - SECONDS))
if [ "$attempt" -lt 24 ] && [ "$remaining" -gt 0 ]; then
sleep "$((remaining < 5 ? remaining : 5))"
fi
done
echo "::error::Demo 版本校验失败,期望 $RELEASE_TAG ($expected_commit),mock=true;最后一次响应如下"
if [ -f "$response" ]; then
cat "$response"
fi
exit 1