fix(ci): restore ql3 multi-profile gates

This commit is contained in:
whyour
2026-08-22 13:44:19 +08:00
parent f1cca9a3ed
commit c8ad5ea368
17 changed files with 320 additions and 111 deletions
@@ -55,6 +55,7 @@ const POSTGRES_ROLE_NAMES = Object.freeze([
'ql3_migration',
'ql3_package_executor',
'ql3_package_manager',
'ql3_run_manager',
'ql3_runtime',
'ql3_worker_credential_executor',
'ql3_worker_credential_manager',
@@ -53,6 +53,7 @@ const ROLE_NAMES = Object.freeze([
'ql3_migration',
'ql3_package_executor',
'ql3_package_manager',
'ql3_run_manager',
'ql3_runtime',
'ql3_worker_credential_executor',
'ql3_worker_credential_manager',
@@ -733,10 +734,14 @@ async function main() {
ql3_migration: randomSecret(),
ql3_runtime: randomSecret(),
ql3_admin: randomSecret(),
ql3_ai_credential_manager: randomSecret(),
ql3_ai_credential_tester: randomSecret(),
ql3_ai_maintenance: randomSecret(),
ql3_automation_manager: randomSecret(),
ql3_approval_manager: randomSecret(),
ql3_package_manager: randomSecret(),
ql3_package_executor: randomSecret(),
ql3_run_manager: randomSecret(),
ql3_worker_credential_manager: randomSecret(),
ql3_worker_credential_executor: randomSecret(),
ql3_worker_ingress: randomSecret(),
+33 -16
View File
@@ -16,6 +16,7 @@ const QL3_VERSION = readReleaseIdentity(DEFAULT_ROOT).version;
const OCI_INDEX_MEDIA_TYPE = 'application/vnd.oci.image.index.v1+json';
const OCI_MANIFEST_MEDIA_TYPE = 'application/vnd.oci.image.manifest.v1+json';
const OCI_CONFIG_MEDIA_TYPE = 'application/vnd.oci.image.config.v1+json';
const OCI_EMPTY_CONFIG_MEDIA_TYPE = 'application/vnd.oci.empty.v1+json';
const OCI_LAYER_MEDIA_TYPE = 'application/vnd.oci.image.layer.v1.tar+gzip';
const IN_TOTO_MEDIA_TYPE = 'application/vnd.in-toto+json';
const SPDX_PREDICATE_TYPE = 'https://spdx.dev/Document';
@@ -447,23 +448,39 @@ function auditAttestation(
OCI_MANIFEST_MEDIA_TYPE,
MAX_INDEX_BYTES,
);
const config = blobReader.readJson(
manifest.config,
OCI_CONFIG_MEDIA_TYPE,
MAX_CONFIG_BYTES,
);
if (
config.architecture !== 'unknown' ||
config.os !== 'unknown' ||
JSON.stringify(config.config) !== JSON.stringify({}) ||
!Array.isArray(config.rootfs?.diff_ids) ||
!Array.isArray(manifest.layers) ||
JSON.stringify(config.rootfs.diff_ids) !==
JSON.stringify(manifest.layers.map((layer) => layer.digest))
) {
throw new Error(
'OCI attestation config must bind an empty unknown platform',
if (manifest.config?.mediaType === OCI_EMPTY_CONFIG_MEDIA_TYPE) {
normalizeDescriptor(
manifest.config,
OCI_EMPTY_CONFIG_MEDIA_TYPE,
MAX_CONFIG_BYTES,
);
const emptyConfig = blobReader.read(
manifest.config,
MAX_CONFIG_BYTES,
true,
);
if (emptyConfig.toString('utf8') !== '{}') {
throw new Error('OCI attestation empty config must be canonical');
}
} else {
const config = blobReader.readJson(
manifest.config,
OCI_CONFIG_MEDIA_TYPE,
MAX_CONFIG_BYTES,
);
if (
config.architecture !== 'unknown' ||
config.os !== 'unknown' ||
JSON.stringify(config.config) !== JSON.stringify({}) ||
!Array.isArray(config.rootfs?.diff_ids) ||
!Array.isArray(manifest.layers) ||
JSON.stringify(config.rootfs.diff_ids) !==
JSON.stringify(manifest.layers.map((layer) => layer.digest))
) {
throw new Error(
'OCI attestation config must bind an empty unknown platform',
);
}
}
if (!Array.isArray(manifest.layers) || manifest.layers.length !== 2) {
throw new Error('OCI attestation must contain exactly SBOM and provenance');
@@ -0,0 +1,119 @@
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const repositoryRoot = path.resolve(__dirname, '..');
const HOST_BOUND_TESTS = Object.freeze({
'packages/ql3-local-owner-cli/test/adoptedDeploymentBundle.test.cjs':
'requires repository-owned deployment entrypoint material',
'packages/ql3-local-owner-cli/test/localDeployment.test.cjs':
'exercises mutable deployment staging and host ownership',
'packages/ql3-local-owner-cli/test/reconciliationCapturePrepare.test.cjs':
'exercises sealed SQLite WAL and SHM capture assets',
'packages/ql3-local-owner-cli/test/serviceBridgeRoot.test.cjs':
'installs systemd and OpenRC descriptors into host service directories',
});
function collectReadonlyOwnerTests(root = repositoryRoot) {
const packagesRoot = path.join(root, 'packages');
const packageNames = fs
.readdirSync(packagesRoot, { withFileTypes: true })
.filter(
(entry) =>
entry.isDirectory() &&
(entry.name === 'ql3-local-command-file' ||
entry.name.startsWith('ql3-local-owner-')),
)
.map(({ name }) => name)
.sort();
const discovered = [];
for (const packageName of packageNames) {
const testRoot = path.join(packagesRoot, packageName, 'test');
if (!fs.existsSync(testRoot)) continue;
for (const entry of fs.readdirSync(testRoot, { withFileTypes: true })) {
if (entry.isFile() && entry.name.endsWith('.test.cjs')) {
discovered.push(
path.posix.join('packages', packageName, 'test', entry.name),
);
}
}
}
discovered.sort();
const missingClassifications = Object.keys(HOST_BOUND_TESTS).filter(
(file) => !discovered.includes(file),
);
if (missingClassifications.length !== 0) {
throw new Error(
`reviewed host-bound Owner tests are missing: ${missingClassifications.join(
', ',
)}`,
);
}
const tests = discovered.filter((file) => !(file in HOST_BOUND_TESTS));
if (tests.length === 0) {
throw new Error('read-only Owner test plan is empty');
}
return Object.freeze({
tests: Object.freeze(tests),
hostBound: HOST_BOUND_TESTS,
});
}
function assertWriteRejected(target) {
try {
fs.writeFileSync(target, 'unexpected-write', { flag: 'wx' });
} catch (error) {
if (error?.code === 'EROFS' || error?.code === 'EACCES') return;
throw error;
}
try {
fs.unlinkSync(target);
} finally {
throw new Error(`read-only boundary accepted a write: ${target}`);
}
}
function assertContainerBoundary(mode) {
if (!['root', 'nonroot'].includes(mode)) {
throw new Error(
'usage: ql3-local-owner-readonly-contract.cjs --mode=root|nonroot',
);
}
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
if (mode === 'root' ? uid !== 0 : uid === null || uid === 0) {
throw new Error(`Owner read-only actor identity is invalid for ${mode}`);
}
const temporaryProbe = fs.mkdtempSync('/tmp/ql3-owner-readonly-');
fs.rmSync(temporaryProbe, { recursive: true });
assertWriteRejected(`/ql3-owner-readonly-root-${process.pid}`);
assertWriteRejected(
path.join(repositoryRoot, `.ql3-owner-readonly-workspace-${process.pid}`),
);
}
function main(argv = process.argv.slice(2)) {
const modeArgument = argv.find((value) => value.startsWith('--mode='));
const mode = modeArgument?.slice('--mode='.length);
assertContainerBoundary(mode);
const plan = collectReadonlyOwnerTests();
const result = spawnSync(
process.execPath,
['--test', '--test-concurrency=1', ...plan.tests],
{ cwd: repositoryRoot, stdio: 'inherit' },
);
if (result.error) throw result.error;
if (result.signal) {
throw new Error(`read-only Owner tests terminated by ${result.signal}`);
}
if (result.status !== 0) process.exitCode = result.status ?? 1;
}
if (require.main === module) main();
module.exports = {
HOST_BOUND_TESTS,
collectReadonlyOwnerTests,
};
@@ -31,6 +31,7 @@ const KIND_NODE_IMAGE =
const POSTGRES_IMAGE = 'postgres:18.4-bookworm';
const POSTGRES_INDEX_DIGEST =
'sha256:1961f96e6029a02c3812d7cb329a3b03a3ac2bb067058dec17b0f5596aca9296';
const POSTGRES_IMAGE_REFERENCE = `${POSTGRES_IMAGE}@${POSTGRES_INDEX_DIGEST}`;
const POSTGRES_REPOSITORY_DIGEST = `postgres@${POSTGRES_INDEX_DIGEST}`;
const DEFAULT_ADMIN_IMAGE = 'qinglong3-cluster-admin:ql3-plugin-recovery-e2e';
const DEFAULT_CONTROL_IMAGE =
@@ -277,13 +278,13 @@ function buildImages(revision) {
}
function ensurePostgresImage() {
if (!imageExists(POSTGRES_IMAGE)) {
run(DOCKER, ['pull', `${POSTGRES_IMAGE}@${POSTGRES_INDEX_DIGEST}`], {
if (!imageExists(POSTGRES_IMAGE_REFERENCE)) {
run(DOCKER, ['pull', POSTGRES_IMAGE_REFERENCE], {
label: 'pull digest-pinned PostgreSQL 18.4 fixture image',
});
}
const inspection = JSON.parse(
run(DOCKER, ['image', 'inspect', POSTGRES_IMAGE], {
run(DOCKER, ['image', 'inspect', POSTGRES_IMAGE_REFERENCE], {
capture: true,
quiet: true,
}).stdout,
@@ -485,7 +486,7 @@ createdb --username "$POSTGRES_USER" --owner ql3_migration qinglong
containers: [
{
name: 'postgres',
image: POSTGRES_IMAGE,
image: POSTGRES_IMAGE_REFERENCE,
imagePullPolicy: 'Never',
env: [
{ name: 'POSTGRES_USER', value: 'postgres' },
@@ -1726,7 +1727,11 @@ async function main(argv = process.argv.slice(2)) {
},
);
created = true;
for (const image of [ADMIN_IMAGE, CONTROL_IMAGE, POSTGRES_IMAGE]) {
for (const image of [
ADMIN_IMAGE,
CONTROL_IMAGE,
POSTGRES_IMAGE_REFERENCE,
]) {
kind(
[
'load',
+1 -1
View File
@@ -117,7 +117,7 @@ function auditPostgresHaEvidence(report) {
'POSTGRES_REPO_DIGESTS_MISSING',
);
add(
postgres.architecture === 'x64' || postgres.architecture === 'arm64',
postgres.architecture === 'amd64' || postgres.architecture === 'arm64',
'POSTGRES_ARCHITECTURE_INVALID',
);
add(
@@ -81,6 +81,7 @@ const ROLE_NAMES = Object.freeze([
'ql3_package_executor',
'ql3_automation_manager',
'ql3_approval_manager',
'ql3_run_manager',
'ql3_worker_credential_manager',
'ql3_worker_credential_executor',
'ql3_worker_ingress',