Repository navigation
199 lines (175 loc) · 7.83 KB
/
Copy pathpublish.yml
File metadata and controls
199 lines (175 loc) · 7.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# Build the catalog from the organization, render the site, publish to Pages.
#
# The site has no content of its own to keep in sync: every tool's card and
# detail page is built from that tool's tool.json, its latest release and the
# images the manifest points at. A release in another repository therefore
# changes a page here with no commit to push, which is why this runs on a
# schedule as well as on a push.
#
# It runs on a schedule rather than being woken by the tools, and that is the
# security decision, not an oversight: waking it would mean a token with write
# access to this repository sitting as a secret on every public tool
# repository. No secrets outside this repository. The tools publish releases,
# and the site comes and looks, hourly.
name: publish
on:
push:
branches: [main]
schedule:
# Hourly, off the hour: a release in another repository shows up here
# within the hour. To have it sooner:
# gh workflow run publish.yml -R tui-tools/tui-tools.github.io
- cron: "17 * * * *"
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
# One deployment at a time, and never cancel one in flight: a half-published
# site is worse than a slightly stale one.
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
name: build the site
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "22"
cache: npm
- name: install
run: npm ci
- name: build the catalog
env:
# Lifts the API rate limit. Read access to public repositories in
# this organization is all it is used for.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run catalog
# After the catalog, because the tool cards embed the screenshots it has
# just downloaded into public/tools/, and before astro, because Astro
# copies public/ into dist/ as it renders.
- name: draw the link previews
run: npm run og
- name: build the site
env:
SITE_URL: ${{ vars.SITE_DOMAIN != '' && format('https://{0}', vars.SITE_DOMAIN) || 'https://tui-tools.github.io' }}
run: npx astro build
# The custom domain is opt-in and inert until the owner sets the
# SITE_DOMAIN repository variable. See the README: the variable and a DNS
# CNAME are the two halves, and setting only one of them breaks the site.
- name: write CNAME
if: vars.SITE_DOMAIN != ''
run: echo "${{ vars.SITE_DOMAIN }}" > dist/CNAME
- uses: actions/upload-pages-artifact@v5
with:
path: dist
# The catalog is what decides what every page says, so its hash is the
# honest answer to "did the site's content change?". The ship job below
# turns it into the build stamp it sends to the container host; see the
# comment there for why that host needs to be told.
- name: keep the catalog for the ship job
uses: actions/upload-artifact@v7
with:
name: catalog
path: src/data/catalog.json
retention-days: 1
deploy:
name: deploy to Pages
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v5
# Off unless the owner turns it on, and unrelated to Pages: the same site,
# shipped to a second host.
#
# Quave ONE has no GitHub App installed on this organization — that is a click
# only the owner can make — so the source is pushed rather than pulled, with
# the documented three-step Code API: ask for a pre-signed URL, PUT the
# tarball, say it landed. The last step triggers the build.
#
# Which leaves the problem this job really exists for. The site's content
# lives outside this repository: a release in another repo changes a page here
# with no commit to push, which is why Pages rebuilds hourly rather than only
# on push. The container host sees the same identical source every hour and
# reuses the image it already built — correctly, and uselessly, because the
# catalog is baked into that image. So the tarball carries `.build-stamp`, the
# hash of the catalog *this run* built. Identical content, identical stamp,
# identical source: the platform reuses the image and no build minutes are
# spent. A release upstream changes the catalog, the stamp, the source and the
# `COPY . .` layer in one go, and the image is rebuilt with the new content.
#
# To turn it on, set the repository variable and the secret:
#
# gh variable set QUAVE_ENV_NAME --body tui-tools-tui-site-production
# gh secret set QUAVE_API_TOKEN # app-environment token
#
# Leave QUAVE_ENV_NAME unset and the job is skipped, which is the state this
# repository is in while Pages is the only host.
ship:
name: ship to the container host
needs: build
if: vars.QUAVE_ENV_NAME != ''
runs-on: ubuntu-latest
env:
API: ${{ vars.QUAVE_API_URL || 'https://api.quave.cloud/api/public/v1' }}
ENV_NAME: ${{ vars.QUAVE_ENV_NAME }}
TOKEN: ${{ secrets.QUAVE_API_TOKEN }}
steps:
- uses: actions/checkout@v7
- name: fetch the catalog this run built
uses: actions/download-artifact@v8
with:
name: catalog
path: ${{ runner.temp }}/catalog
- name: pack the source
run: |
set -euo pipefail
if [ -z "$TOKEN" ]; then
echo "QUAVE_ENV_NAME is set but QUAVE_API_TOKEN is not; both are needed."
exit 1
fi
# The stamp, and the whole reason this is not a plain redeploy call.
# It covers the catalog AND the source commit: a push that changes
# templates without changing the catalog must rebuild the image too.
{ sha256sum "${RUNNER_TEMP}/catalog/catalog.json" | cut -d' ' -f1; echo "${GITHUB_SHA}"; } \
| sha256sum | cut -d' ' -f1 > .build-stamp
# Reproducible flags throughout — sorted entries, a fixed mtime, no
# owner names, and gzip without its timestamp header — so a run that
# changed nothing produces a byte-identical tarball, which is what
# lets the platform recognise it as the same source.
tar --sort=name --mtime='UTC 1970-01-01' \
--owner=0 --group=0 --numeric-owner \
--exclude=./.git \
--exclude=./node_modules \
--exclude=./dist \
--exclude=./.astro \
-cf - . | gzip -n > "${RUNNER_TEMP}/source.tgz"
# --fail-with-body throughout, so a 401 or a 404 stops the job and still
# prints what the API said, rather than leaving a stale site up in silence.
- name: upload it and trigger the build
run: |
set -euo pipefail
key_response=$(curl --silent --show-error --fail-with-body \
-X POST "${API}/app-env/request-storage-key" \
-H "Authorization: ${TOKEN}" \
-H 'Content-Type: application/json' \
-d "$(jq -nc --arg env "$ENV_NAME" '{envName: $env, fileExtension: "tgz"}')")
upload_url=$(printf '%s' "$key_response" | jq -er '.uploadUrl')
storage_key=$(printf '%s' "$key_response" | jq -er '.storageKey')
curl --silent --show-error --fail-with-body \
-X PUT "$upload_url" \
-H 'Content-Type: application/x-gzip' \
--data-binary "@${RUNNER_TEMP}/source.tgz" > /dev/null
curl --silent --show-error --fail-with-body \
-X POST "${API}/app-env/upload-storage-event" \
-H "Authorization: ${TOKEN}" \
-H 'Content-Type: application/json' \
-d "$(jq -nc --arg env "$ENV_NAME" --arg key "$storage_key" \
'{envName: $env, storageKey: $key}')"