Skip to content

Commit 33c21de

Browse files
ChBrainclaude
andauthored
governance: deploy builds once, rsyncs per surface (#473)
deploy-surface splits into resolve -> build -> matrix rsync legs and gains surface=all; deploy-staging drops its six-call matrix for one all-surfaces call. A six-surface release now costs one build (and one baseline-gate run on production) instead of six. include-hidden-files on the dist artifact is load-bearing: .well-known/ and .htaccess must survive the artifact hop or the --delete rsync would remove them live. Claude-Session: https://claude.ai/code/session_0176ZYTwoV8V4So9SBTizc5X Co-authored-by: Claude <noreply@anthropic.com>
1 parent 82a0366 commit 33c21de

2 files changed

Lines changed: 77 additions & 27 deletions

File tree

.github/workflows/deploy-staging.yml

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
name: deploy-staging
22

3-
# After CI is green on main, deploy every surface to staging. Each
4-
# surface runs through the shared deploy-surface reusable workflow, so
5-
# staging and production share one deploy engine.
3+
# After CI is green on main, deploy every surface to staging through
4+
# the shared deploy-surface reusable workflow (surface=all), so staging
5+
# and production share one deploy engine and the site builds ONCE per
6+
# staging deploy instead of once per surface.
67
#
7-
# Surfaces deploy independently (matrix, fail-fast disabled): one
8-
# surface failing does not block the others, and any single surface can
9-
# be re-run on its own from the Actions tab.
8+
# Inside deploy-surface the per-surface rsync legs still run as an
9+
# independent matrix (fail-fast disabled): one surface failing does not
10+
# block the others, and a single leg can be re-run on its own from the
11+
# Actions tab without rebuilding.
1012

1113
on:
1214
workflow_run:
@@ -21,19 +23,9 @@ permissions:
2123
jobs:
2224
deploy:
2325
if: ${{ github.event.workflow_run.conclusion == 'success' }}
24-
strategy:
25-
fail-fast: false
26-
matrix:
27-
surface:
28-
- architecture
29-
- main
30-
- cultures
31-
- plays
32-
- misfits
33-
- writing
3426
uses: ./.github/workflows/deploy-surface.yml
3527
with:
36-
surface: ${{ matrix.surface }}
28+
surface: all
3729
env: staging
3830
# Deploy exactly the commit CI just validated, not the tip of main.
3931
ref: ${{ github.event.workflow_run.head_sha }}

.github/workflows/deploy-surface.yml

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
name: deploy-surface
22

3-
# Reusable single-surface deploy. Builds the site and rsyncs ONE
4-
# surface's dist/ subfolder to its document root for the chosen env.
3+
# Reusable surface deploy: build ONCE, rsync each requested surface's
4+
# dist/ subfolder to its document root for the chosen env.
55
#
6-
# This is the shared engine behind both the auto-staging deploy (called
7-
# once per surface in a matrix) and the per-surface production tag
8-
# deploys, and it can be run by hand from the Actions tab via
9-
# workflow_dispatch ("deploy my sites independently").
6+
# This is the shared engine behind both the auto-staging deploy (one
7+
# call with surface=all) and the per-surface production tag deploys
8+
# (one call with that surface), and it can be run by hand from the
9+
# Actions tab via workflow_dispatch ("deploy my sites independently",
10+
# or surface=all for a whole release in one click).
11+
#
12+
# Astro has no partial build - every build emits every surface - so the
13+
# expensive part (install, prebuild downloads, build, and on production
14+
# the full baseline gates) runs in ONE job and the per-surface legs are
15+
# artifact-download + rsync only. Beyond the compute saved, this means
16+
# every surface in a deploy ships from literally the same build instead
17+
# of N builds that could drift (e.g. a registry package publishing
18+
# mid-release).
1019
#
1120
# The surface -> document-root mapping below is the single source of
1221
# truth; it mirrors the host layout documented in docs/deploy-targets.md.
@@ -15,10 +24,12 @@ on:
1524
workflow_dispatch:
1625
inputs:
1726
surface:
18-
description: Which surface to deploy
27+
description: Which surface to deploy (all = every surface, one build)
1928
type: choice
2029
required: true
30+
default: all
2131
options:
32+
- all
2233
- architecture
2334
- main
2435
- plays
@@ -36,6 +47,7 @@ on:
3647
workflow_call:
3748
inputs:
3849
surface:
50+
description: A surface name, or "all" for every surface
3951
required: true
4052
type: string
4153
env:
@@ -52,7 +64,24 @@ permissions:
5264
packages: read
5365

5466
jobs:
55-
deploy:
67+
resolve:
68+
runs-on: ubuntu-latest
69+
outputs:
70+
surfaces: ${{ steps.s.outputs.surfaces }}
71+
steps:
72+
- id: s
73+
run: |
74+
surface="${{ inputs.surface }}"
75+
case "$surface" in
76+
all)
77+
echo 'surfaces=["architecture","main","cultures","plays","misfits","writing"]' >> "$GITHUB_OUTPUT" ;;
78+
architecture|main|cultures|plays|misfits|writing)
79+
echo "surfaces=[\"$surface\"]" >> "$GITHUB_OUTPUT" ;;
80+
*)
81+
echo "::error::unknown surface '$surface' (expected a surface name or 'all')"; exit 1 ;;
82+
esac
83+
84+
build:
5685
runs-on: ubuntu-latest
5786
steps:
5887
- uses: actions/checkout@v7
@@ -75,10 +104,39 @@ jobs:
75104
# to staging.kaihacks.ai when DEPLOY_ENV=staging.
76105
DEPLOY_ENV=staging npm run build
77106
fi
107+
- name: upload dist
108+
uses: actions/upload-artifact@v4
109+
with:
110+
name: dist-${{ inputs.env }}
111+
path: dist
112+
# dist carries dot-paths the sites need (.well-known/security.txt,
113+
# the apex .htaccess rewrites). upload-artifact excludes hidden
114+
# files by default since v4.4; without this flag they would vanish
115+
# from the artifact and the rsync --delete below would then REMOVE
116+
# them from the live document roots.
117+
include-hidden-files: true
118+
retention-days: 1
119+
120+
deploy:
121+
needs: [resolve, build]
122+
runs-on: ubuntu-latest
123+
strategy:
124+
# Surfaces deploy independently: one failing leg does not block the
125+
# others, and a single leg can be re-run on its own ("re-run failed
126+
# jobs") without rebuilding - the artifact is the build.
127+
fail-fast: false
128+
matrix:
129+
surface: ${{ fromJSON(needs.resolve.outputs.surfaces) }}
130+
steps:
131+
- name: download dist
132+
uses: actions/download-artifact@v4
133+
with:
134+
name: dist-${{ inputs.env }}
135+
path: dist
78136
- name: resolve rsync target
79137
id: target
80138
run: |
81-
surface="${{ inputs.surface }}"
139+
surface="${{ matrix.surface }}"
82140
env="${{ inputs.env }}"
83141
base="/home/c216mkgp1lzk/public_html"
84142
if [ "$env" = "production" ]; then
@@ -144,7 +202,7 @@ jobs:
144202
run: |
145203
rsync -avz --delete --exclude '/_assets/' \
146204
-e "ssh -i ~/.ssh/kaihacksai" \
147-
"./dist/${{ inputs.surface }}/" \
205+
"./dist/${{ matrix.surface }}/" \
148206
"c216mkgp1lzk@92.205.150.56:${{ steps.target.outputs.target }}"
149207
- name: Deploy shared assets via rsync (NO --delete)
150208
# Ship the hashed /_assets/ bundle the HTML references. No --delete:

0 commit comments

Comments
 (0)