Skip to content
Merged
54 changes: 54 additions & 0 deletions .github/workflows/draft-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Draft Release

# When a "Release X.Y.Z" PR (opened by the Create Release PR workflow) is
# merged into master, this prepares a DRAFT GitHub Release with the right tag
# and auto-generated notes. It is intentionally left as a draft — a human
# clicks "Publish" to actually ship, which triggers release.yml + executable.yml.
on:
pull_request:
types: [closed]

permissions:
contents: write

jobs:
draft:
# Only for merged PRs whose title starts with "Release " (our bump PRs).
if: >-
github.event.pull_request.merged == true &&
startsWith(github.event.pull_request.title, 'Release ')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}

- name: Create draft release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TARGET_SHA: ${{ github.event.pull_request.merge_commit_sha }}
run: |
set -euo pipefail
VERSION=$(node -p "require('./lerna.json').version")
TAG="v$VERSION"

# Mark betas (versions containing a hyphen) as pre-releases.
PRERELEASE_FLAG=""
if [[ "$VERSION" == *-* ]]; then
PRERELEASE_FLAG="--prerelease"
fi

# Don't recreate if a draft/release for this tag already exists.
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG already exists — skipping."
exit 0
fi

gh release create "$TAG" \
--draft \
--generate-notes \
--title "$TAG" \
--target "$TARGET_SHA" \
$PRERELEASE_FLAG

echo "Created draft release $TAG. Review it and click Publish to ship."
114 changes: 114 additions & 0 deletions .github/workflows/version-bump.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
name: Create Release PR

# Manually triggered from the Actions tab. Pick how to bump; the target version
# is computed from the CURRENT version at run time (nothing is hard-coded). This
# sets every package in lockstep (lerna fixed mode) and opens a "Release X.Y.Z"
# PR that only touches lerna.json + package.json files. Merging that PR and
# publishing a GitHub Release is what actually ships to npm.
on:
workflow_dispatch:
inputs:
bump:
description: 'How to bump (examples are illustrative; the actual resulting version is shown in the run summary):'
required: true
default: "prerelease (e.g. 1.2.3-beta.4 -> 1.2.3-beta.5)"
type: choice
options:
- "prerelease (e.g. 1.2.3-beta.4 -> 1.2.3-beta.5)"
- "patch (e.g. 1.2.3-beta.4 -> 1.2.3, or 1.2.3 -> 1.2.4)"
- "minor (e.g. 1.2.3 -> 1.3.0)"
- "major (e.g. 1.2.3 -> 2.0.0)"
- "preminor (e.g. 1.2.3 -> 1.3.0-beta.0)"
- "premajor (e.g. 1.2.3 -> 2.0.0-beta.0)"

permissions:
contents: write
pull-requests: write

jobs:
release-pr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0

- uses: actions/setup-node@v5
with:
node-version: 24

- run: yarn install --frozen-lockfile

- name: Configure git
run: |
git config user.name "percy-release-bot"
git config user.email "percy-release-bot@users.noreply.github.com"

- name: Bump version
id: bump
env:
BUMP: ${{ inputs.bump }}
run: |
set -euo pipefail

# The dropdown value is "<keyword> (e.g. ...)"; take the first word.
KEYWORD="${BUMP%% *}"

CURRENT=$(node -p "require('./lerna.json').version")

# Compute the target version dynamically from the current version.
# 'beta' is the prerelease identifier for any pre* bump.
TARGET=$(KEYWORD="$KEYWORD" node -e "const s=require('semver'); const t=s.inc('$CURRENT', process.env.KEYWORD, 'beta'); if(!t){process.exit(1)} process.stdout.write(t)")
if [[ -z "$TARGET" ]]; then
echo "::error::Could not compute a target version for bump '$KEYWORD' from '$CURRENT'."
exit 1
fi

yarn lerna version "$TARGET" \
--exact --no-git-tag-version --no-push --force-publish --yes

VERSION=$(node -p "require('./lerna.json').version")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

# A prerelease version (contains a hyphen) ships under the `beta`
# npm dist-tag; a clean semver ships under `latest`.
if [[ "$VERSION" == *-* ]]; then
DIST_TAG=beta
else
DIST_TAG=latest
fi
echo "dist_tag=$DIST_TAG" >> "$GITHUB_OUTPUT"

# Keep each package's publishConfig.tag (the npm dist-tag used by
# `lerna publish from-package`) in step with the version.
DIST_TAG="$DIST_TAG" node -e 'const fs=require("fs");const t=process.env.DIST_TAG;for(const d of fs.readdirSync("packages")){const f="packages/"+d+"/package.json";if(!fs.existsSync(f))continue;const p=JSON.parse(fs.readFileSync(f));if(p.publishConfig&&p.publishConfig.tag!==t){p.publishConfig.tag=t;fs.writeFileSync(f,JSON.stringify(p,null,2)+"\n")}}'

{
echo "### Version bump"
echo ""
echo "Bump: \`$BUMP\`"
echo ""
echo "\`$CURRENT\` → **\`$VERSION\`** (npm dist-tag: \`$DIST_TAG\`)"
} >> "$GITHUB_STEP_SUMMARY"

- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
base: master
branch: release/${{ steps.bump.outputs.version }}
# Only commit version files — never workflow files (GITHUB_TOKEN may
# not push to .github/workflows, and a release PR shouldn't anyway).
add-paths: |
lerna.json
packages/**/package.json
commit-message: "Release ${{ steps.bump.outputs.version }}"
title: "Release ${{ steps.bump.outputs.version }}"
labels: "🧹 maintenance"
body: |
Automated version bump to **`${{ steps.bump.outputs.version }}`**.

- Triggered by @${{ github.actor }} via `workflow_dispatch` (bump: `${{ inputs.bump }}`).
- On publishing a GitHub Release, this will go to npm under dist-tag **`${{ steps.bump.outputs.dist_tag }}`**.

**Next steps:** review & merge, then cut the GitHub Release.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"build": "lerna run build --stream",
"build:watch": "lerna run build --parallel -- --watch",
"build:pack": "mkdir -p ./packs && lerna exec npm pack && mv ./packages/*/*.tgz ./packs",
"bump-version": "lerna version --exact --no-git-tag-version --no-push",
"bump-version": "lerna version --exact --no-git-tag-version --no-push && node -e \"const fs=require('fs');const v=require('./lerna.json').version;const t=v.includes('-')?'beta':'latest';for(const d of fs.readdirSync('packages')){const f='packages/'+d+'/package.json';if(!fs.existsSync(f))continue;const p=JSON.parse(fs.readFileSync(f));if(p.publishConfig&&p.publishConfig.tag!==t){p.publishConfig.tag=t;fs.writeFileSync(f,JSON.stringify(p,null,2)+String.fromCharCode(10))}}\"",
"chromium-revision": "./scripts/chromium-revision.js",
"clean": "git clean -Xdf -e !node_modules -e !**/node_modules/**",
"lint": "eslint --ignore-path .gitignore .",
Expand Down
Loading