From b5813825be6f80821a811e2752cc9ce8c056a66a Mon Sep 17 00:00:00 2001 From: mkorwel Date: Wed, 10 Sep 2025 11:00:20 -0700 Subject: [PATCH 01/13] feat(release): automate patch creation and release process This commit introduces a comprehensive automation for the patch release process. It includes: 1. A new script, `scripts/create-patch-pr.js`, to handle the git logic of cherry-picking and creating patch PRs. 2. A new GitHub workflow, `create-patch-pr.yml`, for maintainers to trigger the process. 3. A new GitHub workflow, `trigger-patch-release.yml`, to automatically release the patch upon PR merge. 4. Updated documentation in `docs/releases.md` to reflect the new, streamlined process. --- .github/workflows/create-patch-pr.yml | 53 ++++++++++ .github/workflows/trigger-patch-release.yml | 28 +++++ docs/releases.md | 55 ++++------ scripts/create-patch-pr.js | 107 ++++++++++++++++++++ 4 files changed, 209 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/create-patch-pr.yml create mode 100644 .github/workflows/trigger-patch-release.yml create mode 100644 scripts/create-patch-pr.js diff --git a/.github/workflows/create-patch-pr.yml b/.github/workflows/create-patch-pr.yml new file mode 100644 index 00000000000..36a55c85c61 --- /dev/null +++ b/.github/workflows/create-patch-pr.yml @@ -0,0 +1,53 @@ +name: 'Create Patch PR' + +on: + workflow_dispatch: + inputs: + commit: + description: 'The commit SHA to cherry-pick for the patch.' + required: true + type: 'string' + channel: + description: 'The release channel to patch.' + required: true + type: 'choice' + options: + - 'stable' + - 'preview' + +jobs: + create-patch: + runs-on: 'ubuntu-latest' + permissions: + contents: 'write' + pull-requests: 'write' + steps: + - name: 'Checkout' + uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 + with: + fetch-depth: 0 + + - name: 'Setup Node.js' + uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: 'Install Dependencies' + run: 'npm ci' + + - name: 'Configure Git User' + run: |- + git config user.name "gemini-cli-robot" + git config user.email "gemini-cli-robot@google.com" + + - name: 'Create Patch for Stable' + if: "github.event.inputs.channel == 'stable'" + env: + GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + run: 'node scripts/create-patch-pr.js --commit=${{ github.event.inputs.commit }} --channel=stable' + + - name: 'Create Patch for Preview' + env: + GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + run: 'node scripts/create-patch-pr.js --commit=${{ github.event.inputs.commit }} --channel=${{ github.event.inputs.channel }}' diff --git a/.github/workflows/trigger-patch-release.yml b/.github/workflows/trigger-patch-release.yml new file mode 100644 index 00000000000..2ba34cc8040 --- /dev/null +++ b/.github/workflows/trigger-patch-release.yml @@ -0,0 +1,28 @@ +name: 'Trigger Patch Release' + +on: + pull_request: + types: + - 'closed' + +jobs: + trigger-patch-release: + if: "github.event.pull_request.merged == true && startsWith(github.head_ref, 'hotfix/')" + runs-on: 'ubuntu-latest' + steps: + - name: 'Trigger Patch Release' + uses: 'actions/github-script@v6' + with: + script: | + const ref = context.payload.pull_request.base.ref; + const channel = ref.includes('preview') ? 'preview' : 'stable'; + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'patch-release.yml', + ref: ref, + inputs: { + type: channel, + dry_run: 'false' + } + }) diff --git a/docs/releases.md b/docs/releases.md index 62c5d59d871..2b91b70bced 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -58,50 +58,37 @@ After one week (On the following Tuesday) with all signals a go, we will manuall ## Patching Releases -If a critical bug needs to be fixed before the next scheduled release, follow this process to create a patch. +If a critical bug that is already fixed on `main` needs to be patched on a `stable` or `preview` release, the process is now highly automated. -### 1. Create a Hotfix Branch +### 1. Create the Patch Pull Request -First, create a new branch for your fix. The source for this branch depends on whether you are patching a stable or a preview release. +Navigate to the **Actions** tab and run the **Create Patch PR** workflow. -- **For a stable release patch:** - Create a branch from the Git tag of the version you need to patch. Tag names are formatted as `vx.y.z`. +- **Commit**: The full SHA of the commit on `main` that you want to cherry-pick. +- **Channel**: The channel you want to patch (`stable` or `preview`). - ```bash - # Example: Create a hotfix branch for v0.2.0 - git checkout v0.2.0 -b hotfix/issue-123-fix-for-v0.2.0 - ``` +This workflow will automatically: +1. Find the latest release tag for the channel. +2. Create a release branch from that tag if one doesn't exist (e.g., `release/v0.5.1`). +3. Create a new hotfix branch from the release branch. +4. Cherry-pick your specified commit into the hotfix branch. +5. Create a pull request from the hotfix branch back to the release branch. -- **For a preview release patch:** - Create a branch from the existing preview release branch, which is formatted as `release/vx.y.z-preview.n`. +**Important:** If you select `stable`, the workflow will run twice, creating one PR for the `stable` channel and a second PR for the `preview` channel. - ```bash - # Example: Create a hotfix branch for a preview release - git checkout release/v0.2.0-preview.0 && git checkout -b hotfix/issue-456-fix-for-preview - ``` +### 2. Review and Merge -### 2. Implement the Fix +Review the automatically created pull request(s) to ensure the cherry-pick was successful and the changes are correct. Once approved, merge the pull request. -In your new hotfix branch, either create a new commit with the fix or cherry-pick an existing commit from the `main` branch. Merge your changes into the source of the hotfix branch (ex. https://github.com/google-gemini/gemini-cli/pull/6850). +### 3. Automatic Release -### 3. Perform the Release +Upon merging the pull request, a final workflow is automatically triggered. It will: +1. Run the `patch-release` workflow. +2. Build and test the patched code. +3. Publish the new patch version to npm. +4. Create a new GitHub release with the patch notes. -Follow the manual release process using the "Release" GitHub Actions workflow. - -- **Version**: For stable patches, increment the patch version (e.g., `v0.2.0` -> `v0.2.1`). For preview patches, increment the preview number (e.g., `v0.2.0-preview.0` -> `v0.2.0-preview.1`). -- **Ref**: Use your source branch as the reference (ex. `release/v0.2.0-preview.0`) - -![How to run a release](assets/release_patch.png) - -### 4. Update Versions - -After the hotfix is released, merge the changes back to the appropriate branch. - -- **For a stable release hotfix:** - Open a pull request to merge the release branch (e.g., `release/0.2.1`) back into `main`. This keeps the version number in `main` up to date. - -- **For a preview release hotfix:** - Open a pull request to merge the new preview release branch (e.g., `release/v0.2.0-preview.1`) back into the existing preview release branch (`release/v0.2.0-preview.0`) (ex. https://github.com/google-gemini/gemini-cli/pull/6868) +This fully automated process ensures that patches are created and released consistently and reliably. ## Release Schedule diff --git a/scripts/create-patch-pr.js b/scripts/create-patch-pr.js new file mode 100644 index 00000000000..e1dfa1f0953 --- /dev/null +++ b/scripts/create-patch-pr.js @@ -0,0 +1,107 @@ +#!/usr/bin/env node + +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { execSync } from 'node:child_process'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; + +async function main() { + const argv = await yargs(hideBin(process.argv)) + .option('commit', { + alias: 'c', + description: 'The commit SHA to cherry-pick for the patch.', + type: 'string', + demandOption: true, + }) + .option('channel', { + alias: 'h', + description: 'The release channel to patch.', + choices: ['stable', 'preview'], + demandOption: true, + }) + .help() + .alias('help', 'h') + .argv; + + console.log(`Starting patch process for commit: ${argv.commit}`); + console.log(`Targeting channel: ${argv.channel}`); + + const latestTag = getLatestTag(argv.channel); + console.log(`Found latest tag for ${argv.channel}: ${latestTag}`); + + const releaseBranch = `release/${latestTag}`; + const hotfixBranch = `hotfix/${latestTag}/cherry-pick-${argv.commit.substring(0, 7)}`; + + // Create the release branch from the tag if it doesn't exist. + if (!branchExists(releaseBranch)) { + console.log(`Release branch ${releaseBranch} does not exist. Creating it from tag ${latestTag}...`); + run(`git checkout -b ${releaseBranch} ${latestTag}`); + run(`git push origin ${releaseBranch}`); + } else { + console.log(`Release branch ${releaseBranch} already exists.`); + } + + // Create the hotfix branch from the release branch. + console.log(`Creating hotfix branch ${hotfixBranch} from ${releaseBranch}...`); + run(`git checkout -b ${hotfixBranch} ${releaseBranch}`); + + // Cherry-pick the commit. + console.log(`Cherry-picking commit ${argv.commit} into ${hotfixBranch}...`); + run(`git cherry-pick ${argv.commit}`); + + // Push the hotfix branch. + console.log(`Pushing hotfix branch ${hotfixBranch} to origin...`); + run(`git push --set-upstream origin ${hotfixBranch}`); + + // Create the pull request. + console.log(`Creating pull request from ${hotfixBranch} to ${releaseBranch}...`); + const prTitle = `fix(patch): cherry-pick ${argv.commit.substring(0, 7)} to ${releaseBranch}`; + const prBody = `This PR automatically cherry-picks commit ${argv.commit} to patch the ${argv.channel} release.`; + run(`gh pr create --base ${releaseBranch} --head ${hotfixBranch} --title "${prTitle}" --body "${prBody}"`); + + console.log('Patch process completed successfully!'); +} + +function run(command) { + console.log(`> ${command}`); + try { + return execSync(command).toString().trim(); + } catch (err) { + console.error(`Command failed: ${command}`); + throw err; + } +} + +function branchExists(branchName) { + try { + execSync(`git rev-parse --verify ${branchName}`); + return true; + } catch (e) { + return false; + } +} + +function getLatestTag(channel) { + console.log(`Fetching latest tag for channel: ${channel}...`); + const pattern = + channel === 'stable' + ? "'(contains(\"nightly\") or contains(\"preview\")) | not'" + : "'(contains(\"preview\"))'"; + const command = `gh release list --limit 1 --json tagName | jq -r '[.[] | select(.tagName | ${pattern})] | .[0].tagName'`; + try { + return execSync(command).toString().trim(); + } catch (err) { + console.error(`Failed to get latest tag for channel: ${channel}`); + throw err; + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); From c2b95250c660ced32671e577831832da53f17f03 Mon Sep 17 00:00:00 2001 From: mkorwel Date: Wed, 10 Sep 2025 11:01:50 -0700 Subject: [PATCH 02/13] feat(release): allow triggering patch process from comments This commit introduces a new GitHub Actions workflow, `patch-from-comment.yml`, which allows maintainers to trigger the patch creation process by commenting on a pull request. The documentation in `docs/releases.md` has been updated to reflect this new, convenient workflow. --- .github/workflows/patch-from-comment.yml | 36 ++++++++++++++++++++++++ docs/releases.md | 15 ++++++++++ 2 files changed, 51 insertions(+) create mode 100644 .github/workflows/patch-from-comment.yml diff --git a/.github/workflows/patch-from-comment.yml b/.github/workflows/patch-from-comment.yml new file mode 100644 index 00000000000..4705661bb2e --- /dev/null +++ b/.github/workflows/patch-from-comment.yml @@ -0,0 +1,36 @@ +name: 'Patch from Comment' + +on: + issue_comment: + types: [created] + +jobs: + slash-command: + runs-on: ubuntu-latest + steps: + - name: 'Slash Command Dispatch' + uses: peter-evans/slash-command-dispatch@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commands: patch + permission: write + issue-type: pull-request + static-args: | + commit=${{ github.event.comment.html_url }} + + - name: 'Dispatch Patch Workflow' + if: steps.slash-command-dispatch.outputs.dispatched == 'true' + uses: actions/github-script@v6 + with: + script: | + const args = JSON.parse('${{ steps.slash-command-dispatch.outputs.command-arguments }}'); + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'create-patch-pr.yml', + ref: 'main', + inputs: { + commit: args.commit, + channel: args.channel + } + }) diff --git a/docs/releases.md b/docs/releases.md index 2b91b70bced..98ba76f9f27 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -62,6 +62,21 @@ If a critical bug that is already fixed on `main` needs to be patched on a `stab ### 1. Create the Patch Pull Request +There are two ways to create a patch pull request: + +**Option A: From a GitHub Comment (Recommended)** + +On any pull request, a maintainer can add a comment with the following format: + +`/patch ` + +- **channel**: `stable` or `preview` +- **commit_sha**: The full SHA of the commit on `main` to cherry-pick. + +Example: `/patch stable 12345abcdef...` + +**Option B: Manually Triggering the Workflow** + Navigate to the **Actions** tab and run the **Create Patch PR** workflow. - **Commit**: The full SHA of the commit on `main` that you want to cherry-pick. From 986af5c310014a948c065d4279b5c14e16d4c8fc Mon Sep 17 00:00:00 2001 From: mkorwel Date: Wed, 10 Sep 2025 11:06:46 -0700 Subject: [PATCH 03/13] docs(release): add note about branch protection This commit adds a security note to the `releases.md` file, clarifying that the `release/*` branches are protected and require a review before merging. --- docs/releases.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/releases.md b/docs/releases.md index 98ba76f9f27..12bcc67ad7f 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -95,6 +95,8 @@ This workflow will automatically: Review the automatically created pull request(s) to ensure the cherry-pick was successful and the changes are correct. Once approved, merge the pull request. +**Security Note:** The `release/*` branches are protected by branch protection rules. A pull request to one of these branches requires at least one review from a code owner before it can be merged. This ensures that no unauthorized code is released. + ### 3. Automatic Release Upon merging the pull request, a final workflow is automatically triggered. It will: From 0d49d5b30785a2747e0a7855f8a12db16486043b Mon Sep 17 00:00:00 2001 From: mkorwel Date: Wed, 10 Sep 2025 11:10:52 -0700 Subject: [PATCH 04/13] fix(security): pin slash-command-dispatch action to commit SHA This commit improves the security and reproducibility of the `patch-from-comment.yml` workflow by pinning the `peter-evans/slash-command-dispatch` action to a specific commit SHA (`40877f718dce0101edfc7aea2b3800cc192f9ed5`) instead of the floating `v2` tag. --- .github/workflows/patch-from-comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/patch-from-comment.yml b/.github/workflows/patch-from-comment.yml index 4705661bb2e..98938729b1f 100644 --- a/.github/workflows/patch-from-comment.yml +++ b/.github/workflows/patch-from-comment.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Slash Command Dispatch' - uses: peter-evans/slash-command-dispatch@v2 + uses: peter-evans/slash-command-dispatch@40877f718dce0101edfc7aea2b3800cc192f9ed5 with: token: ${{ secrets.GITHUB_TOKEN }} commands: patch From bab2378b96299a1737835d8e9b791734cdbc2f13 Mon Sep 17 00:00:00 2001 From: mkorwel Date: Wed, 10 Sep 2025 11:13:46 -0700 Subject: [PATCH 05/13] feat(release): improve patch command DX and security This commit enhances the comment-based patch workflow by: 1. Automatically detecting the merge commit SHA from the pull request, so the user no longer needs to provide it. 2. Adding a safety check to ensure the command only runs on merged pull requests. 3. Pinning the `peter-evans/slash-command-dispatch` action to a specific commit SHA for improved security. 4. Updating the documentation to reflect the new, simpler `/patch ` command. --- .github/workflows/patch-from-comment.yml | 31 ++++++++++++++++++------ docs/releases.md | 9 ++++--- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/.github/workflows/patch-from-comment.yml b/.github/workflows/patch-from-comment.yml index 98938729b1f..7eac9c2fcb8 100644 --- a/.github/workflows/patch-from-comment.yml +++ b/.github/workflows/patch-from-comment.yml @@ -9,28 +9,45 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Slash Command Dispatch' + id: slash_command uses: peter-evans/slash-command-dispatch@40877f718dce0101edfc7aea2b3800cc192f9ed5 with: token: ${{ secrets.GITHUB_TOKEN }} commands: patch permission: write issue-type: pull-request - static-args: | - commit=${{ github.event.comment.html_url }} - - - name: 'Dispatch Patch Workflow' - if: steps.slash-command-dispatch.outputs.dispatched == 'true' + + - name: 'Get PR Status' + id: pr_status + if: steps.slash_command.outputs.dispatched == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr view ${{ github.event.issue.number }} --json mergeCommit,state > pr_status.json + echo "MERGE_COMMIT_SHA=$(jq -r .mergeCommit.oid pr_status.json)" >> $GITHUB_OUTPUT + echo "STATE=$(jq -r .state pr_status.json)" >> $GITHUB_OUTPUT + + - name: 'Dispatch if Merged' + if: steps.pr_status.outputs.STATE == 'MERGED' uses: actions/github-script@v6 with: script: | - const args = JSON.parse('${{ steps.slash-command-dispatch.outputs.command-arguments }}'); + const args = JSON.parse('${{ steps.slash_command.outputs.command-arguments }}'); github.rest.actions.createWorkflowDispatch({ owner: context.repo.owner, repo: context.repo.repo, workflow_id: 'create-patch-pr.yml', ref: 'main', inputs: { - commit: args.commit, + commit: '${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}', channel: args.channel } }) + + - name: 'Comment on Failure' + if: steps.pr_status.outputs.STATE != 'MERGED' + uses: peter-evans/create-or-update-comment@v2 + with: + issue-number: ${{ github.event.issue.number }} + body: | + :x: The `/patch` command failed. This pull request must be merged before a patch can be created. diff --git a/docs/releases.md b/docs/releases.md index 12bcc67ad7f..7d3cc17810c 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -66,14 +66,15 @@ There are two ways to create a patch pull request: **Option A: From a GitHub Comment (Recommended)** -On any pull request, a maintainer can add a comment with the following format: +After a pull request has been merged, a maintainer can add a comment on that same PR with the following format: -`/patch ` +`/patch ` - **channel**: `stable` or `preview` -- **commit_sha**: The full SHA of the commit on `main` to cherry-pick. -Example: `/patch stable 12345abcdef...` +Example: `/patch stable` + +The workflow will automatically find the merge commit SHA and begin the patch process. If the PR is not yet merged, it will post a comment indicating the failure. **Option B: Manually Triggering the Workflow** From 3c18d56b6517a3d741b336b24c2d8494ec543630 Mon Sep 17 00:00:00 2001 From: mkorwel Date: Wed, 10 Sep 2025 11:18:10 -0700 Subject: [PATCH 06/13] fix(security): pin actions in new workflows This commit pins the external GitHub Actions used in the new patch process workflows (`create-patch-pr.yml`, `patch-from-comment.yml`, and `trigger-patch-release.yml`) to their specific commit SHAs. This improves the security and reproducibility of these workflows. --- .github/workflows/patch-from-comment.yml | 4 ++-- .github/workflows/trigger-patch-release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/patch-from-comment.yml b/.github/workflows/patch-from-comment.yml index 7eac9c2fcb8..139695ca4c9 100644 --- a/.github/workflows/patch-from-comment.yml +++ b/.github/workflows/patch-from-comment.yml @@ -29,7 +29,7 @@ jobs: - name: 'Dispatch if Merged' if: steps.pr_status.outputs.STATE == 'MERGED' - uses: actions/github-script@v6 + uses: actions/github-script@00f12e3e20659f42342b1c0226afda7f7c042325 with: script: | const args = JSON.parse('${{ steps.slash_command.outputs.command-arguments }}'); @@ -46,7 +46,7 @@ jobs: - name: 'Comment on Failure' if: steps.pr_status.outputs.STATE != 'MERGED' - uses: peter-evans/create-or-update-comment@v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d with: issue-number: ${{ github.event.issue.number }} body: | diff --git a/.github/workflows/trigger-patch-release.yml b/.github/workflows/trigger-patch-release.yml index 2ba34cc8040..8229ba4e2ec 100644 --- a/.github/workflows/trigger-patch-release.yml +++ b/.github/workflows/trigger-patch-release.yml @@ -11,7 +11,7 @@ jobs: runs-on: 'ubuntu-latest' steps: - name: 'Trigger Patch Release' - uses: 'actions/github-script@v6' + uses: 'actions/github-script@00f12e3e20659f42342b1c0226afda7f7c042325' with: script: | const ref = context.payload.pull_request.base.ref; From e5332cd36908fd847351b6bb9798dd270f85f15b Mon Sep 17 00:00:00 2001 From: mkorwel Date: Wed, 10 Sep 2025 11:22:11 -0700 Subject: [PATCH 07/13] feat(release): add dry-run capability to patch command This commit adds a `--dry-run` option to the comment-based patch workflow. This allows maintainers to test the entire patch creation and release process without actually publishing any packages. The `dry-run` state is plumbed through all the relevant workflows and scripts, and the documentation has been updated to reflect the new option. --- .github/workflows/create-patch-pr.yml | 9 +++- .github/workflows/patch-from-comment.yml | 5 ++- .github/workflows/trigger-patch-release.yml | 4 +- docs/releases.md | 5 ++- scripts/create-patch-pr.js | 46 ++++++++++++++------- 5 files changed, 48 insertions(+), 21 deletions(-) diff --git a/.github/workflows/create-patch-pr.yml b/.github/workflows/create-patch-pr.yml index 36a55c85c61..2ec6aed3eb1 100644 --- a/.github/workflows/create-patch-pr.yml +++ b/.github/workflows/create-patch-pr.yml @@ -14,6 +14,11 @@ on: options: - 'stable' - 'preview' + dry_run: + description: 'Whether to run in dry-run mode.' + required: false + type: 'boolean' + default: false jobs: create-patch: @@ -45,9 +50,9 @@ jobs: if: "github.event.inputs.channel == 'stable'" env: GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' - run: 'node scripts/create-patch-pr.js --commit=${{ github.event.inputs.commit }} --channel=stable' + run: 'node scripts/create-patch-pr.js --commit=${{ github.event.inputs.commit }} --channel=stable --dry-run=${{ github.event.inputs.dry_run }}' - name: 'Create Patch for Preview' env: GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' - run: 'node scripts/create-patch-pr.js --commit=${{ github.event.inputs.commit }} --channel=${{ github.event.inputs.channel }}' + run: 'node scripts/create-patch-pr.js --commit=${{ github.event.inputs.commit }} --channel=${{ github.event.inputs.channel }} --dry-run=${{ github.event.inputs.dry_run }}' diff --git a/.github/workflows/patch-from-comment.yml b/.github/workflows/patch-from-comment.yml index 139695ca4c9..624b7301d40 100644 --- a/.github/workflows/patch-from-comment.yml +++ b/.github/workflows/patch-from-comment.yml @@ -16,6 +16,8 @@ jobs: commands: patch permission: write issue-type: pull-request + static-args: | + dry_run=false - name: 'Get PR Status' id: pr_status @@ -40,7 +42,8 @@ jobs: ref: 'main', inputs: { commit: '${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}', - channel: args.channel + channel: args.channel, + dry_run: args.dry_run } }) diff --git a/.github/workflows/trigger-patch-release.yml b/.github/workflows/trigger-patch-release.yml index 8229ba4e2ec..4270111bbd9 100644 --- a/.github/workflows/trigger-patch-release.yml +++ b/.github/workflows/trigger-patch-release.yml @@ -14,6 +14,8 @@ jobs: uses: 'actions/github-script@00f12e3e20659f42342b1c0226afda7f7c042325' with: script: | + const body = context.payload.pull_request.body; + const isDryRun = body.includes('[DRY RUN]'); const ref = context.payload.pull_request.base.ref; const channel = ref.includes('preview') ? 'preview' : 'stable'; github.rest.actions.createWorkflowDispatch({ @@ -23,6 +25,6 @@ jobs: ref: ref, inputs: { type: channel, - dry_run: 'false' + dry_run: isDryRun.toString() } }) diff --git a/docs/releases.md b/docs/releases.md index 7d3cc17810c..2322bed6f12 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -68,11 +68,12 @@ There are two ways to create a patch pull request: After a pull request has been merged, a maintainer can add a comment on that same PR with the following format: -`/patch ` +`/patch [--dry-run]` - **channel**: `stable` or `preview` +- **--dry-run** (optional): If included, the workflow will run in dry-run mode. This will create the PR with "[DRY RUN]" in the title, and merging it will trigger a dry run of the final release, so nothing is actually published. -Example: `/patch stable` +Example: `/patch stable --dry-run` The workflow will automatically find the merge commit SHA and begin the patch process. If the PR is not yet merged, it will post a comment indicating the failure. diff --git a/scripts/create-patch-pr.js b/scripts/create-patch-pr.js index e1dfa1f0953..8463e89a9b9 100644 --- a/scripts/create-patch-pr.js +++ b/scripts/create-patch-pr.js @@ -24,51 +24,67 @@ async function main() { choices: ['stable', 'preview'], demandOption: true, }) + .option('dry-run', { + description: 'Whether to run in dry-run mode.', + type: 'boolean', + default: false, + }) .help() .alias('help', 'h') .argv; - console.log(`Starting patch process for commit: ${argv.commit}`); - console.log(`Targeting channel: ${argv.channel}`); + const { commit, channel, dryRun } = argv; + + console.log(`Starting patch process for commit: ${commit}`); + console.log(`Targeting channel: ${channel}`); + if (dryRun) { + console.log('Running in dry-run mode.'); + } - const latestTag = getLatestTag(argv.channel); - console.log(`Found latest tag for ${argv.channel}: ${latestTag}`); + const latestTag = getLatestTag(channel); + console.log(`Found latest tag for ${channel}: ${latestTag}`); const releaseBranch = `release/${latestTag}`; - const hotfixBranch = `hotfix/${latestTag}/cherry-pick-${argv.commit.substring(0, 7)}`; + const hotfixBranch = `hotfix/${latestTag}/cherry-pick-${commit.substring(0, 7)}`; // Create the release branch from the tag if it doesn't exist. if (!branchExists(releaseBranch)) { console.log(`Release branch ${releaseBranch} does not exist. Creating it from tag ${latestTag}...`); - run(`git checkout -b ${releaseBranch} ${latestTag}`); - run(`git push origin ${releaseBranch}`); + run(`git checkout -b ${releaseBranch} ${latestTag}`, dryRun); + run(`git push origin ${releaseBranch}`, dryRun); } else { console.log(`Release branch ${releaseBranch} already exists.`); } // Create the hotfix branch from the release branch. console.log(`Creating hotfix branch ${hotfixBranch} from ${releaseBranch}...`); - run(`git checkout -b ${hotfixBranch} ${releaseBranch}`); + run(`git checkout -b ${hotfixBranch} ${releaseBranch}`, dryRun); // Cherry-pick the commit. - console.log(`Cherry-picking commit ${argv.commit} into ${hotfixBranch}...`); - run(`git cherry-pick ${argv.commit}`); + console.log(`Cherry-picking commit ${commit} into ${hotfixBranch}...`); + run(`git cherry-pick ${commit}`, dryRun); // Push the hotfix branch. console.log(`Pushing hotfix branch ${hotfixBranch} to origin...`); - run(`git push --set-upstream origin ${hotfixBranch}`); + run(`git push --set-upstream origin ${hotfixBranch}`, dryRun); // Create the pull request. console.log(`Creating pull request from ${hotfixBranch} to ${releaseBranch}...`); - const prTitle = `fix(patch): cherry-pick ${argv.commit.substring(0, 7)} to ${releaseBranch}`; - const prBody = `This PR automatically cherry-picks commit ${argv.commit} to patch the ${argv.channel} release.`; - run(`gh pr create --base ${releaseBranch} --head ${hotfixBranch} --title "${prTitle}" --body "${prBody}"`); + const prTitle = `fix(patch): cherry-pick ${commit.substring(0, 7)} to ${releaseBranch}`; + let prBody = `This PR automatically cherry-picks commit ${commit} to patch the ${channel} release.`; + if (dryRun) { + prBody += '\n\n**[DRY RUN]**'; + } + run(`gh pr create --base ${releaseBranch} --head ${hotfixBranch} --title "${prTitle}" --body "${prBody}"`, dryRun); console.log('Patch process completed successfully!'); } -function run(command) { +function run(command, dryRun = false) { console.log(`> ${command}`); + if (dryRun) { + return; + } try { return execSync(command).toString().trim(); } catch (err) { From 1349759a600abb79518648603b3a29e459fc88fc Mon Sep 17 00:00:00 2001 From: mkorwel Date: Wed, 10 Sep 2025 11:24:24 -0700 Subject: [PATCH 08/13] fix(release): address feedback on patch creation script This commit addresses feedback on the `create-patch-pr.js` script by: 1. Fetching all tags and checking the remote for branch existence to improve reliability in CI. 2. Increasing the release fetch limit to 30 to ensure the latest stable tag is found. 3. Resolving the `-h` alias conflict between `channel` and `help`. 4. Correctly creating the hotfix branch from the remote release branch. --- scripts/create-patch-pr.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/create-patch-pr.js b/scripts/create-patch-pr.js index 8463e89a9b9..95a7ea58aa3 100644 --- a/scripts/create-patch-pr.js +++ b/scripts/create-patch-pr.js @@ -19,7 +19,7 @@ async function main() { demandOption: true, }) .option('channel', { - alias: 'h', + alias: 'ch', description: 'The release channel to patch.', choices: ['stable', 'preview'], demandOption: true, @@ -41,6 +41,8 @@ async function main() { console.log('Running in dry-run mode.'); } + run('git fetch --all --tags --prune', dryRun); + const latestTag = getLatestTag(channel); console.log(`Found latest tag for ${channel}: ${latestTag}`); @@ -58,7 +60,7 @@ async function main() { // Create the hotfix branch from the release branch. console.log(`Creating hotfix branch ${hotfixBranch} from ${releaseBranch}...`); - run(`git checkout -b ${hotfixBranch} ${releaseBranch}`, dryRun); + run(`git checkout -b ${hotfixBranch} origin/${releaseBranch}`, dryRun); // Cherry-pick the commit. console.log(`Cherry-picking commit ${commit} into ${hotfixBranch}...`); @@ -95,7 +97,7 @@ function run(command, dryRun = false) { function branchExists(branchName) { try { - execSync(`git rev-parse --verify ${branchName}`); + execSync(`git ls-remote --exit-code --heads origin ${branchName}`); return true; } catch (e) { return false; @@ -108,7 +110,7 @@ function getLatestTag(channel) { channel === 'stable' ? "'(contains(\"nightly\") or contains(\"preview\")) | not'" : "'(contains(\"preview\"))'"; - const command = `gh release list --limit 1 --json tagName | jq -r '[.[] | select(.tagName | ${pattern})] | .[0].tagName'`; + const command = `gh release list --limit 30 --json tagName | jq -r '[.[] | select(.tagName | ${pattern})] | .[0].tagName'`; try { return execSync(command).toString().trim(); } catch (err) { From 17a1b078066dfb06095361e07d63078860f4e1b4 Mon Sep 17 00:00:00 2001 From: mkorwel Date: Wed, 10 Sep 2025 11:28:46 -0700 Subject: [PATCH 09/13] fix(lint): address linting and formatting issues --- docs/releases.md | 2 ++ scripts/create-patch-pr.js | 26 +++++++++++++++++--------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 2322bed6f12..78c2b0845f6 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -85,6 +85,7 @@ Navigate to the **Actions** tab and run the **Create Patch PR** workflow. - **Channel**: The channel you want to patch (`stable` or `preview`). This workflow will automatically: + 1. Find the latest release tag for the channel. 2. Create a release branch from that tag if one doesn't exist (e.g., `release/v0.5.1`). 3. Create a new hotfix branch from the release branch. @@ -102,6 +103,7 @@ Review the automatically created pull request(s) to ensure the cherry-pick was s ### 3. Automatic Release Upon merging the pull request, a final workflow is automatically triggered. It will: + 1. Run the `patch-release` workflow. 2. Build and test the patched code. 3. Publish the new patch version to npm. diff --git a/scripts/create-patch-pr.js b/scripts/create-patch-pr.js index 95a7ea58aa3..7804c2a9f0a 100644 --- a/scripts/create-patch-pr.js +++ b/scripts/create-patch-pr.js @@ -30,8 +30,7 @@ async function main() { default: false, }) .help() - .alias('help', 'h') - .argv; + .alias('help', 'h').argv; const { commit, channel, dryRun } = argv; @@ -51,7 +50,9 @@ async function main() { // Create the release branch from the tag if it doesn't exist. if (!branchExists(releaseBranch)) { - console.log(`Release branch ${releaseBranch} does not exist. Creating it from tag ${latestTag}...`); + console.log( + `Release branch ${releaseBranch} does not exist. Creating it from tag ${latestTag}...`, + ); run(`git checkout -b ${releaseBranch} ${latestTag}`, dryRun); run(`git push origin ${releaseBranch}`, dryRun); } else { @@ -59,7 +60,9 @@ async function main() { } // Create the hotfix branch from the release branch. - console.log(`Creating hotfix branch ${hotfixBranch} from ${releaseBranch}...`); + console.log( + `Creating hotfix branch ${hotfixBranch} from ${releaseBranch}...`, + ); run(`git checkout -b ${hotfixBranch} origin/${releaseBranch}`, dryRun); // Cherry-pick the commit. @@ -71,13 +74,18 @@ async function main() { run(`git push --set-upstream origin ${hotfixBranch}`, dryRun); // Create the pull request. - console.log(`Creating pull request from ${hotfixBranch} to ${releaseBranch}...`); + console.log( + `Creating pull request from ${hotfixBranch} to ${releaseBranch}...`, + ); const prTitle = `fix(patch): cherry-pick ${commit.substring(0, 7)} to ${releaseBranch}`; let prBody = `This PR automatically cherry-picks commit ${commit} to patch the ${channel} release.`; if (dryRun) { prBody += '\n\n**[DRY RUN]**'; } - run(`gh pr create --base ${releaseBranch} --head ${hotfixBranch} --title "${prTitle}" --body "${prBody}"`, dryRun); + run( + `gh pr create --base ${releaseBranch} --head ${hotfixBranch} --title "${prTitle}" --body "${prBody}"`, + dryRun, + ); console.log('Patch process completed successfully!'); } @@ -99,7 +107,7 @@ function branchExists(branchName) { try { execSync(`git ls-remote --exit-code --heads origin ${branchName}`); return true; - } catch (e) { + } catch (_e) { return false; } } @@ -108,8 +116,8 @@ function getLatestTag(channel) { console.log(`Fetching latest tag for channel: ${channel}...`); const pattern = channel === 'stable' - ? "'(contains(\"nightly\") or contains(\"preview\")) | not'" - : "'(contains(\"preview\"))'"; + ? '\'(contains("nightly") or contains("preview")) | not\'' + : '\'(contains("preview"))\''; const command = `gh release list --limit 30 --json tagName | jq -r '[.[] | select(.tagName | ${pattern})] | .[0].tagName'`; try { return execSync(command).toString().trim(); From 30ba68dac67af95d25d5baaf75b06e6827635fd6 Mon Sep 17 00:00:00 2001 From: mkorwel Date: Wed, 10 Sep 2025 11:32:41 -0700 Subject: [PATCH 10/13] fix(lint): correct yaml linting errors --- .github/workflows/patch-from-comment.yml | 26 ++++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/patch-from-comment.yml b/.github/workflows/patch-from-comment.yml index 624b7301d40..1131b7edb3e 100644 --- a/.github/workflows/patch-from-comment.yml +++ b/.github/workflows/patch-from-comment.yml @@ -2,28 +2,28 @@ name: 'Patch from Comment' on: issue_comment: - types: [created] + types: ['created'] jobs: slash-command: - runs-on: ubuntu-latest + runs-on: 'ubuntu-latest' steps: - name: 'Slash Command Dispatch' - id: slash_command - uses: peter-evans/slash-command-dispatch@40877f718dce0101edfc7aea2b3800cc192f9ed5 + id: 'slash_command' + uses: 'peter-evans/slash-command-dispatch@40877f718dce0101edfc7aea2b3800cc192f9ed5' with: - token: ${{ secrets.GITHUB_TOKEN }} - commands: patch - permission: write - issue-type: pull-request + token: '${{ secrets.GITHUB_TOKEN }}' + commands: 'patch' + permission: 'write' + issue-type: 'pull-request' static-args: | dry_run=false - name: 'Get PR Status' - id: pr_status + id: 'pr_status' if: steps.slash_command.outputs.dispatched == 'true' env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' run: | gh pr view ${{ github.event.issue.number }} --json mergeCommit,state > pr_status.json echo "MERGE_COMMIT_SHA=$(jq -r .mergeCommit.oid pr_status.json)" >> $GITHUB_OUTPUT @@ -31,7 +31,7 @@ jobs: - name: 'Dispatch if Merged' if: steps.pr_status.outputs.STATE == 'MERGED' - uses: actions/github-script@00f12e3e20659f42342b1c0226afda7f7c042325 + uses: 'actions/github-script@00f12e3e20659f42342b1c0226afda7f7c042325' with: script: | const args = JSON.parse('${{ steps.slash_command.outputs.command-arguments }}'); @@ -49,8 +49,8 @@ jobs: - name: 'Comment on Failure' if: steps.pr_status.outputs.STATE != 'MERGED' - uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d + uses: 'peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d' with: - issue-number: ${{ github.event.issue.number }} + issue-number: '${{ github.event.issue.number }}' body: | :x: The `/patch` command failed. This pull request must be merged before a patch can be created. From 59a5e0f7a071c90992c58b629a422d21fc510401 Mon Sep 17 00:00:00 2001 From: mkorwel Date: Wed, 10 Sep 2025 11:47:06 -0700 Subject: [PATCH 11/13] fix(lint): correct shellcheck errors in patch-from-comment workflow --- .github/workflows/patch-from-comment.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/patch-from-comment.yml b/.github/workflows/patch-from-comment.yml index 1131b7edb3e..74ef0ad676f 100644 --- a/.github/workflows/patch-from-comment.yml +++ b/.github/workflows/patch-from-comment.yml @@ -25,9 +25,9 @@ jobs: env: GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' run: | - gh pr view ${{ github.event.issue.number }} --json mergeCommit,state > pr_status.json - echo "MERGE_COMMIT_SHA=$(jq -r .mergeCommit.oid pr_status.json)" >> $GITHUB_OUTPUT - echo "STATE=$(jq -r .state pr_status.json)" >> $GITHUB_OUTPUT + gh pr view "${{ github.event.issue.number }}" --json mergeCommit,state > pr_status.json + echo "MERGE_COMMIT_SHA=$(jq -r .mergeCommit.oid pr_status.json)" >> "$GITHUB_OUTPUT" + echo "STATE=$(jq -r .state pr_status.json)" >> "$GITHUB_OUTPUT" - name: 'Dispatch if Merged' if: steps.pr_status.outputs.STATE == 'MERGED' From 3b6ebfb7bd231aa2b01dc1183a3df074f3fec2aa Mon Sep 17 00:00:00 2001 From: mkorwel Date: Wed, 10 Sep 2025 11:49:01 -0700 Subject: [PATCH 12/13] fix(lint): correct all yaml linting errors --- .github/workflows/patch-from-comment.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/patch-from-comment.yml b/.github/workflows/patch-from-comment.yml index 74ef0ad676f..f9d9797c6ca 100644 --- a/.github/workflows/patch-from-comment.yml +++ b/.github/workflows/patch-from-comment.yml @@ -30,7 +30,7 @@ jobs: echo "STATE=$(jq -r .state pr_status.json)" >> "$GITHUB_OUTPUT" - name: 'Dispatch if Merged' - if: steps.pr_status.outputs.STATE == 'MERGED' + if: "steps.pr_status.outputs.STATE == 'MERGED'" uses: 'actions/github-script@00f12e3e20659f42342b1c0226afda7f7c042325' with: script: | @@ -48,7 +48,7 @@ jobs: }) - name: 'Comment on Failure' - if: steps.pr_status.outputs.STATE != 'MERGED' + if: "steps.pr_status.outputs.STATE != 'MERGED'" uses: 'peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d' with: issue-number: '${{ github.event.issue.number }}' From 4c538771fa031a208b0de51ebe5005b7da089922 Mon Sep 17 00:00:00 2001 From: mkorwel Date: Wed, 10 Sep 2025 11:52:14 -0700 Subject: [PATCH 13/13] fix(lint): correct final yaml linting error --- .github/workflows/patch-from-comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/patch-from-comment.yml b/.github/workflows/patch-from-comment.yml index f9d9797c6ca..55065b5b1cd 100644 --- a/.github/workflows/patch-from-comment.yml +++ b/.github/workflows/patch-from-comment.yml @@ -21,7 +21,7 @@ jobs: - name: 'Get PR Status' id: 'pr_status' - if: steps.slash_command.outputs.dispatched == 'true' + if: "steps.slash_command.outputs.dispatched == 'true'" env: GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' run: |