Removing Dead Files During an S3 Publish
Purpose
Update a static website in S3 without temporarily breaking pages, while safely removing files that are no longer included in the latest WES publish.
This process replaces the current external ZIP-unpacking step with a controlled CodePipeline and CodeBuild deployment.
Current Publish Flow
The recommended AWS flow is:
WES
↓
Versioned incoming S3 bucket
↓
CodePipeline
↓
CodeBuild
↓
Customer serving bucket
WES is responsible for producing and uploading the ZIP. AWS handles validation, deployment, and cleanup.
Recommended Cleanup Method: Manifest-Based Deletion
Each publish should produce a manifest containing every live object key.
Example:
index.html
404/index.html
css/site.123abc.css
js/site.456def.js
images/logo.svg
about/index.html
During deployment:
- Generate a manifest from the new package.
- Upload the new site.
- Store the new manifest.
- Compare it with the previous manifest.
- Identify keys that existed previously but do not exist now.
- Wait through the grace period.
- Delete only those orphaned keys.
This is safer than blindly running aws s3 sync --delete because the cleanup process has an explicit list of expected live files.
CodePipeline Structure
Stage 1: Source
Configure an S3 source action using the bucket and key where WES uploads the project ZIP.
The source bucket must have versioning enabled.
Example source key:
incoming/boldo/project.zip
Use CodePipeline when that object is created or updated.
Stage 2: Deploy
Use CodeBuild to:
- Validate the source artifact.
- Generate the new manifest.
- Upload non-HTML files.
- Upload HTML files.
- Save the new manifest.
- Queue or perform delayed orphan cleanup.
CodePipeline passes source output as an artifact to downstream actions. CodeBuild makes the extracted artifact contents available in its source directory. Confirm whether the project files appear directly in $CODEBUILD_SRC_DIR or inside a top-level project directory before finalizing the build commands.
CodeBuild IAM Permissions
The CodeBuild service role needs:
Source bucket
s3:GetObject
s3:GetObjectVersion
s3:ListBucket
Serving bucket
s3:GetObject
s3:PutObject
s3:ListBucket
s3:DeleteObject
DeleteObject is only required for the cleanup step.
Limit the resources to the specific incoming and serving bucket prefixes.
Example Deployment Buildspec
This example assumes CodePipeline has extracted the website package directly into $CODEBUILD_SRC_DIR.
env:
variables:
SERVE_BUCKET: "customer-site-bucket"
DEPLOYMENT_PREFIX: ".wes"
phases:
pre_build:
commands:
- set -euo pipefail
- SITE_DIR="$CODEBUILD_SRC_DIR"
# Validate required files before changing production.
- test -f "$SITE_DIR/index.html" || { echo "Missing index.html"; exit 1; }
- test -f "$SITE_DIR/404/index.html" || { echo "Missing 404/index.html"; exit 1; }
# Generate a normalized manifest of the new site.
- |
cd "$SITE_DIR"
find . -type f \
! -path "./.wes/*" \
-print \
| sed 's#^\./##' \
| LC_ALL=C sort \
> /tmp/new-manifest.txt
# Download the previous manifest when one exists.
- |
aws s3 cp \
"s3://$SERVE_BUCKET/$DEPLOYMENT_PREFIX/current-manifest.txt" \
/tmp/previous-manifest.txt \
--only-show-errors \
|| touch /tmp/previous-manifest.txt
build:
commands:
# Step 1: Upload assets first. Do not delete anything.
- |
aws s3 sync "$SITE_DIR" "s3://$SERVE_BUCKET" \
--exclude "*" \
--include "*.css" \
--include "*.js" \
--include "*.mjs" \
--include "*.json" \
--include "*.xml" \
--include "*.txt" \
--include "*.svg" \
--include "*.png" \
--include "*.jpg" \
--include "*.jpeg" \
--include "*.gif" \
--include "*.webp" \
--include "*.avif" \
--include "*.ico" \
--include "*.woff" \
--include "*.woff2" \
--include "*.ttf" \
--include "*.otf" \
--cache-control "public,max-age=31536000,immutable" \
--only-show-errors
# Step 2: Upload any remaining non-HTML files.
- |
aws s3 sync "$SITE_DIR" "s3://$SERVE_BUCKET" \
--exclude "*.html" \
--exclude "$DEPLOYMENT_PREFIX/*" \
--only-show-errors
# Step 3: Flip HTML files after their dependencies exist.
- |
aws s3 sync "$SITE_DIR" "s3://$SERVE_BUCKET" \
--exclude "*" \
--include "*.html" \
--content-type "text/html; charset=utf-8" \
--cache-control "public,max-age=60,must-revalidate" \
--only-show-errors
post_build:
commands:
# Calculate files that existed previously but are absent now.
- |
comm -23 \
/tmp/previous-manifest.txt \
/tmp/new-manifest.txt \
> /tmp/orphaned-files.txt
# Save deployment records before deleting anything.
- DEPLOYMENT_ID="$(date -u +%Y%m%dT%H%M%SZ)"
- |
aws s3 cp \
/tmp/new-manifest.txt \
"s3://$SERVE_BUCKET/$DEPLOYMENT_PREFIX/manifests/$DEPLOYMENT_ID.txt" \
--content-type "text/plain" \
--cache-control "no-store" \
--only-show-errors
- |
aws s3 cp \
/tmp/new-manifest.txt \
"s3://$SERVE_BUCKET/$DEPLOYMENT_PREFIX/current-manifest.txt" \
--content-type "text/plain" \
--cache-control "no-store" \
--only-show-errors
- |
aws s3 cp \
/tmp/orphaned-files.txt \
"s3://$SERVE_BUCKET/$DEPLOYMENT_PREFIX/orphans/$DEPLOYMENT_ID.txt" \
--content-type "text/plain" \
--cache-control "no-store" \
--only-show-errors
- echo "Publish complete. Orphan cleanup should run after the grace period."
Delayed Cleanup Job
Run cleanup separately from the main deployment.
Good options include:
- A second CodeBuild project invoked after a delay
- A scheduled Lambda function
- An EventBridge Scheduler job
- A scheduled cleanup pipeline
The cleanup job should read the orphan file generated by the deployment and delete those keys.
Example shell logic:
set -euo pipefail
SERVE_BUCKET="customer-site-bucket"
ORPHAN_FILE="/tmp/orphaned-files.txt"
while IFS= read -r key; do
[ -z "$key" ] && continue
case "$key" in
.wes/*)
echo "Skipping protected key: $key"
continue
;;
esac
aws s3 rm \
"s3://$SERVE_BUCKET/$key" \
--only-show-errors
done < "$ORPHAN_FILE"
Only process orphan lists that are older than the configured grace period.
Simpler Alternative: Delayed sync --delete
A separate cleanup job can unpack the same deployment and run:
aws s3 sync site "s3://$SERVE_BUCKET" \
--delete \
--exclude ".wes/*" \
--only-show-errors
This is simpler but less controlled.
Use it only when:
- The bucket or prefix contains only generated website files.
- The cleanup runs after a grace period.
- Operational files are explicitly excluded.
- The deployment package is retained and known to be complete.
Do not run this command in the primary publish pass.
Rollback
Keep:
- Versioning enabled on the incoming source bucket
- Previous project ZIPs
- Historical manifests
- Serving-bucket versioning when practical
To roll back:
- Select the previous source ZIP version.
- Run the deployment again.
- Upload its assets.
- Flip its HTML files back into place.
- Generate a new cleanup comparison.
Do not immediately restore deleted assets solely by reverting HTML. The previous package should be redeployed so all of its required assets are present before its HTML goes live.
Cache-Control Guidance
Fingerprinted files
Use:
Cache-Control: public,max-age=31536000,immutable
This is appropriate only when changing the file contents also changes the filename.
HTML
Use:
Cache-Control: public,max-age=60,must-revalidate
A short HTML cache lifetime helps browsers discover the new site promptly and limits the required cleanup grace period.
Non-fingerprinted assets
Do not automatically assign a one-year immutable cache policy to files such as:
robots.txt
sitemap.xml
favicon.ico
manifest.json
These files can retain the same key while their contents change. Give them a shorter cache policy or handle them separately.
Reliability Checklist
- Incoming S3 bucket has versioning enabled.
- CodePipeline is triggered by the intended ZIP key.
- Build fails when index.html is absent.
- No deletion occurs during the primary publish.
- A new manifest is generated for every publish.
- Previous and current manifests are compared.
- Operational prefixes are protected.
- Cleanup runs only after a grace period.
- Cleanup has access to the exact orphan list.
- Previous ZIP versions are retained for rollback.
- Fingerprinted and non-fingerprinted files receive appropriate cache headers.
Recommended Implementation
Use the manifest-based process rather than relying exclusively on aws s3 sync --delete.
It provides:
- An auditable list of deleted files
- Protection for non-site objects
- Safer rollback
- A configurable cleanup delay
- A clear separation between publishing and destructive cleanup
- Reliable removal of old Webflow pages and fingerprinted assets