跳过正文
Background Image
  1. Posts/

通过git和ci操作博客

·2885 字·6 分钟·
目录

通过git和ci操作博客
#

hugo new的时候虽然会根据模板自动生成date,即hugo new的时间点。 但是我个人更喜欢date是发布的时间点。除此之外,还有draft之类的也需要在发布时更改更改。

优雅发布?
#

有的懒人会选择单一分支发布,通过写个bash脚本来修改。 但是向我这种超级懒狗是不会用bash这么不优雅,也不方便的东西的,所以选择了通过github action来完成merge、修改datedraft状态等等流程,一步到位。

即便头铁写ci不完全依赖kimiv2大人,半问半写的时间比写博客还久,但是我就是要写ci!

实际上是有两方面考虑

一是我用的cf pages部署,master分支和非master分支会生成不同的预览页面,而我只需要把master分支绑定到自己的域名上,即可对外隐藏没写完的draft。(即便压根就没有人看)

二是如果用脚本,可能哪里写错了,一不小心直接把整个本地分支干没了,白干打击热情

需求分析
#

  • 自动判断是否需要合入master
  • 合入之前,自动修改新的文章的datedraft
  • 合入之后,删掉草稿分支

简单分解为这三步之后,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,半分钟内即可更新到博客主页