Skip to content

Build Release

Build Release #11

Workflow file for this run

# Build Release — Builds a numbered release as Docker images and pushes to ECR.
#
# Manually triggered from the master branch. Validates the version input, builds both
# nginx and tomcat images in parallel, pushes them to ECR, tags the commit, and creates
# a GitHub release with auto-generated notes. For snapshot builds, see Build Snapshot.
name: Build Release
on:
workflow_dispatch:
inputs:
version:
description: 'Release Version (e.g. 9.0.0)'
required: true
type: string
# Run all release builds one-by-one, but never cancel a release build due to another one.
concurrency:
group: build-release
cancel-in-progress: false
env:
TOMCAT_IMAGE: xh/toolbox-tomcat
NGINX_IMAGE: xh/toolbox-nginx
jobs:
validate:
# Releases are always cut from master, which tracks the last released code. This guards
# against an accidental release from develop (which carries unreleased SNAPSHOT work) or
# any other branch.
if: github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: true
# Checkout is required to be called with `fetch-depth: 0` and `fetch-tags: true`.
- name: Validate release version
uses: xh/hoist-dev-utils/.github/actions/validate-release-version@master
with:
version: ${{ inputs.version }}
prepare:
needs: validate
runs-on: ubuntu-latest
permissions: {}
outputs:
build-tag: ${{ steps.tag.outputs.build-tag }}
steps:
- name: Generate build tag
id: tag
run: echo "build-tag=${GITHUB_SHA::7}_${GITHUB_REF_NAME}_$(date -u +'%Y-%m-%dT%H:%MZ')" >> "$GITHUB_OUTPUT"
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
env:
VERSION: ${{ inputs.version }}
run: ./gradlew -PxhAppVersion="$VERSION" -PxhAppBuild="${{ needs.prepare.outputs.build-tag }}" 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
- name: Build and push tomcat image
env:
ECR_ROOT: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com
VERSION: ${{ inputs.version }}
run: |
docker build -t "$ECR_ROOT/${{ env.TOMCAT_IMAGE }}:$VERSION" \
-t "$ECR_ROOT/${{ env.TOMCAT_IMAGE }}:latest" \
docker/tomcat
docker push "$ECR_ROOT/${{ env.TOMCAT_IMAGE }}:$VERSION"
docker push "$ECR_ROOT/${{ env.TOMCAT_IMAGE }}:latest"
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.
- 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 dependencies and build client app
env:
VERSION: ${{ inputs.version }}
run: cd client-app && yarn install --frozen-lockfile && yarn lint && yarn build --env appVersion="$VERSION" --env appBuild="${{ needs.prepare.outputs.build-tag }}"
- 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
- name: Build and push nginx image
env:
ECR_ROOT: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com
VERSION: ${{ inputs.version }}
run: |
docker build -t "$ECR_ROOT/${{ env.NGINX_IMAGE }}:$VERSION" \
-t "$ECR_ROOT/${{ env.NGINX_IMAGE }}:latest" \
docker/nginx
docker push "$ECR_ROOT/${{ env.NGINX_IMAGE }}:$VERSION"
docker push "$ECR_ROOT/${{ env.NGINX_IMAGE }}:latest"
release:
needs: [ build-tomcat, build-nginx ]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: true
# Note: The tag intentionally points to a commit where gradle.properties still has
# the SNAPSHOT version. We chose not to commit the version change back to the repo to
# avoid paired set/reset commits on every release. The built WAR artifact has the
# correct release version — the tag is just a pointer to the source commit.
# Checkout is required to be called with `fetch-depth: 0` and `fetch-tags: true`.
- name: Create tag and GitHub release
uses: xh/hoist-dev-utils/.github/actions/create-tag-and-github-release@master
with:
version: ${{ inputs.version }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}