React Deploy to Static Hosting
react-deploy-static@1.0.0 Template Build a React web app and sync it to any static host over rsync or to an S3-compatible bucket.
Start from it
flightstack template use react-deploy-static What it does
Install, build and sync a React bundle to any static host - rsync over SSH, or aws s3 sync for anything S3-compatible (S3, R2, Spaces, MinIO). Sets long-lived immutable caching on hashed assets and no-cache on HTML.
Inputs
-
destinationstring required - Where to sync to. An
s3://bucket/prefixURL usesaws s3 sync; anything else is treated as an rsync target, e.g.deploy@host:/var/www/app. -
workingDirstring default. - Directory holding package.json.
-
outputDirstring defaultdist - Directory the build writes to, and what gets uploaded.
-
deleteRemovedboolean defaultfalse - Delete files at the destination that are no longer in the build. Off by default because it is the option that can empty a live site.
-
buildboolean defaulttrue - Install dependencies and build before deploying.
-
buildScriptstring defaultbuild - The npm script that produces the production bundle.
-
packageManagerauto | npm | pnpm | yarn | bun defaultauto - Which package manager to use.
-
endpointUrlstring - Custom S3 endpoint, for R2 / Spaces / MinIO. Ignored for rsync destinations.
-
envobject - Build-time environment variables, compiled INTO the bundle and therefore public. Never put a secret here.
-
mustContainarray - Strings that must appear in the built JavaScript.
-
mustNotContainarray - Strings that must NOT appear, checked before the upload.
-
includestring default*.js - Filename pattern the assertions are checked against.
Definition
name: React Deploy to Static Hosting
description: >-
Build a React web app and sync the bundle to any static host over rsync or to
an S3-compatible bucket.
triggers:
- type: manual
jobs:
deploy:
name: Sync to bucket
type: plugin/react-deploy-static
with:
destination: s3://postwire-console
outputDir: apps/console/dist
deleteRemoved: true
plugin:
name: react-deploy-static
displayName: React Deploy to Static Hosting
description: >-
Install, build and sync a React bundle to a static host - rsync over SSH,
or `aws s3 sync` for anything S3-compatible (S3, R2, Spaces, MinIO). Sets
long-lived immutable caching on hashed assets and no-cache on index.html.
category: deploy
requiredTools:
- name: nodejs
version: ">=20.0.0"
supportedPlatforms:
- linux
- macos
parameters:
type: object
properties:
destination:
type: string
description: >-
Where to sync to. An `s3://bucket/prefix` URL uses `aws s3 sync`;
anything else is treated as an rsync target, e.g.
`deploy@host:/var/www/app`.
workingDir:
type: string
default: "."
description: Directory holding package.json.
outputDir:
type: string
default: dist
description: Directory the build writes to, and what gets uploaded.
deleteRemoved:
type: boolean
default: false
description: >-
Delete files at the destination that are no longer in the build. Off
by default because it is the option that can empty a live site.
build:
type: boolean
default: true
description: Install dependencies and build before deploying.
buildScript:
type: string
default: build
description: The npm script that produces the production bundle.
packageManager:
type: string
enum: [auto, npm, pnpm, yarn, bun]
default: auto
description: Which package manager to use.
endpointUrl:
type: string
default: ""
description: >-
Custom S3 endpoint, for R2 / Spaces / MinIO. Ignored for rsync
destinations.
env:
type: object
description: >-
Build-time environment variables, compiled INTO the bundle and
therefore public. Never put a secret here.
mustContain:
type: array
description: Strings that must appear in the built JavaScript.
mustNotContain:
type: array
description: Strings that must NOT appear, checked before the upload.
include:
type: string
default: "*.js"
description: Filename pattern the assertions are checked against.
required: [destination]
script: |
set -euo pipefail
cd "{{workingDir}}"
pm="{{packageManager}}"
if [ "$pm" = "auto" ]; then
if [ -f pnpm-lock.yaml ]; then pm=pnpm
elif [ -f yarn.lock ]; then pm=yarn
elif [ -f bun.lockb ] || [ -f bun.lock ]; then pm=bun
else pm=npm
fi
fi
case "$pm" in
pnpm|yarn) corepack enable >/dev/null 2>&1 || true ;;
esac
{{#build}}
echo "--- installing dependencies"
case "$pm" in
npm) if [ -f package-lock.json ]; then npm ci; else npm install; fi ;;
pnpm) pnpm install --frozen-lockfile ;;
yarn) yarn install --immutable || yarn install --frozen-lockfile ;;
bun) bun install --frozen-lockfile ;;
esac
echo "--- building"
{{#each env}}
export {{@key}}="{{this}}"
{{/each}}
"$pm" run "{{buildScript}}"
{{/build}}
out="{{outputDir}}"
if [ ! -d "$out" ]; then
echo "nothing to deploy: '$out' does not exist" >&2
exit 1
fi
failed=0
{{#each mustContain}}
if grep -rqF --include="{{include}}" -- '{{this}}' "$out"; then
echo "ok: bundle contains '{{this}}'"
else
echo "FAIL: '{{this}}' is missing from the bundle - refusing to deploy" >&2
failed=1
fi
{{/each}}
{{#each mustNotContain}}
if grep -rqF --include="{{include}}" -- '{{this}}' "$out"; then
echo "FAIL: '{{this}}' appears in the bundle - refusing to deploy" >&2
failed=1
else
echo "ok: bundle is free of '{{this}}'"
fi
{{/each}}
[ "$failed" = "0" ] || exit 1
dest="{{destination}}"
case "$dest" in
s3://*)
endpoint=""
if [ -n "{{endpointUrl}}" ]; then
endpoint="--endpoint-url {{endpointUrl}}"
fi
delete=""
{{#deleteRemoved}}
delete="--delete"
{{/deleteRemoved}}
# Two passes, because one cache policy cannot be right for both.
# Everything under assets/ carries a content hash in its filename, so a
# changed file is a different URL and this copy can never go stale.
# index.html names those hashed files, so a browser holding an old copy
# loads the PREVIOUS deploy's bundle - it must always be revalidated.
echo "--- syncing hashed assets"
aws s3 sync "$out/" "$dest" $endpoint $delete \
--exclude "*.html" \
--cache-control "public, max-age=31536000, immutable"
echo "--- syncing html"
aws s3 sync "$out/" "$dest" $endpoint \
--exclude "*" --include "*.html" \
--cache-control "no-cache, must-revalidate"
;;
*)
delete=""
{{#deleteRemoved}}
delete="--delete"
{{/deleteRemoved}}
echo "--- rsync to $dest"
# Trailing slash on the source: copy the CONTENTS of the directory, not
# the directory itself. Without it every deploy nests another dist/.
rsync -az --checksum $delete "$out/" "$dest"
;;
esac
echo "--- deployed {{COMMIT_SHA}} to $dest"