Files
qinglong/test/back/static-mirror-trigger.test.cjs
T
whyour e8b6a30bd9 feat: ship verified platform upgrades with channel bundle releases
Package production dependencies per architecture for online and offline upgrades.
Validate dependencies and isolated backend startup before readiness, repair once
when permitted, and preserve service/data through failed preparation or rollback.

Publish release bundles with master images and expose an opt-in develop build
sharing the development index across Alpine and Debian images.
2026-10-18 22:19:49 +08:00

453 lines
17 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 notify = path.resolve('scripts/wait-for-mirror.sh');
const verify = notify;
const triggerRef = 'refs/heads/gitee-sync-trigger';
const git = (...args) =>
execFileSync('git', args, { encoding: 'utf8', stdio: 'pipe' }).trim();
function fixture(t) {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-mirror-trigger-'));
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
const work = path.join(tmp, 'work');
const source = path.join(tmp, 'source.git');
const mirror = path.join(tmp, 'mirror.git');
for (const repo of [source, mirror]) git('init', '--bare', repo);
git('init', work);
for (const repo of [source, work]) {
git('-C', repo, 'config', 'user.name', 'Fixture');
git('-C', repo, 'config', 'user.email', 'fixture@example.invalid');
}
git('-C', work, 'checkout', '-b', 'develop');
fs.writeFileSync(path.join(work, 'snapshot'), 'static artifact');
git('-C', work, 'add', '.');
git('-C', work, 'commit', '-m', 'snapshot');
git('-C', work, 'push', source, 'develop');
const bin = path.join(tmp, 'bin');
fs.mkdirSync(bin);
// Model GitHub's Git Data API with real Git objects and non-forced ref updates.
// The fixture's post-receive hook simulates Gitee receiving the push event.
fs.writeFileSync(
path.join(bin, 'gh'),
`#!${process.execPath}
const fs = require('node:fs');
const path = require('node:path');
const { execFileSync, spawnSync } = require('node:child_process');
const args = process.argv.slice(2);
const endpoint = args.find((arg) => arg.startsWith('repos/'));
const method = args.includes('--method') ? args[args.indexOf('--method') + 1] : 'GET';
const fields = {};
for (let i = 0; i < args.length; i++) {
if (args[i] === '-f' || args[i] === '-F') {
const field = args[++i];
const separator = field.indexOf('=');
fields[field.slice(0, separator)] = field.slice(separator + 1);
}
}
fs.appendFileSync(process.env.QL_TEST_API_LOG, JSON.stringify({method, endpoint, fields, time: Date.now()}) + '\\n');
const source = process.env.QL_TEST_SOURCE;
const git = (...args) => execFileSync('git', ['-C', source, ...args], {encoding: 'utf8', stdio: 'pipe'}).trim();
const fail = (status) => { console.error('gh: API failure (HTTP ' + status + ')'); process.exit(1); };
const ref = 'refs/heads/gitee-sync-trigger';
const isUpdate = method === 'PATCH' || endpoint.endsWith('/git/refs');
if (process.env.QL_TEST_API_HANG) spawnSync('sleep', ['30'], {stdio: 'ignore'});
if (process.env.QL_TEST_API_REJECT) fail(500);
if (isUpdate && process.env.QL_TEST_COLLISION && !fs.existsSync(process.env.QL_TEST_COLLISION)) {
const current = git('rev-parse', ref);
const tree = git('rev-parse', current + '^{tree}');
const peer = git('commit-tree', tree, '-p', current, '-m', 'Concurrent notifier');
git('update-ref', ref, peer);
fs.writeFileSync(process.env.QL_TEST_COLLISION, peer);
}
if (isUpdate && process.env.QL_TEST_UPDATE_FAILURE && !fs.existsSync(process.env.QL_TEST_UPDATE_FAILURE)) {
fs.writeFileSync(process.env.QL_TEST_UPDATE_FAILURE, 'failed');
fail(500);
}
try {
if (method === 'GET' && endpoint.includes('/git/ref/')) {
console.log(git('rev-parse', '--verify', 'refs/' + endpoint.split('/git/ref/')[1]));
} else if (method === 'GET' && endpoint.includes('/git/commits/')) {
console.log(git('rev-parse', endpoint.split('/git/commits/')[1] + '^{tree}'));
} else if (endpoint.endsWith('/git/trees')) {
const body = JSON.parse(fs.readFileSync(0, 'utf8'));
const entries = body.tree.map((entry) => {
const blob = execFileSync('git', ['-C', source, 'hash-object', '-w', '--stdin'], {encoding: 'utf8', input: entry.content}).trim();
return entry.mode + ' blob ' + blob + '\\t' + entry.path + '\\n';
}).join('');
console.log(execFileSync('git', ['-C', source, 'mktree'], {encoding: 'utf8', input: entries}).trim());
} else if (endpoint.endsWith('/git/commits')) {
const parents = fields['parents[]'] ? ['-p', fields['parents[]']] : [];
console.log(git('commit-tree', fields.tree, ...parents, '-m', fields.message));
} else if (isUpdate) {
const current = spawnSync('git', ['-C', source, 'rev-parse', '--verify', ref], {encoding: 'utf8'});
const old = current.status === 0 ? current.stdout.trim() : '0'.repeat(40);
if (method === 'PATCH') {
if (fields.force !== 'false' || current.status !== 0 || spawnSync('git', ['-C', source, 'merge-base', '--is-ancestor', old, fields.sha]).status !== 0) fail(422);
} else if (current.status === 0 || fields.ref !== ref) fail(422);
git('update-ref', ref, fields.sha, old);
const hook = path.join(source, 'hooks/post-receive');
if (fs.existsSync(hook)) execFileSync(hook, {cwd: source, input: old + ' ' + fields.sha + ' ' + ref + '\\n', env: {...process.env, GIT_DIR: source}, stdio: ['pipe', 'pipe', 'pipe']});
} else fail(400);
} catch (error) { fail(method === 'GET' ? 404 : 422); }
`,
{ mode: 0o755 },
);
const env = {
...process.env,
PATH: `${bin}${path.delimiter}${process.env.PATH}`,
QL_TEST_SOURCE: source,
QL_TEST_API_LOG: path.join(tmp, 'api.log'),
QL_MIRROR_NOTIFY_REPO: 'fixture/source',
QL_MIRROR_INCLUDE_VERSION_BRANCH: 'true',
QL_MIRROR_WAIT_SECONDS: '4',
QL_MIRROR_RETRY_AFTER_SECONDS: '1',
QL_MIRROR_POLL_INTERVAL: '1',
QL_MIRROR_QUERY_TIMEOUT: '1',
};
return { tmp, work, source, mirror, env };
}
test('static notifications use a tiny REST branch with unchanged trees and preserve published refs', (t) => {
const { source, env } = fixture(t);
const snapshot = git('-C', source, 'rev-parse', 'develop');
execFileSync('bash', [notify, source, source, 'develop', 'branch'], {
stdio: 'pipe',
env,
});
const first = git('-C', source, 'rev-parse', triggerRef);
execFileSync('bash', [notify, source, source, 'develop', 'branch'], {
stdio: 'pipe',
env,
});
const second = git('-C', source, 'rev-parse', triggerRef);
assert.notEqual(first, second);
assert.equal(git('-C', source, 'rev-parse', `${second}^`), first);
assert.equal(
git('-C', source, 'ls-tree', '-r', '--name-only', second),
'.gitee-sync-trigger',
);
assert.equal(git('-C', source, 'diff', '--name-only', first, second), '');
assert.equal(git('-C', source, 'rev-parse', 'develop'), snapshot);
assert.match(
git('-C', source, 'log', '-1', '--format=%s', second),
new RegExp(snapshot),
);
});
test('REST notifications retry a newer parent and GitHub 500s with backoff', (t) => {
const { tmp, source, env } = fixture(t);
execFileSync('bash', [notify, source, source, 'develop'], {
stdio: 'pipe',
env,
});
const parent = git('-C', source, 'rev-parse', triggerRef);
const flag = path.join(tmp, 'collision');
const collision = spawnSync('bash', [notify, source, source, 'develop'], {
encoding: 'utf8',
env: { ...env, QL_TEST_COLLISION: flag },
});
assert.equal(collision.status, 0, collision.stderr + collision.stdout);
assert.match(collision.stdout, /notification update failed \(attempt 1\)/);
const peer = fs.readFileSync(flag, 'utf8');
assert.equal(git('-C', source, 'rev-parse', `${peer}^`), parent);
assert.equal(git('-C', source, 'rev-parse', `${triggerRef}^`), peer);
const failed = path.join(tmp, 'failed');
const recovery = spawnSync('bash', [notify, source, source, 'develop'], {
encoding: 'utf8',
env: { ...env, QL_TEST_UPDATE_FAILURE: failed },
});
assert.equal(recovery.status, 0, recovery.stderr + recovery.stdout);
assert.match(recovery.stderr, /HTTP 500/);
const calls = fs
.readFileSync(env.QL_TEST_API_LOG, 'utf8')
.trim()
.split('\n')
.map(JSON.parse);
const updates = calls.filter((call) => call.method === 'PATCH');
assert.ok(updates.every((call) => call.fields.force === 'false'));
const last = updates.slice(-2);
assert.ok(
last[1].time - last[0].time >= 1900,
'failed API updates must back off',
);
});
test('missing snapshots fail before sending any notification', (t) => {
const { source, env } = fixture(t);
const missing = spawnSync('bash', [notify, source, source, 'missing'], {
encoding: 'utf8',
env,
});
assert.equal(missing.status, 1, missing.stderr + missing.stdout);
assert.match(missing.stdout, /Published source ref.*does not exist/);
assert.equal(fs.existsSync(env.QL_TEST_API_LOG), false);
});
test('notification failure does not prevent an already synchronized mirror from passing', (t) => {
const { source, mirror, env } = fixture(t);
git('-C', source, 'push', mirror, 'develop');
const result = spawnSync('bash', [verify, source, mirror, 'develop'], {
encoding: 'utf8',
env: { ...env, QL_TEST_API_REJECT: 'true' },
});
assert.equal(result.status, 0, result.stderr + result.stdout);
assert.match(
result.stdout,
/Could not send the mirror notification; continuing/,
);
assert.match(result.stdout, /Mirror matches the latest source refs/);
});
test('a failed delayed notification keeps polling until the mirror updates', async (t) => {
const { source, mirror, env } = fixture(t);
const child = spawn('bash', [verify, source, mirror, 'develop'], {
env: {
...env,
QL_MIRROR_NOTIFY_ON_START: 'false',
QL_MIRROR_WAIT_SECONDS: '10',
QL_TEST_API_REJECT: 'true',
},
});
let output = '';
let error = '';
let synchronized = false;
child.stderr.on('data', (data) => {
error += data;
});
child.stdout.on('data', (data) => {
output += data;
if (
!synchronized &&
output.includes('Could not read notification ref (attempt 1)')
) {
synchronized = true;
git('-C', source, 'push', mirror, 'develop');
}
});
const status = await new Promise((resolve, reject) => {
child.on('error', reject);
child.on('close', resolve);
});
assert.ok(synchronized);
assert.equal(status, 0, error + output);
assert.match(output, /Mirror matches the latest source refs/);
});
test('failed or hung notifications preserve the mirror polling deadline', (t) => {
const { source, mirror, env } = fixture(t);
for (const failure of ['QL_TEST_API_REJECT', 'QL_TEST_API_HANG']) {
const started = Date.now();
const result = spawnSync('bash', [verify, source, mirror, 'develop'], {
encoding: 'utf8',
timeout: 6000,
env: {
...env,
QL_MIRROR_NOTIFY_ON_START: 'false',
QL_MIRROR_WAIT_SECONDS: '3',
[failure]: 'true',
},
});
assert.equal(result.status, 1, result.stderr + result.stdout);
assert.ok(Date.now() - started < 5000);
assert.match(result.stdout, /::error::Mirror did not match.*within 3s/);
}
});
test('static verification retries a small GitHub notification after a missed sync interval', (t) => {
const { tmp, source, mirror, env } = fixture(t);
const snapshot = git('-C', source, 'rev-parse', 'develop');
git('-C', source, 'update-ref', 'refs/heads/v2.22.0', snapshot);
git('-C', source, 'update-ref', 'refs/tags/v2.22.0', snapshot);
const countFile = path.join(tmp, 'requests');
fs.writeFileSync(
path.join(source, 'hooks/post-receive'),
`#!/bin/sh
while read before after ref; do
if [ "$ref" = refs/heads/gitee-sync-trigger ]; then
count=0
if [ -f "$QL_TEST_COUNT" ]; then count=$(cat "$QL_TEST_COUNT"); fi
count=$((count + 1))
printf '%s' "$count" > "$QL_TEST_COUNT"
if [ "$count" -ge 2 ]; then
git push --quiet "$QL_TEST_MIRROR" refs/heads/v2.22.0:refs/heads/v2.22.0 refs/tags/v2.22.0:refs/tags/v2.22.0
fi
fi
done
`,
{ mode: 0o755 },
);
const result = spawnSync('bash', [verify, source, mirror, 'v2.22.0', 'tag'], {
encoding: 'utf8',
env: {
...env,
QL_TEST_COUNT: countFile,
QL_TEST_MIRROR: mirror,
},
});
assert.equal(result.status, 0, result.stderr + result.stdout);
assert.equal(fs.readFileSync(countFile, 'utf8'), '2');
assert.match(result.stdout, /Retrying the GitHub notification/);
assert.match(result.stdout, /Mirror matches the latest source refs/);
assert.doesNotMatch(result.stdout, /::error::/);
for (const ref of ['refs/heads/v2.22.0', 'refs/tags/v2.22.0']) {
assert.equal(git('-C', mirror, 'rev-parse', ref), snapshot);
}
});
test('static verification still fails when fresh notifications never synchronize the mirror', (t) => {
const { source, mirror, env } = fixture(t);
const result = spawnSync(
'bash',
[verify, source, mirror, 'develop', 'branch'],
{
encoding: 'utf8',
env: {
...env,
QL_MIRROR_WAIT_SECONDS: '2',
},
},
);
assert.equal(result.status, 1, result.stderr + result.stdout);
// The retry contract is checked above. A short polling deadline can expire
// while the REST fixture is still sending its second notification.
const notifications = Number(
git('-C', source, 'rev-list', '--count', triggerRef),
);
assert.ok(notifications >= 1 && notifications <= 2);
assert.match(result.stdout, /::error::Mirror did not match/);
const invalid = spawnSync(
'bash',
[verify, source, mirror, 'develop', 'branch'],
{
encoding: 'utf8',
env: { ...env, QL_MIRROR_WAIT_SECONDS: '0' },
},
);
assert.equal(invalid.status, 2);
});
test('code mirrors that already match do not send another notification', (t) => {
const { source, mirror, env } = fixture(t);
git('-C', source, 'push', mirror, 'develop');
const codeEnv = {
...env,
QL_MIRROR_NOTIFY_REPO: 'fixture/source',
QL_MIRROR_NOTIFY_ON_START: 'false',
QL_MIRROR_INCLUDE_VERSION_BRANCH: 'false',
};
const result = spawnSync(
'bash',
[verify, source, mirror, 'develop', 'branch'],
{
encoding: 'utf8',
env: codeEnv,
},
);
assert.equal(result.status, 0, result.stderr + result.stdout);
assert.match(result.stdout, /Mirror matches the latest source refs/);
assert.doesNotMatch(result.stdout, /Sent a small GitHub push event via REST/);
assert.notEqual(
spawnSync('git', ['-C', source, 'rev-parse', '--verify', triggerRef])
.status,
0,
);
const invalid = spawnSync(
'bash',
[verify, source, mirror, 'develop', 'branch'],
{
encoding: 'utf8',
env: { ...codeEnv, QL_MIRROR_NOTIFY_ON_START: 'later' },
},
);
assert.equal(invalid.status, 2);
});
test('code mirrors notify after the sync interval and verify tags without version branches', (t) => {
const { tmp, work, source, mirror, env } = fixture(t);
const snapshot = git('-C', source, 'rev-parse', 'develop');
git('-C', work, 'tag', '-a', 'v2.22.0', '-m', 'release');
git('-C', work, 'push', source, 'refs/tags/v2.22.0');
const tag = git('-C', source, 'rev-parse', 'refs/tags/v2.22.0');
const countFile = path.join(tmp, 'requests');
fs.writeFileSync(
path.join(source, 'hooks/post-receive'),
`#!/bin/sh
while read before after ref; do
if [ "$ref" = refs/heads/gitee-sync-trigger ]; then
printf 'notification\n' >> "$QL_TEST_COUNT"
git push --quiet "$QL_TEST_MIRROR" refs/tags/v2.22.0:refs/tags/v2.22.0
fi
done
`,
{ mode: 0o755 },
);
const started = Date.now();
const result = spawnSync('bash', [verify, source, mirror, 'v2.22.0', 'tag'], {
encoding: 'utf8',
env: {
...env,
QL_MIRROR_NOTIFY_REPO: 'fixture/source',
QL_MIRROR_NOTIFY_ON_START: 'false',
QL_MIRROR_INCLUDE_VERSION_BRANCH: 'false',
// Bash SECONDS has one-second resolution. Two ticks guarantee a real
// wait across a second boundary; a single tick may take only milliseconds.
QL_MIRROR_RETRY_AFTER_SECONDS: '2',
QL_MIRROR_WAIT_SECONDS: '8',
QL_TEST_COUNT: countFile,
QL_TEST_MIRROR: mirror,
},
});
assert.equal(result.status, 0, result.stderr + result.stdout);
assert.ok(
Date.now() - started >= 900,
'notification should wait for the sync interval',
);
const retry = result.stdout.indexOf('Retrying the GitHub notification');
assert.ok(retry >= 0, result.stdout);
const elapsed = [
...result.stdout
.slice(0, retry)
.matchAll(/Waiting for mirror \(attempt \d+, (\d+)\/8s\)/g),
].at(-1);
assert.ok(elapsed && Number(elapsed[1]) >= 2, result.stdout);
assert.equal(fs.readFileSync(countFile, 'utf8'), 'notification\n');
assert.equal(git('-C', source, 'rev-list', '--count', triggerRef), '1');
assert.equal(git('-C', source, 'rev-parse', 'develop'), snapshot);
assert.equal(git('-C', mirror, 'rev-parse', 'refs/tags/v2.22.0'), tag);
assert.notEqual(
spawnSync('git', [
'-C',
mirror,
'rev-parse',
'--verify',
'refs/heads/v2.22.0',
]).status,
0,
);
});
test('code mirrors that never synchronize fail after one delayed notification', (t) => {
const { source, mirror, env } = fixture(t);
const result = spawnSync(
'bash',
[verify, source, mirror, 'develop', 'branch'],
{
encoding: 'utf8',
env: {
...env,
QL_MIRROR_NOTIFY_REPO: 'fixture/source',
QL_MIRROR_NOTIFY_ON_START: 'false',
QL_MIRROR_WAIT_SECONDS: '3',
QL_MIRROR_RETRY_AFTER_SECONDS: '1',
},
},
);
assert.equal(result.status, 1, result.stderr + result.stdout);
assert.equal(git('-C', source, 'rev-list', '--count', triggerRef), '1');
assert.match(result.stdout, /::error::Mirror did not match/);
});