call-images-mirror-missing.ymlΒΆ

name: "πŸͺž Mirror: Docker Hub β†’ GHCR (only missing)"

on:
  workflow_call:
    inputs:
      ghcr_namespace:
        description: "GHCR namespace/org (default: repository owner)"
        required: false
        type: string
      ghcr_prefix:
        description: "GHCR repo prefix (default: mirror)"
        required: false
        type: string
        default: "mirror"
      repo_root:
        description: "Repository root path (default: .)"
        required: false
        type: string
        default: "."
      source_repository:
        description: "Optional source repository for image config scan (e.g. fork owner/repo)"
        required: false
        type: string
        default: ""
      source_ref:
        description: "Optional source ref/SHA for image config scan"
        required: false
        type: string
        default: ""
      source_path:
        description: "Optional checkout path for source_repository/source_ref"
        required: false
        type: string
        default: "_pr_source"
      allow_unsafe_checkout:
        description: "Allow actions/checkout to fetch PR head/merge code under pull_request_target. Only set for maintainer-trusted fork PRs."
        required: false
        type: boolean
        default: false
      images_per_hour:
        description: "Optional throttle: max images mirrored per hour"
        required: false
        type: string
        default: ""
      concurrency_channel:
        description: "Optional channel to separate concurrency groups between callers"
        required: false
        type: string
        default: "default"
      concurrency_key:
        description: "Optional key (e.g. pr-123) to scope concurrency within a channel"
        required: false
        type: string
        default: ""
      image_wait_attempts:
        description: "Polling attempts for waiting on mirrored images"
        required: false
        type: string
        default: "1440"
      image_wait_sleep_seconds:
        description: "Polling interval in seconds for waiting on mirrored images"
        required: false
        type: string
        default: "10"
    secrets:
      DOCKERHUB_USERNAME:
        required: false
      DOCKERHUB_TOKEN:
        required: false

permissions:
  contents: read

concurrency:
  group: mirror-images-${{ github.repository }}-${{ inputs.concurrency_channel }}-${{ inputs.concurrency_key || github.ref_name }}
  cancel-in-progress: true

jobs:
  sync:
    name: πŸͺž Mirror
    runs-on: ubuntu-26.04
    permissions:
      contents: read
      packages: write

    env:
      DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
      DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
      IMAGES_PER_HOUR: ${{ inputs.images_per_hour || '' }}

    steps:
      - name: ⬇️ Checkout
        uses: actions/checkout@v7.0.0

      - name: πŸ” Resolve GHCR namespace and repository
        id: vars
        shell: bash
        env:
          GHCR_NAMESPACE_INPUT: ${{ inputs.ghcr_namespace || '' }}
        run: bash scripts/meta/resolve/ghcr/namespace_and_repository.sh

      - name: ⬇️ Checkout source repo for scan (optional)
        if: ${{ inputs.source_repository != '' && inputs.source_ref != '' }}
        uses: actions/checkout@v7.0.0
        with:
          repository: ${{ inputs.source_repository }}
          ref: ${{ inputs.source_ref }}
          path: ${{ inputs.source_path }}
          fetch-depth: 1
          persist-credentials: false
          allow-unsafe-pr-checkout: ${{ inputs.allow_unsafe_checkout }}

      - name: πŸ”§ Setup Python
        uses: actions/setup-python@v6
        with:
          python-version: "3.12"
          cache: pip
          cache-dependency-path: pyproject.toml

      - name: πŸ“¦ Install Python deps
        run: PYTHON=python bash scripts/install/python.sh

      - name: πŸ“¦ Install skopeo
        run: ./scripts/install/apt.sh skopeo

      - name: πŸ”‘ Login to Docker Hub (optional)
        if: ${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' && !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
        uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
        with:
          username: ${{ env.DOCKERHUB_USERNAME }}
          password: ${{ env.DOCKERHUB_TOKEN }}

      - name: πŸ”‘ Login to GHCR
        uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: ⏳ Wait for mirrored images (fork PR)
        if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
        env:
          GHCR_NAMESPACE: ${{ steps.vars.outputs.ghcr_namespace }}
          GHCR_REPOSITORY: ${{ steps.vars.outputs.ghcr_repository }}
          GHCR_PREFIX: ${{ inputs.ghcr_prefix || 'mirror' }}
          REPO_ROOT: ${{ inputs.repo_root || '.' }}
          IMAGE_WAIT_ATTEMPTS: ${{ inputs.image_wait_attempts }}
          IMAGE_WAIT_SLEEP_SECONDS: ${{ inputs.image_wait_sleep_seconds }}
        run: ./scripts/meta/wait/mirrors.sh

      - name: πŸͺž Mirror images to GHCR (only missing, best-effort)
        if: ${{ !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
        shell: bash
        env:
          GHCR_NAMESPACE: ${{ steps.vars.outputs.ghcr_namespace }}
          GHCR_REPOSITORY: ${{ steps.vars.outputs.ghcr_repository }}
          GHCR_PREFIX: ${{ inputs.ghcr_prefix || 'mirror' }}
          REPO_ROOT: ${{ inputs.repo_root || '.' }}
        run: bash scripts/meta/mirror/sync/missing.sh