Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/hotfix_workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ jobs:
uses: ./
with:
single-action: published_deployment_action
merge-queue-check-attestations: ${{ vars.MERGE_QUEUE_CHECK_ATTESTATIONS || '[]' }}
single-action-issue: ${{ inputs.issue }}
issue-workflow-profile: ${{ vars.COPILOT_ISSUE_WORKFLOW_PROFILE || '' }}
single-action-operation-id: ${{ inputs.operation-id }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release_workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ jobs:
uses: ./
with:
single-action: published_deployment_action
merge-queue-check-attestations: ${{ vars.MERGE_QUEUE_CHECK_ATTESTATIONS || '[]' }}
single-action-issue: ${{ inputs.issue }}
issue-workflow-profile: ${{ vars.COPILOT_ISSUE_WORKFLOW_PROFILE || '' }}
single-action-operation-id: ${{ inputs.operation-id }}
Expand Down
6 changes: 4 additions & 2 deletions docs/issues/deployment-orchestration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,10 @@ branch roles and orchestration choices are snapshotted; changing those Variables
does not rewrite an in-flight deployment. Merge-queue attestations are the
exception: they are live repository policy and are reread with effective target
rules at every readiness decision. Both publishing workflows and the managed-PR
continuation workflow pass `MERGE_QUEUE_CHECK_ATTESTATIONS`, so a multi-target
operation does not fall back to stale or empty trust policy between events.
continuation workflow pass `MERGE_QUEUE_CHECK_ATTESTATIONS` (with `[]` only as
the absent-variable fallback). Normal publication callbacks and reconciliation-only
resume both receive this live value in consumer and source workflows, so a
multi-target operation does not fall back to stale or empty trust policy between events.

A recommended non-interactive fragment is:

Expand Down
4 changes: 4 additions & 0 deletions scripts/validate-workflow-contract.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1145,6 +1145,10 @@ function assertPhaseConditions(relativeFile, jobs, finalJob) {
if (publishedActions.length !== 2) {
throw new Error(`${relativeFile} must project publication through published_deployment_action.`);
}
if ([...preparationActions, ...publishedActions].some(step =>
step.with?.['merge-queue-check-attestations'] !== "${{ vars.MERGE_QUEUE_CHECK_ATTESTATIONS || '[]' }}")) {
throw new Error(`${relativeFile} must pass live merge-queue attestations on every preparation and reconciliation entrypoint.`);
}
const trustedContinuations = Object.values(jobs).flatMap(job => job.steps ?? [])
.filter(step => isCopilotAction(step) && [
'create_tag',
Expand Down
1 change: 1 addition & 0 deletions setup/workflows/hotfix_workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ jobs:
uses: vypdev/copilot@v3
with:
single-action: published_deployment_action
merge-queue-check-attestations: ${{ vars.MERGE_QUEUE_CHECK_ATTESTATIONS || '[]' }}
single-action-issue: ${{ inputs.issue }}
issue-workflow-profile: ${{ vars.COPILOT_ISSUE_WORKFLOW_PROFILE || '' }}
single-action-operation-id: ${{ inputs.operation-id }}
Expand Down
1 change: 1 addition & 0 deletions setup/workflows/release_workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ jobs:
uses: vypdev/copilot@v3
with:
single-action: published_deployment_action
merge-queue-check-attestations: ${{ vars.MERGE_QUEUE_CHECK_ATTESTATIONS || '[]' }}
single-action-issue: ${{ inputs.issue }}
issue-workflow-profile: ${{ vars.COPILOT_ISSUE_WORKFLOW_PROFILE || '' }}
single-action-operation-id: ${{ inputs.operation-id }}
Expand Down
23 changes: 23 additions & 0 deletions specs/merge-queue-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -1075,3 +1075,26 @@ feature must prevent and violates the event-driven no-polling architecture.
for merge-queue targets.
- Automatic edits to rulesets or vendor CI.
- A live destructive merge-queue test in a user repository.


### Publication callback policy parity (2026-10-21, #643)

Preparation, normal publication, reconciliation-only resume and managed-PR
continuation MUST all pass the live `MERGE_QUEUE_CHECK_ATTESTATIONS` Repository
Variable with an absent-value `[]` fallback. Consumer templates and source
release/hotfix workflows share this contract. A normal callback starts
reconciliation just as resume does; neither may substitute an empty literal or
a snapshotted preparation value. Target selection, producer identity, failed
policy handling and retained publication state remain unchanged. No new input,
permission, UI state, automatic retry or deployment is introduced.

Flow: prepared promotion -> confirmed publication -> live target policy ->
reconciliation, or the existing blocked recovery state. Existing status cards
and recovery guidance remain authoritative; install updated generated workflows
to adopt the correction. The added numeric test budget is **24 distinct cases**:
source/consumer × release/hotfix × preparation/publication/resume × missing/stale
input. Existing continuation tests cover its fourth entrypoint. Structural
workflow validation rejects either omission or a literal value; all canonical
workflows must pass. Documentation: deployment orchestration's live-policy mapping.
Acceptance: each entrypoint supplies the expression on the actual Action step;
no publication/release is launched to test wiring.
15 changes: 15 additions & 0 deletions src/tooling/__tests__/validate_workflow_contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,21 @@ describe('workflow contract validator', () => {
}
});

it.each(mutationDirectories.flatMap(directory => mutationWorkflowNames.flatMap(fileName =>
['prepare', 'publish', 'resume'].flatMap(phase => ['missing', 'stale'].map(value => [directory, fileName, phase, value])))))(
'rejects %s/%s %s with %s merge-queue attestations', (directory, fileName, phase, value) => {
expectMutationRejected(directory, fileName, workflow => {
const action = phase === 'prepare' ? 'prepare_deployment_action' : 'published_deployment_action';
const job = phase === 'resume' ? workflow.jobs['resume-reconciliation']
: Object.entries(workflow.jobs).find(([id, job]) => id !== 'resume-reconciliation'
&& (job.steps ?? []).some((step: any) => step.with?.['single-action'] === action))![1];
const step = job.steps.find((step: any) => step.with?.['single-action'] === action);
if (value === 'missing') delete step.with['merge-queue-check-attestations'];
else step.with['merge-queue-check-attestations'] = '[]';
}, 'live merge-queue attestations');
},
);

it.each(mutationWorkflowNames)('enforces the active compiled-files budget for %s', (fileName) => {
expectMutationRejected('.github/workflows', fileName, (workflow) => {
workflow.jobs['prepare-compiled-files']['timeout-minutes'] = PREPARE_COMPILED_TIMEOUT_MINUTES - 1;
Expand Down
Loading