mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
fix(ci): restore ql3 multi-profile gates
This commit is contained in:
@@ -140,6 +140,10 @@ jobs:
|
||||
run: node -e "if (process.arch !== '${{ matrix.arch }}') throw new Error('unexpected architecture ' + process.arch)"
|
||||
- name: Install workspace dependencies without lifecycle scripts
|
||||
run: pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Initialize the public test environment
|
||||
run: |
|
||||
cp .env.example .env
|
||||
mkdir -p data/db
|
||||
- name: Rebuild the reviewed native SQLite binding
|
||||
run: pnpm rebuild @whyour/sqlite3
|
||||
- name: Build benchmark targets once
|
||||
@@ -855,9 +859,7 @@ jobs:
|
||||
--volume "${{ github.workspace }}:/workspace:ro"
|
||||
--workdir /workspace
|
||||
node:24.18.0-bookworm-slim
|
||||
node --test --test-concurrency=1
|
||||
packages/ql3-local-command-file/test/*.test.cjs
|
||||
packages/ql3-local-owner-*/test/*.test.cjs
|
||||
node scripts/ql3-local-owner-readonly-contract.cjs --mode=root
|
||||
- name: Exercise Owner authorities as non-root in a read-only container
|
||||
run: >-
|
||||
docker run --rm --read-only
|
||||
@@ -870,9 +872,7 @@ jobs:
|
||||
--volume "${{ github.workspace }}:/workspace:ro"
|
||||
--workdir /workspace
|
||||
node:24.18.0-bookworm-slim
|
||||
node --test --test-concurrency=1
|
||||
packages/ql3-local-command-file/test/*.test.cjs
|
||||
packages/ql3-local-owner-*/test/*.test.cjs
|
||||
node scripts/ql3-local-owner-readonly-contract.cjs --mode=nonroot
|
||||
- name: Build and audit production-only local Profile artifacts
|
||||
run: |
|
||||
pnpm audit:artifact:edge:ql3
|
||||
@@ -963,18 +963,21 @@ jobs:
|
||||
docker exec ${{ job.services.postgres.id }} psql -U postgres -d postgres -v ON_ERROR_STOP=1 -c "ALTER DATABASE ql3_contract OWNER TO ql3_migration"
|
||||
- name: Create schema and exact grants through the reviewed migration stream
|
||||
env:
|
||||
QL3_TEST_POSTGRES_URL: postgresql://ql3_migration:ql3_migration_test@127.0.0.1:5432/ql3_contract
|
||||
run: node --test packages/ql3-cluster-postgres/test/postgres.integration.test.cjs
|
||||
QL3_POSTGRES_MIGRATION_URL: postgresql://ql3_migration:ql3_migration_test@127.0.0.1:5432/ql3_contract
|
||||
QL3_POSTGRES_TLS_MODE: disable
|
||||
QL3_POSTGRES_ALLOW_INSECURE: 'true'
|
||||
run: node packages/ql3-cluster-postgres/dist/migration/migrationCli.js
|
||||
- name: Test readiness, isolated roles, shared Repositories, rollback and SQLSTATE mapping
|
||||
env:
|
||||
QL3_TEST_POSTGRES_MIGRATION_URL: postgresql://ql3_migration:ql3_migration_test@127.0.0.1:5432/ql3_contract
|
||||
QL3_TEST_POSTGRES_RUNTIME_URL: postgresql://ql3_runtime:ql3_runtime_test@127.0.0.1:5432/ql3_contract
|
||||
QL3_TEST_POSTGRES_ADMIN_URL: postgresql://ql3_admin:ql3_admin_test@127.0.0.1:5432/ql3_contract
|
||||
QL3_TEST_POSTGRES_AUTOMATION_MANAGER_URL: postgresql://ql3_automation_manager:ql3_automation_manager_test@127.0.0.1:5432/ql3_contract
|
||||
QL3_TEST_POSTGRES_RUN_MANAGER_URL: postgresql://ql3_run_manager:ql3_run_manager_test@127.0.0.1:5432/ql3_contract
|
||||
QL3_TEST_POSTGRES_PACKAGE_MANAGER_URL: postgresql://ql3_package_manager:ql3_package_manager_test@127.0.0.1:5432/ql3_contract
|
||||
QL3_TEST_POSTGRES_PACKAGE_EXECUTOR_URL: postgresql://ql3_package_executor:ql3_package_executor_test@127.0.0.1:5432/ql3_contract
|
||||
QL3_TEST_POSTGRES_WORKER_INGRESS_URL: postgresql://ql3_worker_ingress:ql3_worker_ingress_test@127.0.0.1:5432/ql3_contract
|
||||
run: node --test packages/ql3-cluster-postgres/test/postgres.integration.test.cjs
|
||||
run: node --test --test-concurrency=1 packages/ql3-cluster-postgres/test/postgres.integration.test.cjs
|
||||
- name: Test automation management concurrency and post-commit response loss over mTLS
|
||||
env:
|
||||
QL3_TEST_POSTGRES_URL: postgresql://ql3_migration:ql3_migration_test@127.0.0.1:5432/ql3_contract
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
TaskStartUnavailableError,
|
||||
normalizeTaskStartCommand,
|
||||
normalizeTaskStartResult,
|
||||
type TaskStartAllowedRole,
|
||||
type TaskStartCommand,
|
||||
type TaskStartRepository,
|
||||
type TaskStartResult,
|
||||
@@ -32,12 +31,6 @@ import {
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
|
||||
const ALLOWED_ROLES = new Set<TaskStartAllowedRole>([
|
||||
'owner',
|
||||
'admin',
|
||||
'operator',
|
||||
]);
|
||||
|
||||
function unavailable(options?: ErrorOptions): TaskStartUnavailableError {
|
||||
return new TaskStartUnavailableError(options);
|
||||
}
|
||||
@@ -193,34 +186,30 @@ export class PostgresTaskStartRepository implements TaskStartRepository {
|
||||
try {
|
||||
await configurePostgresDefinitionTransaction(client);
|
||||
began = true;
|
||||
const project = await client.query<Row>(`
|
||||
SELECT status AS "projectStatus", version AS "projectVersion"
|
||||
FROM "ql3"."projects" WHERE id = $1 FOR UPDATE
|
||||
`, [command.projectId]);
|
||||
if (project.rows.length === 0) throw new TaskStartNotFoundError();
|
||||
const authorization = await client.query<Row>(
|
||||
`SELECT "ql3"."lock_run_management_policy_fence"(
|
||||
$1::varchar, $2::varchar, $3::varchar, $4::integer, $5::integer
|
||||
) AS "matches"`,
|
||||
[
|
||||
command.projectId,
|
||||
command.subject.type,
|
||||
command.subject.id,
|
||||
command.policyFence.projectVersion,
|
||||
command.policyFence.bindingVersion,
|
||||
],
|
||||
);
|
||||
if (authorization.rows.length !== 1) throw unavailable();
|
||||
if (authorization.rows[0]?.matches !== true) {
|
||||
const project = await client.query<Row>(
|
||||
`SELECT EXISTS (
|
||||
SELECT 1 FROM "ql3"."projects" WHERE id = $1
|
||||
) AS "exists"`,
|
||||
[command.projectId],
|
||||
);
|
||||
if (project.rows.length !== 1) throw unavailable();
|
||||
const binding = await client.query<Row>(`
|
||||
SELECT version AS "bindingVersion", state AS "bindingState",
|
||||
role AS "bindingRole"
|
||||
FROM "ql3"."project_role_bindings"
|
||||
WHERE project_id = $1 AND subject_type = $2 AND subject_id = $3
|
||||
ORDER BY version DESC LIMIT 1
|
||||
FOR SHARE
|
||||
`, [command.projectId, command.subject.type, command.subject.id]);
|
||||
const currentProject = project.rows[0]!;
|
||||
const currentBinding = binding.rows[0];
|
||||
if (
|
||||
text(currentProject, 'projectStatus') !== 'active' ||
|
||||
integer(currentProject, 'projectVersion') !==
|
||||
command.policyFence.projectVersion ||
|
||||
!currentBinding ||
|
||||
integer(currentBinding, 'bindingVersion') !==
|
||||
command.policyFence.bindingVersion ||
|
||||
text(currentBinding, 'bindingState') !== 'active' ||
|
||||
!ALLOWED_ROLES.has(
|
||||
text(currentBinding, 'bindingRole') as TaskStartAllowedRole,
|
||||
)
|
||||
) {
|
||||
if (!postgresRequiredBoolean(project.rows[0]!.exists, unavailable)) {
|
||||
throw new TaskStartNotFoundError();
|
||||
}
|
||||
throw new TaskStartFenceRejectedError('authorization_changed');
|
||||
}
|
||||
|
||||
@@ -261,7 +250,6 @@ export class PostgresTaskStartRepository implements TaskStartRepository {
|
||||
AND revision.task_id = head.task_id
|
||||
AND revision.revision = head.current_revision
|
||||
WHERE head.project_id = $1 AND head.task_id = $2
|
||||
FOR UPDATE OF head
|
||||
`, [command.projectId, command.taskId]);
|
||||
if (task.rows.length === 0) throw new TaskStartNotFoundError();
|
||||
if (task.rows.length !== 1) throw unavailable();
|
||||
|
||||
@@ -626,6 +626,8 @@ const adminConnectionString =
|
||||
const automationManagerConnectionString =
|
||||
process.env.QL3_TEST_POSTGRES_AUTOMATION_MANAGER_URL ??
|
||||
migrationConnectionString;
|
||||
const runManagerConnectionString =
|
||||
process.env.QL3_TEST_POSTGRES_RUN_MANAGER_URL ?? migrationConnectionString;
|
||||
const packageManagerConnectionString =
|
||||
process.env.QL3_TEST_POSTGRES_PACKAGE_MANAGER_URL ??
|
||||
migrationConnectionString;
|
||||
@@ -678,6 +680,8 @@ if (!migrationConnectionString) {
|
||||
? adminConnectionString
|
||||
: role === 'automation-manager'
|
||||
? automationManagerConnectionString
|
||||
: role === 'run-manager'
|
||||
? runManagerConnectionString
|
||||
: role === 'package-manager'
|
||||
? packageManagerConnectionString
|
||||
: role === 'package-executor'
|
||||
@@ -1045,7 +1049,7 @@ if (!migrationConnectionString) {
|
||||
const taskId = 'task-start-command';
|
||||
const subjectId = 'usr_task_start_integration';
|
||||
const migrationDatabase = await open('migration');
|
||||
let runtimeDatabase;
|
||||
let runManagerDatabase;
|
||||
try {
|
||||
await runPostgresMigrations({ pool: migrationDatabase.pool });
|
||||
await migrationDatabase.pool.query(
|
||||
@@ -1102,11 +1106,13 @@ if (!migrationConnectionString) {
|
||||
})
|
||||
).definition;
|
||||
|
||||
runtimeDatabase =
|
||||
runtimeConnectionString === migrationConnectionString
|
||||
runManagerDatabase =
|
||||
runManagerConnectionString === migrationConnectionString
|
||||
? migrationDatabase
|
||||
: await open('runtime');
|
||||
const repository = new PostgresTaskStartRepository(runtimeDatabase.pool);
|
||||
: await open('run-manager');
|
||||
const repository = new PostgresTaskStartRepository(
|
||||
runManagerDatabase.pool,
|
||||
);
|
||||
const command = {
|
||||
projectId,
|
||||
taskId,
|
||||
@@ -1158,8 +1164,8 @@ if (!migrationConnectionString) {
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
if (runtimeDatabase && runtimeDatabase !== migrationDatabase) {
|
||||
await runtimeDatabase.close();
|
||||
if (runManagerDatabase && runManagerDatabase !== migrationDatabase) {
|
||||
await runManagerDatabase.close();
|
||||
}
|
||||
await migrationDatabase.close();
|
||||
}
|
||||
@@ -2193,6 +2199,9 @@ if (!migrationConnectionString) {
|
||||
const migrationDatabase = await open('migration');
|
||||
try {
|
||||
await runPostgresMigrations({ pool: migrationDatabase.pool });
|
||||
await migrationDatabase.pool.query(
|
||||
'TRUNCATE TABLE "ql3"."runs" CASCADE',
|
||||
);
|
||||
await observeContractPublisherTrust(migrationDatabase.pool);
|
||||
await migrationDatabase.pool.query(
|
||||
`INSERT INTO "ql3"."projects" (
|
||||
@@ -2577,6 +2586,9 @@ if (!migrationConnectionString) {
|
||||
const migrationDatabase = await open('migration');
|
||||
try {
|
||||
await runPostgresMigrations({ pool: migrationDatabase.pool });
|
||||
await migrationDatabase.pool.query(
|
||||
'TRUNCATE TABLE "ql3"."plugin_package_installs" CASCADE',
|
||||
);
|
||||
await observeContractPublisherTrust(migrationDatabase.pool);
|
||||
await migrationDatabase.pool.query(
|
||||
`INSERT INTO "ql3"."projects" (
|
||||
@@ -2589,7 +2601,7 @@ if (!migrationConnectionString) {
|
||||
await migrationDatabase.close();
|
||||
}
|
||||
const executorDatabase = await open('package-executor');
|
||||
const adminDatabase = await open('admin');
|
||||
const automationManagerDatabase = await open('automation-manager');
|
||||
return {
|
||||
repository: new PostgresPluginPackageTaskReconciliationRepository(
|
||||
executorDatabase.pool,
|
||||
@@ -2607,11 +2619,14 @@ if (!migrationConnectionString) {
|
||||
),
|
||||
),
|
||||
taskRepository: new PostgresTaskDefinitionRepository(
|
||||
adminDatabase.pool,
|
||||
automationManagerDatabase.pool,
|
||||
fixture.registry,
|
||||
),
|
||||
close: async () => {
|
||||
await Promise.all([executorDatabase.close(), adminDatabase.close()]);
|
||||
await Promise.all([
|
||||
executorDatabase.close(),
|
||||
automationManagerDatabase.close(),
|
||||
]);
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -136,19 +136,12 @@ function fixture(options = {}) {
|
||||
normalized.startsWith('SELECT set_config') ||
|
||||
normalized.startsWith('INSERT INTO')
|
||||
) return { rows: [], rowCount: normalized.startsWith('INSERT') ? 1 : 0 };
|
||||
if (normalized.includes('FROM "ql3"."projects"')) {
|
||||
const rows = options.projectRows ?? [{
|
||||
projectStatus: 'active',
|
||||
projectVersion: 2,
|
||||
}];
|
||||
if (normalized.includes('lock_run_management_policy_fence')) {
|
||||
const rows = options.authorizationRows ?? [{ matches: true }];
|
||||
return { rows, rowCount: rows.length };
|
||||
}
|
||||
if (normalized.includes('FROM "ql3"."project_role_bindings"')) {
|
||||
const rows = options.bindingRows ?? [{
|
||||
bindingVersion: 3,
|
||||
bindingState: 'active',
|
||||
bindingRole: 'operator',
|
||||
}];
|
||||
if (normalized.includes('SELECT EXISTS')) {
|
||||
const rows = options.projectExistenceRows ?? [{ exists: true }];
|
||||
return { rows, rowCount: rows.length };
|
||||
}
|
||||
if (normalized.includes('FROM "ql3"."runs"')) {
|
||||
@@ -203,11 +196,11 @@ test('revalidates Policy and Task/execution digests before one atomic Run aggreg
|
||||
executionRevisionDigest: EXECUTION.contentDigest,
|
||||
createdAtMs: 1_000,
|
||||
});
|
||||
const project = calls.findIndex(({ sql }) => sql.includes('FROM "ql3"."projects"'));
|
||||
const binding = calls.findIndex(({ sql }) => sql.includes('project_role_bindings'));
|
||||
const authorization = calls.findIndex(({ sql }) =>
|
||||
sql.includes('lock_run_management_policy_fence'));
|
||||
const task = calls.findIndex(({ sql }) => sql.includes('task_definitions'));
|
||||
const execution = calls.findIndex(({ sql }) => sql.includes('task_execution_revisions'));
|
||||
assert.ok(project < binding && binding < task && task < execution);
|
||||
assert.ok(authorization < task && task < execution);
|
||||
assert.equal(calls.filter(({ sql }) => sql.startsWith('INSERT INTO')).length, 4);
|
||||
assert.equal(calls.some(({ sql }) => sql === 'COMMIT'), true);
|
||||
});
|
||||
@@ -273,15 +266,17 @@ test('returns the original durable identities for an exact replay', async () =>
|
||||
|
||||
test('rejects missing, authorization, definition and disabled fences', async () => {
|
||||
await assert.rejects(
|
||||
fixture({ projectRows: [] }).repository.startTask(command()),
|
||||
fixture({
|
||||
authorizationRows: [{ matches: null }],
|
||||
projectExistenceRows: [{ exists: false }],
|
||||
}).repository.startTask(command()),
|
||||
TaskStartNotFoundError,
|
||||
);
|
||||
await assert.rejects(
|
||||
fixture({ bindingRows: [{
|
||||
bindingVersion: 4,
|
||||
bindingState: 'revoked',
|
||||
bindingRole: null,
|
||||
}] }).repository.startTask(command()),
|
||||
fixture({
|
||||
authorizationRows: [{ matches: false }],
|
||||
projectExistenceRows: [{ exists: true }],
|
||||
}).repository.startTask(command()),
|
||||
(error) =>
|
||||
error instanceof TaskStartFenceRejectedError &&
|
||||
error.reason === 'authorization_changed',
|
||||
|
||||
@@ -170,6 +170,7 @@ test('accepts timeout only when starting ACK supplied a durable deadline', async
|
||||
}));
|
||||
assert.equal(result.status, 'started');
|
||||
assert.equal(barriers, 1);
|
||||
await waitForReceipt(roots.receiptRoot);
|
||||
});
|
||||
|
||||
test('propagates unknown outcome when durable identity capture fails after spawn', async (t) => {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,6 +448,21 @@ function auditAttestation(
|
||||
OCI_MANIFEST_MEDIA_TYPE,
|
||||
MAX_INDEX_BYTES,
|
||||
);
|
||||
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,
|
||||
@@ -465,6 +481,7 @@ function auditAttestation(
|
||||
'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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -47,6 +47,7 @@ const POSTGRES_ROLES = [
|
||||
'ql3_migration',
|
||||
'ql3_package_executor',
|
||||
'ql3_package_manager',
|
||||
'ql3_run_manager',
|
||||
'ql3_runtime',
|
||||
'ql3_worker_credential_executor',
|
||||
'ql3_worker_credential_manager',
|
||||
|
||||
@@ -248,7 +248,9 @@ function createFixture(t, options = {}) {
|
||||
const attestationLayers = options.omitProvenance
|
||||
? [spdx]
|
||||
: [spdx, provenance];
|
||||
const attestationConfig = blob(
|
||||
const attestationConfig = options.modernAttestationConfig
|
||||
? blob({}, 'application/vnd.oci.empty.v1+json')
|
||||
: blob(
|
||||
{
|
||||
architecture: 'unknown',
|
||||
os: 'unknown',
|
||||
@@ -325,6 +327,15 @@ test('accepts two exact images with bound SBOM and provenance', (t) => {
|
||||
);
|
||||
});
|
||||
|
||||
test('accepts OCI 1.1 empty attestation configs emitted by current BuildKit', (t) => {
|
||||
const report = auditClusterOciLayout({
|
||||
root,
|
||||
layoutRoot: createFixture(t, { modernAttestationConfig: true }),
|
||||
expectedRevision: revision,
|
||||
});
|
||||
assert.equal(report.platforms.length, 2);
|
||||
});
|
||||
|
||||
test('accepts the independent cluster-admin image and attestation closure', (t) => {
|
||||
const report = auditClusterOciLayout({
|
||||
root,
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
HOST_BOUND_TESTS,
|
||||
collectReadonlyOwnerTests,
|
||||
} = require('../../scripts/ql3-local-owner-readonly-contract.cjs');
|
||||
|
||||
const repositoryRoot = path.resolve(__dirname, '../..');
|
||||
const workflowPath = path.join(repositoryRoot, '.github/workflows/ql3-ci.yml');
|
||||
|
||||
test('classifies only reviewed host-bound Owner tests outside the read-only gate', () => {
|
||||
const plan = collectReadonlyOwnerTests(repositoryRoot);
|
||||
assert.deepEqual(Object.keys(HOST_BOUND_TESTS).sort(), [
|
||||
'packages/ql3-local-owner-cli/test/adoptedDeploymentBundle.test.cjs',
|
||||
'packages/ql3-local-owner-cli/test/localDeployment.test.cjs',
|
||||
'packages/ql3-local-owner-cli/test/reconciliationCapturePrepare.test.cjs',
|
||||
'packages/ql3-local-owner-cli/test/serviceBridgeRoot.test.cjs',
|
||||
]);
|
||||
assert.equal(plan.tests.length > 0, true);
|
||||
for (const file of plan.tests) {
|
||||
assert.equal(fs.existsSync(path.join(repositoryRoot, file)), true);
|
||||
assert.equal(file in HOST_BOUND_TESTS, false);
|
||||
}
|
||||
});
|
||||
|
||||
test('runs the reviewed Owner contract as root and non-root in read-only containers', () => {
|
||||
const workflow = fs.readFileSync(workflowPath, 'utf8');
|
||||
const job = workflow.match(
|
||||
/ local-profiles:\n([\s\S]*?)\n cluster-postgres:/,
|
||||
)?.[1];
|
||||
assert.ok(job, 'local-profiles job is missing');
|
||||
assert.equal(job.match(/docker run --rm --read-only/g)?.length, 2);
|
||||
assert.equal(
|
||||
job.match(/scripts\/ql3-local-owner-readonly-contract\.cjs/g)?.length,
|
||||
2,
|
||||
);
|
||||
assert.match(job, /--mode=root/);
|
||||
assert.match(job, /--user 65532:65532[\s\S]*--mode=nonroot/);
|
||||
assert.doesNotMatch(job, /packages\/ql3-local-owner-\*\/test\/\*\.test\.cjs/);
|
||||
});
|
||||
@@ -6,7 +6,7 @@ const {
|
||||
auditPostgresHaEvidence,
|
||||
} = require('../../scripts/ql3-postgres-ha-evidence-audit.cjs');
|
||||
|
||||
function fixture() {
|
||||
function fixture(architecture = 'arm64') {
|
||||
const gates = { passed: true };
|
||||
for (let index = 0; index < 100; index += 1) gates[`gate${index}`] = true;
|
||||
return {
|
||||
@@ -16,7 +16,7 @@ function fixture() {
|
||||
image: 'postgres:18',
|
||||
imageId: `sha256:${'a'.repeat(64)}`,
|
||||
repoDigests: [`postgres@sha256:${'b'.repeat(64)}`],
|
||||
architecture: 'arm64',
|
||||
architecture,
|
||||
version: '18.4',
|
||||
versionNumber: 180004,
|
||||
},
|
||||
@@ -85,6 +85,7 @@ test('accepts complete PostgreSQL HA evidence', () => {
|
||||
compatible: true,
|
||||
findings: [],
|
||||
});
|
||||
assert.equal(auditPostgresHaEvidence(fixture('amd64')).compatible, true);
|
||||
});
|
||||
|
||||
test('rejects false gates, promotion drift and hidden private material', () => {
|
||||
|
||||
@@ -338,6 +338,7 @@ function publisherProvenanceInstallRepository(repository, provenance) {
|
||||
findLock: (...args) => repository.findLock(...args),
|
||||
create: (...args) => repository.create(...args),
|
||||
listRecoveryPage: (...args) => repository.listRecoveryPage(...args),
|
||||
listCurrentPage: (...args) => repository.listCurrentPage(...args),
|
||||
async commit(command) {
|
||||
if (command.record.state !== 'staged') {
|
||||
return repository.commit(command);
|
||||
|
||||
Reference in New Issue
Block a user