mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-17 07:20:51 +08:00
* fix: resolve Debian regression findings * fix: reject failed APT index refreshes and simplify SSH home lookup
258 lines
9.4 KiB
JavaScript
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');
|
|
}
|
|
});
|