Skip to content

Merge pull request #4527 from matthew-dean/port/master-fixtures #245

Merge pull request #4527 from matthew-dean/port/master-fixtures

Merge pull request #4527 from matthew-dean/port/master-fixtures #245

name: Create Release PR
# 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: release vX.Y.Z-alpha.N" PR targets alpha
on:
push:
branches:
- master
- alpha
pull_request:
types: [opened, edited, reopened, synchronize]
branches:
- master
- alpha
permissions:
contents: write
pull-requests: write
jobs:
create-release-pr:
name: Create or Update Release PR
runs-on: ubuntu-latest
# Skip only if this push is itself the merge of a release PR (prevents an
# infinite loop). Check the pushed commit subject, not historical release
# titles that can appear in a normal merge commit body.
if: |
github.event_name == 'push' &&
github.repository == 'less/less.js'
steps:
- name: Detect release PR merge
id: release-merge
env:
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
set -euo pipefail
SUBJECT="${COMMIT_MESSAGE%%$'\n'*}"
IS_RELEASE_MERGE=false
case "$SUBJECT" in
"chore: release v"* | \
"chore: alpha release v"* | \
"Merge pull request #"*" from less/chore/release-v"* | \
"Merge pull request #"*" from less/chore/alpha-release-v"*)
IS_RELEASE_MERGE=true
;;
esac
echo "is_release_merge=${IS_RELEASE_MERGE}" >> "$GITHUB_OUTPUT"
echo "commit_subject=${SUBJECT}"
- name: Checkout
if: steps.release-merge.outputs.is_release_merge != 'true'
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install pnpm
if: steps.release-merge.outputs.is_release_merge != 'true'
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
if: steps.release-merge.outputs.is_release_merge != 'true'
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
if: steps.release-merge.outputs.is_release_merge != 'true'
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Determine release version
if: steps.release-merge.outputs.is_release_merge != 'true'
id: version
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
BRANCH="$REF_NAME"
CURRENT=$(node -p "require('./packages/less/package.json').version")
if [ "$BRANCH" = "alpha" ]; then
NPM_VERSION=$(npm view less dist-tags.alpha 2>/dev/null || echo "")
else
NPM_VERSION=$(npm view less version 2>/dev/null || echo "")
fi
DEFAULT_NEXT=$(node scripts/release-metadata.js next-version "$BRANCH" "$CURRENT" "$NPM_VERSION")
EXISTING=$(gh pr list \
--state open \
--base "$BRANCH" \
--json title,headRefName,headRepository,isCrossRepository \
--jq '[.[] | select((.isCrossRepository | not) and .headRepository.nameWithOwner == "less/less.js" and ((.headRefName | startswith("chore/release-v")) or (.headRefName | startswith("chore/alpha-release-v"))))][0] // {}')
EXISTING_TITLE=$(node -e "const pr = JSON.parse(process.argv[1]); process.stdout.write(pr.title || '')" "$EXISTING")
EXISTING_BRANCH=$(node -e "const pr = JSON.parse(process.argv[1]); process.stdout.write(pr.headRefName || '')" "$EXISTING")
if [ -n "$EXISTING_TITLE" ]; then
NEXT=$(node scripts/release-metadata.js parse-title "$BRANCH" "$EXISTING_TITLE")
RELEASE_BRANCH="$EXISTING_BRANCH"
if ! node scripts/release-metadata.js validate "$BRANCH" "$NEXT" "$NPM_VERSION" 2>/dev/null; then
echo "Existing release PR title v${NEXT} is no longer publishable; falling back to v${DEFAULT_NEXT}"
NEXT="$DEFAULT_NEXT"
RELEASE_BRANCH=$(node scripts/release-metadata.js branch "$BRANCH" "$NEXT")
fi
else
NEXT="$DEFAULT_NEXT"
RELEASE_BRANCH=$(node scripts/release-metadata.js branch "$BRANCH" "$NEXT")
fi
node scripts/release-metadata.js validate "$BRANCH" "$NEXT" "$NPM_VERSION"
TITLE=$(node scripts/release-metadata.js title "$BRANCH" "$NEXT")
echo "next_version=$NEXT" >> "$GITHUB_OUTPUT"
echo "branch=$RELEASE_BRANCH" >> "$GITHUB_OUTPUT"
echo "release_base=$BRANCH" >> "$GITHUB_OUTPUT"
echo "title=$TITLE" >> "$GITHUB_OUTPUT"
- name: Configure Git
if: steps.release-merge.outputs.is_release_merge != 'true'
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: Create or update release branch and PR
if: steps.release-merge.outputs.is_release_merge != 'true'
env:
NEXT_VERSION: ${{ steps.version.outputs.next_version }}
RELEASE_BRANCH: ${{ steps.version.outputs.branch }}
RELEASE_BASE: ${{ steps.version.outputs.release_base }}
RELEASE_TITLE: ${{ steps.version.outputs.title }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
TITLE="${RELEASE_TITLE}"
# 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/${RELEASE_BASE}"
else
git checkout -b "${RELEASE_BRANCH}"
fi
node scripts/release-metadata.js sync-package-versions "${RELEASE_BASE}" "${NEXT_VERSION}"
git add package.json packages/*/package.json
# Auto-generate CHANGELOG entry from merged PRs since last tag.
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -n "$LAST_TAG" ]; then
LAST_TAG_DATE=$(git log -1 --format=%aI "$LAST_TAG")
PR_LINES=$(gh pr list \
--state merged \
--base "${RELEASE_BASE}" \
--search "merged:>${LAST_TAG_DATE}" \
--json number,title,author \
--jq '.[] | "- [#\(.number)](https://github.com/${{ github.repository }}/pull/\(.number)) \(.title) (@\(.author.login))"' \
2>/dev/null || echo "")
if [ -n "$PR_LINES" ] && ! grep -q "^### v${NEXT_VERSION}\\b" CHANGELOG.md; then
TODAY=$(date +%Y-%m-%d)
{
head -1 CHANGELOG.md
echo ""
echo "### v${NEXT_VERSION} (${TODAY})"
echo ""
echo "#### Changes"
echo ""
echo "$PR_LINES"
echo ""
tail -n +2 CHANGELOG.md
} > CHANGELOG.tmp && mv CHANGELOG.tmp CHANGELOG.md
git add CHANGELOG.md
elif grep -q "^### v${NEXT_VERSION}\\b" CHANGELOG.md; then
echo "CHANGELOG.md already has a v${NEXT_VERSION} section; keeping the curated release notes."
fi
fi
if git diff --cached --quiet; then
echo "No manifest changes; creating an explicit release commit for v${NEXT_VERSION}"
git commit --allow-empty -m "${TITLE}"
else
git commit -m "${TITLE}"
fi
# --force-with-lease refuses to overwrite if the remote has advanced
# past what we fetched, which protects against concurrent workflow
# runs. This is intentional: if two code PRs land simultaneously the
# second run will fail-fast here and the release branch stays coherent.
git push origin "${RELEASE_BRANCH}" --force-with-lease
# Compose the PR body: a short preamble plus a grouped, linked
# changelog of everything new since the previous release tag.
# No fallback: a generation failure aborts the step (set -e) rather
# than replacing an existing PR's changelog with an empty one.
PREAMBLE=$(node scripts/release-metadata.js body)
CHANGELOG=$(node scripts/release-changelog.js)
BODY=$(printf '%s\n\n%s\n' "${PREAMBLE}" "${CHANGELOG}")
# Open a PR if one doesn't already exist for this version.
EXISTING=$(gh pr list --head "${RELEASE_BRANCH}" --base "${RELEASE_BASE}" \
--json number --jq '.[0].number' 2>/dev/null || echo "")
if [ -z "${EXISTING}" ]; then
gh pr create \
--title "${TITLE}" \
--body "${BODY}" \
--base "${RELEASE_BASE}" \
--head "${RELEASE_BRANCH}"
echo "✅ Created release PR for v${NEXT_VERSION}"
else
# Refresh the body so the changelog reflects the latest commits.
gh pr edit "${EXISTING}" --body "${BODY}"
echo "✅ Release PR #${EXISTING} already exists; branch and changelog updated"
fi
sync-release-pr-title:
name: Sync Release PR Title
runs-on: ubuntu-latest
concurrency:
group: sync-release-pr-title-${{ github.event.pull_request.number }}
cancel-in-progress: true
if: |
github.event_name == 'pull_request' &&
github.repository == 'less/less.js' &&
github.event.pull_request.head.repo.full_name == github.repository &&
(
(
github.event.pull_request.base.ref == 'master' &&
startsWith(github.event.pull_request.head.ref, 'chore/release-v') &&
startsWith(github.event.pull_request.title, 'chore: release v')
) ||
(
github.event.pull_request.base.ref == 'alpha' &&
startsWith(github.event.pull_request.head.ref, 'chore/alpha-release-v') &&
(
startsWith(github.event.pull_request.title, 'chore: release v') ||
startsWith(github.event.pull_request.title, 'chore: alpha release v')
)
)
)
steps:
- name: Checkout release PR branch
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ github.event.pull_request.head.ref }}
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Load trusted release metadata script
env:
RELEASE_BASE: ${{ github.event.pull_request.base.ref }}
run: |
set -euo pipefail
git fetch origin "${RELEASE_BASE}"
mkdir -p .release-scripts
git show "origin/${RELEASE_BASE}:scripts/release-metadata.js" > .release-scripts/release-metadata.js
- name: Resolve title version
id: title-version
env:
RELEASE_BASE: ${{ github.event.pull_request.base.ref }}
RELEASE_TITLE: ${{ github.event.pull_request.title }}
run: |
set -euo pipefail
PREVIOUS_VERSION=$(node -p "require('./packages/less/package.json').version")
VERSION=$(node .release-scripts/release-metadata.js parse-title "$RELEASE_BASE" "$RELEASE_TITLE")
if [ "$RELEASE_BASE" = "alpha" ]; then
NPM_VERSION=$(npm view less dist-tags.alpha 2>/dev/null || echo "")
else
NPM_VERSION=$(npm view less version 2>/dev/null || echo "")
fi
node .release-scripts/release-metadata.js validate-title-sync "$RELEASE_BASE" "$VERSION" "$PREVIOUS_VERSION" "$NPM_VERSION"
TITLE=$(node .release-scripts/release-metadata.js title "$RELEASE_BASE" "$VERSION")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "previous_version=$PREVIOUS_VERSION" >> "$GITHUB_OUTPUT"
echo "title=$TITLE" >> "$GITHUB_OUTPUT"
- 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: Sync release files to title version
env:
RELEASE_BASE: ${{ github.event.pull_request.base.ref }}
VERSION: ${{ steps.title-version.outputs.version }}
PREVIOUS_VERSION: ${{ steps.title-version.outputs.previous_version }}
TITLE: ${{ steps.title-version.outputs.title }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
node .release-scripts/release-metadata.js sync-files "$RELEASE_BASE" "$VERSION" "$PREVIOUS_VERSION"
git add package.json packages/*/package.json
if [ -f CHANGELOG.md ]; then
git add CHANGELOG.md
fi
if git diff --cached --quiet; then
echo "Release files already match v${VERSION}"
exit 0
fi
git commit -m "$TITLE"
git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:${HEAD_REF}"