mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-28 09:02:12 +08:00
* feat(cli): add unified Commander CLI for QingLong 2.x * fix(cli): publish via npm and address security review feedback * ci(cli): package npm artifacts and remove evaluation collateral * test(cli): use a fixed shell fixture for log retention * refactor(cli): separate remote npm client from panel tools * feat(cli): cover active panel OpenAPI resources * docs(cli): unify authentication and skill guidance * refactor(cli): isolate internal commands and generate Commander help * refactor(cli): organize remote and internal modules by responsibility * ci(cli): publish verified npm archives from master * fix(cli): publish under the whyour npm scope * ci: use npm trusted publishing for both packages * docs: introduce the published CLI on the project homepage * fix(cli): preserve server log truncation and correct login hints * fix(cli): accept dashboard record request bodies * fix(cli): preserve stdin for local task execution * fix(cli): resolve task executables after changing directory * fix(cli): preserve shell function tasks and sanitize test failures * fix(cli): preserve shell hook state and resolve workdir after hooks * fix(cli): preserve cleanup across shared shell task timeouts * fix(cli): isolate shell control descriptors and reap timed-out descendants
133 lines
4.5 KiB
JavaScript
133 lines
4.5 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs/promises');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { execFileSync } = require('node:child_process');
|
|
const { pathToFileURL } = require('node:url');
|
|
|
|
exports.verifyRepository = async function verifyRepository(api, cli) {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-repository-gate-'));
|
|
const repository = path.join(root, 'fixture');
|
|
await fs.mkdir(repository);
|
|
const git = (...args) =>
|
|
execFileSync('git', ['-C', repository, ...args], {
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
git('init', '-b', 'main');
|
|
git('config', 'user.name', 'CLI fixture');
|
|
git('config', 'user.email', 'fixture@example.invalid');
|
|
await fs.writeFile(
|
|
path.join(repository, 'job-wrong.js'),
|
|
'console.log("WRONG_BRANCH")',
|
|
);
|
|
git('add', '.');
|
|
git('commit', '-m', 'main');
|
|
git('checkout', '-b', 'selected');
|
|
await fs.rm(path.join(repository, 'job-wrong.js'));
|
|
await fs.writeFile(
|
|
path.join(repository, 'shared.js'),
|
|
'module.exports="DEPENDENCY_OK";',
|
|
);
|
|
await fs.writeFile(
|
|
path.join(repository, 'job-keep.js'),
|
|
'// cron: 0 0 1 1 *\nconsole.log(require("./shared"));',
|
|
);
|
|
await fs.writeFile(
|
|
path.join(repository, 'job-old.js'),
|
|
'// cron: 0 0 1 1 *\nconsole.log("old");',
|
|
);
|
|
await fs.writeFile(
|
|
path.join(repository, 'job-excluded.js'),
|
|
'throw Error("excluded");',
|
|
);
|
|
git('add', '.');
|
|
git('commit', '-m', 'selected');
|
|
let subscription;
|
|
const tasks = () =>
|
|
cli(['task', 'list']).data.data.filter(
|
|
(row) => row.sub_id === subscription.id,
|
|
);
|
|
try {
|
|
subscription = (
|
|
await api('subscriptions', 'POST', {
|
|
name: 'CLI repository integration',
|
|
alias: 'cli-repository-integration',
|
|
type: 'public-repo',
|
|
url: pathToFileURL(repository).href,
|
|
branch: 'selected',
|
|
whitelist: 'job-',
|
|
blacklist: 'excluded',
|
|
dependences: 'shared',
|
|
extensions: 'js',
|
|
schedule_type: 'crontab',
|
|
schedule: '0 0 1 1 *',
|
|
autoAddCron: true,
|
|
autoDelCron: true,
|
|
})
|
|
).data;
|
|
const run = async (added, removed) => {
|
|
cli(['subscription', 'run', String(subscription.id)]);
|
|
let log = '';
|
|
for (let i = 0; i < 80; i++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
log = cli(['subscription', 'logs', String(subscription.id)]).data;
|
|
if (log.includes(`"added":${added},"removed":${removed}`)) return;
|
|
}
|
|
assert.fail(`Repository sync did not finish: ${log}`);
|
|
};
|
|
await run(2, 0);
|
|
let rows = tasks();
|
|
assert.equal(rows.length, 2);
|
|
assert.ok(rows.some((row) => row.command.endsWith('/job-old.js')));
|
|
const keep = rows.find((row) => row.command.endsWith('/job-keep.js'));
|
|
assert.ok(keep);
|
|
const directory = path.dirname(`/ql/data/scripts/${keep.command.slice(5)}`);
|
|
assert.match(
|
|
await fs.readFile(path.join(directory, 'shared.js'), 'utf8'),
|
|
/DEPENDENCY_OK/,
|
|
);
|
|
await assert.rejects(fs.access(path.join(directory, 'job-wrong.js')));
|
|
await assert.rejects(fs.access(path.join(directory, 'job-excluded.js')));
|
|
cli(['task', 'run', String(keep.id)]);
|
|
let taskLog = '';
|
|
for (let i = 0; i < 60; i++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
taskLog = cli(['task', 'logs', String(keep.id)]).data;
|
|
if (taskLog.includes('DEPENDENCY_OK') && /完成|执行结束/.test(taskLog))
|
|
break;
|
|
}
|
|
assert.match(taskLog, /DEPENDENCY_OK/);
|
|
await fs.rm(path.join(repository, 'job-old.js'));
|
|
await fs.writeFile(
|
|
path.join(repository, 'job-new.js'),
|
|
'// cron: 0 0 1 1 *\nconsole.log("new");',
|
|
);
|
|
git('add', '-A');
|
|
git('commit', '-m', 'replace task');
|
|
await run(1, 1);
|
|
rows = tasks();
|
|
assert.equal(rows.length, 2);
|
|
assert.equal(
|
|
rows.find((row) => row.command.endsWith('/job-keep.js')).id,
|
|
keep.id,
|
|
);
|
|
assert.ok(rows.some((row) => row.command.endsWith('/job-new.js')));
|
|
assert.ok(!rows.some((row) => row.command.endsWith('/job-old.js')));
|
|
await assert.rejects(fs.access(path.join(directory, 'job-old.js')));
|
|
return true;
|
|
} finally {
|
|
if (subscription) {
|
|
const rows = tasks();
|
|
if (rows.length)
|
|
await api(
|
|
'crons',
|
|
'DELETE',
|
|
rows.map((row) => row.id),
|
|
);
|
|
await api('subscriptions', 'DELETE', [subscription.id]);
|
|
}
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
}
|
|
};
|