今天把“发布流程”这件事彻底梳理了一遍:新增了 3 个脚本,分别负责推送源码、构建并推送静态站点,以及一键串联两步。它们很短,但细节不少,我把每行写清楚,方便未来自己或后来者复用与调整。
为什么要做这三个脚本#
- 源码仓库(
hugo_blog
)与静态站点仓库(jwzhukevin.github.io
)是两个独立 Git 仓库,步骤容易混。 - 不同机器的 Git 默认分支、远程命名、权限方式不一致,手敲命令易出错。
- 需要规范 Commit 信息、给出可读的帮助提示,并在“无改动”时快速退出,节省时间。
一、源码推送脚本 push_source.sh(逐行讲解)#
先贴完整脚本,再按行解释:
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
| #!/bin/bash
set -euo pipefail
PROJECT_ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
EXPECTED_REMOTE="git@github.com:jwzhukevin/hugo_blog.git"
TARGET_BRANCH="main"
print_usage() {
cat <<EOF
Usage: $(basename "$0") [-m "commit message"]
Push blog source to $EXPECTED_REMOTE ($TARGET_BRANCH).
Options:
-m, --message Commit message (overrides COMMIT_NOTE)
-h, --help Show this help and exit
Environment variables:
COMMIT_NOTE Optional commit message if -m is not provided
Commit message default:
chore(source): update YYYY-MM-DD HH:MM:SS
EOF
}
MESSAGE=""
while [[ $# -gt 0 ]]; do
case "$1" in
-m|--message)
shift
MESSAGE=${1:-}
[[ -z "${MESSAGE}" ]] && { echo "Error: -m|--message requires an argument" >&2; exit 2; }
shift
;;
-h|--help)
print_usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
print_usage
exit 2
;;
esac
done
cd "$PROJECT_ROOT_DIR"
# Basic checks
command -v git >/dev/null 2>&1 || { echo "Error: git is not installed or not in PATH" >&2; exit 1; }
[[ -f "config.yml" ]] || { echo "Error: config.yml not found; please run from project root" >&2; exit 1; }
# Ensure git repo
[[ -d ".git" ]] || { echo "Error: .git not found; initialize the repo first" >&2; exit 1; }
# Validate or set remote
CURRENT_REMOTE_URL=$(git remote get-url origin 2>/dev/null || true)
if [[ -z "$CURRENT_REMOTE_URL" ]]; then
echo "origin remote not found; configuring to $EXPECTED_REMOTE"
git remote add origin "$EXPECTED_REMOTE"
else
if [[ "$CURRENT_REMOTE_URL" != "$EXPECTED_REMOTE" ]]; then
echo "Error: origin remote is '$CURRENT_REMOTE_URL' but expected '$EXPECTED_REMOTE'" >&2
echo " Please fix the remote URL or run: git remote set-url origin $EXPECTED_REMOTE" >&2
exit 1
fi
fi
# Stage changes
git add -A
# Skip if no staged changes
if git diff --cached --quiet --ignore-submodules --; then
echo "No source changes to commit; exiting."
exit 0
fi
# Compose commit message
if [[ -n "${MESSAGE}" ]]; then
COMMIT_MSG="$MESSAGE"
elif [[ -n "${COMMIT_NOTE:-}" ]]; then
COMMIT_MSG="$COMMIT_NOTE"
else
COMMIT_MSG="chore(source): update $(date "+%F %T")"
fi
git commit -m "$COMMIT_MSG"
echo "Pushing to origin $TARGET_BRANCH ..."
git push origin "$TARGET_BRANCH"
echo "Done."
|
逐行说明:
- 1 行:声明用 Bash 解释器。
- 3 行:
set -euo pipefail
开启“严格模式”:出错即停(-e)、未定义变量报错(-u)、管道任一段失败即失败(pipefail)。 - 5–7 行:计算项目根目录、期待的远程地址和目标分支。
- 9–25 行:
print_usage
打印帮助。这里用 heredoc (<<EOF)
,便于对齐排版。 - 27–47 行:解析命令行选项,支持
-m|--message
自定义提交信息、-h|--help
查看帮助;未识别参数会报错并给出用法。 - 49 行:切换到项目根,确保后续路径相对正确。
- 52 行:校验环境:必须安装 git。
- 54 行:确保
config.yml
存在(防止脚本误从错误目录运行)。 - 57 行:确认当前目录是一个 Git 仓库。
- 60–70 行:校验/设置
origin
远程:- 没有就自动添加为预期地址;
- 有但与预期不一致则报错并给出修复指令,避免把源码推错仓库。
- 73 行:一次性暂存全部改动。
- 76–79 行:如果没有暂存改动,则立即退出(“无事可做”)。
- 82–88 行:生成提交信息,优先级:命令行
-m
> 环境变量 COMMIT_NOTE
> 默认前缀 + 时间戳。 - 90–95 行:提交并推送到
main
,结尾打印 Done.
。
小结:这段脚本确保“远程仓库正确”“无改动就不提交”,同时提供一致的提交信息格式,减少口误和空推送。
二、静态站点推送脚本 push_static.sh(逐行讲解)#
完整脚本:
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
| #!/bin/bash
set -euo pipefail
PROJECT_ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
PUBLIC_DIR="$PROJECT_ROOT_DIR/public"
EXPECTED_REMOTE="git@github.com:jwzhukevin/jwzhukevin.github.io.git"
TARGET_BRANCH="master"
FORCE_PUSH=false
print_usage() {
cat <<EOF
Usage: $(basename "$0") [-m "commit message"] [--force]
Build with Hugo and push static site from ./public to $EXPECTED_REMOTE ($TARGET_BRANCH).
Options:
-m, --message Commit message (overrides COMMIT_NOTE)
--force Use --force-with-lease when pushing
-h, --help Show this help and exit
Environment variables:
COMMIT_NOTE Optional commit message if -m is not provided
Commit message default:
chore(static): publish YYYY-MM-DD HH:MM:SS
EOF
}
MESSAGE=""
while [[ $# -gt 0 ]]; do
case "$1" in
-m|--message)
shift
MESSAGE=${1:-}
[[ -z "${MESSAGE}" ]] && { echo "Error: -m|--message requires an argument" >&2; exit 2; }
shift
;;
--force)
FORCE_PUSH=true
shift
;;
-h|--help)
print_usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
print_usage
exit 2
;;
esac
done
cd "$PROJECT_ROOT_DIR"
# Basic checks
command -v git >/dev/null 2>&1 || { echo "Error: git is not installed or not in PATH" >&2; exit 1; }
command -v hugo >/dev/null 2>&1 || { echo "Error: hugo is not installed or not in PATH" >&2; exit 1; }
[[ -f "config.yml" ]] || { echo "Error: config.yml not found; please run from project root" >&2; exit 1; }
echo "Building site with Hugo ..."
hugo --minify -d "$PUBLIC_DIR"
mkdir -p "$PUBLIC_DIR"
cd "$PUBLIC_DIR"
# Initialize or validate git repo in public/
if [[ ! -d .git ]]; then
git init
# Ensure default branch is master (for newer git versions default may be 'main')
git symbolic-ref HEAD refs/heads/$TARGET_BRANCH || true
git remote add origin "$EXPECTED_REMOTE"
else
CURRENT_REMOTE_URL=$(git remote get-url origin 2>/dev/null || true)
if [[ -z "$CURRENT_REMOTE_URL" ]]; then
git remote add origin "$EXPECTED_REMOTE"
elif [[ "$CURRENT_REMOTE_URL" != "$EXPECTED_REMOTE" ]]; then
echo "Error: origin remote in public/ is '$CURRENT_REMOTE_URL' but expected '$EXPECTED_REMOTE'" >&2
echo " Please fix: (cd public && git remote set-url origin $EXPECTED_REMOTE)" >&2
exit 1
fi
fi
# Ensure .nojekyll exists to bypass Jekyll on GitHub Pages
touch .nojekyll
git add -A
if git diff --cached --quiet --ignore-submodules --; then
echo "No static changes to publish; exiting."
exit 0
fi
if [[ -n "${MESSAGE}" ]]; then
COMMIT_MSG="$MESSAGE"
elif [[ -n "${COMMIT_NOTE:-}" ]]; then
COMMIT_MSG="$COMMIT_NOTE"
else
COMMIT_MSG="chore(static): publish $(date "+%F %T")"
fi
git commit -m "$COMMIT_MSG"
PUSH_ARGS=("origin" "$TARGET_BRANCH")
if [[ "$FORCE_PUSH" == true ]]; then
PUSH_ARGS+=("--force-with-lease")
fi
echo "Pushing static site to origin $TARGET_BRANCH ..."
git push "${PUSH_ARGS[@]}"
echo "Done."
|
逐行说明要点:
- 1–3 行:同样开启 Bash 严格模式。
- 5–9 行:确定项目根、
public
目录、静态站远程、目标分支和是否强推的开关。 - 11–28 行:帮助文本,强调“构建并推送
./public
到 GitHub Pages 仓库”。 - 32–54 行:解析
-m
、--force
、-h
选项;未识别参数直接报错并附用法。 - 56–63 行:校验
git
/hugo
/config.yml
,缺一不可。 - 65 行:
hugo --minify -d public
构建到 public/
,产物更小。 - 71–86 行:在
public/
目录初始化或校验远程:- 第一次构建会
git init
,并把 HEAD 指向 master
(有些 Git 默认是 main)。 - 如果已有
origin
但不是预期地址,则报错并给出修复命令,避免误推。
- 89–90 行:创建
.nojekyll
,让 GitHub Pages 不跑 Jekyll(防止忽略下划线开头的资源等)。 - 93–96 行:若没有静态变更,直接退出(避免空提交)。
- 98–106 行:组装提交信息,和源码脚本逻辑一致。
- 108–114 行:支持
--force-with-lease
的安全强推(默认不开)。
三、一键脚本 push_all.sh(逐行讲解)#
完整脚本:
1
2
3
4
| #!/bin/bash
./push_source.sh
./push_static.sh
|
说明:
- 1 行:Bash 解释器。
- 3 行:先推送源码(保证配置和模板落库)。
- 4 行:再构建并推送静态站(确保 public 与源码一致)。
实际使用时,我把它设为常用命令,配合 SSH key 的 agent(或把私钥解锁缓存),就可以“一键发布”。
四、如何使用与可选参数#
- 只推源码:
./push_source.sh -m "fix: 调整导航高亮逻辑"
- 只推静态:
./push_static.sh -m "chore: 发布 2025-08-12"
- 遇到历史产物冲突时(极少数场景)可附加
--force
:./push_static.sh --force
- 一键执行:
两个脚本都支持 COMMIT_NOTE
环境变量:
1
2
| COMMIT_NOTE="docs: 更新构建指标截图" ./push_source.sh
COMMIT_NOTE="release: 发布 diary" ./push_static.sh
|
五、设计取舍与踩坑记录#
为何强校验远程地址?
- 同时维护两个仓库时,误推一次就很痛。明确远程是“预期地址”,可以在第一时间阻止错误发生。
为什么在 public/
里单独初始化 Git?
- GitHub Pages 部署的是纯静态文件,独立仓库清爽且回滚简单;每次构建只增量提交产物。
为什么默认不开 --force
?
- 强推会覆盖远程历史,除非明确知道自己在做什么,否则尽量避免。提供
--force-with-lease
作为“有保护的强推”。
无改动时退出的意义?
- 持续写作时经常改了两下又撤掉;“无改动退出”可以减少无意义提交,历史更干净。
六、结语#
发布这件事,最理想的体验就是“忘记它的存在”。
今天把三个脚本写全、写稳之后,日常发文就成了一句命令的事:
愿自己每一次记录,专注于内容本身。