任何东西都有丢失的可能性,除非你在丢失前做好了备份!本文将讲解如何使用 GitHub Actions 实现自动备份代码仓。

目标

本文的目标是将 GitHub 用户 user-a 下的所有代码仓同步到 GitHub 用户 user-b 下。

准备

新建一个代码仓,代码仓的目录结构下面这样:

.
├── .github
│   └── workflows
│       └── mirror.yaml

并在代码仓中设置三个 Secret,分别是:

  • GH_TOKEN_A 对应 GitHub 用户 user-a 的 AccessToken
  • GH_TOKEN_B 对应 GitHub 用户 user-b 的 AccessToken
  • SLACK_WEBHOOK 对应 Slack WebHook 地址(可选)

工作流

获取代码仓列表

使用 GitHub CLI gh 获取 GitHub 用户 user-a 下的所有代码仓

echo ${GH_TOKEN_A} > gh_token_a # 将 user-a 的 GitHub AccessToken 写入文件
gh auth login --with-token < gh_token_a # GitHub CLI 登录
gh repo list user-a -L 1000 > a_repos # 获取 user-a 的所有代码仓并写入文件
cat a_repos # 显示代码仓列表
cat a_repos | wc -l # 显示代码仓总数

创建同名代码仓

在 GitHub 用户 user-b 下创建同名代码仓。

gh repo create user-b/${repo_name} --private --description "${repo}" -y || true # 忽略错误,代码仓可能已经存在

克隆代码仓

使用 --bare 参数克隆 GitHub 用户 user-a 下的代码仓。

git clone --bare https://${GH_TOKEN_A}@github.com/user-a/${repo_name}.git ${repo_name}

推送代码

使用 --all--mirror 参数分别进行代码推送,防止推送的数据过大导致推送失败。

  • --all 表示推送所有的分支
  • --mirror 表示推送 refs/ 下的所有引用,包括分支、标签等
cd ${repo_name}
mirror_repo="https://${GH_TOKEN_B}@github.com/user-b/${repo_name}.git"
git push --all -f ${mirror_repo} || true
git push --mirror -f ${mirror_repo} || true

优化

  • 忽略体积过大或者不需要备份的代码仓
# 为了保证代码仓名称判断的准确性,在定义和判断时,在每个代码仓名称的左右添加斜杆
IGNORE_REPOS="/repo_a/repo_b/"

[[ ${IGNORE_REPOS} =~ "/${repo_name}/" ]] && continue || true
  • 对于 tag 数量太多的代码仓,仅备份分支
ONLY_BRANCH_REPOS="/repo_c/repo_d/"

[[ ${ONLY_BRANCH_REPOS} =~ "/${repo_name}/" ]] && continue || true
  • 由于 GitHub Actions 的时区为 UTC,定时任务的时间需要 -8h

周一到周五的凌晨2点:

  • 一般配置:0 2 * * 1-5
  • GitHub Actions 定时任务:0 18 * * 0-4

完整的工作流配置

.github/workflows/mirror.yaml 文件的内容:

name: Mirror repos

on:
  schedule:
    - cron: "0 18 * * 0-4" # 设置定时任务,周一到周五的凌晨2点进行备份
  workflow_dispatch: # 手动触发构建

jobs:
  mirror:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: GitHub CLI version
        run: gh --version

      - name: List repos
        env:
          GH_TOKEN_A: ${{ secrets.GH_TOKEN_A }}
        run: |
          echo ${GH_TOKEN_A} > gh_token_a
          gh auth login --with-token < gh_token_a
          gh repo list user-a -L 1000 > a_repos
          cat a_repos
          cat a_repos | wc -l          

      - name: Mirror repos
        env:
          GH_TOKEN_A: ${{ secrets.GH_TOKEN_A }}
          GH_TOKEN_B: ${{ secrets.GH_TOKEN_B }}
          IGNORE_REPOS: "/repo_a/repo_b/"
          ONLY_BRANCH_REPOS: "/repo_c/repo_d/"
        run: |
          echo ${GH_TOKEN_B} > gh_token_b
          gh auth login --with-token < gh_token_b

          mkdir repos
          cd repos
          set -x
          cat ${GITHUB_WORKSPACE}/a_repos | while read repo; do
            repo_name=$(echo ${repo} | awk '{print $1}' | awk -F/ '{print $2}')
            [[ ${IGNORE_REPOS} =~ "/${repo_name}/" ]] && continue || true

            gh repo create user-b/${repo_name} --private --description "${repo}" -y || true
            rm -rf ${repo_name}

            git clone --bare https://${GH_TOKEN_A}@github.com/user-a/${repo_name}.git ${repo_name}

            cd ${repo_name}
            mirror_repo="https://${GH_TOKEN_B}@github.com/user-b/${repo_name}.git"
            git push --all -f ${mirror_repo} || true
            [[ ${ONLY_BRANCH_REPOS} =~ "/${repo_name}/" ]] && continue || true
            git push --mirror -f ${mirror_repo} || true
            cd -
          done          

      - name: Slack Notification
        uses: rtCamp/action-slack-notify@v2
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}