Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
103 changes: 79 additions & 24 deletions .github/workflows/create-release-pr.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
name: Create Release PR

# When code lands on master (not a release PR merge itself), automatically
# create or update a "chore: release vX.Y.Z" pull request that bumps the
# When code lands on master or alpha (not a release PR merge itself),
# automatically create or update a release pull request that bumps the
# version. Maintainers then merge that PR to trigger publishing.
#
# master → "chore: release vX.Y.Z" PR targets master
# alpha → "chore: alpha release vX.Y.Z" PR targets alpha
on:
push:
branches:
- master
- alpha
# Only trigger for commits that touch package source files.
paths:
- 'packages/**'
Expand All @@ -20,11 +24,14 @@ jobs:
name: Create or Update Release PR
runs-on: ubuntu-latest
# Skip if this push is itself the merge of a release PR (prevents an
# infinite loop). We catch both squash-merged and regular-merged commits.
# infinite loop). We catch both squash-merged and regular-merged commits
# for both the master and alpha release PR title conventions.
if: |
github.repository == 'less/less.js' &&
!contains(github.event.head_commit.message, 'chore: release v') &&
!contains(github.event.head_commit.message, '/release-v')
!contains(github.event.head_commit.message, 'chore: alpha release v') &&
!contains(github.event.head_commit.message, '/release-v') &&
!contains(github.event.head_commit.message, '/alpha-release-v')

steps:
- name: Checkout
Expand All @@ -47,21 +54,46 @@ jobs:
- name: Determine next version
id: version
run: |
BRANCH="${{ github.ref_name }}"
CURRENT=$(node -p "require('./packages/less/package.json').version")
NPM_VERSION=$(npm view less version 2>/dev/null || echo "")
NEXT=$(node -e "
const semver = require('semver');
const cur = process.argv[1];
const npm = process.argv[2] || null;
if (npm && semver.valid(cur) && semver.gt(cur, npm)) {
process.stdout.write(cur);
} else {
const base = npm || cur;
process.stdout.write(semver.inc(base, 'patch'));
}
" "$CURRENT" "$NPM_VERSION")
echo "next_version=$NEXT" >> "$GITHUB_OUTPUT"
echo "branch=chore/release-v$NEXT" >> "$GITHUB_OUTPUT"

if [ "$BRANCH" = "alpha" ]; then
# Alpha: increment the alpha prerelease number.
# X.Y.Z-alpha.N → X.Y.Z-alpha.(N+1)
# If package.json doesn't carry an alpha version yet, bump the
# major and start a fresh alpha.1 series.
NEXT=$(node -e "
const cur = process.argv[1];
const m = cur.match(/^(\d+\.\d+\.\d+)-alpha\.(\d+)$/);
if (m) {
process.stdout.write(m[1] + '-alpha.' + (parseInt(m[2], 10) + 1));
} else {
const parts = cur.replace(/-.*/, '').split('.');
const nextMajor = parseInt(parts[0], 10) + 1;
process.stdout.write(nextMajor + '.0.0-alpha.1');
}
" "$CURRENT")
echo "next_version=$NEXT" >> "$GITHUB_OUTPUT"
echo "branch=chore/alpha-release-v$NEXT" >> "$GITHUB_OUTPUT"
echo "release_base=alpha" >> "$GITHUB_OUTPUT"
else
# Master: patch-increment from the latest npm published version.
NPM_VERSION=$(npm view less version 2>/dev/null || echo "")
NEXT=$(node -e "
const semver = require('semver');
const cur = process.argv[1];
const npm = process.argv[2] || null;
if (npm && semver.valid(cur) && semver.gt(cur, npm)) {
process.stdout.write(cur);
} else {
const base = npm || cur;
process.stdout.write(semver.inc(base, 'patch'));
}
" "$CURRENT" "$NPM_VERSION")
echo "next_version=$NEXT" >> "$GITHUB_OUTPUT"
echo "branch=chore/release-v$NEXT" >> "$GITHUB_OUTPUT"
echo "release_base=master" >> "$GITHUB_OUTPUT"
fi

- name: Configure Git
run: |
Expand All @@ -72,15 +104,21 @@ jobs:
env:
NEXT_VERSION: ${{ steps.version.outputs.next_version }}
RELEASE_BRANCH: ${{ steps.version.outputs.branch }}
RELEASE_BASE: ${{ steps.version.outputs.release_base }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TITLE="chore: release v${NEXT_VERSION}"
set -euo pipefail
if [ "$RELEASE_BASE" = "alpha" ]; then
TITLE="chore: alpha release v${NEXT_VERSION}"
else
TITLE="chore: release v${NEXT_VERSION}"
fi

# Create or reset the release branch off the latest master so it
# Create or reset the release branch off the latest base branch so it
# always includes all recent commits.
if git ls-remote --exit-code origin "refs/heads/${RELEASE_BRANCH}" &>/dev/null; then
git fetch origin "${RELEASE_BRANCH}"
git checkout -B "${RELEASE_BRANCH}" origin/master
git checkout -B "${RELEASE_BRANCH}" "origin/${RELEASE_BASE}"
else
git checkout -b "${RELEASE_BRANCH}"
fi
Expand All @@ -101,10 +139,27 @@ jobs:
"

git add package.json packages/*/package.json
COMMITTED=false
if git diff --cached --quiet; then
echo "No version changes; branch is already at v${NEXT_VERSION}"
else
git commit -m "${TITLE}"
COMMITTED=true
fi

# If no new commit was created the release branch has no commits
# ahead of master, so pushing it and trying to open a PR would fail
# with "no commits between head and base". Instead, just report
# whether an existing release PR is open and exit cleanly.
if [ "$COMMITTED" = "false" ]; then
EXISTING=$(gh pr list --head "${RELEASE_BRANCH}" --base "${RELEASE_BASE}" \
--json number --jq '.[0].number' 2>/dev/null || echo "")
if [ -n "${EXISTING}" ]; then
echo "✅ No new changes; release PR #${EXISTING} already exists"
else
echo "✅ No version bump needed and no existing release PR; nothing to do"
fi
exit 0
fi

# --force-with-lease refuses to overwrite if the remote has advanced
Expand All @@ -114,7 +169,7 @@ jobs:
git push origin "${RELEASE_BRANCH}" --force-with-lease

# Open a PR if one doesn't already exist for this version.
EXISTING=$(gh pr list --head "${RELEASE_BRANCH}" --base master \
EXISTING=$(gh pr list --head "${RELEASE_BRANCH}" --base "${RELEASE_BASE}" \
--json number --jq '.[0].number' 2>/dev/null || echo "")

if [ -z "${EXISTING}" ]; then
Expand All @@ -129,9 +184,9 @@ jobs:
gh pr create \
--title "${TITLE}" \
--body "${BODY}" \
--base master \
--base "${RELEASE_BASE}" \
--head "${RELEASE_BRANCH}"
echo "✅ Created release PR for v${NEXT_VERSION}"
else
echo "✅ Release PR #${EXISTING} already exists; branch updated to include latest master commits"
echo "✅ Release PR #${EXISTING} already exists; branch updated to include latest ${RELEASE_BASE} commits"
fi
48 changes: 17 additions & 31 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
name: Publish to NPM

on:
# Master: publish when a "chore: release vX.Y.Z" pull request is merged.
# The release PR is created automatically by create-release-pr.yml.
# 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: publish on direct push to the alpha branch.
push:
branches:
- alpha
paths-ignore:
- '**.md'
- 'docs/**'
- '.gitignore'
- '.claude/**'
- '.github/**'
- 'scripts/**'

permissions:
id-token: write # Required for OIDC trusted publishing
Expand All @@ -27,18 +19,17 @@ jobs:
publish:
name: Publish to NPM
runs-on: ubuntu-latest
# Master: only run when a release PR (title = "chore: release v*") is merged.
# Alpha: only run on direct pushes; skip if it's a version-bump commit
# (prevents the bump-and-publish script from triggering itself).
# 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_name == 'pull_request' &&
github.event.pull_request.merged == true &&
(github.event.pull_request.base.ref == 'master' &&
startsWith(github.event.pull_request.title, 'chore: release v')) ||
(github.event_name == 'push' &&
github.ref_name == 'alpha' &&
!startsWith(github.event.head_commit.message, 'chore: bump version to'))
(github.event.pull_request.base.ref == 'alpha' &&
startsWith(github.event.pull_request.title, 'chore: alpha release v'))
)

steps:
Expand All @@ -47,9 +38,9 @@ jobs:
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
# For PR events check out the base branch (master) post-merge so the
# Check out the base branch (master or alpha) post-merge so the
# version bump from the release PR is already present.
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || github.ref }}
ref: ${{ github.event.pull_request.base.ref }}

- name: Install pnpm
uses: pnpm/action-setup@v4
Expand Down Expand Up @@ -79,13 +70,8 @@ jobs:
- name: Determine branch and tag type
id: branch-info
run: |
# For PR events the branch is the PR's base (master); for push events
# it is the pushed branch (alpha).
if [ "${{ github.event_name }}" = "pull_request" ]; then
BRANCH="${{ github.event.pull_request.base.ref }}"
else
BRANCH="${{ github.ref_name }}"
fi
# 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
Expand Down Expand Up @@ -165,8 +151,8 @@ jobs:
- name: Bump version and publish
id: publish
env:
# Use the branch name resolved by the branch-info step above rather
# than repeating the PR-vs-push detection logic here.
# 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
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"changelog": "github-changes -o less -r less.js -a --only-pulls --use-commit-body -m \"(YYYY-MM-DD)\"",
"test": "cd packages/less && npm test",
"test:node": "cd packages/less && npm run test:node",
"test:release": "node scripts/test-release-automation.js",
"postinstall": "npx only-allow pnpm"
},
"author": "Alexis Sellier <self@cloudhead.net>",
Expand Down
Loading
Loading