网页部署

记录一次 Hugo + GitHub Actions + 外部仓库部署的认证报错,以及 PAT、Secrets 和 workflow 的正确配置方式。

这篇主要记录我这次博客部署里踩到的一个坑:Hugo 本地构建没有问题,GitHub Actions 里也能正常编译,但是最后推送到站点仓库时报了认证错误。

我最后把问题收敛到了三件事:

  • 外部仓库部署和单仓部署不是一回事。
  • 公开仓库不等于可以匿名写入。
  • token 不能明文写在 workflow 里,应该放到 GitHub Actions Secrets 中。

当前部署结构

我现在的博客不是直接把源码放在 hebohang/hebohang.github.io 里,而是分成了两层:

  • HebohangWebsiteHugo:存放 Hugo 源文件、主题和工作流。
  • hebohang/hebohang.github.io:作为最终发布仓库。

GitHub Actions 的流程是:

  1. 在源码仓库里执行 hugo --minify --gc
  2. 得到生成后的 public/
  3. 再把 public/ 推送到 hebohang/hebohang.github.iohugo 分支

所以这不是“当前仓库自己发布自己”,而是“当前仓库构建后,推送到另一个仓库”。

这次遇到的报错

报错大概是这样:

1
2
3
remote: Invalid username or token. Password authentication is not supported for Git operations.
fatal: Authentication failed for 'https://github.com/hebohang/hebohang.github.io.git/'
Error: Action failed with "The process '/usr/bin/git' failed with exit code 128"

出错的位置不是 Hugo 构建,而是部署阶段的:

1
git push origin hugo

也就是说,页面已经生成出来了,只是最后一步推送失败。

后来在把明文 token 改成 GitHub Actions secret 之后,我又遇到了第二种报错:

1
2
3
remote: Permission to hebohang/hebohang.github.io.git denied to hebohang.
fatal: unable to access 'https://github.com/hebohang/hebohang.github.io.git/': The requested URL returned error: 403
Error: Action failed with "The process '/usr/bin/git' failed with exit code 128"

这个 403 和前面的 Invalid username or token 不一样,它说明:

  • token 已经是有效的
  • GitHub 也识别出了这个 token 对应的账号
  • 但是这个 token 没有目标仓库的写权限

所以这里失败的不是“身份校验”,而是“授权不足”。

为什么公开仓库也会认证失败

一开始容易误会:hebohang/hebohang.github.io 是公开仓库,为什么还会认证失败?

原因很简单,公开仓库只代表任何人都能读,不代表任何人都能写。

我的 workflow 需要把内容推送到目标仓库,这本质上就是一次写操作,所以必须要有有效的写权限。

如果是把站点直接部署回当前仓库,很多时候可以使用 GITHUB_TOKEN
但我这里是推送到外部仓库,因此要额外准备能够写入这个目标仓库的凭据,通常就是:

  • PAT(Personal Access Token)
  • 或者 deploy key

我这次采用的是 PAT。

PAT 是什么

PAT 全称是 Personal Access Token,也就是“个人访问令牌”。

可以把它理解成 GitHub 提供给程序、命令行和 CI 使用的一种访问凭据。
它的作用有点像“专门给自动化流程使用的密码”,但是它比密码更细,可以单独限制权限、单独撤销,也更适合放进 CI 里。

在这个场景里,PAT 的作用就是:

  • 让 GitHub Actions 有权限把构建产物推送到 hebohang/hebohang.github.io

PAT 在哪里创建

登录 GitHub 之后,路径是:

  1. 右上角头像
  2. Settings
  3. Developer settings
  4. Personal access tokens

这里会看到两种 token:

  • Tokens (classic)
  • Fine-grained tokens

我更推荐 Fine-grained token,因为权限更细,也更安全。

PAT 该给什么权限

如果使用 Fine-grained token,我这里推荐的配置是:

  • Token owner:你的 GitHub 账号
  • Repository access:只选择 hebohang/hebohang.github.io
  • Repository permissions:
    • Contents: Read and write

这样就足够支撑这类部署流程了。

如果使用的是 Tokens (classic),通常会直接勾:

  • repo

它也能用,只是权限范围更大,不如 fine-grained 精确。

Fine-grained PAT 最容易配错的地方

真正需要写入的是目标仓库 hebohang/hebohang.github.io,而不是当前这个源码仓库 HebohangWebsiteHugo

所以如果你使用的是 fine-grained PAT,最常见的 403 原因通常是:

  • 选错了仓库,只授权了 HebohangWebsiteHugo
  • 选对了仓库,但没有给 Contents: Read and write

出现下面这种报错时:

1
Permission to hebohang/hebohang.github.io.git denied to hebohang

优先就检查这两个点。

Secrets 应该怎么配

就算拿到了 PAT,也不要把它直接写进仓库。

正确做法是把它放进 GitHub Actions Secrets:

  1. 打开源码仓库 HebohangWebsiteHugo
  2. 进入 Settings
  3. Secrets and variables
  4. Actions
  5. 新建一个 secret

我这里使用的名称是:

1
GH_PAGES_DEPLOY_TOKEN

然后把刚创建的 PAT 值填进去。

这样 workflow 就可以通过:

1
${{ secrets.GH_PAGES_DEPLOY_TOKEN }}

来读取这个凭据,而不是把 token 明文提交到 git 历史中。

workflow 的正确写法

我现在用的是 peaceiris/actions-gh-pages@v3,核心配置如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
- name: Build
  run: hugo --minify --gc

- name: Validate deploy secret
  if: github.event_name == 'push' && github.ref == 'refs/heads/main'
  env:
      GH_PAGES_DEPLOY_TOKEN: ${{ secrets.GH_PAGES_DEPLOY_TOKEN }}
  run: |
      if [ -z "$GH_PAGES_DEPLOY_TOKEN" ]; then
        echo "Missing GH_PAGES_DEPLOY_TOKEN secret." >&2
        exit 1
      fi      

- name: Deploy
  if: github.event_name == 'push' && github.ref == 'refs/heads/main'
  uses: peaceiris/actions-gh-pages@v3
  with:
      PERSONAL_TOKEN: ${{ secrets.GH_PAGES_DEPLOY_TOKEN }}
      EXTERNAL_REPOSITORY: hebohang/hebohang.github.io
      PUBLISH_BRANCH: hugo
      PUBLISH_DIR: ./public
      commit_message: ${{ github.event.head_commit.message }}

这里有几个点要注意:

1. 不要把 token 明文写进 workflow

错误示范:

1
PERSONAL_TOKEN: ghp_xxx

这会带来几个问题:

  • token 暴露在仓库里
  • 可能被 GitHub 自动吊销
  • 后续维护时也很容易忘记它来自哪里

如果 token 已经明文提交过,最稳妥的处理方式是:

  1. 删除明文引用
  2. 重新创建一个新的 token
  3. 旧 token 直接废弃

2. 只在 push main 时执行 deploy

PR 场景下通常只需要验证构建能不能通过,不应该真的往生产仓库推内容。
因此我这里把 deploy 限制在:

1
github.event_name == 'push' && github.ref == 'refs/heads/main'

这样 pull request 仍然可以跑 Hugo build,但不会触发外部仓库写入。

3. EXTERNAL_REPOSITORY 要写对

因为我这里是把产物推送到外部仓库,所以这个字段必须明确写成:

1
EXTERNAL_REPOSITORY: hebohang/hebohang.github.io

如果你自己的站点仓库不同,这里也要改成对应地址。

修复完成后怎么验证

可以按下面这个顺序检查:

本地验证

先确认本地构建没问题:

1
hugo --minify --gc

远端验证

然后推送一次提交,或者在 Actions 中手动重跑 workflow:

  1. Build 是否成功
  2. Validate deploy secret 是否通过
  3. Deploy 是否成功

如果成功,还可以继续检查:

  • hebohang/hebohang.github.iohugo 分支是否有新提交
  • 页面内容是否已经更新

常见排查点

如果还是失败,可以优先看下面几项:

1. secret 名称写错

代码里写的是:

1
${{ secrets.GH_PAGES_DEPLOY_TOKEN }}

那 GitHub 仓库里的 secret 名称也必须一模一样。

2. 把 secret 加错仓库

GH_PAGES_DEPLOY_TOKEN 要加在源码仓库 HebohangWebsiteHugo 的 Actions secrets 中,因为 workflow 是在这个仓库执行的。

3. token 没有目标仓库权限

如果 token 对 hebohang/hebohang.github.io 没有写权限,也会部署失败。

4. fine-grained PAT 授权到了错误的仓库

如果 token 只授权给了 HebohangWebsiteHugo,而没有授权给 hebohang/hebohang.github.io,也会得到 403。

5. token 已经过期或被撤销

重新生成一个新的 PAT 通常是最快的排查手段。

6. 把外部仓库和当前仓库混淆了

当前 workflow 是“源码仓库构建,再推送到站点仓库”。
所以权限检查要看的是目标仓库,不是源码仓库本身。

7. 用 classic PAT 做一次快速排查

如果你一时不确定是 fine-grained PAT 的哪个权限没配对,可以临时创建一个 classic PAT,直接授予:

1
repo

然后把它写入 GH_PAGES_DEPLOY_TOKEN 再跑一次 workflow。

如果 classic PAT 可以通过,而 fine-grained PAT 不行,那基本就可以确认问题出在 fine-grained PAT 的仓库选择或权限勾选上。

总结

这次问题看起来像是 Hugo 部署失败,实际上根因和 Hugo 本身关系不大,而是 GitHub Actions 的认证配置出了问题。

核心经验就是:

  • 公开仓库也不能匿名写入
  • 外部仓库部署通常需要单独的 PAT 或 deploy key
  • token 不要明文写在 workflow 里
  • Secrets 名称、仓库权限和目标仓库地址要完全对齐

如果只是单仓部署,事情会简单一些;但只要涉及“源码仓库”和“发布仓库”分离,认证和权限就必须单独处理清楚。