-
Notifications
You must be signed in to change notification settings - Fork 5
278 lines (256 loc) · 13.3 KB
/
Copy pathsync-static-site.yml
File metadata and controls
278 lines (256 loc) · 13.3 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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
name: Sync static-site docs
on:
schedule:
- cron: '0 9 * * 3' # Weekly on Wednesday
workflow_dispatch:
inputs:
ref:
description: >-
Ref to sync, for a docs fix that has shipped upstream but not been
released. Any commit-ish the clone can resolve. Leave empty to sync
the latest release, which is what the weekly run does.
required: false
type: string
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
- name: Create GitHub App Token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
id: app-token
with:
client-id: ${{ vars.PR_AUTOMATION_BOT_PUBLIC_CLIENT_ID }}
private-key: ${{ secrets.PR_AUTOMATION_BOT_PUBLIC_PRIVATE_KEY }}
# certified-assets is not a submodule: the build resolves no file from it.
# A blobless clone is only needed to reason about refs (is the latest
# release already contained in the pin, did docs/ change), which the
# contents API cannot answer.
- name: Clone certified-assets
run: git clone --filter=blob:none --no-checkout https://github.com/dfinity/certified-assets.git /tmp/certified-assets
- name: Resolve the ref to sync
id: check
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
# A dispatch input reaches the shell as data, never as script.
INPUT_REF: ${{ inputs.ref }}
run: |
PIN=$(node -p "require('./.sources/upstream.json').synced.find(e => e.repo === 'dfinity/certified-assets').pinned")
echo "pin=$PIN" >> $GITHUB_OUTPUT
if [ -n "$INPUT_REF" ]; then
# A ref given by hand is the maintainer's call, so the release
# checks below do not apply: the reason to pass one is a docs fix
# that is not in a release yet.
#
# Tried as given and then under `origin/`, because a clone creates a
# local branch only for the default branch and leaves every other
# one reachable as `origin/<branch>` alone. A sha and a tag resolve
# on the first attempt.
REF_COMMIT=$(git -C /tmp/certified-assets rev-parse --verify --quiet "${INPUT_REF}^{commit}" \
|| git -C /tmp/certified-assets rev-parse --verify --quiet "origin/${INPUT_REF}^{commit}") || {
echo "::error::Cannot resolve ref '$INPUT_REF' in dfinity/certified-assets."
exit 1
}
TAG=$(git -C /tmp/certified-assets rev-parse --short "$REF_COMMIT")
echo "Manual ref: $INPUT_REF resolved to $TAG. Pinned: $PIN."
# Peel the pin to a commit before comparing. The tags here are
# annotated, so an unpeeled tag name resolves to the tag object and
# would never equal a commit: dispatching the pinned tag would then
# read as a change and rewrite the pin from that tag to its own
# commit sha, moving a tag pin onto a commit for no reason.
PIN_COMMIT=$(git -C /tmp/certified-assets rev-parse --verify --quiet "${PIN}^{commit}") || {
echo "::error::Pin '$PIN' does not resolve in dfinity/certified-assets."
exit 1
}
if [ "$REF_COMMIT" = "$PIN_COMMIT" ]; then
echo "That is the pin already. Nothing to sync."
echo "needed=false" >> $GITHUB_OUTPUT
exit 0
fi
# A ref the pin already contains moves the pin backwards. That is
# what a rollback is, so it runs, but it is also what a mistyped sha
# looks like, so it is not silent: the run warns and the PR body
# says so, which is the difference between the two.
if git -C /tmp/certified-assets merge-base --is-ancestor \
"$REF_COMMIT" "$PIN_COMMIT"; then
echo "::warning::$INPUT_REF is behind the pin $PIN. This will move the pin backwards."
echo "backwards=true" >> $GITHUB_OUTPUT
fi
else
# Stable releases only, the same pattern the upstream.json watcher uses.
# `^v[0-9]` would accept v0.4.0-rc.1 and publish docs for a prerelease.
TAG=$(git -C /tmp/certified-assets tag --sort=-version:refname \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1)
echo "Pinned: $PIN. Latest release: $TAG."
# The pin is allowed to sit ahead of the latest release while a docs
# fix has shipped but a release has not (the state this sync started
# in). Syncing the tag then would publish older prose.
if git -C /tmp/certified-assets merge-base --is-ancestor "$TAG" "$PIN"; then
echo "Pin already contains $TAG. Nothing to sync."
echo "needed=false" >> $GITHUB_OUTPUT
exit 0
fi
fi
echo "tag=$TAG" >> $GITHUB_OUTPUT
# Skip only when a PR is actually open. A branch on its own proves
# nothing: if a previous run pushed and then failed at `gh pr create`,
# treating the branch as a PR would strand that release forever, with
# nothing to review and no further attempts.
BRANCH="infra/sync-static-site-${TAG}"
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
if [ -n "$(gh pr list --head "$BRANCH" --state open --json number --jq '.[].number')" ]; then
echo "A PR for $BRANCH is already open. Skipping."
echo "needed=false" >> $GITHUB_OUTPUT
exit 0
fi
if git ls-remote --exit-code origin "refs/heads/${BRANCH}" > /dev/null 2>&1; then
echo "Branch $BRANCH exists with no open PR (an earlier run stopped"
echo "between push and PR creation). Deleting it so this run can retry."
git push origin --delete "$BRANCH"
fi
CHANGED=$(git -C /tmp/certified-assets diff --name-only "${PIN}..${TAG}" -- docs/)
if [ -z "$CHANGED" ]; then
# A release that ships canister changes without touching docs/ leaves
# the synced pages byte-identical, so there is nothing to review. Two
# cases, and only one of them is worth a pull request.
if [ -n "$INPUT_REF" ] || ! git -C /tmp/certified-assets show-ref \
--verify --quiet "refs/tags/${PIN}"; then
# The pin is a commit (or a ref was dispatched by hand). Moving it
# onto the tag is the point: a commit pin is allowed only while no
# release carries the pages, and skipping here would strand it
# there for good.
echo "No docs/ changes between $PIN and $TAG: advancing the pin only."
echo "needed=true" >> $GITHUB_OUTPUT
echo "pin_only=true" >> $GITHUB_OUTPUT
else
# The pin is already a tag, so a pull request would carry an empty
# page diff and a bumped ref, for a reader to review and merge with
# nothing in it. The pin lags the release and stays accurate: it
# says which ref this copy came from, and the copy still matches it.
# The release itself is not lost, since the recipe that deploys this
# canister releases in lockstep and is tracked under `watched`.
echo "No docs/ changes between $PIN and $TAG, and the pin is a tag."
echo "Nothing to sync."
echo "needed=false" >> $GITHUB_OUTPUT
exit 0
fi
else
echo "needed=true" >> $GITHUB_OUTPUT
echo "Changed upstream pages:"
echo "$CHANGED"
echo "pin_only=false" >> $GITHUB_OUTPUT
echo "changed_files<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGED" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
fi
- name: Move the pin to the release tag
if: steps.check.outputs.needed == 'true'
run: |
node -e '
const fs = require("fs");
const file = ".sources/upstream.json";
const config = JSON.parse(fs.readFileSync(file, "utf8"));
const entry = config.synced.find((e) => e.repo === "dfinity/certified-assets");
entry.pinned = process.argv[1];
fs.writeFileSync(file, JSON.stringify(config, null, 2) + "\n");
' "$TAG"
env:
TAG: ${{ steps.check.outputs.tag }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
if: steps.check.outputs.needed == 'true'
with:
node-version: 22
cache: npm
- name: Install dependencies
if: steps.check.outputs.needed == 'true'
run: npm ci
- name: Run static-site docs sync
if: steps.check.outputs.needed == 'true'
run: npm run sync:static-site
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
# The synced tree is deliberately not in the validator's SYNCED allowlist:
# the sync normalizes and rewrites so that the pages pass the same checks
# as a hand-written page, and an exemption would hide the day they stop.
- name: Validate
if: steps.check.outputs.needed == 'true'
run: npm run validate
# Same set and the same URL rewrite as build.yml. `.gitmodules` uses SSH
# URLs, which a runner cannot fetch, and `.sources/motoko` is required
# because pages under docs/languages/motoko/ pull code through
# `file=<motokoExamples>/...` includes: without it they render empty
# without failing the build, so this step would have reported a passing
# build for a site with empty Motoko pages.
- name: Initialize submodules (required for build)
if: steps.check.outputs.needed == 'true'
run: |
git config --global url."https://github.com/".insteadOf "git@github.com:"
git submodule update --init --depth 1 .sources/examples .sources/motoko
- name: Build check
if: steps.check.outputs.needed == 'true'
run: npm run build
- name: Create PR
if: steps.check.outputs.needed == 'true'
run: |
git config user.name "pr-automation-bot-public[bot]"
git config user.email "pr-automation-bot-public[bot]@users.noreply.github.com"
BRANCH="infra/sync-static-site-${TAG}"
git checkout -b "$BRANCH"
git add .sources/upstream.json docs/guides/frontends/static-site
git commit -m "chore: sync static-site docs to dfinity/certified-assets ${TAG}"
git push -u origin "$BRANCH"
{
echo "## Summary"
echo ""
echo "Automated sync of the certified-assets user docs."
echo ""
if [ -n "$INPUT_REF" ]; then
echo "**Ref:** \`$TAG\` (pinned from \`$PIN\`), dispatched by hand as \`$INPUT_REF\`."
echo "A ref is synced by hand when a docs fix has shipped upstream but not"
echo "been released; the next release moves the pin back onto a tag."
if [ "$BACKWARDS" = "true" ]; then
echo ""
echo "> [!WARNING]"
echo "> This ref is **behind** the pin, so the pin moves backwards and any"
echo "> page below is reverted to the older text. Intended for a rollback."
echo "> If you meant to move forward, close this and dispatch the right ref."
fi
else
echo "**Release:** \`$TAG\` (pinned from \`$PIN\`)"
fi
echo ""
if [ "$PIN_ONLY" = "true" ]; then
echo "No page changed in this range, so the only diff is the pin and the"
echo "\`source_ref\` each page records."
else
echo "**Changed upstream files:**"
while IFS= read -r f; do
[ -n "$f" ] && echo "- \`$f\`"
done <<< "$CHANGED"
fi
echo ""
echo "- Ran \`npm run sync:static-site\`, regenerating \`docs/guides/frontends/static-site/\`"
echo "- Validator and build passed"
echo ""
echo "## Checklist"
echo ""
echo "- [ ] Review the page diffs for content changes"
echo "- [ ] Check whether a behavior change contradicts our own Frontends pages (\`certification.md\`, \`asset-canister.md\`)"
echo "- [ ] Check whether the recipe version named in \`icp.yaml\` examples needs bumping with it"
} > /tmp/pr-body.md
gh pr create \
--title "chore: sync static-site docs to dfinity/certified-assets ${TAG}" \
--body-file /tmp/pr-body.md
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
# Values from the upstream repo reach the shell as data, never as
# script: a filename containing shell metacharacters would otherwise
# be interpolated into this step's source.
TAG: ${{ steps.check.outputs.tag }}
PIN: ${{ steps.check.outputs.pin }}
CHANGED: ${{ steps.check.outputs.changed_files }}
PIN_ONLY: ${{ steps.check.outputs.pin_only }}
INPUT_REF: ${{ inputs.ref }}
BACKWARDS: ${{ steps.check.outputs.backwards }}