通过git和ci操作博客#
hugo new的时候虽然会根据模板自动生成date,即hugo new的时间点。
但是我个人更喜欢date是发布的时间点。除此之外,还有draft之类的也需要在发布时更改更改。
优雅发布?#
有的懒人会选择单一分支发布,通过写个bash脚本来修改。
但是向我这种超级懒狗是不会用bash这么不优雅,也不方便的东西的,所以选择了通过github action来完成merge、修改date和draft状态等等流程,一步到位。
即便头铁写ci不完全依赖kimiv2大人,半问半写的时间比写博客还久,但是我就是要写ci!
实际上是有两方面考虑
一是我用的
cf pages部署,master分支和非master分支会生成不同的预览页面,而我只需要把master分支绑定到自己的域名上,即可对外隐藏没写完的draft。(即便压根就没有人看)二是如果用脚本,可能哪里写错了,一不小心直接把整个本地分支干没了,白干打击热情
需求分析#
- 自动判断是否需要合入master
- 合入之前,自动修改新的文章的
date和draft - 合入之后,删掉草稿分支
简单分解为这三步之后,ci就好写很多了。 一开始写的ci还是摸着石头过河,还要自己提pr才能触发,完全不好用,后面重新梳理了一下要求,把拆开来的yml合到同一个里面舒服多了。 虽然有桥,但是别问为什么要摸着石头。
编写ci配置#
github action不用多说,建一个.github/workflows酷酷写yml就行了。
在ci中我主要通过提pr的方式来合入,毕竟ci写错了还能自己关掉pr重新来过,留点余地。
因为后续涉及到分支修改等操作,所以还是要给相应的权限。
name: Auto PR on 'post:' prefix, update posts and merge PR
on:
push: # 每次push都检查一下条件
branches-ignore:
- master # 忽略 master 分支的 push
permissions: # 给写权限
contents: write
pull-requests: write
因为写博客可能会在这台机器写一点,那台机器写一点,所以不是每笔提交都需要合进master,更何况修ci那一大堆commit。
因此,我们需要判断哪笔提交需要合入,主要思路就是通过判断commit msg的关键字,简单又直接。
判断是否需要提pr#
工作中用得比较多的一般是.WIP前缀,提示ci不要触发。
但是写博客也不是什么很严谨的流程,要是忘了写.WIP被ci酷酷合入了不就炸缸了。
所以我采取的是白名单判断,msg有关键字才提pr。
jobs:
check-need-pr:
runs-on: ubuntu-latest
outputs: # 留两个标记给后面的流程用
has_prefix: ${{ steps.extract_title.outputs.has_prefix }}
pr_title: ${{ steps.extract_title.outputs.pr_title }}
steps:
- name: Checkout code
uses: actions/checkout@v3
# 获取commit msg
- name: Get latest commit message
id: get_commit
run: |
echo "commit_msg=$(git log -1 --pretty=%B)" >> $GITHUB_OUTPUT
- name: Check commit message prefix and extract title
id: extract_title
run: |
PREFIX="post:"
MSG="${{ steps.get_commit.outputs.commit_msg }}"
# 这里判断msg有'post:'前缀才提pr
if [[ "$MSG" == $PREFIX* ]]; then
# 去掉前缀和前后空格
TITLE="${MSG#$PREFIX}"
TITLE="${TITLE#"${TITLE%%[![:space:]]*}"}" # 去左空格
TITLE="${TITLE%"${TITLE##*[![:space:]]}"}" # 去右空格
echo "has_prefix=true" >> $GITHUB_OUTPUT
echo "pr_title=$TITLE" >> $GITHUB_OUTPUT
else
echo "has_prefix=false" >> $GITHUB_OUTPUT
fi
提pr#
因为我们的目标很简单,把当前分支合入master,所以很多地方写死即可
jobs:
# check-need-pr: ...
create-pr-and-merge:
runs-on: ubuntu-latest
needs: check-need-pr # 链式job
if: needs.check-need-pr.outputs.has_prefix == 'true' #如果不需要pr就不用走到这里了
steps:
- name: Checkout current branch
uses: actions/checkout@v3
with:
ref: ${{ github.ref }}
fetch-depth: 0
- name: Create PR
env: # 这里在env设置了token之后,就不要在gh cli上面使用token手动登陆了
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
sudo apt-get update
sudo apt-get install -y gh
BRANCH=$(git rev-parse --abbrev-ref HEAD)
TITLE="${{ needs.check-need-pr.outputs.pr_title }}"
# 这里做一个判断,检查是否已经有pr开着
# 因为成功的话同时只会有一个pr,所以不需要考虑多个pr没关的情况
EXISTING=$(gh pr list --base master --head "$BRANCH" --state open --json number -q '.[0].number')
# 有pr就直接复用,没有pr才提一个新的
if [ -n "$EXISTING" ]; then
echo "PR #$EXISTING already exists, skipping creation."
else
gh pr create --title "$TITLE" \
--body "Auto generated PR from commit" \
--base master \
--head "$BRANCH"
fi
- name: Checkout PR branch
uses: actions/checkout@v3
with:
ref: ${{ github.head_ref }}
fetch-depth: 0
使用gh这个cli的时候还是挺坑的,弱智gpt会设置了token env之后再获取一次token,手动登陆gh cli,然后被github拦下来报错。
修改草稿信息#
提好pr之后,我们就可以去修改一下新文章的信息了。 根据需求,我们要修改两部份
- 把
date改为当前时间 - 把
draft状态设为false,这样部署后的页面上不会出现【草稿】字样
关于第二点,其实是cf pages的一个问题引起的。
按道理来说,既然他可以自动把master和非master分支分开部署,那他应该也能针对这两种分支设置不同的编译命令。
但是我鼓捣了半天没发现怎么做,一旦设置了非生产分支的编译命令后(比如支持编译draft),生产分支的设置也会被同步修改。也就是说,master分支部署的页面也会把草稿编进去。
- name: Get changed markdown files in content/posts
id: changed_files
run: |
# 提取diff的文章
files=$(git diff --name-only origin/master HEAD | grep '^content/posts/.*\.md$' || true)
echo "files<<EOF" >> $GITHUB_OUTPUT
echo "$files" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Update date and draft in changed files
if: steps.changed_files.outputs.files != ''
run: |
now=$(TZ=Asia/Shanghai date +"%Y-%m-%dT%H:%M:%S%:z")
echo "Current time: $now"
while IFS= read -r file; do
if [ -z "$file" ]; then
continue
fi
echo "Processing $file"
# 修改文章时间
sed -i -E "s/date\s*=\s*'[^']*'/date = '2025-08-14T15:01:34+08:00'/g" "$file"
# 修改draft为false
sed -i -E "s/draft\s*=\s*true/draft = false/g" "$file"
done <<< "${{ steps.changed_files.outputs.files }}"
# 用bot commit
- name: Commit and push changes
if: steps.changed_files.outputs.files != ''
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add content/posts/*.md
if git diff --cached --quiet; then
echo "No changes to commit"
exit 0
fi
git commit -m "ci: update date and draft in posts [skip ci]"
BRANCH=$(git rev-parse --abbrev-ref HEAD)
git push origin HEAD:$BRANCH
最关键的其实是这一步
git diff --name-only origin/master HEAD \
| grep '^content/posts/.*\.md$' || true
通过指定diff master和当前分支的HEAD,来得到diff的文件。
有些写法可能会写成origin/master..HEAD 或者 origin/master...HEAD,这里不再赘述这几个点的作用。
我们的草稿分支很可能和master在很远的地方就已经叉开了,因此我们简单粗暴判断两个分支头的差异即可,只要保证草稿分支上的文章是master的超集。
合入master#
合入master就比较简单,用gh cli merge一下当前打开的pr即可。
- name: Auto squash merge PR
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
BRANCH=$(git rev-parse --abbrev-ref HEAD)
pr_number=$(gh pr list --base master --head "$BRANCH" --state open --json number -q '.[0].number')
if [ -z "$pr_number" ]; then
echo "No open PR found for branch $BRANCH"
exit 1
fi
echo "Merging PR #$pr_number"
gh pr merge "$pr_number" --squash # squash保持一次pr只有一个commit,美观
自动删除当前分支不需要通过ci配置,在仓库的Setting/General/Pull Requests, 勾选上Automatically delete head branches即可。这样成功合入master并关闭pr后,该分支会删除。
完整版#
name: Auto PR on 'post:' prefix, update posts and merge PR
on:
push:
branches-ignore:
- master # 忽略 master 分支的 push
permissions:
contents: write
pull-requests: write
jobs:
check-need-pr:
runs-on: ubuntu-latest
outputs:
has_prefix: ${{ steps.extract_title.outputs.has_prefix }}
pr_title: ${{ steps.extract_title.outputs.pr_title }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Get latest commit message
id: get_commit
run: |
echo "commit_msg=$(git log -1 --pretty=%B)" >> $GITHUB_OUTPUT
- name: Check commit message prefix and extract title
id: extract_title
run: |
PREFIX="post:"
MSG="${{ steps.get_commit.outputs.commit_msg }}"
if [[ "$MSG" == $PREFIX* ]]; then
# 去掉前缀和前后空格
TITLE="${MSG#$PREFIX}"
TITLE="${TITLE#"${TITLE%%[![:space:]]*}"}" # 去左空格
TITLE="${TITLE%"${TITLE##*[![:space:]]}"}" # 去右空格
echo "has_prefix=true" >> $GITHUB_OUTPUT
echo "pr_title=$TITLE" >> $GITHUB_OUTPUT
else
echo "has_prefix=false" >> $GITHUB_OUTPUT
fi
create-pr-and-merge:
runs-on: ubuntu-latest
needs: check-need-pr
if: needs.check-need-pr.outputs.has_prefix == 'true'
steps:
- name: Checkout current branch
uses: actions/checkout@v3
with:
ref: ${{ github.ref }}
fetch-depth: 0
- name: Create PR
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
sudo apt-get update
sudo apt-get install -y gh
BRANCH=$(git rev-parse --abbrev-ref HEAD)
TITLE="${{ needs.check-need-pr.outputs.pr_title }}"
EXISTING=$(gh pr list --base master --head "$BRANCH" --state open --json number -q '.[0].number')
if [ -n "$EXISTING" ]; then
echo "PR #$EXISTING already exists, skipping creation."
else
gh pr create --title "$TITLE" \
--body "Auto generated PR from commit" \
--base master \
--head "$BRANCH"
fi
- name: Checkout PR branch
uses: actions/checkout@v3
with:
ref: ${{ github.head_ref }}
fetch-depth: 0
- name: Get changed markdown files in content/posts
id: changed_files
run: |
files=$(git diff --name-only origin/master HEAD | grep '^content/posts/.*\.md$' || true)
echo "files<<EOF" >> $GITHUB_OUTPUT
echo "$files" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Update date and draft in changed files
if: steps.changed_files.outputs.files != ''
run: |
now=$(TZ=Asia/Shanghai date +"%Y-%m-%dT%H:%M:%S%:z")
echo "Current time: $now"
while IFS= read -r file; do
if [ -z "$file" ]; then
continue
fi
echo "Processing $file"
sed -i -E "s/date\s*=\s*'[^']*'/date = '2025-08-14T15:01:34+08:00'/g" "$file"
sed -i -E "s/draft\s*=\s*true/draft = false/g" "$file"
done <<< "${{ steps.changed_files.outputs.files }}"
- name: Commit and push changes
if: steps.changed_files.outputs.files != ''
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add content/posts/*.md
if git diff --cached --quiet; then
echo "No changes to commit"
exit 0
fi
git commit -m "ci: update date and draft in posts [skip ci]"
BRANCH=$(git rev-parse --abbrev-ref HEAD)
git push origin HEAD:$BRANCH
- name: Auto squash merge PR
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
BRANCH=$(git rev-parse --abbrev-ref HEAD)
pr_number=$(gh pr list --base master --head "$BRANCH" --state open --json number -q '.[0].number')
if [ -z "$pr_number" ]; then
echo "No open PR found for branch $BRANCH"
exit 1
fi
echo "Merging PR #$pr_number"
gh pr merge "$pr_number" --squash
编写并推送文章#
流程比较清晰明了,也可以很好隔离不同草稿
- 开一个新分支,写到一半的草稿推送上该分支,msg不要带
post:前缀 - 最终定稿之后,msg带
post:前缀即可 - push到github会自动触发ci,半分钟内即可更新到博客主页
