Skip to content

Commit 02f735a

Browse files
authored
ci: use shared cache-header deploy action for all storybook publishes (#5219)
* ci: use shared cache-header deploy action for all storybook publishes The cache-header fix in #5206 only changed publish_storybook.yaml, but production Storybook is deployed by the publish-storybook job in publish_core_react.yaml (dispatched with environment=production on every release). That job — and publish_lab.yaml / publish_data_grid.yaml — used tibor19/static-website-deploy@v4, which sets no Cache-Control headers, so Azure Front Door fell back to its 48h default and the stale-cache Manager crash from #5205 kept happening in prod after every release. Extract the deploy logic from #5206 into a composite action (.github/actions/deploy-storybook) and use it in all four workflows: - immutable cache headers on hashed assets/*, no-cache on the rest - zero-downtime overwrite-then-delete-stale instead of delete-all-first - skip the assets/ upload when the build has no assets directory The publish jobs check out .github explicitly because the setup job's sparse checkout only includes packages/apps/scripts. The az login/logout steps are dropped: --connection-string is complete authentication on its own. Closes #5212 * ci: document deploy action assumptions and checkout ordering Follow-ups from review: - action.yml: note the az CLI runner dependency, that static-website hosting / container access policy are pre-existing account config the action does not re-apply (unlike tibor19/static-website-deploy), and that the stale cleanup requires one storage account per environment. - all four workflows: state explicitly that the sparse .github checkout must stay the first step, since checkout cleans the workspace and would wipe the restored storybook build. - publish_storybook.yaml: align the checkout comment with the other three workflows. * ci: authenticate az via env var instead of --connection-string argument The Azure CLI reads AZURE_STORAGE_CONNECTION_STRING from the environment, so the flag was redundant — and passing the secret as an argument exposes it in process args on the runner (GitHub masks logs, not argv).
1 parent 49f6c16 commit 02f735a

5 files changed

Lines changed: 188 additions & 132 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
name: 'Deploy Storybook to Azure Blob Storage'
2+
description: >-
3+
Zero-downtime upload of a static Storybook build to the $web container with
4+
correct Cache-Control headers: content-hashed assets/* are cached forever,
5+
everything else must be revalidated on every load. Without these headers
6+
Azure Front Door applies its 48h default TTL and warm browser caches mix
7+
bundles from two deploys, crashing the Manager UI (#5205, #5212).
8+
9+
Requires Azure CLI on the runner (pre-installed on GitHub-hosted runners).
10+
Static-website hosting and the container access policy are pre-existing
11+
account configuration and are NOT (re)applied here — unlike the previous
12+
tibor19/static-website-deploy action, this action will not self-heal an
13+
account whose static-website config has been reset.
14+
15+
The stale cleanup assumes this deploy is the sole writer to the target
16+
account's $web container, so each environment must point its
17+
connection-string secret at a separate storage account.
18+
inputs:
19+
folder:
20+
description: 'Path to the built Storybook (static website root)'
21+
required: true
22+
connection-string:
23+
description: 'Connection string for the target Azure Storage account'
24+
required: true
25+
runs:
26+
using: 'composite'
27+
steps:
28+
- name: Deploy to Azure Blob Storage 🚀
29+
shell: bash
30+
env:
31+
AZURE_STORAGE_CONNECTION_STRING: ${{ inputs.connection-string }}
32+
FOLDER: ${{ inputs.folder }}
33+
run: |
34+
set -euo pipefail
35+
echo "Deploying to Azure Blob Storage..."
36+
37+
# All az storage commands authenticate through the
38+
# AZURE_STORAGE_CONNECTION_STRING env var set on the step. Never pass
39+
# it as a --connection-string argument — argv is visible to every
40+
# process on the runner (GitHub masks logs, not process args).
41+
42+
if [ ! -d "$FOLDER" ]; then
43+
echo "❌ Error: folder '$FOLDER' not found"
44+
exit 1
45+
fi
46+
47+
FILE_COUNT=$(find "$FOLDER" -type f | wc -l)
48+
echo "Uploading $FILE_COUNT files..."
49+
50+
# Capture the full set of blob names this deploy produces. Blobs not
51+
# in this set are orphans from a previous deploy and are cleaned up
52+
# after the upload (zero-downtime deploy: upload with --overwrite
53+
# first, delete stale blobs afterwards). Deleting by name set is
54+
# deterministic — timestamps would race the Azure server clock.
55+
# Must run before the mv of assets/ below, or assets/* drops out of
56+
# the set and the cleanup deletes all live assets.
57+
(cd "$FOLDER" && find . -type f | sed 's|^\./||') \
58+
| LC_ALL=C sort > "$RUNNER_TEMP/deployed-blobs.txt"
59+
60+
# 1) Content-hashed assets: cache forever. Uploaded first so the new
61+
# index.html never references chunks that don't exist yet.
62+
if [ -d "$FOLDER/assets" ]; then
63+
az storage blob upload-batch \
64+
--destination '$web' \
65+
--destination-path assets \
66+
--source "$FOLDER/assets" \
67+
--content-cache-control 'public, max-age=31536000, immutable' \
68+
--overwrite
69+
70+
# Move assets/ aside so it is not re-uploaded with the wrong header.
71+
mv "$FOLDER/assets" "$RUNNER_TEMP/assets-uploaded"
72+
fi
73+
74+
# 2) Un-hashed files (index.html, iframe.html, sb-manager/**,
75+
# sb-addons/** etc.): browsers must revalidate on every load, or a
76+
# stale bundle mix breaks the Manager UI after each deploy (#5205).
77+
# Revalidation is cheap 304s via ETag/Last-Modified.
78+
az storage blob upload-batch \
79+
--destination '$web' \
80+
--source "$FOLDER" \
81+
--content-cache-control 'no-cache' \
82+
--overwrite
83+
84+
UPLOADED=$(az storage blob list \
85+
--container-name '$web' \
86+
--num-results '*' \
87+
--query 'length(@)' \
88+
--output tsv)
89+
90+
echo "✅ Deployment completed successfully. Total blobs in container (before stale cleanup): $UPLOADED"
91+
92+
- name: Delete stale blobs from previous deploy 🗑️
93+
shell: bash
94+
env:
95+
AZURE_STORAGE_CONNECTION_STRING: ${{ inputs.connection-string }}
96+
run: |
97+
set -euo pipefail
98+
echo "Deleting blobs not part of this deploy..."
99+
100+
# An empty deploy set would classify every blob in the container as
101+
# stale and wipe the live site — refuse to continue.
102+
if [ ! -s "$RUNNER_TEMP/deployed-blobs.txt" ]; then
103+
echo "❌ Deploy set is empty — refusing to run stale cleanup"
104+
exit 1
105+
fi
106+
107+
az storage blob list \
108+
--container-name '$web' \
109+
--num-results '*' \
110+
--query '[].name' \
111+
--output tsv \
112+
| LC_ALL=C sort > "$RUNNER_TEMP/existing-blobs.txt"
113+
114+
# Blobs present in the container but not in this deploy's file set.
115+
# Assumes this deploy is the sole writer to the $web container —
116+
# anything uploaded out-of-band (e.g. a manual 404.html) is treated
117+
# as stale and deleted on the next deploy.
118+
LC_ALL=C comm -13 "$RUNNER_TEMP/deployed-blobs.txt" "$RUNNER_TEMP/existing-blobs.txt" > "$RUNNER_TEMP/stale-blobs.txt"
119+
120+
COUNT=$(wc -l < "$RUNNER_TEMP/stale-blobs.txt" | tr -d ' ')
121+
echo "Deleting $COUNT stale blobs..."
122+
123+
# Every deploy renames all content-hashed assets, so the whole
124+
# previous assets/* set is stale each run — parallelise the deletes.
125+
# Cleanup is best-effort: the new content is already live at this
126+
# point, and blobs that survive a transient delete failure are
127+
# re-detected as stale on the next deploy. Don't fail the job.
128+
set +e
129+
xargs -r -P 8 -I{} az storage blob delete \
130+
--container-name '$web' \
131+
--name '{}' \
132+
--output none < "$RUNNER_TEMP/stale-blobs.txt"
133+
rc=$?
134+
set -e
135+
136+
if [ "$rc" -eq 0 ]; then
137+
echo "✅ Stale blob cleanup completed. Deleted $COUNT stale blobs."
138+
else
139+
echo "⚠️ Some stale deletes failed (rc=$rc); orphans will be retried on the next deploy"
140+
fi

.github/workflows/publish_core_react.yaml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,15 @@ jobs:
186186
environment:
187187
name: ${{ github.event.inputs.environment || 'development' }}
188188
steps:
189+
# The setup job's sparse checkout only includes packages/apps/scripts
190+
# (and the workspace cache glob skips dot-directories), so .github and
191+
# the local deploy action must be checked out here. Must stay the first
192+
# step: checkout cleans the workspace and would wipe the restored
193+
# storybook build otherwise.
194+
- name: Checkout deploy action
195+
uses: actions/checkout@v7
196+
with:
197+
sparse-checkout: .github
189198
- name: Use cache with storybook files
190199
id: use-cache-storybook
191200
uses: actions/cache@v6
@@ -196,11 +205,9 @@ jobs:
196205
key: ${{ github.sha }}-dist-${{ github.event.inputs.environment }}-core-react
197206
- name: Deploy the website
198207
id: deploy-website
199-
uses: tibor19/static-website-deploy@v4
208+
uses: ./.github/actions/deploy-storybook
200209
with:
201-
enabled-static-website: 'true'
202-
folder: 'packages/eds-core-react/storybook-build'
203-
public-access-policy: 'container'
210+
folder: packages/eds-core-react/storybook-build
204211
connection-string: ${{ secrets.AZ_STORYBOOK_CONNECTION_STRING }}
205212
- name: log-errors-to-slack
206213
uses: act10ns/slack@v2

.github/workflows/publish_data_grid.yaml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ jobs:
115115
name: Publish Storybook
116116
runs-on: ubuntu-latest
117117
steps:
118+
# The setup job's sparse checkout only includes packages/apps/scripts
119+
# (and the workspace cache glob skips dot-directories), so .github and
120+
# the local deploy action must be checked out here. Must stay the first
121+
# step: checkout cleans the workspace and would wipe the restored
122+
# storybook build otherwise.
123+
- name: Checkout deploy action
124+
uses: actions/checkout@v7
125+
with:
126+
sparse-checkout: .github
118127
- name: Use cache with storybook files
119128
id: use-cache-storybook
120129
uses: actions/cache@v6
@@ -125,11 +134,9 @@ jobs:
125134
key: ${{ github.sha }}-dist-${{ github.event.inputs.environment }}-data-grid
126135
- name: Deploy the website
127136
id: deploy-website
128-
uses: tibor19/static-website-deploy@v4
137+
uses: ./.github/actions/deploy-storybook
129138
with:
130-
enabled-static-website: 'true'
131-
folder: 'packages/eds-data-grid-react/storybook-build'
132-
public-access-policy: 'container'
139+
folder: packages/eds-data-grid-react/storybook-build
133140
connection-string: ${{ secrets.AZ_STORAGE_STORYBOOK_DATAGRID_CONNECTION_STRING }}
134141
- name: log-errors-to-slack
135142
uses: act10ns/slack@v2

.github/workflows/publish_lab.yaml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,15 @@ jobs:
151151
name: Publish Storybook
152152
runs-on: ubuntu-latest
153153
steps:
154+
# The setup job's sparse checkout only includes packages/apps/scripts
155+
# (and the workspace cache glob skips dot-directories), so .github and
156+
# the local deploy action must be checked out here. Must stay the first
157+
# step: checkout cleans the workspace and would wipe the restored
158+
# storybook build otherwise.
159+
- name: Checkout deploy action
160+
uses: actions/checkout@v7
161+
with:
162+
sparse-checkout: .github
154163
- name: Use cache with storybook files
155164
id: use-cache-storybook
156165
uses: actions/cache@v6
@@ -161,11 +170,9 @@ jobs:
161170
key: ${{ github.sha }}-dist-${{ github.event.inputs.environment }}-lab
162171
- name: Deploy the website
163172
id: deploy-website
164-
uses: tibor19/static-website-deploy@v4
173+
uses: ./.github/actions/deploy-storybook
165174
with:
166-
enabled-static-website: 'true'
167-
folder: 'packages/eds-lab-react/storybook-build'
168-
public-access-policy: 'container'
175+
folder: packages/eds-lab-react/storybook-build
169176
connection-string: ${{ secrets.AZ_STORAGE_STORYBOOK_LAB_CONNECTION_STRING }}
170177
- name: log-errors-to-slack
171178
uses: act10ns/slack@v2

.github/workflows/publish_storybook.yaml

Lines changed: 15 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@ jobs:
7373
environment:
7474
name: ${{ github.event.inputs.environment || 'development' }}
7575
steps:
76+
# The setup job's sparse checkout only includes packages/apps/scripts
77+
# (and the workspace cache glob skips dot-directories), so .github and
78+
# the local deploy action must be checked out here. Must stay the first
79+
# step: checkout cleans the workspace and would wipe the downloaded
80+
# storybook build otherwise.
81+
- name: Checkout deploy action
82+
uses: actions/checkout@v7
83+
with:
84+
sparse-checkout: .github
85+
7686
- name: Download storybook build
7787
uses: actions/download-artifact@v8
7888
with:
@@ -94,127 +104,12 @@ jobs:
94104
echo "=== All Foundation/Spacing entries ==="
95105
grep -o '"foundation-spacing--[^"]*"' packages/eds-core-react/storybook-build/index.json || echo "No Foundation/Spacing entries found"
96106
97-
- name: Az CLI login 🔑
98-
uses: azure/login@v3
99-
with:
100-
client-id: d58b2e85-2d34-4cdb-ad70-5f2b767dd8e2
101-
tenant-id: 3aa4a235-b6e2-48d5-9195-7fcf05b459b0
102-
allow-no-subscriptions: true
103-
104-
- name: Deploy to Azure Blob Storage 🚀
107+
- name: Deploy the website
105108
id: deploy-website
106-
env:
107-
AZURE_STORAGE_CONNECTION_STRING: ${{ secrets.AZ_STORYBOOK_CONNECTION_STRING }}
108-
run: |
109-
set -euo pipefail
110-
echo "Deploying to Azure Blob Storage..."
111-
112-
# Verify source directory exists and has content
113-
if [ ! -d "packages/eds-core-react/storybook-build" ]; then
114-
echo "❌ Error: storybook-build directory not found"
115-
exit 1
116-
fi
117-
118-
FILE_COUNT=$(find packages/eds-core-react/storybook-build -type f | wc -l)
119-
echo "Uploading $FILE_COUNT files..."
120-
121-
# Capture the full set of blob names this deploy produces. Blobs not
122-
# in this set are orphans from a previous deploy and are cleaned up
123-
# after the upload (zero-downtime deploy: upload with --overwrite
124-
# first, delete stale blobs afterwards). Deleting by name set is
125-
# deterministic — timestamps would race the Azure server clock.
126-
# Must run before the mv of assets/ below, or assets/* drops out of
127-
# the set and the cleanup deletes all live assets.
128-
(cd packages/eds-core-react/storybook-build && find . -type f | sed 's|^\./||') \
129-
| LC_ALL=C sort > /tmp/deployed-blobs.txt
130-
131-
# 1) Content-hashed assets: cache forever. Uploaded first so the new
132-
# index.html never references chunks that don't exist yet.
133-
az storage blob upload-batch \
134-
--destination '$web' \
135-
--destination-path assets \
136-
--source packages/eds-core-react/storybook-build/assets \
137-
--content-cache-control 'public, max-age=31536000, immutable' \
138-
--overwrite \
139-
--connection-string "$AZURE_STORAGE_CONNECTION_STRING"
140-
141-
# 2) Un-hashed files (index.html, iframe.html, sb-manager/**,
142-
# sb-addons/** etc.): browsers must revalidate on every load, or a
143-
# stale bundle mix breaks the Manager UI after each deploy (#5205).
144-
# Revalidation is cheap 304s via ETag/Last-Modified. Move assets/
145-
# aside so it is not re-uploaded with the wrong header.
146-
mv packages/eds-core-react/storybook-build/assets /tmp/assets-uploaded
147-
az storage blob upload-batch \
148-
--destination '$web' \
149-
--source packages/eds-core-react/storybook-build \
150-
--content-cache-control 'no-cache' \
151-
--overwrite \
152-
--connection-string "$AZURE_STORAGE_CONNECTION_STRING"
153-
154-
# Verify upload
155-
UPLOADED=$(az storage blob list \
156-
--container-name '$web' \
157-
--num-results '*' \
158-
--connection-string "$AZURE_STORAGE_CONNECTION_STRING" \
159-
--query 'length(@)' \
160-
--output tsv)
161-
162-
echo "✅ Deployment completed successfully. Total blobs in container (before stale cleanup): $UPLOADED"
163-
164-
- name: Delete stale blobs from previous deploy 🗑️
165-
env:
166-
AZURE_STORAGE_CONNECTION_STRING: ${{ secrets.AZ_STORYBOOK_CONNECTION_STRING }}
167-
run: |
168-
set -euo pipefail
169-
echo "Deleting blobs not part of this deploy..."
170-
171-
# An empty deploy set would classify every blob in the container as
172-
# stale and wipe the live site — refuse to continue.
173-
if [ ! -s /tmp/deployed-blobs.txt ]; then
174-
echo "❌ Deploy set is empty — refusing to run stale cleanup"
175-
exit 1
176-
fi
177-
178-
az storage blob list \
179-
--container-name '$web' \
180-
--num-results '*' \
181-
--connection-string "$AZURE_STORAGE_CONNECTION_STRING" \
182-
--query '[].name' \
183-
--output tsv \
184-
| LC_ALL=C sort > /tmp/existing-blobs.txt
185-
186-
# Blobs present in the container but not in this deploy's file set.
187-
# Assumes this workflow is the sole writer to the $web container —
188-
# anything uploaded out-of-band (e.g. a manual 404.html) is treated
189-
# as stale and deleted on the next deploy.
190-
LC_ALL=C comm -13 /tmp/deployed-blobs.txt /tmp/existing-blobs.txt > /tmp/stale-blobs.txt
191-
192-
COUNT=$(wc -l < /tmp/stale-blobs.txt | tr -d ' ')
193-
echo "Deleting $COUNT stale blobs..."
194-
195-
# Every deploy renames all content-hashed assets, so the whole
196-
# previous assets/* set is stale each run — parallelise the deletes.
197-
# Cleanup is best-effort: the new content is already live at this
198-
# point, and blobs that survive a transient delete failure are
199-
# re-detected as stale on the next deploy. Don't fail the job.
200-
set +e
201-
xargs -r -P 8 -I{} az storage blob delete \
202-
--container-name '$web' \
203-
--name '{}' \
204-
--connection-string "$AZURE_STORAGE_CONNECTION_STRING" \
205-
--output none < /tmp/stale-blobs.txt
206-
rc=$?
207-
set -e
208-
209-
if [ "$rc" -eq 0 ]; then
210-
echo "✅ Stale blob cleanup completed. Deleted $COUNT stale blobs."
211-
else
212-
echo "⚠️ Some stale deletes failed (rc=$rc); orphans will be retried on the next deploy"
213-
fi
214-
215-
- name: logout 🔓
216-
run: az logout
217-
if: always()
109+
uses: ./.github/actions/deploy-storybook
110+
with:
111+
folder: packages/eds-core-react/storybook-build
112+
connection-string: ${{ secrets.AZ_STORYBOOK_CONNECTION_STRING }}
218113

219114
- name: log-errors-to-slack
220115
uses: act10ns/slack@v2

0 commit comments

Comments
 (0)