Skip to content

Purge Fastly

Purge Fastly #4

Workflow file for this run

name: Purge Fastly
# **What it does**: Purges Fastly after a deploy and on demand. Soft purge by
# default; can hard purge specific languages, or hard purge the ENTIRE cache.
# **Why we have it**: So that, right after a production deploy, we start afresh,
# and so docs engineering can clear a bad cache state without the Fastly UI.
# **Who does it impact**: Writers and engineers. A full purge impacts all readers,
# origin sees a traffic spike while the cache refills, so it's gated below.
on:
deployment_status:
schedule:
# Daily soft purge of all languages to catch reusables and AUTOTITLE links.
# Intentionally off-peak, around 6-7pm PT.
- cron: '20 2 * * *'
workflow_dispatch:
inputs:
languages:
description: "Languages: Comma separated languages, e.g. 'en,es,ja,pt,zh,ru,fr,ko,de'. Blank = all languages."
required: false
default: 'en'
hard:
description: 'Hard purge: Evict immediately instead of the default soft purge. Use when a soft purge fails to clear stale content.'
type: boolean
required: false
default: false
everything:
description: 'Everything: Hard-purge the entire Fastly cache... every key, all readers. Ignores the languages/hard inputs. To confirm, type exactly: "purge everything". Otherwise leave blank.'
required: false
default: ''
permissions:
contents: read
deployments: read
# Serialize full-cache purges so two can't overlap and leave the cache in an
# unknown state. Every other run (per-deploy, per-language) gets a unique group
# so those never block each other.
concurrency:
group: ${{ (inputs.everything == 'purge everything' && 'purge-fastly-all') || format('purge-fastly-{0}', github.run_id) }}
cancel-in-progress: false
env:
FASTLY_TOKEN: ${{ secrets.FASTLY_TOKEN }}
FASTLY_SERVICE_ID: ${{ secrets.FASTLY_SERVICE_ID }}
jobs:
send-purges:
# Run when workflow_dispatch is the event
# or when deployment_status is the event and it's a successful production deploy.
# NOTE: This workflow triggers on all deployment_status events,
# including staging, but only runs for production.
# Non-production deploys will show as "skipped" - this is expected behavior.
if: >-
${{
github.repository == 'github/docs-internal' &&
(github.event_name != 'deployment_status' ||
github.event.deployment_status.state == 'success' && github.event.deployment_status.environment == 'production')
}}
runs-on: ubuntu-latest
steps:
- name: Check out repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Generate GitHub App token
# Lets a scheduled-run failure open a tracking issue in docs-engineering
# (the default GITHUB_TOKEN can't write cross-repo).
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.DOCS_BOT_APP_ID }}
private-key: ${{ secrets.DOCS_BOT_APP_PRIVATE_KEY }}
owner: github
repositories: docs-engineering
- uses: ./.github/actions/node-npm-setup
- name: Validate confirmation input
# A full-cache purge only triggers on the exact string "purge everything".
# Any other non-empty value (e.g. a typo) would otherwise be silently
# ignored and fall through to a normal soft purge that finishes green, so
# an operator could think they evicted the whole cache when they didn't.
# Fail loudly instead.
env:
EVERYTHING_INPUT: ${{ inputs.everything }}
run: |
if [ -n "$EVERYTHING_INPUT" ] && [ "$EVERYTHING_INPUT" != "purge everything" ]; then
echo "::error::To purge the entire cache, the 'everything' input must be exactly 'purge everything'. Got: '$EVERYTHING_INPUT'. Leave it blank for a normal purge."
exit 1
fi
- name: Purge Fastly
# Auto post-deploy runs wait for the build, purge English only (temporary),
# and stay soft. The daily scheduled run waits for nothing, purges all
# languages (blank = all), and stays soft. A manual run uses the inputs:
# blank languages = all, the `hard` toggle, or a confirmed full-cache purge.
#
# Raw inputs are passed through the environment and quoted, never spliced
# into the command string, so a value like `en' --everything` can't break
# out of its argument and inject another flag.
env:
EVENT_NAME: ${{ github.event_name }}
LANGUAGES_INPUT: ${{ inputs.languages }}
HARD_INPUT: ${{ inputs.hard }}
EVERYTHING_INPUT: ${{ inputs.everything }}
run: |
args=()
if [ "$EVENT_NAME" = "deployment_status" ]; then
args+=(--wait-for-build)
args+=(--languages en)
elif [ -n "$LANGUAGES_INPUT" ]; then
args+=(--languages "$LANGUAGES_INPUT")
fi
if [ "$HARD_INPUT" = "true" ]; then
args+=(--hard)
fi
if [ "$EVERYTHING_INPUT" = "purge everything" ]; then
args+=(--everything)
fi
npm run purge-fastly -- "${args[@]}"
- name: Hard-purge changed English content URLs
# Prod deploys only. The soft purge above just marks `language:en` stale,
# so stale-while-revalidate can keep serving the pre-deploy copy of a
# just-changed page for a while. This evicts the specific English URLs
# whose content/ files changed in this deploy, so the next request fetches
# fresh. By the time the deploy succeeds the old pods are already gone, so
# the refill is deterministically the new content. data/ changes stay
# covered by the soft purge above (too many URLs to enumerate cheaply).
if: ${{ github.event_name == 'deployment_status' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ github.event.deployment.sha }}
run: npm run purge-fastly-changed-content
- uses: ./.github/actions/slack-alert
if: ${{ failure() && github.event_name != 'workflow_dispatch' }}
with:
slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }}
- uses: ./.github/actions/create-workflow-failure-issue
if: ${{ failure() && github.event_name != 'workflow_dispatch' }}
with:
token: ${{ steps.app-token.outputs.token }}