原理:前端 Cloudflare worker ,接收用户的参数,访问 Github Action api,激活 Action 下载镜像,同步到阿里云镜像仓库,然后将镜像拉取命令保存私库

1、阿里云创建仓库

https://cr.console.aliyun.com/

收集 4 个值

  • 用户名
  • 密码
  • 仓库地址
  • 命名空间

2、Github 【私库】

新建一个【私库】,专门用来存放生成的镜像列表,Action执行后,会在仓库生成一个 images_list.txt 文件

里面记载了所有同步到阿里云的镜像的拉取命令,方便以后查找

创建的时候,勾选生成 .md 文件

2、Github 【Action库】

新建一个公开的库,主要执行Action用,称为【Action库】

创建2个 github token

https://github.com/settings/tokens?type=beta

两个仓库,分别创建一个限权token:

  • 【私库】token 需填入 Action 仓库的环境变量,
  • 【Action库】 token 需填入 Cloudflare worker 的环境变量

也可以创建一个全局 token,替代这两个

在【Action库】添加6个环境变量

进入 Settings -> Secret and variables -> Actions -> New Repository secret 添加下面的

  • ALIYUN_NAME_SPACE -> 阿里云命名空间
  • ALIYUN_REGISTRY_USER -> 阿里云用户名
  • ALIYUN_REGISTRY_PASSWORD -> 阿里云密码
  • ALIYUN_REGISTRY -> 阿里云仓库地址
  • S_REPO -> Github用户名/【私库】名
  • S_TOKEN -> 【私库】token

添加文件,路径为 .github/workeflows/docker.yaml

docker.yaml
name: Sync Docker Image To Aliyun Repo By Api
on:
  repository_dispatch:
    types: sync_docker # 只有当 event_type 为 sync_docker 时触发
jobs:
  sync-task:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        images: ${{ github.event.client_payload.images }}
    steps:
      - uses: actions/checkout@v4
      - name: Sync ${{ matrix.images.source }} to ${{ matrix.images.target }}
        run: |
          docker pull --platform ${{ matrix.images.platform || 'linux/amd64' }} $source_docker_image
          docker tag $source_docker_image $target_docker_image
          docker login --username=${{ secrets.ALIYUN_REGISTRY_USER }} --password=${{ secrets.ALIYUN_REGISTRY_PASSWORD }} ${{ secrets.ALIYUN_REGISTRY }}
          docker push $target_docker_image
        env:
          source_docker_image: ${{ matrix.images.source }}
          target_docker_image: ${{ secrets.ALIYUN_REGISTRY }}/${{ secrets.ALIYUN_NAME_SPACE }}/${{ matrix.images.target }}

      - name: Acquire lock using GitHub API
        id: lock
        run: |
          # 检查锁文件是否存在
          if curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token ${{ secrets.S_TOKEN }}" \
            https://api.github.com/repos/${{ secrets.S_REPO }}/contents/.lock | grep -q 200; then
            echo "Lock file exists, waiting..."
            sleep 10
            exit 1
          else
            # 创建锁文件
            echo "Lock file does not exist, creating lock..."
            curl -X PUT -H "Authorization: token ${{ secrets.S_TOKEN }}" \
              -d '{"message":"Create lock file","content":"IyBsb2NrCg=="}' \
              https://api.github.com/repos/${{ secrets.S_REPO }}/contents/.lock
            echo "Lock acquired."
          fi
        continue-on-error: true

      - name: Checkout private repository
        uses: actions/checkout@v4
        with:
          repository: ${{ secrets.S_REPO }} # 使用 secrets.S_REPO 动态指定私库
          token: ${{ secrets.S_TOKEN }}    # 使用 S_TOKEN 进行身份验证
          path: private-repo              # 指定拉取的私库存放路径




      - name: Append docker pull command to image_list.txt
        run: |
          # 去重处理
          cat private-repo/image_list.txt | awk '!seen[$0]++' > private-repo/image_list_temp.txt
          mv private-repo/image_list_temp.txt private-repo/image_list.txt  # 修复路径

          # 检查是否已经存在相同的 docker pull 命令
          if ! grep -Fxq "docker pull $target_docker_image" private-repo/image_list.txt; then
            # 如果不存在,则追加新的 docker pull 命令
            echo "docker pull $target_docker_image" >> private-repo/image_list.txt
          else
            echo "Docker pull command already exists in image_list.txt, skipping append."
          fi
        env:
          target_docker_image: ${{ secrets.ALIYUN_REGISTRY }}/${{ secrets.ALIYUN_NAME_SPACE }}/${{ matrix.images.target }}

      - name: Check if there are changes to commit
        id: check_changes
        run: |
          cd private-repo
          if [ -n "$(git status --porcelain)" ]; then
            echo "::set-output name=has_changes::true"
          else
            echo "::set-output name=has_changes::false"
          fi

      - name: Commit local changes
        if: steps.check_changes.outputs.has_changes == 'true'
        run: |
          cd private-repo
          git config --global user.name "github-actions[bot]"
          git config --global user.email "github-actions[bot]@users.noreply.github.com"
          git add image_list.txt  # 确保添加正确的文件
          git commit -m "Update image_list.txt with new docker pull command"

      - name: Fetch and merge remote changes
        if: steps.check_changes.outputs.has_changes == 'true'
        run: |
          cd private-repo
          git fetch origin main
          git merge origin/main

      - name: Push changes
        if: steps.check_changes.outputs.has_changes == 'true'
        run: |
          cd private-repo
          git push https://x-access-token:${{ secrets.S_TOKEN }}@github.com/${{ secrets.S_REPO }}.git HEAD:main

      - name: Get lock file SHA
        id: get_lock_sha
        run: |
          LOCK_SHA=$(curl -s -H "Authorization: token ${{ secrets.S_TOKEN }}" \
            https://api.github.com/repos/${{ secrets.S_REPO }}/contents/.lock | jq -r '.sha')
          echo "::set-output name=sha::$LOCK_SHA"

      - name: Release lock using GitHub API
        if: always()
        run: |
          curl -X DELETE -H "Authorization: token ${{ secrets.S_TOKEN }}" \
            -d '{"message":"Release lock file", "sha": "${{ steps.get_lock_sha.outputs.sha }}"}' \
            https://api.github.com/repos/${{ secrets.S_REPO }}/contents/.lock
          echo "Lock released."

3、Cloudflare worker 前端

新建一个 worker,将下面的代码填入

worker.js
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  const url = new URL(request.url);
  const path = url.pathname;

  // 检查路径是否匹配自定义路径
  if (path !== '/520') {
    return new Response("Not Found", { status: 404 });
  }
  if (request.method === 'GET') {
    try {
      const vueScript = await fetch('https://unpkg.com/vue@3/dist/vue.global.prod.js').then(r => r.text());
      const tailwindCSS = await fetch('https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css').then(r => r.text());

      const appTemplate = `
        <div class="min-h-screen bg-gradient-to-r from-pink-100 to-blue-100 flex items-center justify-center">
          <div class="bg-white shadow-lg rounded-lg p-8 max-w-xl w-full">
            <h1 class="text-3xl font-bold text-center text-gray-800 mb-6">Docker 镜像同步</h1>
            <div v-for="(image, index) in images" :key="index" class="border border-gray-200 rounded-lg p-6 mb-6 bg-white shadow-sm">
              <h2 class="text-xl font-semibold text-gray-700 mb-4">镜像 {{ index + 1 }}</h2>
              <div class="mb-4">
                <label class="block text-gray-700 text-sm font-bold mb-2">来源镜像(例:vaultwarden/server:1.26.0):</label>
                <input type="text" v-model="image.source" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400">
              </div>
              <div class="mb-4">
                <label class="block text-gray-700 text-sm font-bold mb-2">CPU架构:</label>
                <select v-model="image.platform" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400">
                  <option value="linux/amd64">linux/amd64</option>
                  <option value="linux/arm64">linux/arm64</option>
                  <option value="linux/arm/v7">linux/arm/v7</option>
                </select>
              </div>
              <div class="mb-4">
                <label class="block text-gray-700 text-sm font-bold mb-2">目标镜像(例:bitwarden:AMD64_1.26.0):</label>
                <input type="text" v-model="image.target" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400">
              </div>
              <button @click="removeImage(index)" type="button" class="w-full bg-red-400 hover:bg-red-500 text-white font-bold py-2 px-4 rounded-lg transition duration-300">删除</button>
            </div>
            <div class="flex justify-between">
              <button @click="addImage" type="button" class="bg-blue-400 hover:bg-blue-500 text-white font-bold py-2 px-4 rounded-lg transition duration-300">添加镜像</button>
              <button @click="syncImages" type="button" class="bg-green-400 hover:bg-green-500 text-white font-bold py-2 px-4 rounded-lg transition duration-300">同步镜像</button>
            </div>
            <div v-if="message" class="mt-6 p-4 bg-gray-100 rounded-lg" :class="messageClass">
              <p class="text-left overflow-auto text-gray-800" v-html="message"></p>
            </div>
          </div>
        </div>
      `;

      const html = `<!DOCTYPE html>
      <html>
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Docker Image Sync</title>
        <style>${tailwindCSS}</style>
      </head>
      <body class="bg-gray-50">
        <div id="app"></div>
        <script>${vueScript}</script>
        <script>
          const { createApp, h } = Vue;

          const App = {
            data() {
              return {
                repoOwner: '${REPO_OWNER}', // 从环境变量读取
                repoName: '${REPO_NAME}', // 从环境变量读取
                images: [{ source: '', target: '', platform: 'linux/amd64' }],
                message: null,
                messageClass: null
              };
            },
            methods: {
              addImage() {
                this.images.push({ source: '', target: '', platform: 'linux/amd64' });
              },
              removeImage(index) {
                this.images.splice(index, 1);
              },
              async syncImages() {
                if (!this.githubToken) {
                  this.message = 'GitHub Token未配置';
                  this.messageClass = 'bg-red-100 text-red-600';
                  return;
                }
                if (this.images.some(item => !item.source || !item.target)) {
                  this.message = '请填写完整的镜像信息';
                  this.messageClass = 'bg-red-100 text-red-600';
                  return;
                }
                try {
                  const response = await fetch(
                    \`https://api.github.com/repos/\${this.repoOwner}/\${this.repoName}/dispatches\`,
                    {
                      method: 'POST',
                      headers: {
                        Accept: 'application/vnd.github.v3+json',
                        Authorization: \`token \${this.githubToken}\`,
                        'Content-Type': 'application/json'
                      },
                      body: JSON.stringify({
                        event_type: 'sync_docker',
                        client_payload: {
                          images: this.images,
                          message: 'github action sync'
                        }
                      })
                    }
                  );
                  if (!response.ok) {
                    const errorData = await response.json();
                    throw new Error(\`HTTP error \${response.status}: \${errorData.message || response.statusText}\`);
                  }

                  // 获取当前时间
                  const now = new Date();
                  const formattedTime = \`\${now.getFullYear()}-\${(now.getMonth() + 1).toString().padStart(2, '0')}-\${now.getDate().toString().padStart(2, '0')} \${now.getHours().toString().padStart(2, '0')}:\${now.getMinutes().toString().padStart(2, '0')}:\${now.getSeconds().toString().padStart(2, '0')}\`;

                  // 生成拉取命令
                  const pullCommands = this.images.map(image => \`docker pull ${ALIYUN_REGISTRY}/${ALIYUN_NAME_SPACE}/\${image.target}\`).join('<br><br>');

                  // 更新消息
                  this.message = \`同步请求已发送,时间:\${formattedTime}<br>稍等30S~60S后,请执行以下拉取命令:<br><br>\${pullCommands}<br>\`;
                  this.messageClass = 'bg-green-100 text-green-600';
                } catch (error) {
                  console.error("Error:", error);
                  this.message = \`同步请求失败: \${error.message}\`;
                  this.messageClass = 'bg-red-100 text-red-600';
                }
              }
            },
            computed: {
              githubToken() {
                return '${GITHUB_TOKEN}'; // 从环境变量读取
              },
              imageTargets() {
                return this.images.map(image => {
                  const sourceParts = image.source.split('/');
                  const imageName = sourceParts.length > 1 ? sourceParts[sourceParts.length - 1] : image.source;
                  let platformSuffix = image.platform.split('/')[1].toUpperCase();
                  if (platformSuffix == "ARM"){
                    platformSuffix = "ARM_V7"
                  }
                  return \`\${imageName}_\${platformSuffix}\`;
                });
              }
            },
            watch: {
              imageTargets(newTargets) {
                this.images.forEach((image, index) => {
                  image.target = newTargets[index];
                });
              }
            },
            template: \`${appTemplate}\`
          };

          const app = createApp(App);
          app.mount('#app');
        </script>
      </body>
      </html>`;

      return new Response(html, {
        headers: { 'Content-Type': 'text/html;charset=UTF-8' },
      });
    } catch (error) {
      return new Response(`Error: ${error.message}`, { status: 500 });
    }
  }
  return new Response("Method Not Allowed", { status: 405 });
}

添加环境变量

设置 -> 变量和机密

  • GITHUB_TOKEN -> github【Action库】token
  • REPO_OWNER -> github 用户名
  • REPO_NAME -> github【Action库】
  • ALIYUN_NAME_SPACE -> 阿里云仓库命名空间
  • ALIYUN_REGISTRY -> 阿里云仓库地址