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:
@@ -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