Publish to NPM workflow (less/less.js)
The Publish to NPM workflow from less/less.js, explained and optimized by Latchkey.
CI health: C - fair
Point runs-on at Latchkey and get run de-duplication, job timeouts, SHA-pinned actions, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the Publish to NPM workflow from the less/less.js repository, a real project running GitHub Actions. It is shown here with attribution under its Apache-2.0 license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
name: Publish to NPM
on:
# Publish when a release PR is merged:
# master branch: "chore: release vX.Y.Z" PR β publishes latest
# alpha branch: "chore: alpha release vX.Y.Z" PR β publishes alpha
# Both release PRs are created automatically by create-release-pr.yml.
pull_request:
types: [closed]
branches:
- master
- alpha
permissions:
id-token: write # Required for OIDC trusted publishing
contents: write # Required for creating releases and pushing tags
jobs:
publish:
name: Publish to NPM
runs-on: ubuntu-latest
# Only run when a release PR with the expected title is merged into master
# or alpha. Any other PR close (or merge without the right title) is
# silently skipped.
if: |
github.repository == 'less/less.js' &&
github.event.pull_request.merged == true &&
(
(github.event.pull_request.base.ref == 'master' &&
startsWith(github.event.pull_request.title, 'chore: release v')) ||
(github.event.pull_request.base.ref == 'alpha' &&
startsWith(github.event.pull_request.title, 'chore: alpha release v'))
)
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
# Check out the base branch (master or alpha) post-merge so the
# version bump from the release PR is already present.
ref: ${{ github.event.pull_request.base.ref }}
- name: Install pnpm
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Run node tests (ESM + CJS)
run: pnpm run test:node
- name: Build
run: |
cd packages/less
pnpm run build
- name: Configure Git
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: Determine branch and tag type
id: branch-info
run: |
# Always a pull_request event; base.ref is master or alpha.
BRANCH="${{ github.event.pull_request.base.ref }}"
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
if [ "$BRANCH" = "alpha" ]; then
echo "is_alpha=true" >> $GITHUB_OUTPUT
echo "npm_tag=alpha" >> $GITHUB_OUTPUT
echo "release_type=prerelease" >> $GITHUB_OUTPUT
else
echo "is_alpha=false" >> $GITHUB_OUTPUT
echo "npm_tag=latest" >> $GITHUB_OUTPUT
echo "release_type=release" >> $GITHUB_OUTPUT
fi
- name: Validate alpha branch requirements
if: steps.branch-info.outputs.is_alpha == 'true'
run: |
# Fetch master branch
git fetch origin master:master || true
# Check 1: Alpha branch must not be behind master
echo "π Checking if alpha branch is up to date with master..."
MASTER_COMMITS=$(git rev-list --count alpha..master 2>/dev/null || echo "0")
if [ "$MASTER_COMMITS" -gt 0 ]; then
echo "β ERROR: Alpha branch is behind master by $MASTER_COMMITS commit(s)"
echo " Alpha branch must include all commits from master before publishing"
exit 1
fi
echo "β
Alpha branch is up to date with master"
# Check 2: Get current version and validate it contains 'alpha'
CURRENT_VERSION=$(node -p "require('./packages/less/package.json').version")
echo "π¦ Current version: $CURRENT_VERSION"
if [[ ! "$CURRENT_VERSION" =~ -alpha\. ]]; then
echo "β ERROR: Alpha branch version must contain '-alpha.'"
echo " Current version: $CURRENT_VERSION"
echo " Expected format: X.Y.Z-alpha.N"
exit 1
fi
echo "β
Version contains 'alpha' suffix"
# Check 3: Alpha base version must be >= master version
echo "π Comparing alpha base version with master version..."
MASTER_VERSION=$(git show master:packages/less/package.json 2>/dev/null | node -p "try { JSON.parse(require('fs').readFileSync(0, 'utf-8')).version } catch(e) { '0.0.0' }" || echo "0.0.0")
if [ "$MASTER_VERSION" = "0.0.0" ]; then
echo "β οΈ Could not determine master version, skipping comparison"
else
echo "π¦ Master version: $MASTER_VERSION"
# Extract base version (remove -alpha.X suffix)
ALPHA_BASE=$(echo "$CURRENT_VERSION" | sed 's/-alpha\.[0-9]*$//')
echo "π¦ Alpha base version: $ALPHA_BASE"
# Compare versions using semver from root workspace
COMPARE_RESULT=$(node -e "
const semver = require('semver');
const alphaBase = process.argv[1];
const master = process.argv[2];
if (semver.lt(alphaBase, master)) {
console.log('ERROR');
} else {
console.log('OK');
}
" "$ALPHA_BASE" "$MASTER_VERSION" 2>/dev/null || echo "ERROR")
if [ "$COMPARE_RESULT" = "ERROR" ]; then
echo "β ERROR: Alpha base version ($ALPHA_BASE) is lower than master version ($MASTER_VERSION)"
echo " According to semver, alpha base version must be >= master version"
exit 1
fi
echo "β
Alpha base version is >= master version"
fi
- name: Ensure npm 11.5.1 or later for trusted publishing
run: npm install -g npm@latest
- name: Bump version and publish
id: publish
env:
# Pass the resolved base branch name (master or alpha) so that
# bump-and-publish.js knows which branch it is publishing for.
GITHUB_REF_NAME: ${{ steps.branch-info.outputs.branch }}
run: |
pnpm run publish
# Extract version from package.json
VERSION=$(node -p "require('./packages/less/package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=v$VERSION" >> $GITHUB_OUTPUT
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ steps.publish.outputs.tag }}"
VERSION="${{ steps.publish.outputs.version }}"
IS_ALPHA="${{ steps.branch-info.outputs.is_alpha }}"
if [ "$IS_ALPHA" = "true" ]; then
TITLE="Alpha Release $TAG"
PRERELEASE="--prerelease"
BODY="## Alpha Release
This is an alpha release from the alpha branch.
## Installation
\`\`\`bash
npm install less@${VERSION} --tag alpha
\`\`\`
Or:
\`\`\`bash
npm install less@alpha
\`\`\`"
else
TITLE="Release $TAG"
PRERELEASE=""
BODY="## Changes
See [CHANGELOG.md](https://github.com/less/less.js/blob/master/CHANGELOG.md) for details.
## Installation
\`\`\`bash
npm install less@${VERSION}
\`\`\`"
fi
if gh release view "$TAG" &>/dev/null; then
echo "Release $TAG already exists, uploading assets to existing release"
gh release upload "$TAG" packages/less/dist/less.js packages/less/dist/less.min.js --clobber
else
gh release create "$TAG" \
--title "$TITLE" \
$PRERELEASE \
--notes "$BODY" \
packages/less/dist/less.js \
packages/less/dist/less.min.js
fi
The same workflow, on Latchkey
Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.
name: Publish to NPM on: # Publish when a release PR is merged: # master branch: "chore: release vX.Y.Z" PR β publishes latest # alpha branch: "chore: alpha release vX.Y.Z" PR β publishes alpha # Both release PRs are created automatically by create-release-pr.yml. pull_request: types: [closed] branches: - master - alpha permissions: id-token: write # Required for OIDC trusted publishing contents: write # Required for creating releases and pushing tags concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: publish: timeout-minutes: 30 name: Publish to NPM runs-on: latchkey-small # Only run when a release PR with the expected title is merged into master # or alpha. Any other PR close (or merge without the right title) is # silently skipped. if: | github.repository == 'less/less.js' && github.event.pull_request.merged == true && ( (github.event.pull_request.base.ref == 'master' && startsWith(github.event.pull_request.title, 'chore: release v')) || (github.event.pull_request.base.ref == 'alpha' && startsWith(github.event.pull_request.title, 'chore: alpha release v')) ) steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} # Check out the base branch (master or alpha) post-merge so the # version bump from the release PR is already present. ref: ${{ github.event.pull_request.base.ref }} - name: Install pnpm uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 'lts/*' registry-url: 'https://registry.npmjs.org' cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile --ignore-scripts - name: Run node tests (ESM + CJS) run: pnpm run test:node - name: Build run: | cd packages/less pnpm run build - name: Configure Git run: | git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" - name: Determine branch and tag type id: branch-info run: | # Always a pull_request event; base.ref is master or alpha. BRANCH="${{ github.event.pull_request.base.ref }}" echo "branch=$BRANCH" >> $GITHUB_OUTPUT if [ "$BRANCH" = "alpha" ]; then echo "is_alpha=true" >> $GITHUB_OUTPUT echo "npm_tag=alpha" >> $GITHUB_OUTPUT echo "release_type=prerelease" >> $GITHUB_OUTPUT else echo "is_alpha=false" >> $GITHUB_OUTPUT echo "npm_tag=latest" >> $GITHUB_OUTPUT echo "release_type=release" >> $GITHUB_OUTPUT fi - name: Validate alpha branch requirements if: steps.branch-info.outputs.is_alpha == 'true' run: | # Fetch master branch git fetch origin master:master || true # Check 1: Alpha branch must not be behind master echo "π Checking if alpha branch is up to date with master..." MASTER_COMMITS=$(git rev-list --count alpha..master 2>/dev/null || echo "0") if [ "$MASTER_COMMITS" -gt 0 ]; then echo "β ERROR: Alpha branch is behind master by $MASTER_COMMITS commit(s)" echo " Alpha branch must include all commits from master before publishing" exit 1 fi echo "β Alpha branch is up to date with master" # Check 2: Get current version and validate it contains 'alpha' CURRENT_VERSION=$(node -p "require('./packages/less/package.json').version") echo "π¦ Current version: $CURRENT_VERSION" if [[ ! "$CURRENT_VERSION" =~ -alpha\. ]]; then echo "β ERROR: Alpha branch version must contain '-alpha.'" echo " Current version: $CURRENT_VERSION" echo " Expected format: X.Y.Z-alpha.N" exit 1 fi echo "β Version contains 'alpha' suffix" # Check 3: Alpha base version must be >= master version echo "π Comparing alpha base version with master version..." MASTER_VERSION=$(git show master:packages/less/package.json 2>/dev/null | node -p "try { JSON.parse(require('fs').readFileSync(0, 'utf-8')).version } catch(e) { '0.0.0' }" || echo "0.0.0") if [ "$MASTER_VERSION" = "0.0.0" ]; then echo "β οΈ Could not determine master version, skipping comparison" else echo "π¦ Master version: $MASTER_VERSION" # Extract base version (remove -alpha.X suffix) ALPHA_BASE=$(echo "$CURRENT_VERSION" | sed 's/-alpha\.[0-9]*$//') echo "π¦ Alpha base version: $ALPHA_BASE" # Compare versions using semver from root workspace COMPARE_RESULT=$(node -e " const semver = require('semver'); const alphaBase = process.argv[1]; const master = process.argv[2]; if (semver.lt(alphaBase, master)) { console.log('ERROR'); } else { console.log('OK'); } " "$ALPHA_BASE" "$MASTER_VERSION" 2>/dev/null || echo "ERROR") if [ "$COMPARE_RESULT" = "ERROR" ]; then echo "β ERROR: Alpha base version ($ALPHA_BASE) is lower than master version ($MASTER_VERSION)" echo " According to semver, alpha base version must be >= master version" exit 1 fi echo "β Alpha base version is >= master version" fi - name: Ensure npm 11.5.1 or later for trusted publishing run: npm install -g npm@latest - name: Bump version and publish id: publish env: # Pass the resolved base branch name (master or alpha) so that # bump-and-publish.js knows which branch it is publishing for. GITHUB_REF_NAME: ${{ steps.branch-info.outputs.branch }} run: | pnpm run publish # Extract version from package.json VERSION=$(node -p "require('./packages/less/package.json').version") echo "version=$VERSION" >> $GITHUB_OUTPUT echo "tag=v$VERSION" >> $GITHUB_OUTPUT - name: Create GitHub Release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG="${{ steps.publish.outputs.tag }}" VERSION="${{ steps.publish.outputs.version }}" IS_ALPHA="${{ steps.branch-info.outputs.is_alpha }}" if [ "$IS_ALPHA" = "true" ]; then TITLE="Alpha Release $TAG" PRERELEASE="--prerelease" BODY="## Alpha Release This is an alpha release from the alpha branch. ## Installation \`\`\`bash npm install less@${VERSION} --tag alpha \`\`\` Or: \`\`\`bash npm install less@alpha \`\`\`" else TITLE="Release $TAG" PRERELEASE="" BODY="## Changes See [CHANGELOG.md](https://github.com/less/less.js/blob/master/CHANGELOG.md) for details. ## Installation \`\`\`bash npm install less@${VERSION} \`\`\`" fi if gh release view "$TAG" &>/dev/null; then echo "Release $TAG already exists, uploading assets to existing release" gh release upload "$TAG" packages/less/dist/less.js packages/less/dist/less.min.js --clobber else gh release create "$TAG" \ --title "$TITLE" \ $PRERELEASE \ --notes "$BODY" \ packages/less/dist/less.js \ packages/less/dist/less.min.js fi
What changed
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Cancel superseded runs when a branch or PR gets a newer push.
- Add a job timeout so a hung step cannot burn hours of runner time.
1 third-party action is referenced by a movable tag. Pin it to the commit SHA (Latchkey resolves and applies this automatically) so a repointed tag cannot change what runs.
What Latchkey heals here
This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:
- Dependency installs
This workflow runs 1 job per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.