mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
feat(release): automate patch creation and release process (#8202)
This commit is contained in:
58
.github/workflows/create-patch-pr.yml
vendored
Normal file
58
.github/workflows/create-patch-pr.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
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'
|
||||
dry_run:
|
||||
description: 'Whether to run in dry-run mode.'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: false
|
||||
|
||||
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 --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 }} --dry-run=${{ github.event.inputs.dry_run }}'
|
||||
56
.github/workflows/patch-from-comment.yml
vendored
Normal file
56
.github/workflows/patch-from-comment.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: 'Patch from Comment'
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: ['created']
|
||||
|
||||
jobs:
|
||||
slash-command:
|
||||
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: |
|
||||
dry_run=false
|
||||
|
||||
- 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@00f12e3e20659f42342b1c0226afda7f7c042325'
|
||||
with:
|
||||
script: |
|
||||
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: '${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}',
|
||||
channel: args.channel,
|
||||
dry_run: args.dry_run
|
||||
}
|
||||
})
|
||||
|
||||
- name: 'Comment on Failure'
|
||||
if: "steps.pr_status.outputs.STATE != 'MERGED'"
|
||||
uses: 'peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d'
|
||||
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.
|
||||
30
.github/workflows/trigger-patch-release.yml
vendored
Normal file
30
.github/workflows/trigger-patch-release.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
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@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({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'patch-release.yml',
|
||||
ref: ref,
|
||||
inputs: {
|
||||
type: channel,
|
||||
dry_run: isDryRun.toString()
|
||||
}
|
||||
})
|
||||
@@ -58,50 +58,58 @@ 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.
|
||||
There are two ways to create a patch pull request:
|
||||
|
||||
- **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`.
|
||||
**Option A: From a GitHub Comment (Recommended)**
|
||||
|
||||
```bash
|
||||
# Example: Create a hotfix branch for v0.2.0
|
||||
git checkout v0.2.0 -b hotfix/issue-123-fix-for-v0.2.0
|
||||
```
|
||||
After a pull request has been merged, a maintainer can add a comment on that same PR with the following format:
|
||||
|
||||
- **For a preview release patch:**
|
||||
Create a branch from the existing preview release branch, which is formatted as `release/vx.y.z-preview.n`.
|
||||
`/patch <channel> [--dry-run]`
|
||||
|
||||
```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
|
||||
```
|
||||
- **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.
|
||||
|
||||
### 2. Implement the Fix
|
||||
Example: `/patch stable --dry-run`
|
||||
|
||||
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).
|
||||
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.
|
||||
|
||||
### 3. Perform the Release
|
||||
**Option B: Manually Triggering the Workflow**
|
||||
|
||||
Follow the manual release process using the "Release" GitHub Actions workflow.
|
||||
Navigate to the **Actions** tab and run the **Create Patch PR** 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`)
|
||||
- **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`).
|
||||
|
||||

|
||||
This workflow will automatically:
|
||||
|
||||
### 4. Update Versions
|
||||
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.
|
||||
|
||||
After the hotfix is released, merge the changes back to the appropriate branch.
|
||||
**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.
|
||||
|
||||
- **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.
|
||||
### 2. Review and Merge
|
||||
|
||||
- **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)
|
||||
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:
|
||||
|
||||
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.
|
||||
|
||||
This fully automated process ensures that patches are created and released consistently and reliably.
|
||||
|
||||
## Release Schedule
|
||||
|
||||
|
||||
133
scripts/create-patch-pr.js
Normal file
133
scripts/create-patch-pr.js
Normal file
@@ -0,0 +1,133 @@
|
||||
#!/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: 'ch',
|
||||
description: 'The release channel to patch.',
|
||||
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;
|
||||
|
||||
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.');
|
||||
}
|
||||
|
||||
run('git fetch --all --tags --prune', dryRun);
|
||||
|
||||
const latestTag = getLatestTag(channel);
|
||||
console.log(`Found latest tag for ${channel}: ${latestTag}`);
|
||||
|
||||
const releaseBranch = `release/${latestTag}`;
|
||||
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}`, 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} origin/${releaseBranch}`, dryRun);
|
||||
|
||||
// Cherry-pick the 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}`, dryRun);
|
||||
|
||||
// Create the pull request.
|
||||
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,
|
||||
);
|
||||
|
||||
console.log('Patch process completed successfully!');
|
||||
}
|
||||
|
||||
function run(command, dryRun = false) {
|
||||
console.log(`> ${command}`);
|
||||
if (dryRun) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
return execSync(command).toString().trim();
|
||||
} catch (err) {
|
||||
console.error(`Command failed: ${command}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function branchExists(branchName) {
|
||||
try {
|
||||
execSync(`git ls-remote --exit-code --heads origin ${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 30 --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);
|
||||
});
|
||||
Reference in New Issue
Block a user