shared-actions/publish-static-contents/action.yml
Michael Seele 08123e3f56
All checks were successful
Aikido Security PR Check / Aikido Security Scan (pull_request) Successful in 49s
validate-shared-actions / validate-shared-actions (pull_request) Successful in 26s
fix: update cache control argument handling for S3 sync
2026-06-01 12:35:17 +00:00

141 lines
5.6 KiB
YAML

name: Publish to CloudFront
description: Syncs frontend assets to S3 and invalidates the CloudFront distribution
inputs:
dist_dir:
description: Path to the frontend dist directory
required: false
default: frontend/dist
s3_bucket_name:
description: Name of the S3 bucket to sync assets to
required: true
cloudfront_distribution_ids:
description: Space-separated list of CloudFront distribution IDs to invalidate
required: false
default: ''
versioning:
description: >
When set to true, enables versioned builds: assets are synced with
"public, max-age=31536000, immutable" and old versions older than 7 days
are deleted, keeping at least the 2 newest.
required: false
default: 'false'
versioning_prefix:
description: >
S3 prefix under which versioned builds are stored
(e.g. "_static" → _static/1234567890/). When omitted, versions are
stored at the bucket root. Requires versioning to be true.
required: false
default: ''
cache_rules:
description: >
JSON array of per-file cache overrides. Each matched file is uploaded
individually with the given headers and excluded from the bulk sync.
content_type is optional. Applied independently of versioning.
When versioning is enabled and cache_rules is empty, defaults to
short-caching index.html — the standard SPA behaviour.
Example:
cache_rules: |
[
{"file": "index.html", "cache_control": "no-cache, no-store, must-revalidate", "content_type": "text/html"},
{"file": "sw.js", "cache_control": "no-cache, no-store, must-revalidate", "content_type": "application/javascript"},
{"file": "manifest.webmanifest", "cache_control": "no-cache, max-age=300", "content_type": "application/manifest+json"}
]
required: false
default: '[]'
runs:
using: composite
steps:
- name: Install AWS CLI
shell: bash
run: |
if ! command -v aws &> /dev/null; then
curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip
unzip -q /tmp/awscliv2.zip -d /tmp
sudo /tmp/aws/install
rm -rf /tmp/awscliv2.zip /tmp/aws
fi
- name: Publish frontend assets
shell: bash
env:
INPUT_DIST_DIR: ${{ inputs.dist_dir }}
INPUT_S3_BUCKET_NAME: ${{ inputs.s3_bucket_name }}
INPUT_VERSIONING: ${{ inputs.versioning }}
INPUT_VERSIONING_PREFIX: ${{ inputs.versioning_prefix }}
INPUT_CACHE_RULES: ${{ inputs.cache_rules }}
run: |
# When versioning is active and no cache_rules provided, short-cache index.html by default
EFFECTIVE_CACHE_RULES="${INPUT_CACHE_RULES}"
if [[ "${INPUT_VERSIONING}" == "true" && "${EFFECTIVE_CACHE_RULES}" == "[]" && -f "${INPUT_DIST_DIR}/index.html" ]]; then
EFFECTIVE_CACHE_RULES='[{"file": "index.html", "cache_control": "no-cache, max-age=300", "content_type": "text/html"}]'
fi
# Upload each file from cache_rules with its own headers,
# and collect --exclude args so the bulk sync skips them
EXCLUDE_ARGS=()
if [[ "${EFFECTIVE_CACHE_RULES}" != "[]" ]]; then
while IFS= read -r rule; do
file=$(echo "$rule" | jq -r '.file')
cache_control=$(echo "$rule" | jq -r '.cache_control')
content_type=$(echo "$rule" | jq -r '.content_type // empty')
EXCLUDE_ARGS+=("--exclude" "$file")
if [[ -f "${INPUT_DIST_DIR}/${file}" ]]; then
CT_ARG=()
if [[ -n "$content_type" ]]; then
CT_ARG=("--content-type" "$content_type")
fi
aws s3 cp "${INPUT_DIST_DIR}/${file}" "s3://${INPUT_S3_BUCKET_NAME}/${file}" \
--cache-control "$cache_control" \
--metadata-directive REPLACE \
"${CT_ARG[@]}"
fi
done < <(echo "$EFFECTIVE_CACHE_RULES" | jq -c '.[]')
fi
# Bulk sync remaining files; versioned builds get immutable cache headers
CACHE_CONTROL_ARG=()
if [[ "${INPUT_VERSIONING}" == "true" ]]; then
CACHE_CONTROL_ARG=("--cache-control" "public, max-age=31536000, immutable")
fi
aws s3 sync "${INPUT_DIST_DIR}" "s3://${INPUT_S3_BUCKET_NAME}" "${CACHE_CONTROL_ARG[@]}" "${EXCLUDE_ARGS[@]}"
- name: Clean up old versioned static builds
if: ${{ inputs.versioning == 'true' }}
shell: bash
env:
INPUT_S3_BUCKET_NAME: ${{ inputs.s3_bucket_name }}
INPUT_VERSIONING_PREFIX: ${{ inputs.versioning_prefix }}
run: |
S3_PATH="s3://$INPUT_S3_BUCKET_NAME"
if [[ -n "${INPUT_VERSIONING_PREFIX}" ]]; then
S3_PATH="s3://$INPUT_S3_BUCKET_NAME/$INPUT_VERSIONING_PREFIX"
fi
aws s3 ls "${S3_PATH}/" \
| grep -oP '(?<=PRE )[0-9]+' \
| sort --stable --reverse \
| tail -n +3 \
| while read version; do
now=$(($(date +%s%N)/1000000))
diff=$(($now-$version))
# delete if older than 7 days
if [ $diff -gt 604800000 ]; then
echo "Deleting $version"
aws s3 rm --recursive "${S3_PATH}/$version"
fi
done
- name: Invalidate CloudFront
if: ${{ inputs.cloudfront_distribution_ids != '' }}
shell: bash
env:
INPUT_CLOUDFRONT_DISTRIBUTION_IDS: ${{ inputs.cloudfront_distribution_ids }}
run: |
echo "$INPUT_CLOUDFRONT_DISTRIBUTION_IDS" \
| tr ' ' '\n' \
| xargs -I% aws cloudfront create-invalidation --distribution-id % --paths '/*'