Skip to content

Commit 32d0fd0

Browse files
heiskrCopilot
andauthored
Add Purge Fastly (all) workflow for on-demand full cache purge (#61734)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 121d024 commit 32d0fd0

3 files changed

Lines changed: 104 additions & 0 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Purge Fastly (all)
2+
3+
# **What it does**: Purges the ENTIRE Fastly cache for docs (every URL) on demand.
4+
# **Why we have it**: So docs engineering can clear a bad cache state without asking core engineering to run a purge-all in the Fastly UI.
5+
# **Who does it impact**: All readers. Origin sees a traffic spike while the cache refills, so only run this when a targeted purge will not do.
6+
7+
on:
8+
workflow_dispatch:
9+
inputs:
10+
confirm:
11+
description: "Type 'purge everything' to confirm a full hard purge of the Fastly cache."
12+
required: true
13+
14+
permissions:
15+
contents: read
16+
17+
# Only one purge-all may run at a time. Do not cancel an in-flight purge:
18+
# a half-finished purge leaves the cache in an unknown state.
19+
concurrency:
20+
group: purge-fastly-all
21+
cancel-in-progress: false
22+
23+
env:
24+
FASTLY_TOKEN: ${{ secrets.FASTLY_TOKEN }}
25+
FASTLY_SERVICE_ID: ${{ secrets.FASTLY_SERVICE_ID }}
26+
27+
jobs:
28+
purge-all:
29+
if: github.repository == 'github/docs-internal'
30+
runs-on: ubuntu-latest
31+
steps:
32+
- name: Validate confirmation input
33+
if: ${{ inputs.confirm != 'purge everything' }}
34+
run: |
35+
echo "::error::Confirmation text did not match. Re-run and type exactly: purge everything"
36+
exit 1
37+
38+
- name: Check out repo
39+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
40+
41+
- uses: ./.github/actions/node-npm-setup
42+
43+
- name: Purge entire Fastly cache
44+
run: npm run purge-fastly-all

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"prevent-pushes-to-main": "tsx src/workflows/prevent-pushes-to-main.ts",
8181
"purge-fastly-edge-cache": "tsx src/workflows/purge-fastly-edge-cache.ts",
8282
"purge-fastly-edge-cache-per-language": "tsx src/languages/scripts/purge-fastly-edge-cache-per-language.ts",
83+
"purge-fastly-all": "tsx src/workflows/purge-fastly-all.ts",
8384
"readability-report": "tsx src/workflows/experimental/readability-report.ts",
8485
"ready-for-docs-review": "tsx src/workflows/ready-for-docs-review.ts",
8586
"release-banner": "tsx src/ghes-releases/scripts/release-banner.ts",

src/workflows/purge-fastly-all.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { fetchWithRetry } from '@/frame/lib/fetch-utils'
2+
3+
// Purges the ENTIRE Fastly cache for the docs service via Fastly's purge_all
4+
// endpoint. This is the workflow equivalent of the "Purge all" button in the
5+
// Fastly UI. It is intentionally a separate, manually triggered entry point:
6+
// routine post-deploy purging is handled by
7+
// `purge-fastly-edge-cache-per-language.ts`, and a gentler "refresh everything"
8+
// can be done by running the Purge Fastly workflow with an empty languages
9+
// input (a soft surrogate-key purge of every language).
10+
//
11+
// NOTE: Fastly's purge_all is always a HARD purge. The `fastly-soft-purge`
12+
// header has no effect on this endpoint, so every object is evicted immediately
13+
// and origin sees a traffic spike while the cache refills. Only reach for this
14+
// when a targeted purge will not do.
15+
// https://www.fastly.com/documentation/reference/api/purging/
16+
17+
async function purgeFastlyAll({ apiToken, serviceId }: { apiToken: string; serviceId: string }) {
18+
const safeServiceId = encodeURIComponent(serviceId)
19+
const requestPath = `https://api.fastly.com/service/${safeServiceId}/purge_all`
20+
const response = await fetchWithRetry(
21+
requestPath,
22+
{
23+
method: 'POST',
24+
headers: {
25+
'fastly-key': apiToken,
26+
accept: 'application/json',
27+
'Content-Type': 'application/json',
28+
},
29+
},
30+
{
31+
retries: 0,
32+
timeout: 30_000,
33+
throwHttpErrors: false,
34+
},
35+
)
36+
if (!response.ok) {
37+
// Fastly puts permission/feature-disabled details in the response body,
38+
// which is often the only actionable signal, so surface it best-effort.
39+
let body = ''
40+
try {
41+
body = await response.text()
42+
} catch {
43+
body = ''
44+
}
45+
throw new Error(
46+
`Fastly purge_all failed: HTTP ${response.status} ${response.statusText}${body ? `: ${body}` : ''}`,
47+
)
48+
}
49+
return response
50+
}
51+
52+
const { FASTLY_TOKEN, FASTLY_SERVICE_ID } = process.env
53+
if (!FASTLY_TOKEN || !FASTLY_SERVICE_ID) {
54+
throw new Error('Fastly env vars not detected; refusing to run purge_all')
55+
}
56+
57+
console.log('Attempting Fastly purge_all (hard purge of the entire cache)...')
58+
const result = await purgeFastlyAll({ apiToken: FASTLY_TOKEN, serviceId: FASTLY_SERVICE_ID })
59+
console.log('Fastly purge_all result:', result.status)

0 commit comments

Comments
 (0)