PR Publish #5
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
| # Publish PR container images after pr-build.yaml completes | |
| # This workflow runs in a privileged context with access to secrets. | |
| # It downloads the container artifacts built by pr-build.yaml, pushes to registry, | |
| # creates a multi-platform manifest, and comments on the PR with image details. | |
| name: PR Publish | |
| on: | |
| workflow_run: | |
| workflows: ["PR Build"] | |
| types: [completed] | |
| env: | |
| REGISTRY: quay.io | |
| REGISTRY_IMAGE: rhdh-community/dynamic-plugins-factory | |
| jobs: | |
| publish: | |
| runs-on: ubuntu-latest | |
| if: github.event.workflow_run.conclusion == 'success' | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| actions: read | |
| env: | |
| HAS_QUAY_AUTH: ${{ secrets.QUAY_USERNAME != '' && secrets.QUAY_TOKEN != '' }} | |
| steps: | |
| - name: Download PR metadata artifact | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 | |
| with: | |
| name: pr-metadata | |
| path: /tmp/pr-metadata | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Read PR metadata | |
| id: pr-info | |
| run: | | |
| # Read and validate PR metadata | |
| # SECURITY: This artifact comes from an untrusted PR workflow | |
| # All values must be strictly validated before use | |
| if [[ ! -f "/tmp/pr-metadata/pr-info.json" ]]; then | |
| echo "Error: PR metadata file not found" | |
| exit 1 | |
| fi | |
| PR_NUMBER=$(jq -r '.pr_number' /tmp/pr-metadata/pr-info.json) | |
| COMMIT_SHA=$(jq -r '.commit_sha' /tmp/pr-metadata/pr-info.json) | |
| SHORT_SHA=$(jq -r '.short_sha' /tmp/pr-metadata/pr-info.json) | |
| PRIMARY_TAG=$(jq -r '.primary_tag' /tmp/pr-metadata/pr-info.json) | |
| # Convert JSON array to comma-separated string | |
| ALL_TAGS=$(jq -r '.all_tags | if type == "array" then join(",") else . end' /tmp/pr-metadata/pr-info.json) | |
| # Validate PR number is numeric (1-7 digits) | |
| if ! [[ "$PR_NUMBER" =~ ^[0-9]{1,7}$ ]]; then | |
| echo "Error: Invalid PR number: $PR_NUMBER" | |
| exit 1 | |
| fi | |
| # Validate commit SHA is 40 hex characters | |
| if ! [[ "$COMMIT_SHA" =~ ^[a-f0-9]{40}$ ]]; then | |
| echo "Error: Invalid commit SHA: $COMMIT_SHA" | |
| exit 1 | |
| fi | |
| # Validate short SHA is 7 hex characters | |
| if ! [[ "$SHORT_SHA" =~ ^[a-f0-9]{7}$ ]]; then | |
| echo "Error: Invalid short SHA: $SHORT_SHA" | |
| exit 1 | |
| fi | |
| # Validate primary tag format (pr-NUMBER or pr-NUMBER-SHA) | |
| if ! [[ "$PRIMARY_TAG" =~ ^pr-[0-9]{1,7}(-[a-f0-9]{7})?$ ]]; then | |
| echo "Error: Invalid primary tag: $PRIMARY_TAG" | |
| exit 1 | |
| fi | |
| # Validate all_tags contains only safe characters (alphanumeric, dash, comma) | |
| if ! [[ "$ALL_TAGS" =~ ^[a-zA-Z0-9,.-]+$ ]]; then | |
| echo "Error: Invalid all_tags format: $ALL_TAGS" | |
| exit 1 | |
| fi | |
| echo "pr_number=${PR_NUMBER}" >> $GITHUB_OUTPUT | |
| echo "commit_sha=${COMMIT_SHA}" >> $GITHUB_OUTPUT | |
| echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT | |
| echo "primary_tag=${PRIMARY_TAG}" >> $GITHUB_OUTPUT | |
| echo "all_tags=${ALL_TAGS}" >> $GITHUB_OUTPUT | |
| echo "PR Number: $PR_NUMBER" | |
| echo "Commit SHA: $COMMIT_SHA" | |
| echo "Primary Tag: $PRIMARY_TAG" | |
| - name: Download amd64 container artifact | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 | |
| with: | |
| name: container-image-pr-${{ steps.pr-info.outputs.pr_number }}-amd64 | |
| path: /tmp/container-amd64 | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Download arm64 container artifact | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 | |
| with: | |
| name: container-image-pr-${{ steps.pr-info.outputs.pr_number }}-arm64 | |
| path: /tmp/container-arm64 | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 | |
| - name: Login to Quay | |
| if: env.HAS_QUAY_AUTH == 'true' | |
| uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ secrets.QUAY_USERNAME }} | |
| password: ${{ secrets.QUAY_TOKEN }} | |
| - name: Load, push images and create manifest | |
| if: env.HAS_QUAY_AUTH == 'true' | |
| id: push | |
| continue-on-error: true | |
| env: | |
| PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }} | |
| SHORT_SHA: ${{ steps.pr-info.outputs.short_sha }} | |
| ALL_TAGS: ${{ steps.pr-info.outputs.all_tags }} | |
| run: | | |
| echo "::group::Load container images from artifacts" | |
| # Images are built with arch-specific tags (e.g., pr-8-amd64, pr-8-arm64) | |
| # so they won't overwrite each other when loaded | |
| docker load -i /tmp/container-amd64/container-image.tar | |
| docker load -i /tmp/container-arm64/container-image.tar | |
| echo "Loaded images:" | |
| docker images | |
| # Get the primary tag with arch suffix for each platform | |
| IFS=',' read -ra TAG_ARRAY <<< "$ALL_TAGS" | |
| PRIMARY_TAG="${TAG_ARRAY[0]}" | |
| AMD64_IMAGE="${REGISTRY}/${REGISTRY_IMAGE}:${PRIMARY_TAG}-amd64" | |
| ARM64_IMAGE="${REGISTRY}/${REGISTRY_IMAGE}:${PRIMARY_TAG}-arm64" | |
| echo "amd64 image: $AMD64_IMAGE" | |
| echo "arm64 image: $ARM64_IMAGE" | |
| # Verify we have two different images | |
| AMD64_DIGEST=$(docker inspect --format='{{.Id}}' "$AMD64_IMAGE") | |
| ARM64_DIGEST=$(docker inspect --format='{{.Id}}' "$ARM64_IMAGE") | |
| echo "amd64 digest: $AMD64_DIGEST" | |
| echo "arm64 digest: $ARM64_DIGEST" | |
| if [[ "$AMD64_DIGEST" == "$ARM64_DIGEST" ]]; then | |
| echo "ERROR: amd64 and arm64 images have the same digest!" | |
| echo "This indicates a build or artifact issue." | |
| exit 1 | |
| fi | |
| echo "Verified: images have different digests" | |
| echo "::endgroup::" | |
| echo "::group::Push architecture-specific images" | |
| # Images already have arch suffix from build, just need to push | |
| # Also create additional tags for each arch | |
| for tag in "${TAG_ARRAY[@]}"; do | |
| AMD64_TAG="${REGISTRY}/${REGISTRY_IMAGE}:${tag}-amd64" | |
| ARM64_TAG="${REGISTRY}/${REGISTRY_IMAGE}:${tag}-arm64" | |
| echo "Pushing: $AMD64_TAG" | |
| docker tag "$AMD64_IMAGE" "$AMD64_TAG" | |
| docker push "$AMD64_TAG" | |
| echo "Pushing: $ARM64_TAG" | |
| docker tag "$ARM64_IMAGE" "$ARM64_TAG" | |
| docker push "$ARM64_TAG" | |
| done | |
| echo "::endgroup::" | |
| echo "::group::Create and push multi-platform manifests" | |
| for tag in "${TAG_ARRAY[@]}"; do | |
| MANIFEST_TAG="${REGISTRY}/${REGISTRY_IMAGE}:${tag}" | |
| AMD64_TAG="${REGISTRY}/${REGISTRY_IMAGE}:${tag}-amd64" | |
| ARM64_TAG="${REGISTRY}/${REGISTRY_IMAGE}:${tag}-arm64" | |
| echo "Creating manifest: $MANIFEST_TAG" | |
| docker manifest create "$MANIFEST_TAG" \ | |
| --amend "$AMD64_TAG" \ | |
| --amend "$ARM64_TAG" | |
| echo "Pushing manifest: $MANIFEST_TAG" | |
| docker manifest push "$MANIFEST_TAG" | |
| done | |
| echo "::endgroup::" | |
| echo "pushed_tags=${ALL_TAGS}" >> $GITHUB_OUTPUT | |
| - name: Clean up architecture-specific tags | |
| if: env.HAS_QUAY_AUTH == 'true' && steps.push.outcome == 'success' && env.QUAY_OAUTH_TOKEN != '' | |
| env: | |
| ALL_TAGS: ${{ steps.pr-info.outputs.all_tags }} | |
| QUAY_OAUTH_TOKEN: ${{ secrets.QUAY_OAUTH_TOKEN }} | |
| run: | | |
| # Quay robot tokens cannot delete tags via API for security reasons | |
| # Using an OAuth application token instead | |
| echo "::group::Clean up architecture-specific tags" | |
| IFS=',' read -ra TAG_ARRAY <<< "$ALL_TAGS" | |
| for tag in "${TAG_ARRAY[@]}"; do | |
| for arch in amd64 arm64; do | |
| ARCH_TAG="${tag}-${arch}" | |
| echo "Deleting tag: ${REGISTRY}/${REGISTRY_IMAGE}:${ARCH_TAG}" | |
| HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ | |
| -X DELETE \ | |
| -H "Authorization: Bearer ${QUAY_OAUTH_TOKEN}" \ | |
| "https://${REGISTRY}/api/v1/repository/${REGISTRY_IMAGE}/tag/${ARCH_TAG}") | |
| if [[ "$HTTP_STATUS" == "204" || "$HTTP_STATUS" == "200" ]]; then | |
| echo "Successfully deleted ${ARCH_TAG}" | |
| else | |
| echo "Warning: Failed to delete ${ARCH_TAG} (HTTP ${HTTP_STATUS}), continuing..." | |
| fi | |
| done | |
| done | |
| echo "::endgroup::" | |
| - name: Comment on PR | |
| if: env.HAS_QUAY_AUTH == 'true' | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 | |
| env: | |
| PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }} | |
| SHORT_SHA: ${{ steps.pr-info.outputs.short_sha }} | |
| PRIMARY_TAG: ${{ steps.pr-info.outputs.primary_tag }} | |
| BUILD_RUN_ID: ${{ github.event.workflow_run.id }} | |
| PUBLISH_RUN_ID: ${{ github.run_id }} | |
| PUSH_OUTCOME: ${{ steps.push.outcome }} | |
| with: | |
| script: | | |
| const prNumber = parseInt(process.env.PR_NUMBER); | |
| const shortSha = process.env.SHORT_SHA; | |
| const primaryTag = process.env.PRIMARY_TAG; | |
| const buildRunId = process.env.BUILD_RUN_ID; | |
| const publishRunId = process.env.PUBLISH_RUN_ID; | |
| const registry = process.env.REGISTRY; | |
| const image = process.env.REGISTRY_IMAGE; | |
| const pushOutcome = process.env.PUSH_OUTCOME; | |
| const prTag = `pr-${prNumber}`; | |
| const prShaTag = `pr-${prNumber}-${shortSha}`; | |
| let body; | |
| if (pushOutcome === 'success') { | |
| const expirationDate = new Date(); | |
| expirationDate.setDate(expirationDate.getDate() + 14); | |
| const expirationStr = expirationDate.toLocaleDateString('en-US', { | |
| year: 'numeric', | |
| month: 'long', | |
| day: 'numeric' | |
| }); | |
| body = `## Container Image Published | |
| Multi-platform container images are now available. | |
| | Tag | Image | Platforms | | |
| |-----|-------|-----------| | |
| | \`${prTag}\` | \`${registry}/${image}:${prTag}\` | linux/amd64, linux/arm64 | | |
| | \`${prShaTag}\` | \`${registry}/${image}:${prShaTag}\` | linux/amd64, linux/arm64 | | |
| **Expires:** ${expirationStr} | |
| ### Pull Commands | |
| \`\`\`bash | |
| # Multi-platform (auto-selects correct architecture) | |
| podman pull ${registry}/${image}:${prTag} | |
| # Or with specific commit SHA | |
| podman pull ${registry}/${image}:${prShaTag} | |
| \`\`\` | |
| ### Traceability | |
| - **Build:** [PR Build #${buildRunId}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${buildRunId}) | |
| - **Publish:** [PR Publish #${publishRunId}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${publishRunId}) | |
| - **Commit:** \`${shortSha}\` | |
| `; | |
| } else { | |
| body = `## Container Image Publish Failed | |
| The container image publish step failed. Please check the workflow logs for details. | |
| ### Traceability | |
| - **Build:** [PR Build #${buildRunId}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${buildRunId}) | |
| - **Publish:** [PR Publish #${publishRunId}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${publishRunId}) (failed) | |
| - **Commit:** \`${shortSha}\` | |
| `; | |
| } | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: body | |
| }); | |
| - name: Publish Summary | |
| if: env.HAS_QUAY_AUTH == 'true' && steps.push.outcome == 'success' | |
| env: | |
| PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }} | |
| SHORT_SHA: ${{ steps.pr-info.outputs.short_sha }} | |
| COMMIT_SHA: ${{ steps.pr-info.outputs.commit_sha }} | |
| WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }} | |
| SERVER_URL: ${{ github.server_url }} | |
| REPOSITORY: ${{ github.repository }} | |
| run: | | |
| PR_TAG="pr-${PR_NUMBER}" | |
| PR_SHA_TAG="pr-${PR_NUMBER}-${SHORT_SHA}" | |
| EXPIRATION_DATE=$(date -d "+14 days" "+%B %d, %Y") | |
| echo "## PR Image Published" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Images" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Image | Tag | Platforms |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-------|-----|-----------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| \`${REGISTRY}/${REGISTRY_IMAGE}:${PR_TAG}\` | ${PR_TAG} | linux/amd64, linux/arm64 |" >> $GITHUB_STEP_SUMMARY | |
| echo "| \`${REGISTRY}/${REGISTRY_IMAGE}:${PR_SHA_TAG}\` | ${PR_SHA_TAG} | linux/amd64, linux/arm64 |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Expires:** ${EXPIRATION_DATE}" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Traceability" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Pull Request:** [#${PR_NUMBER}](${SERVER_URL}/${REPOSITORY}/pull/${PR_NUMBER})" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Triggered by:** [PR Build #${WORKFLOW_RUN_ID}](${SERVER_URL}/${REPOSITORY}/actions/runs/${WORKFLOW_RUN_ID})" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Commit:** \`${SHORT_SHA}\`" >> $GITHUB_STEP_SUMMARY | |
| - name: No Auth Summary | |
| if: env.HAS_QUAY_AUTH != 'true' | |
| env: | |
| PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }} | |
| SHORT_SHA: ${{ steps.pr-info.outputs.short_sha }} | |
| run: | | |
| echo "## PR Image Publish Skipped" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "Registry credentials not configured. Image was not pushed." >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **PR Number**: #${PR_NUMBER}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Commit SHA**: \`${SHORT_SHA}\`" >> $GITHUB_STEP_SUMMARY | |
| - name: Publish Failed Summary | |
| if: env.HAS_QUAY_AUTH == 'true' && steps.push.outcome != 'success' | |
| env: | |
| PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }} | |
| SHORT_SHA: ${{ steps.pr-info.outputs.short_sha }} | |
| BUILD_RUN_ID: ${{ github.event.workflow_run.id }} | |
| PUBLISH_RUN_ID: ${{ github.run_id }} | |
| SERVER_URL: ${{ github.server_url }} | |
| REPOSITORY: ${{ github.repository }} | |
| run: | | |
| echo "## PR Image Publish Failed" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "The container image publish step failed. Please check the workflow logs for details." >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Traceability" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Pull Request:** [#${PR_NUMBER}](${SERVER_URL}/${REPOSITORY}/pull/${PR_NUMBER})" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Build:** [PR Build #${BUILD_RUN_ID}](${SERVER_URL}/${REPOSITORY}/actions/runs/${BUILD_RUN_ID})" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Publish:** [PR Publish #${PUBLISH_RUN_ID}](${SERVER_URL}/${REPOSITORY}/actions/runs/${PUBLISH_RUN_ID}) (failed)" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Commit:** \`${SHORT_SHA}\`" >> $GITHUB_STEP_SUMMARY | |
| - name: Fail workflow if publish failed | |
| if: env.HAS_QUAY_AUTH == 'true' && steps.push.outcome != 'success' | |
| run: | | |
| echo "Publish step failed. Failing workflow." | |
| exit 1 | |
| # Handle failed upstream workflow | |
| notify-failure: | |
| runs-on: ubuntu-latest | |
| if: github.event.workflow_run.conclusion == 'failure' | |
| permissions: | |
| pull-requests: write | |
| steps: | |
| - name: Comment on PR | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 | |
| env: | |
| BUILD_RUN_ID: ${{ github.event.workflow_run.id }} | |
| with: | |
| script: | | |
| const buildRunId = process.env.BUILD_RUN_ID; | |
| const pullRequests = context.payload.workflow_run.pull_requests; | |
| if (!pullRequests || pullRequests.length === 0) { | |
| console.log('No pull request associated with this workflow run'); | |
| return; | |
| } | |
| const prNumber = pullRequests[0].number; | |
| const body = `## Container Image Build Failed | |
| The PR Build workflow failed. No container image was published. | |
| ### Traceability | |
| - **Build:** [PR Build #${buildRunId}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${buildRunId}) (failed) | |
| `; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: body | |
| }); | |
| - name: Failure Summary | |
| env: | |
| WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }} | |
| SERVER_URL: ${{ github.server_url }} | |
| REPOSITORY: ${{ github.repository }} | |
| run: | | |
| echo "## PR Build Failed" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "The upstream PR Build workflow failed. No image was published." >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Failed workflow:** [PR Build #${WORKFLOW_RUN_ID}](${SERVER_URL}/${REPOSITORY}/actions/runs/${WORKFLOW_RUN_ID})" >> $GITHUB_STEP_SUMMARY |