Mirror Images - update-snapshot-release-5.1 #3019
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Mirror External Images | |
| run-name: "Mirror Images - ${{ github.event.workflow_run.head_branch || inputs.branch || github.ref_name }}" | |
| # SECURITY NOTES: | |
| # 1. This workflow handles sensitive secrets (registry credentials, private keys) | |
| # 2. Command injection mitigations: | |
| # - All untrusted inputs (branch names, etc.) are passed via environment variables | |
| # - Never use ${{ }} syntax directly in bash contexts with untrusted data | |
| # 3. workflow_run validation: | |
| # - Only accepts triggers from this repository (not forks) | |
| # - Validates repository source before processing | |
| # 4. Minimal GITHUB_TOKEN permissions (permissions: {}) | |
| # 5. If secrets are compromised, rotate immediately: | |
| # - TOOLS_REPO_READER_PRIVATE_KEY | |
| # - REGISTRY_REDHAT_IO_RGY_PASSWORD | |
| # - REGISTRY_STAGE_REDHAT_IO_RGY_PASSWORD | |
| # - QUAY_IO_ACMD_RGY_PASSWORD | |
| # - SLACK_WEBHOOK_URL | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| branch: | |
| description: 'Branch to checkout (defaults to current branch)' | |
| required: false | |
| type: string | |
| dry-run: | |
| description: 'Run in dry-run mode (no actual mirroring)' | |
| required: false | |
| type: boolean | |
| default: false | |
| workflow_run: | |
| workflows: ["Gen Bundle Contents When Triggered"] | |
| types: | |
| - completed | |
| defaults: | |
| run: | |
| shell: bash | |
| # Restrict GITHUB_TOKEN permissions to minimum required | |
| # This prevents token misuse if the workflow is compromised | |
| permissions: {} | |
| jobs: | |
| mirror-images: | |
| runs-on: ubuntu-24.04 | |
| # Only run if the triggering workflow succeeded or if manually triggered | |
| if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} | |
| steps: | |
| - name: Validate workflow_run repository | |
| if: github.event_name == 'workflow_run' | |
| env: | |
| WORKFLOW_REPO: ${{ github.event.workflow_run.head_repository.full_name }} | |
| EXPECTED_REPO: ${{ github.repository }} | |
| run: | | |
| echo "Validating workflow_run trigger source..." | |
| echo "Expected repository: ${EXPECTED_REPO}" | |
| echo "Triggering repository: ${WORKFLOW_REPO}" | |
| if [ "${WORKFLOW_REPO}" != "${EXPECTED_REPO}" ]; then | |
| echo "ERROR: workflow_run triggered from untrusted repository!" | |
| echo "This workflow only accepts triggers from ${EXPECTED_REPO}" | |
| exit 1 | |
| fi | |
| echo "Repository validation passed." | |
| - name: Log workflow trigger context | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| WORKFLOW_NAME: ${{ github.event.workflow_run.name }} | |
| WORKFLOW_SHA: ${{ github.event.workflow_run.head_sha }} | |
| WORKFLOW_BRANCH: ${{ github.event.workflow_run.head_branch }} | |
| WORKFLOW_REPO: ${{ github.event.workflow_run.head_repository.full_name }} | |
| INPUT_BRANCH: ${{ inputs.branch }} | |
| run: | | |
| echo "Workflow triggered by: ${EVENT_NAME}" | |
| if [ "${EVENT_NAME}" = "workflow_run" ]; then | |
| echo "Triggering workflow: ${WORKFLOW_NAME}" | |
| echo "Triggering commit SHA: ${WORKFLOW_SHA}" | |
| echo "Triggering branch: ${WORKFLOW_BRANCH}" | |
| echo "Triggering repo: ${WORKFLOW_REPO}" | |
| elif [ "${EVENT_NAME}" = "workflow_dispatch" ]; then | |
| echo "Manual trigger - branch input: ${INPUT_BRANCH}" | |
| fi | |
| - name: Determine branch context | |
| id: branch | |
| env: | |
| # Use the branch that triggered the workflow: | |
| # - workflow_run: use head_branch (the PR branch, e.g., release-2.14) | |
| # - workflow_dispatch: use ref_name or input branch | |
| WORKFLOW_RUN_BRANCH: ${{ github.event.workflow_run.head_branch }} | |
| INPUT_BRANCH: ${{ inputs.branch }} | |
| REF_NAME: ${{ github.ref_name }} | |
| run: | | |
| BRANCH_NAME="${WORKFLOW_RUN_BRANCH:-${INPUT_BRANCH:-${REF_NAME}}}" | |
| echo "name=${BRANCH_NAME}" >> $GITHUB_OUTPUT | |
| # Extract release version from branch name | |
| # Matches: release-2.14, update-snapshot-release-5.0, etc. | |
| RELEASE_VERSION="" | |
| if [[ "${BRANCH_NAME}" =~ release-([0-9]+\.[0-9]+) ]]; then | |
| RELEASE_VERSION="${BASH_REMATCH[1]}" | |
| fi | |
| echo "release_version=${RELEASE_VERSION}" >> $GITHUB_OUTPUT | |
| echo "Branch name: ${BRANCH_NAME}" | |
| echo "Release version: ${RELEASE_VERSION}" | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| # When triggered by workflow_run, checkout the commit SHA that triggered it (persists even if branch is deleted) | |
| # When triggered manually, checkout the specified branch or current branch | |
| ref: ${{ inputs.branch || github.event.workflow_run.head_sha || github.ref }} | |
| path: workflow | |
| sparse-checkout: | | |
| config | |
| show-progress: false | |
| - name: Generate GitHub token to read release tools repo | |
| id: gen-tools-repo-token | |
| uses: actions/create-github-app-token@v1 | |
| with: | |
| app-id: ${{ vars.TOOLS_REPO_READER_APP_ID }} | |
| private-key: ${{ secrets.TOOLS_REPO_READER_PRIVATE_KEY }} | |
| owner: ${{ vars.TOOLS_REPO_OWNER }} | |
| repositories: "release" | |
| - name: Checkout release tools repo | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: ${{ vars.TOOLS_REPO_OWNER }}/release | |
| ref: master | |
| token: ${{ steps.gen-tools-repo-token.outputs.token }} | |
| path: release-tools | |
| sparse-checkout: | | |
| tools/konflux/bundle/utils | |
| show-progress: false | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| - name: Install skopeo | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y skopeo | |
| - name: Authenticate and run mirror script | |
| id: mirror | |
| continue-on-error: true | |
| run: | | |
| BRANCH_NAME="${{ steps.branch.outputs.name }}" | |
| RELEASE_VERSION="${{ steps.branch.outputs.release_version }}" | |
| # Add to GitHub Actions summary | |
| echo "### Image Mirroring for ACM ${RELEASE_VERSION:-${BRANCH_NAME}}" >> $GITHUB_STEP_SUMMARY | |
| echo "**Branch:** ${BRANCH_NAME}" >> $GITHUB_STEP_SUMMARY | |
| if [ -n "$RELEASE_VERSION" ]; then | |
| echo "**Release Version:** ${RELEASE_VERSION}" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Authenticate to needed image registries... | |
| # Access to registry.redhat.io needed to resolve external image references (shipped images): | |
| echo "Doing skopeo login to registry.redhat.io" | |
| echo "${{ secrets.REGISTRY_REDHAT_IO_RGY_PASSWORD }}" \ | |
| | skopeo login -u="${{ vars.REGISTRY_REDHAT_IO_RGY_USERNAME }}" --password-stdin registry.redhat.io | |
| # Access to registry.stage.redhat.io needed to resolve staging image references: | |
| echo "Doing skopeo login to registry.stage.redhat.io" | |
| echo "${{ secrets.REGISTRY_STAGE_REDHAT_IO_RGY_PASSWORD }}" \ | |
| | skopeo login -u="${{ vars.REGISTRY_STAGE_REDHAT_IO_RGY_USERNAME }}" --password-stdin registry.stage.redhat.io | |
| # Access to quay.io/acm-d needed to resolve external image references (in-dev images): | |
| echo "Doing skopeo login to quay.io (for acm-d)" | |
| echo "${{ secrets.QUAY_IO_ACMD_RGY_PASSWORD }}" \ | |
| | skopeo login -u="${{ vars.QUAY_IO_ACMD_RGY_USERNAME }}" --password-stdin quay.io | |
| # Run the mirror script | |
| ARGS="--operator acm" | |
| if [ "${{ inputs.dry-run }}" == "true" ]; then | |
| ARGS="$ARGS --dry-run" | |
| fi | |
| # Run the script from stolostron/release repo, using config from workflow (bundle) repo | |
| cd workflow | |
| python ../release-tools/tools/konflux/bundle/utils/mirror-external-images.py $ARGS | |
| - name: Send Slack notification | |
| if: always() | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| SLACK_USER_ID: ${{ vars.SLACK_USER_ID }} | |
| run: | | |
| BRANCH_NAME="${{ steps.branch.outputs.name }}" | |
| RELEASE_VERSION="${{ steps.branch.outputs.release_version }}" | |
| # Read results from the JSON file | |
| if [ ! -f workflow/mirror-results.json ]; then | |
| echo "No results file found, creating minimal report" | |
| STATUS="❌ *FAILED*" | |
| SUMMARY="Mirror script did not complete successfully" | |
| DETAILS="" | |
| HAS_FAILURE=true | |
| else | |
| # Parse the JSON results | |
| MIRRORED=$(jq -r '.mirrored' workflow/mirror-results.json) | |
| SKIPPED=$(jq -r '.skipped' workflow/mirror-results.json) | |
| FAILED=$(jq -r '.failed' workflow/mirror-results.json) | |
| DRY_RUN=$(jq -r '.dry_run' workflow/mirror-results.json) | |
| # Determine status | |
| if [ "$FAILED" -gt 0 ]; then | |
| STATUS="⚠️ *COMPLETED WITH FAILURES*" | |
| HAS_FAILURE=true | |
| elif [ "$DRY_RUN" = "true" ]; then | |
| STATUS="🔍 *DRY RUN COMPLETED*" | |
| HAS_FAILURE=false | |
| else | |
| STATUS="✅ *SUCCESS*" | |
| HAS_FAILURE=false | |
| fi | |
| # Get failure details if any | |
| FAILURES_JSON="" | |
| if [ "$FAILED" -gt 0 ]; then | |
| FAILURES_JSON=$(jq -c '[.failures[] | "• `\(.image_key)` [\(.operator)]\n Source: `\(.source)`\n Target: `\(.target)`\n Error: \(.error)"]' workflow/mirror-results.json) | |
| fi | |
| fi | |
| # Add user mention if there's a failure and SLACK_USER_ID is set | |
| USER_MENTION="" | |
| if [ "$HAS_FAILURE" = "true" ] && [ -n "$SLACK_USER_ID" ]; then | |
| USER_MENTION=" <@$SLACK_USER_ID>" | |
| fi | |
| # Build header with release version if available | |
| HEADER_TEXT="Image Mirroring Report" | |
| if [ -n "$RELEASE_VERSION" ]; then | |
| HEADER_TEXT="Image Mirroring Report - ACM ${RELEASE_VERSION}" | |
| fi | |
| # Build the Slack message using jq for safe JSON construction | |
| WORKFLOW_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| MESSAGE=$(jq -n \ | |
| --arg header "$HEADER_TEXT" \ | |
| --arg status "$STATUS$USER_MENTION" \ | |
| --argjson mirrored "$MIRRORED" \ | |
| --argjson skipped "$SKIPPED" \ | |
| --argjson failed "$FAILED" \ | |
| --argjson failures "${FAILURES_JSON:-[]}" \ | |
| --arg event "$EVENT_NAME" \ | |
| --arg branch "$BRANCH_NAME" \ | |
| --arg url "$WORKFLOW_URL" \ | |
| '{ | |
| "blocks": [ | |
| { | |
| "type": "header", | |
| "text": { | |
| "type": "plain_text", | |
| "text": $header | |
| } | |
| }, | |
| { | |
| "type": "section", | |
| "text": { | |
| "type": "mrkdwn", | |
| "text": ($status + "\n\n*Summary:*\n• Mirrored: " + ($mirrored|tostring) + "\n• Skipped: " + ($skipped|tostring) + "\n• Failed: " + ($failed|tostring) + (if ($failures | length) > 0 then "\n\n*Failed Images:*\n" + ($failures | join("\n")) else "" end)) | |
| } | |
| }, | |
| { | |
| "type": "section", | |
| "fields": [ | |
| { | |
| "type": "mrkdwn", | |
| "text": ("*Triggered by:*\n" + $event) | |
| }, | |
| { | |
| "type": "mrkdwn", | |
| "text": ("*Branch:*\n" + $branch) | |
| } | |
| ] | |
| }, | |
| { | |
| "type": "actions", | |
| "elements": [ | |
| { | |
| "type": "button", | |
| "text": { | |
| "type": "plain_text", | |
| "text": "View Workflow Run" | |
| }, | |
| "url": $url | |
| } | |
| ] | |
| } | |
| ] | |
| }') | |
| # Send to Slack | |
| curl -X POST -H 'Content-type: application/json' \ | |
| --data "$MESSAGE" \ | |
| "${{ secrets.SLACK_WEBHOOK_URL }}" | |
| - name: Fail workflow if mirroring failed | |
| if: steps.mirror.outcome == 'failure' | |
| run: exit 1 |