feat(ci): harden the supply chain and build the server once #205
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: Preview deployment | |
| on: | |
| pull_request_target: # zizmor: ignore[dangerous-triggers] No pull-request code is checked out or executed. | |
| branches: [main] | |
| types: [labeled, synchronize, reopened, ready_for_review] | |
| permissions: {} | |
| jobs: | |
| deploy: | |
| name: "Preview / Deploy" | |
| if: >- | |
| vars.COOLIFY_URL != '' && | |
| vars.COOLIFY_APP_UUID != '' && | |
| vars.COOLIFY_PREVIEW_URL_TEMPLATE != '' && | |
| (github.event.label.name == 'preview' || | |
| contains(github.event.pull_request.labels.*.name, 'preview')) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| attestations: read | |
| contents: read | |
| deployments: write | |
| packages: read | |
| pull-requests: write | |
| # Longer than the budgets inside the scripts (image wait, deploy, reachability), so a run that | |
| # exceeds them reports a deployment status instead of being cancelled without one. | |
| timeout-minutes: 40 | |
| concurrency: | |
| group: hephaestus-preview-admission | |
| cancel-in-progress: false | |
| env: | |
| COOLIFY_URL: ${{ vars.COOLIFY_URL }} | |
| COOLIFY_APP_UUID: ${{ vars.COOLIFY_APP_UUID }} | |
| COOLIFY_PREVIEW_URL_TEMPLATE: ${{ vars.COOLIFY_PREVIEW_URL_TEMPLATE }} | |
| PREVIEW_MAX_ACTIVE: ${{ vars.PREVIEW_MAX_ACTIVE }} | |
| steps: | |
| - name: Load the trusted preview controller | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ github.sha }} | |
| persist-credentials: false | |
| sparse-checkout: | | |
| .github/actions/setup-node-pnpm | |
| package.json | |
| pnpm-lock.yaml | |
| pnpm-workspace.yaml | |
| scripts | |
| - name: Set up the repository's Node.js and pnpm versions | |
| uses: ./.github/actions/setup-node-pnpm | |
| with: | |
| install: "none" | |
| - name: Resolve the pull request head | |
| id: context | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const controller = await import(`${process.env.GITHUB_WORKSPACE}/scripts/preview-controller.ts`); | |
| await controller.resolve({ github, context, core }); | |
| - name: Explain why the preview did not deploy | |
| if: steps.context.outputs.announce == 'true' | |
| uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 | |
| with: | |
| header: app-preview | |
| number: ${{ steps.context.outputs.pr_number }} | |
| message: | | |
| ## 🚧 App Preview | |
| Not deployed: ${{ steps.context.outputs.reason }} | |
| <sub>Remove the `preview` label to opt out.</sub> | |
| - name: Wait for CI to publish this commit's signed images | |
| if: steps.context.outputs.eligible == 'true' | |
| env: | |
| GHCR_TOKEN: ${{ github.token }} | |
| GH_TOKEN: ${{ github.token }} | |
| HEAD_SHA: ${{ steps.context.outputs.head_sha }} | |
| run: node scripts/coolify-preview.ts images | |
| - name: Recheck opt-in and head before queueing | |
| id: recheck | |
| if: steps.context.outputs.eligible == 'true' | |
| env: | |
| PR_NUMBER: ${{ steps.context.outputs.pr_number }} | |
| HEAD_SHA: ${{ steps.context.outputs.head_sha }} | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const controller = await import(`${process.env.GITHUB_WORKSPACE}/scripts/preview-controller.ts`); | |
| await controller.recheck({ github, context, core }); | |
| - name: Reserve the native GitHub deployment | |
| id: github_deployment | |
| if: steps.recheck.outputs.proceed == 'true' | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| PR_NUMBER: ${{ steps.context.outputs.pr_number }} | |
| HEAD_SHA: ${{ steps.context.outputs.head_sha }} | |
| ENVIRONMENT: ${{ steps.context.outputs.environment }} | |
| PREVIEW_URL: ${{ steps.context.outputs.preview_url }} | |
| SOURCE_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| with: | |
| script: | | |
| const controller = await import(`${process.env.GITHUB_WORKSPACE}/scripts/preview-controller.ts`); | |
| await controller.create({ github, context, core }); | |
| - name: Announce the deployment | |
| if: steps.github_deployment.outputs.deployment_id != '' | |
| uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 | |
| with: | |
| header: app-preview | |
| number: ${{ steps.context.outputs.pr_number }} | |
| message: | | |
| ## 🚧 App Preview | |
| Deploying `${{ steps.context.outputs.head_sha }}`… | |
| <sub>Runs the images CI published for this commit. Updates on every commit. Remove the `preview` label to tear it down.</sub> | |
| - name: Queue the exact commit through Coolify's signed PR webhook | |
| id: queue | |
| if: steps.recheck.outputs.proceed == 'true' | |
| env: | |
| COOLIFY_TOKEN: ${{ secrets.COOLIFY_PREVIEW_READ_TOKEN }} | |
| COOLIFY_WEBHOOK_SECRET: ${{ secrets.COOLIFY_PREVIEW_WEBHOOK_SECRET }} | |
| PR_NUMBER: ${{ steps.context.outputs.pr_number }} | |
| HEAD_SHA: ${{ steps.context.outputs.head_sha }} | |
| PR_URL: ${{ steps.context.outputs.pr_url }} | |
| PR_TITLE: ${{ steps.context.outputs.pr_title }} | |
| AUTHOR_ASSOCIATION: ${{ steps.context.outputs.author_association }} | |
| HEAD_REF: ${{ steps.context.outputs.head_ref }} | |
| BASE_REF: ${{ steps.context.outputs.base_ref }} | |
| run: node scripts/coolify-preview.ts queue | |
| - name: Wait for Coolify and verify the preview | |
| id: wait | |
| if: steps.queue.outputs.deployment_uuid != '' | |
| env: | |
| COOLIFY_TOKEN: ${{ secrets.COOLIFY_PREVIEW_READ_TOKEN }} | |
| DEPLOYMENT_UUID: ${{ steps.queue.outputs.deployment_uuid }} | |
| EXPECTED_SHA: ${{ steps.context.outputs.head_sha }} | |
| PR_NUMBER: ${{ steps.context.outputs.pr_number }} | |
| PREVIEW_URL: ${{ steps.context.outputs.preview_url }} | |
| run: node scripts/coolify-preview.ts wait | |
| - name: Finalize the GitHub deployment | |
| id: finalize | |
| if: always() && steps.github_deployment.outputs.deployment_id != '' | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| DEPLOYMENT_ID: ${{ steps.github_deployment.outputs.deployment_id }} | |
| PR_NUMBER: ${{ steps.context.outputs.pr_number }} | |
| ENVIRONMENT: ${{ steps.context.outputs.environment }} | |
| PREVIEW_URL: ${{ steps.context.outputs.preview_url }} | |
| SOURCE_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| FINAL_STATE: ${{ steps.wait.outputs.state || 'error' }} | |
| DESCRIPTION: ${{ steps.wait.outputs.description || 'Preview workflow stopped unexpectedly.' }} | |
| LOG_URL: ${{ steps.wait.outputs.log_url || format('{0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id) }} | |
| with: | |
| script: | | |
| const controller = await import(`${process.env.GITHUB_WORKSPACE}/scripts/preview-controller.ts`); | |
| await controller.finalize({ github, context, core }); | |
| - name: Publish the preview link | |
| if: steps.finalize.outputs.final_state == 'success' | |
| uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 | |
| with: | |
| header: app-preview | |
| number: ${{ steps.context.outputs.pr_number }} | |
| message: | | |
| ## 🚀 App Preview | |
| 🔗 **[Open preview](${{ steps.context.outputs.preview_url }})** | |
| <sub>Preview for commit ${{ steps.context.outputs.head_sha }}. Updates on every commit. Remove the `preview` label to tear it down.</sub> | |
| # An opted-in pull request must not learn about a red preview only from the Actions tab. | |
| - name: Report a failed preview | |
| if: >- | |
| always() && | |
| steps.context.outputs.eligible == 'true' && | |
| steps.recheck.outputs.opted_out != 'true' && | |
| steps.wait.outputs.state != 'success' | |
| uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 | |
| with: | |
| header: app-preview | |
| number: ${{ steps.context.outputs.pr_number }} | |
| message: | | |
| ## ❌ App Preview | |
| ${{ steps.wait.outputs.description || 'CI did not publish images for this commit in time, or the preview could not be prepared.' }} | |
| 🔎 **[Deployment log](${{ steps.wait.outputs.log_url || format('{0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id) }})** | |
| <sub>The next commit retries automatically.</sub> | |
| - name: Fail when the preview failed | |
| if: steps.queue.outputs.deployment_uuid != '' && steps.wait.outputs.state != 'success' | |
| env: | |
| FINAL_STATE: ${{ steps.wait.outputs.state || 'error' }} | |
| run: | | |
| echo "::error::Preview deployment ended in state ${FINAL_STATE}." | |
| exit 1 |