aws s3 sync ./dist s3://my-bucket --delete
sync uploads new and changed files, --delete removes the ones that no longer exist locally. Add --dryrun to see what would happen.
Hashed assets can be cached forever, HTML shouldn't be. Two passes do it:
aws s3 sync ./dist s3://my-bucket --delete --exclude '*.html' \
--cache-control 'public, max-age=31536000, immutable'
aws s3 sync ./dist s3://my-bucket --exclude '*' --include '*.html' \
--cache-control 'no-cache'
If CloudFront sits in front, invalidate it after: aws cloudfront create-invalidation --distribution-id E123 --paths '/*'.