-
Notifications
You must be signed in to change notification settings - Fork 14
265 lines (243 loc) · 12.2 KB
/
Copy pathbuild-and-deploy.yml
File metadata and controls
265 lines (243 loc) · 12.2 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
name: Build and deploy docs
# Part of the gha-docs-build migration: builds the docs via `make dist`
# and pushes the result to a Cloudflare Pages project via Direct Upload.
# Replaces CF Pages' native build runner — CF Pages stays only as the
# static host.
#
# Production-only path (mirrors main). PR previews are handled by the two-stage
# pair `build-pr.yml` (untrusted build, no secrets) + `deploy-pr-preview.yml`
# (trusted workflow_run deploy, has secrets), because GitHub does NOT expose repo
# secrets to pull_request workflows triggered from forks. See DOC-1228.
#
# Triggers:
# - push to v1 — production deploy (project=docs, --branch=v1). The prod path;
# CF auto-deploys are disabled, so GHA owns prod end-to-end.
# - workflow_dispatch — manual run; redeploys the branch's current commit.
on:
push:
branches:
- v1
workflow_dispatch:
# contents: write — the one-merge cut (DOC-1245) materializes + pushes the stable
# tag named by versions.toml as a pre-build step (mirrors main; inert without one).
permissions:
contents: write
pull-requests: write
# Opt in early to the Node.js 24 runtime for JavaScript actions. GitHub forces
# this default on 2026-06-02 and removes Node.js 20 on 2026-09-16. Setting the
# variable now silences the deprecation warning, tests compatibility with our
# pinned action versions (checkout@v4, setup-python@v5, upload-artifact@v4,
# setup-uv@v5, wrangler-action@v3, sticky-pull-request-comment@v2), and gives
# us a controlled window to bump if any break.
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
# Concurrency policy (mirrors main):
# - production deploys use one `prod` group PER LINE, queued in order (never
# cancel), so two merges on this line can't race on the CF Pages project,
# and can't race on cutting the same stable tag (the cut runs in-job, so
# serializing the job serializes the cut).
# - workflow_dispatch: own `manual` group, so manual re-runs never block or
# get blocked by an in-progress production deploy. The ref suffix applies to
# this arm too, so manual runs are per-line as well -- a manual v1 deploy
# cannot evict a manual main one, which is the same guarantee `prod` gets and
# the reason the suffix is unconditional rather than applied to `prod` only.
# This workflow never runs on `pull_request` (see Triggers above), so there is
# no PR arm here. PR previews live in build-pr.yml + deploy-pr-preview.yml.
#
# `github.ref_name` is what makes the group per-line. Without it this file and
# main's evaluate to the SAME group string -- both said "one prod group", each
# reading as though it owned its own queue, and together they formed one shared
# queue for two lines. "Mirrors main" was the trap, not the safeguard.
#
# The failure that produced is silent, and it is documented behaviour rather
# than a quirk: `cancel-in-progress: false` protects a RUNNING job, but per
# GitHub's concurrency docs, "any existing pending job or workflow in the same
# concurrency group will be canceled and the new queued job or workflow will
# take its place" -- newest wins, cancellation setting notwithstanding.
# https://docs.github.com/en/actions/using-jobs/using-concurrency
# So a v1 deploy queued behind a main deploy was discarded the moment a second
# main deploy arrived, reporting `cancelled` rather than `failure` -- nothing
# turned red and this line simply did not ship. Observed 2026-08-18.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && 'manual' || 'prod' }}-${{ github.ref_name }}
cancel-in-progress: false
jobs:
build-and-deploy:
name: "Build and deploy docs"
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
# 0 (was 1): the multi-version assembly + cut checkout tags + v1 into
# isolated worktrees, so it needs full history + all tags.
fetch-depth: 0
- name: Set up Hugo
uses: peaceiris/actions-hugo@v3
with:
hugo-version: '0.161.1'
extended: true
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Print environment
run: |
echo "::group::versions"
hugo version
python --version
uv --version
echo "::endgroup::"
# One-merge cut (DOC-1245), mirrored from main: versions.toml names the stable
# tag /docs/v1 serves. If it doesn't exist yet, THIS merge is the promotion —
# materialize it (guarded) BEFORE the build, so the v1 line assembles from it in
# the same job (no cut↔deploy race). Inert until versioning go-live (no
# versions.toml → skipped), so v1 stays a single static build until then.
- name: Materialize stable tag if needed (cut)
if: github.event_name == 'push'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO_ROOT: ${{ github.workspace }}
run: |
set -euo pipefail
if [ ! -f versions.toml ]; then
echo "no versions.toml → versioning off; skip cut"; exit 0
fi
STABLE=$(sed -n 's/^stable *= *"\(.*\)"/\1/p' versions.toml | head -1)
if [ -z "$STABLE" ]; then echo "versions.toml has no stable → skip"; exit 0; fi
if git rev-parse -q --verify "refs/tags/$STABLE" >/dev/null; then
echo "stable tag $STABLE already cut → no-op"; exit 0
fi
echo "stable $STABLE not yet cut → materializing at this merge"
bash unionai-docs-infra/scripts/cut-docs-version.sh --push
- name: Build dist
run: |
start=$(date +%s)
# One shared CI build step (DOC-1333): `auto` = the versions.toml gate,
# identical to the previous inline logic. build-pr.yml calls the SAME
# script with an explicit mode, so the two build paths cannot drift.
LATEST_REF=origin/v1 bash unionai-docs-infra/scripts/ci-build-dist.sh auto
echo "BUILD_SECONDS=$(( $(date +%s) - start ))" >> "$GITHUB_ENV"
- name: Check generated links
run: |
# The links the GENERATOR writes into dist/. `Check Internal Links`
# reads content/ and cannot see these: v1 had 1,649 of them pointing at
# nothing while that job stayed green (DOC-1525).
# Runs here rather than in its own workflow because it needs the dist
# this job already built, and a second full Hugo build to check links
# is not worth the minutes.
make check-generated-links
- name: Check rendered images
run: |
# The images the BUILT site serves, resolved the way a browser does --
# against the page's URL, not the source file's directory. `Check
# Images` reads content/ and cannot see this: Hugo's render hooks
# rebase an image src, so a path can be correct in the markdown and
# wrong in the HTML. Two v1 tutorials shipped images that 404 on the
# live site while every check passed (DOC-1525, unionai-examples#307).
make check-rendered-images
- name: Emit build provenance
if: success()
run: |
# Write build-info.json at dist/docs/v1/ so it's served at
# www.union.ai/docs/v1/build-info.json (CloudFront routes
# /docs/v1/* to v1.docs-dog.pages.dev). On main the equivalent
# path is dist/docs/build-info.json since CloudFront routes
# /docs/* (precedence 3) to the main docs-dog.pages.dev origin.
mkdir -p dist/docs/v1
cat > dist/docs/v1/build-info.json <<EOF
{
"builder": "github-actions",
"repository": "${{ github.repository }}",
"workflow_file": ".github/workflows/build-and-deploy.yml",
"run_id": "${{ github.run_id }}",
"run_url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
"commit": "${{ github.sha }}",
"ref": "${{ github.ref_name }}",
"event": "${{ github.event_name }}",
"built_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
cat dist/docs/v1/build-info.json
- name: Build summary
if: always()
run: |
echo "::group::dist tree (top 2 levels)"
find dist -maxdepth 2 -mindepth 1 -print 2>/dev/null | sort || true
echo "::endgroup::"
echo "::group::dist size"
du -sh dist 2>/dev/null || true
du -sh dist/docs/v2 dist/docs/v1 2>/dev/null || true
echo "::endgroup::"
if [ -n "${BUILD_SECONDS:-}" ]; then
echo "Build wall time: ${BUILD_SECONDS}s"
fi
- name: Upload dist artifact
if: success()
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
if-no-files-found: error
- name: Sanitize branch name for CF Pages
if: success()
id: branch
run: |
# CF Pages branch names: lowercase alphanumeric + hyphens, max 28 chars.
# Production-only workflow, so `github.ref_name` is always `v1`
# (push) or the dispatched ref. The `pr-<num>-` aliasing lives in
# deploy-pr-preview.yml, which is what handles PR events.
raw="${{ github.ref_name }}"
sanitized=$(echo "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's#[^a-z0-9]+#-#g' | sed -E 's#^-+|-+$##g' | cut -c1-28)
echo "Source branch: $raw → CF Pages branch: $sanitized"
echo "name=$sanitized" >> "$GITHUB_OUTPUT"
- name: Deploy to Cloudflare Pages
if: success()
id: deploy
# Step-level cap: a normal CF Pages deploy is ~1-3 min. Without this a
# hung wrangler upload eats the whole job's timeout-minutes budget
# (incl. the build) before failing — observed once at ~26 min on a large
# API-docs regen. Fail fast so the deploy can be re-run. See DOC-1229.
timeout-minutes: 10
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_PAGES_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
# --commit-dirty: make dist regenerates tracked files (notebooks,
# API docs), so the working tree is always dirty by the time we hit
# wrangler. The warning is noise, not a real condition.
# We don't pass --commit-hash because we use fetch-depth: 1 above
# (shallow checkout), which means the local repo can't resolve the
# SHA wrangler tries to look up and would emit "fatal: bad object".
# wrangler-action records the commit metadata on its own via the
# GITHUB_SHA env var.
command: pages deploy ./dist --project-name=docs --branch=${{ steps.branch.outputs.name }} --commit-dirty=true
- name: Update search index
# After the deploy, not before: the index should describe what is now
# served. Scoped to the slices THIS branch built -- v1 only -- so main
# (v2 + latest) and this branch cannot clobber each other's records.
#
# Skips itself when the secret is absent, so forks and any repo without
# Algolia credentials still deploy normally.
if: success() && github.event_name == 'push'
timeout-minutes: 15
env:
ALGOLIA_DOCS_2_APPLICATION_ID: ${{ secrets.ALGOLIA_DOCS_2_APPLICATION_ID }}
ALGOLIA_DOCS_2_WRITE_API_KEY: ${{ secrets.ALGOLIA_DOCS_2_WRITE_API_KEY }}
run: make index-search
- name: Deploy summary
if: success()
run: |
echo "## Deployment" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- Project: \`docs\`" >> "$GITHUB_STEP_SUMMARY"
echo "- Branch (CF Pages): \`${{ steps.branch.outputs.name }}\`" >> "$GITHUB_STEP_SUMMARY"
echo "- Commit: \`${{ github.sha }}\`" >> "$GITHUB_STEP_SUMMARY"
if [ -n "${{ steps.deploy.outputs.deployment-url }}" ]; then
echo "- Deployment URL: ${{ steps.deploy.outputs.deployment-url }}" >> "$GITHUB_STEP_SUMMARY"
fi