文档站与静态站点发布流水线

系统讲解在 GitHub Actions 中构建与发布文档站和静态站点的完整流水线,涵盖 Hugo/Docusaurus/MkDocs 构建缓存、GitHub Pages 与 Cloudflare/Vercel 部署、PR 预览环境、链接与性能质量门禁、CDN 失效与版本化回滚策略。

文档站和静态站点是 CI/CD 里"看起来最简单、实际最容易出细节问题"的一类。构建本身通常几十秒就完成,真正的难点在于:构建产物如何可靠地传到部署目标、预览环境如何自动生成、失效链接和性能回归如何被发现、以及发布后如何快速回滚。

本文以"构建 → 产物 → 部署"三段式为主线,覆盖 Hugo、Docusaurus、MkDocs 等常见生成器,以及 GitHub Pages、Cloudflare Pages、Vercel、S3+CDN 等部署目标,给出可以直接复用的 workflow 片段。

一、静态站点发布的产物模型

1.1 三段式流水线

源码(Markdown / MDX / 组件)
        │  build(Hugo / Docusaurus / MkDocs)
        ▼
   静态产物目录(public/ dist/ site/)
        │  artifact 上传
        ▼
   部署目标(Pages / CDN / 对象存储)

把"构建"与"部署"拆成两个 job 的价值在于:构建可以在 PR 上跑(做校验),部署只在合并后跑。构建产物通过 upload-artifact / download-artifact 传递,避免重复构建。产物传递的细节可参考 /github-actions-artifacts-custom-actions/。

1.2 触发策略

事件构建部署
pull_request是(校验)否(或部署预览)
push to main是是
release是是(版本化快照)
schedule是否(链接巡检)

1.3 权限最小化

部署 Pages 需要 pages: write 与 id-token: write,其余一律 read:

permissions:
  contents: read
  pages: write
  id-token: write

id-token: write 用于 OIDC 部署令牌,避免使用长期有效的部署密钥。

二、构建阶段

2.1 通用骨架

name: Docs

on:
  push:
    branches: [main]
    paths:
      - "docs/**"
      - "content/**"
      - "package.json"
  pull_request:
    paths:
      - "docs/**"
      - "content/**"

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # Hugo 需要完整历史计算 .Lastmod

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - run: npm ci
      - run: npm run docs:build

      - uses: actions/upload-artifact@v4
        with:
          name: site
          path: dist/

paths 过滤让文档站只在相关内容变更时构建,避免每次改后端代码都触发一遍。

2.2 Hugo 构建

Hugo 是单二进制,构建极快,但有两个坑:

- name: Setup Hugo
  uses: peaceiris/actions-hugo@v3
  with:
    hugo-version: "0.128.0"
    extended: true

- name: Build
  run: hugo --minify --gc
  env:
    HUGO_ENVIRONMENT: production
    HUGO_ENV: production

坑一:Hugo 版本必须锁定。不同大版本的模板行为可能不同,latest 会在某天突然构建失败。坑二:fetch-depth: 0。如果主题用 .Lastmod 或 .GitInfo,浅克隆会导致日期全变成构建时间。

--minify 会去掉 HTML 中不必要的空白与属性引号,--gc 清理未使用的缓存条目。

2.3 Docusaurus / Next.js 构建

- run: npm ci
- run: npm run build
  env:
    NEXT_TELEMETRY_DISABLED: "1"

- uses: actions/cache@v4
  with:
    path: .next/cache
    key: next-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
    restore-keys: next-${{ runner.os }}-

把框架的增量构建缓存(.next/cache)持久化,第二次构建能快很多。注意缓存键要绑定 lockfile。

2.4 MkDocs 构建

- uses: actions/setup-python@v5
  with:
    python-version: "3.12"
    cache: pip

- run: pip install -r requirements.txt
- run: mkdocs build --strict

--strict 让 MkDocs 把警告升级为错误——文档站里最常见的警告就是"引用了不存在的页面",用严格模式可以在 CI 阶段直接拦下。

2.5 构建缓存

不同生成器的缓存位置不同:

生成器缓存目录
Hugoresources/_gen/
Docusaurusnode_modules/.cache
Next.js.next/cache
MkDocs~/.cache/pip

统一用 actions/cache 按生成器配置,能显著缩短构建时间。

三、部署目标与策略

3.1 目标对比

目标部署方式预览环境成本
GitHub Pagesactions/deploy-pages无(需自建)免费
Cloudflare Pageswrangler pages deploy内置分支预览免费额度大
VercelCLI 或 Git 集成内置免费额度
NetlifyCLI 或 Git 集成内置免费额度
S3 + CloudFrontaws s3 sync + 失效需自建按量

选型核心:如果只要"发布一个站",Pages 最省心;如果要每个 PR 一个预览 URL,Cloudflare Pages / Vercel 开箱即用。

3.2 GitHub Pages 部署

这是最标准的一条路,用官方三个 Action 串联:

jobs:
  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    permissions:
      pages: write
      id-token: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: site
          path: dist

      - uses: actions/configure-pages@v5

      - uses: actions/upload-pages-artifact@v3
        with:
          path: dist

      - id: deployment
        uses: actions/deploy-pages@v4

三个关键点:

  • environment: github-pages 必须在 job 上声明,否则部署 URL 无法正确回填;
  • upload-pages-artifact 与 deploy-pages 必须成对使用,中间不要插入其他上传;
  • permissions 必须包含 pages: write 与 id-token: write。

3.3 并发控制

文档站不需要并发部署。加 concurrency 防止多次 push 时后发先至、旧版本覆盖新版本:

concurrency:
  group: pages-deploy
  cancel-in-progress: false   # 让进行中的部署跑完,避免半成品

cancel-in-progress: false 是刻意的:部署是"非幂等且不可中断"的操作,中途取消可能留下不完整站点。

3.4 Cloudflare Pages 部署

- name: Publish to Cloudflare Pages
  uses: cloudflare/wrangler-action@v3
  with:
    apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
    accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
    command: pages deploy dist --project-name=my-docs --branch=${{ github.head_ref || github.ref_name }}

--branch 决定是生产部署还是预览部署:分支为 main 时是生产,其他分支自动生成预览 URL。完整的 Hugo + Cloudflare Pages 组合可参考 Cloudflare Pages 与 Hugo 部署 。

3.5 Vercel 部署

- run: npm i -g vercel@latest
- run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
- run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
- run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}

用 vercel build --prebuilt 可以在 CI 里完成构建,避免 Vercel 侧重复构建。Astro/Svelte 这类框架的部署细节可参考 Vercel 部署 Astro 与 Svelte 。更系统的 Vercel 集成可参考 /github-actions-deploy-vercel/。

3.6 S3 + CDN 部署

- name: Sync to S3
  run: |
    aws s3 sync dist/ s3://my-docs-bucket/ \
      --delete \
      --cache-control "public, max-age=31536000, immutable" \
      --exclude "*.html" \
      --exclude "*.xml"
    aws s3 sync dist/ s3://my-docs-bucket/ \
      --cache-control "public, max-age=0, must-revalidate" \
      --exclude "*" --include "*.html" --include "*.xml"

这段配置体现了静态站点缓存的核心原则:带哈希的静态资源长期缓存,HTML 短缓存。HTML 引用的是带哈希的资源名,所以 HTML 必须每次都校验,而资源可以永久缓存。

四、预览环境与 PR Preview

4.1 为什么需要预览

文档改动最需要"所见即所得"的评审。预览环境让评审者在合并前就能看到渲染效果,而不是靠脑补 Markdown。

4.2 在 PR 中回链预览 URL

- name: Comment preview URL
  uses: actions/github-script@v7
  with:
    script: |
      const url = "${{ steps.deploy.outputs.url }}";
      const marker = "<!-- docs-preview -->";
      const body = `${marker}\n📄 文档预览:${url}`;
      const { data: comments } = await github.rest.issues.listComments({
        ...context.repo, issue_number: context.issue.number,
      });
      const prev = comments.find(c => c.body.includes(marker));
      if (prev) {
        await github.rest.issues.updateComment({ ...context.repo, comment_id: prev.id, body });
      } else {
        await github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body });
      }

用标记注释实现幂等更新,避免每次 push 都追加一条新评论。

4.3 清理旧预览

预览部署会累积。定时任务里清理已合并/已关闭 PR 对应的预览,避免配额被占满。

五、质量门禁

5.1 失效链接检查

文档站最影响体验的问题就是死链。用 link checker 在 CI 中拦截:

- name: Check links
  uses: lycheeverse/lychee-action@v2
  with:
    args: --no-progress --max-retries 2 --accept 200,206 "dist/**/*.html"
    fail: true

对内部链接(相对路径)可以严格失败,对外部链接建议容忍偶发超时(--max-retries)。

5.2 构建告警即失败

hugo --minify 遇到模板错误会返回非零,但 REF_NOT_FOUND 这类警告默认不会让构建失败。要把它变成硬错误:

- run: hugo --minify --gc --panicOnWarning

--panicOnWarning 让任何警告都变成 panic,从而让 CI 失败。这能在合并前抓出"引用了不存在的页面"。

5.3 性能预算

用 Lighthouse CI 守住性能预算:

- name: Lighthouse CI
  run: |
    npm i -g @lhci/cli
    lhci autorun
  env:
    LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
// lighthouserc.json
{
  "ci": {
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "categories:accessibility": ["error", { "minScore": 0.95 }]
      }
    }
  }
}

5.4 拼写与风格检查

文档站可以加拼写检查(如 cspell)和 Markdown lint,把低级错误挡在合并前。

5.5 门禁分级

不是所有检查都该阻断。建议分级:

检查级别理由
构建失败阻断产物不可用
内部死链阻断用户必然踩到
外部死链仅警告对方站点可能临时故障
性能预算阻断(阈值宽松)防止明显回归
拼写仅警告误报多

门禁的松紧需要按"误报成本"与"漏报成本"权衡。一条经常误报的硬门禁,很快会被团队用 continue-on-error 绕过,反而失去意义。

六、缓存、失效与回滚

6.1 缓存失效策略

资源类型Cache-Control原因
带哈希的 JS/CSSmax-age=31536000, immutable内容变了文件名也变
HTMLmax-age=0, must-revalidate引用关系会变
图片(无哈希)max-age=86400折中
sitemap / RSSmax-age=3600更新频率中等

6.2 CDN 失效

改动了无哈希的资源(如 logo.png),需要主动失效 CDN:

aws cloudfront create-invalidation \
  --distribution-id ABCDEF123456 \
  --paths "/logo.png" "/favicon.ico"

失效是按路径计费的,尽量精确到变更的文件而非 /*。

6.3 版本化与回滚

两种版本化方式:

  • 路径版本化:/v1.2/、/latest/,适合产品文档;
  • 部署版本化:保留最近 N 次部署产物,回滚时重新部署旧产物。
- uses: actions/upload-artifact@v4
  with:
    name: site-${{ github.sha }}
    path: dist/
    retention-days: 90

回滚时用 gh run download 取回旧产物,重新执行部署 job。保留 90 天足够覆盖绝大多数"发错了要回退"的场景。

七、常见问题

Q:Pages 部署报 Not Found 或 404?
检查 configure-pages 是否在 upload-pages-artifact 之前执行,以及 environment 是否声明。

Q:Hugo 构建出的日期全一样?
fetch-depth: 0 缺失,浅克隆下 .GitInfo 拿不到提交时间。

Q:预览 URL 每次都变,评论刷屏?
用标记注释 + updateComment 实现幂等(见 4.2)。

Q:链接检查总是因为外链超时失败?
对外链设 --max-retries,或只对内部链接开启 fail: true。

总结

静态站点发布流水线的成熟度,体现在三个细节上:构建产物与部署解耦(PR 校验、合并部署)、缓存策略分资源类型(哈希资源永久缓存、HTML 短缓存)、质量门禁前置(死链、构建警告、性能预算都在合并前拦下)。Pages 适合追求简单,Cloudflare/Vercel 适合要预览环境,S3+CDN 适合要完全掌控缓存与成本。把版本化产物保留下来,回滚就从"紧急救火"变成"一条命令"。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「github-actions」更多文章

  1. 多云部署编排与基础设施漂移检测
  2. AI 代码审查与 PR 助手集成
  3. Actions Runner Controller 与 Kubernetes 自动扩缩