hoist-react-snapshot #526
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |