Skip to content
Open
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
60 changes: 60 additions & 0 deletions .github/workflows/preview-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# PR 预览第一阶段:在 PR 上下文中构建文档站点
#
# 该工作流会执行 PR 中的代码(包括来自 fork 的 PR),因此:
# - 只授予只读权限,不接触任何 secrets
# - 只负责构建并上传产物,部署交给 preview-deploy.yml(workflow_run 触发,在主仓库上下文中运行)
name: Preview Build
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- "course/**"
- "package.json"
- "pnpm-lock.yaml"
- ".github/workflows/preview-*.yml"
permissions:
contents: read
concurrency:
group: preview-build-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
preview-build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0 # lastUpdated 依赖完整的 git 历史
persist-credentials: false
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build with VitePress
run: pnpm run build
- name: Save PR metadata
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
mkdir -p preview-meta
printf '%s' "$PR_NUMBER" > preview-meta/pr-number
printf '%s' "$HEAD_SHA" > preview-meta/head-sha
- name: Upload site
uses: actions/upload-artifact@v7
with:
name: preview-site
path: course/.vitepress/dist
include-hidden-files: true
retention-days: 3
if-no-files-found: error
- name: Upload metadata
uses: actions/upload-artifact@v7
with:
name: preview-meta
path: preview-meta
retention-days: 3
if-no-files-found: error
171 changes: 171 additions & 0 deletions .github/workflows/preview-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# PR 预览第二阶段:把 Preview Build 产出的静态站点部署到 Cloudflare Pages,并在 PR 中回帖
#
# workflow_run 始终使用默认分支上的工作流定义,在主仓库上下文中运行,可以读取 secrets,
# 因此即使是来自 fork 的 PR 也能获得预览。这里绝不检出或执行 PR 中的代码:
# - 构建产物只作为静态文件上传
# - 元数据(PR 编号、提交)先做格式校验,再通过 GitHub API 交叉验证
#
# 需要的配置(仓库 Settings → Secrets and variables → Actions):
# - secrets.CLOUDFLARE_API_TOKEN:权限为 Account → Cloudflare Pages → Edit
# - secrets.CLOUDFLARE_ACCOUNT_ID
# - vars.CLOUDFLARE_PAGES_PROJECT(可选,默认 zig-course)
# 未配置 secrets 时只输出提示并跳过,不会让 CI 失败。
name: Preview Deploy
on:
workflow_run:
workflows: ["Preview Build"]
types: [completed]
permissions:
actions: read # 下载其他运行的产物
pull-requests: write # 在 PR 中创建 / 更新预览评论
statuses: write # 在 PR 提交上显示 Preview 状态与链接
concurrency:
group: preview-deploy-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
jobs:
preview-deploy:
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
PAGES_PROJECT: ${{ vars.CLOUDFLARE_PAGES_PROJECT || 'zig-course' }}
steps:
- name: Download metadata
uses: actions/download-artifact@v8
with:
name: preview-meta
path: preview-meta
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}

- name: Validate metadata
id: meta
env:
RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
RUN_HEAD_REPO: ${{ github.event.workflow_run.head_repository.full_name }}
RUN_HEAD_OWNER: ${{ github.event.workflow_run.head_repository.owner.login }}
RUN_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
pr="$(head -c 32 preview-meta/pr-number)"
sha="$(head -c 64 preview-meta/head-sha)"
if ! [[ "$pr" =~ ^[0-9]+$ ]] || ! [[ "$sha" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::预览元数据格式不正确"
exit 1
fi
if [ "$sha" != "$RUN_HEAD_SHA" ]; then
echo "::error::元数据中的提交与触发本次运行的提交不一致"
exit 1
fi
if [ -z "$RUN_HEAD_REPO" ] || [ -z "$RUN_HEAD_OWNER" ] || [ -z "$RUN_HEAD_BRANCH" ]; then
echo "::error::无法确定触发本次运行的 head 仓库与分支"
exit 1
fi
# 元数据由执行过 PR 代码的构建环境写出,其中的 PR 编号不能单独信任。
# 这里用 workflow_run 事件自带的 head 仓库、分支与提交(由 GitHub 提供)反查打开的 PR,
# 元数据中的编号必须在结果中。不使用 workflow_run.pull_requests:来自 fork 的 PR 该字段为空。
matched="$(gh api -X GET "repos/$GITHUB_REPOSITORY/pulls" \
-f state=open -f head="$RUN_HEAD_OWNER:$RUN_HEAD_BRANCH" -f per_page=100 \
--jq '.[] | select(.head.sha == env.RUN_HEAD_SHA and .head.repo.full_name == env.RUN_HEAD_REPO) | .number')"
if [ -z "$matched" ]; then
echo "::notice::触发本次运行的 PR 已关闭或已有更新的提交,跳过本次过期的预览部署"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if ! grep -qx "$pr" <<< "$matched"; then
echo "::error::元数据中的 PR 编号与触发本次运行的 PR 不一致"
exit 1
fi
{
echo "skip=false"
echo "pr=$pr"
echo "sha=$sha"
} >> "$GITHUB_OUTPUT"

- name: Check Cloudflare credentials
id: cf
if: steps.meta.outputs.skip == 'false'
env:
CF_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CF_ACCOUNT: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
if [ -n "$CF_TOKEN" ] && [ -n "$CF_ACCOUNT" ]; then
echo "ok=true" >> "$GITHUB_OUTPUT"
else
echo "::notice::未配置 CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID,跳过预览部署"
echo "ok=false" >> "$GITHUB_OUTPUT"
fi

- name: Download site
if: steps.cf.outputs.ok == 'true'
uses: actions/download-artifact@v8
with:
name: preview-site
path: dist
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}

- name: Deploy to Cloudflare Pages
id: deploy
if: steps.cf.outputs.ok == 'true'
uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
wranglerVersion: "4"
command: pages deploy dist --project-name=${{ env.PAGES_PROJECT }} --branch=pr-${{ steps.meta.outputs.pr }} --commit-hash=${{ steps.meta.outputs.sha }} --commit-dirty=true

- name: Comment on PR
if: steps.cf.outputs.ok == 'true'
env:
PR: ${{ steps.meta.outputs.pr }}
SHA: ${{ steps.meta.outputs.sha }}
ALIAS_URL: ${{ steps.deploy.outputs.pages-deployment-alias-url }}
DEPLOY_URL: ${{ steps.deploy.outputs.deployment-url }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
preview_url="${ALIAS_URL:-$DEPLOY_URL}"
if [ -z "$preview_url" ]; then
echo "::error::未能从 wrangler 输出中获取预览地址"
exit 1
fi
marker='<!-- zig-course-preview -->'
{
echo "$marker"
echo "### 文档预览已部署"
echo
echo "| 项目 | 链接 |"
echo "| --- | --- |"
echo "| 预览地址(随 PR 更新) | $preview_url |"
echo "| 本次部署(固定版本) | ${DEPLOY_URL:-$preview_url} |"
echo "| 对应提交 | $SHA |"
echo
echo "由 [Preview Deploy]($RUN_URL) 自动生成,PR 每次更新后会刷新这条评论。"
} > "$RUNNER_TEMP/preview-comment.md"
ids="$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR/comments" --paginate \
--jq '.[] | select(.user.login == "github-actions[bot]" and (.body | startswith("<!-- zig-course-preview -->"))) | .id')"
cid="${ids%%$'\n'*}"
if [ -n "$cid" ]; then
gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$cid" -F body=@"$RUNNER_TEMP/preview-comment.md" > /dev/null
else
gh api "repos/$GITHUB_REPOSITORY/issues/$PR/comments" -F body=@"$RUNNER_TEMP/preview-comment.md" > /dev/null
fi
gh api "repos/$GITHUB_REPOSITORY/statuses/$SHA" \
-f state=success -f context=Preview \
-f description="文档预览已部署" -f target_url="$preview_url" > /dev/null
{
echo "### 文档预览"
echo
echo "- PR:#$PR"
echo "- 预览地址:$preview_url"
echo "- 本次部署:${DEPLOY_URL:-$preview_url}"
} >> "$GITHUB_STEP_SUMMARY"

- name: Report failure
if: failure() && steps.meta.outputs.sha != ''
env:
SHA: ${{ steps.meta.outputs.sha }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
gh api "repos/$GITHUB_REPOSITORY/statuses/$SHA" \
-f state=failure -f context=Preview \
-f description="文档预览部署失败" -f target_url="$RUN_URL" > /dev/null
Loading