mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
feat: simplify patch release workflow (#8196)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Richie Foreman <richie.foreman@gmail.com>
This commit is contained in:
67
.github/actions/publish-release/action.yml
vendored
67
.github/actions/publish-release/action.yml
vendored
@@ -19,9 +19,10 @@ inputs:
|
||||
required: true
|
||||
dry-run:
|
||||
description: 'Whether to run in dry-run mode.'
|
||||
type: 'boolean'
|
||||
required: true
|
||||
release-branch:
|
||||
description: 'The branch to target for the release.'
|
||||
release-tag:
|
||||
description: 'The release tag for the release (e.g., v0.1.11).'
|
||||
required: true
|
||||
previous-tag:
|
||||
description: 'The previous tag to use for generating release notes.'
|
||||
@@ -34,6 +35,44 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Configure Git User'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
run: |-
|
||||
git config user.name "gemini-cli-robot"
|
||||
git config user.email "gemini-cli-robot@google.com"
|
||||
|
||||
- name: 'Create and switch to a release branch'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
id: 'release_branch'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
BRANCH_NAME="release/${{ inputs.release-tag }}"
|
||||
git switch -c "${BRANCH_NAME}"
|
||||
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: 'Update package versions'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
run: 'npm run release:version "${{ inputs.release-version }}"'
|
||||
|
||||
- name: 'Commit and Conditionally Push package versions'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
env:
|
||||
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||
DRY_RUN: '${{ inputs.dry-run }}'
|
||||
RELEASE_TAG: '${{ inputs.release-tag }}'
|
||||
run: |-
|
||||
git add package.json package-lock.json packages/*/package.json
|
||||
git commit -m "chore(release): ${RELEASE_TAG}"
|
||||
if [[ "${DRY_RUN}" == "false" ]]; then
|
||||
echo "Pushing release branch to remote..."
|
||||
git push --set-upstream origin "${BRANCH_NAME}" --follow-tags
|
||||
else
|
||||
echo "Dry run enabled. Skipping push."
|
||||
fi
|
||||
|
||||
- name: 'Build and Prepare Packages'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |-
|
||||
@@ -61,7 +100,7 @@ runs:
|
||||
|
||||
- name: 'Install latest core package'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
if: '${{ inputs.dry-run == "false" }}'
|
||||
if: '${{ inputs.dry-run == false }}'
|
||||
run: |-
|
||||
npm install "@google/gemini-cli-core@${{ inputs.release-version }}" \
|
||||
--workspace="@google/gemini-cli" \
|
||||
@@ -86,14 +125,28 @@ runs:
|
||||
|
||||
- name: 'Create GitHub Release'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
if: '${{ inputs.dry-run == "false" }}'
|
||||
if: '${{ inputs.dry-run == false }}'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ inputs.github-token }}'
|
||||
run: |-
|
||||
gh release create "v${{ inputs.release-version }}" \
|
||||
gh release create "${{ inputs.release-tag }}" \
|
||||
bundle/gemini.js \
|
||||
--target "${{ inputs.release-branch }}" \
|
||||
--title "Release v${{ inputs.release-version }}" \
|
||||
--target "${{ steps.release_branch.outputs.BRANCH_NAME }}" \
|
||||
--title "Release ${{ inputs.release-tag }}" \
|
||||
--notes-start-tag "${{ inputs.previous-tag }}" \
|
||||
--generate-notes
|
||||
shell: 'bash'
|
||||
|
||||
- name: 'Create Issue on Failure'
|
||||
if: |-
|
||||
${{ failure() }}
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ inputs.github-token }}'
|
||||
RELEASE_TAG: '${{ inputs.release-tag }} || "N/A"'
|
||||
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
run: |-
|
||||
gh issue create \
|
||||
--title "Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')" \
|
||||
--body "The release workflow failed. See the full run for details: ${DETAILS_URL}" \
|
||||
--label "kind/bug,release-failure,priority/p0"
|
||||
shell: 'bash'
|
||||
|
||||
24
.github/actions/run-tests/action.yml
vendored
Normal file
24
.github/actions/run-tests/action.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: 'Run Tests'
|
||||
description: 'Runs the preflight checks and integration tests.'
|
||||
|
||||
inputs:
|
||||
force_skip_tests:
|
||||
description: 'Whether to force skip the tests.'
|
||||
required: false
|
||||
default: 'false'
|
||||
gemini_api_key:
|
||||
description: 'The API key for running integration tests.'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Run Tests'
|
||||
if: "inputs.force_skip_tests != 'true'"
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ inputs.gemini_api_key }}'
|
||||
run: |-
|
||||
npm run preflight
|
||||
npm run test:integration:sandbox:none
|
||||
npm run test:integration:sandbox:docker
|
||||
shell: 'bash'
|
||||
13
.github/workflows/nightly-release.yml
vendored
13
.github/workflows/nightly-release.yml
vendored
@@ -10,6 +10,11 @@ on:
|
||||
required: true
|
||||
type: 'boolean'
|
||||
default: true
|
||||
force_skip_tests:
|
||||
description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@@ -29,6 +34,12 @@ jobs:
|
||||
- name: 'Install Dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Run Tests'
|
||||
uses: './.github/actions/run-tests'
|
||||
with:
|
||||
force_skip_tests: '${{ github.event.inputs.force_skip_tests }}'
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
|
||||
- name: 'Get Nightly Version'
|
||||
id: 'nightly_version'
|
||||
env:
|
||||
@@ -44,10 +55,10 @@ jobs:
|
||||
uses: './.github/actions/publish-release'
|
||||
with:
|
||||
release-version: '${{ steps.nightly_version.outputs.RELEASE_VERSION }}'
|
||||
release-tag: '${{ steps.nightly_version.outputs.RELEASE_TAG }}'
|
||||
npm-tag: '${{ steps.nightly_version.outputs.NPM_TAG }}'
|
||||
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
dry-run: '${{ github.event.inputs.dry_run }}'
|
||||
release-branch: 'main'
|
||||
previous-tag: '${{ steps.nightly_version.outputs.PREVIOUS_TAG }}'
|
||||
|
||||
94
.github/workflows/patch-release.yml
vendored
Normal file
94
.github/workflows/patch-release.yml
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
name: 'Patch Release'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
type:
|
||||
description: 'The type of release to patch from.'
|
||||
required: true
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'stable'
|
||||
- 'preview'
|
||||
ref:
|
||||
description: 'The branch or ref (full git sha) to release from.'
|
||||
required: true
|
||||
type: 'string'
|
||||
default: 'main'
|
||||
dry_run:
|
||||
description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.'
|
||||
required: true
|
||||
type: 'boolean'
|
||||
default: true
|
||||
force_skip_tests:
|
||||
description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment:
|
||||
name: 'production-release'
|
||||
url: '${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ steps.version.outputs.RELEASE_TAG }}'
|
||||
if: |-
|
||||
${{ github.repository == 'google-gemini/gemini-cli' }}
|
||||
permissions:
|
||||
contents: 'write'
|
||||
packages: 'write'
|
||||
id-token: 'write'
|
||||
issues: 'write' # For creating issues on failure
|
||||
outputs:
|
||||
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ github.event.inputs.ref || github.sha }}'
|
||||
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: 'Get the version'
|
||||
id: 'version'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: |-
|
||||
VERSION_JSON="$(node scripts/get-release-version.js --type=patch --patch-from=${{ github.event.inputs.type }})"
|
||||
echo "${VERSION_JSON}"
|
||||
echo "RELEASE_TAG=$(echo "${VERSION_JSON}" | jq -r .releaseTag)" >> "${GITHUB_OUTPUT}"
|
||||
echo "RELEASE_VERSION=$(echo "${VERSION_JSON}" | jq -r .releaseVersion)" >> "${GITHUB_OUTPUT}"
|
||||
echo "NPM_TAG=$(echo "${VERSION_JSON}" | jq -r .npmTag)" >> "${GITHUB_OUTPUT}"
|
||||
echo "PREVIOUS_TAG=$(echo "${VERSION_JSON}" | jq -r .previousReleaseTag)" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: 'Print Calculated Version'
|
||||
run: |-
|
||||
echo "Calculated version: ${{ steps.version.outputs.RELEASE_VERSION }}"
|
||||
|
||||
- name: 'Run Tests'
|
||||
uses: './.github/actions/run-tests'
|
||||
with:
|
||||
force_skip_tests: '${{ github.event.inputs.force_skip_tests }}'
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
|
||||
- name: 'Publish Release'
|
||||
uses: './.github/actions/publish-release'
|
||||
with:
|
||||
release-version: '${{ steps.version.outputs.RELEASE_VERSION }}'
|
||||
release-tag: '${{ steps.version.outputs.RELEASE_TAG }}'
|
||||
npm-tag: '${{ steps.version.outputs.NPM_TAG }}'
|
||||
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
dry-run: '${{ github.event.inputs.dry_run }}'
|
||||
previous-tag: '${{ steps.version.outputs.PREVIOUS_TAG }}'
|
||||
45
.github/workflows/promote-release.yml
vendored
45
.github/workflows/promote-release.yml
vendored
@@ -8,6 +8,11 @@ on:
|
||||
required: true
|
||||
type: 'boolean'
|
||||
default: true
|
||||
force_skip_tests:
|
||||
description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
calculate-versions:
|
||||
@@ -99,50 +104,22 @@ jobs:
|
||||
working-directory: './release'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Configure Git User'
|
||||
working-directory: './release'
|
||||
run: |-
|
||||
git config user.name "gemini-cli-robot"
|
||||
git config user.email "gemini-cli-robot@google.com"
|
||||
|
||||
- name: 'Create and switch to a release branch'
|
||||
working-directory: './release'
|
||||
id: 'release_branch'
|
||||
run: |
|
||||
BRANCH_NAME="release/v${{ matrix.version }}"
|
||||
git switch -c "${BRANCH_NAME}"
|
||||
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: 'Update package versions'
|
||||
working-directory: './release'
|
||||
run: 'npm run release:version "${{ matrix.version }}"'
|
||||
|
||||
- name: 'Commit and Conditionally Push package versions'
|
||||
working-directory: './release'
|
||||
env:
|
||||
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||
DRY_RUN: '${{ github.event.inputs.dry_run }}'
|
||||
RELEASE_TAG: 'v${{ matrix.version }}'
|
||||
run: |-
|
||||
git add package.json package-lock.json packages/*/package.json
|
||||
git commit -m "chore(release): ${RELEASE_TAG}"
|
||||
if [[ "${DRY_RUN}" == "false" ]]; then
|
||||
echo "Pushing release branch to remote..."
|
||||
git push --set-upstream origin "${BRANCH_NAME}" --follow-tags
|
||||
else
|
||||
echo "Dry run enabled. Skipping push."
|
||||
fi
|
||||
- name: 'Run Tests'
|
||||
uses: './.github/actions/run-tests'
|
||||
with:
|
||||
force_skip_tests: '${{ github.event.inputs.force_skip_tests }}'
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
|
||||
- name: 'Publish Release'
|
||||
uses: './.github/actions/publish-release'
|
||||
with:
|
||||
release-version: '${{ matrix.version }}'
|
||||
release-tag: 'v${{ matrix.version }}'
|
||||
npm-tag: '${{ matrix.npm-tag }}'
|
||||
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
dry-run: '${{ github.event.inputs.dry_run }}'
|
||||
release-branch: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||
previous-tag: '${{ matrix.previous-tag }}'
|
||||
working-directory: './release'
|
||||
|
||||
|
||||
@@ -28,9 +28,7 @@ npm install -g @google/gemini-cli@latest
|
||||
npm install -g @google/gemini-cli@nightly
|
||||
```
|
||||
|
||||
# Release Process.
|
||||
|
||||
Where `x.y.z` is the next version to be released. In most all cases for the weekly release this will be an increment on `y`, aka minor version update. Major version updates `x` will need broader coordination and communication. For patches `z` see below. When possible we will do our best to adher to https://semver.org/
|
||||
# Release Process
|
||||
|
||||
Our release cadence is new releases are sent to a preview channel for a week and then promoted to stable after a week. Version numbers will follow SemVer with weekly releases incrementing the minor version. Patches and bug fixes to both preview and stable releases will increment the patch version.
|
||||
|
||||
@@ -38,23 +36,15 @@ Our release cadence is new releases are sent to a preview channel for a week and
|
||||
|
||||
Each night at UTC 0000 we will auto deploy a nightly release from `main`. This will be a version of the next production release, x.y.z, with the nightly tag.
|
||||
|
||||
## Create Preview Release
|
||||
## Weekly Release Promotion
|
||||
|
||||
Each Tuesday at UTC 2359 we will auto deploy a preview release of the next production release x.y.z.
|
||||
Each Tuesday, the on-call engineer will trigger the "Promote Release" workflow. This single action automates the entire weekly release process:
|
||||
|
||||
- This will happen as a scheduled instance of the ‘release’ action. It will be cut off of Main.
|
||||
- This will create a branch `release/vx.y.z-preview.n`
|
||||
- We will run evals and smoke testing against this branch and the npm package. For now this should be manual smoke testing, we don't have a dedicated matrix or specific detailed process. There is work coming soon to make this more formalized and automatic see https://github.com/google-gemini/gemini-cli/issues/3788
|
||||
- Users installing `@preview` will get this release as well
|
||||
1. **Promotes Preview to Stable:** The workflow identifies the latest `preview` release and promotes it to `stable`. This becomes the new `latest` version on npm.
|
||||
2. **Promotes Nightly to Preview:** The latest `nightly` release is then promoted to become the new `preview` version.
|
||||
3. **Prepares for next Nightly:** A pull request is automatically created and merged to bump the version in `main` in preparation for the next nightly release.
|
||||
|
||||
## Promote Stable Release
|
||||
|
||||
After one week (On the following Tuesday) with all signals a go, we will manually release at 2000 UTC via the current on-call person.
|
||||
|
||||
- The release action will be used with the source branch as `release/vx.y.z-preview.n`
|
||||
- The version will be x.y.z
|
||||
- The releaser will create and merge a pr into main with the version changes.
|
||||
- Smoke tests and manual validation will be run. For now this should be manual smoke testing, we don't have a dedicated matrix or specific detailed process. There is work coming soon to make this more formalized and automatic see https://github.com/google-gemini/gemini-cli/issues/3788
|
||||
This process ensures a consistent and reliable release cadence with minimal manual intervention.
|
||||
|
||||
## Patching Releases
|
||||
|
||||
@@ -79,7 +69,11 @@ The workflow will automatically find the merge commit SHA and begin the patch pr
|
||||
|
||||
**Option B: Manually Triggering the Workflow**
|
||||
|
||||
Navigate to the **Actions** tab and run the **Create Patch PR** workflow.
|
||||
Follow the manual release process using the "Patch Release" GitHub Actions workflow.
|
||||
|
||||
- **Type**: Select whether you are patching a `stable` or `preview` release. The workflow will automatically calculate the next patch version.
|
||||
- **Ref**: Use your source branch as the reference (ex. `release/v0.2.0-preview.0`)
|
||||
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.
|
||||
- **Channel**: The channel you want to patch (`stable` or `preview`).
|
||||
@@ -174,14 +168,28 @@ This fully automated process ensures that patches are created and released consi
|
||||
|
||||
## How To Release
|
||||
|
||||
Releases are managed through the [release.yml](https://github.com/google-gemini/gemini-cli/actions/workflows/release.yml) GitHub Actions workflow. To perform a manual release for a patch or hotfix:
|
||||
Releases are managed through GitHub Actions workflows.
|
||||
|
||||
### Weekly Promotions
|
||||
|
||||
To perform the weekly promotion of `preview` to `stable` and `nightly` to `preview`:
|
||||
|
||||
1. Navigate to the **Actions** tab of the repository.
|
||||
2. Select the **Release** workflow from the list.
|
||||
2. Select the **Promote Release** workflow from the list.
|
||||
3. Click the **Run workflow** dropdown button.
|
||||
4. Leave **Dry Run** as `true` to test the workflow without publishing, or set to `false` to perform a live release.
|
||||
5. Click **Run workflow**.
|
||||
|
||||
### Patching a Release
|
||||
|
||||
To perform a manual release for a patch or hotfix:
|
||||
|
||||
1. Navigate to the **Actions** tab of the repository.
|
||||
2. Select the **Patch Release** workflow from the list.
|
||||
3. Click the **Run workflow** dropdown button.
|
||||
4. Fill in the required inputs:
|
||||
- **Version**: The exact version to release (e.g., `v0.2.1`).
|
||||
- **Ref**: The branch or commit SHA to release from (defaults to `main`).
|
||||
- **Type**: Select whether you are patching a `stable` or `preview` release.
|
||||
- **Ref**: The branch or commit SHA to release from.
|
||||
- **Dry Run**: Leave as `true` to test the workflow without publishing, or set to `false` to perform a live release.
|
||||
5. Click **Run workflow**.
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"lint": "eslint . --ext .ts,.tsx && eslint integration-tests",
|
||||
"lint:fix": "eslint . --fix && eslint integration-tests --fix",
|
||||
"lint:ci": "eslint . --ext .ts,.tsx --max-warnings 0 && eslint integration-tests --max-warnings 0",
|
||||
"lint:all": "node scripts/lint.js",
|
||||
"format": "prettier --experimental-cli --write .",
|
||||
"typecheck": "npm run typecheck --workspaces --if-present",
|
||||
"preflight": "npm run clean && npm ci && npm run format && npm run lint:ci && npm run build && npm run typecheck && npm run test:ci",
|
||||
|
||||
@@ -47,8 +47,10 @@ export function getVersion(options = {}) {
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'),
|
||||
);
|
||||
const [major, minor] = packageJson.version.split('.');
|
||||
const nextMinor = parseInt(minor) + 1;
|
||||
const versionParts = packageJson.version.split('.');
|
||||
const major = versionParts[0];
|
||||
const minor = versionParts[1] ? parseInt(versionParts[1]) : 0;
|
||||
const nextMinor = minor + 1;
|
||||
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
||||
const gitShortHash = execSync('git rev-parse --short HEAD')
|
||||
.toString()
|
||||
@@ -71,6 +73,37 @@ export function getVersion(options = {}) {
|
||||
latestNightlyTag.replace(/-nightly.*/, '').replace(/^v/, '') + '-preview';
|
||||
npmTag = 'preview';
|
||||
previousReleaseTag = getLatestTag('contains("preview")');
|
||||
} else if (type === 'patch') {
|
||||
const patchFrom = options.patchFrom || args.patchFrom;
|
||||
if (!patchFrom || (patchFrom !== 'stable' && patchFrom !== 'preview')) {
|
||||
throw new Error(
|
||||
'Patch type must be specified with --patch-from=stable or --patch-from=preview',
|
||||
);
|
||||
}
|
||||
|
||||
if (patchFrom === 'stable') {
|
||||
previousReleaseTag = getLatestTag(
|
||||
'(contains("nightly") or contains("preview")) | not',
|
||||
);
|
||||
const versionParts = previousReleaseTag.replace(/^v/, '').split('.');
|
||||
const major = versionParts[0];
|
||||
const minor = versionParts[1];
|
||||
const patch = versionParts[2] ? parseInt(versionParts[2]) : 0;
|
||||
releaseVersion = `${major}.${minor}.${patch + 1}`;
|
||||
npmTag = 'latest';
|
||||
} else {
|
||||
// patchFrom === 'preview'
|
||||
previousReleaseTag = getLatestTag('contains("preview")');
|
||||
const [version, prerelease] = previousReleaseTag
|
||||
.replace(/^v/, '')
|
||||
.split('-');
|
||||
const versionParts = version.split('.');
|
||||
const major = versionParts[0];
|
||||
const minor = versionParts[1];
|
||||
const patch = versionParts[2] ? parseInt(versionParts[2]) : 0;
|
||||
releaseVersion = `${major}.${minor}.${patch + 1}-${prerelease}`;
|
||||
npmTag = 'preview';
|
||||
}
|
||||
}
|
||||
|
||||
const releaseTag = `v${releaseVersion}`;
|
||||
|
||||
161
scripts/lint.js
Normal file
161
scripts/lint.js
Normal file
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { mkdirSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const ACTIONLINT_VERSION = '1.7.7';
|
||||
const SHELLCHECK_VERSION = '0.11.0';
|
||||
const YAMLLINT_VERSION = '1.35.1';
|
||||
|
||||
const TEMP_DIR = join(tmpdir(), 'gemini-cli-linters');
|
||||
|
||||
function getPlatformArch() {
|
||||
const platform = process.platform;
|
||||
const arch = process.arch;
|
||||
if (platform === 'linux' && arch === 'x64') {
|
||||
return {
|
||||
actionlint: 'linux_amd64',
|
||||
shellcheck: 'linux.x86_64',
|
||||
};
|
||||
}
|
||||
if (platform === 'darwin' && arch === 'x64') {
|
||||
return {
|
||||
actionlint: 'darwin_amd64',
|
||||
shellcheck: 'darwin.x86_64',
|
||||
};
|
||||
}
|
||||
if (platform === 'darwin' && arch === 'arm64') {
|
||||
return {
|
||||
actionlint: 'darwin_arm64',
|
||||
shellcheck: 'darwin.aarch64',
|
||||
};
|
||||
}
|
||||
throw new Error(`Unsupported platform/architecture: ${platform}/${arch}`);
|
||||
}
|
||||
|
||||
const platformArch = getPlatformArch();
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* check: string;
|
||||
* installer: string;
|
||||
* run: string;
|
||||
* }} Linter
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {{[linterName: string]: Linter}}
|
||||
*/
|
||||
const LINTERS = {
|
||||
actionlint: {
|
||||
check: 'command -v actionlint',
|
||||
installer: `
|
||||
mkdir -p "${TEMP_DIR}/actionlint"
|
||||
curl -sSLo "${TEMP_DIR}/.actionlint.tgz" "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_${platformArch.actionlint}.tar.gz"
|
||||
tar -xzf "${TEMP_DIR}/.actionlint.tgz" -C "${TEMP_DIR}/actionlint"
|
||||
`,
|
||||
run: `
|
||||
actionlint \
|
||||
-color \
|
||||
-ignore 'SC2002:' \
|
||||
-ignore 'SC2016:' \
|
||||
-ignore 'SC2129:' \
|
||||
-ignore 'label ".+" is unknown'
|
||||
`,
|
||||
},
|
||||
shellcheck: {
|
||||
check: 'command -v shellcheck',
|
||||
installer: `
|
||||
mkdir -p "${TEMP_DIR}/shellcheck"
|
||||
curl -sSLo "${TEMP_DIR}/.shellcheck.txz" "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.${platformArch.shellcheck}.tar.xz"
|
||||
tar -xf "${TEMP_DIR}/.shellcheck.txz" -C "${TEMP_DIR}/shellcheck" --strip-components=1
|
||||
`,
|
||||
run: `
|
||||
git ls-files | grep -E '^([^.]+|.*\\.(sh|zsh|bash))' | xargs file --mime-type \
|
||||
| grep "text/x-shellscript" | awk '{ print substr($1, 1, length($1)-1) }' \
|
||||
| xargs shellcheck \
|
||||
--check-sourced \
|
||||
--enable=all \
|
||||
--exclude=SC2002,SC2129,SC2310 \
|
||||
--severity=style \
|
||||
--format=gcc \
|
||||
--color=never | sed -e 's/note:/warning:/g' -e 's/style:/warning:/g'
|
||||
`,
|
||||
},
|
||||
yamllint: {
|
||||
check: 'command -v yamllint',
|
||||
installer: `pip3 install --user "yamllint==${YAMLLINT_VERSION}"`,
|
||||
run: "git ls-files | grep -E '\\.(yaml|yml)' | xargs yamllint --format github",
|
||||
},
|
||||
};
|
||||
|
||||
function runCommand(command, stdio = 'inherit') {
|
||||
try {
|
||||
const env = { ...process.env };
|
||||
env.PATH = `${TEMP_DIR}/actionlint:${TEMP_DIR}/shellcheck:${env.PATH}`;
|
||||
if (process.platform === 'darwin') {
|
||||
env.PATH = `${env.PATH}:${process.env.HOME}/Library/Python/3.12/bin`;
|
||||
} else if (process.platform === 'linux') {
|
||||
env.PATH = `${env.PATH}:${process.env.HOME}/.local/bin`;
|
||||
}
|
||||
execSync(command, { stdio, env });
|
||||
return true;
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function setupLinters() {
|
||||
console.log('Setting up linters...');
|
||||
rmSync(TEMP_DIR, { recursive: true, force: true });
|
||||
mkdirSync(TEMP_DIR, { recursive: true });
|
||||
|
||||
for (const linter in LINTERS) {
|
||||
const { check, installer } = LINTERS[linter];
|
||||
if (!runCommand(check, 'ignore')) {
|
||||
console.log(`Installing ${linter}...`);
|
||||
if (!runCommand(installer)) {
|
||||
console.error(
|
||||
`Failed to install ${linter}. Please install it manually.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('All required linters are available.');
|
||||
}
|
||||
|
||||
function runLinters() {
|
||||
console.log('\nRunning ESLint...');
|
||||
if (!runCommand('npm run lint:ci')) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('\nRunning actionlint...');
|
||||
if (!runCommand(LINTERS.actionlint.run)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('\nRunning shellcheck...');
|
||||
if (!runCommand(LINTERS.shellcheck.run)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('\nRunning yamllint...');
|
||||
if (!runCommand(LINTERS.yamllint.run)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('\nAll linting checks passed!');
|
||||
}
|
||||
|
||||
setupLinters();
|
||||
runLinters();
|
||||
@@ -7,28 +7,11 @@
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { getVersion } from '../get-release-version.js';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
vi.mock('node:child_process');
|
||||
vi.mock('node:fs');
|
||||
|
||||
vi.mock('../get-release-version.js', async () => {
|
||||
const actual = await vi.importActual('../get-release-version.js');
|
||||
return {
|
||||
...actual,
|
||||
getVersion: (options) => {
|
||||
if (options.type === 'nightly') {
|
||||
return {
|
||||
releaseTag: 'v0.6.0-nightly.20250911.a1b2c3d',
|
||||
releaseVersion: '0.6.0-nightly.20250911.a1b2c3d',
|
||||
npmTag: 'nightly',
|
||||
previousReleaseTag: 'v0.5.0-nightly.20250910.abcdef',
|
||||
};
|
||||
}
|
||||
return actual.getVersion(options);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('getReleaseVersion', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
@@ -37,14 +20,37 @@ describe('getReleaseVersion', () => {
|
||||
});
|
||||
|
||||
describe('Nightly Workflow Logic', () => {
|
||||
it('should calculate the next nightly version based on package.json', async () => {
|
||||
const { getVersion } = await import('../get-release-version.js');
|
||||
it('should calculate the next nightly version based on package.json', () => {
|
||||
vi.mocked(readFileSync).mockReturnValue('{"version": "0.5.0"}');
|
||||
vi.mocked(execSync).mockImplementation((command) => {
|
||||
if (command.includes('rev-parse')) return 'a1b2c3d';
|
||||
if (command.includes('release list'))
|
||||
return 'v0.5.0-nightly.20250910.abcdef';
|
||||
return '';
|
||||
});
|
||||
|
||||
const result = getVersion({ type: 'nightly' });
|
||||
|
||||
expect(result.releaseVersion).toBe('0.6.0-nightly.20250911.a1b2c3d');
|
||||
expect(result.npmTag).toBe('nightly');
|
||||
expect(result.previousReleaseTag).toBe('v0.5.0-nightly.20250910.abcdef');
|
||||
});
|
||||
|
||||
it('should default minor to 0 if missing in package.json version', () => {
|
||||
vi.mocked(readFileSync).mockReturnValue('{"version": "0"}');
|
||||
vi.mocked(execSync).mockImplementation((command) => {
|
||||
if (command.includes('rev-parse')) return 'a1b2c3d';
|
||||
if (command.includes('release list'))
|
||||
return 'v0.0.0-nightly.20250910.abcdef';
|
||||
return '';
|
||||
});
|
||||
|
||||
const result = getVersion({ type: 'nightly' });
|
||||
|
||||
expect(result.releaseVersion).toBe('0.1.0-nightly.20250911.a1b2c3d');
|
||||
expect(result.npmTag).toBe('nightly');
|
||||
expect(result.previousReleaseTag).toBe('v0.0.0-nightly.20250910.abcdef');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Promote Workflow Logic', () => {
|
||||
@@ -82,4 +88,39 @@ describe('getReleaseVersion', () => {
|
||||
expect(result.previousReleaseTag).toBe(latestPreview);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Patch Workflow Logic', () => {
|
||||
it('should calculate the next patch version for a stable release', () => {
|
||||
const latestStable = 'v0.5.1';
|
||||
vi.mocked(execSync).mockReturnValue(latestStable);
|
||||
|
||||
const result = getVersion({ type: 'patch', patchFrom: 'stable' });
|
||||
|
||||
expect(result.releaseVersion).toBe('0.5.2');
|
||||
expect(result.npmTag).toBe('latest');
|
||||
expect(result.previousReleaseTag).toBe(latestStable);
|
||||
});
|
||||
|
||||
it('should calculate the next patch version for a preview release', () => {
|
||||
const latestPreview = 'v0.6.0-preview';
|
||||
vi.mocked(execSync).mockReturnValue(latestPreview);
|
||||
|
||||
const result = getVersion({ type: 'patch', patchFrom: 'preview' });
|
||||
|
||||
expect(result.releaseVersion).toBe('0.6.1-preview');
|
||||
expect(result.npmTag).toBe('preview');
|
||||
expect(result.previousReleaseTag).toBe(latestPreview);
|
||||
});
|
||||
|
||||
it('should default patch to 0 if missing in stable release', () => {
|
||||
const latestStable = 'v0.5';
|
||||
vi.mocked(execSync).mockReturnValue(latestStable);
|
||||
|
||||
const result = getVersion({ type: 'patch', patchFrom: 'stable' });
|
||||
|
||||
expect(result.releaseVersion).toBe('0.5.1');
|
||||
expect(result.npmTag).toBe('latest');
|
||||
expect(result.previousReleaseTag).toBe(latestStable);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user