Hexo 双仓库 GitHub Actions 自动化部署踩坑记录

一、双仓库部署架构说明

早期采用 Hexo 博客经典双仓库 CI/CD 架构:

  • 源码仓库:存放 Hexo 源码、配置、Markdown 文章,日常开发与提交

  • Pages 静态仓库用户名.github.io 专属仓库,用于托管最终生成的静态页面、对外提供博客访问服务

期望实现:本地推送源码仓库 main 分支 → GitHub Actions 自动构建静态资源 → 跨仓库自动发布到 Pages 仓库,完成博客更新。

二、核心问题:跨仓库部署 403 权限拒绝

配置完成后,流水线持续报错:

remote: Permission to xxx.github.io.git denied to github-actions[bot].

fatal: unable to access ... The requested URL returned error: 403

问题根因:

  1. GitHub 内置的 GITHUB_TOKEN仅对当前仓库有权限,无法跨仓库推送代码;

  2. 双仓库模式下,Actions 机器人需要操作另一个独立仓库,直接触发权限拦截,返回 403;

  3. 即使两个仓库归属同一账号、存在 Fork 关联,Fork 工作流也无法解决跨仓写入权限问题,官方安全策略严格限制跨仓库资源写入。

三、双仓库模式衍生痛点

1. 权限配置繁琐、极易出错

必须手动创建永久 PAT 令牌、配置 repo 全量权限、在源码仓库配置 Secrets,步骤冗余。一旦令牌过期、权限缺失、Secret 配置错误,流水线直接挂掉,无兜底机制。

2. 分支保护冲突,部署规则受限

Pages 仓库默认 main 分支受系统保护,禁止同分支覆盖写入。必须额外修改部署分支为 gh-pages,同时同步修改 GitHub Pages 访问分支配置,增加大量适配成本。

3. 故障排查成本高

跨仓库报错模糊,常见 403 权限拒绝git 128 异常、分支冲突、资源路径404等连锁问题,新手极难定位根因,整体稳定性极差。

四、最终解决方案:弃用双仓库,改用单仓库部署

对比踩坑成本后,放弃双仓库架构,切换为单仓库标准工作流

  • 唯一仓库:用户名.github.io

  • main 分支:存放全部 Hexo 源码、文章、配置(日常迭代提交)

  • gh-pages 分支:Actions 自动生成、自动推送静态资源

优势:

  1. 无需手动配置 PAT 令牌,内置 GITHUB_TOKEN 完全够用;

  2. 无跨仓库权限问题,彻底解决 403 报错;

  3. 流程极简、社区标准、稳定性极高,几乎零运维;

  4. 分支隔离规范,源码与静态资源互不干扰,无覆盖风险。

五、总结

Hexo 双仓库自动化部署方案理论可行,实际落地坑极多,核心瓶颈为 GitHub 跨仓库权限机制限制,衍生大量配置、权限、分支冲突问题。对于个人博客场景,单仓库 + main/gh-pages 双分支隔离 是最优、最稳、最省心的 CI/CD 方案。

(注:文档部分内容可能由 AI 生成)

Hexo GitHub Pages 踩坑笔记

本文记录我搭建 Hexo 博客过程中遇到的各种坑和解决方案,希望能帮到遇到同样问题的朋友。

坑 1:Hexo 7 的破坏性更新

Hexo 7 移除了许多默认依赖,导致构建失败:

问题表现:

  • SCSS 文件无法编译
  • EJS 模板显示原始代码
  • hexo server 命令不存在

解决方案:

1
2
3
4
5
6
7
8
# 安装 SCSS 编译器
npm install sass

# 安装 EJS 渲染器
npm install hexo-renderer-ejs

# 安装开发服务器
npm install hexo-server

坑 2:GitHub Actions 部署权限错误

问题表现:

1
2
remote: Permission to xxx/xxx.github.io.git denied to github-actions[bot].
fatal: unable to access 'https://github.com/xxx/xxx.github.io.git/': The requested URL returned error: 403

原因: GitHub Actions 默认的 GITHUB_TOKEN 没有写权限

解决方案: 在 workflow 中添加权限配置:

1
2
permissions:
contents: write

坑 3:CSS 文件 404 错误

问题表现: 网站打开后样式丢失,浏览器控制台报 main.css: 404

原因: NexT 主题使用 Stylus 编写样式,需要 Stylus 渲染器

解决方案:

1
npm install hexo-renderer-stylus

坑 4:主题切换的坑

最初我尝试使用 npm install hexo-theme-next 安装主题,但生成的内容没有样式。

根本原因: 主题文件没有被正确加载,导致 main.styl 不会被编译。

最终方案: 使用 Git Submodule 管理主题

1
2
3
4
5
6
# 初始化主题子模块
git submodule add https://github.com/ahonn/hexo-theme-even.git themes/even

# 更新主题
cd themes/even
git pull origin master

优势:

  • 主题独立管理,不污染主仓库
  • 版本可锁定,避免破坏性更新
  • 方便后期维护和升级

坑 5:CI/CD 中 SCSS 编译失败

问题表现: 本地构建正常,GitHub Actions 构建失败

原因: 工作流中没有调用 SCSS 编译脚本

解决方案:

  1. 创建编译脚本 scripts/compile-scss.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const sass = require('sass');
const fs = require('fs');
const path = require('path');

function compileScss() {
const scssPath = 'themes/even/source/css/style.scss';
const cssPath = 'themes/even/source/css/style.css';

const result = sass.compile(scssPath);
fs.writeFileSync(cssPath, result.css);
console.log('SCSS compiled successfully!');
}

compileScss();
  1. 修改 package.json
1
2
3
4
5
{
"scripts": {
"build": "node scripts/compile-scss.js && hexo generate"
}
}

坑 6:子模块提交找不到

问题表现:

1
Fetched in submodule path 'themes/even', but it did not contain xxx. Direct fetching of that commit failed.

原因: 在子模块中做了修改但没有推送到远程

教训: 不要修改第三方主题仓库的代码!

坑 7:双仓库部署的权限噩梦(血的教训!)

在采用同一个仓库、不同分支的方案之前,我踩过更大的坑——分两个 GitHub 仓库部署:

  • 一个 private 私有仓库 存放 Hexo 源码
  • 一个 public 公开仓库(即 homleening.github.io)存放生成的静态文件

方案设想

1
2
3
4
5
6
7
8
9
private 仓库 (源码)                    public 仓库 (静态文件)
main 分支 main 分支
├── source/ ├── index.html
├── themes/ ├── css/
└── _config.yml └── js/
│ ▲
│ GitHub Actions 自动部署 │
└──────────────────────────────────────────┘
编译生成 → 推送到 public 仓库

遇到的问题

1. 默认 GITHUB_TOKEN 权限不足

GitHub Actions 默认的 GITHUB_TOKEN 只能作用于当前仓库,无法推送到其他仓库(无论是 public 还是 private)。

2. 403 权限错误

1
2
remote: Permission to homleening/homleening.github.io.git denied to github-actions[bot].
fatal: unable to access 'https://github.com/homleening/homleening.github.io.git/': The requested URL returned error: 403

3. Fork 工作流也行不通

即使是 Fork 关系,跨仓库的自动部署依然受 Token 权限限制。

尝试过的方案

方案一:使用 Deploy Key

  • 为 public 仓库配置 SSH Deploy Key
  • 在 private 仓库的 Secrets 中添加私钥
  • 问题:需要精细管理密钥对,每次更换密钥都麻烦
  • 问题:没有 fine-grained 权限控制

方案二:使用 GITHUB_TOKEN + 手动触发

  • 不行!GITHUB_TOKEN 跨仓库天然受限

方案三:创建 Personal Access Token (PAT)

  • 在 GitHub Settings → Developer settings → Personal access tokens
  • 勾选 repo 全权限或 public_repo 权限
  • 将 Token 添加到 private 仓库的 Secrets 中
  • 最终能工作,但有隐患!

使用高权限 PAT 的痛点

  1. Token 泄露风险

    • 一旦 PAT 泄露,攻击者拥有你所有仓库的完全控制权
    • 日志中如果不小心打印 Token,会永久泄露
  2. 权限过度授予

    • 经典 PAT 只能选 repo 全权限
    • 无法只授予”推送代码到指定仓库”的最小权限
    • 违背最小权限原则
  3. Token 管理复杂

    • 多个项目需要多个 Token
    • 定期轮换 Token 很麻烦
    • Token 失效后所有部署都会失败
  4. 审计困难

    • 不知道是哪个 workflow 在用这个 Token
    • 出问题难以追踪

顿悟:同仓库不同分支才是最佳实践

核心思想: 既然 GITHUB_TOKEN 默认有当前仓库的所有权限,那就把源代码和静态文件放在同一个仓库

1
2
3
4
5
6
7
8
9
10
同一个仓库 (homleening.github.io)
├── main 分支(默认分支,存放 Hexo 源码)
│ ├── source/
│ ├── themes/
│ └── _config.yml

└── gh-pages 分支(自动生成,存放静态文件)
├── index.html
├── css/
└── js/

优势:

  1. 零额外配置GITHUB_TOKEN 默认就能推送
  2. 零安全风险:不需要管理 PAT
  3. 零成本:GitHub Pages 本身免费
  4. 权限最小化:只需要 contents: write 即可
  5. 审计清晰:所有操作都在一个仓库内

经验总结

方案 安全性 复杂度 推荐度
双仓库 + PAT ❌ 低(Token 泄露风险) ❌ 高
双仓库 + Deploy Key ⚠️ 中 ⚠️ 中 ⭐⭐
同仓库不同分支 ✅ 高 ✅ 低 ⭐⭐⭐⭐⭐

教训:能用 GitHub 原生机制解决的,就不要引入额外的安全风险。同仓库 + 不同分支 是 GitHub Pages 自动部署的最佳实践!

总结

搭建博客的过程虽然充满挑战,但每解决一个问题都是一次成长。希望这篇踩坑笔记能帮到你!

HDU 1011 Starship Troopers(星河战队)题解

HDU 1011 Starship Troopers(星河战队)题解

题目来源:HDU http://acm.hdu.edu.cn/showproblem.php?pid=1011

算法类型:树形DP / 树形背包

通过率:Total Submission(s): 6816   Accepted Submission(s): 1845

题目信息

Time Limit: 10000/5000 MS (Java/Others)

Memory Limit: 65536/32768 K (Java/Others)

Problem Description

You, the leader of Starship Troopers, are sent to destroy a base of the bugs. The base is built underground. It is actually a huge cavern, which consists of many rooms connected with tunnels. Each room is occupied by some bugs, and their brains hide in some of the rooms. Scientists have just developed a new weapon and want to experiment it on some brains. Your task is to destroy the whole base, and capture as many brains as possible.

To kill all the bugs is always easier than to capture their brains. A map is drawn for you, with all the rooms marked by the amount of bugs inside, and the possibility of containing a brain. The cavern’s structure is like a tree in such a way that there is one unique path leading to each room from the entrance. To finish the battle as soon as possible, you do not want to wait for the troopers to clear a room before advancing to the next one, instead you have to leave some troopers at each room passed to fight all the bugs inside. The troopers never re-enter a room where they have visited before.

A starship trooper can fight against 20 bugs. Since you do not have enough troopers, you can only take some of the rooms and let the nerve gas do the rest of the job. At the mean time, you should maximize the possibility of capturing a brain. To simplify the problem, just maximize the sum of all the possibilities of containing brains for the taken rooms. Making such a plan is a difficult job. You need the help of a computer.

Input

The input contains several test cases. The first line of each test case contains two integers N (0 < N <= 100) and M (0 <= M <= 100), which are the number of rooms in the cavern and the number of starship troopers you have, respectively. The following N lines give the description of the rooms. Each line contains two non-negative integers – the amount of bugs inside and the possibility of containing a brain, respectively. The next N - 1 lines give the description of tunnels. Each tunnel is described by two integers, which are the indices of the two rooms it connects. Rooms are numbered from 1 and room 1 is the entrance to the cavern.

The last test case is followed by two -1’s.

Output

For each test case, print on a single line the maximum sum of all the possibilities of containing brains for the taken rooms.

Sample Input

Text
1
2
3
4
5
6
7
8
9
10
11
12
13
5 10
50 10
40 10
40 20
65 30
70 30
1 2
1 3
2 4
2 5
1 1
20 7
-1 -1

Sample Output

Text
1
2
50
7

中文题意描述

你作为星河战队的队长,被派去消灭地下臭虫基地。基地由若干房间和通道组成,整体为树形结构,每个房间有且仅有一条通往入口(1号房间)的路径。

每个房间存在一定数量的臭虫,以及对应的大脑虫价值。每1名士兵可以消灭20只臭虫,攻占房间需要消耗对应数量的士兵。行进规则为:经过房间必须留守士兵清剿,士兵不会重复访问房间。

士兵数量有限,无法攻占所有房间,需要合理选择攻占房间,最大化获取的大脑虫总价值

解题思路

  1. 房间构成树形结构,访问子节点必须经过父节点,属于树形依赖背包问题

  2. 每个房间所需士兵数:bug数量 / 20,向上取整;

  3. 通过深度优先搜索遍历所有可行路径,枚举所有合法房间组合,计算士兵消耗与最大价值;

  4. 回溯遍历所有树形分支,剔除士兵不足的方案,保留最优解。

AC代码(Java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
* for 杭电 ACM1011
* @author 718225250
*
*/
public class Main{

public static class Cavern{

Integer id;
int bugs;
int brains;
int troopers;
boolean isPass = false;//是否已经过
List<Integer> next = new ArrayList<Integer>();
public Cavern(int id, int bugs, int brains){
this.id = Integer.valueOf(id);
this.bugs = bugs;
this.brains = brains;
this.troopers = (bugs%20==0 ? bugs/20 : bugs/20+1);
}

}
/**
* 获得一次遍历的最大值
* @return
*/
public static int getMax(){
int Max =curMax;
int id = _list.get(_list.size()-1);
boolean hasNextForPass = false;/*是否还有下一个节点供遍历*/
for(int i=0;i< _caverns[id].next.size();i++){
Integer nextId = _caverns[id].next.get(i);
if(!_caverns[nextId].isPass){
hasNextForPass = true;
_list.add(nextId);
_caverns[nextId].isPass = true;
Max = getMax();
_caverns[nextId].isPass = false;
_list.remove(_list.size()-1);
}
}
if(!hasNextForPass){
int tempMax = getResult() ;
if(tempMax > Max){
curMax = tempMax;
Max = curMax;
}
}
return Max;
}

/**
* 获得结果
* @return
*/

public static int getResult(){
int result = 0;
int tempTrooperNumber = __TrooperNumber;
int tempResult = 0;
boolean flag = true;
for(int i =0; i < _list.size(); i++ ){
Integer id = _list.get(i);
if(tempTrooperNumber >= _caverns[id].troopers){
tempTrooperNumber -= _caverns[id].troopers;
tempResult += _caverns[id].brains;
}
else{
flag = false;
break;
}
}
if(flag){
result = tempResult;
}else{
tempResult = 0;
for(int i =0; i < _list.size(); i++ ){
Integer id = _list.get(i);
_list.remove(id);
tempResult = getResult();
if(tempResult > result){
result = tempResult;
}
_list.add(id);
}
}
return result;
}


public static int __CavernNumber;
public static int __TrooperNumber;
public static Cavern[] _caverns;
public static int curMax;
public static final List<Integer> _list = new ArrayList<Integer>();
public static void main(String[] args){

Scanner in=new Scanner(System.in);
while(true){
__CavernNumber = in.nextInt();
__TrooperNumber = in.nextInt();
if( __CavernNumber < 0 && __TrooperNumber < 0 ){
break;
} else if(__CavernNumber == 0 || __TrooperNumber == 0){
System.out.println(0);
}
else{
_caverns = new Cavern[__CavernNumber];
for(int i=0;i<__CavernNumber;i++){
int bugs = in.nextInt();
int brains = in.nextInt();
_caverns[i] = new Cavern(i, bugs, brains);
}
for(int i=0;i<__CavernNumber-1;i++){
int m = in.nextInt();
int n = in.nextInt();
_caverns[m-1].next.add(n-1);
_caverns[n-1].next.add(m-1);
}
curMax = 0;
Integer start = Integer.valueOf(0);
_caverns[0].isPass = true;
_list.add(start);
System.out.println(getMax());
}
}

}
}

代码说明

  1. Cavern 内部类:存储每个房间的虫子数量、大脑价值、所需士兵、邻接节点及访问状态;

  2. getMax() 递归回溯:深度优先遍历整棵树,枚举所有可行访问路径;

  3. getResult() 价值计算:校验当前路径士兵是否充足,不足则剔除节点递归求解最优解;

4.树形建图:采用双向邻接表存储,遍历过程中通过访问状态去重,避免回路。


版权声明:本文为原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。