mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-28 01:03:50 +08:00
240 lines
11 KiB
YAML
240 lines
11 KiB
YAML
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
|