Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions .github/workflows/deployRelease.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# 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. 8.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
Comment thread
jskupsik marked this conversation as resolved.
Outdated

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: Install dependencies
run: yarn install --frozen-lockfile

- 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
Comment thread
jskupsik marked this conversation as resolved.
Outdated

- 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"
Comment thread
jskupsik marked this conversation as resolved.
Outdated
68 changes: 68 additions & 0 deletions .github/workflows/deploySnapshot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Deploy Snapshot — Publishes a SNAPSHOT build to npm on every push to develop.
#
# Snapshots are mutable development builds (version from package.json, e.g. 8.0.0-SNAPSHOT).
# They are published with the `next` dist-tag so they don't affect `latest`. For numbered
# releases, see Deploy Release.

name: Deploy Snapshot

on:
push:
branches: [ "develop" ]
workflow_dispatch:
inputs:
xhSnapshotVersion:
description: '(Optional) Snapshot version to override default in package.json. Note that the suffix "-SNAPSHOT" will be automatically appended if not present.'
required: false
default: ''
type: string

concurrency:
group: deploy-snapshot
cancel-in-progress: true

jobs:
build:

runs-on: ubuntu-latest
permissions:
contents: read

steps:
- uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
registry-url: 'https://registry.npmjs.org'

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Override snapshot version in package.json
if: inputs.xhSnapshotVersion != ''
env:
VERSION: ${{ inputs.xhSnapshotVersion }}
run: |
if [[ ! "$VERSION" == *-SNAPSHOT ]]; then
VERSION="$VERSION-SNAPSHOT"
fi
npm version --no-git-tag-version --new-version "$VERSION"
echo "Updated version to SNAPSHOT: $VERSION"

- name: Validate SNAPSHOT version
run: |
VERSION=$(node -p "require('./package.json').version")
TIME=$(node -p "new Date().getTime()")
if [[ ! "$VERSION" == *-SNAPSHOT ]]; then
echo "::error::'$VERSION' is not a SNAPSHOT version. Aborting."
exit 1
fi
npm version --no-git-tag-version --new-version "$VERSION.$TIME"
echo "Validated snapshot version: $VERSION.$TIME"

- name: Publish snapshot to npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish --tag next
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lts/*