From c05b68e5547a11eae4e746f7279d7232d77b1e11 Mon Sep 17 00:00:00 2001 From: "melodic-standards-sync[bot]" <300666570+melodic-standards-sync[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 07:51:18 +0000 Subject: [PATCH] chore: sync standards components (15278e2d873d23928f6d982364e7657459c08607) --- .github/standards/runner-policy/policy.json | 5 +- .../runner-policy/policy.schema.json | 5 +- .../standards/runner-policy/runner-policy.mjs | 199 ++++++++++++++++-- 3 files changed, 188 insertions(+), 21 deletions(-) diff --git a/.github/standards/runner-policy/policy.json b/.github/standards/runner-policy/policy.json index ca80a8768..a998980b4 100644 --- a/.github/standards/runner-policy/policy.json +++ b/.github/standards/runner-policy/policy.json @@ -106,7 +106,7 @@ "allowedInputs": ["runner", "prerequisite-result"], "allowedSecrets": {} }, - "melodic-software/ci-workflows/.github/workflows/standards-sync.yml@99ac2f8c5b09dbb785d4eaf18465cbd96c30290c": { + "melodic-software/ci-workflows/.github/workflows/standards-sync.yml@35f2684ac953794b854bac1959df00e74eeca1d9": { "routing": "hosted-only", "allowedInputs": ["dry-run", "targets"], "allowedSecrets": { @@ -159,7 +159,8 @@ "governedReusableRunnerInput": { "name": "runner", "expression": "${{ inputs.runner }}", - "default": "ubuntu-24.04" + "default": "ubuntu-24.04", + "failureSentinel": "ci-runner-selection-failed" }, "forbiddenHostedRunnerLabels": ["macos-latest", "ubuntu-latest", "windows-latest"], "managedLabelPatterns": [ diff --git a/.github/standards/runner-policy/policy.schema.json b/.github/standards/runner-policy/policy.schema.json index 55479c940..b61b07bf4 100644 --- a/.github/standards/runner-policy/policy.schema.json +++ b/.github/standards/runner-policy/policy.schema.json @@ -64,11 +64,12 @@ "governedReusableRunnerInput": { "type": "object", "additionalProperties": false, - "required": ["name", "expression", "default"], + "required": ["name", "expression", "default", "failureSentinel"], "properties": { "name": { "$ref": "#/$defs/nonWhitespaceString" }, "expression": { "$ref": "#/$defs/nonWhitespaceString" }, - "default": { "$ref": "#/$defs/nonWhitespaceString" } + "default": { "$ref": "#/$defs/nonWhitespaceString" }, + "failureSentinel": { "const": "ci-runner-selection-failed" } } }, "forbiddenHostedRunnerLabels": { "$ref": "#/$defs/nonEmptyUniqueStringArray" }, diff --git a/.github/standards/runner-policy/runner-policy.mjs b/.github/standards/runner-policy/runner-policy.mjs index 74acd721b..1af620f0f 100755 --- a/.github/standards/runner-policy/runner-policy.mjs +++ b/.github/standards/runner-policy/runner-policy.mjs @@ -58,6 +58,8 @@ const validatePolicyStructure = SCHEMA_VALIDATOR.compile(POLICY_SCHEMA); const validateRepositoryPolicyStructure = SCHEMA_VALIDATOR.compile(REPOSITORY_POLICY_SCHEMA); const RUNNER_OUTPUT = /^\s*\$\{\{\s*needs\.(?[A-Za-z0-9_-]+)\.outputs\.runner\s*\|\|\s*'(?[^'\r\n]+)'\s*}}\s*$/; +const REQUIRED_RUNNER_OUTPUT = + /^\s*\$\{\{\s*needs\.(?[A-Za-z0-9_-]+)\.outputs\.runner\s*}}\s*$/; const MATRIX_OUTPUT = /^\$\{\{ matrix\.([A-Za-z0-9_-]+) }}$/; const FULL_SHA = /^[0-9a-f]{40}$/i; const REUSABLE_WORKFLOW_PATH = @@ -217,6 +219,19 @@ function validatePolicy(value) { "policy.governedReusableRunnerInput.default must be an approved hosted runner label", ); } + if ( + approvedHostedRunnerLabels.has(value.governedReusableRunnerInput.failureSentinel) || + forbiddenHostedRunnerLabels.has( + value.governedReusableRunnerInput.failureSentinel.toLowerCase(), + ) || + managedLabelRegexes.some((pattern) => + pattern.test(value.governedReusableRunnerInput.failureSentinel), + ) + ) { + throw new ConfigurationError( + "policy.governedReusableRunnerInput.failureSentinel must remain outside every hosted and managed runner label set", + ); + } const hostedMatrixAxes = new Map(); for (const expression of value.hostedMatrixExpressions) { const match = MATRIX_OUTPUT.exec(expression); @@ -562,10 +577,18 @@ function localReusableWorkflowStatus(callerFile, job, policy, workflowIndex) { isLocal: true, approved: false, reason: - "repository-local runner-input workflows must use workflow_call exclusively and declare the governed optional runner default", + "repository-local runner-input workflows must use workflow_call exclusively and declare either the governed optional runner default or a required runner with no default", }; } - return { isLocal: true, approved: true, record, routing }; + const runnerInput = + routing === "runner-input" ? governedReusableRunnerStatus(record.workflow, policy) : undefined; + return { + isLocal: true, + approved: true, + record, + routing, + ...(runnerInput?.mode ? { runnerInputMode: runnerInput.mode } : {}), + }; } function permissionCapability(workflow, job, inherited = "may-write") { @@ -848,18 +871,140 @@ function reusableWorkflowStatus(job, policy) { return { isReusable: true, approved: true, contract }; } -function routeStatus(jobId, target, job, jobs, policy, reusableContract) { - const match = RUNNER_OUTPUT.exec(target); +function normalizedConditionExpression(value) { + if (typeof value !== "string") { + return undefined; + } + const normalized = value.replace(/\s+/g, " ").trim(); + const wrapper = /^\$\{\{ (.*) }}$/.exec(normalized); + return wrapper?.[1]; +} + +function selfHostedSelectorConditionStatus(job, selectorId) { + const cancellation = cancellationSafeConditionStatus(job.if); + if (!cancellation.approved) { + return cancellation; + } + const expression = normalizedConditionExpression(job.if); + const suffix = [ + `needs.${selectorId}.result == 'success'`, + `needs.${selectorId}.outputs.route == 'self-hosted'`, + `needs.${selectorId}.outputs.runner != ''`, + `needs.${selectorId}.outputs.runner == vars.CI_SELF_HOSTED_LABEL`, + ].join(" && "); + if (expression !== `!cancelled() && ${suffix}` && !expression?.endsWith(` && ${suffix}`)) { + return { + approved: false, + reason: + "required local runner inputs must be guarded by selector success, the exact self-hosted route, a nonempty runner, and the governed self-hosted label", + }; + } + return { approved: true }; +} + +function unroutableFailureStatus(jobId, target, job, jobs, policy) { + if (target !== policy.governedReusableRunnerInput.failureSentinel) { + return undefined; + } + const prerequisites = normalizeNeeds(job.needs); + if (prerequisites.length !== 1) { + return { + approved: false, + reason: `${jobId} must declare exactly one selector job in needs to use the unroutable failure sentinel`, + }; + } + const [selectorId] = prerequisites; + const selector = jobs[selectorId]; + const selectorResult = selectorStatus(selector, policy); + if (!selectorResult.isSelector || !selectorResult.approved) { + return { + approved: false, + reason: selectorResult.reason ?? `${selectorId} does not call an approved selector workflow`, + }; + } + const expectedCondition = + `!cancelled() && (needs.${selectorId}.result != 'success' || ` + + `!(needs.${selectorId}.outputs.route == 'self-hosted' && ` + + `needs.${selectorId}.outputs.runner != '' && ` + + `needs.${selectorId}.outputs.runner == vars.CI_SELF_HOSTED_LABEL))`; + if (normalizedConditionExpression(job.if) !== expectedCondition) { + return { + approved: false, + reason: + "the unroutable failure sentinel requires the exact complement of a successful governed self-hosted selector route", + }; + } + const allowedJobKeys = new Set([ + "name", + "needs", + "if", + "runs-on", + "timeout-minutes", + "permissions", + "steps", + ]); + const extraJobKeys = Object.keys(job).filter((key) => !allowedJobKeys.has(key)); + if (extraJobKeys.length > 0) { + return { + approved: false, + reason: `the unroutable failure sentinel job has forbidden keys: ${extraJobKeys.join(", ")}`, + }; + } + if ( + job["timeout-minutes"] !== 1 || + job.permissions === null || + typeof job.permissions !== "object" || + Array.isArray(job.permissions) || + Object.keys(job.permissions).length !== 0 + ) { + return { + approved: false, + reason: "the unroutable failure sentinel job requires timeout-minutes: 1 and permissions: {}", + }; + } + if (!Array.isArray(job.steps) || job.steps.length !== 1) { + return { + approved: false, + reason: "the unroutable failure sentinel job requires exactly one rejecting shell step", + }; + } + const [step] = job.steps; + const stepKeys = + step !== null && typeof step === "object" && !Array.isArray(step) ? Object.keys(step) : []; + const lines = typeof step?.run === "string" ? step.run.trim().split(/\r?\n/) : []; + if ( + stepKeys.some((key) => key !== "name" && key !== "run") || + typeof step?.name !== "string" || + step.name.trim() === "" || + lines.length !== 2 || + !/^echo "::error::[A-Za-z0-9][A-Za-z0-9 .:_-]*"$/.test(lines[0].trim()) || + lines[1].trim() !== "exit 1" + ) { + return { + approved: false, + reason: + "the unroutable failure sentinel step must only emit a static error annotation and exit 1", + }; + } + return { approved: true, selectorId }; +} + +function routeStatus(jobId, target, job, jobs, policy, reusableContract, localRunnerInputMode) { + const fallbackMatch = RUNNER_OUTPUT.exec(target); + const requiredMatch = REQUIRED_RUNNER_OUTPUT.exec(target); const configuredDefault = policy.governedReusableRunnerInput.default; - if (!match || match.groups.fallback !== configuredDefault) { + const usesOptionalDefault = + fallbackMatch !== null && fallbackMatch.groups.fallback === configuredDefault; + const usesRequiredInput = requiredMatch !== null && localRunnerInputMode === "required"; + if (!usesOptionalDefault && !usesRequiredInput) { return { attempted: target.includes("outputs.runner"), approved: false, - reason: `runner routing must use exactly needs..outputs.runner || '${configuredDefault}'`, + reason: `runner routing must use exactly needs..outputs.runner || '${configuredDefault}', or a raw selector output passed to a required no-default repository-local runner input`, }; } - const selectorId = match.groups.selectorId; + const selectorId = (fallbackMatch ?? requiredMatch).groups.selectorId; if (!normalizeNeeds(job.needs).includes(selectorId)) { return { attempted: true, @@ -882,9 +1027,11 @@ function routeStatus(jobId, target, job, jobs, policy, reusableContract) { if (!status.approved) { return { attempted: true, approved: false, reason: status.reason }; } - const condition = reusableContract?.selectorResultInput - ? failClosedSelectorConditionStatus(job, selectorId, reusableContract.selectorResultInput) - : cancellationSafeConditionStatus(job.if); + const condition = usesRequiredInput + ? selfHostedSelectorConditionStatus(job, selectorId) + : reusableContract?.selectorResultInput + ? failClosedSelectorConditionStatus(job, selectorId, reusableContract.selectorResultInput) + : cancellationSafeConditionStatus(job.if); if (!condition.approved) { return { attempted: true, approved: false, reason: condition.reason }; } @@ -1022,17 +1169,21 @@ function governedReusableRunnerStatus(workflow, policy) { reason: `${contract.expression} requires on.workflow_call.inputs.${contract.name}`, }; } - if ( - declaration.type !== "string" || - declaration.default !== contract.default || - declaration.required === true - ) { + const optionalDefault = + declaration.type === "string" && + declaration.default === contract.default && + (declaration.required === undefined || declaration.required === false); + const requiredNoDefault = + declaration.type === "string" && + declaration.required === true && + !Object.hasOwn(declaration, "default"); + if (!optionalDefault && !requiredNoDefault) { return { approved: false, - reason: `on.workflow_call.inputs.${contract.name} must be an optional string defaulting to ${contract.default}`, + reason: `on.workflow_call.inputs.${contract.name} must be either an optional string defaulting to ${contract.default} or a required string with no default`, }; } - return { approved: true }; + return { approved: true, mode: requiredNoDefault ? "required" : "optional-default" }; } function hostedMatrixStatus(job, target, policy) { @@ -1117,6 +1268,16 @@ function runnerTargetStatus(jobId, job, jobs, workflow, policy, file, workflowIn }; } + const unroutableFailure = unroutableFailureStatus(jobId, target, job, jobs, policy); + if (unroutableFailure) { + return { + approved: unroutableFailure.approved, + kind: unroutableFailure.approved ? "unroutable-failure" : "invalid", + route: { attempted: true }, + ...(unroutableFailure.reason ? { reason: unroutableFailure.reason } : {}), + }; + } + const route = routeStatus( jobId, target, @@ -1124,6 +1285,7 @@ function runnerTargetStatus(jobId, job, jobs, workflow, policy, file, workflowIn jobs, policy, !local.approved && reusable.approved ? reusable.contract : undefined, + local.approved ? local.runnerInputMode : undefined, ); if (route.approved) { return { approved: true, kind: "selector-output", route }; @@ -1844,6 +2006,9 @@ export async function auditRepository({ continue; } + if (target?.kind === "unroutable-failure" && target.approved) { + continue; + } if (target?.kind === "selector-output" && target.approved) { continue; }