今天把“发布流程”这件事彻底梳理了一遍:新增了 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
  • 一键执行:
    • ./push_all.sh

两个脚本都支持 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 作为“有保护的强推”。
  • 无改动时退出的意义?

    • 持续写作时经常改了两下又撤掉;“无改动退出”可以减少无意义提交,历史更干净。

六、结语

发布这件事,最理想的体验就是“忘记它的存在”。 今天把三个脚本写全、写稳之后,日常发文就成了一句命令的事:

1
./push_all.sh

愿自己每一次记录,专注于内容本身。