/ concept-collection / turing-surface
Sign in
concept-collection / turing-surface
153 lines · 6.3 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. One file in it is
97 # generated rather than committed — the interpreter's stdlib bundle,
98 # which numbl gitignores — so a bare checkout is missing it and
99 # `executeCode.ts` fails to resolve it; its generator only reads .m files
100 # off disk, so plain `node` runs it without installing anything.
101 - name: Check out numbl (sibling dependency)
102 env:
103 NUMBL_REF: main
104 run: |
105 git clone --filter=blob:none --no-checkout \
106 https://github.com/flatironinstitute/numbl.git "$GITHUB_WORKSPACE/../../numbl"
107 git -C "$GITHUB_WORKSPACE/../../numbl" checkout --quiet "$NUMBL_REF"
108 node "$GITHUB_WORKSPACE/../../numbl/scripts/bundle-stdlib.ts"
109 # --ignore-scripts: npm runs a linked package's `prepare` script, and
110 # numbl's is husky, which is not installed here.
111 - run: npm ci --ignore-scripts
112 - run: npm run build
113 ## ---- end repo-specific build ----------------------------------------
115 - name: Publish to R2
116 env:
117 AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
118 AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
119 AWS_DEFAULT_REGION: auto
120 R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
121 # R2 rejects the extra integrity checksums AWS CLI v2 adds by default.
122 AWS_REQUEST_CHECKSUM_CALCULATION: when_required
123 AWS_RESPONSE_CHECKSUM_VALIDATION: when_required
124 KEY: ${{ steps.path.outputs.key }}
125 run: |
126 # Two passes so the entry HTML is always revalidated while the
127 # content-hashed assets around it can be cached hard. The filters
128 # apply to both sides of the sync, so `--delete` in each pass only
129 # ever removes files of that same kind.
130 aws s3 sync "$DIST_DIR" "s3://$R2_BUCKET/$KEY" \
131 --endpoint-url "$R2_ENDPOINT" --delete --no-progress \
132 --exclude '*.html' \
133 --cache-control 'public, max-age=31536000, immutable'
134 aws s3 sync "$DIST_DIR" "s3://$R2_BUCKET/$KEY" \
135 --endpoint-url "$R2_ENDPOINT" --delete --no-progress \
136 --exclude '*' --include '*.html' \
137 --cache-control 'no-cache'
139 - name: Comment with the preview link
140 env:
141 URL: ${{ steps.path.outputs.url }}
142 SHA: ${{ steps.head.outputs.sha }}
143 run: |
144 gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" \
145 --body "Preview of \`${SHA:0:7}\` is live: $URL"
147 - name: Report failure
148 if: failure()
149 env:
150 RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
151 run: |
152 gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" \
153 --body "Preview build failed — [run log]($RUN_URL)."
moveopenescclose