mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-13 02:35:57 +00:00
refactor: extract shared release helper utilities (#3834)
Move four duplicated utility functions (getArgs, readJson,
validateVersion, isExpectedMissingGitHubRelease) from the three
get-release-version.js scripts into a shared module at
scripts/lib/release-helpers.js so that changes only need to happen
in one place.
Also fixes a pre-existing bug in getArgs where argument values
containing '=' were silently truncated (e.g. --msg=a=b produced
{msg:'a'} instead of {msg:'a=b'}).
Closes #3795
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com>
This commit is contained in:
parent
59845407fc
commit
2c93fd670c
5 changed files with 211 additions and 108 deletions
|
|
@ -10,6 +10,11 @@ import { execSync } from 'node:child_process';
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
getArgs,
|
||||
isExpectedMissingGitHubRelease,
|
||||
validateVersion,
|
||||
} from '../../../scripts/lib/release-helpers.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
|
@ -27,18 +32,6 @@ function readPyprojectVersion() {
|
|||
return match[1];
|
||||
}
|
||||
|
||||
function getArgs() {
|
||||
const args = {};
|
||||
for (const arg of process.argv.slice(2)) {
|
||||
if (!arg.startsWith('--')) {
|
||||
continue;
|
||||
}
|
||||
const [key, value] = arg.slice(2).split('=');
|
||||
args[key] = value === undefined ? true : value;
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function parseVersion(version) {
|
||||
let match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
||||
if (match) {
|
||||
|
|
@ -251,26 +244,6 @@ function getGitShortHash() {
|
|||
return execSync('git rev-parse --short HEAD').toString().trim();
|
||||
}
|
||||
|
||||
function validateVersion(version, format, name) {
|
||||
const versionRegex = {
|
||||
'X.Y.Z': /^\d+\.\d+\.\d+$/,
|
||||
'X.Y.Z-preview.N': /^\d+\.\d+\.\d+-preview\.\d+$/,
|
||||
};
|
||||
|
||||
if (!versionRegex[format]?.test(version)) {
|
||||
throw new Error(
|
||||
`Invalid ${name}: ${version}. Must be in ${format} format.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function isExpectedMissingGitHubRelease(error) {
|
||||
const stderr = error.stderr?.toString() ?? '';
|
||||
const stdout = error.stdout?.toString() ?? '';
|
||||
const message = `${error.message}\n${stderr}\n${stdout}`;
|
||||
return message.includes('release not found') || message.includes('Not Found');
|
||||
}
|
||||
|
||||
async function getReleaseState({ packageVersion, releaseTag }, allVersions) {
|
||||
const state = {
|
||||
packageVersionExistsOnPyPI: allVersions.includes(packageVersion),
|
||||
|
|
|
|||
|
|
@ -8,8 +8,13 @@
|
|||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import {
|
||||
getArgs,
|
||||
isExpectedMissingGitHubRelease,
|
||||
readJson,
|
||||
validateVersion,
|
||||
} from '../../../scripts/lib/release-helpers.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
|
@ -17,21 +22,6 @@ const __dirname = dirname(__filename);
|
|||
const PACKAGE_NAME = '@qwen-code/sdk';
|
||||
const TAG_PREFIX = 'sdk-typescript-v';
|
||||
|
||||
function readJson(filePath) {
|
||||
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
||||
}
|
||||
|
||||
function getArgs() {
|
||||
const args = {};
|
||||
process.argv.slice(2).forEach((arg) => {
|
||||
if (arg.startsWith('--')) {
|
||||
const [key, value] = arg.substring(2).split('=');
|
||||
args[key] = value === undefined ? true : value;
|
||||
}
|
||||
});
|
||||
return args;
|
||||
}
|
||||
|
||||
function getVersionFromNPM(distTag) {
|
||||
const command = `npm view ${PACKAGE_NAME} version --tag=${distTag}`;
|
||||
try {
|
||||
|
|
@ -142,15 +132,6 @@ function detectRollbackAndGetBaseline(npmDistTag) {
|
|||
}
|
||||
|
||||
function doesVersionExist(version) {
|
||||
const isExpectedMissingGitHubRelease = (error) => {
|
||||
const stderr = error.stderr?.toString() ?? '';
|
||||
const stdout = error.stdout?.toString() ?? '';
|
||||
const message = `${error.message}\n${stderr}\n${stdout}`;
|
||||
return (
|
||||
message.includes('release not found') || message.includes('Not Found')
|
||||
);
|
||||
};
|
||||
|
||||
// Check NPM
|
||||
try {
|
||||
const command = `npm view ${PACKAGE_NAME}@${version} version 2>/dev/null`;
|
||||
|
|
@ -254,19 +235,6 @@ function getNightlyVersion() {
|
|||
};
|
||||
}
|
||||
|
||||
function validateVersion(version, format, name) {
|
||||
const versionRegex = {
|
||||
'X.Y.Z': /^\d+\.\d+\.\d+$/,
|
||||
'X.Y.Z-preview.N': /^\d+\.\d+\.\d+-preview\.\d+$/,
|
||||
};
|
||||
|
||||
if (!versionRegex[format] || !versionRegex[format].test(version)) {
|
||||
throw new Error(
|
||||
`Invalid ${name}: ${version}. Must be in ${format} format.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getStableVersion(args) {
|
||||
let releaseVersion;
|
||||
if (args.stable_version_override) {
|
||||
|
|
|
|||
|
|
@ -8,23 +8,13 @@
|
|||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import semver from 'semver';
|
||||
|
||||
function readJson(filePath) {
|
||||
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
||||
}
|
||||
|
||||
function getArgs() {
|
||||
const args = {};
|
||||
process.argv.slice(2).forEach((arg) => {
|
||||
if (arg.startsWith('--')) {
|
||||
const [key, value] = arg.substring(2).split('=');
|
||||
args[key] = value === undefined ? true : value;
|
||||
}
|
||||
});
|
||||
return args;
|
||||
}
|
||||
import {
|
||||
getArgs,
|
||||
isExpectedMissingGitHubRelease,
|
||||
readJson,
|
||||
validateVersion,
|
||||
} from './lib/release-helpers.js';
|
||||
|
||||
function getVersionFromNPM(distTag) {
|
||||
const command = `npm view @qwen-code/qwen-code version --tag=${distTag}`;
|
||||
|
|
@ -129,15 +119,6 @@ function detectRollbackAndGetBaseline(npmDistTag) {
|
|||
}
|
||||
|
||||
function doesVersionExist(version) {
|
||||
const isExpectedMissingGitHubRelease = (error) => {
|
||||
const stderr = error.stderr?.toString() ?? '';
|
||||
const stdout = error.stdout?.toString() ?? '';
|
||||
const message = `${error.message}\n${stderr}\n${stdout}`;
|
||||
return (
|
||||
message.includes('release not found') || message.includes('Not Found')
|
||||
);
|
||||
};
|
||||
|
||||
// Check NPM
|
||||
try {
|
||||
const command = `npm view @qwen-code/qwen-code@${version} version 2>/dev/null`;
|
||||
|
|
@ -244,19 +225,6 @@ function getNightlyVersion() {
|
|||
};
|
||||
}
|
||||
|
||||
function validateVersion(version, format, name) {
|
||||
const versionRegex = {
|
||||
'X.Y.Z': /^\d+\.\d+\.\d+$/,
|
||||
'X.Y.Z-preview.N': /^\d+\.\d+\.\d+-preview\.\d+$/,
|
||||
};
|
||||
|
||||
if (!versionRegex[format] || !versionRegex[format].test(version)) {
|
||||
throw new Error(
|
||||
`Invalid ${name}: ${version}. Must be in ${format} format.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getStableVersion(args) {
|
||||
const { latestVersion: latestPreviewVersion } = getAndVerifyTags(
|
||||
'preview',
|
||||
|
|
|
|||
62
scripts/lib/release-helpers.js
Normal file
62
scripts/lib/release-helpers.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
/**
|
||||
* Parse command-line arguments in `--key=value` format.
|
||||
* Flags without a value (e.g. `--dry-run`) are set to `true`.
|
||||
*/
|
||||
export function getArgs() {
|
||||
const args = {};
|
||||
process.argv.slice(2).forEach((arg) => {
|
||||
if (arg.startsWith('--')) {
|
||||
const stripped = arg.substring(2);
|
||||
const eqIndex = stripped.indexOf('=');
|
||||
if (eqIndex === -1) {
|
||||
args[stripped] = true;
|
||||
} else {
|
||||
args[stripped.substring(0, eqIndex)] = stripped.substring(eqIndex + 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse a JSON file.
|
||||
*/
|
||||
export function readJson(filePath) {
|
||||
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a version string matches the expected format.
|
||||
* Throws if the version is invalid.
|
||||
*/
|
||||
export function validateVersion(version, format, name) {
|
||||
const versionRegex = {
|
||||
'X.Y.Z': /^\d+\.\d+\.\d+$/,
|
||||
'X.Y.Z-preview.N': /^\d+\.\d+\.\d+-preview\.\d+$/,
|
||||
};
|
||||
|
||||
if (!versionRegex[format] || !versionRegex[format].test(version)) {
|
||||
throw new Error(
|
||||
`Invalid ${name}: ${version}. Must be in ${format} format.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an error from `gh release view` indicates the release
|
||||
* simply doesn't exist (as opposed to an unexpected failure).
|
||||
*/
|
||||
export function isExpectedMissingGitHubRelease(error) {
|
||||
const stderr = error.stderr?.toString() ?? '';
|
||||
const stdout = error.stdout?.toString() ?? '';
|
||||
const message = `${error.message}\n${stderr}\n${stdout}`;
|
||||
return message.includes('release not found') || message.includes('Not Found');
|
||||
}
|
||||
132
scripts/tests/release-helpers.test.js
Normal file
132
scripts/tests/release-helpers.test.js
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { vi, describe, it, expect, afterEach, beforeEach } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
vi.mock('node:fs');
|
||||
|
||||
import {
|
||||
getArgs,
|
||||
readJson,
|
||||
validateVersion,
|
||||
isExpectedMissingGitHubRelease,
|
||||
} from '../lib/release-helpers.js';
|
||||
|
||||
describe('getArgs', () => {
|
||||
const originalArgv = process.argv;
|
||||
|
||||
beforeEach(() => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.argv = originalArgv;
|
||||
});
|
||||
|
||||
it('parses --key=value arguments', () => {
|
||||
process.argv = ['node', 'script.js', '--type=nightly', '--channel=preview'];
|
||||
expect(getArgs()).toEqual({ type: 'nightly', channel: 'preview' });
|
||||
});
|
||||
|
||||
it('sets boolean true for flags without a value', () => {
|
||||
process.argv = ['node', 'script.js', '--dry-run', '--verbose'];
|
||||
expect(getArgs()).toEqual({ 'dry-run': true, verbose: true });
|
||||
});
|
||||
|
||||
it('ignores arguments that do not start with --', () => {
|
||||
process.argv = ['node', 'script.js', 'positional', '-short', '--valid=1'];
|
||||
expect(getArgs()).toEqual({ valid: '1' });
|
||||
});
|
||||
|
||||
it('preserves equals signs in argument values', () => {
|
||||
process.argv = ['node', 'script.js', '--message=hello=world'];
|
||||
expect(getArgs()).toEqual({ message: 'hello=world' });
|
||||
});
|
||||
|
||||
it('returns an empty object when there are no arguments', () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
expect(getArgs()).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('readJson', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('reads and parses a JSON file', () => {
|
||||
vi.mocked(readFileSync).mockReturnValue('{"version": "1.0.0"}');
|
||||
expect(readJson('/path/to/file.json')).toEqual({ version: '1.0.0' });
|
||||
expect(readFileSync).toHaveBeenCalledWith('/path/to/file.json', 'utf-8');
|
||||
});
|
||||
|
||||
it('propagates errors from readFileSync', () => {
|
||||
vi.mocked(readFileSync).mockImplementation(() => {
|
||||
throw new Error('ENOENT');
|
||||
});
|
||||
expect(() => readJson('/nonexistent.json')).toThrow('ENOENT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateVersion', () => {
|
||||
it('accepts a valid X.Y.Z version', () => {
|
||||
expect(() => validateVersion('1.2.3', 'X.Y.Z', 'test')).not.toThrow();
|
||||
});
|
||||
|
||||
it('accepts a valid X.Y.Z-preview.N version', () => {
|
||||
expect(() =>
|
||||
validateVersion('1.2.3-preview.4', 'X.Y.Z-preview.N', 'test'),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('throws for an invalid X.Y.Z version', () => {
|
||||
expect(() => validateVersion('bad', 'X.Y.Z', 'test')).toThrow(
|
||||
'Invalid test: bad. Must be in X.Y.Z format.',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when version does not match the requested format', () => {
|
||||
expect(() => validateVersion('1.2.3', 'X.Y.Z-preview.N', 'test')).toThrow(
|
||||
'Invalid test: 1.2.3. Must be in X.Y.Z-preview.N format.',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws for an unknown format key', () => {
|
||||
expect(() => validateVersion('1.2.3', 'unknown', 'test')).toThrow(
|
||||
'Invalid test: 1.2.3. Must be in unknown format.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isExpectedMissingGitHubRelease', () => {
|
||||
it('returns true when message contains "release not found"', () => {
|
||||
const error = new Error('release not found');
|
||||
expect(isExpectedMissingGitHubRelease(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when stderr contains "Not Found"', () => {
|
||||
const error = new Error('command failed');
|
||||
error.stderr = Buffer.from('Not Found');
|
||||
expect(isExpectedMissingGitHubRelease(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when stdout contains "release not found"', () => {
|
||||
const error = new Error('command failed');
|
||||
error.stdout = Buffer.from('release not found');
|
||||
expect(isExpectedMissingGitHubRelease(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for an unrelated error', () => {
|
||||
const error = new Error('network timeout');
|
||||
expect(isExpectedMissingGitHubRelease(error)).toBe(false);
|
||||
});
|
||||
|
||||
it('handles errors without stderr or stdout properties', () => {
|
||||
const error = new Error('something went wrong');
|
||||
expect(isExpectedMissingGitHubRelease(error)).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue