Skip to content

hoist-react-snapshot #527

hoist-react-snapshot

hoist-react-snapshot #527

Workflow file for this run

# Build Snapshot — Builds snapshot Docker images on every push to develop.
#
# Builds both nginx (React frontend) and tomcat (Grails backend) images and pushes them
# to ECR with snapshot tags. Also triggered when hoist-core or hoist-react publish new
# snapshots (via repository_dispatch). For numbered releases, see Build Release.
name: Build Snapshot
on:
push:
branches: [ "develop" ]
repository_dispatch:
types: [ hoist-core-snapshot, hoist-react-snapshot ]
workflow_dispatch:
# Debounce builds by branch. Newer runs from a branch will cancel the current run and start over.
concurrency:
group: build-snapshot
cancel-in-progress: true
env:
TOMCAT_IMAGE: xh/toolbox-tomcat
NGINX_IMAGE: xh/toolbox-nginx
jobs:
prepare:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
app-build: ${{ steps.tag.outputs.app-build }}
image-tag: ${{ steps.tag.outputs.image-tag }}
steps:
# Build identifiers come from the shared dev-utils composite action — one
# unit-tested implementation, consumed by every app repo. It snaps a single
# timestamp and emits both `app-build` (readable, baked into appBuild on client
# + server, shown in-app) and `image-tag` (sanitized, snap_-prefixed, immutable
# ECR tag for push/promote/cleanup); container-tag constraints live in the action,
# never on the displayed appBuild.
- name: Generate build identifiers
id: tag
uses: xh/hoist-dev-utils/.github/actions/build-snapshot-tag@master
build-tomcat:
needs: prepare
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
build-scan-publish: false
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
build-scan-terms-of-use-agree: 'yes'
- name: Build WAR
run: ./gradlew -PxhAppBuild="${{ needs.prepare.outputs.app-build }}" war
- name: Copy WAR into Docker context
run: cp build/libs/*.war docker/tomcat/app.war
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/xh-github-actions-deploy
aws-region: ${{ secrets.AWS_REGION }}
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v2
# Push to the immutable per-build tag, NOT :snapshot. The :snapshot deploy
# pointer is advanced later by the `promote` job, and only once both images
# for this run exist — so the deployed pair can never straddle two runs.
- name: Build and push tomcat image
env:
ECR_ROOT: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com
IMAGE_TAG: ${{ needs.prepare.outputs.image-tag }}
run: |
docker build -t "$ECR_ROOT/${{ env.TOMCAT_IMAGE }}:$IMAGE_TAG" docker/tomcat
docker push "$ECR_ROOT/${{ env.TOMCAT_IMAGE }}:$IMAGE_TAG"
build-nginx:
needs: prepare
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: 'client-app/.nvmrc'
# Cache the yarn download directory directly (rather than via setup-node's
# `cache: yarn`, which keys strictly on the lockfile hash with no fallback).
# The `restore-keys` prefix lets a lockfile-changed run restore the most
# recent prior cache, so `yarn install` only downloads packages that
# actually moved — important given the FontAwesome Pro bandwidth cap.
# Note: snapshot runs `yarn upgrade`, which can fetch packages beyond the
# committed lockfile. On primary-key hits those deltas aren't persisted,
# so a newly-published FontAwesome version gets re-downloaded on each
# snapshot until the lockfile catches up. The "Detect FontAwesome
# lockfile drift" step below surfaces exactly this case as a warning so
# it gets resolved by committing an updated lockfile.
- name: Resolve Yarn cache directory
id: yarn-cache-dir
run: echo "dir=$(cd client-app && yarn cache dir)" >> "$GITHUB_OUTPUT"
- name: Restore Yarn cache
uses: actions/cache@v5
with:
path: ${{ steps.yarn-cache-dir.outputs.dir }}
key: yarn-${{ runner.os }}-${{ hashFiles('client-app/yarn.lock') }}
restore-keys: |
yarn-${{ runner.os }}-
- name: Configure Font Awesome registry auth
env:
FONTAWESOME_PACKAGE_TOKEN: ${{ secrets.FONTAWESOME_PACKAGE_TOKEN }}
run: echo "//npm.fontawesome.com/:_authToken=$FONTAWESOME_PACKAGE_TOKEN" >> client-app/.npmrc
- name: Install and upgrade dependencies, lint, and build client app
run: cd client-app && yarn install && yarn upgrade && yarn lint && yarn build --env appBuild="${{ needs.prepare.outputs.app-build }}"
# `yarn upgrade` rewrites yarn.lock when any semver-compatible transitive
# has moved. That's expected and usually too noisy to act on — but a
# FontAwesome bump matters, because every run until we commit the updated
# lockfile will re-download the new FA tarballs against our bandwidth cap.
# This step filters lockfile drift specifically to @fortawesome changes
# and surfaces a workflow warning + summary pointing at the standard
# remediation: run `yarn upgrade` locally, verify, and commit.
- name: Detect FontAwesome lockfile drift
working-directory: client-app
run: |
if git diff yarn.lock | grep -E '^[+-][^+-]' | grep -iq fortawesome; then
echo "::warning title=FontAwesome lockfile drift::yarn upgrade picked up a newer FontAwesome version than the committed yarn.lock. Run yarn upgrade locally, verify, and commit the updated lockfile to stop re-downloading these packages on every snapshot build."
{
echo "### FontAwesome lockfile drift detected"
echo ""
echo "\`yarn upgrade\` moved one or more \`@fortawesome/*\` packages beyond the committed \`client-app/yarn.lock\`. Until the new lockfile is committed, each snapshot build will re-download these tarballs against the FontAwesome Pro bandwidth cap."
echo ""
echo "**Action:** run \`yarn upgrade\` locally, verify the app still builds and behaves correctly, and commit the updated \`yarn.lock\`."
echo ""
echo "<details><summary>FontAwesome diff</summary>"
echo ""
echo '```diff'
git --no-pager diff yarn.lock | grep -iE '^[+-].*fortawesome' || true
echo '```'
echo ""
echo "</details>"
} >> "$GITHUB_STEP_SUMMARY"
fi
- name: Copy client build into Docker context
run: cp -r client-app/build/ docker/nginx/build/
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/xh-github-actions-deploy
aws-region: ${{ secrets.AWS_REGION }}
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v2
# Push to the immutable per-build tag, NOT :snapshot (see build-tomcat).
- name: Build and push nginx image
env:
ECR_ROOT: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com
IMAGE_TAG: ${{ needs.prepare.outputs.image-tag }}
run: |
docker build -t "$ECR_ROOT/${{ env.NGINX_IMAGE }}:$IMAGE_TAG" docker/nginx
docker push "$ECR_ROOT/${{ env.NGINX_IMAGE }}:$IMAGE_TAG"
promote:
# Advance the mutable :snapshot deploy pointer ONLY after BOTH images for
# this run have built and pushed. This is the atomicity guarantee that fixes
# the client/server skew: the pair that gets deployed always comes from a
# single run, so re-running one build job in isolation can no longer desync
# what's live. Deploy Snapshot keys on this workflow's success, so it won't
# fire until the retag below has happened.
needs: [ prepare, build-tomcat, build-nginx ]
runs-on: ubuntu-latest
# id-token: write lets this job mint a GitHub OIDC token to assume the AWS deploy role.
permissions:
id-token: write
contents: read
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/xh-github-actions-deploy
aws-region: ${{ secrets.AWS_REGION }}
# Registry-side manifest copy — retags the existing build images to
# :snapshot without pulling or moving any layers. After this, each deployed
# image carries both its immutable snap_ tag and :snapshot.
- name: Retag build images as :snapshot
env:
IMAGE_TAG: ${{ needs.prepare.outputs.image-tag }}
run: |
for image in "${{ env.TOMCAT_IMAGE }}" "${{ env.NGINX_IMAGE }}"; do
manifest=$(aws ecr batch-get-image \
--repository-name "$image" \
--image-ids imageTag="$IMAGE_TAG" \
--query 'images[0].imageManifest' --output text)
aws ecr put-image \
--repository-name "$image" \
--image-tag snapshot \
--image-manifest "$manifest"
done