[pull] main from TryGhost:main #464
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: PR Preview | |
| on: | |
| pull_request_target: | |
| types: [labeled, unlabeled, closed] | |
| jobs: | |
| deploy: | |
| name: Deploy Preview | |
| # Runs when the "preview" label is added — requires collaborator write access | |
| if: >- | |
| github.event.action == 'labeled' | |
| && github.event.label.name == 'preview' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| actions: read | |
| env: | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| steps: | |
| - name: Wait for Docker build job | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| BUILD_JOB_NAME: Build & Publish Artifacts | |
| run: | | |
| echo "Waiting for '${BUILD_JOB_NAME}' job to complete for $HEAD_SHA..." | |
| TIMEOUT=1800 # 30 minutes | |
| INTERVAL=30 | |
| START=$(date +%s) | |
| while true; do | |
| ELAPSED=$(( $(date +%s) - START )) | |
| if [ "$ELAPSED" -ge "$TIMEOUT" ]; then | |
| echo "::error::Timed out waiting for '${BUILD_JOB_NAME}' (${TIMEOUT}s)" | |
| exit 1 | |
| fi | |
| # Find the CI run for this SHA | |
| RUN=$(gh api "repos/${{ github.repository }}/actions/workflows/ci.yml/runs?head_sha=${HEAD_SHA}&per_page=1" \ | |
| --jq '.workflow_runs[0] | {id, status}' 2>/dev/null || echo "") | |
| if [ -z "$RUN" ] || [ "$RUN" = "null" ]; then | |
| echo " No CI run found yet, waiting ${INTERVAL}s... (${ELAPSED}s elapsed)" | |
| sleep "$INTERVAL" | |
| continue | |
| fi | |
| RUN_ID=$(echo "$RUN" | jq -r '.id') | |
| RUN_STATUS=$(echo "$RUN" | jq -r '.status') | |
| # Look up the build job specifically (paginate — CI has 30+ jobs) | |
| BUILD_JOB=$(gh api --paginate "repos/${{ github.repository }}/actions/runs/${RUN_ID}/jobs?per_page=100" \ | |
| --jq ".jobs[] | select(.name == \"${BUILD_JOB_NAME}\") | {status, conclusion}") | |
| if [ -z "$BUILD_JOB" ]; then | |
| if [ "$RUN_STATUS" = "completed" ]; then | |
| echo "::error::CI run ${RUN_ID} completed but '${BUILD_JOB_NAME}' job was not found" | |
| exit 1 | |
| fi | |
| echo " '${BUILD_JOB_NAME}' job not started yet (run ${RUN_STATUS}), waiting ${INTERVAL}s... (${ELAPSED}s elapsed)" | |
| sleep "$INTERVAL" | |
| continue | |
| fi | |
| JOB_STATUS=$(echo "$BUILD_JOB" | jq -r '.status') | |
| JOB_CONCLUSION=$(echo "$BUILD_JOB" | jq -r '.conclusion // empty') | |
| if [ "$JOB_STATUS" = "completed" ]; then | |
| if [ "$JOB_CONCLUSION" = "success" ]; then | |
| echo "Docker build ready (CI run $RUN_ID)" | |
| break | |
| fi | |
| echo "::error::'${BUILD_JOB_NAME}' did not succeed (conclusion: $JOB_CONCLUSION)" | |
| exit 1 | |
| fi | |
| echo " '${BUILD_JOB_NAME}' still ${JOB_STATUS}, waiting ${INTERVAL}s... (${ELAPSED}s elapsed)" | |
| sleep "$INTERVAL" | |
| done | |
| - name: Re-check PR eligibility | |
| id: recheck | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| PR=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" \ | |
| --jq '{state, labels: [.labels[].name]}') | |
| STATE=$(echo "$PR" | jq -r '.state') | |
| HAS_LABEL=$(echo "$PR" | jq '.labels | any(. == "preview")') | |
| if [ "$STATE" != "open" ]; then | |
| echo "::warning::PR is no longer open ($STATE), skipping dispatch" | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| elif [ "$HAS_LABEL" != "true" ]; then | |
| echo "::warning::preview label was removed, skipping dispatch" | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "PR still eligible for preview deploy" | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Resolve image digest from GHCR | |
| id: digest | |
| if: steps.recheck.outputs.skip != 'true' | |
| env: | |
| IMAGE_REPO: tryghost/ghost | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| run: | | |
| # Pin the deploy to an immutable digest so Artifact Registry's pull-through | |
| # cache cannot serve a stale manifest for the mutable pr-N tag. | |
| TOKEN=$(curl -sf "https://ghcr.io/token?service=ghcr.io&scope=repository:${IMAGE_REPO}:pull" | jq -r '.token') | |
| if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then | |
| echo "::error::Failed to acquire anonymous GHCR token for ${IMAGE_REPO}" | |
| exit 1 | |
| fi | |
| DIGEST=$(curl -sfI \ | |
| -H "Authorization: Bearer $TOKEN" \ | |
| -H "Accept: application/vnd.oci.image.index.v1+json" \ | |
| -H "Accept: application/vnd.docker.distribution.manifest.list.v2+json" \ | |
| -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ | |
| "https://ghcr.io/v2/${IMAGE_REPO}/manifests/pr-${PR_NUMBER}" \ | |
| | awk 'tolower($1) == "docker-content-digest:" {print $2}' | tr -d '\r\n') | |
| if [[ ! "$DIGEST" =~ ^sha256:[a-f0-9]{64}$ ]]; then | |
| echo "::error::Could not resolve digest for ghcr.io/${IMAGE_REPO}:pr-${PR_NUMBER} (got: '$DIGEST')" | |
| exit 1 | |
| fi | |
| echo "Resolved digest: $DIGEST" | |
| echo "digest=$DIGEST" >> "$GITHUB_OUTPUT" | |
| - name: Dispatch deploy to Ghost-Moya | |
| if: steps.recheck.outputs.skip != 'true' | |
| uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4 | |
| with: | |
| token: ${{ secrets.CANARY_DOCKER_BUILD }} | |
| repository: TryGhost/Ghost-Moya | |
| event-type: preview-deploy | |
| client-payload: >- | |
| { | |
| "pr_number": "${{ github.event.pull_request.number }}", | |
| "image_digest": "${{ steps.digest.outputs.digest }}", | |
| "action": "deploy", | |
| "seed": "true" | |
| } | |
| destroy: | |
| name: Destroy Preview | |
| # Runs when "preview" label is removed, or the PR is closed/merged while labeled | |
| if: >- | |
| (github.event.action == 'unlabeled' && github.event.label.name == 'preview') | |
| || (github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview')) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Dispatch destroy to Ghost-Moya | |
| uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4 | |
| with: | |
| token: ${{ secrets.CANARY_DOCKER_BUILD }} | |
| repository: TryGhost/Ghost-Moya | |
| event-type: preview-destroy | |
| client-payload: >- | |
| { | |
| "pr_number": "${{ github.event.pull_request.number }}", | |
| "action": "destroy" | |
| } |