Files
qinglong/test/back/mirror-status.test.cjs
T
whyour 68ffa106f8 fix: resolve Debian regression findings (#3087)
* fix: resolve Debian regression findings

* fix: reject failed APT index refreshes and simplify SSH home lookup
2026-10-09 01:41:29 +08:00

258 lines
9.4 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const { execFileSync, spawn, spawnSync } = require('node:child_process');
const script = path.resolve('scripts/wait-for-mirror.sh');
const git = (...args) =>
execFileSync('git', args, { encoding: 'utf8', stdio: 'pipe' }).trim();
const options = (env = {}) => ({
encoding: 'utf8',
env: {
...process.env,
QL_MIRROR_WAIT_SECONDS: '3',
QL_MIRROR_POLL_INTERVAL: '1',
QL_MIRROR_QUERY_TIMEOUT: '1',
...env,
},
});
function fixture(t) {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-mirror-status-'));
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
const source = path.join(tmp, 'source');
const mirror = path.join(tmp, 'mirror.git');
git('init', source);
git('init', '--bare', mirror);
git('-C', source, 'config', 'user.name', 'Fixture');
git('-C', source, 'config', 'user.email', 'fixture@example.invalid');
git('-C', source, 'checkout', '-b', 'develop');
fs.writeFileSync(path.join(source, 'snapshot'), 'first');
git('-C', source, 'add', '.');
git('-C', source, 'commit', '-m', 'first');
return { tmp, source, mirror };
}
test('mirror verification reads matching refs without changing either repository', (t) => {
const { source, mirror } = fixture(t);
git('-C', source, 'push', mirror, 'develop');
const before = git('ls-remote', '--refs', mirror);
const result = spawnSync(
'bash',
[script, source, mirror, 'develop'],
options(),
);
assert.equal(result.status, 0, result.stderr + result.stdout);
assert.match(result.stdout, /Mirror matches the latest source refs/);
assert.equal(git('ls-remote', '--refs', mirror), before);
assert.equal(git('-C', source, 'status', '--porcelain'), '');
});
test('mirror polling follows a source that advances while waiting', async (t) => {
const { source, mirror } = fixture(t);
git('-C', source, 'push', mirror, 'develop');
fs.writeFileSync(path.join(source, 'snapshot'), 'second');
git('-C', source, 'commit', '-am', 'second');
const child = spawn(
'bash',
[script, source, mirror, 'develop'],
options({
QL_MIRROR_WAIT_SECONDS: '6',
}),
);
let output = '';
let error = '';
let moved = false;
child.stderr.on('data', (data) => {
error += data;
});
child.stdout.on('data', (data) => {
output += data;
if (!moved && output.includes('Waiting for mirror')) {
moved = true;
fs.writeFileSync(path.join(source, 'snapshot'), 'third');
git('-C', source, 'commit', '-am', 'third');
git('-C', source, 'push', mirror, 'develop');
}
});
const status = await new Promise((resolve, reject) => {
child.on('error', reject);
child.on('close', resolve);
});
assert.equal(status, 0, error + output);
assert.ok(moved);
assert.match(output, new RegExp(git('-C', source, 'rev-parse', 'develop')));
});
test('empty or missing refs never count as a synchronized mirror', (t) => {
const { source, mirror } = fixture(t);
for (const ref of ['develop', 'missing']) {
const result = spawnSync(
'bash',
[script, source, mirror, ref],
// Leave time to read both refs before checking the missing-ref diagnostic.
options(),
);
assert.equal(result.status, 1, result.stderr + result.stdout);
assert.match(result.stdout, /is missing refs\/heads\//);
assert.match(result.stdout, /::error::Mirror did not match/);
}
});
test('static releases require both the version branch and annotated tag object', (t) => {
const { source, mirror } = fixture(t);
git('-C', source, 'branch', 'v2.22.0');
git('-C', source, 'tag', '-a', 'v2.22.0', '-m', 'release');
git('-C', source, 'push', mirror, 'refs/tags/v2.22.0');
const args = [script, source, mirror, 'v2.22.0', 'tag'];
const env = {
QL_MIRROR_INCLUDE_VERSION_BRANCH: 'true',
QL_MIRROR_WAIT_SECONDS: '3',
};
const partial = spawnSync('bash', args, options(env));
assert.equal(partial.status, 1, partial.stderr + partial.stdout);
assert.match(partial.stdout, /mirror is missing refs\/heads\/v2.22.0/);
git('-C', source, 'push', mirror, 'refs/heads/v2.22.0');
const complete = spawnSync('bash', args, options(env));
assert.equal(complete.status, 0, complete.stderr + complete.stdout);
assert.match(
complete.stdout,
new RegExp(git('-C', source, 'rev-parse', 'refs/tags/v2.22.0')),
);
// A tag pointing to the same commit with different annotation is still stale.
git('-C', source, 'tag', '-fa', 'v2.22.0', '-m', 'updated annotation');
const stale = spawnSync('bash', args, options(env));
assert.equal(stale.status, 1, stale.stderr + stale.stdout);
});
test('network failures and hung ref queries stop within the configured deadline', (t) => {
const { tmp, source, mirror } = fixture(t);
const failure = spawnSync(
'bash',
[script, source, path.join(tmp, 'absent'), 'develop'],
options({
QL_MIRROR_WAIT_SECONDS: '3',
}),
);
assert.equal(failure.status, 1, failure.stderr + failure.stdout);
assert.match(failure.stdout, /Could not read mirror refs/);
const bin = path.join(tmp, 'bin');
fs.mkdirSync(bin);
const realGit = execFileSync('which', ['git'], { encoding: 'utf8' }).trim();
fs.writeFileSync(
path.join(bin, 'git'),
'#!/bin/sh\nif [ "$1" = ls-remote ]; then sleep 30; fi\nexec "$QL_TEST_GIT" "$@"\n',
{ mode: 0o755 },
);
const started = Date.now();
const hung = spawnSync('bash', [script, source, mirror, 'develop'], {
...options({
PATH: `${bin}${path.delimiter}${process.env.PATH}`,
QL_TEST_GIT: realGit,
QL_MIRROR_WAIT_SECONDS: '2',
QL_MIRROR_QUERY_TIMEOUT: '20',
}),
timeout: 5000,
});
assert.equal(hung.status, 1, hung.stderr + hung.stdout);
assert.ok(Date.now() - started < 4500);
assert.match(hung.stdout, /within 2s/);
});
test('invalid mirror parameters fail before polling', (t) => {
const { source, mirror } = fixture(t);
for (const args of [
[],
[source, mirror, 'develop', 'other'],
[source, mirror, 'bad ref'],
]) {
assert.notEqual(spawnSync('bash', [script, ...args], options()).status, 0);
}
for (const value of ['0', '-1', '1.5', '1+2', '9999999999999']) {
const result = spawnSync(
'bash',
[script, source, mirror, 'develop'],
options({
QL_MIRROR_WAIT_SECONDS: value,
}),
);
assert.equal(result.status, 2, result.stderr + result.stdout);
}
});
test('Gitee workflows verify both mirrors and do not gate image publication', () => {
const workflow = require('js-yaml').load(
fs.readFileSync('.github/workflows/build-docker-image.yml', 'utf8'),
);
assert.equal(workflow.jobs.code_gitee, undefined);
assert.equal(workflow.jobs.static_gitee, undefined);
const job = workflow.jobs['verify-gitee-mirrors'];
assert.equal(job.needs, 'build-static');
assert.equal(job['timeout-minutes'], 14);
assert.deepEqual(job.permissions, { contents: 'write' });
assert.deepEqual(job.strategy.matrix.repository, [
'qinglong',
'qinglong-static',
]);
const verify = job.steps.find((step) => step.run);
assert.equal(verify.env.MIRROR_REF, '${{ github.ref_name }}');
assert.equal(verify.env.MIRROR_REF_TYPE, '${{ github.ref_type }}');
assert.equal(
verify.env.QL_MIRROR_INCLUDE_VERSION_BRANCH,
"${{ matrix.repository == 'qinglong-static' }}",
);
assert.match(verify.run, /bash scripts\/wait-for-mirror.sh/);
assert.equal(verify.if, "${{ matrix.repository == 'qinglong' }}");
assert.equal(verify.env.GH_TOKEN, '${{ github.token }}');
assert.equal(verify.env.QL_MIRROR_NOTIFY_ON_START, 'false');
assert.equal(verify.env.QL_MIRROR_NOTIFY_REPO, 'whyour/qinglong');
assert.doesNotMatch(
JSON.stringify(verify),
/secrets\.|git push|sync-static|ssh-keyscan/,
);
const notify = job.steps.find((step) =>
/Notify and wait/.test(step.name || ''),
);
assert.equal(notify.if, "${{ matrix.repository == 'qinglong-static' }}");
assert.equal(notify.env.GH_TOKEN, '${{ secrets.API_TOKEN }}');
assert.match(notify.run, /bash scripts\/wait-for-mirror.sh/);
assert.equal(notify.env.QL_MIRROR_NOTIFY_REPO, 'whyour/qinglong-static');
assert.equal(notify.env.QL_MIRROR_INCLUDE_VERSION_BRANCH, 'true');
assert.doesNotMatch(
JSON.stringify(job),
/GITEE_TOKEN|git@gitee|sync-static|ssh-keyscan/,
);
const backfill = workflow.jobs['verify-existing-static-gitee'];
assert.match(backfill.if, /workflow_dispatch.*inputs.static_sync_only/);
assert.equal(backfill.needs, undefined);
const verifyExisting = backfill.steps.find((step) => step.run);
assert.equal(verifyExisting.env.MIRROR_REF, '${{ inputs.static_ref }}');
assert.equal(
verifyExisting.env.MIRROR_REF_TYPE,
'${{ inputs.static_ref_type }}',
);
assert.equal(verifyExisting.env.GH_TOKEN, '${{ secrets.API_TOKEN }}');
assert.match(verifyExisting.run, /wait-for-mirror.sh/);
assert.equal(
verifyExisting.env.QL_MIRROR_NOTIFY_REPO,
'whyour/qinglong-static',
);
assert.equal(verifyExisting.env.QL_MIRROR_INCLUDE_VERSION_BRANCH, 'true');
assert.deepEqual(workflow.on.push.branches, ['master', 'develop']);
assert.doesNotMatch(
JSON.stringify(backfill),
/GITEE_TOKEN|git@gitee|sync-static|ssh-keyscan/,
);
for (const name of [
'build-alpine',
'build-debian',
'build-alpine310',
'build-debian310',
]) {
assert.equal(workflow.jobs[name].needs, 'build-static');
}
});