feat(ql3): close public local release pair

This commit is contained in:
whyour
2026-08-27 14:51:33 +08:00
parent 238df17fdf
commit 78c261b556
31 changed files with 558 additions and 91 deletions
@@ -12,9 +12,15 @@ function writeSyntheticLocalReleaseSelection(options) {
const releaseSetDigest = sha256(`release-set:${options.image}`);
const manifestDigest = sha256(`catalog-manifest:${options.image}`);
const consumptionReportDigest = sha256(`catalog-report:${options.image}`);
const operatorImage =
options.operatorImage ||
options.image.replace(
'/qinglong3-local-application@',
'/qinglong3-local-operator@',
);
const unsigned = {
schemaVersion: 1,
schema: 'qinglong/local-compose-release-image@v2',
schema: 'qinglong/local-compose-release-image@v3',
release: {
version: '3.0.0-alpha.0',
sourceRevision: options.sourceRevision ?? '3'.repeat(40),
@@ -39,6 +45,11 @@ function writeSyntheticLocalReleaseSelection(options) {
image: options.image,
allowRootService: options.allowRootService,
},
operator: {
kind: 'short-lived',
image: operatorImage,
network: 'none-by-default',
},
verification: {
releaseSet: 'standalone_structure_identity_and_self_digest',
sourceRecordsReplayed: false,
+44 -13
View File
@@ -168,6 +168,11 @@ function auditClusterImageCiWorkflow(
dockerfile: 'deploy/containers/ql3-local-application/Dockerfile',
target: 'runtime',
},
{
image: 'local-operator',
dockerfile: 'deploy/containers/ql3-local-operator/Dockerfile',
target: 'runtime',
},
{
image: 'worker',
dockerfile: 'deploy/containers/ql3-worker/Dockerfile',
@@ -185,7 +190,7 @@ function auditClusterImageCiWorkflow(
JSON.stringify(expectedOciMatrix)
) {
throw new Error(
'image CI matrices must contain only exact control/control-ai/admin/local/worker amd64/arm64 evidence targets',
'image CI matrices must contain only exact control/control-ai/admin/local/local-operator/worker amd64/arm64 evidence targets',
);
}
const expectedTrivyInputs = {
@@ -205,7 +210,13 @@ function auditClusterImageCiWorkflow(
localImageJob,
'qinglong3-local-application:ci-${{ matrix.image_arch }}',
'${{ runner.temp }}/ql3-local-${{ matrix.image_arch }}.trivyignore.yaml',
'local',
'Local application',
],
[
localImageJob,
'qinglong3-local-operator:ci-${{ matrix.image_arch }}',
'${{ runner.temp }}/ql3-local-operator-${{ matrix.image_arch }}.trivyignore.yaml',
'Local operator',
],
[
clusterImageJob,
@@ -217,7 +228,8 @@ function auditClusterImageCiWorkflow(
const step = job?.steps?.find(
(entry) =>
entry.uses ===
'aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25',
'aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25' &&
entry.with?.['image-ref'] === imageRef,
);
if (
!step ||
@@ -246,14 +258,19 @@ function auditClusterImageCiWorkflow(
requireOccurrences(
source,
/node scripts\/ql3-image-os-vulnerability-policy\.cjs/g,
2,
3,
'native image CI must materialize reviewed image-scoped Trivy exceptions',
);
requirePattern(
source,
/--image=local\s+--output=\$\{\{ runner\.temp \}\}\/ql3-local-\$\{\{ matrix\.image_arch \}\}\.trivyignore\.yaml/,
/--image=local \\\s+--output=\$\{\{ runner\.temp \}\}\/ql3-local-\$\{\{ matrix\.image_arch \}\}\.trivyignore\.yaml/,
'local native image CI must materialize the local exception view',
);
requirePattern(
source,
/--image=local-operator \\\s+--output=\$\{\{ runner\.temp \}\}\/ql3-local-operator-\$\{\{ matrix\.image_arch \}\}\.trivyignore\.yaml/,
'Local operator native image CI must materialize its own exception view',
);
requirePattern(
source,
/--image=\$\{\{ matrix\.image \}\}\s+--output=\$\{\{ runner\.temp \}\}\/ql3-\$\{\{ matrix\.image \}\}-\$\{\{ matrix\.image_arch \}\}\.trivyignore\.yaml/,
@@ -489,8 +506,8 @@ function auditClusterImageCiWorkflow(
);
requirePattern(
source,
/- image: control\s+dockerfile: deploy\/containers\/ql3-cluster-control\/Dockerfile\s+target: runtime\s+- image: control-ai\s+dockerfile: deploy\/containers\/ql3-cluster-control\/Dockerfile\s+target: runtime-ai\s+- image: admin\s+dockerfile: deploy\/containers\/ql3-cluster-admin\/Dockerfile\s+target: runtime\s+- image: local\s+dockerfile: deploy\/containers\/ql3-local-application\/Dockerfile\s+target: runtime\s+- image: worker\s+dockerfile: deploy\/containers\/ql3-worker\/Dockerfile\s+target: runtime/,
'OCI evidence CI must build independent control, control-ai, admin, local and worker images',
/- image: control\s+dockerfile: deploy\/containers\/ql3-cluster-control\/Dockerfile\s+target: runtime\s+- image: control-ai\s+dockerfile: deploy\/containers\/ql3-cluster-control\/Dockerfile\s+target: runtime-ai\s+- image: admin\s+dockerfile: deploy\/containers\/ql3-cluster-admin\/Dockerfile\s+target: runtime\s+- image: local\s+dockerfile: deploy\/containers\/ql3-local-application\/Dockerfile\s+target: runtime\s+- image: local-operator\s+dockerfile: deploy\/containers\/ql3-local-operator\/Dockerfile\s+target: runtime\s+- image: worker\s+dockerfile: deploy\/containers\/ql3-worker\/Dockerfile\s+target: runtime/,
'OCI evidence CI must build independent control, control-ai, admin, local, local-operator and worker images',
);
requirePattern(
source,
@@ -519,7 +536,14 @@ function auditClusterImageCiWorkflow(
'local image CI must generate and inventory-check the exact local SBOM profile',
);
return {
images: ['control', 'control-ai', 'admin', 'local', 'worker'],
images: [
'control',
'control-ai',
'admin',
'local',
'local-operator',
'worker',
],
nativeArchitectures: ['amd64', 'arm64'],
runtimeInventory: true,
clusterAdminProductFacade: true,
@@ -844,7 +868,7 @@ function auditReleaseWorkflow(source) {
);
}
if (
!/ql3-release-set-contract\.cjs[\s\S]*--mode=record-image[\s\S]*--version="\$\{RELEASE_VERSION\}"[\s\S]*--source-revision="\$\{GITHUB_SHA\}"[\s\S]*--source-ref="\$\{GITHUB_REF\}"[\s\S]*--release-scope="\$\{RELEASE_SCOPE\}"[\s\S]*--repository-owner="\$\{owner\}"[\s\S]*--candidate="\$\{RUNNER_TEMP\}\/\$\{\{ matrix\.repository \}\}-release-candidate-contract\.json"[\s\S]*--image="\$\{\{ matrix\.image \}\}"[\s\S]*--digest="\$\{DIGEST\}"/.test(
!/ql3-release-set-contract\.cjs[\s\S]*--mode=record-image[\s\S]*--version="\$\{RELEASE_VERSION\}"[\s\S]*--source-revision="\$\{GITHUB_SHA\}"[\s\S]*--source-ref="\$\{GITHUB_REF\}"[\s\S]*--release-scope="\$\{RELEASE_SCOPE\}"[\s\S]*--repository-owner="\$\{owner\}"[\s\S]*--candidate="\$\{RUNNER_TEMP\}\/\$\{\{ matrix\.repository \}\}-release-candidate-contract\.json"[\s\S]*--image="\$\{\{ matrix\.image \}\}"[\s\S]*--local-role-verification="\$\{\{ matrix\.local_role_verification \}\}"[\s\S]*--digest="\$\{DIGEST\}"/.test(
publishSteps[recordIndex]?.run ?? '',
) ||
JSON.stringify(publishSteps[recordUploadIndex]?.with) !==
@@ -991,10 +1015,10 @@ function auditReleaseWorkflow(source) {
localCatalogDeploymentSteps[5]?.run ?? '',
) ||
localCatalogDeploymentSteps[6]?.id !== 'local-selection' ||
!/ql3-deployment-lock-contract\.cjs[\s\S]*--mode=local-create[\s\S]*--consumption-bundle="\$\{CONSUMPTION_BUNDLE\}"[\s\S]*--allow-root-service=false[\s\S]*--output="\$\{selection\}"[\s\S]*ql3-deployment-lock-contract\.cjs[\s\S]*--mode=local-audit[\s\S]*--selection="\$\{selection\}"[\s\S]*selection-digest=\$\{selection\.selectionDigest\}[\s\S]*image=\$\{selection\.service\.image\}/u.test(
!/ql3-deployment-lock-contract\.cjs[\s\S]*--mode=local-create[\s\S]*--consumption-bundle="\$\{CONSUMPTION_BUNDLE\}"[\s\S]*--allow-root-service=false[\s\S]*--output="\$\{selection\}"[\s\S]*ql3-deployment-lock-contract\.cjs[\s\S]*--mode=local-audit[\s\S]*--selection="\$\{selection\}"[\s\S]*selection-digest=\$\{selection\.selectionDigest\}[\s\S]*image=\$\{selection\.service\.image\}[\s\S]*operator-image=\$\{selection\.operator\.image\}/u.test(
localCatalogDeploymentSteps[6]?.run ?? '',
) ||
!/docker pull "\$\{IMAGE\}"[\s\S]*for profile in edge standalone[\s\S]*ql3-local-compose-rollout-live-contract\.cjs[\s\S]*--image="\$\{IMAGE\}"[\s\S]*--profile="\$\{profile\}"[\s\S]*--release-selection="\$\{RELEASE_SELECTION\}"[\s\S]*--expected-selection-digest="\$\{SELECTION_DIGEST\}"[\s\S]*verified_release_catalog[\s\S]*catalogConsumptionDigest/u.test(
!/docker pull "\$\{IMAGE\}"[\s\S]*docker pull "\$\{OPERATOR_IMAGE\}"[\s\S]*"\$\{OPERATOR_IMAGE\}" --version[\s\S]*"\$\{OPERATOR_IMAGE\}" setup --help[\s\S]*for profile in edge standalone[\s\S]*ql3-local-compose-rollout-live-contract\.cjs[\s\S]*--image="\$\{IMAGE\}"[\s\S]*--profile="\$\{profile\}"[\s\S]*--release-selection="\$\{RELEASE_SELECTION\}"[\s\S]*--expected-selection-digest="\$\{SELECTION_DIGEST\}"[\s\S]*verified_release_catalog[\s\S]*catalogConsumptionDigest/u.test(
localCatalogDeploymentSteps[7]?.run ?? '',
) ||
localCatalogDeploymentSteps[8]?.if !== 'always()' ||
@@ -1540,7 +1564,14 @@ function auditReleaseWorkflow(source) {
immutableArtifactRetentionDays: 1,
attestedToPublishedDigest: true,
},
images: ['control', 'control-ai', 'admin', 'worker', 'local'],
images: [
'control',
'control-ai',
'admin',
'worker',
'local',
'local-operator',
],
platforms: ['linux/amd64', 'linux/arm64'],
keylessSignature: true,
buildkitAttestations: ['sbom', 'provenance'],
@@ -1578,7 +1609,7 @@ function auditReleaseWorkflow(source) {
},
durableCatalog: {
repository: 'qinglong3-release-catalog',
artifactType: 'application/vnd.qinglong.release-set.v3+json',
artifactType: 'application/vnd.qinglong.release-set.v4+json',
planSchema: 'qinglong/release-catalog-plan@v2',
receiptSchema: 'qinglong/release-catalog-receipt@v2',
tagInventoryDecisionSchema:
+36 -1
View File
@@ -241,6 +241,39 @@ function expectedImageConfig(architecture, revision, image) {
},
};
}
if (image === 'local-operator') {
return {
architecture,
os: 'linux',
config: {
User: '65532:65532',
Env: [
'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
'NODE_VERSION=24.18.0',
'YARN_VERSION=1.22.22',
'NODE_ENV=production',
],
Entrypoint: [
'node',
'/opt/qinglong/node_modules/@qinglong/local-owner-cli/dist/product-cli/cli.js',
],
WorkingDir: '/opt/qinglong',
Labels: {
'io.qinglong.authority': 'local-owner-management',
'io.qinglong.lifecycle': 'short-lived',
'io.qinglong.network': 'none-by-default',
'org.opencontainers.image.description':
'QingLong 3.0 short-lived Local management authority',
'org.opencontainers.image.licenses': 'Apache-2.0',
'org.opencontainers.image.revision': revision,
'org.opencontainers.image.source':
'https://github.com/whyour/qinglong',
'org.opencontainers.image.title': 'QingLong 3.0 Local Operator',
'org.opencontainers.image.version': QL3_VERSION,
},
},
};
}
const isControl = image === 'control' || image === 'control-ai';
const isControlAi = image === 'control-ai';
return {
@@ -529,7 +562,9 @@ function auditClusterOciLayout(options = {}) {
const image = resolveImageProfile(options.image).id;
const expectedPlatforms = options.expectedPlatforms || EXPECTED_PLATFORMS;
const maximumPlatformBytes =
image === 'local' ? 128 * 1024 * 1024 : 512 * 1024 * 1024;
image === 'local' || image === 'local-operator'
? 128 * 1024 * 1024
: 512 * 1024 * 1024;
if (
!path.isAbsolute(layoutRoot) ||
typeof expectedRevision !== 'string' ||
+8 -1
View File
@@ -14,7 +14,7 @@ const {
const { inspectReleaseSet } = require('./ql3-release-set-contract.cjs');
const DEFAULT_ROOT = path.resolve(__dirname, '..');
const LOCAL_SELECTION_SCHEMA = 'qinglong/local-compose-release-image@v2';
const LOCAL_SELECTION_SCHEMA = 'qinglong/local-compose-release-image@v3';
const KUBERNETES_LOCK_SCHEMA = 'qinglong/kubernetes-deployment-lock@v2';
const MAX_RELEASE_SET_BYTES = 1024 * 1024;
const MAX_MANIFEST_BYTES = 8 * 1024 * 1024;
@@ -193,6 +193,7 @@ function createLocalSelection(releaseSet, options) {
fail('allow-root-service must be an explicit boolean');
}
const local = imageByName(releaseSet, 'local');
const operator = imageByName(releaseSet, 'local-operator');
const unsigned = {
schemaVersion: 1,
schema: LOCAL_SELECTION_SCHEMA,
@@ -205,6 +206,11 @@ function createLocalSelection(releaseSet, options) {
image: local.reference,
allowRootService: options.allowRootService,
},
operator: {
kind: 'short-lived',
image: operator.reference,
network: 'none-by-default',
},
verification: {
releaseSet: inspection.verification,
sourceRecordsReplayed: inspection.sourceRecordsReplayed,
@@ -233,6 +239,7 @@ function auditLocalSelection(actual, releaseSet, options) {
catalogManifestDigest: actual.catalog.manifestDigest,
immutableReference: actual.catalog.immutableReference,
image: actual.service.image,
operatorImage: actual.operator.image,
networkAccess: false,
deploymentMutation: false,
});
@@ -12,6 +12,7 @@ const IMAGES = Object.freeze([
'control',
'control-ai',
'local',
'local-operator',
'worker',
]);
const MAX_POLICY_BYTES = 256 * 1024;
@@ -108,6 +109,7 @@ function auditImageOsVulnerabilityPolicy(
control: 0,
'control-ai': 0,
local: 0,
'local-operator': 0,
worker: 0,
}),
});
@@ -118,6 +120,7 @@ function auditImageOsVulnerabilityPolicy(
control: 0,
'control-ai': 0,
local: 0,
'local-operator': 0,
worker: 0,
};
const seen = new Set();
@@ -120,6 +120,14 @@ function resolveReleaseSelection(input, temporaryRoot, uid) {
const unsigned = { ...selection };
delete unsigned.selectionDigest;
const computedDigest = sha256(JSON.stringify(unsigned));
const applicationImageMatch =
/^ghcr\.io\/([a-z0-9](?:[a-z0-9-]{0,38}))\/qinglong3-local-application@sha256:[a-f0-9]{64}$/u.exec(
selection.service?.image ?? '',
);
const operatorImageMatch =
/^ghcr\.io\/([a-z0-9](?:[a-z0-9-]{0,38}))\/qinglong3-local-operator@sha256:[a-f0-9]{64}$/u.exec(
selection.operator?.image ?? '',
);
if (
!stat.isFile() ||
stat.isSymbolicLink() ||
@@ -135,7 +143,7 @@ function resolveReleaseSelection(input, temporaryRoot, uid) {
computedDigest !== input.releaseSelection.expectedSelectionDigest ||
selection.selectionDigest !== computedDigest ||
selection.schemaVersion !== 1 ||
selection.schema !== 'qinglong/local-compose-release-image@v2' ||
selection.schema !== 'qinglong/local-compose-release-image@v3' ||
!/^3\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$/u.test(
selection.release?.version ?? '',
) ||
@@ -148,6 +156,11 @@ function resolveReleaseSelection(input, temporaryRoot, uid) {
selection.service?.kind !== 'compose' ||
selection.service?.image !== input.image ||
selection.service?.allowRootService !== (uid === 0) ||
!applicationImageMatch ||
!operatorImageMatch ||
applicationImageMatch[1] !== operatorImageMatch[1] ||
selection.operator?.kind !== 'short-lived' ||
selection.operator?.network !== 'none-by-default' ||
selection.catalog?.schema !==
'qinglong/release-catalog-consumption-ceremony@v1' ||
!DIGEST_PATTERN.test(selection.catalog?.manifestDigest ?? '') ||
+17 -2
View File
@@ -38,6 +38,13 @@ const LOCAL_IMAGES = Object.freeze([
runtime_root:
'deploy/containers/ql3-local-application/runtime-dependencies',
}),
Object.freeze({
image: 'local-operator',
repository: 'qinglong3-local-operator',
dockerfile: 'deploy/containers/ql3-local-operator/Dockerfile',
target: 'runtime',
runtime_root: 'deploy/containers/ql3-local-operator/runtime-dependencies',
}),
]);
const CLUSTER_IMAGES = Object.freeze([
Object.freeze({
@@ -208,7 +215,15 @@ function createReleaseCandidateContract(options) {
version: manifest.version,
});
});
const publishMatrix = images.map(({ dockerfile, target, ...image }) => image);
const publishMatrix = images.map(({ dockerfile, target, ...image }) => ({
...image,
local_role_verification:
image.image === 'local'
? 'application_rollout_verified'
: image.image === 'local-operator'
? 'operator_entrypoint_verified'
: 'not_applicable',
}));
const osMatrix = images.flatMap((image) =>
nativeArchitectures.map((architecture) => ({
image: image.image,
@@ -285,7 +300,7 @@ function createReleaseCandidateContract(options) {
'durable-oci-release-catalog',
'offline-deployment-lock-materialization',
...(options.releaseScope !== 'cluster'
? ['edge-and-standalone-rollout']
? ['edge-and-standalone-rollout', 'local-operator-entrypoint']
: []),
...(options.releaseScope !== 'local'
? [
+1 -1
View File
@@ -16,7 +16,7 @@ const CATALOG_PUBLICATION_DECISION_SCHEMA =
const CATALOG_RECEIPT_SCHEMA = 'qinglong/release-catalog-receipt@v2';
const CATALOG_TAG_INVENTORY_DECISION_SCHEMA =
'qinglong/release-catalog-tag-inventory-decision@v1';
const ARTIFACT_TYPE = 'application/vnd.qinglong.release-set.v3+json';
const ARTIFACT_TYPE = 'application/vnd.qinglong.release-set.v4+json';
const FILE_MEDIA_TYPE = ARTIFACT_TYPE;
const OCI_MANIFEST_MEDIA_TYPE = 'application/vnd.oci.image.manifest.v1+json';
const OCI_EMPTY_CONFIG_MEDIA_TYPE = 'application/vnd.oci.empty.v1+json';
@@ -311,10 +311,10 @@ function validatePublicationPlan(plan) {
!Array.isArray(plan.images) ||
plan.images.length !==
(plan.release.scope === 'local'
? 1
? 2
: plan.release.scope === 'cluster'
? 4
: 5)
: 6)
) {
fail('publication plan shape is invalid');
}
+34 -9
View File
@@ -19,8 +19,8 @@ const {
const { VERSION_PATTERN } = require('./lib/ql3-release-identity.cjs');
const DEFAULT_ROOT = path.resolve(__dirname, '..');
const IMAGE_RECORD_SCHEMA = 'qinglong/release-set-image-record@v1';
const RELEASE_SET_SCHEMA = 'qinglong/release-set@v3';
const IMAGE_RECORD_SCHEMA = 'qinglong/release-set-image-record@v2';
const RELEASE_SET_SCHEMA = 'qinglong/release-set@v4';
const MAX_JSON_BYTES = 1024 * 1024;
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
const OWNER_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?$/u;
@@ -135,6 +135,14 @@ function selectedImage(candidate, imageName) {
return matches[0];
}
function expectedLocalRoleVerification(imageName) {
return imageName === 'local'
? 'application_rollout_verified'
: imageName === 'local-operator'
? 'operator_entrypoint_verified'
: 'not_applicable';
}
function deriveVerifiedImageRecord(
candidate,
repositoryOwner,
@@ -166,7 +174,7 @@ function deriveVerifiedImageRecord(
remoteDigestVerified: true,
keylessSignatureVerified: true,
githubAttestations: [...REQUIRED_IMAGE_ATTESTATIONS],
localProfileRolloutVerified: selected.image === 'local',
localRoleVerification: expectedLocalRoleVerification(selected.image),
tagPromotion: 'deferred_to_complete_release_set',
},
};
@@ -179,6 +187,12 @@ function deriveVerifiedImageRecord(
function createVerifiedImageRecord(options) {
const candidate = verifyCandidate(options.candidate, options);
const owner = normalizeRepositoryOwner(options.repositoryOwner);
if (
options.localRoleVerification !==
expectedLocalRoleVerification(options.image)
) {
fail('local role verification differs from the selected image role');
}
return deriveVerifiedImageRecord(
candidate,
owner,
@@ -214,7 +228,7 @@ function validateImageRecord(record, candidate, repositoryOwner) {
'remoteDigestVerified',
'keylessSignatureVerified',
'githubAttestations',
'localProfileRolloutVerified',
'localRoleVerification',
'tagPromotion',
])
) {
@@ -306,10 +320,14 @@ function createReleaseSet(options) {
return record;
});
const localImages = orderedRecords
.filter((record) => record.image.name === 'local')
.filter((record) =>
LOCAL_IMAGES.some((entry) => entry.image === record.image.name),
)
.map((record) => record.image.name);
const clusterImages = orderedRecords
.filter((record) => record.image.name !== 'local')
.filter((record) =>
CLUSTER_IMAGES.some((entry) => entry.image === record.image.name),
)
.map((record) => record.image.name);
const images = orderedRecords.map((record) => ({
...record.image,
@@ -497,12 +515,16 @@ function inspectReleaseSet(actual, options) {
local: {
selected: ['local', 'all'].includes(options.releaseScope),
profiles: ['edge', 'standalone'],
images: expectedNames.filter((name) => name === 'local'),
images: expectedNames.filter((name) =>
LOCAL_IMAGES.some((entry) => entry.image === name),
),
},
cluster: {
selected: ['cluster', 'all'].includes(options.releaseScope),
profiles: ['cluster', 'worker-edge', 'worker-node'],
images: expectedNames.filter((name) => name !== 'local'),
images: expectedNames.filter((name) =>
CLUSTER_IMAGES.some((entry) => entry.image === name),
),
},
};
if (
@@ -619,7 +641,7 @@ function parseArguments(argv) {
const common = ['candidate', ...identity];
const expected =
values.mode === 'record-image'
? [...common, 'digest', 'image', 'output']
? [...common, 'digest', 'image', 'local-role-verification', 'output']
: values.mode === 'aggregate'
? [...common, 'evidence-receipts', 'output', 'records']
: values.mode === 'audit'
@@ -643,6 +665,9 @@ function parseArguments(argv) {
releaseScope: values['release-scope'],
repositoryOwner: values['repository-owner'],
...(values.image ? { image: values.image } : {}),
...(values['local-role-verification']
? { localRoleVerification: values['local-role-verification'] }
: {}),
...(values.digest ? { digest: values.digest } : {}),
...(values.records ? { records: values.records } : {}),
...(values['evidence-receipts']