Skip to content

Deploy Release

Deploy Release #8

Workflow file for this run

# Deploy Release — Publishes a numbered release to npm.
#
# Manually triggered from the master branch. Validates the version input, publishes to npm,
# tags the commit, and creates a GitHub release with auto-generated notes. For snapshot
# builds, see Deploy Snapshot.
name: Deploy Release
on:
workflow_dispatch:
inputs:
xhReleaseVersion:
description: 'Release Version'
required: true
type: string
isHotfix:
description: 'As hotfix. Check when releasing a hotfix to a version other than the latest.'
required: true
default: false
type: boolean
jobs:
build:
# Guards against accidental release from develop. Requires master for standard
# releases and a branch other than master (or develop) when isHotfix is set.
if: github.ref != 'refs/heads/develop' && ((github.ref == 'refs/heads/master') != inputs.isHotfix)
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: true
- name: Validate release version
env:
VERSION: ${{ inputs.xhReleaseVersion }}
IS_HOTFIX: ${{ inputs.isHotfix }}
run: |
# Must be semver (X.Y.Z) with no leading zeros.
if [[ ! "$VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
echo "::error::Invalid version '$VERSION'. Must be semver with no leading zeros (e.g. 82.0.0)."
exit 1
fi
# Must not duplicate an existing release
if git tag -l "v$VERSION" | grep -q .; then
echo "::error::Tag v$VERSION already exists. This version has already been released."
exit 1
fi
# Strict version validation — the new version must be exactly one
# increment from the latest relevant tag and hotfix cannot be latest.
LATEST=$(git tag -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)
if [ -z "$LATEST" ]; then
echo "::error::No existing release tags found. Cannot validate version."
exit 1
fi
LATEST_MAJOR=$(echo "$LATEST" | cut -d. -f1)
LATEST_MINOR=$(echo "$LATEST" | cut -d. -f2)
LATEST_PATCH=$(echo "$LATEST" | cut -d. -f3)
# The three versions that would be valid as a standard (non-hotfix) release.
NEXT_MAJOR="$(( LATEST_MAJOR + 1 )).0.0"
NEXT_MINOR="${LATEST_MAJOR}.$(( LATEST_MINOR + 1 )).0"
NEXT_PATCH="${LATEST_MAJOR}.${LATEST_MINOR}.$(( LATEST_PATCH + 1 ))"
if [ "$IS_HOTFIX" = "true" ]; then
# A hotfix must NOT be a standard next-release version.
if [ "$VERSION" = "$NEXT_MAJOR" ] || [ "$VERSION" = "$NEXT_MINOR" ] || [ "$VERSION" = "$NEXT_PATCH" ]; then
echo "::error::Hotfix version $VERSION matches a standard release increment (latest is v$LATEST). Use a standard release instead."
exit 1
fi
NEW_MAJOR=$(echo "$VERSION" | cut -d. -f1)
NEW_MINOR=$(echo "$VERSION" | cut -d. -f2)
# Validate against the highest tags for this major version.
MAX_MINOR=$(git tag -l "v${NEW_MAJOR}.*" | grep -E "^v${NEW_MAJOR}\.[0-9]+\.[0-9]+$" | sed 's/^v//' | cut -d. -f2 | sort -n | tail -1)
if [ -z "$MAX_MINOR" ]; then
echo "::error::No existing tags found for major version ${NEW_MAJOR}. Cannot validate hotfix."
exit 1
fi
# Allowed: next minor bump for this major.
ALLOWED_MINOR="${NEW_MAJOR}.$(( MAX_MINOR + 1 )).0"
# Only offer a patch bump if tags exist for this specific MAJOR.MINOR.
MAX_PATCH=$(git tag -l "v${NEW_MAJOR}.${NEW_MINOR}.*" | grep -E "^v${NEW_MAJOR}\.${NEW_MINOR}\.[0-9]+$" | sed 's/^v//' | cut -d. -f3 | sort -n | tail -1)
if [ -n "$MAX_PATCH" ]; then
ALLOWED_PATCH="${NEW_MAJOR}.${NEW_MINOR}.$(( MAX_PATCH + 1 ))"
fi
if [ "$VERSION" != "$ALLOWED_MINOR" ] && [ "$VERSION" != "${ALLOWED_PATCH:-}" ]; then
ALLOWED="$ALLOWED_MINOR"
[ -n "${ALLOWED_PATCH:-}" ] && ALLOWED="$ALLOWED or $ALLOWED_PATCH"
echo "::error::Hotfix version $VERSION is not a valid next version. Allowed: $ALLOWED."
exit 1
fi
else
# Standard release: must be exactly one increment from the latest tag.
if [ "$VERSION" != "$NEXT_MAJOR" ] && [ "$VERSION" != "$NEXT_MINOR" ] && [ "$VERSION" != "$NEXT_PATCH" ]; then
echo "::error::Version $VERSION is not a valid next version (latest is v$LATEST). Allowed: $NEXT_MAJOR, $NEXT_MINOR, or $NEXT_PATCH."
exit 1
fi
fi
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
registry-url: 'https://registry.npmjs.org'
- name: Configure Font Awesome registry auth
env:
FONTAWESOME_PACKAGE_TOKEN: ${{ secrets.FONTAWESOME_PACKAGE_TOKEN }}
run: echo "//npm.fontawesome.com/:_authToken=$FONTAWESOME_PACKAGE_TOKEN" >> .npmrc
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Yarn lint
run: yarn lint:all
- name: Set release version in package.json
env:
VERSION: ${{ inputs.xhReleaseVersion }}
run: npm version --no-git-tag-version --new-version "$VERSION"
- name: Publish release to npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish
- name: Tag release
env:
VERSION: ${{ inputs.xhReleaseVersion }}
run: |
git tag "v$VERSION"
git push origin "v$VERSION"
- name: Create GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.xhReleaseVersion }}
IS_HOTFIX: ${{ inputs.isHotfix }}
run: |
LATEST_FLAG="--latest"
if [ "$IS_HOTFIX" = "true" ]; then
LATEST_FLAG="--latest=false"
fi
gh release create "v$VERSION" \
--title "v$VERSION" \
--generate-notes \
"$LATEST_FLAG"