CI/CD Asset Pipeline Integration for Fingerprinted Static Assets
Getting fingerprinted assets into production without stale HTML references or cache poisoning requires a precise deployment sequence that build tools alone cannot enforce — your CI/CD pipeline must coordinate hashing, uploading, and atomic origin swaps as a single reproducible unit.
When to Use This Approach vs. Alternatives
This pipeline pattern — hash at build time, upload assets before HTML, deploy HTML last — is the right choice when:
- You control the build environment and want deterministic, reproducible outputs (see deterministic build outputs)
- Your CDN serves assets from S3, R2, or a similar object store where you control
Cache-Controlheaders per object - You need zero-downtime deploys where no user ever receives HTML referencing assets that do not yet exist on the CDN
- You want a hash manifest artifact as an audit trail or for rollback automation
Use a simpler approach (e.g., a one-command deploy on a managed static host) when:
- You do not need fine-grained cache header control per file
- Your host handles atomic deploys internally and does not expose upload order
- You are deploying a prototype or internal tool where stale-asset windows are acceptable
The critical differentiator is upload order. A naive aws s3 sync dist/ s3://bucket/ uploads files in arbitrary order. If index.html lands before main-a1b2c3d4.js, users who receive the new HTML immediately hit a 403 or 404 for the new asset. The pattern in this page eliminates that race by uploading assets first and HTML last.
Prerequisites
| Requirement | Minimum Version / Detail |
|---|---|
| Node.js | 20.x LTS (matches GitHub Actions node-version: '20') |
| npm | 10.x (bundled with Node 20) |
| Vite or Webpack | Vite 5.x / Webpack 5.x with content hashing enabled |
| AWS CLI | v2.x for s3 sync and cloudfront create-invalidation |
| Wrangler | 3.x for Cloudflare Pages / R2 deploys |
| GitHub Actions | Current runner: ubuntu-24.04 |
actions/cache |
v4 |
actions/upload-artifact |
v4 |
actions/download-artifact |
v4 |
Your bundler must emit a hash manifest. In Vite asset pipeline configuration, setting build.manifest: true produces dist/.vite/manifest.json. In Webpack output hashing setup, use WebpackManifestPlugin from webpack-manifest-plugin. Both produce a JSON map of logical chunk names to hashed filenames; the shape and the trade-offs between the two formats are covered in the asset manifest generation reference.
Configuration Reference
| Key / Flag | Type | Default | Effect |
|---|---|---|---|
build.manifest (Vite) |
boolean | false |
Emits .vite/manifest.json mapping logical names to hashed filenames |
output.filename [contenthash:8] (Webpack) |
string template | [name].js |
Appends 8-hex-char content hash to JS output files |
--cache-control (aws s3 sync) |
string | (none) | Sets Cache-Control header on uploaded objects |
--exclude (aws s3 sync) |
glob | (none) | Skips matching keys; used to exclude *.html on first pass |
--metadata-directive REPLACE (aws s3 cp) |
enum | COPY |
Forces S3 to write new metadata even when content is unchanged |
--size-only (aws s3 sync) |
boolean | false |
Compares object size instead of size+mtime; avoids needless re-uploads of identical hashed files |
concurrency.group (GitHub Actions) |
string | (none) | Serialises concurrent workflow runs for the same ref |
concurrency.cancel-in-progress |
boolean | false |
Cancels a queued run when a newer run starts for the same group |
retention-days (upload-artifact) |
integer | 90 | How long the manifest artifact is kept in GitHub |
permissions.id-token (GitHub Actions) |
enum | none |
Must be write to mint an OIDC token for keyless cloud auth |
--commit-dirty (wrangler pages deploy) |
boolean | false |
Allows deploy from an unclean git working tree inside CI |
Two of these deserve emphasis. --metadata-directive REPLACE matters because S3 will happily leave an old Cache-Control header in place when the object body is byte-identical; without it, a header policy change never reaches already-uploaded objects. And --size-only is worth adding once your asset directory is large: content-hashed files never change body for a given key, so comparing timestamps only produces wasted PUT calls after every fresh CI checkout.
Step-by-Step Implementation
The five stages below map one-to-one onto workflow steps. Stages one through three are entirely reversible — nothing user-visible has changed yet. Stage four is the commit point of the release.
--cache-control flag is the most common cause of a stale deploy.Step 1 — Configure Your Bundler for Reproducible Hashes
Content hashes must derive from file content only, not from timestamps or build-order IDs. The content hashing vs. semantic versioning guide explains why content-only hashes are the correct primitive here. 8-character hex hashes are sufficient for most projects; use 12–16 characters when you have a monorepo with thousands of chunks and need stronger collision resistance.
Vite (vite.config.ts):
import { defineConfig } from 'vite';
export default defineConfig({
build: {
manifest: true,
rollupOptions: {
output: {
entryFileNames: 'assets/[name]-[hash:8].js',
chunkFileNames: 'assets/[name]-[hash:8].js',
assetFileNames: 'assets/[name]-[hash:8][extname]',
},
},
},
});
Webpack (webpack.config.js):
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
module.exports = {
mode: 'production',
output: {
filename: 'assets/[name]-[contenthash:8].js',
chunkFilename: 'assets/[name]-[contenthash:8].js',
assetModuleFilename: 'assets/[name]-[contenthash:8][ext]',
clean: true,
},
plugins: [
new WebpackManifestPlugin({
fileName: 'asset-manifest.json',
}),
],
};
Step 2 — GitHub Actions Workflow: S3 + CloudFront
Save this file at .github/workflows/deploy-s3.yml. The workflow splits into two jobs: build and deploy. Separating them lets the deploy job run on a minimal runner without Node.js installed, and makes the artifact handoff explicit.
name: Deploy to S3 + CloudFront
on:
push:
branches:
- main
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Cache npm downloads
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
env:
NODE_ENV: production
- name: Upload dist artifact
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
- name: Upload manifest artifact
uses: actions/upload-artifact@v4
with:
name: asset-manifest
path: dist/.vite/manifest.json
retention-days: 30
deploy:
needs: build
runs-on: ubuntu-24.04
environment: production
steps:
- name: Download dist artifact
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
# Upload all fingerprinted assets FIRST with immutable headers.
# Exclude *.html so no user can receive new HTML before new assets exist.
- name: Upload hashed assets to S3 (immutable)
run: |
aws s3 sync dist/ s3://${{ secrets.S3_BUCKET }}/ \
--exclude "*.html" \
--cache-control "public, max-age=31536000, immutable" \
--metadata-directive REPLACE \
--size-only
# Upload HTML LAST with must-revalidate so browsers always check freshness.
# At this point all assets the new HTML references already exist on the CDN.
- name: Upload HTML to S3 (no-cache)
run: |
aws s3 sync dist/ s3://${{ secrets.S3_BUCKET }}/ \
--include "*.html" \
--exclude "*" \
--cache-control "no-cache, must-revalidate" \
--metadata-directive REPLACE
# Invalidate only HTML. Fingerprinted assets are immutable and must NOT be
# invalidated — doing so evicts cold-cached assets and spikes origin load.
- name: Invalidate CloudFront HTML only
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
--paths "/index.html"
- name: Download manifest artifact (for audit log)
uses: actions/download-artifact@v4
with:
name: asset-manifest
path: manifests/
- name: Print deployed manifest
run: cat manifests/manifest.json
The concurrency.cancel-in-progress: false setting is intentional. When two pushes land in quick succession, cancelling the in-flight deploy could leave the bucket in a half-uploaded state. Instead, the second run queues and executes cleanly after the first finishes.
Step 3 — GitHub Actions Workflow: Cloudflare Pages + R2
For Cloudflare-hosted projects, Wrangler’s pages deploy command handles the atomic swap internally. Static assets uploaded to a Pages deployment are served from Cloudflare’s edge before the deployment is activated, which means the upload-order concern is handled by the platform. You still need to manage the manifest artifact for rollback and audit purposes.
name: Deploy to Cloudflare Pages
on:
push:
branches:
- main
concurrency:
group: deploy-production-cf
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Cache npm downloads
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
env:
NODE_ENV: production
- name: Upload dist artifact
uses: actions/upload-artifact@v4
with:
name: dist-cf
path: dist/
retention-days: 7
- name: Upload manifest artifact
uses: actions/upload-artifact@v4
with:
name: asset-manifest-cf
path: dist/.vite/manifest.json
retention-days: 30
deploy:
needs: build
runs-on: ubuntu-24.04
environment: production
steps:
- name: Download dist artifact
uses: actions/download-artifact@v4
with:
name: dist-cf
path: dist/
- name: Install Wrangler
run: npm install -g wrangler@3
# wrangler pages deploy activates the new deployment atomically after
# all assets have been uploaded to Cloudflare's edge network.
# No manual HTML invalidation is needed — Pages handles cache routing.
- name: Deploy to Cloudflare Pages
run: |
wrangler pages deploy dist/ \
--project-name ${{ secrets.CF_PAGES_PROJECT }} \
--commit-dirty=true \
--branch main
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
- name: Download manifest artifact (for audit log)
uses: actions/download-artifact@v4
with:
name: asset-manifest-cf
path: manifests/
- name: Print deployed manifest
run: cat manifests/manifest.json
On Cloudflare Pages, there is no equivalent of aws cloudfront create-invalidation because the platform performs an atomic deployment switch. The old deployment continues serving traffic until the new one is fully staged, then the routing rules flip in one operation. You can read more about targeted purge strategies in the Cloudflare cache rules and purge reference.
Step 4 — Manifest Artifact Handoff Pattern
The artifact handoff between jobs is what makes rollback feasible. The build job uploads manifest.json as a named artifact. The deploy job downloads it, uses it (or logs it), and retains it for post-deploy verification and the rolling back fingerprinted assets in CI/CD workflow.
In a downstream rollback job, you would download a previous run’s manifest artifact using the GitHub API:
# List recent workflow runs and find the run ID to roll back to
gh run list --workflow=deploy-s3.yml --limit 10
# Download the manifest artifact from a specific run
gh run download <RUN_ID> --name asset-manifest --dir rollback-manifests/
# Inspect the manifest to find previous hashed filenames
cat rollback-manifests/manifest.json
Storing manifests with a 30-day retention gives you enough history to roll back any deploy within a sprint cycle without bloating GitHub artifact storage.
Step 5 — Gate the HTML Publish on a Manifest Preflight
Ordering alone guarantees that the upload started before HTML publishes. It does not guarantee that every object actually landed — a partial multipart upload, an expired credential mid-sync, or an --exclude pattern that silently skipped a directory all produce a bucket that is missing exactly the file the new HTML needs. A preflight step closes that gap by reading the manifest and issuing a HEAD request for every hashed key before the commit point.
Add this step to the deploy job, immediately before the HTML upload:
#!/usr/bin/env bash
# scripts/preflight-manifest.sh — verify every hashed asset exists at the origin
set -euo pipefail
MANIFEST="${1:?usage: preflight-manifest.sh <manifest.json> <bucket>}"
BUCKET="${2:?usage: preflight-manifest.sh <manifest.json> <bucket>}"
# Vite manifest: every entry has a "file" key; some also have "css" arrays.
KEYS=$(python3 - "$MANIFEST" <<'PY'
import json, sys
data = json.load(open(sys.argv[1]))
out = set()
for entry in data.values():
if isinstance(entry, dict):
if entry.get("file"):
out.add(entry["file"])
for css in entry.get("css", []):
out.add(css)
elif isinstance(entry, str):
out.add(entry)
for key in sorted(out):
print(key)
PY
)
missing=0
while read -r key; do
[ -z "$key" ] && continue
if aws s3api head-object --bucket "$BUCKET" --key "$key" >/dev/null 2>&1; then
printf 'ok %s\n' "$key"
else
printf 'MISSING %s\n' "$key"
missing=$((missing + 1))
fi
done <<< "$KEYS"
if [ "$missing" -gt 0 ]; then
echo "Preflight failed: $missing object(s) missing. HTML will not be published."
exit 1
fi
echo "Preflight passed: all manifest entries present in s3://$BUCKET/"
Wire it in with a single step, and the job fails loudly before the commit point rather than quietly after it:
- name: Preflight — every manifest entry must exist in S3
run: |
chmod +x scripts/preflight-manifest.sh
scripts/preflight-manifest.sh dist/.vite/manifest.json "${{ secrets.S3_BUCKET }}"
The cost is one HEAD request per asset — a few hundred milliseconds for a typical 40-chunk build, and far less than the cost of a partial release. Teams that also publish subresource integrity attributes usually extend the same script to compare the stored object’s ETag against the locally computed digest.
Deploy Credentials Without Long-Lived Secrets
Storing AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as repository secrets works, but those credentials never expire and are readable by every workflow in the repository. GitHub Actions can instead mint a short-lived OpenID Connect token that AWS STS exchanges for a session lasting minutes rather than forever. The exchange requires exactly two things: permissions.id-token: write on the job, and a trust policy on the IAM role that pins the repository and ref.
The workflow side is a two-line change. Replace the static key pair with a role ARN and grant the token permission:
deploy:
needs: build
runs-on: ubuntu-24.04
environment: production
permissions:
id-token: write
contents: read
steps:
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/frontend-deploy
role-session-name: gha-frontend-deploy
aws-region: us-east-1
The IAM side pins the token to a single repository and branch so a fork or a pull-request run cannot assume the role. The sub claim is the control point — matching on repo:acme/storefront:* would let any branch deploy production:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:acme/storefront:ref:refs/heads/main"
}
}
}
]
}
Scope the attached policy narrowly too: s3:PutObject and s3:ListBucket on the asset bucket, plus cloudfront:CreateInvalidation on the one distribution. A deploy role does not need s3:DeleteObject unless you actually run --delete, and withholding it turns a compromised workflow into a nuisance rather than an outage. On Cloudflare, the equivalent is a scoped API token limited to the specific Pages project and zone rather than a global account key.
Verification
Run these commands after a deploy to confirm headers and manifest integrity.
# 1. Verify a hashed asset has the immutable Cache-Control header
curl -sI "https://assets.example.com/assets/main-a1b2c3d4.js" \
| grep -i cache-control
# Expected: cache-control: public, max-age=31536000, immutable
# 2. Verify index.html has no-cache
curl -sI "https://assets.example.com/index.html" \
| grep -i cache-control
# Expected: cache-control: no-cache, must-revalidate
# 3. Confirm the manifest artifact was saved in the latest workflow run
gh run list --workflow=deploy-s3.yml --limit 1 --json databaseId --jq '.[0].databaseId'
# Then download and inspect:
gh run download <RUN_ID> --name asset-manifest --dir /tmp/manifest-check/
cat /tmp/manifest-check/manifest.json | python3 -m json.tool | head -30
# 4. Confirm no extra CloudFront invalidations were created (only /index.html)
aws cloudfront list-invalidations \
--distribution-id $CLOUDFRONT_DISTRIBUTION_ID \
--query "InvalidationList.Items[0].{Status:Status,Paths:InvalidationBatch.Paths}" \
--output json
# 5. For Cloudflare Pages: confirm deployment is live
wrangler pages deployment list --project-name my-project | head -5
The four responses you should be able to recite from memory are below. If any row disagrees with what curl prints, the cause is almost always a --cache-control flag applied to the wrong s3 sync pass.
no-cache means the asset sync ran without its --cache-control flag — check the step order before blaming the CDN.The /favicon.ico row is the odd one out on purpose. Root-level files that browsers request by a fixed name cannot carry a hash, so they get a moderate TTL instead of an immutable one — a week is long enough to be cheap and short enough that a redesign reaches users without a purge. The same reasoning applies to robots.txt, manifest.webmanifest, and any service-worker script.
Edge Cases and Known Issues
Stale assets from partial uploads. If the Upload hashed assets step succeeds but the HTML upload fails, you end up with new assets on S3 but the old index.html still pointing to old hashes. This is safe — users continue receiving the old HTML with the old assets, which are still in the bucket. Re-run the workflow to complete the deploy.
--delete flag removes old assets too quickly. The aws s3 sync --delete flag removes any S3 key not present in the local dist/ directory. If users are still on the old deploy when --delete runs, their browsers will request old hashed filenames that no longer exist. This is why the workflow above omits --delete entirely and defers pruning to a scheduled job.
A practical cleanup rule: run the prune job on a schedule, and only delete objects whose LastModified is older than the longest HTML TTL plus your longest plausible open-tab session. Seven days is a common setting; the trade-off between that number and storage cost is discussed further in the cache-control immutable and TTL tuning reference.
Concurrency and overlapping deploys. The concurrency.cancel-in-progress: false setting queues overlapping runs rather than cancelling them. With cancel-in-progress: true, a cancelled deploy could leave S3 in a mixed state (some new assets, old HTML). Never use cancel-in-progress: true for the deploy job.
CloudFront invalidation path case sensitivity. CloudFront paths are case-sensitive. If your server renders /Index.html (capital I), the invalidation for /index.html will not clear it. Use the exact path as it appears in the S3 key. See AWS CloudFront invalidation for batching multiple paths.
Wrangler --commit-dirty in a clean checkout. wrangler pages deploy reads git log to attach a commit hash to the deployment. In CI, the checkout is clean, but if you’re building inside a subdirectory or monorepo, Wrangler may report a dirty tree because build artifacts are untracked. The --commit-dirty=true flag suppresses this check. It does not affect the deployed content.
Manifest path differs between Vite and Webpack. Vite 5.x emits the manifest to dist/.vite/manifest.json. Vite 4.x emitted it to dist/manifest.json. Webpack with WebpackManifestPlugin emits to dist/asset-manifest.json by default. Update the path: in upload-artifact to match your actual output.
SRI hashes and CI reproducibility. If you generate integrity attributes in CI, the SRI values must match the deployed file content byte-for-byte. Any post-build transformation (minification, comment stripping) that runs outside the build step will break validation. Ensure all transformations happen inside npm run build.
Compression applied after upload. If your CDN or origin gzips or brotli-compresses objects on the fly, the bytes on the wire differ from the bytes you hashed. That is harmless for cache-busting but fatal for SRI, and it also makes ETag comparisons in the preflight script unreliable. Either pre-compress in the build and upload both variants with an explicit Content-Encoding, or compare against x-amz-meta checksums you set yourself at upload time.
Runner clock skew and artifact expiry. download-artifact fails with a confusing 404 when the referenced run’s artifacts have aged out. If your rollback path depends on a 30-day artifact but your incident is 45 days after the fact, the rollback simply is not available. Treat retention as a recovery-window SLA and set it deliberately rather than accepting the 90-day default by inertia.
Performance Impact
| Operation | Typical Duration | Notes |
|---|---|---|
npm ci with warm cache |
5–15 s | actions/cache on ~/.npm typically saves 30–60 s on medium-sized projects |
| Vite production build | 10–60 s | Depends on chunk count; Vite’s Rollup bundler is single-threaded |
aws s3 sync (assets only) |
10–120 s | Proportional to changed file count; unchanged hashes are skipped |
aws s3 sync (HTML only) |
1–3 s | Typically only 1–3 HTML files |
Manifest preflight (head-object per key) |
1–4 s | 40 chunks at roughly 40 ms per request, run sequentially |
| CloudFront invalidation | 5–30 s | Single path /index.html completes faster than wildcard invalidations |
wrangler pages deploy |
15–90 s | Includes asset upload and deployment activation |
| Manifest artifact upload | 1–3 s | Manifest JSON is typically under 50 KB |
Caching ~/.npm with actions/cache using hashFiles('package-lock.json') as the cache key is the single highest-impact optimisation for build time. When the lockfile does not change, npm ci reduces from 60–90 s to under 10 s on most projects.
Adding --delete to aws s3 sync costs an extra LIST operation before syncing — 1–5 seconds on a normal bucket, considerably more once you cross a few hundred thousand objects. Since the safe-pruning rule above already pushes deletion into a separate scheduled job, the LIST cost disappears from the critical path of every deploy.
Pre-Deploy Checklist
Walk this list before the first production run of a new pipeline, and again whenever you change bundler output options.
FAQ
Should I purge all fingerprinted assets after each deploy?
No. Fingerprinted assets are immutable by definition — their content hash guarantees that the filename changes whenever the content changes. Invalidating them evicts perfectly valid objects from CDN edge caches, forcing a cold-cache origin fetch for every user on every deploy. Only invalidate HTML entry points that do not carry a content hash in their filename. For the mechanics of targeted invalidation, see AWS CloudFront invalidation.
Why upload assets before HTML rather than deploying everything at once?
If HTML and assets upload simultaneously (or in an undefined order), a user can receive the new index.html while the new main-a1b2c3d4.js has not yet been written to S3. Their browser requests an asset that returns 403 or 404, breaking the page. Uploading all hashed assets first creates a safe state where new filenames exist before any HTML references them. The HTML upload is the commit point of the atomic swap.
Can I use actions/cache for the build output instead of actions/upload-artifact?
Cache and artifact serve different purposes. actions/cache is keyed on inputs (lockfile hash, source hash) and is designed for reuse across runs to skip work. actions/upload-artifact stores the actual output of a specific run for handoff between jobs in the same run or for post-deploy download. You need the artifact for the deploy job to receive the built files. You optionally use cache to speed up npm ci or to skip rebuilds when source is unchanged.
How do I handle multiple HTML entry points?
Pass multiple paths to CloudFront in a single invalidation call to stay within the 1000-path-per-invalidation limit and to avoid per-invalidation charges:
aws cloudfront create-invalidation \
--distribution-id $CLOUDFRONT_DISTRIBUTION_ID \
--paths "/index.html" "/about/index.html" "/contact/index.html"
For sites with hundreds of HTML files, generate the path list from the build output rather than hard-coding it, and remember that CloudFront wildcards match a single path segment unless you use a trailing /*, which evicts every cached object including unhashed media. The GitHub Actions hash manifest and atomic CDN deploy guide covers scripting dynamic path lists from the manifest.
How do I run this pipeline for preview environments without duplicating the workflow?
Parameterise the bucket, distribution, and origin prefix as job outputs derived from the ref, then reuse the same job definitions through workflow_call. Preview deploys write to a per-branch prefix with a short TTL and skip the invalidation step entirely, because nothing is cached long enough to need it. The one thing you should not parameterise is ordering: preview environments that publish HTML first teach the team a habit that will eventually be exercised in production.
Does this pattern change if I deploy two generations of the site at once?
Only in where the HTML lives. Because hashed assets are already immutable, two generations can share one asset prefix indefinitely and the cutover reduces to which HTML document a request resolves to. The blue-green asset deploys with fingerprinted files page covers that model, including the traffic-splitting case where both generations serve real users simultaneously.
Related
- Build Tool & Framework Asset Pipeline Integration — parent overview of build-time hashing strategies across all major bundlers
- Manifest-driven atomic deploys — the three-job workflow, staging pattern, and single-path invalidation in detail
- Rollback without re-uploading assets — restoring a previous manifest and re-pointing HTML at old asset hashes
- Two live generations and instant cutover — additive versus two-prefix deploys, canary splits, and safe garbage collection
- Targeted CDN invalidation — batching invalidation requests, cost control, and wildcard vs. exact-path tradeoffs
- Reproducible hash output — ensuring the same source always produces the same hashes across CI workers and local machines