mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
feat(ql3): close local alpha milestone
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
const {
|
||||
auditLocalAlphaTrialKit,
|
||||
createLocalAlphaTrialKit,
|
||||
createLocalAlphaTrialKitVerificationEvidence,
|
||||
} = require('../../scripts/ql3-local-alpha-trial-kit-bundle.cjs');
|
||||
const {
|
||||
auditLocalAlphaMilestone,
|
||||
auditLocalAlphaMilestoneWorkflow,
|
||||
finalizeLocalAlphaMilestone,
|
||||
parseArguments,
|
||||
} = require('../../scripts/ql3-local-alpha-milestone.cjs');
|
||||
const {
|
||||
createClusterImageSbom,
|
||||
} = require('../../scripts/ql3-cluster-image-sbom.cjs');
|
||||
const {
|
||||
readReleaseIdentity,
|
||||
} = require('../../scripts/lib/ql3-release-identity.cjs');
|
||||
|
||||
const root = path.resolve(__dirname, '../..');
|
||||
const version = readReleaseIdentity(root).version;
|
||||
const revision = 'a'.repeat(40);
|
||||
const runId = '33047425710';
|
||||
const runAttempt = '2';
|
||||
|
||||
function imageInspection(role, architecture, idCharacter) {
|
||||
return {
|
||||
Id: `sha256:${idCharacter.repeat(64)}`,
|
||||
Os: 'linux',
|
||||
Architecture: architecture,
|
||||
Config: {
|
||||
User: '65532:65532',
|
||||
Labels: {
|
||||
'org.opencontainers.image.title':
|
||||
role === 'application'
|
||||
? 'QingLong 3.0 Local Application'
|
||||
: 'QingLong 3.0 Local Operator',
|
||||
'org.opencontainers.image.source': 'https://github.com/whyour/qinglong',
|
||||
'org.opencontainers.image.revision': revision,
|
||||
'org.opencontainers.image.version': version,
|
||||
...(role === 'application'
|
||||
? {
|
||||
'io.qinglong.profile': 'edge,standalone',
|
||||
'io.qinglong.ai': 'excluded',
|
||||
}
|
||||
: {
|
||||
'io.qinglong.lifecycle': 'short-lived',
|
||||
'io.qinglong.authority': 'local-owner-management',
|
||||
'io.qinglong.network': 'none-by-default',
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createBundle(fixtureRoot, architecture, options = {}) {
|
||||
const bundleFixture = path.join(fixtureRoot, architecture);
|
||||
fs.mkdirSync(bundleFixture);
|
||||
const applicationSbom = path.join(bundleFixture, 'application.json');
|
||||
const operatorSbom = path.join(bundleFixture, 'operator.json');
|
||||
const verificationEvidence = path.join(bundleFixture, 'verification.json');
|
||||
const readme = path.join(bundleFixture, 'README-source.md');
|
||||
const outputRoot = path.join(bundleFixture, 'bundle');
|
||||
fs.writeFileSync(
|
||||
applicationSbom,
|
||||
`${JSON.stringify(createClusterImageSbom({ root, image: 'local' }))}\n`,
|
||||
);
|
||||
fs.writeFileSync(
|
||||
operatorSbom,
|
||||
`${JSON.stringify(
|
||||
createClusterImageSbom({ root, image: 'local-operator' }),
|
||||
)}\n`,
|
||||
);
|
||||
fs.writeFileSync(readme, '# Local Alpha Trial Kit\n');
|
||||
const defaultCharacters =
|
||||
architecture === 'amd64'
|
||||
? { application: '1', operator: '2' }
|
||||
: { application: '3', operator: '4' };
|
||||
const characters = options.characters || defaultCharacters;
|
||||
const applicationImage = `qinglong3-local-application:test-${architecture}`;
|
||||
const operatorImage = `qinglong3-local-operator:test-${architecture}`;
|
||||
const adapters = {
|
||||
inspectImage(image) {
|
||||
const role = image.includes('operator') ? 'operator' : 'application';
|
||||
return imageInspection(role, architecture, characters[role]);
|
||||
},
|
||||
saveImages(images, archivePath) {
|
||||
assert.deepEqual(images, [applicationImage, operatorImage]);
|
||||
fs.writeFileSync(
|
||||
archivePath,
|
||||
Buffer.alloc(2048, architecture === 'amd64' ? 7 : 8),
|
||||
{ flag: 'wx' },
|
||||
);
|
||||
},
|
||||
};
|
||||
createLocalAlphaTrialKitVerificationEvidence(
|
||||
{
|
||||
root,
|
||||
output: verificationEvidence,
|
||||
architecture,
|
||||
sourceRevision: revision,
|
||||
applicationImage,
|
||||
operatorImage,
|
||||
repository: 'whyour/qinglong',
|
||||
workflowRef:
|
||||
'whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
|
||||
workflowSha: revision,
|
||||
eventName: 'workflow_dispatch',
|
||||
job: 'local-image',
|
||||
runId,
|
||||
runAttempt: options.runAttempt || runAttempt,
|
||||
},
|
||||
adapters,
|
||||
);
|
||||
createLocalAlphaTrialKit(
|
||||
{
|
||||
root,
|
||||
outputRoot,
|
||||
architecture,
|
||||
sourceRevision: revision,
|
||||
applicationImage,
|
||||
operatorImage,
|
||||
applicationSbom,
|
||||
operatorSbom,
|
||||
verificationEvidence,
|
||||
readme,
|
||||
},
|
||||
adapters,
|
||||
);
|
||||
assert.equal(
|
||||
auditLocalAlphaTrialKit({ bundleRoot: outputRoot }).compatible,
|
||||
true,
|
||||
);
|
||||
return outputRoot;
|
||||
}
|
||||
|
||||
function fixture(t, options = {}) {
|
||||
const fixtureRoot = fs.realpathSync(
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-local-alpha-milestone-')),
|
||||
);
|
||||
t.after(() => fs.rmSync(fixtureRoot, { recursive: true, force: true }));
|
||||
const readme = path.join(fixtureRoot, 'README-source.md');
|
||||
fs.writeFileSync(readme, '# QingLong 3.0 Local Alpha Milestone\n');
|
||||
return {
|
||||
fixtureRoot,
|
||||
readme,
|
||||
outputRoot: path.join(fixtureRoot, 'milestone'),
|
||||
bundles: {
|
||||
amd64: createBundle(fixtureRoot, 'amd64', options.amd64),
|
||||
arm64: createBundle(fixtureRoot, 'arm64', options.arm64),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function finalizeOptions(paths) {
|
||||
return {
|
||||
root,
|
||||
outputRoot: paths.outputRoot,
|
||||
bundles: paths.bundles,
|
||||
readme: paths.readme,
|
||||
sourceRevision: revision,
|
||||
repository: 'whyour/qinglong',
|
||||
workflowRef: 'whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
|
||||
workflowSha: revision,
|
||||
eventName: 'workflow_dispatch',
|
||||
runId,
|
||||
runAttempt,
|
||||
};
|
||||
}
|
||||
|
||||
test('finalizes two exact native trial kits into one closed milestone index', (t) => {
|
||||
const paths = fixture(t);
|
||||
const manifest = finalizeLocalAlphaMilestone(finalizeOptions(paths));
|
||||
assert.equal(manifest.schema, 'qinglong/alpha-local-milestone@v1');
|
||||
assert.equal(manifest.sourceRevision, revision);
|
||||
assert.deepEqual(Object.keys(manifest.artifacts), ['amd64', 'arm64']);
|
||||
assert.equal(
|
||||
manifest.artifacts.amd64.artifactName,
|
||||
`ql3-alpha-${revision}-local-amd64`,
|
||||
);
|
||||
assert.equal(
|
||||
manifest.artifacts.arm64.artifactName,
|
||||
`ql3-alpha-${revision}-local-arm64`,
|
||||
);
|
||||
assert.notEqual(
|
||||
manifest.artifacts.amd64.archiveSha256,
|
||||
manifest.artifacts.arm64.archiveSha256,
|
||||
);
|
||||
assert.deepEqual(fs.readdirSync(paths.outputRoot).sort(), [
|
||||
'README.md',
|
||||
'SHA256SUMS',
|
||||
'manifest.json',
|
||||
]);
|
||||
const report = auditLocalAlphaMilestone({
|
||||
milestoneRoot: paths.outputRoot,
|
||||
});
|
||||
assert.equal(report.compatible, true);
|
||||
assert.deepEqual(report.architectures, ['amd64', 'arm64']);
|
||||
assert.equal(report.workflowRunId, runId);
|
||||
assert.equal(report.workflowRunAttempt, runAttempt);
|
||||
});
|
||||
|
||||
test('rejects a trial kit from another run attempt before publishing', (t) => {
|
||||
const paths = fixture(t, { arm64: { runAttempt: '1' } });
|
||||
assert.throws(
|
||||
() => finalizeLocalAlphaMilestone(finalizeOptions(paths)),
|
||||
/detached from the milestone run/,
|
||||
);
|
||||
assert.equal(fs.existsSync(paths.outputRoot), false);
|
||||
});
|
||||
|
||||
test('rejects cross-architecture image or archive identity reuse', (t) => {
|
||||
const paths = fixture(t, {
|
||||
arm64: { characters: { application: '1', operator: '2' } },
|
||||
});
|
||||
assert.throws(
|
||||
() => finalizeLocalAlphaMilestone(finalizeOptions(paths)),
|
||||
/architecture subjects must be distinct/,
|
||||
);
|
||||
assert.equal(fs.existsSync(paths.outputRoot), false);
|
||||
});
|
||||
|
||||
test('offline milestone audit rejects mutation and extra files', (t) => {
|
||||
const paths = fixture(t);
|
||||
finalizeLocalAlphaMilestone(finalizeOptions(paths));
|
||||
fs.appendFileSync(path.join(paths.outputRoot, 'README.md'), 'tamper\n');
|
||||
assert.throws(
|
||||
() => auditLocalAlphaMilestone({ milestoneRoot: paths.outputRoot }),
|
||||
/README differs/,
|
||||
);
|
||||
fs.writeFileSync(path.join(paths.outputRoot, 'credential.txt'), 'secret');
|
||||
assert.throws(
|
||||
() => auditLocalAlphaMilestone({ milestoneRoot: paths.outputRoot }),
|
||||
/file set is not closed/,
|
||||
);
|
||||
});
|
||||
|
||||
test('workflow audit requires full-CI needs, scoped packaging and finalizer order', () => {
|
||||
const report = auditLocalAlphaMilestoneWorkflow(root);
|
||||
assert.equal(report.compatible, true);
|
||||
assert.deepEqual(report.findings, []);
|
||||
assert.equal(report.requiredNeeds.includes('cluster-postgres-ha'), true);
|
||||
});
|
||||
|
||||
test('workflow audit rejects a partial milestone finalizer', (t) => {
|
||||
const fixtureRoot = fs.realpathSync(
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-local-alpha-workflow-')),
|
||||
);
|
||||
t.after(() => fs.rmSync(fixtureRoot, { recursive: true, force: true }));
|
||||
fs.mkdirSync(path.join(fixtureRoot, '.github/workflows'), {
|
||||
recursive: true,
|
||||
});
|
||||
const workflow = fs
|
||||
.readFileSync(path.join(root, '.github/workflows/ql3-ci.yml'), 'utf8')
|
||||
.replace(' - cluster-postgres-ha\n', '')
|
||||
.replace(
|
||||
'scripts/ql3-local-alpha-milestone.cjs',
|
||||
'scripts/unreviewed-finalizer.cjs',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(fixtureRoot, '.github/workflows/ql3-ci.yml'),
|
||||
workflow,
|
||||
);
|
||||
const report = auditLocalAlphaMilestoneWorkflow(fixtureRoot);
|
||||
assert.equal(report.compatible, false);
|
||||
assert.equal(
|
||||
report.findings.includes('MILESTONE_FINALIZER_CONTRACT_DRIFT'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('CLI grammar separates finalization, index audit and workflow audit', () => {
|
||||
assert.deepEqual(
|
||||
parseArguments(['--mode=audit', '--milestone=/tmp/ql3-alpha-milestone']),
|
||||
{
|
||||
mode: 'audit',
|
||||
milestoneRoot: '/tmp/ql3-alpha-milestone',
|
||||
},
|
||||
);
|
||||
assert.deepEqual(
|
||||
parseArguments(['--mode=audit-workflow', `--root=${root}`]),
|
||||
{ mode: 'audit-workflow', root },
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
parseArguments([
|
||||
'--mode=audit',
|
||||
'--milestone=/tmp/ql3-alpha-milestone',
|
||||
'--allow-partial=true',
|
||||
]),
|
||||
/audit arguments are invalid/,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user