Repository navigation
Merge pull request #4527 from matthew-dean/port/master-fixtures #245
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}" |