name: Publish to npm
on:
push:
branches:
- main
permissions:
id-token: write # OIDC for npm trusted publishing + provenance
contents: write # for tag + GitHub Release creation
# Release is driven by the head commit message. Supported forms:
# release: 2.1.0 -> apex-grid (core) only — back-compat
# release: apex-grid@2.1.0 -> core
# release: apex-grid-enterprise@0.1.0 -> enterprise
# release: apex-grid@2.1.0 apex-grid-enterprise@0.1.0 -> both, on their own version lines
# A pre-release version (containing a hyphen, e.g. 2.2.0-beta.1) publishes under the
# `next` dist-tag; otherwise `latest`. Each package is verified, published with
# provenance from its own dist/, git-tagged, and given a GitHub Release.
jobs:
publish:
runs-on: ubuntu-latest
if: startsWith(github.event.head_commit.message, 'release:')
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# Parse the release commit into newline-separated entries:
#
||||
- name: Parse release commit
id: parse
env:
MSG: ${{ github.event.head_commit.message }}
run: |
set -euo pipefail
# Map an npm package name to its workspace dir (case, not an associative
# array, so it runs the same on bash 3.x and 5.x).
dir_of() {
case "$1" in
apex-grid) echo packages/core;;
apex-grid-enterprise) echo packages/enterprise;;
*) echo "";;
esac
}
HEAD_LINE=$(printf '%s\n' "$MSG" | head -1)
BODY=$(printf '%s' "${HEAD_LINE#release:}" | xargs || true) # strip prefix + trim
RELEASES=""
npm_tag_for() { case "$1" in *-*) echo next;; *) echo latest;; esac; }
if [[ "$BODY" =~ ^[0-9]+\.[0-9]+\.[0-9]+[A-Za-z0-9.+-]*$ ]]; then
# Bare version -> core, legacy git tag form vX.Y.Z.
RELEASES="packages/core|apex-grid|$BODY|v$BODY|$(npm_tag_for "$BODY")"
else
for tok in $BODY; do
name=${tok%@*}; version=${tok#*@}
dir=$(dir_of "$name")
if [[ -z "$dir" ]]; then echo "❌ Unknown package '$name'"; exit 1; fi
if [[ "$tok" != *@* || -z "$version" ]]; then echo "❌ Expected @, got '$tok'"; exit 1; fi
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+[A-Za-z0-9.+-]*$ ]]; then echo "❌ Bad version '$version'"; exit 1; fi
RELEASES+="${dir}|${name}|${version}|${name}@${version}|$(npm_tag_for "$version")"$'\n'
done
fi
RELEASES=$(printf '%s' "$RELEASES" | sed '/^$/d')
if [[ -z "$RELEASES" ]]; then echo "❌ Commit must match 'release: X.Y.Z' or 'release: @ ...'"; exit 1; fi
echo "Planned releases:"; printf '%s\n' "$RELEASES"
{ echo "releases<> "$GITHUB_OUTPUT"
# Fail fast if a requested version doesn't match the package's package.json,
# BEFORE we spend time installing/building/publishing anything.
- name: Verify source versions
env:
RELEASES: ${{ steps.parse.outputs.releases }}
run: |
set -euo pipefail
while IFS='|' read -r dir name version gittag npmtag; do
[[ -z "$dir" ]] && continue
pv=$(node -p "require('./$dir/package.json').version")
if [[ "$pv" != "$version" ]]; then
echo "❌ $name: $dir/package.json version ($pv) does not match requested ($version)"; exit 1
fi
echo "✅ $name source version $pv"
done < <(printf '%s\n' "$RELEASES")
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: https://registry.npmjs.org/
cache: 'npm'
- run: npm install -g npm@latest
- run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
# Build before Test: the enterprise unit tests import `apex-grid/internal`,
# which resolves to core's built dist/ (gitignored, so absent in a fresh
# checkout). Building first ensures that artifact exists.
- name: Build
run: npm run build
- name: Test
run: npm test
- name: Publish, tag, and release each package
env:
RELEASES: ${{ steps.parse.outputs.releases }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
while IFS='|' read -r dir name version gittag npmtag; do
[[ -z "$dir" ]] && continue
echo "::group::Release $name@$version ($npmtag)"
# The built dist/ is the publishable manifest — confirm the build injected the version.
dist_v=$(node -p "require('./$dir/dist/package.json').version")
if [[ "$dist_v" != "$version" ]]; then
echo "❌ $name: dist version ($dist_v) does not match expected ($version). Build did not inject the version."; exit 1
fi
( cd "$dir/dist" && npm publish --provenance --tag "$npmtag" )
echo "✅ Published $name@$version to npm (dist-tag: $npmtag)"
if git rev-parse "$gittag" >/dev/null 2>&1; then
echo "Tag $gittag already exists, skipping"
else
git tag -a "$gittag" -m "Release $gittag"
git push origin "$gittag"
fi
if gh release view "$gittag" >/dev/null 2>&1; then
echo "Release $gittag already exists, skipping"
else
notes=$(printf '%s\n\n%s' "\`$name@$version\` published to npm with provenance." "See the commit history for changes since the previous release.")
gh release create "$gittag" --title "$gittag" --notes "$notes"
fi
echo "::endgroup::"
done < <(printf '%s\n' "$RELEASES")