/ concept-collection / turing-surface
Sign in
concept-collection / turing-surface
148 lines · 6.0 KBBlameHistoryRaw
1## Comment `/preview` on a pull request to build it and publish the result to
2## https://tempory.net/previews/<repo>/<branch>/index.html
3##
4## This file is meant to be copied into other repos as-is. Only the "build"
5## block below is repo-specific; everything else is driven by `env` at the top.
6##
7## Two things to know before copying it:
8##
9## 1. `issue_comment` workflows only ever run the copy of this file on the
10## DEFAULT branch, so it has to be merged to main before `/preview` works
11## on any PR — including the PR that adds it.
12## 2. It checks out and builds the PR's head commit while the R2 credentials
13## are in scope. The `author_association` guard means only the repo owner,
14## an org member, or a collaborator can trigger it, but one of those people
15## asking for a preview of an untrusted fork PR would run that fork's code.
16## Read the diff before commenting `/preview` on a fork.
18name: preview
20on:
21 issue_comment:
22 types: [created]
24env:
25 R2_BUCKET: tempory
26 R2_PREFIX: previews
27 PREVIEW_BASE_URL: https://tempory.net/previews
28 DIST_DIR: dist
29 NODE_VERSION: "24"
31permissions:
32 contents: read
33 issues: write
34 pull-requests: write
36concurrency:
37 group: preview-${{ github.event.issue.number }}
38 cancel-in-progress: true
40jobs:
41 preview:
42 if: >-
43 github.event.issue.pull_request &&
44 startsWith(github.event.comment.body, '/preview') &&
45 contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
46 runs-on: ubuntu-latest
47 env:
48 GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49 PR_NUMBER: ${{ github.event.issue.number }}
50 APP_NAME: ${{ github.event.repository.name }}
51 steps:
52 - name: Acknowledge the command
53 env:
54 COMMENT_ID: ${{ github.event.comment.id }}
55 run: |
56 gh api --silent -X POST \
57 "repos/$GITHUB_REPOSITORY/issues/comments/$COMMENT_ID/reactions" \
58 -f content=eyes
60 # The comment event carries no commit info, so ask the API what the PR
61 # currently points at. `head.repo` differs from this repo for fork PRs.
62 - name: Resolve the PR head
63 id: head
64 run: |
65 gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" \
66 --jq '"repo=\(.head.repo.full_name)", "sha=\(.head.sha)", "branch=\(.head.ref)"' \
67 >> "$GITHUB_OUTPUT"
69 # Branch names may contain characters that are awkward in a URL path
70 # (most commonly `/`, as in `feat/thing`), so flatten to one path segment.
71 # BRANCH goes through the environment rather than `${{ }}` because on a
72 # fork PR its value is chosen by someone outside the org.
73 - name: Compute the deploy path
74 id: path
75 env:
76 BRANCH: ${{ steps.head.outputs.branch }}
77 run: |
78 slug=$(printf '%s' "$BRANCH" | tr -c 'A-Za-z0-9._-' '-')
79 echo "key=$R2_PREFIX/$APP_NAME/$slug" >> "$GITHUB_OUTPUT"
80 echo "url=$PREVIEW_BASE_URL/$APP_NAME/$slug/index.html" >> "$GITHUB_OUTPUT"
82 - uses: actions/checkout@v4
83 with:
84 repository: ${{ steps.head.outputs.repo }}
85 ref: ${{ steps.head.outputs.sha }}
87 - uses: actions/setup-node@v4
88 with:
89 node-version: ${{ env.NODE_VERSION }}
90 cache: npm
92 ## ---- repo-specific build (replace this block when copying) ----------
93 # numbl is a `file:../../numbl` dependency: we use its compiler internals
94 # (parser, lowerer, IR, inline pass), which its published package
95 # `exports` do not expose. Clone it where that relative path expects it,
96 # pinned to the same ref as ci.yml and deploy.yml.
97 - name: Check out numbl (sibling dependency)
98 env:
99 NUMBL_REF: 38ce14046d64d03ecf05cb57def53057a6bc64ab
100 run: |
101 git clone --filter=blob:none --no-checkout \
102 https://github.com/flatironinstitute/numbl.git "$GITHUB_WORKSPACE/../../numbl"
103 git -C "$GITHUB_WORKSPACE/../../numbl" checkout --quiet "$NUMBL_REF"
104 # --ignore-scripts: npm runs a linked package's `prepare` script, and
105 # numbl's is husky, which is not installed here.
106 - run: npm ci --ignore-scripts
107 - run: npm run build
108 ## ---- end repo-specific build ----------------------------------------
110 - name: Publish to R2
111 env:
112 AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
113 AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
114 AWS_DEFAULT_REGION: auto
115 R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
116 # R2 rejects the extra integrity checksums AWS CLI v2 adds by default.
117 AWS_REQUEST_CHECKSUM_CALCULATION: when_required
118 AWS_RESPONSE_CHECKSUM_VALIDATION: when_required
119 KEY: ${{ steps.path.outputs.key }}
120 run: |
121 # Two passes so the entry HTML is always revalidated while the
122 # content-hashed assets around it can be cached hard. The filters
123 # apply to both sides of the sync, so `--delete` in each pass only
124 # ever removes files of that same kind.
125 aws s3 sync "$DIST_DIR" "s3://$R2_BUCKET/$KEY" \
126 --endpoint-url "$R2_ENDPOINT" --delete --no-progress \
127 --exclude '*.html' \
128 --cache-control 'public, max-age=31536000, immutable'
129 aws s3 sync "$DIST_DIR" "s3://$R2_BUCKET/$KEY" \
130 --endpoint-url "$R2_ENDPOINT" --delete --no-progress \
131 --exclude '*' --include '*.html' \
132 --cache-control 'no-cache'
134 - name: Comment with the preview link
135 env:
136 URL: ${{ steps.path.outputs.url }}
137 SHA: ${{ steps.head.outputs.sha }}
138 run: |
139 gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" \
140 --body "Preview of \`${SHA:0:7}\` is live: $URL"
142 - name: Report failure
143 if: failure()
144 env:
145 RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
146 run: |
147 gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" \
148 --body "Preview build failed — [run log]($RUN_URL)."
moveopenescclose