mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-10 08:05:14 +02:00
chore: upgrade dependencies and tooling (#232)
* chore: upgrade dependencies and tooling * chore: upgrade dependencies and tooling
This commit is contained in:
parent
ed8f523362
commit
d53cdac366
14 changed files with 2737 additions and 2710 deletions
111
scripts/upgrade-dependencies.mjs
Normal file
111
scripts/upgrade-dependencies.mjs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { execFile as execFileCallback } from 'node:child_process';
|
||||
import { readFile as fsReadFile } from 'node:fs/promises';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
const execFileAsync = promisify(execFileCallback);
|
||||
const npmCheckUpdatesRejectArgs = ['--reject', 'fumadocs-core,fumadocs-ui'];
|
||||
|
||||
function ktxRootDir() {
|
||||
return resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
}
|
||||
|
||||
function failureText(error) {
|
||||
const stdout = typeof error?.stdout === 'string' ? error.stdout.trim() : '';
|
||||
const stderr = typeof error?.stderr === 'string' ? error.stderr.trim() : '';
|
||||
const message = error instanceof Error ? error.message.trim() : String(error);
|
||||
return [stderr, stdout, message].filter((line) => line.length > 0).join('\n') || 'Command failed';
|
||||
}
|
||||
|
||||
function commandText(command, args) {
|
||||
return [command, ...args].join(' ');
|
||||
}
|
||||
|
||||
function pythonDependencyUpdatePhases() {
|
||||
const manifests = ['pyproject.toml', 'python/ktx-sl/pyproject.toml', 'python/ktx-daemon/pyproject.toml'];
|
||||
return manifests.map((manifest) => ({
|
||||
name: `Python dependency constraints: ${manifest}`,
|
||||
command: 'uvx',
|
||||
args: ['dependency-check-updates', '--manifest', manifest, '-u'],
|
||||
retry: commandText('uvx', ['dependency-check-updates', '--manifest', manifest, '-u']),
|
||||
}));
|
||||
}
|
||||
|
||||
async function pnpmMinimumReleaseAgeCooldown(rootDir, readFile) {
|
||||
let workspaceConfig;
|
||||
try {
|
||||
workspaceConfig = await readFile(resolve(rootDir, 'pnpm-workspace.yaml'), 'utf8');
|
||||
} catch (error) {
|
||||
if (error?.code === 'ENOENT') {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const match = workspaceConfig.match(/^\s*minimumReleaseAge:\s*(\d+)\s*$/m);
|
||||
if (!match) {
|
||||
return [];
|
||||
}
|
||||
return ['--cooldown', `${match[1]}m`];
|
||||
}
|
||||
|
||||
export async function runDependencyUpgrade(options = {}) {
|
||||
const rootDir = options.rootDir ?? ktxRootDir();
|
||||
const execFile = options.execFile ?? execFileAsync;
|
||||
const readFile = options.readFile ?? fsReadFile;
|
||||
const log = options.log ?? ((line) => process.stdout.write(`${line}\n`));
|
||||
const npmCheckUpdatesCooldownArgs = await pnpmMinimumReleaseAgeCooldown(rootDir, readFile);
|
||||
const phases = [
|
||||
{
|
||||
name: 'TypeScript dependency constraints',
|
||||
command: 'pnpm',
|
||||
args: ['dlx', 'npm-check-updates', '-u', '--deep', ...npmCheckUpdatesRejectArgs, ...npmCheckUpdatesCooldownArgs],
|
||||
retry: commandText('pnpm', [
|
||||
'dlx',
|
||||
'npm-check-updates',
|
||||
'-u',
|
||||
'--deep',
|
||||
...npmCheckUpdatesRejectArgs,
|
||||
...npmCheckUpdatesCooldownArgs,
|
||||
]),
|
||||
},
|
||||
...pythonDependencyUpdatePhases(),
|
||||
{
|
||||
name: 'TypeScript lockfile',
|
||||
command: 'pnpm',
|
||||
args: ['install'],
|
||||
retry: 'pnpm install',
|
||||
},
|
||||
{
|
||||
name: 'Python lockfile',
|
||||
command: 'uv',
|
||||
args: ['lock', '--upgrade'],
|
||||
retry: 'uv lock --upgrade',
|
||||
},
|
||||
];
|
||||
|
||||
for (const phase of phases) {
|
||||
log(`RUN ${phase.name}: ${commandText(phase.command, phase.args)}`);
|
||||
try {
|
||||
await execFile(phase.command, phase.args, { cwd: rootDir, maxBuffer: 1024 * 1024 * 64 });
|
||||
log(`PASS ${phase.name}`);
|
||||
} catch (error) {
|
||||
log(`FAIL ${phase.name}: ${failureText(error)}`);
|
||||
log(`Retry: ${phase.retry}`);
|
||||
return { ok: false, failedPhase: phase };
|
||||
}
|
||||
}
|
||||
|
||||
log('Dependency manifests and lockfiles were updated. Run `pnpm run check` before committing.');
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
const result = await runDependencyUpgrade();
|
||||
if (!result.ok) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
123
scripts/upgrade-dependencies.test.mjs
Normal file
123
scripts/upgrade-dependencies.test.mjs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { test } from 'node:test';
|
||||
import { runDependencyUpgrade } from './upgrade-dependencies.mjs';
|
||||
|
||||
test('runDependencyUpgrade updates TypeScript and Python manifests before regenerating lockfiles', async () => {
|
||||
const calls = [];
|
||||
const logs = [];
|
||||
|
||||
const result = await runDependencyUpgrade({
|
||||
rootDir: '/workspace/ktx',
|
||||
readFile: async (path) => {
|
||||
assert.equal(path, '/workspace/ktx/pnpm-workspace.yaml');
|
||||
return 'packages: []\nminimumReleaseAge: 10080\n';
|
||||
},
|
||||
execFile: async (command, args, options) => {
|
||||
calls.push({ command, args, cwd: options.cwd });
|
||||
return { stdout: '', stderr: '' };
|
||||
},
|
||||
log: (line) => logs.push(line),
|
||||
});
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.deepEqual(
|
||||
calls.map((call) => [call.command, call.args]),
|
||||
[
|
||||
[
|
||||
'pnpm',
|
||||
[
|
||||
'dlx',
|
||||
'npm-check-updates',
|
||||
'-u',
|
||||
'--deep',
|
||||
'--reject',
|
||||
'fumadocs-core,fumadocs-ui',
|
||||
'--cooldown',
|
||||
'10080m',
|
||||
],
|
||||
],
|
||||
['uvx', ['dependency-check-updates', '--manifest', 'pyproject.toml', '-u']],
|
||||
['uvx', ['dependency-check-updates', '--manifest', 'python/ktx-sl/pyproject.toml', '-u']],
|
||||
['uvx', ['dependency-check-updates', '--manifest', 'python/ktx-daemon/pyproject.toml', '-u']],
|
||||
['pnpm', ['install']],
|
||||
['uv', ['lock', '--upgrade']],
|
||||
],
|
||||
);
|
||||
assert.equal(calls.every((call) => call.cwd === '/workspace/ktx'), true);
|
||||
assert.equal(logs.some((line) => line.includes('PASS Python dependency constraints')), true);
|
||||
});
|
||||
|
||||
test('runDependencyUpgrade stops at the failed phase and prints a retry command', async () => {
|
||||
const calls = [];
|
||||
const logs = [];
|
||||
|
||||
const result = await runDependencyUpgrade({
|
||||
rootDir: '/workspace/ktx',
|
||||
readFile: async () => 'packages: []\n',
|
||||
execFile: async (command, args) => {
|
||||
calls.push({ command, args });
|
||||
if (command === 'uvx' && args.includes('python/ktx-sl/pyproject.toml')) {
|
||||
const error = new Error('dependency-check-updates failed');
|
||||
error.stdout = 'checking Python dependencies';
|
||||
error.stderr = 'could not read pyproject.toml';
|
||||
throw error;
|
||||
}
|
||||
return { stdout: '', stderr: '' };
|
||||
},
|
||||
log: (line) => logs.push(line),
|
||||
});
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.failedPhase.name, 'Python dependency constraints: python/ktx-sl/pyproject.toml');
|
||||
assert.equal(result.failedPhase.retry, 'uvx dependency-check-updates --manifest python/ktx-sl/pyproject.toml -u');
|
||||
assert.deepEqual(
|
||||
calls.map((call) => [call.command, call.args]),
|
||||
[
|
||||
['pnpm', ['dlx', 'npm-check-updates', '-u', '--deep', '--reject', 'fumadocs-core,fumadocs-ui']],
|
||||
['uvx', ['dependency-check-updates', '--manifest', 'pyproject.toml', '-u']],
|
||||
['uvx', ['dependency-check-updates', '--manifest', 'python/ktx-sl/pyproject.toml', '-u']],
|
||||
],
|
||||
);
|
||||
assert.equal(logs.some((line) => line.includes('FAIL Python dependency constraints')), true);
|
||||
assert.equal(logs.some((line) => line.includes('could not read pyproject.toml')), true);
|
||||
assert.equal(logs.some((line) => line.includes('checking Python dependencies')), true);
|
||||
assert.equal(
|
||||
logs.some((line) => line.includes('Retry: uvx dependency-check-updates --manifest python/ktx-sl/pyproject.toml -u')),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('runDependencyUpgrade ignores missing pnpm minimum release age config', async () => {
|
||||
const calls = [];
|
||||
|
||||
const result = await runDependencyUpgrade({
|
||||
rootDir: '/workspace/ktx',
|
||||
readFile: async () => {
|
||||
throw Object.assign(new Error('missing'), { code: 'ENOENT' });
|
||||
},
|
||||
execFile: async (command, args) => {
|
||||
calls.push({ command, args });
|
||||
return { stdout: '', stderr: '' };
|
||||
},
|
||||
log: () => undefined,
|
||||
});
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.deepEqual(calls[0], {
|
||||
command: 'pnpm',
|
||||
args: ['dlx', 'npm-check-updates', '-u', '--deep', '--reject', 'fumadocs-core,fumadocs-ui'],
|
||||
});
|
||||
assert.equal(
|
||||
calls
|
||||
.filter((call) => call.command === 'uvx')
|
||||
.every((call) => call.args.includes('--manifest') && !call.args.includes('-d')),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('package scripts expose the full dependency upgrade command', async () => {
|
||||
const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
||||
|
||||
assert.equal(packageJson.scripts['deps:upgrade'], 'node scripts/upgrade-dependencies.mjs');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue