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)"
|
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();
|
[
|
||||||
if (project.rows.length !== 1) throw unavailable();
|
command.projectId,
|
||||||
const binding = await client.query<Row>(`
|
command.subject.type,
|
||||||
SELECT version AS "bindingVersion", state AS "bindingState",
|
command.subject.id,
|
||||||
role AS "bindingRole"
|
command.policyFence.projectVersion,
|
||||||
FROM "ql3"."project_role_bindings"
|
command.policyFence.bindingVersion,
|
||||||
WHERE project_id = $1 AND subject_type = $2 AND subject_id = $3
|
],
|
||||||
ORDER BY version DESC LIMIT 1
|
);
|
||||||
FOR SHARE
|
if (authorization.rows.length !== 1) throw unavailable();
|
||||||
`, [command.projectId, command.subject.type, command.subject.id]);
|
if (authorization.rows[0]?.matches !== true) {
|
||||||
const currentProject = project.rows[0]!;
|
const project = await client.query<Row>(
|
||||||
const currentBinding = binding.rows[0];
|
`SELECT EXISTS (
|
||||||
if (
|
SELECT 1 FROM "ql3"."projects" WHERE id = $1
|
||||||
text(currentProject, 'projectStatus') !== 'active' ||
|
) AS "exists"`,
|
||||||
integer(currentProject, 'projectVersion') !==
|
[command.projectId],
|
||||||
command.policyFence.projectVersion ||
|
);
|
||||||
!currentBinding ||
|
if (project.rows.length !== 1) throw unavailable();
|
||||||
integer(currentBinding, 'bindingVersion') !==
|
if (!postgresRequiredBoolean(project.rows[0]!.exists, unavailable)) {
|
||||||
command.policyFence.bindingVersion ||
|
throw new TaskStartNotFoundError();
|
||||||
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(),
|
||||||
|
|||||||
@@ -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,23 +448,39 @@ function auditAttestation(
|
|||||||
OCI_MANIFEST_MEDIA_TYPE,
|
OCI_MANIFEST_MEDIA_TYPE,
|
||||||
MAX_INDEX_BYTES,
|
MAX_INDEX_BYTES,
|
||||||
);
|
);
|
||||||
const config = blobReader.readJson(
|
if (manifest.config?.mediaType === OCI_EMPTY_CONFIG_MEDIA_TYPE) {
|
||||||
manifest.config,
|
normalizeDescriptor(
|
||||||
OCI_CONFIG_MEDIA_TYPE,
|
manifest.config,
|
||||||
MAX_CONFIG_BYTES,
|
OCI_EMPTY_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',
|
|
||||||
);
|
);
|
||||||
|
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) {
|
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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -248,18 +248,20 @@ 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')
|
||||||
architecture: 'unknown',
|
: blob(
|
||||||
os: 'unknown',
|
{
|
||||||
config: {},
|
architecture: 'unknown',
|
||||||
rootfs: {
|
os: 'unknown',
|
||||||
type: 'layers',
|
config: {},
|
||||||
diff_ids: attestationLayers.map((layer) => layer.digest),
|
rootfs: {
|
||||||
},
|
type: 'layers',
|
||||||
},
|
diff_ids: attestationLayers.map((layer) => layer.digest),
|
||||||
'application/vnd.oci.image.config.v1+json',
|
},
|
||||||
);
|
},
|
||||||
|
'application/vnd.oci.image.config.v1+json',
|
||||||
|
);
|
||||||
const attestationManifest = blob(
|
const attestationManifest = blob(
|
||||||
{
|
{
|
||||||
schemaVersion: 2,
|
schemaVersion: 2,
|
||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user