Merge pull request 'feat: add per-file cache_rules and fix argument escaping' (#32) from feature/publish-static-contents-cache-rules into main

Reviewed-on: #32
Reviewed-by: Böhringer_Sebastian_-_J._Schmalz_GmbH <Sebastian.Boehringer@schmalz.de>
This commit is contained in:
Michael.Seele@schmalz.de 2026-05-22 05:09:47 +00:00
commit 97d17f46e8
Signed by: schmalz-git.git.onstackit.cloud
GPG key ID: 569DFBE669A0D544
2 changed files with 83 additions and 26 deletions

View file

@ -9,11 +9,14 @@ Syncs frontend assets to S3 and invalidates a CloudFront distribution. Optionall
| `dist_dir` | No | `frontend/dist` | Path to the frontend dist directory |
| `s3_bucket_name` | Yes | — | Name of the S3 bucket to sync assets to |
| `cloudfront_distribution_ids` | No | `''` | Space-separated list of CloudFront distribution IDs to invalidate |
| `versioning` | No | `false` | When `true`, enables versioned builds. Old versions older than 7 days are deleted, keeping at least the 2 newest |
| `versioning_prefix` | No | `''` | S3 prefix under which versioned builds are stored (e.g. `_static``_static/1234567890/`). When omitted, versions are stored at the bucket root (e.g. `1234567890/`) |
| `versioning` | No | `false` | When `true`, enables versioned builds. All assets get `Cache-Control: public, max-age=31536000, immutable`. Old versions older than 7 days are deleted, keeping at least the 2 newest |
| `versioning_prefix` | No | `''` | S3 prefix under which versioned builds are stored (e.g. `_static``_static/1234567890/`). Requires `versioning: true` |
| `cache_rules` | No | `[]` | JSON array of per-file cache overrides. Each matched file is uploaded individually with the given headers and excluded from the bulk sync. When `versioning` is `true` and `cache_rules` is empty and `index.html` exists, defaults to short-caching `index.html` |
## Usage
Plain sync:
```yaml
- uses: https://schmalz-git.git.onstackit.cloud/schmalz/shared-actions/publish-static-contents@publish-static-contents-v1
with:
@ -21,7 +24,7 @@ Syncs frontend assets to S3 and invalidates a CloudFront distribution. Optionall
cloudfront_distribution_ids: ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }}
```
With versioned static assets at the bucket root:
With versioned static assets:
```yaml
- uses: https://schmalz-git.git.onstackit.cloud/schmalz/shared-actions/publish-static-contents@publish-static-contents-v1
@ -42,9 +45,25 @@ With versioned static assets under a prefix:
versioning_prefix: _static
```
With custom per-file cache rules (e.g. for a PWA):
```yaml
- uses: https://schmalz-git.git.onstackit.cloud/schmalz/shared-actions/publish-static-contents@publish-static-contents-v1
with:
s3_bucket_name: my-bucket
cloudfront_distribution_ids: ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }}
versioning: true
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"}
]
```
## Notes
- When `versioning` is `true`, all assets are synced with `Cache-Control: public, max-age=31536000, immutable`.
- `index.html` is always synced separately with `Cache-Control: no-cache, max-age=300` so browsers pick up new deployments quickly.
- When `versioning` is `true` and `cache_rules` is empty, `index.html` is automatically short-cached (`no-cache, max-age=300`) if it exists in the dist directory.
- `cache_rules` works independently of `versioning` and can be used for plain syncs too.
- Old versioned builds are pruned: any version folder older than 7 days is deleted, with at least the 2 newest versions always retained.
- CloudFront invalidation is skipped when `cloudfront_distribution_ids` is empty.

View file

@ -14,13 +14,35 @@ inputs:
required: false
default: ''
versioning:
description: 'When set to true, enables versioned builds. Old versions older than 7 days are deleted, keeping at least the 2 newest.'
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
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 (e.g. 1234567890/).'
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
@ -42,29 +64,45 @@ runs:
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
EXCLUDE_INDEX_ARG=""
if [[ "${INPUT_VERSIONING}" == "true" && -f "${INPUT_DIST_DIR}/index.html" ]]; then
EXCLUDE_INDEX_ARG="--exclude index.html"
fi
aws s3 sync "${INPUT_DIST_DIR}" "s3://${INPUT_S3_BUCKET_NAME}" $CACHE_CONTROL_ARG $EXCLUDE_INDEX_ARG
- name: Publish index.html without immutable cache
if: ${{ inputs.versioning == 'true' }}
shell: bash
env:
INPUT_DIST_DIR: ${{ inputs.dist_dir }}
INPUT_S3_BUCKET_NAME: ${{ inputs.s3_bucket_name }}
run: |
if [[ -f "${INPUT_DIST_DIR}/index.html" ]]; then
aws s3 cp "${INPUT_DIST_DIR}/index.html" "s3://${INPUT_S3_BUCKET_NAME}/index.html" \
--cache-control "no-cache, max-age=300" \
--metadata-directive REPLACE
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' }}
@ -77,7 +115,7 @@ runs:
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 \