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
+12 -9
View File
@@ -140,6 +140,10 @@ jobs:
run: node -e "if (process.arch !== '${{ matrix.arch }}') throw new Error('unexpected architecture ' + process.arch)" run: node -e "if (process.arch !== '${{ matrix.arch }}') throw new Error('unexpected architecture ' + process.arch)"
- name: Install workspace dependencies without lifecycle scripts - name: Install workspace dependencies without lifecycle scripts
run: pnpm install --frozen-lockfile --ignore-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 - name: Rebuild the reviewed native SQLite binding
run: pnpm rebuild @whyour/sqlite3 run: pnpm rebuild @whyour/sqlite3
- name: Build benchmark targets once - name: Build benchmark targets once
@@ -855,9 +859,7 @@ jobs:
--volume "${{ github.workspace }}:/workspace:ro" --volume "${{ github.workspace }}:/workspace:ro"
--workdir /workspace --workdir /workspace
node:24.18.0-bookworm-slim node:24.18.0-bookworm-slim
node --test --test-concurrency=1 node scripts/ql3-local-owner-readonly-contract.cjs --mode=root
packages/ql3-local-command-file/test/*.test.cjs
packages/ql3-local-owner-*/test/*.test.cjs
- name: Exercise Owner authorities as non-root in a read-only container - name: Exercise Owner authorities as non-root in a read-only container
run: >- run: >-
docker run --rm --read-only docker run --rm --read-only
@@ -870,9 +872,7 @@ jobs:
--volume "${{ github.workspace }}:/workspace:ro" --volume "${{ github.workspace }}:/workspace:ro"
--workdir /workspace --workdir /workspace
node:24.18.0-bookworm-slim node:24.18.0-bookworm-slim
node --test --test-concurrency=1 node scripts/ql3-local-owner-readonly-contract.cjs --mode=nonroot
packages/ql3-local-command-file/test/*.test.cjs
packages/ql3-local-owner-*/test/*.test.cjs
- name: Build and audit production-only local Profile artifacts - name: Build and audit production-only local Profile artifacts
run: | run: |
pnpm audit:artifact:edge:ql3 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" 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 - name: Create schema and exact grants through the reviewed migration stream
env: env:
QL3_TEST_POSTGRES_URL: postgresql://ql3_migration:ql3_migration_test@127.0.0.1:5432/ql3_contract QL3_POSTGRES_MIGRATION_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_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 - name: Test readiness, isolated roles, shared Repositories, rollback and SQLSTATE mapping
env: env:
QL3_TEST_POSTGRES_MIGRATION_URL: postgresql://ql3_migration:ql3_migration_test@127.0.0.1:5432/ql3_contract 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_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_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_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_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_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 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 - name: Test automation management concurrency and post-commit response loss over mTLS
env: env:
QL3_TEST_POSTGRES_URL: postgresql://ql3_migration:ql3_migration_test@127.0.0.1:5432/ql3_contract QL3_TEST_POSTGRES_URL: postgresql://ql3_migration:ql3_migration_test@127.0.0.1:5432/ql3_contract
@@ -6,7 +6,6 @@ import {
TaskStartUnavailableError, TaskStartUnavailableError,
normalizeTaskStartCommand, normalizeTaskStartCommand,
normalizeTaskStartResult, normalizeTaskStartResult,
type TaskStartAllowedRole,
type TaskStartCommand, type TaskStartCommand,
type TaskStartRepository, type TaskStartRepository,
type TaskStartResult, type TaskStartResult,
@@ -32,12 +31,6 @@ import {
type Row = Record<string, unknown>; type Row = Record<string, unknown>;
const ALLOWED_ROLES = new Set<TaskStartAllowedRole>([
'owner',
'admin',
'operator',
]);
function unavailable(options?: ErrorOptions): TaskStartUnavailableError { function unavailable(options?: ErrorOptions): TaskStartUnavailableError {
return new TaskStartUnavailableError(options); return new TaskStartUnavailableError(options);
} }
@@ -193,34 +186,30 @@ export class PostgresTaskStartRepository implements TaskStartRepository {
try { try {
await configurePostgresDefinitionTransaction(client); await configurePostgresDefinitionTransaction(client);
began = true; began = true;
const project = await client.query<Row>(` const authorization = await client.query<Row>(
SELECT status AS "projectStatus", version AS "projectVersion" `SELECT "ql3"."lock_run_management_policy_fence"(
FROM "ql3"."projects" WHERE id = $1 FOR UPDATE $1::varchar, $2::varchar, $3::varchar, $4::integer, $5::integer
`, [command.projectId]); ) AS "matches"`,
if (project.rows.length === 0) throw new TaskStartNotFoundError(); [
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(); if (project.rows.length !== 1) throw unavailable();
const binding = await client.query<Row>(` if (!postgresRequiredBoolean(project.rows[0]!.exists, unavailable)) {
SELECT version AS "bindingVersion", state AS "bindingState", throw new TaskStartNotFoundError();
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,
)
) {
throw new TaskStartFenceRejectedError('authorization_changed'); throw new TaskStartFenceRejectedError('authorization_changed');
} }
@@ -261,7 +250,6 @@ export class PostgresTaskStartRepository implements TaskStartRepository {
AND revision.task_id = head.task_id AND revision.task_id = head.task_id
AND revision.revision = head.current_revision AND revision.revision = head.current_revision
WHERE head.project_id = $1 AND head.task_id = $2 WHERE head.project_id = $1 AND head.task_id = $2
FOR UPDATE OF head
`, [command.projectId, command.taskId]); `, [command.projectId, command.taskId]);
if (task.rows.length === 0) throw new TaskStartNotFoundError(); if (task.rows.length === 0) throw new TaskStartNotFoundError();
if (task.rows.length !== 1) throw unavailable(); if (task.rows.length !== 1) throw unavailable();
@@ -626,6 +626,8 @@ const adminConnectionString =
const automationManagerConnectionString = const automationManagerConnectionString =
process.env.QL3_TEST_POSTGRES_AUTOMATION_MANAGER_URL ?? process.env.QL3_TEST_POSTGRES_AUTOMATION_MANAGER_URL ??
migrationConnectionString; migrationConnectionString;
const runManagerConnectionString =
process.env.QL3_TEST_POSTGRES_RUN_MANAGER_URL ?? migrationConnectionString;
const packageManagerConnectionString = const packageManagerConnectionString =
process.env.QL3_TEST_POSTGRES_PACKAGE_MANAGER_URL ?? process.env.QL3_TEST_POSTGRES_PACKAGE_MANAGER_URL ??
migrationConnectionString; migrationConnectionString;
@@ -678,6 +680,8 @@ if (!migrationConnectionString) {
? adminConnectionString ? adminConnectionString
: role === 'automation-manager' : role === 'automation-manager'
? automationManagerConnectionString ? automationManagerConnectionString
: role === 'run-manager'
? runManagerConnectionString
: role === 'package-manager' : role === 'package-manager'
? packageManagerConnectionString ? packageManagerConnectionString
: role === 'package-executor' : role === 'package-executor'
@@ -1045,7 +1049,7 @@ if (!migrationConnectionString) {
const taskId = 'task-start-command'; const taskId = 'task-start-command';
const subjectId = 'usr_task_start_integration'; const subjectId = 'usr_task_start_integration';
const migrationDatabase = await open('migration'); const migrationDatabase = await open('migration');
let runtimeDatabase; let runManagerDatabase;
try { try {
await runPostgresMigrations({ pool: migrationDatabase.pool }); await runPostgresMigrations({ pool: migrationDatabase.pool });
await migrationDatabase.pool.query( await migrationDatabase.pool.query(
@@ -1102,11 +1106,13 @@ if (!migrationConnectionString) {
}) })
).definition; ).definition;
runtimeDatabase = runManagerDatabase =
runtimeConnectionString === migrationConnectionString runManagerConnectionString === migrationConnectionString
? migrationDatabase ? migrationDatabase
: await open('runtime'); : await open('run-manager');
const repository = new PostgresTaskStartRepository(runtimeDatabase.pool); const repository = new PostgresTaskStartRepository(
runManagerDatabase.pool,
);
const command = { const command = {
projectId, projectId,
taskId, taskId,
@@ -1158,8 +1164,8 @@ if (!migrationConnectionString) {
}, },
]); ]);
} finally { } finally {
if (runtimeDatabase && runtimeDatabase !== migrationDatabase) { if (runManagerDatabase && runManagerDatabase !== migrationDatabase) {
await runtimeDatabase.close(); await runManagerDatabase.close();
} }
await migrationDatabase.close(); await migrationDatabase.close();
} }
@@ -2193,6 +2199,9 @@ if (!migrationConnectionString) {
const migrationDatabase = await open('migration'); const migrationDatabase = await open('migration');
try { try {
await runPostgresMigrations({ pool: migrationDatabase.pool }); await runPostgresMigrations({ pool: migrationDatabase.pool });
await migrationDatabase.pool.query(
'TRUNCATE TABLE "ql3"."runs" CASCADE',
);
await observeContractPublisherTrust(migrationDatabase.pool); await observeContractPublisherTrust(migrationDatabase.pool);
await migrationDatabase.pool.query( await migrationDatabase.pool.query(
`INSERT INTO "ql3"."projects" ( `INSERT INTO "ql3"."projects" (
@@ -2577,6 +2586,9 @@ if (!migrationConnectionString) {
const migrationDatabase = await open('migration'); const migrationDatabase = await open('migration');
try { try {
await runPostgresMigrations({ pool: migrationDatabase.pool }); await runPostgresMigrations({ pool: migrationDatabase.pool });
await migrationDatabase.pool.query(
'TRUNCATE TABLE "ql3"."plugin_package_installs" CASCADE',
);
await observeContractPublisherTrust(migrationDatabase.pool); await observeContractPublisherTrust(migrationDatabase.pool);
await migrationDatabase.pool.query( await migrationDatabase.pool.query(
`INSERT INTO "ql3"."projects" ( `INSERT INTO "ql3"."projects" (
@@ -2589,7 +2601,7 @@ if (!migrationConnectionString) {
await migrationDatabase.close(); await migrationDatabase.close();
} }
const executorDatabase = await open('package-executor'); const executorDatabase = await open('package-executor');
const adminDatabase = await open('admin'); const automationManagerDatabase = await open('automation-manager');
return { return {
repository: new PostgresPluginPackageTaskReconciliationRepository( repository: new PostgresPluginPackageTaskReconciliationRepository(
executorDatabase.pool, executorDatabase.pool,
@@ -2607,11 +2619,14 @@ if (!migrationConnectionString) {
), ),
), ),
taskRepository: new PostgresTaskDefinitionRepository( taskRepository: new PostgresTaskDefinitionRepository(
adminDatabase.pool, automationManagerDatabase.pool,
fixture.registry, fixture.registry,
), ),
close: async () => { 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('SELECT set_config') ||
normalized.startsWith('INSERT INTO') normalized.startsWith('INSERT INTO')
) return { rows: [], rowCount: normalized.startsWith('INSERT') ? 1 : 0 }; ) return { rows: [], rowCount: normalized.startsWith('INSERT') ? 1 : 0 };
if (normalized.includes('FROM "ql3"."projects"')) { if (normalized.includes('lock_run_management_policy_fence')) {
const rows = options.projectRows ?? [{ const rows = options.authorizationRows ?? [{ matches: true }];
projectStatus: 'active',
projectVersion: 2,
}];
return { rows, rowCount: rows.length }; return { rows, rowCount: rows.length };
} }
if (normalized.includes('FROM "ql3"."project_role_bindings"')) { if (normalized.includes('SELECT EXISTS')) {
const rows = options.bindingRows ?? [{ const rows = options.projectExistenceRows ?? [{ exists: true }];
bindingVersion: 3,
bindingState: 'active',
bindingRole: 'operator',
}];
return { rows, rowCount: rows.length }; return { rows, rowCount: rows.length };
} }
if (normalized.includes('FROM "ql3"."runs"')) { 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, executionRevisionDigest: EXECUTION.contentDigest,
createdAtMs: 1_000, createdAtMs: 1_000,
}); });
const project = calls.findIndex(({ sql }) => sql.includes('FROM "ql3"."projects"')); const authorization = calls.findIndex(({ sql }) =>
const binding = calls.findIndex(({ sql }) => sql.includes('project_role_bindings')); sql.includes('lock_run_management_policy_fence'));
const task = calls.findIndex(({ sql }) => sql.includes('task_definitions')); const task = calls.findIndex(({ sql }) => sql.includes('task_definitions'));
const execution = calls.findIndex(({ sql }) => sql.includes('task_execution_revisions')); 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.filter(({ sql }) => sql.startsWith('INSERT INTO')).length, 4);
assert.equal(calls.some(({ sql }) => sql === 'COMMIT'), true); 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 () => { test('rejects missing, authorization, definition and disabled fences', async () => {
await assert.rejects( await assert.rejects(
fixture({ projectRows: [] }).repository.startTask(command()), fixture({
authorizationRows: [{ matches: null }],
projectExistenceRows: [{ exists: false }],
}).repository.startTask(command()),
TaskStartNotFoundError, TaskStartNotFoundError,
); );
await assert.rejects( await assert.rejects(
fixture({ bindingRows: [{ fixture({
bindingVersion: 4, authorizationRows: [{ matches: false }],
bindingState: 'revoked', projectExistenceRows: [{ exists: true }],
bindingRole: null, }).repository.startTask(command()),
}] }).repository.startTask(command()),
(error) => (error) =>
error instanceof TaskStartFenceRejectedError && error instanceof TaskStartFenceRejectedError &&
error.reason === 'authorization_changed', 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(result.status, 'started');
assert.equal(barriers, 1); assert.equal(barriers, 1);
await waitForReceipt(roots.receiptRoot);
}); });
test('propagates unknown outcome when durable identity capture fails after spawn', async (t) => { 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_migration',
'ql3_package_executor', 'ql3_package_executor',
'ql3_package_manager', 'ql3_package_manager',
'ql3_run_manager',
'ql3_runtime', 'ql3_runtime',
'ql3_worker_credential_executor', 'ql3_worker_credential_executor',
'ql3_worker_credential_manager', 'ql3_worker_credential_manager',
@@ -53,6 +53,7 @@ const ROLE_NAMES = Object.freeze([
'ql3_migration', 'ql3_migration',
'ql3_package_executor', 'ql3_package_executor',
'ql3_package_manager', 'ql3_package_manager',
'ql3_run_manager',
'ql3_runtime', 'ql3_runtime',
'ql3_worker_credential_executor', 'ql3_worker_credential_executor',
'ql3_worker_credential_manager', 'ql3_worker_credential_manager',
@@ -733,10 +734,14 @@ async function main() {
ql3_migration: randomSecret(), ql3_migration: randomSecret(),
ql3_runtime: randomSecret(), ql3_runtime: randomSecret(),
ql3_admin: randomSecret(), ql3_admin: randomSecret(),
ql3_ai_credential_manager: randomSecret(),
ql3_ai_credential_tester: randomSecret(),
ql3_ai_maintenance: randomSecret(),
ql3_automation_manager: randomSecret(), ql3_automation_manager: randomSecret(),
ql3_approval_manager: randomSecret(), ql3_approval_manager: randomSecret(),
ql3_package_manager: randomSecret(), ql3_package_manager: randomSecret(),
ql3_package_executor: randomSecret(), ql3_package_executor: randomSecret(),
ql3_run_manager: randomSecret(),
ql3_worker_credential_manager: randomSecret(), ql3_worker_credential_manager: randomSecret(),
ql3_worker_credential_executor: randomSecret(), ql3_worker_credential_executor: randomSecret(),
ql3_worker_ingress: randomSecret(), ql3_worker_ingress: randomSecret(),
+17
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_INDEX_MEDIA_TYPE = 'application/vnd.oci.image.index.v1+json';
const OCI_MANIFEST_MEDIA_TYPE = 'application/vnd.oci.image.manifest.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_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 OCI_LAYER_MEDIA_TYPE = 'application/vnd.oci.image.layer.v1.tar+gzip';
const IN_TOTO_MEDIA_TYPE = 'application/vnd.in-toto+json'; const IN_TOTO_MEDIA_TYPE = 'application/vnd.in-toto+json';
const SPDX_PREDICATE_TYPE = 'https://spdx.dev/Document'; const SPDX_PREDICATE_TYPE = 'https://spdx.dev/Document';
@@ -447,6 +448,21 @@ function auditAttestation(
OCI_MANIFEST_MEDIA_TYPE, OCI_MANIFEST_MEDIA_TYPE,
MAX_INDEX_BYTES, 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( const config = blobReader.readJson(
manifest.config, manifest.config,
OCI_CONFIG_MEDIA_TYPE, OCI_CONFIG_MEDIA_TYPE,
@@ -465,6 +481,7 @@ function auditAttestation(
'OCI attestation config must bind an empty unknown platform', 'OCI attestation config must bind an empty unknown platform',
); );
} }
}
if (!Array.isArray(manifest.layers) || manifest.layers.length !== 2) { if (!Array.isArray(manifest.layers) || manifest.layers.length !== 2) {
throw new Error('OCI attestation must contain exactly SBOM and provenance'); 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_IMAGE = 'postgres:18.4-bookworm';
const POSTGRES_INDEX_DIGEST = const POSTGRES_INDEX_DIGEST =
'sha256:1961f96e6029a02c3812d7cb329a3b03a3ac2bb067058dec17b0f5596aca9296'; 'sha256:1961f96e6029a02c3812d7cb329a3b03a3ac2bb067058dec17b0f5596aca9296';
const POSTGRES_IMAGE_REFERENCE = `${POSTGRES_IMAGE}@${POSTGRES_INDEX_DIGEST}`;
const POSTGRES_REPOSITORY_DIGEST = `postgres@${POSTGRES_INDEX_DIGEST}`; const POSTGRES_REPOSITORY_DIGEST = `postgres@${POSTGRES_INDEX_DIGEST}`;
const DEFAULT_ADMIN_IMAGE = 'qinglong3-cluster-admin:ql3-plugin-recovery-e2e'; const DEFAULT_ADMIN_IMAGE = 'qinglong3-cluster-admin:ql3-plugin-recovery-e2e';
const DEFAULT_CONTROL_IMAGE = const DEFAULT_CONTROL_IMAGE =
@@ -277,13 +278,13 @@ function buildImages(revision) {
} }
function ensurePostgresImage() { function ensurePostgresImage() {
if (!imageExists(POSTGRES_IMAGE)) { if (!imageExists(POSTGRES_IMAGE_REFERENCE)) {
run(DOCKER, ['pull', `${POSTGRES_IMAGE}@${POSTGRES_INDEX_DIGEST}`], { run(DOCKER, ['pull', POSTGRES_IMAGE_REFERENCE], {
label: 'pull digest-pinned PostgreSQL 18.4 fixture image', label: 'pull digest-pinned PostgreSQL 18.4 fixture image',
}); });
} }
const inspection = JSON.parse( const inspection = JSON.parse(
run(DOCKER, ['image', 'inspect', POSTGRES_IMAGE], { run(DOCKER, ['image', 'inspect', POSTGRES_IMAGE_REFERENCE], {
capture: true, capture: true,
quiet: true, quiet: true,
}).stdout, }).stdout,
@@ -485,7 +486,7 @@ createdb --username "$POSTGRES_USER" --owner ql3_migration qinglong
containers: [ containers: [
{ {
name: 'postgres', name: 'postgres',
image: POSTGRES_IMAGE, image: POSTGRES_IMAGE_REFERENCE,
imagePullPolicy: 'Never', imagePullPolicy: 'Never',
env: [ env: [
{ name: 'POSTGRES_USER', value: 'postgres' }, { name: 'POSTGRES_USER', value: 'postgres' },
@@ -1726,7 +1727,11 @@ async function main(argv = process.argv.slice(2)) {
}, },
); );
created = true; created = true;
for (const image of [ADMIN_IMAGE, CONTROL_IMAGE, POSTGRES_IMAGE]) { for (const image of [
ADMIN_IMAGE,
CONTROL_IMAGE,
POSTGRES_IMAGE_REFERENCE,
]) {
kind( kind(
[ [
'load', 'load',
+1 -1
View File
@@ -117,7 +117,7 @@ function auditPostgresHaEvidence(report) {
'POSTGRES_REPO_DIGESTS_MISSING', 'POSTGRES_REPO_DIGESTS_MISSING',
); );
add( add(
postgres.architecture === 'x64' || postgres.architecture === 'arm64', postgres.architecture === 'amd64' || postgres.architecture === 'arm64',
'POSTGRES_ARCHITECTURE_INVALID', 'POSTGRES_ARCHITECTURE_INVALID',
); );
add( add(
@@ -81,6 +81,7 @@ const ROLE_NAMES = Object.freeze([
'ql3_package_executor', 'ql3_package_executor',
'ql3_automation_manager', 'ql3_automation_manager',
'ql3_approval_manager', 'ql3_approval_manager',
'ql3_run_manager',
'ql3_worker_credential_manager', 'ql3_worker_credential_manager',
'ql3_worker_credential_executor', 'ql3_worker_credential_executor',
'ql3_worker_ingress', 'ql3_worker_ingress',
@@ -47,6 +47,7 @@ const POSTGRES_ROLES = [
'ql3_migration', 'ql3_migration',
'ql3_package_executor', 'ql3_package_executor',
'ql3_package_manager', 'ql3_package_manager',
'ql3_run_manager',
'ql3_runtime', 'ql3_runtime',
'ql3_worker_credential_executor', 'ql3_worker_credential_executor',
'ql3_worker_credential_manager', 'ql3_worker_credential_manager',
+12 -1
View File
@@ -248,7 +248,9 @@ function createFixture(t, options = {}) {
const attestationLayers = options.omitProvenance const attestationLayers = options.omitProvenance
? [spdx] ? [spdx]
: [spdx, provenance]; : [spdx, provenance];
const attestationConfig = blob( const attestationConfig = options.modernAttestationConfig
? blob({}, 'application/vnd.oci.empty.v1+json')
: blob(
{ {
architecture: 'unknown', architecture: 'unknown',
os: '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) => { test('accepts the independent cluster-admin image and attestation closure', (t) => {
const report = auditClusterOciLayout({ const report = auditClusterOciLayout({
root, 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, auditPostgresHaEvidence,
} = require('../../scripts/ql3-postgres-ha-evidence-audit.cjs'); } = require('../../scripts/ql3-postgres-ha-evidence-audit.cjs');
function fixture() { function fixture(architecture = 'arm64') {
const gates = { passed: true }; const gates = { passed: true };
for (let index = 0; index < 100; index += 1) gates[`gate${index}`] = true; for (let index = 0; index < 100; index += 1) gates[`gate${index}`] = true;
return { return {
@@ -16,7 +16,7 @@ function fixture() {
image: 'postgres:18', image: 'postgres:18',
imageId: `sha256:${'a'.repeat(64)}`, imageId: `sha256:${'a'.repeat(64)}`,
repoDigests: [`postgres@sha256:${'b'.repeat(64)}`], repoDigests: [`postgres@sha256:${'b'.repeat(64)}`],
architecture: 'arm64', architecture,
version: '18.4', version: '18.4',
versionNumber: 180004, versionNumber: 180004,
}, },
@@ -85,6 +85,7 @@ test('accepts complete PostgreSQL HA evidence', () => {
compatible: true, compatible: true,
findings: [], findings: [],
}); });
assert.equal(auditPostgresHaEvidence(fixture('amd64')).compatible, true);
}); });
test('rejects false gates, promotion drift and hidden private material', () => { test('rejects false gates, promotion drift and hidden private material', () => {
@@ -338,6 +338,7 @@ function publisherProvenanceInstallRepository(repository, provenance) {
findLock: (...args) => repository.findLock(...args), findLock: (...args) => repository.findLock(...args),
create: (...args) => repository.create(...args), create: (...args) => repository.create(...args),
listRecoveryPage: (...args) => repository.listRecoveryPage(...args), listRecoveryPage: (...args) => repository.listRecoveryPage(...args),
listCurrentPage: (...args) => repository.listCurrentPage(...args),
async commit(command) { async commit(command) {
if (command.record.state !== 'staged') { if (command.record.state !== 'staged') {
return repository.commit(command); return repository.commit(command);