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:
matt korwel
2025-09-12 10:22:10 -07:00
committed by GitHub
parent eaadc6d93d
commit c99539b991
10 changed files with 489 additions and 86 deletions

View File

@@ -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
View 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();

View File

@@ -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);
});
});
});