diff --git a/README.md b/README.md
index 8917d7d1c..02a72f41d 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,11 @@ flowchart LR
AI capabilities require a configured agent runtime, an allowed model, and working authentication on the selected runner. Code-changing requests require repository write authority and run configured verification before commit/push. Release promotion follows the repository's merge policy. Installing Copilot does not grant permission to merge or deploy.
+On shared runners, the runtime verifies the executing workflow's native PR or
+branch concurrency before setup, so unrelated PRs can proceed independently.
+Custom and legacy workflows retain the conservative predecessor queue. See
+[workflow coordination and recovery](https://docs.page/vypdev/copilot/features#workflow-concurrency-and-sequential-execution).
+
## Start with the web assistant
Use **Node.js 24 or newer**, `pnpm`, and a working checkout of the repository you want to configure:
diff --git a/build/cli/index.js b/build/cli/index.js
index 0960966fa..bc1e7e6e4 100755
--- a/build/cli/index.js
+++ b/build/cli/index.js
@@ -36822,10 +36822,12 @@ async function mainRun(execution, projectBoardCommandPort, latestTagQueryPort, c
(0, logger_1.logInfo)('GitHub Action: starting main run.');
(0, logger_1.logDebugInfo)(`Event: ${execution.eventName}, actor: ${execution.actor}, repo: ${repository.owner}/${repository.repo}, debug: ${execution.debug}`);
if (process.env.GITHUB_ACTIONS === 'true' && !execution.singleAction.isPublishIssueCommentAction) {
- // Every GitHub workflow invocation queues before setup or route work so
- // executions of the same workflow file cannot overlap mutations. A
- // failure notification must remain runnable when that queue gate fails.
- await (0, main_run_lifecycle_1.waitForPreviousWorkflowRuns)(execution.tokens.token, repository);
+ // Verify native resource/lane coordination or retain the predecessor
+ // fence before setup. Failure notifications must remain runnable.
+ await (0, main_run_lifecycle_1.waitForPreviousWorkflowRuns)(execution.tokens.token, repository, {
+ isSingleAction: execution.isSingleAction,
+ pullRequestNumber: execution.inputs?.pull_request?.number,
+ });
}
const setupExecution = (0, execution_setup_composition_root_1.createSetupExecutionUseCase)(latestTagQueryPort, {
owner: repository.owner,
@@ -37672,19 +37674,29 @@ class WorkflowQueueFailureError extends Error {
}
}
exports.WorkflowQueueFailureError = WorkflowQueueFailureError;
-function buildPreviousWorkflowRunsQuery(repository) {
+function buildPreviousWorkflowRunsQuery(repository, event) {
const workflowIdentifier = (0, workflow_context_1.resolveWorkflowIdentifier)(process.env.GITHUB_WORKFLOW_REF);
const query = {
owner: repository.owner,
repository: repository.repo,
- currentRunId: Number.parseInt(process.env.GITHUB_RUN_ID ?? '', 10),
+ currentRunId: Number(process.env.GITHUB_RUN_ID ?? Number.NaN),
...(workflowIdentifier ? { workflowIdentifier } : {}),
+ ...(event && !event.isSingleAction && process.env.GITHUB_WORKFLOW_SHA && process.env.GITHUB_JOB ? {
+ nativeCoordination: Object.freeze({
+ workflowRef: process.env.GITHUB_WORKFLOW_REF ?? '',
+ workflowRevision: process.env.GITHUB_WORKFLOW_SHA,
+ jobId: process.env.GITHUB_JOB,
+ eventName: process.env.GITHUB_EVENT_NAME ?? '',
+ ref: process.env.GITHUB_REF,
+ pullRequestNumber: event.pullRequestNumber,
+ }),
+ } : {}),
};
return query;
}
-async function waitForPreviousWorkflowRuns(token, repository) {
- const query = buildPreviousWorkflowRunsQuery(repository);
- if (process.env.GITHUB_ACTIONS === 'true' && !Number.isSafeInteger(query.currentRunId)) {
+async function waitForPreviousWorkflowRuns(token, repository, event) {
+ const query = buildPreviousWorkflowRunsQuery(repository, event);
+ if (process.env.GITHUB_ACTIONS === 'true' && (!Number.isSafeInteger(query.currentRunId) || query.currentRunId <= 0)) {
throw new Error('GitHub workflow identity is unavailable; refusing to bypass sequential execution.');
}
if (process.env.GITHUB_ACTIONS === 'true' && !query.workflowIdentifier) {
@@ -44668,6 +44680,60 @@ function mergeSetupOverrides(fileOverrides, flagOverrides) {
}
+/***/ }),
+
+/***/ 70998:
+/***/ ((__unused_webpack_module, exports) => {
+
+"use strict";
+
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.expectedNativeWorkflowGroup = expectedNativeWorkflowGroup;
+exports.hasNativeWorkflowCoordination = hasNativeWorkflowCoordination;
+const PUSH_GROUP = 'copilot-push-${{ github.repository }}-${{ github.ref_name }}';
+const PR_GROUP = 'copilot-pr-${{ github.repository }}-${{ github.event.pull_request.number }}';
+/** Closed recognition of the installed contract, without evaluating expressions. */
+function expectedNativeWorkflowGroup(query) {
+ const native = query.nativeCoordination;
+ if (!native || !Number.isSafeInteger(query.currentRunId) || query.currentRunId <= 0
+ || !/^[\w.-]+\.ya?ml$/u.test(query.workflowIdentifier ?? '')
+ || !native.workflowRef.startsWith(`${query.owner}/${query.repository}/.github/workflows/${query.workflowIdentifier}@`)
+ || native.workflowRef.endsWith('@')
+ || !/^[a-f\d]{40}$/iu.test(native.workflowRevision)
+ || !/^[a-zA-Z_][\w-]*$/u.test(native.jobId))
+ return undefined;
+ if (native.eventName === 'push' && native.ref?.startsWith('refs/heads/')
+ && native.ref.length > 'refs/heads/'.length)
+ return PUSH_GROUP;
+ const pullRequestNumber = native.pullRequestNumber;
+ if (typeof pullRequestNumber !== 'number' || !Number.isSafeInteger(pullRequestNumber) || pullRequestNumber <= 0)
+ return undefined;
+ if (native.eventName === 'pull_request')
+ return `${PR_GROUP}-analysis`;
+ if (native.eventName === 'pull_request_review')
+ return `${PR_GROUP}-review-state`;
+ return undefined;
+}
+function hasNativeWorkflowCoordination(workflow, jobId, expectedGroup) {
+ const definition = record(workflow);
+ const jobs = record(definition?.jobs);
+ const job = record(jobs?.[jobId]);
+ const concurrency = record(job?.concurrency);
+ // Custom/reusable/workflow-level contracts retain conservative polling.
+ // A single direct job also avoids confusing a reusable callee's job ID
+ // with a different protected job in its caller's workflow context.
+ return definition?.concurrency === undefined && jobs !== undefined && Object.keys(jobs).length === 1
+ && Array.isArray(job?.steps)
+ && job?.uses === undefined && concurrency?.group === expectedGroup
+ && concurrency?.['cancel-in-progress'] === true
+ && (concurrency.queue === undefined || concurrency.queue === 'single');
+}
+function record(value) {
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
+ ? value : undefined;
+}
+
+
/***/ }),
/***/ 70628:
@@ -73353,18 +73419,28 @@ const application_error_1 = __nccwpck_require__(75999);
const SYSTEM_CLOCK = { nowMilliseconds: () => Date.now() };
const SYSTEM_RANDOM = { next: () => Math.random() };
class WaitForPreviousWorkflowRunsUseCase {
- constructor(queryPort, delayPort, observerPort, policy = workflow_queue_policy_1.WORKFLOW_QUEUE_POLICY, clock = SYSTEM_CLOCK, random = SYSTEM_RANDOM) {
+ constructor(queryPort, delayPort, observerPort, policy = workflow_queue_policy_1.WORKFLOW_QUEUE_POLICY, clock = SYSTEM_CLOCK, random = SYSTEM_RANDOM, nativeCoordination) {
this.queryPort = queryPort;
this.delayPort = delayPort;
this.observerPort = observerPort;
this.policy = policy;
this.clock = clock;
this.random = random;
+ this.nativeCoordination = nativeCoordination;
this.taskId = 'WaitForPreviousWorkflowRunsUseCase';
}
async invoke(query) {
const deadlineAtMilliseconds = this.clock.nowMilliseconds() + this.policy.maximumQueueWaitMilliseconds;
let pollIndex = 0;
+ if (this.nativeCoordination && this.clock.nowMilliseconds() < deadlineAtMilliseconds) {
+ const coordinated = await this.nativeCoordination.isCoordinated(query, { deadlineAtMilliseconds });
+ if (this.clock.nowMilliseconds() >= deadlineAtMilliseconds)
+ throw queueTimeoutError();
+ if (coordinated) {
+ this.observerPort.nativeCoordinationVerified?.();
+ return;
+ }
+ }
while (true) {
if (this.clock.nowMilliseconds() >= deadlineAtMilliseconds) {
throw queueTimeoutError();
@@ -89585,6 +89661,55 @@ function isActivePreviousRun(run, query) {
}
+/***/ }),
+
+/***/ 74039:
+/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
+
+"use strict";
+
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.NativeWorkflowCoordinationRepository = void 0;
+const js_yaml_1 = __nccwpck_require__(783);
+const native_workflow_coordination_policy_1 = __nccwpck_require__(70998);
+const workflow_runs_retry_1 = __nccwpck_require__(86434);
+const MAX_WORKFLOW_BYTES = 256 * 1024;
+/** Reads the workflow GitHub executed, never the possibly modified checkout. */
+class NativeWorkflowCoordinationRepository {
+ constructor(client, retry) {
+ this.client = client;
+ this.retry = retry;
+ }
+ async isCoordinated(query, context) {
+ const group = (0, native_workflow_coordination_policy_1.expectedNativeWorkflowGroup)(query);
+ const native = query.nativeCoordination;
+ if (!group || !native)
+ return false;
+ const response = await (0, workflow_runs_retry_1.withWorkflowRunsRetry)(() => this.client.rest.repos.getContent({
+ owner: query.owner,
+ repo: query.repository,
+ path: `.github/workflows/${query.workflowIdentifier}`,
+ ref: native.workflowRevision,
+ }), { ...this.retry, policy: this.retry.policy ?? workflow_runs_retry_1.WORKFLOW_RUNS_RETRY_POLICY,
+ deadlineAtMilliseconds: context.deadlineAtMilliseconds });
+ const file = response.data;
+ if (!file || Array.isArray(file) || file.type !== 'file' || file.encoding !== 'base64'
+ || typeof file.content !== 'string' || file.content.length > MAX_WORKFLOW_BYTES * 2) {
+ throw new Error('Executing workflow content is unavailable.');
+ }
+ const encoded = file.content.replace(/\s/gu, '');
+ const bytes = Buffer.from(encoded, 'base64');
+ if (bytes.toString('base64') !== encoded)
+ throw new Error('Executing workflow encoding is invalid.');
+ if (bytes.length > MAX_WORKFLOW_BYTES)
+ throw new Error('Executing workflow exceeds the inspection limit.');
+ const workflow = (0, js_yaml_1.load)(bytes.toString('utf8'), { schema: js_yaml_1.JSON_SCHEMA });
+ return (0, native_workflow_coordination_policy_1.hasNativeWorkflowCoordination)(workflow, native.jobId, group);
+ }
+}
+exports.NativeWorkflowCoordinationRepository = NativeWorkflowCoordinationRepository;
+
+
/***/ }),
/***/ 29509:
@@ -97677,12 +97802,17 @@ const timer_workflow_polling_delay_adapter_1 = __nccwpck_require__(10339);
const logger_workflow_polling_observer_adapter_1 = __nccwpck_require__(52883);
const system_workflow_queue_clock_adapter_1 = __nccwpck_require__(49664);
const system_workflow_polling_random_adapter_1 = __nccwpck_require__(32679);
-const github_workflow_client_factory_1 = __nccwpck_require__(29839);
+const native_workflow_coordination_repository_1 = __nccwpck_require__(74039);
+const octokit_client_resolver_1 = __nccwpck_require__(54047);
function createWaitForPreviousWorkflowRunsUseCase(token) {
- const client = (0, github_workflow_client_factory_1.createWorkflowRunsClient)().getClient(token);
+ const client = (0, octokit_client_resolver_1.getOctokitClient)(token);
const delayPort = new timer_workflow_polling_delay_adapter_1.TimerWorkflowPollingDelayAdapter();
const observerPort = new logger_workflow_polling_observer_adapter_1.LoggerWorkflowPollingObserverAdapter();
- return new wait_for_previous_workflow_runs_use_case_1.WaitForPreviousWorkflowRunsUseCase(new active_previous_workflow_runs_repository_1.ActivePreviousWorkflowRunsRepository(client, delayPort, undefined, new system_workflow_queue_clock_adapter_1.SystemWorkflowQueueClockAdapter(), new system_workflow_polling_random_adapter_1.SystemWorkflowPollingRandomAdapter(), observerPort), delayPort, observerPort);
+ const clock = new system_workflow_queue_clock_adapter_1.SystemWorkflowQueueClockAdapter();
+ const random = new system_workflow_polling_random_adapter_1.SystemWorkflowPollingRandomAdapter();
+ return new wait_for_previous_workflow_runs_use_case_1.WaitForPreviousWorkflowRunsUseCase(new active_previous_workflow_runs_repository_1.ActivePreviousWorkflowRunsRepository(client, delayPort, undefined, clock, random, observerPort), delayPort, observerPort, undefined, clock, random, new native_workflow_coordination_repository_1.NativeWorkflowCoordinationRepository(client, {
+ delayPort, clock, random, observer: observerPort,
+ }));
}
@@ -98983,6 +99113,9 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.LoggerWorkflowPollingObserverAdapter = void 0;
const logger_1 = __nccwpck_require__(91151);
class LoggerWorkflowPollingObserverAdapter {
+ nativeCoordinationVerified() {
+ (0, logger_1.logInfo)('Native workflow resource/lane coordination verified; continuing before setup.');
+ }
noActivePreviousRuns() {
(0, logger_1.logDebugInfo)('✅ No previous runs active. Continuing...');
}
diff --git a/build/github_action/index.js b/build/github_action/index.js
index 2fd7f15bf..333bc5dcc 100644
--- a/build/github_action/index.js
+++ b/build/github_action/index.js
@@ -36807,10 +36807,12 @@ async function mainRun(execution, projectBoardCommandPort, latestTagQueryPort, c
(0, logger_1.logInfo)('GitHub Action: starting main run.');
(0, logger_1.logDebugInfo)(`Event: ${execution.eventName}, actor: ${execution.actor}, repo: ${repository.owner}/${repository.repo}, debug: ${execution.debug}`);
if (process.env.GITHUB_ACTIONS === 'true' && !execution.singleAction.isPublishIssueCommentAction) {
- // Every GitHub workflow invocation queues before setup or route work so
- // executions of the same workflow file cannot overlap mutations. A
- // failure notification must remain runnable when that queue gate fails.
- await (0, main_run_lifecycle_1.waitForPreviousWorkflowRuns)(execution.tokens.token, repository);
+ // Verify native resource/lane coordination or retain the predecessor
+ // fence before setup. Failure notifications must remain runnable.
+ await (0, main_run_lifecycle_1.waitForPreviousWorkflowRuns)(execution.tokens.token, repository, {
+ isSingleAction: execution.isSingleAction,
+ pullRequestNumber: execution.inputs?.pull_request?.number,
+ });
}
const setupExecution = (0, execution_setup_composition_root_1.createSetupExecutionUseCase)(latestTagQueryPort, {
owner: repository.owner,
@@ -38253,19 +38255,29 @@ class WorkflowQueueFailureError extends Error {
}
}
exports.WorkflowQueueFailureError = WorkflowQueueFailureError;
-function buildPreviousWorkflowRunsQuery(repository) {
+function buildPreviousWorkflowRunsQuery(repository, event) {
const workflowIdentifier = (0, workflow_context_1.resolveWorkflowIdentifier)(process.env.GITHUB_WORKFLOW_REF);
const query = {
owner: repository.owner,
repository: repository.repo,
- currentRunId: Number.parseInt(process.env.GITHUB_RUN_ID ?? '', 10),
+ currentRunId: Number(process.env.GITHUB_RUN_ID ?? Number.NaN),
...(workflowIdentifier ? { workflowIdentifier } : {}),
+ ...(event && !event.isSingleAction && process.env.GITHUB_WORKFLOW_SHA && process.env.GITHUB_JOB ? {
+ nativeCoordination: Object.freeze({
+ workflowRef: process.env.GITHUB_WORKFLOW_REF ?? '',
+ workflowRevision: process.env.GITHUB_WORKFLOW_SHA,
+ jobId: process.env.GITHUB_JOB,
+ eventName: process.env.GITHUB_EVENT_NAME ?? '',
+ ref: process.env.GITHUB_REF,
+ pullRequestNumber: event.pullRequestNumber,
+ }),
+ } : {}),
};
return query;
}
-async function waitForPreviousWorkflowRuns(token, repository) {
- const query = buildPreviousWorkflowRunsQuery(repository);
- if (process.env.GITHUB_ACTIONS === 'true' && !Number.isSafeInteger(query.currentRunId)) {
+async function waitForPreviousWorkflowRuns(token, repository, event) {
+ const query = buildPreviousWorkflowRunsQuery(repository, event);
+ if (process.env.GITHUB_ACTIONS === 'true' && (!Number.isSafeInteger(query.currentRunId) || query.currentRunId <= 0)) {
throw new Error('GitHub workflow identity is unavailable; refusing to bypass sequential execution.');
}
if (process.env.GITHUB_ACTIONS === 'true' && !query.workflowIdentifier) {
@@ -46209,6 +46221,60 @@ function boundedMergeQueueDiagnostic(value) {
}
+/***/ }),
+
+/***/ 70998:
+/***/ ((__unused_webpack_module, exports) => {
+
+"use strict";
+
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.expectedNativeWorkflowGroup = expectedNativeWorkflowGroup;
+exports.hasNativeWorkflowCoordination = hasNativeWorkflowCoordination;
+const PUSH_GROUP = 'copilot-push-${{ github.repository }}-${{ github.ref_name }}';
+const PR_GROUP = 'copilot-pr-${{ github.repository }}-${{ github.event.pull_request.number }}';
+/** Closed recognition of the installed contract, without evaluating expressions. */
+function expectedNativeWorkflowGroup(query) {
+ const native = query.nativeCoordination;
+ if (!native || !Number.isSafeInteger(query.currentRunId) || query.currentRunId <= 0
+ || !/^[\w.-]+\.ya?ml$/u.test(query.workflowIdentifier ?? '')
+ || !native.workflowRef.startsWith(`${query.owner}/${query.repository}/.github/workflows/${query.workflowIdentifier}@`)
+ || native.workflowRef.endsWith('@')
+ || !/^[a-f\d]{40}$/iu.test(native.workflowRevision)
+ || !/^[a-zA-Z_][\w-]*$/u.test(native.jobId))
+ return undefined;
+ if (native.eventName === 'push' && native.ref?.startsWith('refs/heads/')
+ && native.ref.length > 'refs/heads/'.length)
+ return PUSH_GROUP;
+ const pullRequestNumber = native.pullRequestNumber;
+ if (typeof pullRequestNumber !== 'number' || !Number.isSafeInteger(pullRequestNumber) || pullRequestNumber <= 0)
+ return undefined;
+ if (native.eventName === 'pull_request')
+ return `${PR_GROUP}-analysis`;
+ if (native.eventName === 'pull_request_review')
+ return `${PR_GROUP}-review-state`;
+ return undefined;
+}
+function hasNativeWorkflowCoordination(workflow, jobId, expectedGroup) {
+ const definition = record(workflow);
+ const jobs = record(definition?.jobs);
+ const job = record(jobs?.[jobId]);
+ const concurrency = record(job?.concurrency);
+ // Custom/reusable/workflow-level contracts retain conservative polling.
+ // A single direct job also avoids confusing a reusable callee's job ID
+ // with a different protected job in its caller's workflow context.
+ return definition?.concurrency === undefined && jobs !== undefined && Object.keys(jobs).length === 1
+ && Array.isArray(job?.steps)
+ && job?.uses === undefined && concurrency?.group === expectedGroup
+ && concurrency?.['cancel-in-progress'] === true
+ && (concurrency.queue === undefined || concurrency.queue === 'single');
+}
+function record(value) {
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
+ ? value : undefined;
+}
+
+
/***/ }),
/***/ 70628:
@@ -69055,18 +69121,28 @@ const application_error_1 = __nccwpck_require__(75999);
const SYSTEM_CLOCK = { nowMilliseconds: () => Date.now() };
const SYSTEM_RANDOM = { next: () => Math.random() };
class WaitForPreviousWorkflowRunsUseCase {
- constructor(queryPort, delayPort, observerPort, policy = workflow_queue_policy_1.WORKFLOW_QUEUE_POLICY, clock = SYSTEM_CLOCK, random = SYSTEM_RANDOM) {
+ constructor(queryPort, delayPort, observerPort, policy = workflow_queue_policy_1.WORKFLOW_QUEUE_POLICY, clock = SYSTEM_CLOCK, random = SYSTEM_RANDOM, nativeCoordination) {
this.queryPort = queryPort;
this.delayPort = delayPort;
this.observerPort = observerPort;
this.policy = policy;
this.clock = clock;
this.random = random;
+ this.nativeCoordination = nativeCoordination;
this.taskId = 'WaitForPreviousWorkflowRunsUseCase';
}
async invoke(query) {
const deadlineAtMilliseconds = this.clock.nowMilliseconds() + this.policy.maximumQueueWaitMilliseconds;
let pollIndex = 0;
+ if (this.nativeCoordination && this.clock.nowMilliseconds() < deadlineAtMilliseconds) {
+ const coordinated = await this.nativeCoordination.isCoordinated(query, { deadlineAtMilliseconds });
+ if (this.clock.nowMilliseconds() >= deadlineAtMilliseconds)
+ throw queueTimeoutError();
+ if (coordinated) {
+ this.observerPort.nativeCoordinationVerified?.();
+ return;
+ }
+ }
while (true) {
if (this.clock.nowMilliseconds() >= deadlineAtMilliseconds) {
throw queueTimeoutError();
@@ -80097,6 +80173,55 @@ function isActivePreviousRun(run, query) {
}
+/***/ }),
+
+/***/ 74039:
+/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
+
+"use strict";
+
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.NativeWorkflowCoordinationRepository = void 0;
+const js_yaml_1 = __nccwpck_require__(783);
+const native_workflow_coordination_policy_1 = __nccwpck_require__(70998);
+const workflow_runs_retry_1 = __nccwpck_require__(86434);
+const MAX_WORKFLOW_BYTES = 256 * 1024;
+/** Reads the workflow GitHub executed, never the possibly modified checkout. */
+class NativeWorkflowCoordinationRepository {
+ constructor(client, retry) {
+ this.client = client;
+ this.retry = retry;
+ }
+ async isCoordinated(query, context) {
+ const group = (0, native_workflow_coordination_policy_1.expectedNativeWorkflowGroup)(query);
+ const native = query.nativeCoordination;
+ if (!group || !native)
+ return false;
+ const response = await (0, workflow_runs_retry_1.withWorkflowRunsRetry)(() => this.client.rest.repos.getContent({
+ owner: query.owner,
+ repo: query.repository,
+ path: `.github/workflows/${query.workflowIdentifier}`,
+ ref: native.workflowRevision,
+ }), { ...this.retry, policy: this.retry.policy ?? workflow_runs_retry_1.WORKFLOW_RUNS_RETRY_POLICY,
+ deadlineAtMilliseconds: context.deadlineAtMilliseconds });
+ const file = response.data;
+ if (!file || Array.isArray(file) || file.type !== 'file' || file.encoding !== 'base64'
+ || typeof file.content !== 'string' || file.content.length > MAX_WORKFLOW_BYTES * 2) {
+ throw new Error('Executing workflow content is unavailable.');
+ }
+ const encoded = file.content.replace(/\s/gu, '');
+ const bytes = Buffer.from(encoded, 'base64');
+ if (bytes.toString('base64') !== encoded)
+ throw new Error('Executing workflow encoding is invalid.');
+ if (bytes.length > MAX_WORKFLOW_BYTES)
+ throw new Error('Executing workflow exceeds the inspection limit.');
+ const workflow = (0, js_yaml_1.load)(bytes.toString('utf8'), { schema: js_yaml_1.JSON_SCHEMA });
+ return (0, native_workflow_coordination_policy_1.hasNativeWorkflowCoordination)(workflow, native.jobId, group);
+ }
+}
+exports.NativeWorkflowCoordinationRepository = NativeWorkflowCoordinationRepository;
+
+
/***/ }),
/***/ 29509:
@@ -88044,12 +88169,17 @@ const timer_workflow_polling_delay_adapter_1 = __nccwpck_require__(10339);
const logger_workflow_polling_observer_adapter_1 = __nccwpck_require__(52883);
const system_workflow_queue_clock_adapter_1 = __nccwpck_require__(49664);
const system_workflow_polling_random_adapter_1 = __nccwpck_require__(32679);
-const github_workflow_client_factory_1 = __nccwpck_require__(29839);
+const native_workflow_coordination_repository_1 = __nccwpck_require__(74039);
+const octokit_client_resolver_1 = __nccwpck_require__(54047);
function createWaitForPreviousWorkflowRunsUseCase(token) {
- const client = (0, github_workflow_client_factory_1.createWorkflowRunsClient)().getClient(token);
+ const client = (0, octokit_client_resolver_1.getOctokitClient)(token);
const delayPort = new timer_workflow_polling_delay_adapter_1.TimerWorkflowPollingDelayAdapter();
const observerPort = new logger_workflow_polling_observer_adapter_1.LoggerWorkflowPollingObserverAdapter();
- return new wait_for_previous_workflow_runs_use_case_1.WaitForPreviousWorkflowRunsUseCase(new active_previous_workflow_runs_repository_1.ActivePreviousWorkflowRunsRepository(client, delayPort, undefined, new system_workflow_queue_clock_adapter_1.SystemWorkflowQueueClockAdapter(), new system_workflow_polling_random_adapter_1.SystemWorkflowPollingRandomAdapter(), observerPort), delayPort, observerPort);
+ const clock = new system_workflow_queue_clock_adapter_1.SystemWorkflowQueueClockAdapter();
+ const random = new system_workflow_polling_random_adapter_1.SystemWorkflowPollingRandomAdapter();
+ return new wait_for_previous_workflow_runs_use_case_1.WaitForPreviousWorkflowRunsUseCase(new active_previous_workflow_runs_repository_1.ActivePreviousWorkflowRunsRepository(client, delayPort, undefined, clock, random, observerPort), delayPort, observerPort, undefined, clock, random, new native_workflow_coordination_repository_1.NativeWorkflowCoordinationRepository(client, {
+ delayPort, clock, random, observer: observerPort,
+ }));
}
@@ -89058,6 +89188,9 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.LoggerWorkflowPollingObserverAdapter = void 0;
const logger_1 = __nccwpck_require__(91151);
class LoggerWorkflowPollingObserverAdapter {
+ nativeCoordinationVerified() {
+ (0, logger_1.logInfo)('Native workflow resource/lane coordination verified; continuing before setup.');
+ }
noActivePreviousRuns() {
(0, logger_1.logDebugInfo)('✅ No previous runs active. Continuing...');
}
diff --git a/docs/features.mdx b/docs/features.mdx
index 8339fd373..8acd72fbc 100644
--- a/docs/features.mdx
+++ b/docs/features.mdx
@@ -166,7 +166,7 @@ decorative media or expose configuration for it.
## Workflow concurrency and sequential execution
- **Coordination policies:** issue and comment mutation workflows preserve admitted events in a workflow-local queue. Deployment mutations use a shared issue-scoped native queue. Commit and Pull Request each have a separate repository/branch latest-revision lane, so paired events cannot cancel one another.
+ **Coordination policies:** issue and comment mutation workflows preserve admitted events in a workflow-local queue. Deployment mutations use a shared issue-scoped native queue. Commit uses a branch lane; PR analysis and review-state use separate lanes per PR number. The runtime verifies those native lanes before setup, so unrelated PRs do not wait for each other while occupying shared runners.
GitHub's native [concurrency](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency) can cancel in-progress runs when a newer one starts. Its default `queue: single` retains only one pending run; `queue: max` allows up to 100 pending jobs or workflow runs when `cancel-in-progress` is `false` or omitted. The replaceable event jobs therefore use event-specific keys: Commit uses `copilot-push-${{ github.repository }}-${{ github.ref_name }}`; Pull Request code/lifecycle analysis uses the repository and PR number with an `analysis` suffix; and the dedicated review-state workflow uses the same PR-number key with a `review-state` suffix. Different PRs sharing a head branch cannot cancel one another. Each PR lane cancels only its own obsolete revision. Commit still synchronizes issue progress, then Bugbot performs a read-only exact-head lookup because a `push` payload does not contain PR identity. A validated open same-repository PR makes the push stop before review-context loading or agent invocation; the PR `synchronize` event exclusively owns review for that head. This prevents duplicate reviews without letting a push or review-state event cancel useful PR analysis. Branches without an open PR retain push-time Bugbot. Metadata-only `pull_request: edited` events are not subscribed because Copilot changes the PR body itself; excluding them prevents self-generated run cascades and preserves human metadata edits. Application-level head guards prevent an older run from updating a newer PR, and the next code/lifecycle run repairs any durable half-transition discovered after cancellation. Fixed workflow/job names expose analysis and review-state checks without leaking an unevaluated expression when GitHub skips bot-authored events. A separate merge-group workflow avoids a skipped duplicate on normal PRs while deliberately preserving the exact `Copilot - Pull Request` required-check context for branch-protection compatibility.
@@ -184,10 +184,19 @@ is no alternate queue-only Action mode or direct/local deployment mutation path.
### How it works
1. For a GitHub Action run with a PAT, the action first compares the event actor with the authenticated PAT user. A normal run from the same account completes successfully before project composition, agent provisioning, setup, or queue polling. A valid explicit single action continues through the normal lifecycle.
-2. An admitted run resolves the current workflow file from `GITHUB_WORKFLOW_REF` and queries the workflow-scoped endpoint concurrently for `in_progress`, `queued`, `requested`, `waiting`, and `pending` runs with `per_page: 100`. It then keeps only runs with a **lower run ID** (i.e. started earlier). Completed history and unrelated workflows are never included.
-3. Provider failures fail closed. Transient 408/5xx/network errors use bounded exponential retry; HTTP 429 and rate-limited 403 responses honor `Retry-After` or `x-ratelimit-reset`, then use a slower bounded fallback. Diagnostics contain only the retry reason, attempt, delay, and safe reset timestamp metadata.
-4. If any such run exists, the action polls immediately and then uses adaptive 5s, 10s, 20s, 40s, and 60s maximum delays with bounded ±20% jitter. The absolute queue wait is limited to 90 minutes.
-5. When no earlier active run in the mutation queue remains, the action continues. A provider failure or queue deadline never becomes an empty result, so setup and mutation work cannot proceed with an unknown queue state.
+2. Before setup, an admitted normal branch push, PR or review-state run can prove native coordination. Copilot reads the workflow from the current repository at `GITHUB_WORKFLOW_SHA`, uses `GITHUB_JOB` to select the executing job, and requires its exact group above with literal `cancel-in-progress: true`. It reads GitHub's pinned source using existing repository contents-read access, never the checkout or a moving branch. Proven jobs log `Native workflow resource/lane coordination verified; continuing before setup.` and use GitHub's existing resource/lane fence without polling other runs. The checks and findings still run normally.
+3. Without proof, the existing workflow queue applies: this includes legacy/custom groups, reusable or multi-job workflows, missing metadata, single actions and unrelated event types. Copilot resolves the workflow file from `GITHUB_WORKFLOW_REF` and queries the workflow-scoped endpoint concurrently for `in_progress`, `queued`, `requested`, `waiting`, and `pending` runs with `per_page: 100`. It keeps only runs with a **lower run ID**. Completed history and unrelated workflows are never included. A matching filename alone does not establish native coordination.
+4. Provider failures fail closed for both native inspection and predecessor queries. Transient 408/5xx/network errors use bounded exponential retry; HTTP 429 and rate-limited 403 responses honor `Retry-After` or `x-ratelimit-reset`, then use a slower bounded fallback. Diagnostics contain only the retry reason, attempt, delay, and safe reset timestamp metadata.
+5. If any predecessor exists, the action polls immediately and then uses adaptive 5s, 10s, 20s, 40s, and 60s maximum delays with bounded ±20% jitter. Native inspection and polling share the absolute 90-minute queue budget.
+6. When no earlier active run in the mutation queue remains, the action continues. A provider failure, malformed workflow source or queue deadline never grants permission to execute, so setup and mutation work cannot proceed with an unknown coordination state.
+
+After upgrading the runtime, installations already using the current exact
+groups need no workflow change or new Variable. If an older runtime is already
+waiting while occupying a shared runner, inspect the waiting run and its
+predecessors in Actions. Preserve necessary predecessor callbacks and rerun the
+affected workflow after the fix is available; a running older process cannot
+adopt the new policy. Legacy/custom workflows retain conservative serialization.
+Do not remove their queue or weaken checks to recover runner capacity.
The queue deliberately traverses every page in each active-status subset because
exact counting must detect a matching run on a later page. Querying only active
diff --git a/docs/pull-requests/workflow-setup.mdx b/docs/pull-requests/workflow-setup.mdx
index 0ef51e1ee..1adde09e5 100644
--- a/docs/pull-requests/workflow-setup.mdx
+++ b/docs/pull-requests/workflow-setup.mdx
@@ -129,6 +129,17 @@ reviewed maintenance PR. Update analysis and review-state together and retain
project-specific setup/release customizations. No new Variable, Secret or
permission is required. Current-head checks and review remain the merge gate.
+The runtime verifies these native groups before setup by reading the executing
+workflow at GitHub's `GITHUB_WORKFLOW_SHA` and selecting `GITHUB_JOB`. Existing
+repository contents-read access is required. Verified PR lanes do not poll
+queued runs for other PRs, which prevents a later job from holding a shared
+runner while its predecessor needs that same pool. Same-PR/lane replacement
+and all application freshness, findings and permission checks remain in force.
+There is no queue-bypass setting. Missing metadata or custom concurrency uses
+the conservative workflow queue; a workflow-read failure stops before setup.
+See [coordination and recovery](/features#workflow-concurrency-and-sequential-execution)
+for upgrading and rerunning affected older jobs.
+
- **`token`** is required (use a fine-grained PAT with repo and project permissions; see [Authentication](/authentication)).
- **`project-ids`** is optional but needed if you want PRs linked to GitHub Project boards and moved to columns (e.g. "In Progress").
- The conditional fetch downloads only the two commit objects needed by the incremental reviewer. Keep it when `synchronize` is enabled; a full-history checkout is not required.
diff --git a/specs/CATALOG.md b/specs/CATALOG.md
index a0d046df3..c445d59b6 100644
--- a/specs/CATALOG.md
+++ b/specs/CATALOG.md
@@ -14,7 +14,7 @@ debt or convert unknown historic intent into a design decision.
| `release-orchestration` | Implemented | Release and hotfix promotion, publication, bounded merge readiness recovery, and receipt-verified reconciliation without republication | [Configurable production-first release orchestration](./configurable-release-orchestration.md) + 2 companion | 59 paths · 2026-10-16 |
| `merge-queue-readiness` | Implemented | Fail-closed validation of required checks and merge-group workflow support; optional manual-only RepoWise analysis with independent CI and verified required-check migration; cached RepoWise exports and bounded full-history secret processing with truthful completeness and resource evidence | [Merge queue readiness and effective target rules](./merge-queue-readiness.md) | 29 paths · 2026-10-21 |
| `bugbot-review-state-reconciliation` | Implemented | Reconcile review snapshots, findings, threads, comments, and check conclusions | [Bugbot review-state reconciliation](./bugbot-review-state-reconciliation.md) | 74 paths · 2026-10-24 |
-| `execution-lifecycle` | Implemented | Shared GitHub Action lifecycle from event admission through durable user-facing results | [Execution admission, queueing, routing, and result publication](./execution-admission-queue-and-publication.md) + 3 companion | 105 paths · 2026-10-21 |
+| `execution-lifecycle` | Implemented | Shared GitHub Action lifecycle with pinned native resource/lane coordination proof, conservative predecessor fencing, routing and durable user-facing results | [Execution admission, queueing, routing, and result publication](./execution-admission-queue-and-publication.md) + 3 companion | 119 paths · 2026-10-25 |
| `architecture-quality-hardening` | Implemented | Close verified concurrency, error-contract, context-coupling, fan-out, setup/doctor, and provider-policy risks in dependency order | [Architecture quality and scalability hardening](./architecture-quality-and-scalability-hardening.md) + 1 companion | 72 paths · 2026-10-13 |
| `setup-and-doctor` | Implemented | Plan, validate, provision, and audit a repository installation without exposing credentials | [Setup, configuration, credentials, and doctor](./setup-configuration-credentials-and-doctor.md) + 6 companion | 332 paths · 2026-10-24 |
| `local-web-setup-assistant` | Proposed | Offer a packaged, loopback-only Svelte setup interface over the existing engine with four-language guidance, English CLI help, basic/custom first-run paths, editable question and plan review, source-labelled GitHub facts, assisted CI and Project evidence, role-separated PATs, and truthful itemized outcomes; inspect existing configuration and apply independent, scope-aware runtime adjustments with permission, drift and read-back verification | [Local web setup assistant](./local-web-setup-assistant.md) + 2 companion | 247 paths · 2026-10-21 |
@@ -81,13 +81,13 @@ debt or convert unknown historic intent into a design decision.
### `execution-lifecycle` — Execution admission, queueing, routing, and result publication
- Owner: Copilot maintainers
-- Last verified: 2026-10-21
+- Last verified: 2026-10-25
- Specifications: [`specs/execution-admission-queue-and-publication.md`](./execution-admission-queue-and-publication.md) · [`specs/execution-error-and-context-hardening.md`](./execution-error-and-context-hardening.md) · [`specs/execution-boundary-closure-audit.md`](./execution-boundary-closure-audit.md) · [`specs/push-and-single-action-context-hardening.md`](./push-and-single-action-context-hardening.md)
- Workflows: [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) · [`.github/workflows/copilot_issue_comment.yml`](../.github/workflows/copilot_issue_comment.yml) · [`.github/workflows/copilot_pull_request.yml`](../.github/workflows/copilot_pull_request.yml) · [`.github/workflows/copilot_pull_request_comment.yml`](../.github/workflows/copilot_pull_request_comment.yml) · [`.github/workflows/copilot_commit.yml`](../.github/workflows/copilot_commit.yml)
- Entrypoints: [`src/actions/github_action.ts`](../src/actions/github_action.ts) · [`src/actions/common_action.ts`](../src/actions/common_action.ts)
-- Core code: [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`scripts/validate-coverage-budgets.cjs`](../scripts/validate-coverage-budgets.cjs) · [`src/actions/main_run_lifecycle.ts`](../src/actions/main_run_lifecycle.ts) · [`src/actions/main_run_route.ts`](../src/actions/main_run_route.ts) · [`src/actions/github_action_completion.ts`](../src/actions/github_action_completion.ts) · [`src/api.ts`](../src/api.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/errors/application_error.ts`](../src/application/errors/application_error.ts) · [`src/application/errors/application_error_context.ts`](../src/application/errors/application_error_context.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/application/policies/application_error_presentation_policy.ts`](../src/application/policies/application_error_presentation_policy.ts) · [`src/application/policies/bugbot_result_finding_state_projection_policy.ts`](../src/application/policies/bugbot_result_finding_state_projection_policy.ts) · [`src/application/policies/bugbot_telemetry_projection_policy.ts`](../src/application/policies/bugbot_telemetry_projection_policy.ts) · [`src/application/policies/action_summary_policy.ts`](../src/application/policies/action_summary_policy.ts) · [`src/application/policies/copilot_evidence_policy.ts`](../src/application/policies/copilot_evidence_policy.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/application/policies/status_command_policy.ts`](../src/application/policies/status_command_policy.ts) · [`src/application/policies/github_execution_admission_policy.ts`](../src/application/policies/github_execution_admission_policy.ts) · [`src/application/policies/workflow_queue_policy.ts`](../src/application/policies/workflow_queue_policy.ts) · [`src/application/usecases/actions/lifecycle_synchronization_context.ts`](../src/application/usecases/actions/lifecycle_synchronization_context.ts) · [`src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts`](../src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts) · [`src/application/usecases/steps/commit/bugbot/bugbot_review_operation_context.ts`](../src/application/usecases/steps/commit/bugbot/bugbot_review_operation_context.ts) · [`src/application/policies/semantic_result_publication_policy.ts`](../src/application/policies/semantic_result_publication_policy.ts) · [`src/application/usecases/steps/common/status_card_publication_workflow.ts`](../src/application/usecases/steps/common/status_card_publication_workflow.ts) · [`src/application/usecases/comment_automation_context.ts`](../src/application/usecases/comment_automation_context.ts) · [`src/application/usecases/issue_workflow_context.ts`](../src/application/usecases/issue_workflow_context.ts) · [`src/application/usecases/pull_request_workflow_context.ts`](../src/application/usecases/pull_request_workflow_context.ts) · [`src/application/usecases/push_single_action_contexts.ts`](../src/application/usecases/push_single_action_contexts.ts) · [`src/application/usecases/steps/common/check_permissions_workflow.ts`](../src/application/usecases/steps/common/check_permissions_workflow.ts) · [`src/application/usecases/steps/common/comment_language_translation_workflow.ts`](../src/application/usecases/steps/common/comment_language_translation_workflow.ts) · [`src/application/usecases/steps/common/think_workflow.ts`](../src/application/usecases/steps/common/think_workflow.ts) · [`src/application/usecases/steps/common/update_title_workflow.ts`](../src/application/usecases/steps/common/update_title_workflow.ts) · [`src/application/usecases/steps/common/project_content_link_workflow.ts`](../src/application/usecases/steps/common/project_content_link_workflow.ts) · [`src/application/usecases/steps/common/publish_resume_workflow.ts`](../src/application/usecases/steps/common/publish_resume_workflow.ts) · [`src/application/usecases/steps/common/store_configuration_use_case.ts`](../src/application/usecases/steps/common/store_configuration_use_case.ts) · [`src/infrastructure/composition/shared_capability_port_binding.ts`](../src/infrastructure/composition/shared_capability_port_binding.ts) · [`src/infrastructure/composition/lifecycle_capability_port_binding.ts`](../src/infrastructure/composition/lifecycle_capability_port_binding.ts) · [`src/infrastructure/composition/lifecycle_state_composition_root.ts`](../src/infrastructure/composition/lifecycle_state_composition_root.ts) · [`src/infrastructure/composition/push_single_action_capability_port_binding.ts`](../src/infrastructure/composition/push_single_action_capability_port_binding.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`src/application/policies/configuration_persistence_policy.ts`](../src/application/policies/configuration_persistence_policy.ts) · [`src/application/ports/project_board_query_ports.ts`](../src/application/ports/project_board_query_ports.ts) · [`src/application/usecases/steps/commit/check_changes_issue_size_use_case.ts`](../src/application/usecases/steps/commit/check_changes_issue_size_use_case.ts) · [`src/application/usecases/steps/commit/check_changes_issue_size_workflow.ts`](../src/application/usecases/steps/commit/check_changes_issue_size_workflow.ts) · [`src/application/usecases/steps/commit/update_change_size_labels.ts`](../src/application/usecases/steps/commit/update_change_size_labels.ts) · [`src/data/repository/project/project_board_item_query.ts`](../src/data/repository/project/project_board_item_query.ts) · [`src/infrastructure/composition/change_size_project_membership_composition_root.ts`](../src/infrastructure/composition/change_size_project_membership_composition_root.ts) · [`src/manager/description/configuration_handler.ts`](../src/manager/description/configuration_handler.ts) · [`src/manager/description/configuration_markers.ts`](../src/manager/description/configuration_markers.ts) · [`src/data/repository/issue/issue_content_repository.ts`](../src/data/repository/issue/issue_content_repository.ts) · [`src/data/repository/github/github_error_policy.ts`](../src/data/repository/github/github_error_policy.ts)
-- Tests: [`src/tooling/__tests__/coverage_budget_validator.test.ts`](../src/tooling/__tests__/coverage_budget_validator.test.ts) · [`src/__tests__/api.test.ts`](../src/__tests__/api.test.ts) · [`src/actions/__tests__/github_action.test.ts`](../src/actions/__tests__/github_action.test.ts) · [`src/actions/__tests__/github_action_completion.test.ts`](../src/actions/__tests__/github_action_completion.test.ts) · [`src/application/errors/__tests__/application_error.test.ts`](../src/application/errors/__tests__/application_error.test.ts) · [`src/application/policies/__tests__/application_error_message_catalog.test.ts`](../src/application/policies/__tests__/application_error_message_catalog.test.ts) · [`src/application/policies/__tests__/application_error_presentation_policy.test.ts`](../src/application/policies/__tests__/application_error_presentation_policy.test.ts) · [`src/application/policies/__tests__/bugbot_result_finding_state_projection_policy.test.ts`](../src/application/policies/__tests__/bugbot_result_finding_state_projection_policy.test.ts) · [`src/application/policies/__tests__/bugbot_telemetry_projection_policy.test.ts`](../src/application/policies/__tests__/bugbot_telemetry_projection_policy.test.ts) · [`src/application/policies/__tests__/action_summary_policy.test.ts`](../src/application/policies/__tests__/action_summary_policy.test.ts) · [`src/application/policies/__tests__/copilot_evidence_policy.test.ts`](../src/application/policies/__tests__/copilot_evidence_policy.test.ts) · [`src/application/policies/__tests__/lifecycle_state_policy.test.ts`](../src/application/policies/__tests__/lifecycle_state_policy.test.ts) · [`src/application/policies/__tests__/status_command_policy.test.ts`](../src/application/policies/__tests__/status_command_policy.test.ts) · [`src/application/usecases/actions/__tests__/lifecycle_synchronization_context.test.ts`](../src/application/usecases/actions/__tests__/lifecycle_synchronization_context.test.ts) · [`src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts`](../src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts) · [`src/application/usecases/actions/__tests__/lifecycle_event_replay.integration.test.ts`](../src/application/usecases/actions/__tests__/lifecycle_event_replay.integration.test.ts) · [`src/architecture/__tests__/execution_import_ratchet.test.ts`](../src/architecture/__tests__/execution_import_ratchet.test.ts) · [`src/architecture/__tests__/raw_error_logging_boundaries.test.ts`](../src/architecture/__tests__/raw_error_logging_boundaries.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/bugbot_review_operation_context.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/bugbot_review_operation_context.test.ts) · [`src/application/usecases/steps/common/__tests__/shared_capability_context_projection.test.ts`](../src/application/usecases/steps/common/__tests__/shared_capability_context_projection.test.ts) · [`src/application/usecases/steps/common/__tests__/publish_resume_use_case.test.ts`](../src/application/usecases/steps/common/__tests__/publish_resume_use_case.test.ts) · [`src/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts) · [`src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts`](../src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts) · [`src/infrastructure/composition/__tests__/lifecycle_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/lifecycle_capability_port_binding.test.ts) · [`src/infrastructure/composition/__tests__/lifecycle_state_composition_root.test.ts`](../src/infrastructure/composition/__tests__/lifecycle_state_composition_root.test.ts) · [`src/application/usecases/__tests__/push_single_action_contexts.test.ts`](../src/application/usecases/__tests__/push_single_action_contexts.test.ts) · [`src/application/usecases/__tests__/single_action_workflow.test.ts`](../src/application/usecases/__tests__/single_action_workflow.test.ts) · [`src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts) · [`src/application/usecases/execution/__tests__/resolve_github_execution_admission_use_case.test.ts`](../src/application/usecases/execution/__tests__/resolve_github_execution_admission_use_case.test.ts) · [`src/application/usecases/workflow/__tests__/wait_for_previous_workflow_runs_use_case.test.ts`](../src/application/usecases/workflow/__tests__/wait_for_previous_workflow_runs_use_case.test.ts) · [`src/application/policies/__tests__/configuration_persistence_policy.test.ts`](../src/application/policies/__tests__/configuration_persistence_policy.test.ts) · [`src/application/usecases/steps/commit/__tests__/check_changes_issue_size_use_case.test.ts`](../src/application/usecases/steps/commit/__tests__/check_changes_issue_size_use_case.test.ts) · [`src/application/usecases/steps/commit/__tests__/update_change_size_labels.test.ts`](../src/application/usecases/steps/commit/__tests__/update_change_size_labels.test.ts) · [`src/data/repository/project/__tests__/project_board_visibility_recovery.test.ts`](../src/data/repository/project/__tests__/project_board_visibility_recovery.test.ts) · [`src/infrastructure/composition/__tests__/change_size_project_membership_composition_root.test.ts`](../src/infrastructure/composition/__tests__/change_size_project_membership_composition_root.test.ts) · [`src/manager/description/__tests__/configuration_recovery.test.ts`](../src/manager/description/__tests__/configuration_recovery.test.ts) · [`src/manager/description/__tests__/configuration_handler.test.ts`](../src/manager/description/__tests__/configuration_handler.test.ts) · [`src/data/repository/issue/__tests__/issue_content_repository.test.ts`](../src/data/repository/issue/__tests__/issue_content_repository.test.ts)
-- User documentation: [`docs/overview.mdx`](../docs/overview.mdx) · [`docs/bugbot/programmatic-api.mdx`](../docs/bugbot/programmatic-api.mdx) · [`docs/security-operations/operations/error-reference.mdx`](../docs/security-operations/operations/error-reference.mdx) · [`docs/security-operations/operations/troubleshooting.mdx`](../docs/security-operations/operations/troubleshooting.mdx) · [`docs/bugbot/quality-observability.mdx`](../docs/bugbot/quality-observability.mdx) · [`docs/bugbot/failure-scenarios.mdx`](../docs/bugbot/failure-scenarios.mdx) · [`docs/development/architecture.mdx`](../docs/development/architecture.mdx) · [`docs/issues/assignees-and-projects.mdx`](../docs/issues/assignees-and-projects.mdx) · [`docs/configuration.mdx`](../docs/configuration.mdx)
+- Core code: [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`scripts/validate-coverage-budgets.cjs`](../scripts/validate-coverage-budgets.cjs) · [`src/actions/main_run_lifecycle.ts`](../src/actions/main_run_lifecycle.ts) · [`src/actions/main_run_route.ts`](../src/actions/main_run_route.ts) · [`src/actions/github_action_completion.ts`](../src/actions/github_action_completion.ts) · [`src/api.ts`](../src/api.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/errors/application_error.ts`](../src/application/errors/application_error.ts) · [`src/application/errors/application_error_context.ts`](../src/application/errors/application_error_context.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/application/policies/application_error_presentation_policy.ts`](../src/application/policies/application_error_presentation_policy.ts) · [`src/application/policies/bugbot_result_finding_state_projection_policy.ts`](../src/application/policies/bugbot_result_finding_state_projection_policy.ts) · [`src/application/policies/bugbot_telemetry_projection_policy.ts`](../src/application/policies/bugbot_telemetry_projection_policy.ts) · [`src/application/policies/action_summary_policy.ts`](../src/application/policies/action_summary_policy.ts) · [`src/application/policies/copilot_evidence_policy.ts`](../src/application/policies/copilot_evidence_policy.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/application/policies/status_command_policy.ts`](../src/application/policies/status_command_policy.ts) · [`src/application/policies/github_execution_admission_policy.ts`](../src/application/policies/github_execution_admission_policy.ts) · [`src/application/policies/workflow_queue_policy.ts`](../src/application/policies/workflow_queue_policy.ts) · [`src/application/usecases/actions/lifecycle_synchronization_context.ts`](../src/application/usecases/actions/lifecycle_synchronization_context.ts) · [`src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts`](../src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts) · [`src/application/usecases/steps/commit/bugbot/bugbot_review_operation_context.ts`](../src/application/usecases/steps/commit/bugbot/bugbot_review_operation_context.ts) · [`src/application/policies/semantic_result_publication_policy.ts`](../src/application/policies/semantic_result_publication_policy.ts) · [`src/application/usecases/steps/common/status_card_publication_workflow.ts`](../src/application/usecases/steps/common/status_card_publication_workflow.ts) · [`src/application/usecases/comment_automation_context.ts`](../src/application/usecases/comment_automation_context.ts) · [`src/application/usecases/issue_workflow_context.ts`](../src/application/usecases/issue_workflow_context.ts) · [`src/application/usecases/pull_request_workflow_context.ts`](../src/application/usecases/pull_request_workflow_context.ts) · [`src/application/usecases/push_single_action_contexts.ts`](../src/application/usecases/push_single_action_contexts.ts) · [`src/application/usecases/steps/common/check_permissions_workflow.ts`](../src/application/usecases/steps/common/check_permissions_workflow.ts) · [`src/application/usecases/steps/common/comment_language_translation_workflow.ts`](../src/application/usecases/steps/common/comment_language_translation_workflow.ts) · [`src/application/usecases/steps/common/think_workflow.ts`](../src/application/usecases/steps/common/think_workflow.ts) · [`src/application/usecases/steps/common/update_title_workflow.ts`](../src/application/usecases/steps/common/update_title_workflow.ts) · [`src/application/usecases/steps/common/project_content_link_workflow.ts`](../src/application/usecases/steps/common/project_content_link_workflow.ts) · [`src/application/usecases/steps/common/publish_resume_workflow.ts`](../src/application/usecases/steps/common/publish_resume_workflow.ts) · [`src/application/usecases/steps/common/store_configuration_use_case.ts`](../src/application/usecases/steps/common/store_configuration_use_case.ts) · [`src/infrastructure/composition/shared_capability_port_binding.ts`](../src/infrastructure/composition/shared_capability_port_binding.ts) · [`src/infrastructure/composition/lifecycle_capability_port_binding.ts`](../src/infrastructure/composition/lifecycle_capability_port_binding.ts) · [`src/infrastructure/composition/lifecycle_state_composition_root.ts`](../src/infrastructure/composition/lifecycle_state_composition_root.ts) · [`src/infrastructure/composition/push_single_action_capability_port_binding.ts`](../src/infrastructure/composition/push_single_action_capability_port_binding.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`src/application/policies/configuration_persistence_policy.ts`](../src/application/policies/configuration_persistence_policy.ts) · [`src/application/ports/project_board_query_ports.ts`](../src/application/ports/project_board_query_ports.ts) · [`src/application/usecases/steps/commit/check_changes_issue_size_use_case.ts`](../src/application/usecases/steps/commit/check_changes_issue_size_use_case.ts) · [`src/application/usecases/steps/commit/check_changes_issue_size_workflow.ts`](../src/application/usecases/steps/commit/check_changes_issue_size_workflow.ts) · [`src/application/usecases/steps/commit/update_change_size_labels.ts`](../src/application/usecases/steps/commit/update_change_size_labels.ts) · [`src/data/repository/project/project_board_item_query.ts`](../src/data/repository/project/project_board_item_query.ts) · [`src/infrastructure/composition/change_size_project_membership_composition_root.ts`](../src/infrastructure/composition/change_size_project_membership_composition_root.ts) · [`src/manager/description/configuration_handler.ts`](../src/manager/description/configuration_handler.ts) · [`src/manager/description/configuration_markers.ts`](../src/manager/description/configuration_markers.ts) · [`src/data/repository/issue/issue_content_repository.ts`](../src/data/repository/issue/issue_content_repository.ts) · [`src/data/repository/github/github_error_policy.ts`](../src/data/repository/github/github_error_policy.ts) · [`src/application/policies/native_workflow_coordination_policy.ts`](../src/application/policies/native_workflow_coordination_policy.ts) · [`src/application/ports/workflow_run_ports.ts`](../src/application/ports/workflow_run_ports.ts) · [`src/application/usecases/workflow/wait_for_previous_workflow_runs_use_case.ts`](../src/application/usecases/workflow/wait_for_previous_workflow_runs_use_case.ts) · [`src/data/repository/workflow/active_previous_workflow_runs_repository.ts`](../src/data/repository/workflow/active_previous_workflow_runs_repository.ts) · [`src/data/repository/workflow/native_workflow_coordination_repository.ts`](../src/data/repository/workflow/native_workflow_coordination_repository.ts) · [`src/infrastructure/composition/workflow_queue_composition_root.ts`](../src/infrastructure/composition/workflow_queue_composition_root.ts)
+- Tests: [`src/tooling/__tests__/coverage_budget_validator.test.ts`](../src/tooling/__tests__/coverage_budget_validator.test.ts) · [`src/__tests__/api.test.ts`](../src/__tests__/api.test.ts) · [`src/actions/__tests__/github_action.test.ts`](../src/actions/__tests__/github_action.test.ts) · [`src/actions/__tests__/github_action_completion.test.ts`](../src/actions/__tests__/github_action_completion.test.ts) · [`src/application/errors/__tests__/application_error.test.ts`](../src/application/errors/__tests__/application_error.test.ts) · [`src/application/policies/__tests__/application_error_message_catalog.test.ts`](../src/application/policies/__tests__/application_error_message_catalog.test.ts) · [`src/application/policies/__tests__/application_error_presentation_policy.test.ts`](../src/application/policies/__tests__/application_error_presentation_policy.test.ts) · [`src/application/policies/__tests__/bugbot_result_finding_state_projection_policy.test.ts`](../src/application/policies/__tests__/bugbot_result_finding_state_projection_policy.test.ts) · [`src/application/policies/__tests__/bugbot_telemetry_projection_policy.test.ts`](../src/application/policies/__tests__/bugbot_telemetry_projection_policy.test.ts) · [`src/application/policies/__tests__/action_summary_policy.test.ts`](../src/application/policies/__tests__/action_summary_policy.test.ts) · [`src/application/policies/__tests__/copilot_evidence_policy.test.ts`](../src/application/policies/__tests__/copilot_evidence_policy.test.ts) · [`src/application/policies/__tests__/lifecycle_state_policy.test.ts`](../src/application/policies/__tests__/lifecycle_state_policy.test.ts) · [`src/application/policies/__tests__/status_command_policy.test.ts`](../src/application/policies/__tests__/status_command_policy.test.ts) · [`src/application/usecases/actions/__tests__/lifecycle_synchronization_context.test.ts`](../src/application/usecases/actions/__tests__/lifecycle_synchronization_context.test.ts) · [`src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts`](../src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts) · [`src/application/usecases/actions/__tests__/lifecycle_event_replay.integration.test.ts`](../src/application/usecases/actions/__tests__/lifecycle_event_replay.integration.test.ts) · [`src/architecture/__tests__/execution_import_ratchet.test.ts`](../src/architecture/__tests__/execution_import_ratchet.test.ts) · [`src/architecture/__tests__/raw_error_logging_boundaries.test.ts`](../src/architecture/__tests__/raw_error_logging_boundaries.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/bugbot_review_operation_context.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/bugbot_review_operation_context.test.ts) · [`src/application/usecases/steps/common/__tests__/shared_capability_context_projection.test.ts`](../src/application/usecases/steps/common/__tests__/shared_capability_context_projection.test.ts) · [`src/application/usecases/steps/common/__tests__/publish_resume_use_case.test.ts`](../src/application/usecases/steps/common/__tests__/publish_resume_use_case.test.ts) · [`src/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts) · [`src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts`](../src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts) · [`src/infrastructure/composition/__tests__/lifecycle_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/lifecycle_capability_port_binding.test.ts) · [`src/infrastructure/composition/__tests__/lifecycle_state_composition_root.test.ts`](../src/infrastructure/composition/__tests__/lifecycle_state_composition_root.test.ts) · [`src/application/usecases/__tests__/push_single_action_contexts.test.ts`](../src/application/usecases/__tests__/push_single_action_contexts.test.ts) · [`src/application/usecases/__tests__/single_action_workflow.test.ts`](../src/application/usecases/__tests__/single_action_workflow.test.ts) · [`src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts) · [`src/application/usecases/execution/__tests__/resolve_github_execution_admission_use_case.test.ts`](../src/application/usecases/execution/__tests__/resolve_github_execution_admission_use_case.test.ts) · [`src/application/usecases/workflow/__tests__/wait_for_previous_workflow_runs_use_case.test.ts`](../src/application/usecases/workflow/__tests__/wait_for_previous_workflow_runs_use_case.test.ts) · [`src/application/policies/__tests__/configuration_persistence_policy.test.ts`](../src/application/policies/__tests__/configuration_persistence_policy.test.ts) · [`src/application/usecases/steps/commit/__tests__/check_changes_issue_size_use_case.test.ts`](../src/application/usecases/steps/commit/__tests__/check_changes_issue_size_use_case.test.ts) · [`src/application/usecases/steps/commit/__tests__/update_change_size_labels.test.ts`](../src/application/usecases/steps/commit/__tests__/update_change_size_labels.test.ts) · [`src/data/repository/project/__tests__/project_board_visibility_recovery.test.ts`](../src/data/repository/project/__tests__/project_board_visibility_recovery.test.ts) · [`src/infrastructure/composition/__tests__/change_size_project_membership_composition_root.test.ts`](../src/infrastructure/composition/__tests__/change_size_project_membership_composition_root.test.ts) · [`src/manager/description/__tests__/configuration_recovery.test.ts`](../src/manager/description/__tests__/configuration_recovery.test.ts) · [`src/manager/description/__tests__/configuration_handler.test.ts`](../src/manager/description/__tests__/configuration_handler.test.ts) · [`src/data/repository/issue/__tests__/issue_content_repository.test.ts`](../src/data/repository/issue/__tests__/issue_content_repository.test.ts) · [`src/application/policies/__tests__/native_workflow_coordination_policy.test.ts`](../src/application/policies/__tests__/native_workflow_coordination_policy.test.ts) · [`src/data/repository/workflow/__tests__/native_workflow_coordination_repository.test.ts`](../src/data/repository/workflow/__tests__/native_workflow_coordination_repository.test.ts) · [`src/data/repository/workflow/__tests__/active_previous_workflow_runs_repository.test.ts`](../src/data/repository/workflow/__tests__/active_previous_workflow_runs_repository.test.ts) · [`src/actions/__tests__/common_action.test.ts`](../src/actions/__tests__/common_action.test.ts) · [`src/tooling/__tests__/validate_workflow_contract.test.ts`](../src/tooling/__tests__/validate_workflow_contract.test.ts)
+- User documentation: [`docs/overview.mdx`](../docs/overview.mdx) · [`docs/bugbot/programmatic-api.mdx`](../docs/bugbot/programmatic-api.mdx) · [`docs/security-operations/operations/error-reference.mdx`](../docs/security-operations/operations/error-reference.mdx) · [`docs/security-operations/operations/troubleshooting.mdx`](../docs/security-operations/operations/troubleshooting.mdx) · [`docs/bugbot/quality-observability.mdx`](../docs/bugbot/quality-observability.mdx) · [`docs/bugbot/failure-scenarios.mdx`](../docs/bugbot/failure-scenarios.mdx) · [`docs/development/architecture.mdx`](../docs/development/architecture.mdx) · [`docs/issues/assignees-and-projects.mdx`](../docs/issues/assignees-and-projects.mdx) · [`docs/configuration.mdx`](../docs/configuration.mdx) · [`README.md`](../README.md) · [`docs/features.mdx`](../docs/features.mdx) · [`docs/pull-requests/workflow-setup.mdx`](../docs/pull-requests/workflow-setup.mdx)
### `architecture-quality-hardening` — Architecture quality and scalability hardening
diff --git a/specs/catalog.json b/specs/catalog.json
index 57b8cf4ab..9c42e0fd1 100644
--- a/specs/catalog.json
+++ b/specs/catalog.json
@@ -533,9 +533,9 @@
"id": "execution-lifecycle",
"title": "Execution admission, queueing, routing, and result publication",
"status": "implemented",
- "scope": "Shared GitHub Action lifecycle from event admission through durable user-facing results",
+ "scope": "Shared GitHub Action lifecycle with pinned native resource/lane coordination proof, conservative predecessor fencing, routing and durable user-facing results",
"owner": "Copilot maintainers",
- "lastVerified": "2026-10-21",
+ "lastVerified": "2026-10-25",
"specs": [
"specs/execution-admission-queue-and-publication.md",
"specs/execution-error-and-context-hardening.md",
@@ -604,7 +604,13 @@
"src/manager/description/configuration_handler.ts",
"src/manager/description/configuration_markers.ts",
"src/data/repository/issue/issue_content_repository.ts",
- "src/data/repository/github/github_error_policy.ts"
+ "src/data/repository/github/github_error_policy.ts",
+ "src/application/policies/native_workflow_coordination_policy.ts",
+ "src/application/ports/workflow_run_ports.ts",
+ "src/application/usecases/workflow/wait_for_previous_workflow_runs_use_case.ts",
+ "src/data/repository/workflow/active_previous_workflow_runs_repository.ts",
+ "src/data/repository/workflow/native_workflow_coordination_repository.ts",
+ "src/infrastructure/composition/workflow_queue_composition_root.ts"
],
"tests": [
"src/tooling/__tests__/coverage_budget_validator.test.ts",
@@ -644,7 +650,12 @@
"src/infrastructure/composition/__tests__/change_size_project_membership_composition_root.test.ts",
"src/manager/description/__tests__/configuration_recovery.test.ts",
"src/manager/description/__tests__/configuration_handler.test.ts",
- "src/data/repository/issue/__tests__/issue_content_repository.test.ts"
+ "src/data/repository/issue/__tests__/issue_content_repository.test.ts",
+ "src/application/policies/__tests__/native_workflow_coordination_policy.test.ts",
+ "src/data/repository/workflow/__tests__/native_workflow_coordination_repository.test.ts",
+ "src/data/repository/workflow/__tests__/active_previous_workflow_runs_repository.test.ts",
+ "src/actions/__tests__/common_action.test.ts",
+ "src/tooling/__tests__/validate_workflow_contract.test.ts"
],
"documentation": [
"docs/overview.mdx",
@@ -655,7 +666,10 @@
"docs/bugbot/failure-scenarios.mdx",
"docs/development/architecture.mdx",
"docs/issues/assignees-and-projects.mdx",
- "docs/configuration.mdx"
+ "docs/configuration.mdx",
+ "README.md",
+ "docs/features.mdx",
+ "docs/pull-requests/workflow-setup.mdx"
]
},
{
diff --git a/specs/execution-admission-queue-and-publication.md b/specs/execution-admission-queue-and-publication.md
index 89c5b7070..bcfc1f154 100644
--- a/specs/execution-admission-queue-and-publication.md
+++ b/specs/execution-admission-queue-and-publication.md
@@ -1,21 +1,23 @@
# Execution Admission, Queueing, Routing, and Result Publication
-- Status: As-built baseline with implemented review-evidence ownership hardening
+- Status: As-built baseline with review-evidence and native coordination hardening
- Date: 2026-09-15
-- Last verified: 2026-09-15 on `develop` plus PR workflow UX implementation branch
+- Last verified: 2026-10-25; issue #692 coordination regression verification
- Owners: Copilot maintainers
- Scope: the shared GitHub Action lifecycle from an incoming event to visible results and persisted execution state
- Related issues/PRs: architecture quality and scalability hardening SDD;
[PR #363](https://github.com/vypdev/copilot/pull/363);
- [PR #378](https://github.com/vypdev/copilot/pull/378); historic motivation
+ [PR #378](https://github.com/vypdev/copilot/pull/378);
+ [issue #692](https://github.com/vypdev/copilot/issues/692); historic motivation
before that live evidence is not recoverable from repository evidence
- Required review gates: product UX, architecture, testing, documentation, security/operations
- Open decisions blocking readiness: none for the baseline; see known debt and limitations
## 1. Executive summary
-Copilot admits an event before expensive setup, serializes mutation-capable runs
-per workflow, resolves one application route, and publishes a bounded result to
+Copilot admits an event before expensive setup, verifies native resource/lane
+coordination or serializes mutation-capable runs per workflow, resolves one
+application route, and publishes a bounded result to
the relevant GitHub conversation and Job Summary. A run created by the account
behind the workflow PAT is ignored unless it is an explicit valid single action.
The native `Copilot / Review` name is reserved for results carrying exactly one
@@ -49,8 +51,10 @@ create inconsistent authorization and failure behavior.
2. Bot-authored normal events are discarded; valid explicit single actions pass.
3. Only agent roles reachable by the event are prepared, subject to
`ai-members-only` authorization.
-4. `mainRun` waits for older active runs of the same workflow, except the
- dedicated failure-comment action.
+4. Before setup, `mainRun` verifies the executing job's native resource/lane
+ coordination as specified in §6.4. Without that proof it waits for older
+ active runs of the same workflow. The dedicated failure-comment action stays
+ runnable when queue verification fails.
5. Setup restores issue, branch, and durable operation state; the route policy
selects exactly one of issue, issue-comment, PR, review-comment, push, or
single-action.
@@ -69,7 +73,8 @@ create inconsistent authorization and failure behavior.
- Observed behavior: `src/actions/github_action.ts`, `common_action.ts`,
`main_run_lifecycle.ts`, `main_run_route.ts`, and
`github_action_completion.ts`.
-- Intentional contract: early bot-loop admission, workflow-scoped serialization,
+- Intentional contract: early bot-loop admission, verified native resource/lane
+ coordination with workflow-scoped serialization as the conservative fallback,
single-route ownership, sanitized publication, and fail-closed errors.
- Known debt and limitations: queue state is polled and bounded by a 90-minute
internal budget; optional Check Run publication is best-effort; 26 production
@@ -83,8 +88,10 @@ create inconsistent authorization and failure behavior.
[`execution-error-and-context-hardening.md`](./execution-error-and-context-hardening.md),
under the sequencing and cross-cutting gates in
[`architecture-quality-and-scalability-hardening.md`](./architecture-quality-and-scalability-hardening.md).
- Changes to queue ownership or durable event processing beyond that proposal
- require a separate design; they are not part of this baseline.
+ Issue #692 corrects a verified scheduler inversion: a later PR job occupied a
+ shared runner while waiting for an older queued job for a different PR. That
+ predecessor needed a runner from the same finite pool. The solution below
+ recognizes the existing native lanes rather than increasing the wait budget.
## 3. Actors, surfaces, and terminology
@@ -104,7 +111,8 @@ presentation adapters decide where that fact appears.
### 4.1 Goals
1. Every accepted event MUST resolve to at most one top-level route.
-2. Mutation-capable workflow runs MUST wait for older active runs or fail.
+2. Mutation-capable workflow runs MUST have verified native resource/lane
+ coordination, wait for older active workflow runs, or fail before setup.
3. Results MUST remain understandable without raw logs.
4. A named native Check MUST represent only its semantic capability; a metadata
result MUST NOT supersede a same-head review in GitHub's latest-by-name view.
@@ -133,7 +141,7 @@ presentation adapters decide where that fact appears.
| Stage | Pre-baseline understanding | As-built contract | Effect |
|---|---|---|---|
| Admission | implicit in entrypoint | explicit identity decision | bot loops stop early |
-| Queue | implementation detail | fail-closed workflow gate | mutations do not overlap |
+| Queue | every run polls the entire workflow | verify native lane before setup; otherwise fail-closed workflow gate | unrelated PRs do not hold runners waiting for each other |
| Routing | spread across handlers | one route policy | predictable ownership |
| Completion | comments only were assumed | route-owned comment when applicable + summary + optional check + state | evidence is inspectable without duplicate discussion noise |
| Review evidence | every PR completion could reuse the Review name | exact-head telemetry owns Review; metadata keeps workflow/summary only | latest-by-name remains truthful |
@@ -176,6 +184,60 @@ neutral.
Duplicate events are tolerated through route-specific idempotency and persisted
markers; this shared layer does not itself deduplicate GitHub deliveries.
+### 6.4 Native coordination recognition — issue #692
+
+The recommended installed Commit, PR analysis and PR review-state jobs already
+declare native job concurrency. The runtime MUST recognize that exact contract
+before setup, without a user-controlled bypass input and without enumerating
+older runs when native coordination is proven.
+
+```text
+Validate run/workflow identity
+ -> eligible normal event + pinned workflow/job metadata?
+ -> read executing workflow at GITHUB_WORKFLOW_SHA
+ -> exact job concurrency proven? -> setup -> existing guarded route
+ -> custom/legacy shape? -> existing predecessor queue -> setup
+ -> no eligible metadata? -> existing predecessor queue -> setup
+ -> provider/parse error or deadline -> fail before setup
+```
+
+Text equivalent: native proof delegates coordination to GitHub; missing proof
+retains the predecessor fence; errors never grant permission to execute.
+
+| Normal event/resource | Required literal job group | Native cancellation |
+|---|---|---|
+| Branch `push` | `copilot-push-${{ github.repository }}-${{ github.ref_name }}` | `true` |
+| `pull_request` with positive safe-integer PR number | `copilot-pr-${{ github.repository }}-${{ github.event.pull_request.number }}-analysis` | `true` |
+| `pull_request_review` with positive safe-integer PR number | `copilot-pr-${{ github.repository }}-${{ github.event.pull_request.number }}-review-state` | `true` |
+
+The entrypoint MUST use GitHub's workflow ref, workflow commit SHA and current
+job ID, bound to the current repository, rather than a checked-out file, workflow
+display name or filename allowlist. Single actions, missing/invalid metadata,
+tag pushes, unrelated events, reusable or multi-job workflows, workflow-level concurrency and
+nonmatching groups MUST retain the existing workflow queue. The source read is
+limited to a normal YAML workflow file at a full 40-hex commit SHA, at most
+256 KiB. Parsing uses the JSON schema and executes no expressions or tags.
+Recognition requires one direct job, preventing a reusable callee's job ID from
+being mistaken for another protected job in the caller's workflow context.
+Missing/malformed provider content, YAML errors, provider errors and exhausted
+retry/deadline budgets MUST fail closed through the existing sanitized queue
+failure. There is no disk fallback or read from a moving branch.
+
+GitHub's native key owns serialization/replacement within the same resource and
+lane; PR identity keeps two PRs sharing a head branch independent. No runtime
+comparison of `head_branch` substitutes for PR identity. Existing head/base,
+finding, permission, publication and deployment fences remain mandatory.
+Durable issue/comment mutations and legacy/custom jobs retain active-status
+pagination, same-workflow predecessor ordering, retries and the 90-minute bound.
+The recognition read and any fallback queue share that absolute deadline.
+
+No extra comments, checks or labels are introduced. A proven job logs
+`Native workflow resource/lane coordination verified; continuing before setup.`
+Legacy waiting and sanitized failures keep their existing messages. The run
+and Job Summary remain the operator navigation surfaces. Cancellation before
+setup leaves no application mutation; cancellation after setup retains the
+existing route recovery rules.
+
## 7. User-facing configuration
| Input | Type | Recommended default | Allowed values | Scope/persistence |
@@ -190,6 +252,11 @@ jitter, admission rules, route precedence, and error-to-exit mapping are not
configurable. Inputs use Action values; stored issue configuration restores
operation facts but never secrets.
+Native recognition adds no public configuration or persisted state. Recommended
+setup uses the three exact job groups in §6.4; a custom workflow without those
+groups is a supported conservative alternative. Missing or future metadata
+selects the existing queue, never a permissive mode.
+
## 8. Clean Architecture design
| Boundary | Owns | Must not own/import |
@@ -216,6 +283,15 @@ application use cases decide; provider adapters satisfy ports; renderers expose
sanitized results. Architecture and cycle tests MUST prevent inward layers from
importing concrete infrastructure.
+The entrypoint projects immutable workflow/job/event metadata before setup. The
+waiting use case asks a semantic native-coordination query port once, then either
+continues or uses its existing predecessor port. The GitHub adapter reads and
+parses the pinned workflow; a pure application policy recognizes the exact group
+and cancellation contract. Only infrastructure owns YAML/Octokit and environment
+access stays at the entrypoint. Architecture import tests, workflow parser
+contracts and the integrated scheduler-inversion regression enforce these
+boundaries. No provider DTO or `Execution` dependency enters the pure policy.
+
## 9. UI/UX and content contract
The first view MUST say status, completed facts, next step, action, impact, and
@@ -285,13 +361,22 @@ as `invalid`, never as zero findings.
## 13. Compatibility, migration, rollout, and rollback
-This document records the repository behavior at the audit commit. There are no
-installed users or real in-flight markers to preserve. The linked hardening work
-therefore makes a clean cut: removed API/marker/result shapes are rejected and
-no compatibility parser, migration, deprecation window, or dual reader ships.
-After first real use, already completed provider mutations are never hidden or
-rewound; incompatible future state fails closed and recovery starts from
-inspectable provider facts.
+For issue #692, consumers with the current native groups need only the fixed
+runtime; workflow changes are unnecessary. Older/custom workflows keep the
+queue. Roll out through a reviewed PR and the normal release process. A run
+already waiting in an older runtime cannot adopt the fix in place; inspect its
+predecessors and rerun after the fix is available. Changing concurrency keys
+during an active run may overlap an old key, so existing freshness/idempotency
+guards remain essential. Rollback restores the earlier runtime and its global
+wait behavior; it does not undo provider mutations. Live consumer cancellation,
+deployment and release are outside this issue's implementation authorization.
+
+The native coordination increment changes no installed input or durable marker
+schema. Existing production consumers retain their workflow groups and operation
+state. Already completed provider mutations are never hidden or rewound;
+incompatible state fails closed and recovery starts from inspectable provider
+facts. The historical evidence-schema cutover remains documented in the linked
+hardening specifications; it is not a migration introduced by issue #692.
## 14. Testing strategy and numeric budget
@@ -311,6 +396,20 @@ Tests use fake clocks/randomness/providers and no live waits. Golden Markdown
needs semantic assertions. Human evidence remains required for desktop/mobile,
light/dark, and screen-reader order.
+Issue #692 adds a minimum **24 distinct cases**, derived from the new proof
+boundary: **12 pure policy/context** cases (three lanes, same-owner stability,
+invalid resources/metadata, single actions and custom groups), **6 adapter**
+cases (pinned contents, malformed content/YAML, endpoint errors, safe fallback
+and bounds), **4 orchestration/integration** cases (scheduler inversion before
+setup, retained mutation fence, native failure before setup and deadline), and
+**2 shipped/setup workflow replays** proving different-PR independence and
+same-resource/lane stability. Existing predecessor pagination/retry/error and
+workflow mutant cases remain required. New pure policy targets at least 95%
+branch coverage; changed adapters/use cases target at least 90% lines. Fake
+providers, clocks and delays exercise the race without live runner sleeps.
+No new graphical UI exists, so human visual captures are not needed for this
+increment; native log copy is checked semantically.
+
## 15. Documentation and discoverability
| Audience | Artifact | Contract |
@@ -320,11 +419,16 @@ light/dark, and screen-reader order.
| Operator | troubleshooting | timeout, partial publication, retry |
| Contributor | architecture + this SDD | boundaries and route ownership |
+Issue #692 updates `docs/features.mdx`, `docs/pull-requests/workflow-setup.mdx`
+and the README with native proof, conservative custom fallback, pinned-content
+read permissions, shared-runner recovery and unchanged safety guards.
+
## 16. Acceptance scenarios
1. Given a PAT-authored normal event, the run exits before queue or mutation.
2. Given a valid explicit single action from that account, it executes.
-3. Given older active runs, the current run waits with bounded jitter.
+3. Given older active runs without native coordination proof, the current run
+ waits with bounded jitter.
4. Given unavailable workflow identity on GitHub Actions, queueing fails closed.
5. Given a known event, exactly one route owns it.
6. Given successful core work and failed optional check publication, the summary
@@ -350,6 +454,20 @@ light/dark, and screen-reader order.
telemetry, the telemetry-set and finding-state projections are invalid; no
Review Check is published and every other current-state consumer fails or
blocks instead of reporting success.
+16. Given an older queued PR A and a later executing PR B on a finite shared
+ pool, a proven native job reaches setup without enumerating predecessors
+ or waiting; the shipped and setup workflow fixtures reproduce this.
+17. Given successive events for the same PR/lane, GitHub's exact group stays
+ stable; different PRs and analysis/review-state lanes remain independent.
+18. Given a legacy/custom job, missing metadata, tag push or single action, the
+ original predecessor fence blocks setup until older active runs finish.
+19. Given malformed source, unreadable workflow content or an expired proof
+ deadline, setup/routes do not run and raw provider diagnostics are absent
+ from the public queue failure.
+20. Given a misleading filename, different checkout or moving branch, native
+ recognition uses only the executing repository/workflow SHA/job and rejects
+ nonmatching contracts. Policy and adapter tests enforce the inspection bound
+ and prohibit executable YAML tags.
## 17. Requirements traceability
@@ -357,6 +475,7 @@ light/dark, and screen-reader order.
|---|---|---|---|
| admission invariants | admission policy/use case | admission tests | overview |
| serialization | queue policy/use case/adapters | queue + workflow tests | troubleshooting |
+| §6.4 native proof and conservative fallback | native coordination policy/port/adapter + waiting use case | policy/context, pinned-content, scheduler-inversion and failure/deadline cases; shipped/setup replays | features and PR workflow setup |
| one route | route policy/dispatcher | route tests | architecture |
| safe publication | completion/presentation policies | completion tests | overview |
| metadata noise bound | explicit generic-comment publication mode | metadata-edit context/use-case/completion negatives | workflow setup and troubleshooting |
@@ -374,6 +493,10 @@ light/dark, and screen-reader order.
## 19. Definition of Done
+- [x] Issue #692: the additional 24-case budget, policy/adapter coverage,
+ scheduler inversion, same-resource contracts, sanitized failures,
+ documentation, typecheck/lint, workflow and specification validation pass.
+
- [x] Normative changes have acceptance and traceability.
- [x] Architecture, workflow, catalog, and cycle checks pass.
- [x] The 70-case risk budget and repository coverage gates pass; both shared
@@ -387,6 +510,13 @@ light/dark, and screen-reader order.
## 20. References and decisions
- Primary sources: catalogued code, workflows, tests, and documentation.
+- Issue #692 primary provider contracts: [native job concurrency](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency),
+ [workflow SHA and job metadata](https://docs.github.com/en/actions/reference/workflows-and-actions/variables),
+ and [repository content at a ref](https://docs.github.com/en/rest/repos/contents#get-repository-content).
+ Decision: prove the executing job's existing native contract, then delegate
+ to GitHub. Reject a filename-only allowlist, a caller bypass flag, checked-out
+ YAML, branch-name predecessor filtering and larger timeouts: none establishes
+ safe PR identity and native fencing before setup.
- Related specs: release orchestration, merge-queue readiness, Bugbot reconciliation.
- Planned hardening: `execution-error-and-context-hardening.md` owns the semantic
error boundary and `Execution` context replacement; the architecture hardening
diff --git a/src/actions/__tests__/common_action.test.ts b/src/actions/__tests__/common_action.test.ts
index 3693afd2e..3f000fa4f 100644
--- a/src/actions/__tests__/common_action.test.ts
+++ b/src/actions/__tests__/common_action.test.ts
@@ -20,6 +20,13 @@ import type { Execution } from '../../data/model/execution';
import type { SynchronizeAgentActivityUseCase } from '../../application/usecases/actions/synchronize_agent_activity_use_case';
import { Result } from '../../data/model/result';
import { logInfo } from '../../utils/logger';
+import { readFileSync } from 'node:fs';
+import { buildPreviousWorkflowRunsQuery } from '../main_run_lifecycle';
+import * as issueWorkflowRuntimePolicy from '../../domain/issue_workflow_runtime_policy';
+import { WaitForPreviousWorkflowRunsUseCase } from '../../application/usecases/workflow/wait_for_previous_workflow_runs_use_case';
+import { NativeWorkflowCoordinationRepository } from '../../data/repository/workflow/native_workflow_coordination_repository';
+import { ActivePreviousWorkflowRunsRepository } from '../../data/repository/workflow/active_previous_workflow_runs_repository';
+import type { PreviousWorkflowRunsQuery } from '../../application/ports/workflow_run_ports';
jest.mock('@actions/core', () => ({
setFailed: jest.fn(),
@@ -244,6 +251,8 @@ const originalRunId = process.env.GITHUB_RUN_ID;
const originalWorkflow = process.env.GITHUB_WORKFLOW;
const originalWorkflowRef = process.env.GITHUB_WORKFLOW_REF;
const originalGithubActions = process.env.GITHUB_ACTIONS;
+const nativeEnvironment = ['GITHUB_WORKFLOW_SHA', 'GITHUB_JOB', 'GITHUB_EVENT_NAME', 'GITHUB_REF'] as const;
+const originalNativeEnvironment = nativeEnvironment.map(name => [name, process.env[name]] as const);
function restoreEnvironmentVariable(name: string, value: string | undefined): void {
if (value === undefined) {
@@ -262,6 +271,7 @@ describe('mainRun', () => {
delete process.env.GITHUB_WORKFLOW;
delete process.env.GITHUB_WORKFLOW_REF;
delete process.env.GITHUB_ACTIONS;
+ for (const name of nativeEnvironment) delete process.env[name];
mockWaitForPreviousWorkflowRunsInvoke.mockResolvedValue(undefined);
mockSingleActionInvoke.mockResolvedValue([]);
mockIssueCommentInvoke.mockResolvedValue([]);
@@ -280,6 +290,7 @@ describe('mainRun', () => {
restoreEnvironmentVariable('GITHUB_WORKFLOW', originalWorkflow);
restoreEnvironmentVariable('GITHUB_WORKFLOW_REF', originalWorkflowRef);
restoreEnvironmentVariable('GITHUB_ACTIONS', originalGithubActions);
+ for (const [name, value] of originalNativeEnvironment) restoreEnvironmentVariable(name, value);
});
it('delegates setup to the composed use case and clears accumulated logs', async () => {
@@ -326,6 +337,139 @@ describe('mainRun', () => {
});
});
+ function nativeRunEnvironment() {
+ Object.assign(process.env, {
+ GITHUB_ACTIONS: 'true', GITHUB_RUN_ID: '200', GITHUB_EVENT_NAME: 'pull_request',
+ GITHUB_WORKFLOW_REF: 'org/repo/.github/workflows/copilot_pull_request.yml@refs/heads/main',
+ GITHUB_WORKFLOW_SHA: 'a'.repeat(40), GITHUB_JOB: 'copilot-pull-requests',
+ });
+ }
+
+ it.each(['.github/workflows', 'setup/workflows'])(
+ 'replays scheduler inversion before setup using the native contract in %s', async directory => {
+ nativeRunEnvironment();
+ const order: string[] = [];
+ const source = readFileSync(`${directory}/copilot_pull_request.yml`, 'utf8');
+ const getContent = jest.fn(async () => {
+ order.push('proof');
+ return { data: { type: 'file', encoding: 'base64', content: Buffer.from(source).toString('base64') } };
+ });
+ // An older PR A is queued and cannot acquire this runner while B holds it.
+ const iterator = jest.fn(async function* () {
+ yield { data: { workflow_runs: [{ id: 199, status: 'queued' }] } };
+ });
+ const delay = { wait: jest.fn() };
+ const clock = { nowMilliseconds: () => 0 };
+ const native = new NativeWorkflowCoordinationRepository({ rest: { repos: { getContent } } }, {
+ delayPort: delay, clock, random: { next: () => 0.5 },
+ });
+ const predecessors = new ActivePreviousWorkflowRunsRepository({
+ rest: { actions: { listWorkflowRuns: jest.fn() } }, paginate: { iterator },
+ });
+ mockWaitForPreviousWorkflowRunsInvoke.mockImplementation((query: PreviousWorkflowRunsQuery) =>
+ new WaitForPreviousWorkflowRunsUseCase(predecessors, delay, {
+ noActivePreviousRuns: jest.fn(), waitingForPreviousRuns: jest.fn(),
+ }, undefined, clock, undefined, native).invoke(query));
+ mockSetupExecutionInvoke.mockImplementation(async context => {
+ order.push('setup'); return unresolvedSetupResult(context);
+ });
+ await runMain(mockExecution({ isPullRequest: true, eventName: 'pull_request', inputs: { pull_request: { number: 22 } } }));
+ expect(order).toEqual(['proof', 'setup']);
+ expect(iterator).not.toHaveBeenCalled();
+ expect(delay.wait).not.toHaveBeenCalled();
+ expect(mockPullRequestInvoke).toHaveBeenCalledTimes(1);
+ expect(getContent).toHaveBeenCalledWith({ owner: 'org', repo: 'repo',
+ path: '.github/workflows/copilot_pull_request.yml', ref: 'a'.repeat(40) });
+ },
+ );
+
+ it('retains the mutation fence before setup for a custom job without native coordination', async () => {
+ nativeRunEnvironment();
+ const order: string[] = [];
+ let predecessorActive = true;
+ const iterator = jest.fn(async function* () {
+ yield { data: { workflow_runs: predecessorActive ? [{ id: 199, status: 'queued' }] : [] } };
+ });
+ const delay = { wait: jest.fn(async () => { order.push('wait'); predecessorActive = false; }) };
+ const clock = { nowMilliseconds: () => 0 };
+ const native = new NativeWorkflowCoordinationRepository({ rest: { repos: {
+ getContent: jest.fn().mockResolvedValue({ data: { type: 'file', encoding: 'base64',
+ content: Buffer.from('jobs: { copilot-pull-requests: { steps: [] } }').toString('base64') } }),
+ } } }, { delayPort: delay, clock, random: { next: () => 0.5 } });
+ const predecessors = new ActivePreviousWorkflowRunsRepository({
+ rest: { actions: { listWorkflowRuns: jest.fn() } }, paginate: { iterator },
+ }, undefined, undefined, clock);
+ mockWaitForPreviousWorkflowRunsInvoke.mockImplementation((query: PreviousWorkflowRunsQuery) =>
+ new WaitForPreviousWorkflowRunsUseCase(predecessors, delay, {
+ noActivePreviousRuns: jest.fn(), waitingForPreviousRuns: jest.fn(),
+ }, undefined, clock, { next: () => 0.5 }, native).invoke(query));
+ mockSetupExecutionInvoke.mockImplementation(async context => {
+ expect(predecessorActive).toBe(false); order.push('setup'); return unresolvedSetupResult(context);
+ });
+ await runMain(mockExecution({ inputs: { pull_request: { number: 21 } } }));
+ expect(order).toEqual(['wait', 'setup']);
+ });
+
+ it('fails before setup with sanitized diagnostics when native proof cannot be read', async () => {
+ nativeRunEnvironment();
+ const native = new NativeWorkflowCoordinationRepository({ rest: { repos: {
+ getContent: jest.fn().mockRejectedValue({ status: 403, message: 'secret-provider-body' }),
+ } } }, { delayPort: { wait: jest.fn() }, clock: { nowMilliseconds: () => 0 }, random: { next: () => 0.5 } });
+ mockWaitForPreviousWorkflowRunsInvoke.mockImplementation((query: PreviousWorkflowRunsQuery) =>
+ new WaitForPreviousWorkflowRunsUseCase({ countActivePreviousRuns: jest.fn() }, { wait: jest.fn() }, {
+ noActivePreviousRuns: jest.fn(), waitingForPreviousRuns: jest.fn(),
+ }, undefined, { nowMilliseconds: () => 0 }, undefined, native).invoke(query));
+ await expect(runMain(mockExecution({ inputs: { pull_request: { number: 21 } } })))
+ .rejects.toThrow('Workflow queue check failed; sequential execution was not bypassed.');
+ expect(mockSetupExecutionInvoke).not.toHaveBeenCalled();
+ expect(mockPullRequestInvoke).not.toHaveBeenCalled();
+ expect(logger.logError).not.toHaveBeenCalledWith(expect.stringContaining('secret-provider-body'));
+ });
+
+ it('projects only GitHub workflow metadata and omits native delegation for single actions', () => {
+ nativeRunEnvironment();
+ const repository = { owner: 'org', repo: 'repo' };
+ const query = buildPreviousWorkflowRunsQuery(repository, { isSingleAction: false, pullRequestNumber: 21 });
+ expect(query.nativeCoordination).toMatchObject({ workflowRevision: 'a'.repeat(40),
+ jobId: 'copilot-pull-requests', eventName: 'pull_request', pullRequestNumber: 21 });
+ expect(Object.isFrozen(query.nativeCoordination)).toBe(true);
+ expect(buildPreviousWorkflowRunsQuery(repository, { isSingleAction: true }).nativeCoordination).toBeUndefined();
+ });
+
+ it('refuses admission before setup when pinned metadata lacks the workflow identity', async () => {
+ nativeRunEnvironment();
+ delete process.env.GITHUB_WORKFLOW_REF;
+ delete process.env.GITHUB_EVENT_NAME;
+
+ const query = buildPreviousWorkflowRunsQuery({ owner: 'org', repo: 'repo' }, { isSingleAction: false });
+ expect(query.nativeCoordination).toMatchObject({ workflowRef: '', eventName: '' });
+ expect(query.workflowIdentifier).toBeUndefined();
+ await expect(runMain(mockExecution())).rejects.toThrow('GitHub workflow identifier is unavailable');
+ expect(createWaitForPreviousWorkflowRunsUseCase).not.toHaveBeenCalled();
+ expect(mockSetupExecutionInvoke).not.toHaveBeenCalled();
+ });
+
+ it('retains the predecessor fence when runner metadata has no event name', async () => {
+ nativeRunEnvironment();
+ delete process.env.GITHUB_EVENT_NAME;
+ const countActivePreviousRuns = jest.fn().mockResolvedValue(0);
+ const getContent = jest.fn();
+ const clock = { nowMilliseconds: () => 0 };
+ const native = new NativeWorkflowCoordinationRepository({ rest: { repos: { getContent } } }, {
+ clock, delayPort: { wait: jest.fn() }, random: { next: () => 0.5 },
+ });
+ mockWaitForPreviousWorkflowRunsInvoke.mockImplementation(query =>
+ new WaitForPreviousWorkflowRunsUseCase({ countActivePreviousRuns }, { wait: jest.fn() }, {
+ noActivePreviousRuns: jest.fn(), waitingForPreviousRuns: jest.fn(), providerRetry: jest.fn(),
+ }, undefined, clock, undefined, native).invoke(query));
+
+ await runMain(mockExecution({ isPullRequest: true, inputs: { pull_request: { number: 21 } } }));
+
+ expect(getContent).not.toHaveBeenCalled();
+ expect(countActivePreviousRuns).toHaveBeenCalledTimes(1);
+ expect(mockSetupExecutionInvoke).toHaveBeenCalledTimes(1);
+ });
+
it('does not queue the issue-comment single action so a failed queue gate can still report', async () => {
process.env.GITHUB_ACTIONS = 'true';
delete process.env.GITHUB_RUN_ID;
@@ -794,6 +938,71 @@ describe('mainRun', () => {
expect(execution.labels.currentIssueLabels).toEqual(['state:ready']);
});
+ it.each([
+ undefined,
+ { labels: ['state:ready'] },
+ { target: { kind: 'issue', number: 42 } },
+ ])('leaves label caches intact when activity returns no complete patch (%j)', async outcome => {
+ mockAgentActivityStart.mockResolvedValue(outcome);
+ mockAgentActivityFinish.mockResolvedValue(outcome);
+ const execution = mockExecution({
+ eventName: 'push', isPush: true, commit: { commits: [{ id: 'commit-1' }] },
+ ai: {
+ getAgentConfiguration: jest.fn(() => ({ model: 'model' })),
+ getPullRequestDescriptionMode: jest.fn(() => 'disabled'),
+ },
+ });
+
+ await runMainWithActivity(execution);
+
+ expect(mockAgentActivityStart).toHaveBeenCalledTimes(1);
+ expect(mockAgentActivityFinish).toHaveBeenCalledTimes(1);
+ expect(mockCommitInvoke).toHaveBeenCalledWith(execution);
+ expect(execution.labels.currentIssueLabels).toEqual([]);
+ expect(execution.labels.currentPullRequestLabels).toEqual([]);
+ });
+
+ it.each([
+ ['in-progress', false],
+ ['deploy', false],
+ ['unrelated', true],
+ ])('blocks explicit disabled-work intent for %s while leaving unrelated callbacks idle', async (labelAdded, success) => {
+ const execution = mockExecution({
+ eventName: 'issues', isIssue: true,
+ issue: { labeled: true, labelAdded },
+ });
+ execution.labels.deploy = 'deploy';
+ mockSetupExecutionInvoke.mockImplementation(async context => configuredSetupResult(
+ context, { status: 'disabled', kind: 'feature' },
+ ));
+
+ const results = await runMain(execution);
+
+ expect(results).toHaveLength(1);
+ expect(results[0]).toMatchObject({ id: 'IssueWorkflowAdmission', success, executed: false });
+ expect(mockIssueInvoke).not.toHaveBeenCalled();
+ });
+
+ it('renders a bounded fallback when an admission decision has no optional message', async () => {
+ const decision = jest.spyOn(issueWorkflowRuntimePolicy, 'decideIssueWorkflowRuntime')
+ .mockReturnValueOnce({ mode: 'noop' });
+ try {
+ const results = await runMain(mockExecution());
+ expect(results[0].steps).toEqual(['⏭️ Issue workflow admission stopped this run.']);
+ expect(mockIssueInvoke).not.toHaveBeenCalled();
+ } finally {
+ decision.mockRestore();
+ }
+ });
+
+ it('summarizes route results that omit optional step details', async () => {
+ const results = [{ id: 'push', success: true, executed: true }] as Result[];
+ mockCommitInvoke.mockResolvedValue(results);
+
+ expect(await runMain(mockExecution({ isPush: true }))).toEqual(results);
+ expect(logInfo).toHaveBeenCalledWith('Main run finished. Results: 1, total steps: 0.');
+ });
+
it.each(['help', 'status'])('dispatches deterministic %s without an agent activity mutation', async command => {
const execution = mockExecution({
eventName: 'issue_comment', isIssue: true,
diff --git a/src/actions/common_action.ts b/src/actions/common_action.ts
index 9f36dda99..11a514405 100644
--- a/src/actions/common_action.ts
+++ b/src/actions/common_action.ts
@@ -59,10 +59,12 @@ export async function mainRun(
logDebugInfo(`Event: ${execution.eventName}, actor: ${execution.actor}, repo: ${repository.owner}/${repository.repo}, debug: ${execution.debug}`);
if (process.env.GITHUB_ACTIONS === 'true' && !execution.singleAction.isPublishIssueCommentAction) {
- // Every GitHub workflow invocation queues before setup or route work so
- // executions of the same workflow file cannot overlap mutations. A
- // failure notification must remain runnable when that queue gate fails.
- await waitForPreviousWorkflowRuns(execution.tokens.token, repository);
+ // Verify native resource/lane coordination or retain the predecessor
+ // fence before setup. Failure notifications must remain runnable.
+ await waitForPreviousWorkflowRuns(execution.tokens.token, repository, {
+ isSingleAction: execution.isSingleAction,
+ pullRequestNumber: execution.inputs?.pull_request?.number,
+ });
}
const setupExecution = createSetupExecutionUseCase(latestTagQueryPort, {
diff --git a/src/actions/main_run_lifecycle.ts b/src/actions/main_run_lifecycle.ts
index 1fb40018e..881aa1771 100644
--- a/src/actions/main_run_lifecycle.ts
+++ b/src/actions/main_run_lifecycle.ts
@@ -28,13 +28,24 @@ export class WorkflowQueueFailureError extends Error {
export function buildPreviousWorkflowRunsQuery(
repository: RepositoryCoordinates,
+ event?: { readonly isSingleAction: boolean; readonly pullRequestNumber?: number },
): PreviousWorkflowRunsQuery {
const workflowIdentifier = resolveWorkflowIdentifier(process.env.GITHUB_WORKFLOW_REF);
const query: PreviousWorkflowRunsQuery = {
owner: repository.owner,
repository: repository.repo,
- currentRunId: Number.parseInt(process.env.GITHUB_RUN_ID ?? '', 10),
+ currentRunId: Number(process.env.GITHUB_RUN_ID ?? Number.NaN),
...(workflowIdentifier ? { workflowIdentifier } : {}),
+ ...(event && !event.isSingleAction && process.env.GITHUB_WORKFLOW_SHA && process.env.GITHUB_JOB ? {
+ nativeCoordination: Object.freeze({
+ workflowRef: process.env.GITHUB_WORKFLOW_REF ?? '',
+ workflowRevision: process.env.GITHUB_WORKFLOW_SHA,
+ jobId: process.env.GITHUB_JOB,
+ eventName: process.env.GITHUB_EVENT_NAME ?? '',
+ ref: process.env.GITHUB_REF,
+ pullRequestNumber: event.pullRequestNumber,
+ }),
+ } : {}),
};
return query;
}
@@ -42,9 +53,10 @@ export function buildPreviousWorkflowRunsQuery(
export async function waitForPreviousWorkflowRuns(
token: string,
repository: RepositoryCoordinates,
+ event?: { readonly isSingleAction: boolean; readonly pullRequestNumber?: number },
): Promise {
- const query = buildPreviousWorkflowRunsQuery(repository);
- if (process.env.GITHUB_ACTIONS === 'true' && !Number.isSafeInteger(query.currentRunId)) {
+ const query = buildPreviousWorkflowRunsQuery(repository, event);
+ if (process.env.GITHUB_ACTIONS === 'true' && (!Number.isSafeInteger(query.currentRunId) || query.currentRunId <= 0)) {
throw new Error('GitHub workflow identity is unavailable; refusing to bypass sequential execution.');
}
if (process.env.GITHUB_ACTIONS === 'true' && !query.workflowIdentifier) {
diff --git a/src/application/policies/__tests__/native_workflow_coordination_policy.test.ts b/src/application/policies/__tests__/native_workflow_coordination_policy.test.ts
new file mode 100644
index 000000000..6834f0e30
--- /dev/null
+++ b/src/application/policies/__tests__/native_workflow_coordination_policy.test.ts
@@ -0,0 +1,61 @@
+import { expectedNativeWorkflowGroup, hasNativeWorkflowCoordination } from '../native_workflow_coordination_policy';
+import type { PreviousWorkflowRunsQuery } from '../../ports/workflow_run_ports';
+
+const group = 'copilot-pr-${{ github.repository }}-${{ github.event.pull_request.number }}-analysis';
+const native = {
+ workflowRef: 'org/repo/.github/workflows/custom.yml@refs/heads/main',
+ workflowRevision: 'a'.repeat(40), jobId: 'review', eventName: 'pull_request', pullRequestNumber: 21,
+};
+const query: PreviousWorkflowRunsQuery = {
+ owner: 'org', repository: 'repo', currentRunId: 200, workflowIdentifier: 'custom.yml', nativeCoordination: native,
+};
+const workflow = (concurrency: unknown = { group, 'cancel-in-progress': true }) => ({
+ jobs: { review: { steps: [], concurrency } },
+});
+
+describe('native workflow coordination policy', () => {
+ it.each([
+ ['pull_request', undefined, 21, group],
+ ['pull_request_review', undefined, 21, group.replace('analysis', 'review-state')],
+ ['push', 'refs/heads/feature/work', undefined, 'copilot-push-${{ github.repository }}-${{ github.ref_name }}'],
+ ])('recognizes the exact %s lane with valid resource identity', (eventName, ref, pullRequestNumber, expected) => {
+ expect(expectedNativeWorkflowGroup({ ...query, nativeCoordination: { ...native, eventName, ref, pullRequestNumber } })).toBe(expected);
+ expect(hasNativeWorkflowCoordination(workflow({ group: expected, 'cancel-in-progress': true }), 'review', expected)).toBe(true);
+ });
+
+ it.each([
+ { nativeCoordination: undefined },
+ { currentRunId: Number.NaN }, { currentRunId: 0 },
+ { workflowIdentifier: '../custom.yml' }, { workflowIdentifier: undefined },
+ { nativeCoordination: { ...native, workflowRef: 'elsewhere/repo/.github/workflows/custom.yml@main' } },
+ { nativeCoordination: { ...native, workflowRef: 'org/repo/.github/workflows/custom.yml@' } },
+ { nativeCoordination: { ...native, workflowRevision: 'main' } },
+ { nativeCoordination: { ...native, jobId: '' } },
+ { nativeCoordination: { ...native, pullRequestNumber: 0 } },
+ { nativeCoordination: { ...native, pullRequestNumber: undefined } },
+ { nativeCoordination: { ...native, pullRequestNumber: 1.5 } },
+ { nativeCoordination: { ...native, eventName: 'issues' } },
+ { nativeCoordination: { ...native, eventName: 'push', ref: 'refs/tags/v3' } },
+ { nativeCoordination: { ...native, eventName: 'push', ref: undefined } },
+ { nativeCoordination: { ...native, eventName: 'push', ref: 'refs/heads/', pullRequestNumber: undefined } },
+ ])('retains the conservative queue for invalid or unrelated metadata %j', patch => {
+ expect(expectedNativeWorkflowGroup({ ...query, ...patch })).toBeUndefined();
+ });
+
+ it.each([
+ undefined, null, [], {}, { concurrency: {} }, { jobs: [] }, { jobs: { review: null } },
+ { jobs: { review: { uses: 'org/repo/workflow.yml@v1', concurrency: { group, 'cancel-in-progress': true } } } },
+ { ...workflow(), concurrency: 'outer' },
+ { jobs: { ...workflow().jobs, other: { uses: 'org/repo/workflow.yml@v1' } } },
+ workflow('custom'), workflow({ group: 'custom', 'cancel-in-progress': true }),
+ workflow({ group, 'cancel-in-progress': false }),
+ workflow({ group, 'cancel-in-progress': '${{ true }}' }),
+ workflow({ group, 'cancel-in-progress': true, queue: 'max' }),
+ ])('does not infer native protection from a custom or incomplete workflow %j', value => {
+ expect(hasNativeWorkflowCoordination(value, 'review', group)).toBe(false);
+ });
+
+ it('accepts explicit single-pending native queue semantics', () => {
+ expect(hasNativeWorkflowCoordination(workflow({ group, 'cancel-in-progress': true, queue: 'single' }), 'review', group)).toBe(true);
+ });
+});
diff --git a/src/application/policies/native_workflow_coordination_policy.ts b/src/application/policies/native_workflow_coordination_policy.ts
new file mode 100644
index 000000000..338c09c51
--- /dev/null
+++ b/src/application/policies/native_workflow_coordination_policy.ts
@@ -0,0 +1,43 @@
+import type { PreviousWorkflowRunsQuery } from '../ports/workflow_run_ports';
+
+const PUSH_GROUP = 'copilot-push-${{ github.repository }}-${{ github.ref_name }}';
+const PR_GROUP = 'copilot-pr-${{ github.repository }}-${{ github.event.pull_request.number }}';
+
+/** Closed recognition of the installed contract, without evaluating expressions. */
+export function expectedNativeWorkflowGroup(query: PreviousWorkflowRunsQuery): string | undefined {
+ const native = query.nativeCoordination;
+ if (!native || !Number.isSafeInteger(query.currentRunId) || query.currentRunId <= 0
+ || !/^[\w.-]+\.ya?ml$/u.test(query.workflowIdentifier ?? '')
+ || !native.workflowRef.startsWith(`${query.owner}/${query.repository}/.github/workflows/${query.workflowIdentifier}@`)
+ || native.workflowRef.endsWith('@')
+ || !/^[a-f\d]{40}$/iu.test(native.workflowRevision)
+ || !/^[a-zA-Z_][\w-]*$/u.test(native.jobId)) return undefined;
+
+ if (native.eventName === 'push' && native.ref?.startsWith('refs/heads/')
+ && native.ref.length > 'refs/heads/'.length) return PUSH_GROUP;
+ const pullRequestNumber = native.pullRequestNumber;
+ if (typeof pullRequestNumber !== 'number' || !Number.isSafeInteger(pullRequestNumber) || pullRequestNumber <= 0) return undefined;
+ if (native.eventName === 'pull_request') return `${PR_GROUP}-analysis`;
+ if (native.eventName === 'pull_request_review') return `${PR_GROUP}-review-state`;
+ return undefined;
+}
+
+export function hasNativeWorkflowCoordination(workflow: unknown, jobId: string, expectedGroup: string): boolean {
+ const definition = record(workflow);
+ const jobs = record(definition?.jobs);
+ const job = record(jobs?.[jobId]);
+ const concurrency = record(job?.concurrency);
+ // Custom/reusable/workflow-level contracts retain conservative polling.
+ // A single direct job also avoids confusing a reusable callee's job ID
+ // with a different protected job in its caller's workflow context.
+ return definition?.concurrency === undefined && jobs !== undefined && Object.keys(jobs).length === 1
+ && Array.isArray(job?.steps)
+ && job?.uses === undefined && concurrency?.group === expectedGroup
+ && concurrency?.['cancel-in-progress'] === true
+ && (concurrency.queue === undefined || concurrency.queue === 'single');
+}
+
+function record(value: unknown): Record | undefined {
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
+ ? value as Record : undefined;
+}
diff --git a/src/application/ports/workflow_run_ports.ts b/src/application/ports/workflow_run_ports.ts
index 2f56f1cd8..35f8f1f58 100644
--- a/src/application/ports/workflow_run_ports.ts
+++ b/src/application/ports/workflow_run_ports.ts
@@ -4,6 +4,21 @@ export interface PreviousWorkflowRunsQuery {
currentRunId: number;
/** Workflow file name accepted by GitHub to scope the queue query. */
workflowIdentifier?: string;
+ /** GitHub-provided executing workflow metadata; never an opt-out flag. */
+ nativeCoordination?: NativeWorkflowCoordinationContext;
+}
+
+export interface NativeWorkflowCoordinationContext {
+ readonly workflowRef: string;
+ readonly workflowRevision: string;
+ readonly jobId: string;
+ readonly eventName: string;
+ readonly ref?: string;
+ readonly pullRequestNumber?: number;
+}
+
+export interface NativeWorkflowCoordinationQueryPort {
+ isCoordinated(query: PreviousWorkflowRunsQuery, context: WorkflowQueueRequestContext): Promise;
}
export interface WorkflowQueueRequestContext {
@@ -34,6 +49,7 @@ export interface WorkflowProviderRetryObservation {
}
export interface WorkflowPollingObserverPort {
+ nativeCoordinationVerified?(): void;
noActivePreviousRuns(): void;
waitingForPreviousRuns(activeRunCount: number, delayMilliseconds: number): void;
providerRetry?(observation: WorkflowProviderRetryObservation): void;
diff --git a/src/application/usecases/workflow/__tests__/wait_for_previous_workflow_runs_use_case.test.ts b/src/application/usecases/workflow/__tests__/wait_for_previous_workflow_runs_use_case.test.ts
index 1170158a9..04ce5eb46 100644
--- a/src/application/usecases/workflow/__tests__/wait_for_previous_workflow_runs_use_case.test.ts
+++ b/src/application/usecases/workflow/__tests__/wait_for_previous_workflow_runs_use_case.test.ts
@@ -29,6 +29,75 @@ function policy(overrides: Partial = {}): WorkflowPolling
}
describe('WaitForPreviousWorkflowRunsUseCase', () => {
+ afterEach(() => jest.restoreAllMocks());
+
+ it('uses the system clock and random source for bounded polling by default', async () => {
+ jest.spyOn(Date, 'now').mockReturnValue(1000);
+ const random = jest.spyOn(Math, 'random').mockReturnValue(0.5);
+ const predecessors = { countActivePreviousRuns: jest.fn().mockResolvedValueOnce(1).mockResolvedValue(0) };
+ const delay = { wait: jest.fn().mockResolvedValue(undefined) };
+ const observed = observer();
+
+ await new WaitForPreviousWorkflowRunsUseCase(predecessors, delay, observed).invoke(query);
+
+ expect(predecessors.countActivePreviousRuns).toHaveBeenCalledWith(query, { deadlineAtMilliseconds: 5401000 });
+ expect(delay.wait).toHaveBeenCalledWith(5000);
+ expect(random).toHaveBeenCalledTimes(1);
+ expect(observed.noActivePreviousRuns).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not require a native-coordination observer to delegate safely', async () => {
+ const predecessors = { countActivePreviousRuns: jest.fn() };
+ const delay = { wait: jest.fn() };
+ await new WaitForPreviousWorkflowRunsUseCase(predecessors, delay, observer(), undefined,
+ { nowMilliseconds: () => 0 }, undefined, { isCoordinated: jest.fn().mockResolvedValue(true) }).invoke(query);
+ expect(predecessors.countActivePreviousRuns).not.toHaveBeenCalled();
+ expect(delay.wait).not.toHaveBeenCalled();
+ });
+
+ it('delegates to proven native coordination without enumerating or waiting for queued predecessors', async () => {
+ const predecessors = { countActivePreviousRuns: jest.fn().mockResolvedValue(1) };
+ const native = { isCoordinated: jest.fn().mockResolvedValue(true) };
+ const delay = { wait: jest.fn() };
+ const observed = { ...observer(), nativeCoordinationVerified: jest.fn() };
+ await new WaitForPreviousWorkflowRunsUseCase(predecessors, delay, observed, undefined,
+ { nowMilliseconds: () => 0 }, undefined, native).invoke(query);
+ expect(native.isCoordinated).toHaveBeenCalledWith(query, { deadlineAtMilliseconds: 5400000 });
+ expect(predecessors.countActivePreviousRuns).not.toHaveBeenCalled();
+ expect(delay.wait).not.toHaveBeenCalled();
+ expect(observed.nativeCoordinationVerified).toHaveBeenCalledTimes(1);
+ expect(observed.noActivePreviousRuns).not.toHaveBeenCalled();
+ });
+
+ it('keeps the predecessor fence when native proof is absent and inspects proof only once', async () => {
+ const predecessors = { countActivePreviousRuns: jest.fn().mockResolvedValueOnce(1).mockResolvedValue(0) };
+ const native = { isCoordinated: jest.fn().mockResolvedValue(false) };
+ const delay = { wait: jest.fn() };
+ await new WaitForPreviousWorkflowRunsUseCase(predecessors, delay, observer(), undefined,
+ { nowMilliseconds: () => 0 }, { next: () => 0.5 }, native).invoke(query);
+ expect(predecessors.countActivePreviousRuns).toHaveBeenCalledTimes(2);
+ expect(delay.wait).toHaveBeenCalledWith(5000);
+ expect(native.isCoordinated).toHaveBeenCalledTimes(1);
+ });
+
+ it('propagates proof errors without treating the predecessor queue as empty', async () => {
+ const predecessors = { countActivePreviousRuns: jest.fn() };
+ const native = { isCoordinated: jest.fn().mockRejectedValue(new Error('denied')) };
+ await expect(new WaitForPreviousWorkflowRunsUseCase(predecessors, { wait: jest.fn() }, observer(),
+ undefined, { nowMilliseconds: () => 0 }, undefined, native).invoke(query)).rejects.toThrow('denied');
+ expect(predecessors.countActivePreviousRuns).not.toHaveBeenCalled();
+ });
+
+ it('includes native proof in the absolute queue deadline', async () => {
+ let now = 0;
+ const predecessors = { countActivePreviousRuns: jest.fn() };
+ const native = { isCoordinated: jest.fn(async () => { now = 100; return true; }) };
+ await expect(new WaitForPreviousWorkflowRunsUseCase(predecessors, { wait: jest.fn() }, observer(),
+ policy({ maximumQueueWaitMilliseconds: 100 }), { nowMilliseconds: () => now }, undefined, native)
+ .invoke(query)).rejects.toThrow('Timeout');
+ expect(predecessors.countActivePreviousRuns).not.toHaveBeenCalled();
+ });
+
it('queries immediately and reports when no previous runs are active', async () => {
const queryPort: PreviousWorkflowRunsQueryPort = {
countActivePreviousRuns: jest.fn().mockResolvedValue(0),
@@ -145,4 +214,28 @@ describe('WaitForPreviousWorkflowRunsUseCase', () => {
).invoke(query)).rejects.toThrow('Timeout waiting for previous runs to finish.');
expect(queryPort.countActivePreviousRuns).not.toHaveBeenCalled();
});
+
+ it('does not start native proof when the queue deadline has already expired', async () => {
+ const queryPort = { countActivePreviousRuns: jest.fn() };
+ const native = { isCoordinated: jest.fn() };
+ await expect(new WaitForPreviousWorkflowRunsUseCase(queryPort, { wait: jest.fn() }, observer(),
+ policy({ maximumQueueWaitMilliseconds: 100 }),
+ { nowMilliseconds: jest.fn().mockReturnValueOnce(0).mockReturnValue(100) }, undefined, native,
+ ).invoke(query)).rejects.toThrow('Timeout waiting for previous runs to finish.');
+ expect(native.isCoordinated).not.toHaveBeenCalled();
+ expect(queryPort.countActivePreviousRuns).not.toHaveBeenCalled();
+ });
+
+ it.each([0, 1])('rejects a provider result arriving at the deadline, even with %i predecessors', async active => {
+ let now = 0;
+ const queryPort = { countActivePreviousRuns: jest.fn(async () => { now = 100; return active; }) };
+ const delay = { wait: jest.fn() };
+ const observed = observer();
+ await expect(new WaitForPreviousWorkflowRunsUseCase(queryPort, delay, observed,
+ policy({ maximumQueueWaitMilliseconds: 100 }), { nowMilliseconds: () => now },
+ ).invoke(query)).rejects.toThrow('Timeout waiting for previous runs to finish.');
+ expect(observed.noActivePreviousRuns).not.toHaveBeenCalled();
+ expect(observed.waitingForPreviousRuns).not.toHaveBeenCalled();
+ expect(delay.wait).not.toHaveBeenCalled();
+ });
});
diff --git a/src/application/usecases/workflow/wait_for_previous_workflow_runs_use_case.ts b/src/application/usecases/workflow/wait_for_previous_workflow_runs_use_case.ts
index 309d99792..8676ff815 100644
--- a/src/application/usecases/workflow/wait_for_previous_workflow_runs_use_case.ts
+++ b/src/application/usecases/workflow/wait_for_previous_workflow_runs_use_case.ts
@@ -1,5 +1,6 @@
import type {
PreviousWorkflowRunsQuery,
+ NativeWorkflowCoordinationQueryPort,
PreviousWorkflowRunsQueryPort,
WorkflowPollingDelayPort,
WorkflowPollingObserverPort,
@@ -27,12 +28,22 @@ export class WaitForPreviousWorkflowRunsUseCase implements ParamUseCase {
const deadlineAtMilliseconds = this.clock.nowMilliseconds() + this.policy.maximumQueueWaitMilliseconds;
let pollIndex = 0;
+ if (this.nativeCoordination && this.clock.nowMilliseconds() < deadlineAtMilliseconds) {
+ const coordinated = await this.nativeCoordination.isCoordinated(query, { deadlineAtMilliseconds });
+ if (this.clock.nowMilliseconds() >= deadlineAtMilliseconds) throw queueTimeoutError();
+ if (coordinated) {
+ this.observerPort.nativeCoordinationVerified?.();
+ return;
+ }
+ }
+
while (true) {
if (this.clock.nowMilliseconds() >= deadlineAtMilliseconds) {
throw queueTimeoutError();
diff --git a/src/data/repository/workflow/__tests__/native_workflow_coordination_repository.test.ts b/src/data/repository/workflow/__tests__/native_workflow_coordination_repository.test.ts
new file mode 100644
index 000000000..a003ac904
--- /dev/null
+++ b/src/data/repository/workflow/__tests__/native_workflow_coordination_repository.test.ts
@@ -0,0 +1,87 @@
+import { NativeWorkflowCoordinationRepository } from '../native_workflow_coordination_repository';
+import type { PreviousWorkflowRunsQuery } from '../../../../application/ports/workflow_run_ports';
+
+const group = 'copilot-pr-${{ github.repository }}-${{ github.event.pull_request.number }}-analysis';
+const source = `jobs:\n review:\n steps: []\n concurrency:\n group: ${group}\n cancel-in-progress: true\n`;
+const query: PreviousWorkflowRunsQuery = {
+ owner: 'org', repository: 'repo', currentRunId: 200, workflowIdentifier: 'custom.yml',
+ nativeCoordination: {
+ workflowRef: 'org/repo/.github/workflows/custom.yml@refs/pull/21/merge',
+ workflowRevision: 'a'.repeat(40), jobId: 'review', eventName: 'pull_request', pullRequestNumber: 21,
+ },
+};
+const context = { deadlineAtMilliseconds: 100000 };
+const content = (text: string) => ({ data: { type: 'file', encoding: 'base64', content: Buffer.from(text).toString('base64') } });
+
+function setup() {
+ const getContent = jest.fn().mockResolvedValue(content(source));
+ const delayPort = { wait: jest.fn().mockResolvedValue(undefined) };
+ const repository = new NativeWorkflowCoordinationRepository({ rest: { repos: { getContent } } }, {
+ clock: { nowMilliseconds: () => 0 }, random: { next: () => 0.5 }, delayPort,
+ });
+ return { repository, getContent, delayPort };
+}
+
+describe('NativeWorkflowCoordinationRepository', () => {
+ it('reads the executing workflow revision and current job rather than a branch or checkout', async () => {
+ const { repository, getContent } = setup();
+ await expect(repository.isCoordinated(query, context)).resolves.toBe(true);
+ expect(getContent).toHaveBeenCalledWith({ owner: 'org', repo: 'repo', path: '.github/workflows/custom.yml', ref: 'a'.repeat(40) });
+ });
+
+ it('does not perform a content read for legacy metadata', async () => {
+ const { repository, getContent } = setup();
+ await expect(repository.isCoordinated({ ...query, nativeCoordination: undefined }, context)).resolves.toBe(false);
+ expect(getContent).not.toHaveBeenCalled();
+ });
+
+ it('retains polling for a valid but nonmatching workflow, even with the usual filename', async () => {
+ const { repository, getContent } = setup();
+ getContent.mockResolvedValue(content('jobs: { review: { steps: [] } }'));
+ await expect(repository.isCoordinated(query, context)).resolves.toBe(false);
+ });
+
+ it.each([null, [], { type: 'dir' }, { type: 'file', encoding: 'base64' },
+ { type: 'file', encoding: 'utf8', content: source },
+ { type: 'file', encoding: 'base64', content: 'A'.repeat(256 * 1024 * 2 + 1) },
+ ])('fails closed for malformed or oversized provider content %#', async value => {
+ const { repository, getContent } = setup();
+ getContent.mockResolvedValue({ data: value });
+ await expect(repository.isCoordinated(query, context)).rejects.toThrow('content is unavailable');
+ });
+
+ it('rejects decoded content over the bound', async () => {
+ const { repository, getContent } = setup();
+ getContent.mockResolvedValue(content('a'.repeat(256 * 1024 + 1)));
+ await expect(repository.isCoordinated(query, context)).rejects.toThrow('inspection limit');
+ });
+
+ it('rejects malformed base64 instead of silently discarding invalid characters', async () => {
+ const { repository, getContent } = setup();
+ const invalid = content(source);
+ invalid.data.content += '!';
+ getContent.mockResolvedValue(invalid);
+ await expect(repository.isCoordinated(query, context)).rejects.toThrow('encoding is invalid');
+ });
+
+ it.each(['jobs: [', 'jobs: !!js/function function() {}'])('rejects invalid YAML or executable tags', async text => {
+ const { repository, getContent } = setup();
+ getContent.mockResolvedValue(content(text));
+ await expect(repository.isCoordinated(query, context)).rejects.toThrow();
+ });
+
+ it('retries a transient proof read using the existing bounded retry policy', async () => {
+ const { repository, getContent, delayPort } = setup();
+ getContent.mockRejectedValueOnce({ status: 500 });
+ await expect(repository.isCoordinated(query, context)).resolves.toBe(true);
+ expect(getContent).toHaveBeenCalledTimes(2);
+ expect(delayPort.wait).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not turn a permanent proof-read failure into permission to proceed', async () => {
+ const { repository, getContent, delayPort } = setup();
+ getContent.mockRejectedValue({ status: 403 });
+ await expect(repository.isCoordinated(query, context)).rejects.toEqual({ status: 403 });
+ expect(delayPort.wait).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/data/repository/workflow/native_workflow_coordination_repository.ts b/src/data/repository/workflow/native_workflow_coordination_repository.ts
new file mode 100644
index 000000000..e3085aa25
--- /dev/null
+++ b/src/data/repository/workflow/native_workflow_coordination_repository.ts
@@ -0,0 +1,48 @@
+import { JSON_SCHEMA, load } from 'js-yaml';
+import type {
+ NativeWorkflowCoordinationQueryPort,
+ PreviousWorkflowRunsQuery,
+ WorkflowQueueRequestContext,
+} from '../../../application/ports/workflow_run_ports';
+import {
+ expectedNativeWorkflowGroup,
+ hasNativeWorkflowCoordination,
+} from '../../../application/policies/native_workflow_coordination_policy';
+import type { GithubWorkflowDefinitionClient } from '../../../infrastructure/github/ports/github_workflow_provider_ports';
+import { withWorkflowRunsRetry, WORKFLOW_RUNS_RETRY_POLICY, type WorkflowRunsRetryPolicy } from './workflow_runs_retry';
+
+const MAX_WORKFLOW_BYTES = 256 * 1024;
+type RetryDependencies = Parameters[1];
+
+/** Reads the workflow GitHub executed, never the possibly modified checkout. */
+export class NativeWorkflowCoordinationRepository implements NativeWorkflowCoordinationQueryPort {
+ constructor(
+ private readonly client: GithubWorkflowDefinitionClient,
+ private readonly retry: Omit & { policy?: WorkflowRunsRetryPolicy },
+ ) {}
+
+ async isCoordinated(query: PreviousWorkflowRunsQuery, context: WorkflowQueueRequestContext): Promise {
+ const group = expectedNativeWorkflowGroup(query);
+ const native = query.nativeCoordination;
+ if (!group || !native) return false;
+
+ const response = await withWorkflowRunsRetry(() => this.client.rest.repos.getContent({
+ owner: query.owner,
+ repo: query.repository,
+ path: `.github/workflows/${query.workflowIdentifier}`,
+ ref: native.workflowRevision,
+ }), { ...this.retry, policy: this.retry.policy ?? WORKFLOW_RUNS_RETRY_POLICY,
+ deadlineAtMilliseconds: context.deadlineAtMilliseconds });
+ const file = response.data as { type?: string; encoding?: string; content?: string } | null;
+ if (!file || Array.isArray(file) || file.type !== 'file' || file.encoding !== 'base64'
+ || typeof file.content !== 'string' || file.content.length > MAX_WORKFLOW_BYTES * 2) {
+ throw new Error('Executing workflow content is unavailable.');
+ }
+ const encoded = file.content.replace(/\s/gu, '');
+ const bytes = Buffer.from(encoded, 'base64');
+ if (bytes.toString('base64') !== encoded) throw new Error('Executing workflow encoding is invalid.');
+ if (bytes.length > MAX_WORKFLOW_BYTES) throw new Error('Executing workflow exceeds the inspection limit.');
+ const workflow: unknown = load(bytes.toString('utf8'), { schema: JSON_SCHEMA });
+ return hasNativeWorkflowCoordination(workflow, native.jobId, group);
+ }
+}
diff --git a/src/infrastructure/composition/workflow_queue_composition_root.ts b/src/infrastructure/composition/workflow_queue_composition_root.ts
index f1f1ad9d5..2baada013 100644
--- a/src/infrastructure/composition/workflow_queue_composition_root.ts
+++ b/src/infrastructure/composition/workflow_queue_composition_root.ts
@@ -4,23 +4,33 @@ import { TimerWorkflowPollingDelayAdapter } from '../time/timer_workflow_polling
import { LoggerWorkflowPollingObserverAdapter } from '../logging/logger_workflow_polling_observer_adapter';
import { SystemWorkflowQueueClockAdapter } from '../time/system_workflow_queue_clock_adapter';
import { SystemWorkflowPollingRandomAdapter } from '../time/system_workflow_polling_random_adapter';
-import { createWorkflowRunsClient } from './github_workflow_client_factory';
+import { NativeWorkflowCoordinationRepository } from '../../data/repository/workflow/native_workflow_coordination_repository';
+import { getOctokitClient } from '../github/octokit_client_resolver';
+import type { GithubWorkflowDefinitionClient, GithubWorkflowRunsClient } from '../github/ports/github_workflow_provider_ports';
export function createWaitForPreviousWorkflowRunsUseCase(token: string): WaitForPreviousWorkflowRunsUseCase {
- const client = createWorkflowRunsClient().getClient(token);
+ const client = getOctokitClient(token);
const delayPort = new TimerWorkflowPollingDelayAdapter();
const observerPort = new LoggerWorkflowPollingObserverAdapter();
+ const clock = new SystemWorkflowQueueClockAdapter();
+ const random = new SystemWorkflowPollingRandomAdapter();
return new WaitForPreviousWorkflowRunsUseCase(
new ActivePreviousWorkflowRunsRepository(
client,
delayPort,
undefined,
- new SystemWorkflowQueueClockAdapter(),
- new SystemWorkflowPollingRandomAdapter(),
+ clock,
+ random,
observerPort,
),
delayPort,
observerPort,
+ undefined,
+ clock,
+ random,
+ new NativeWorkflowCoordinationRepository(client, {
+ delayPort, clock, random, observer: observerPort,
+ }),
);
}
diff --git a/src/infrastructure/github/ports/github_workflow_provider_ports.ts b/src/infrastructure/github/ports/github_workflow_provider_ports.ts
index 58183b549..d70689c67 100644
--- a/src/infrastructure/github/ports/github_workflow_provider_ports.ts
+++ b/src/infrastructure/github/ports/github_workflow_provider_ports.ts
@@ -6,6 +6,14 @@ export interface GithubWorkflowRunsParameters {
status?: string;
}
+export interface GithubWorkflowDefinitionClient {
+ rest: {
+ repos: {
+ getContent(parameters: { owner: string; repo: string; path: string; ref: string }): Promise<{ data: unknown }>;
+ };
+ };
+}
+
export interface GithubWorkflowRunsResponse {
data: { workflow_runs: GithubWorkflowRun[] } | GithubWorkflowRun[];
}
diff --git a/src/infrastructure/logging/__tests__/logger_workflow_polling_observer_adapter.test.ts b/src/infrastructure/logging/__tests__/logger_workflow_polling_observer_adapter.test.ts
index f87737c2a..1608b8d7b 100644
--- a/src/infrastructure/logging/__tests__/logger_workflow_polling_observer_adapter.test.ts
+++ b/src/infrastructure/logging/__tests__/logger_workflow_polling_observer_adapter.test.ts
@@ -13,11 +13,23 @@ describe('LoggerWorkflowPollingObserverAdapter', () => {
const observer = new LoggerWorkflowPollingObserverAdapter();
observer.noActivePreviousRuns();
+ observer.nativeCoordinationVerified();
observer.waitingForPreviousRuns(2, 2000);
expect(logDebugInfo).toHaveBeenNthCalledWith(1, '✅ No previous runs active. Continuing...');
+ expect(logInfo).toHaveBeenCalledWith('Native workflow resource/lane coordination verified; continuing before setup.');
expect(logInfo).toHaveBeenCalledWith(
'⏳ Found 2 previous run(s) still active. Waiting 2s...',
);
});
+
+ it.each([
+ { reason: 'transient' as const, attempt: 1, delayMilliseconds: 1000 },
+ { reason: 'rate_limit' as const, attempt: 2, delayMilliseconds: 2000, resetEpochSeconds: 1700000000 },
+ ])('reports bounded retry metadata for $reason', observation => {
+ new LoggerWorkflowPollingObserverAdapter().providerRetry(observation);
+
+ expect(logDebugInfo).toHaveBeenCalledWith('GitHub workflow polling retry scheduled.', false, observation);
+ expect(logInfo).not.toHaveBeenCalled();
+ });
});
diff --git a/src/infrastructure/logging/logger_workflow_polling_observer_adapter.ts b/src/infrastructure/logging/logger_workflow_polling_observer_adapter.ts
index f3f31ff0a..2a028aee8 100644
--- a/src/infrastructure/logging/logger_workflow_polling_observer_adapter.ts
+++ b/src/infrastructure/logging/logger_workflow_polling_observer_adapter.ts
@@ -2,6 +2,10 @@ import type { WorkflowPollingObserverPort } from '../../application/ports/workfl
import { logDebugInfo, logInfo } from '../../utils/logger';
export class LoggerWorkflowPollingObserverAdapter implements WorkflowPollingObserverPort {
+ nativeCoordinationVerified(): void {
+ logInfo('Native workflow resource/lane coordination verified; continuing before setup.');
+ }
+
noActivePreviousRuns(): void {
logDebugInfo('✅ No previous runs active. Continuing...');
}