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 '/*'