Rolling Back Fingerprinted Assets in CI/CD
You merged the PR, the deploy ran clean, and then the error reports started rolling in. Now you need to undo the release—but your project uses content-addressed asset fingerprinting, and the rollback playbook you wrote for server-side apps does not directly apply here. Do you roll forward with a hot-fix commit, or do you reach for the prior build artifact and redeploy the old HTML?
This page answers that decision precisely, covers the retain-N artifact strategy and blue/green approach, and provides a single GitHub Actions workflow you can trigger with one click. It assumes familiarity with the CI/CD asset pipeline integration pattern where an atomic deploy uploads hashed assets first and rewrites index.html last.
Why fingerprinted-asset rollback is different
In a classic server-side deploy, rolling back means reverting the running binary to an older version. Every resource the old version needs travels with it.
With fingerprinted assets, the CDN origin (S3, R2, or similar) accumulates immutable files forever. A file named main.a3f8c21b.js is never overwritten—if its content changes, it gets a new hash and a new key. This means:
- Every asset from every prior build is already live in object storage. You do not need to re-upload them.
- The only mutable file in the entire system is
index.html(and any asset manifest it references). - Rolling back = re-deploying only the old
index.htmlthat references the old hashes.
This is fundamentally simpler than a full deploy, but it requires that you saved the old artifact. Understanding the rollback concept for content-hashed releases in depth is useful here, as is the companion page on rolling back cache keys after a bad deploy.
Diagnosis: confirming the problem before you act
Before choosing roll-forward or roll-back, confirm where the breakage lives:
| Symptom | Likely cause | Approach |
|---|---|---|
JS runtime error referencing main.<hash>.js |
Bad JS in the new build | Roll-back HTML |
| Layout broken, CSS looks wrong | Bad CSS in the new build | Roll-back HTML |
404 on a hashed asset URL |
Asset was never uploaded or was deleted | Roll-forward only (rollback cannot fix missing files) |
| Build tool config changed (output dir, hash algo) | Config drift, old artifacts incompatible | Roll-forward only |
Error in index.html template rendering |
Bad HTML template | Roll-back HTML |
| Problem only in one region | CDN propagation lag | Wait or invalidate only |
Run a quick header check first:
curl -sI https://your-site.com/assets/main.a3f8c21b.js | grep -E "HTTP|cache-control|x-cache"
If the asset returns 404, you cannot roll back—the problem is a missing object, not a bad manifest. If the asset returns 200 immutable, the files are fine and only the HTML needs to change.
Decision matrix: roll-forward vs. roll-back
The table below maps conditions to strategies:
| Condition | Recommended strategy | Reasoning |
|---|---|---|
| Bug is in JS/CSS logic, all assets uploaded correctly | Roll-back | Fastest recovery; assets already live |
Bug is in index.html template or manifest |
Roll-back | Swap one file |
| Breaking change was in webpack/Vite config (output dir, chunk names changed) | Roll-forward | Artifact incompatible with current infra |
| One or more hashed assets were deleted from S3/R2 | Roll-forward | Prior artifact references objects that no longer exist |
| Rollback target is older than your artifact retention window | Roll-forward | Artifact gone; cannot retrieve it |
| Schema migration ran on deploy and cannot be reversed | Roll-forward | Database state makes rollback unsafe |
| Bug only affects one browser or a feature flag cohort | Roll-forward | Targeted fix is safer |
Retain-N artifact strategy
The GitHub Actions default artifact retention window is 90 days, but you can set it explicitly per job to enforce a deliberate policy. A common pattern is to keep the last 10 production artifacts:
- Tag each release:
git tag v$(date +%Y%m%d%H%M%S)before the deploy job. - Upload the entire
dist/folder (which containsindex.htmland the asset manifest but not the immutable chunks—those are in S3) as a named artifact tied to the workflow run. - Set
retention-days: 30to bound storage cost while preserving a rolling window.
Record the mapping between git tag and GitHub Actions run_id in your release notes or a lightweight DynamoDB/KV table. During an incident you need to retrieve the run_id immediately—not search through GitHub UI pages.
For monorepos, hash lengths of 12–16 hex characters reduce collision probability across hundreds of parallel asset graphs. Single-repo projects can stay at the default 8 hex characters.
Blue/green approach with S3 prefix switching
An alternative to artifact-based rollback is maintaining two live copies of your HTML layer:
- Blue prefix:
s3://your-bucket/blue/index.html - Green prefix:
s3://your-bucket/green/index.html
Each deploy writes to the inactive slot. CloudFront’s origin path is set to /blue or /green via a parameter store value. Switching means updating the origin path and running a CloudFront invalidation for /index.html.
The hashed assets always live at the root (s3://your-bucket/assets/main.a3f8c21b.js), never inside a prefixed path, because both the blue and green index.html files reference the same immutable objects.
Blue/green is most practical when:
- You have high deploy frequency and want an always-ready rollback target.
- Your infra team prefers DNS/origin-level switching to re-running CI jobs.
- You need sub-30-second rollback SLA.
For teams that deploy once a day or less, the artifact approach below is simpler to reason about.
GitHub Actions rollback workflow
The following workflow is triggered manually with a workflow_dispatch event. You supply the run_id of the prior successful deploy. The job downloads the saved dist artifact from that run, re-uploads only index.html with no-cache headers, and invalidates the CDN—without touching any immutable asset. This builds directly on the atomic deploy pattern described in the GitHub Actions hash manifest and atomic CDN deploy reference.
# .github/workflows/rollback.yml
name: Rollback fingerprinted HTML
on:
workflow_dispatch:
inputs:
run_id:
description: "GitHub Actions run_id of the build to restore"
required: true
type: string
s3_bucket:
description: "S3 bucket name (without s3:// prefix)"
required: true
type: string
default: "my-frontend-bucket"
cloudfront_distribution_id:
description: "CloudFront distribution ID"
required: true
type: string
permissions:
actions: read
id-token: write
contents: read
jobs:
rollback:
runs-on: ubuntu-latest
environment: production
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
aws-region: us-east-1
- name: Download prior build artifact
uses: actions/download-artifact@v4
with:
name: dist-production
run-id: ${{ inputs.run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: prior-dist
- name: Verify artifact contains index.html
run: |
if [ ! -f prior-dist/index.html ]; then
echo "ERROR: prior-dist/index.html not found. Artifact may be incomplete."
exit 1
fi
echo "Artifact OK. Restoring index.html from run ${{ inputs.run_id }}"
- name: Re-upload index.html with no-cache headers
run: |
aws s3 cp prior-dist/index.html \
s3://${{ inputs.s3_bucket }}/index.html \
--cache-control "no-cache, must-revalidate" \
--content-type "text/html; charset=utf-8" \
--metadata "rollback-source-run=${{ inputs.run_id }}"
- name: Re-upload asset manifest (if present)
run: |
if [ -f prior-dist/asset-manifest.json ]; then
aws s3 cp prior-dist/asset-manifest.json \
s3://${{ inputs.s3_bucket }}/asset-manifest.json \
--cache-control "no-cache, must-revalidate" \
--content-type "application/json"
echo "Manifest restored."
else
echo "No asset-manifest.json found; skipping."
fi
- name: Invalidate CloudFront HTML paths
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ inputs.cloudfront_distribution_id }} \
--paths "/index.html" "/asset-manifest.json"
- name: Print rollback summary
run: |
echo "Rollback complete."
echo " Source run_id : ${{ inputs.run_id }}"
echo " Bucket : ${{ inputs.s3_bucket }}"
echo " Distribution : ${{ inputs.cloudfront_distribution_id }}"
echo "Immutable assets in S3 were NOT touched."
Two points to note. First, the actions/download-artifact@v4 action accepts a run-id input that lets you reach across workflow runs—you are not limited to the current run’s artifacts. Second, only index.html and the manifest are uploaded; the immutable hashed chunks (main.a3f8c21b.js, vendor.d9f1a04c.css, etc.) are already in S3 from the prior deploy and do not need to be re-sent. This keeps the rollback job under 30 seconds end-to-end.
If your frontend is on Cloudflare R2 + Cloudflare CDN purge rather than CloudFront, replace the AWS steps with the equivalent wrangler r2 object put and Cloudflare Cache API purge calls, but the structural logic is identical.
Why you never re-upload immutable assets
This is worth stating explicitly because it surprises engineers who are new to content-addressed deploys.
The Vite rollback and webpack rollback guides both confirm the same behaviour: a file with a given content hash represents exactly one immutable state. If main.a3f8c21b.js was uploaded during the prior deploy, it is still in S3 right now—nothing deleted it. The CDN’s cache for that file also has no reason to expire: you set Cache-Control: public, max-age=31536000, immutable on every hashed asset during the original upload.
Re-uploading those files would be wasted bandwidth and would re-trigger CDN edge population for no benefit. The only file that changed between the bad deploy and the prior deploy is index.html, which points to different hashes. Swap the pointer, invalidate the pointer’s CDN cache entry, and you are done.
Verification command
After the workflow finishes, confirm the old asset hashes are being served. Replace a3f8c21b with a hash that appeared in your prior index.html:
curl -sI https://your-site.com/index.html | grep -E "x-cache|age|cache-control|etag"
Expected output after a successful rollback and CDN invalidation:
cache-control: no-cache, must-revalidate
x-cache: Miss from cloudfront
age: 0
The age: 0 and Miss from cloudfront confirm CloudFront fetched a fresh copy from S3. If age is non-zero, the invalidation has not propagated to all edges yet—wait 30–60 seconds and retry. You can also open DevTools → Network, reload the page, and confirm the src attributes of <script> and <link> tags point to the hashes from your prior build rather than the bad build.
When to reconsider: prefer roll-forward instead
Roll-back is not always the right answer. Choose roll-forward when:
- The breakage is in your build configuration itself. If the bad deploy changed webpack’s
output.filenamepattern from[name].[contenthash:8].jsto something else, the artifact’sindex.htmlreferences paths that no longer match the objects in S3. A roll-back would serve 404s. - Hashed assets were deleted from S3. Object storage lifecycle rules or a manual deletion may have removed old chunks. The prior
index.htmlwould reference missing objects. - The artifact is older than your retention window. If you kept 30 days of artifacts and you need to revert a 45-day-old state, the artifact is gone. You must roll forward or restore from a full S3 version snapshot.
- A database migration shipped with the deploy. Rolling back the HTML while the database schema is in its new state can cause runtime errors worse than the original bug. Roll forward with a compensating schema change instead.
- The incident is in a region where assets are already propagated. Depending on the failure mode, rolling forward with a one-line hot-fix commit may be faster than waiting for manual rollback approval.
The decision matrix at the top of this page captures these conditions. When in doubt, ask: “Do I know for certain that all objects referenced in the prior index.html still exist in S3?” If the answer is yes, roll-back is safe. If no, roll-forward.
Related
- CI/CD Asset Pipeline Integration — parent overview of CI/CD deploy patterns for fingerprinted assets
- GitHub Actions Hash Manifest and Atomic CDN Deploy — the forward deploy workflow this rollback reverses
- AWS CloudFront Invalidation — invalidation paths, pricing, and wildcard strategy
- Rolling Back a Content-Hashed Release — conceptual foundation for content-hash rollback
- Cloudflare Cache Rules and Purge — equivalent purge approach for Cloudflare-fronted deployments