diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml new file mode 100644 index 000000000..9a3a51854 --- /dev/null +++ b/.github/workflows/draft-release.yml @@ -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." diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml new file mode 100644 index 000000000..d91179baa --- /dev/null +++ b/.github/workflows/version-bump.yml @@ -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 " (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. diff --git a/package.json b/package.json index b82e83328..a9a14942f 100644 --- a/package.json +++ b/package.json @@ -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 .",