Deploy Preview #7367
This file contains 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
# Because C.I. jobs could expose secrets to malicious pull requsets, | |
# GitHub prevents (by default) exposing action secrets to pull requests | |
# from forks. | |
# | |
# This is great, however, the jobs that use the secrets are still useful on | |
# pull requests. | |
# | |
# To run a _trusted_ workflow, we can trigger it from an event from an _untrusted_ | |
# workflow. This keeps the secrets out of reach from the fork, but still allows | |
# us to keep the utility of pull request preview deploys, browserstack running, etc. | |
# Normally, this _trusted_ behavior is offloaded by Cloudflare, Netlify, Vercel, etc | |
# -- their own workers are trusted and can push comments / updates to pull requests. | |
# | |
# We don't want to use their slower (and sometimes paid) hardware. | |
# When we use our own workflows, we can re-use the cache built from the PR | |
# (or elsewhere). | |
# | |
# To be *most* secure, you'd need to build all the artifacts in the PR, | |
# then upload them to then be downloaded in the trusted workflows. | |
# Trusted workflows should not run any scripts from a PR, as malicious | |
# submitters may tweak the build scripts. | |
# Since all build artifacts are for the web browser, and not executed in | |
# node-space, we can be reasonably confident that downloading and testing/deploying | |
# those artifacts does not compromise our secrets. | |
# | |
# | |
# More information here: | |
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ | |
# | |
# Things that would make this easier: | |
# Readablity: https://github.com/actions/download-artifact/issues/172 | |
# Security: | |
# - if there was a way to avoid pnpm install *entirely* | |
name: Deploy Preview | |
# read-write repo token | |
# access to secrets | |
on: | |
workflow_dispatch: | |
inputs: | |
prNumber: | |
required: true | |
description: Which PR to create a preview for | |
workflow_run: | |
workflows: ["CI"] | |
types: | |
# as early as possible | |
- requested | |
concurrency: | |
group: deploy-preview-${{ github.event.workflow_run.pull_requests[0].number }} | |
cancel-in-progress: true | |
env: | |
TURBO_API: http://127.0.0.1:9080 | |
TURBO_TOKEN: this-is-not-a-secret | |
TURBO_TEAM: myself | |
jobs: | |
# This is the only job that needs access to the source code | |
Build: | |
runs-on: ubuntu-latest | |
timeout-minutes: 15 | |
needs: [determinePR] | |
steps: | |
- uses: actions/checkout@v4 | |
with: | |
# Defaults to ${{ github.repository }}, but that doesn't work for forks | |
# This should also make it obvious why this is in a separate workflow | |
# and in a job that doesn't have access to our secrets. | |
repository: ${{ github.event.workflow_run.head_repository.full_name }} | |
ref: ${{ github.event.workflow_run.head_branch }} | |
- name: TurboRepo local server | |
uses: felixmosh/turborepo-gh-artifacts@v3 | |
with: | |
repo-token: ${{ secrets.GITHUB_TOKEN }} | |
- uses: wyvox/action-setup-pnpm@v3 | |
- run: pnpm turbo build | |
- uses: actions/upload-artifact@v4 | |
with: | |
name: deploy-prep-dist | |
if-no-files-found: error | |
path: | | |
./apps/**/dist/**/* | |
!node_modules/ | |
!./**/node_modules/ | |
################################################################# | |
# For the rest: | |
# Does not checkout code, has access to secrets | |
################################################################# | |
determinePR: | |
# this job gates the others -- if the workflow_run request did not come from a PR, | |
# exit as early as possible | |
runs-on: ubuntu-latest | |
if: github.event.workflow_run.event == 'pull_request' || github.event.inputs.prNumber | |
outputs: | |
number: ${{ steps.pr.outputs.result || steps.number.outputs.pr-number || github.event.inputs.prNumber }} | |
steps: | |
- run: echo '${{ toJSON(github.event) }}' | |
name: "debug workflow_run event" | |
- run: echo "${{ github.event.number }}" | |
name: 'debug github.event.number' | |
# When PR comes from a fork, | |
# github.event.workflow_run.pull_requests is empty | |
- run: echo "${{ github.event.workflow_run.pull_requests[0].number }}" | |
id: number | |
# https://github.com/orgs/community/discussions/25220#discussioncomment-8697399 | |
- name: Find associated pull request | |
# if: "!${{ steps.number.output.pr-number }}" | |
id: pr | |
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 | |
with: | |
script: | | |
const response = await github.rest.search.issuesAndPullRequests({ | |
q: 'repo:${{ github.repository }} is:pr sha:${{ github.event.workflow_run.head_sha }}', | |
per_page: 1, | |
}) | |
const items = response.data.items | |
if (items.length < 1) { | |
console.error('No PRs found') | |
return | |
} | |
const pullRequestNumber = items[0].number | |
console.info("Pull request number is", pullRequestNumber) | |
return pullRequestNumber | |
# We can know the URL ahead of time: | |
# https://<SHA>.limber-glimmer-tutorial.pages.dev | |
# https://<SHA>.limber-glimdown.pages.dev | |
DeployPreview: | |
name: "Deploy: Preview ${{ matrix.app.name }}" | |
runs-on: ubuntu-latest | |
timeout-minutes: 15 | |
needs: [Build] | |
permissions: | |
contents: read | |
strategy: | |
matrix: | |
app: | |
- { path: "./tutorial/dist", cloudflareName: "limber-glimmer-tutorial", name: "tutorial" } | |
- { path: "./repl/dist", cloudflareName: "limber-glimdown", name: "limber" } | |
steps: | |
- uses: actions/download-artifact@v4 | |
name: deploy-prep-dist | |
- name: Preview ${{ matrix.app.name }} | |
working-directory: ./deploy-prep-dist/${{ matrix.app.path }} | |
run: | | |
npx wrangler pages deploy ./ \ | |
--project-name=${{ matrix.app.cloudflareName }} \ | |
--branch=${{ github.event.workflow_run.head_branch }} \ | |
--commit-hash=${{ github.event.workflow_run.head_sha }} | |
env: | |
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
ReformatSubDomain: | |
name: Reformat Sub-domain Alias | |
runs-on: ubuntu-latest | |
needs: [determinePR] | |
outputs: | |
subdomain: ${{ steps.reformat.outputs.subdomain }} | |
steps: | |
- id: reformat | |
run: | | |
branch="${{ github.event.workflow_run.head_branch }}" | |
# Remove any / in the branch name | |
# renovate/uuid becomes renovate-uuid | |
subdomain="${branch//\//-}" | |
subdomain=$(echo $subdomain | cut -c -28) | |
echo "subdomain=$subdomain" >> $GITHUB_OUTPUT | |
PostComment: | |
name: Post Preview URL as comment to PR | |
runs-on: ubuntu-latest | |
needs: [DeployPreview, ReformatSubDomain, determinePR] | |
permissions: | |
pull-requests: write | |
steps: | |
- uses: marocchino/sticky-pull-request-comment@v2 | |
with: | |
header: preview-urls | |
number: ${{ needs.determinePR.outputs.number }} | |
message: |+ | |
| Project | Preview URL[^note] | Manage | | |
| ------- | ------------------ | ------ | | |
| Limber | https://${{ needs.ReformatSubDomain.outputs.subdomain }}.limber-glimdown.pages.dev | [on Cloudflare](https://dash.cloudflare.com/c67910a047e1510fec6d0d0cf442934c/pages/view/limber-glimdown) | | |
| Tutorial | https://${{ needs.ReformatSubDomain.outputs.subdomain }}.limber-glimmer-tutorial.pages.dev | [on Cloudflare](https://dash.cloudflare.com/c67910a047e1510fec6d0d0cf442934c/pages/view/limber-glimmer-tutorial) | |
[Logs](https://github.com/NullVoxPopuli/limber/actions/runs/${{ github.run_id }}) | |
[^note]: if these branch preview links are not working, please check the logs for the commit-based preview link. There is a character limit of 28 for the branch subdomain, as well as some other heuristics, [described here](https://community.cloudflare.com/t/algorithm-to-generate-a-preview-dns-subdomain-from-a-branch-name/477633?u=walshymvp) for the sake of implementation ease in deploy-preview.yml, that algo has been omitted. The URLs are logged in the wrangler output, but it's hard to get outputs from a matrix job. | |