@vypdev/copilot 3.5.2 → 3.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -51946,7 +51946,14 @@ class DeploymentOrchestrationRuntime {
51946
51946
  selectedPrMode: inspection.decision.mode,
51947
51947
  };
51948
51948
  await this.persist(context, managed);
51949
- await this.applyMergeBehavior(context, operation, pullRequest, inspection);
51949
+ try {
51950
+ await this.applyMergeBehavior(context, operation, pullRequest, inspection);
51951
+ }
51952
+ catch (cause) {
51953
+ const message = `GitHub could not configure automatic merge for PR #${pullRequest.number}. Check the PR and retry the stored ${category} phase.`;
51954
+ const error = new application_error_1.ApplicationError("provider.unavailable", message, { cause, retryable: true });
51955
+ return { kind: "blocked", result: await this.block(context, managed, category, message, true, error) };
51956
+ }
51950
51957
  await this.publishDashboard(context, managed);
51951
51958
  return { kind: "configured", operation: managed };
51952
51959
  }
@@ -52876,22 +52883,19 @@ const application_error_1 = __nccwpck_require__(75999);
52876
52883
  const deployment_plan_policy_1 = __nccwpck_require__(8352);
52877
52884
  const deployment_operation_1 = __nccwpck_require__(92730);
52878
52885
  const deployment_orchestration_runtime_1 = __nccwpck_require__(77658);
52886
+ const deployment_continuation_guard_1 = __nccwpck_require__(1779);
52879
52887
  class ConfirmPublicationHandler {
52880
52888
  constructor(runtime, reconciliation) {
52881
52889
  this.runtime = runtime;
52882
52890
  this.reconciliation = reconciliation;
52883
52891
  }
52884
52892
  async invoke(context) {
52885
- const operation = await this.resumeRetryableBlock(context, (0, deployment_orchestration_runtime_1.requireDeploymentOperation)(context));
52886
- if (operation.phase === "reconciliation_pending" && operation.publicationVerified) {
52887
- return await this.reconciliation.plan(context, operation)
52888
- ?? (0, deployment_orchestration_runtime_1.deploymentSuccess)(`Publication for ${operation.tag} was already verified; reconciliation state was recovered.`);
52889
- }
52890
- if (operation.phase === "completed" && operation.publicationVerified) {
52891
- await this.runtime.publishDashboard(context, operation);
52892
- return (0, deployment_orchestration_runtime_1.deploymentSuccess)(`Publication for ${operation.tag} was already verified; duplicate notification ignored.`);
52893
- }
52894
- assertPublicationPhase(operation);
52893
+ const current = (0, deployment_orchestration_runtime_1.requireDeploymentOperation)(context);
52894
+ const invalid = (0, deployment_continuation_guard_1.validateDeploymentContinuation)(current, context.singleAction.operationId, ["published", "publishing", "promoted", "reconciliation_pending", "completed"], context.singleAction.version);
52895
+ if (invalid)
52896
+ throw new application_error_1.ApplicationError("workflow.stale", invalid);
52897
+ // Verify external receipts before clearing a block or retrying any merge/cleanup effect.
52898
+ const operation = current;
52895
52899
  const productionSha = requireProductionSha(operation);
52896
52900
  const publication = await this.runtime.dependencies.publication.inspect({
52897
52901
  tag: operation.tag,
@@ -52908,8 +52912,18 @@ class ConfirmPublicationHandler {
52908
52912
  if (!reachable) {
52909
52913
  return await this.runtime.block(context, operation, "publication", "Published SHA is not reachable from the stored production branch.", false);
52910
52914
  }
52915
+ const resumed = await this.resumeRetryableBlock(context, operation);
52916
+ if (resumed.phase === "reconciliation_pending" && resumed.publicationVerified) {
52917
+ return await this.reconciliation.plan(context, resumed)
52918
+ ?? (0, deployment_orchestration_runtime_1.deploymentSuccess)(`Publication for ${resumed.tag} was already verified; reconciliation state was recovered.`);
52919
+ }
52920
+ if (resumed.phase === "completed" && resumed.publicationVerified) {
52921
+ await this.runtime.publishDashboard(context, resumed);
52922
+ return (0, deployment_orchestration_runtime_1.deploymentSuccess)(`Publication for ${resumed.tag} was already verified; duplicate notification ignored.`);
52923
+ }
52924
+ assertPublicationPhase(resumed);
52911
52925
  const published = {
52912
- ...operation,
52926
+ ...resumed,
52913
52927
  phase: "published",
52914
52928
  publicationVerified: true,
52915
52929
  publicationReceipt: publication.receipt,
@@ -53228,6 +53242,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
53228
53242
  exports.ReconciliationHandler = void 0;
53229
53243
  const deployment_plan_policy_1 = __nccwpck_require__(8352);
53230
53244
  const deployment_operation_1 = __nccwpck_require__(92730);
53245
+ const managed_pull_request_1 = __nccwpck_require__(95914);
53231
53246
  const deployment_orchestration_runtime_1 = __nccwpck_require__(77658);
53232
53247
  class ReconciliationHandler {
53233
53248
  constructor(runtime) {
@@ -53264,10 +53279,12 @@ class ReconciliationHandler {
53264
53279
  return await this.complete(context, updated, ` after reconciliation PR #${pullRequest.number}`);
53265
53280
  }
53266
53281
  async plan(context, operation) {
53267
- const index = operation.reconciliationTargets.findIndex((target) => target.status === "pending" && target.pullRequest === undefined);
53282
+ const index = operation.reconciliationTargets.findIndex((target) => target.status === "pending");
53268
53283
  if (index < 0)
53269
53284
  return await this.finishOrPresent(context, operation);
53270
53285
  let target = operation.reconciliationTargets[index];
53286
+ if (target.pullRequest !== undefined)
53287
+ return await this.resumePullRequest(context, operation, target);
53271
53288
  const targetRole = (0, deployment_orchestration_runtime_1.reconciliationTargetRole)(operation, target.targetBranch);
53272
53289
  const preflight = await this.runtime.inspectMergeBehavior(context, operation, target.targetBranch, targetRole, target.sourceSha);
53273
53290
  if (preflight.kind === "blocked") {
@@ -53324,6 +53341,26 @@ class ReconciliationHandler {
53324
53341
  await this.runtime.publishMilestone(context, completed, { kind: "orchestration-complete", tag: completed.tag });
53325
53342
  return (0, deployment_orchestration_runtime_1.deploymentSuccess)(`Deployment ${completed.tag} completed${completionContext}.`);
53326
53343
  }
53344
+ async resumePullRequest(context, operation, target) {
53345
+ const pullRequest = await this.runtime.dependencies.pullRequests.getPullRequest(target.pullRequest);
53346
+ const identity = (0, managed_pull_request_1.parseManagedPullRequestMarker)(pullRequest.body);
53347
+ const expectedHead = target.syncBranch ? target.syncSha : target.sourceSha;
53348
+ if (!identity || identity.operationId !== operation.operationId || identity.phase !== "reconciliation"
53349
+ || identity.issue !== context.singleAction.issue || pullRequest.number !== target.pullRequest
53350
+ || pullRequest.repositoryFullName.toLowerCase() !== `${context.owner}/${context.repo}`.toLowerCase()
53351
+ || pullRequest.baseBranch !== target.targetBranch
53352
+ || pullRequest.headBranch !== (target.syncBranch ?? target.sourceBranch)
53353
+ || !expectedHead || pullRequest.headSha !== expectedHead) {
53354
+ return await this.runtime.block(context, operation, "reconciliation", "The recorded reconciliation PR no longer matches its durable identity and head SHA.", false);
53355
+ }
53356
+ if (pullRequest.merged)
53357
+ return await this.accept(context, operation, pullRequest);
53358
+ if (pullRequest.state === "closed") {
53359
+ return await this.runtime.block(context, operation, "reconciliation", `Reconciliation PR #${pullRequest.number} was closed without merge.`, true);
53360
+ }
53361
+ const configured = await this.runtime.configureMergeBehavior(context, operation, pullRequest, "reconciliation", (0, deployment_orchestration_runtime_1.reconciliationTargetRole)(operation, target.targetBranch));
53362
+ return configured.kind === "blocked" ? configured.result : undefined;
53363
+ }
53327
53364
  async finishOrPresent(context, operation) {
53328
53365
  if (operation.reconciliationTargets.length > 0
53329
53366
  && operation.reconciliationTargets.every((target) => target.status === "completed")) {
@@ -50260,7 +50260,14 @@ class DeploymentOrchestrationRuntime {
50260
50260
  selectedPrMode: inspection.decision.mode,
50261
50261
  };
50262
50262
  await this.persist(context, managed);
50263
- await this.applyMergeBehavior(context, operation, pullRequest, inspection);
50263
+ try {
50264
+ await this.applyMergeBehavior(context, operation, pullRequest, inspection);
50265
+ }
50266
+ catch (cause) {
50267
+ const message = `GitHub could not configure automatic merge for PR #${pullRequest.number}. Check the PR and retry the stored ${category} phase.`;
50268
+ const error = new application_error_1.ApplicationError("provider.unavailable", message, { cause, retryable: true });
50269
+ return { kind: "blocked", result: await this.block(context, managed, category, message, true, error) };
50270
+ }
50264
50271
  await this.publishDashboard(context, managed);
50265
50272
  return { kind: "configured", operation: managed };
50266
50273
  }
@@ -51190,22 +51197,19 @@ const application_error_1 = __nccwpck_require__(75999);
51190
51197
  const deployment_plan_policy_1 = __nccwpck_require__(8352);
51191
51198
  const deployment_operation_1 = __nccwpck_require__(92730);
51192
51199
  const deployment_orchestration_runtime_1 = __nccwpck_require__(77658);
51200
+ const deployment_continuation_guard_1 = __nccwpck_require__(1779);
51193
51201
  class ConfirmPublicationHandler {
51194
51202
  constructor(runtime, reconciliation) {
51195
51203
  this.runtime = runtime;
51196
51204
  this.reconciliation = reconciliation;
51197
51205
  }
51198
51206
  async invoke(context) {
51199
- const operation = await this.resumeRetryableBlock(context, (0, deployment_orchestration_runtime_1.requireDeploymentOperation)(context));
51200
- if (operation.phase === "reconciliation_pending" && operation.publicationVerified) {
51201
- return await this.reconciliation.plan(context, operation)
51202
- ?? (0, deployment_orchestration_runtime_1.deploymentSuccess)(`Publication for ${operation.tag} was already verified; reconciliation state was recovered.`);
51203
- }
51204
- if (operation.phase === "completed" && operation.publicationVerified) {
51205
- await this.runtime.publishDashboard(context, operation);
51206
- return (0, deployment_orchestration_runtime_1.deploymentSuccess)(`Publication for ${operation.tag} was already verified; duplicate notification ignored.`);
51207
- }
51208
- assertPublicationPhase(operation);
51207
+ const current = (0, deployment_orchestration_runtime_1.requireDeploymentOperation)(context);
51208
+ const invalid = (0, deployment_continuation_guard_1.validateDeploymentContinuation)(current, context.singleAction.operationId, ["published", "publishing", "promoted", "reconciliation_pending", "completed"], context.singleAction.version);
51209
+ if (invalid)
51210
+ throw new application_error_1.ApplicationError("workflow.stale", invalid);
51211
+ // Verify external receipts before clearing a block or retrying any merge/cleanup effect.
51212
+ const operation = current;
51209
51213
  const productionSha = requireProductionSha(operation);
51210
51214
  const publication = await this.runtime.dependencies.publication.inspect({
51211
51215
  tag: operation.tag,
@@ -51222,8 +51226,18 @@ class ConfirmPublicationHandler {
51222
51226
  if (!reachable) {
51223
51227
  return await this.runtime.block(context, operation, "publication", "Published SHA is not reachable from the stored production branch.", false);
51224
51228
  }
51229
+ const resumed = await this.resumeRetryableBlock(context, operation);
51230
+ if (resumed.phase === "reconciliation_pending" && resumed.publicationVerified) {
51231
+ return await this.reconciliation.plan(context, resumed)
51232
+ ?? (0, deployment_orchestration_runtime_1.deploymentSuccess)(`Publication for ${resumed.tag} was already verified; reconciliation state was recovered.`);
51233
+ }
51234
+ if (resumed.phase === "completed" && resumed.publicationVerified) {
51235
+ await this.runtime.publishDashboard(context, resumed);
51236
+ return (0, deployment_orchestration_runtime_1.deploymentSuccess)(`Publication for ${resumed.tag} was already verified; duplicate notification ignored.`);
51237
+ }
51238
+ assertPublicationPhase(resumed);
51225
51239
  const published = {
51226
- ...operation,
51240
+ ...resumed,
51227
51241
  phase: "published",
51228
51242
  publicationVerified: true,
51229
51243
  publicationReceipt: publication.receipt,
@@ -51542,6 +51556,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
51542
51556
  exports.ReconciliationHandler = void 0;
51543
51557
  const deployment_plan_policy_1 = __nccwpck_require__(8352);
51544
51558
  const deployment_operation_1 = __nccwpck_require__(92730);
51559
+ const managed_pull_request_1 = __nccwpck_require__(95914);
51545
51560
  const deployment_orchestration_runtime_1 = __nccwpck_require__(77658);
51546
51561
  class ReconciliationHandler {
51547
51562
  constructor(runtime) {
@@ -51578,10 +51593,12 @@ class ReconciliationHandler {
51578
51593
  return await this.complete(context, updated, ` after reconciliation PR #${pullRequest.number}`);
51579
51594
  }
51580
51595
  async plan(context, operation) {
51581
- const index = operation.reconciliationTargets.findIndex((target) => target.status === "pending" && target.pullRequest === undefined);
51596
+ const index = operation.reconciliationTargets.findIndex((target) => target.status === "pending");
51582
51597
  if (index < 0)
51583
51598
  return await this.finishOrPresent(context, operation);
51584
51599
  let target = operation.reconciliationTargets[index];
51600
+ if (target.pullRequest !== undefined)
51601
+ return await this.resumePullRequest(context, operation, target);
51585
51602
  const targetRole = (0, deployment_orchestration_runtime_1.reconciliationTargetRole)(operation, target.targetBranch);
51586
51603
  const preflight = await this.runtime.inspectMergeBehavior(context, operation, target.targetBranch, targetRole, target.sourceSha);
51587
51604
  if (preflight.kind === "blocked") {
@@ -51638,6 +51655,26 @@ class ReconciliationHandler {
51638
51655
  await this.runtime.publishMilestone(context, completed, { kind: "orchestration-complete", tag: completed.tag });
51639
51656
  return (0, deployment_orchestration_runtime_1.deploymentSuccess)(`Deployment ${completed.tag} completed${completionContext}.`);
51640
51657
  }
51658
+ async resumePullRequest(context, operation, target) {
51659
+ const pullRequest = await this.runtime.dependencies.pullRequests.getPullRequest(target.pullRequest);
51660
+ const identity = (0, managed_pull_request_1.parseManagedPullRequestMarker)(pullRequest.body);
51661
+ const expectedHead = target.syncBranch ? target.syncSha : target.sourceSha;
51662
+ if (!identity || identity.operationId !== operation.operationId || identity.phase !== "reconciliation"
51663
+ || identity.issue !== context.singleAction.issue || pullRequest.number !== target.pullRequest
51664
+ || pullRequest.repositoryFullName.toLowerCase() !== `${context.owner}/${context.repo}`.toLowerCase()
51665
+ || pullRequest.baseBranch !== target.targetBranch
51666
+ || pullRequest.headBranch !== (target.syncBranch ?? target.sourceBranch)
51667
+ || !expectedHead || pullRequest.headSha !== expectedHead) {
51668
+ return await this.runtime.block(context, operation, "reconciliation", "The recorded reconciliation PR no longer matches its durable identity and head SHA.", false);
51669
+ }
51670
+ if (pullRequest.merged)
51671
+ return await this.accept(context, operation, pullRequest);
51672
+ if (pullRequest.state === "closed") {
51673
+ return await this.runtime.block(context, operation, "reconciliation", `Reconciliation PR #${pullRequest.number} was closed without merge.`, true);
51674
+ }
51675
+ const configured = await this.runtime.configureMergeBehavior(context, operation, pullRequest, "reconciliation", (0, deployment_orchestration_runtime_1.reconciliationTargetRole)(operation, target.targetBranch));
51676
+ return configured.kind === "blocked" ? configured.result : undefined;
51677
+ }
51641
51678
  async finishOrPresent(context, operation) {
51642
51679
  if (operation.reconciliationTargets.length > 0
51643
51680
  && operation.reconciliationTargets.every((target) => target.status === "completed")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vypdev/copilot",
3
- "version": "3.5.2",
3
+ "version": "3.5.3",
4
4
  "packageManager": "pnpm@10.12.4",
5
5
  "pnpm": {
6
6
  "overrides": {
@@ -8,7 +8,7 @@ on:
8
8
  required: true
9
9
  default: prepare
10
10
  type: choice
11
- options: [prepare, publish]
11
+ options: [prepare, publish, resume]
12
12
  operation-id:
13
13
  description: 'Internal durable operation identifier'
14
14
  required: false
@@ -66,10 +66,10 @@ jobs:
66
66
  if (operationId) errors.push('Operation ID must be empty in prepare mode.');
67
67
  if (!title || title.length > 1000) errors.push('Title must contain 1-1000 characters.');
68
68
  if (!changelog || changelog.length > 50000) errors.push('Changelog must contain 1-50000 characters.');
69
- } else if (mode === 'publish') {
69
+ } else if (mode === 'publish' || mode === 'resume') {
70
70
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{7,127}$/.test(operationId)) errors.push('Operation ID is invalid.');
71
- if (title || changelog) errors.push('Title and changelog must be empty in publish mode.');
72
- } else errors.push('Mode must be prepare or publish.');
71
+ if (title || changelog) errors.push('Title and changelog must be empty in publish or resume mode.');
72
+ } else errors.push('Mode must be prepare, publish, or resume.');
73
73
  if (errors.length) core.setFailed(errors.join(' '));
74
74
 
75
75
  prepare-version-files:
@@ -185,17 +185,66 @@ jobs:
185
185
  single-action-version: ${{ inputs.version }}
186
186
  token: ${{ secrets.PAT }}
187
187
 
188
+ resume-reconciliation:
189
+ name: Resume published deployment
190
+ if: ${{ inputs.mode == 'resume' }}
191
+ runs-on: ubuntu-latest
192
+ needs: validate
193
+ timeout-minutes: 15
194
+ permissions:
195
+ issues: write
196
+ pull-requests: write
197
+ steps:
198
+ - name: Verify published operation before recovery
199
+ uses: actions/github-script@v9
200
+ env:
201
+ ISSUE: ${{ inputs.issue }}
202
+ OPERATION_ID: ${{ inputs.operation-id }}
203
+ VERSION: ${{ inputs.version }}
204
+ with:
205
+ script: |
206
+ const response = await github.rest.issues.get({ ...context.repo, issue_number: Number(process.env.ISSUE) });
207
+ if (response.data.pull_request) throw new Error('The launcher must be an issue.');
208
+ const body = response.data.body || '';
209
+ const start = '<!-- copilot-configuration-start';
210
+ const end = 'copilot-configuration-end -->';
211
+ const first = body.indexOf(start);
212
+ const last = body.indexOf(end, first + start.length);
213
+ if (first < 0 || last < 0 || body.indexOf(start, first + start.length) >= 0) throw new Error('A unique durable configuration is required.');
214
+ const payload = JSON.parse(body.slice(first + start.length, last));
215
+ const state = payload?.deploymentOrchestration;
216
+ const receipt = state?.publicationReceipt;
217
+ const phase = state?.phase === 'blocked' && state?.lastFailure?.retryable === true
218
+ ? state.lastFailure.previousPhase : state?.phase;
219
+ if (payload?.schemaVersion !== 3 || state?.stateVersion !== 1
220
+ || !Number.isSafeInteger(state?.revision) || state.revision < 1
221
+ || state.operationId !== process.env.OPERATION_ID || state.version !== process.env.VERSION
222
+ || !['reconciliation_pending', 'completed'].includes(phase) || state.publicationVerified !== true
223
+ || !/^[a-f0-9]{40}$/i.test(state.productionSha || '') || state.tag !== `v${process.env.VERSION}`
224
+ || receipt?.operationId !== state.operationId || receipt?.productionSha !== state.productionSha
225
+ || receipt?.tag !== state.tag) throw new Error('Recovery requires the matching, already verified publication and reconciliation state.');
226
+ - name: Resume reconciliation from verified receipts
227
+ uses: vypdev/copilot@v3
228
+ with:
229
+ single-action: published_deployment_action
230
+ single-action-issue: ${{ inputs.issue }}
231
+ issue-workflow-profile: ${{ vars.COPILOT_ISSUE_WORKFLOW_PROFILE || '' }}
232
+ single-action-operation-id: ${{ inputs.operation-id }}
233
+ single-action-version: ${{ inputs.version }}
234
+ merge-queue-check-attestations: ${{ vars.MERGE_QUEUE_CHECK_ATTESTATIONS || '[]' }}
235
+ token: ${{ secrets.PAT }}
236
+
188
237
  report-failure:
189
238
  name: Report orchestration failure
190
239
  runs-on: ubuntu-latest
191
240
  timeout-minutes: 5
192
- needs: [validate, prepare-version-files, promote, publish]
241
+ needs: [validate, prepare-version-files, promote, publish, resume-reconciliation]
193
242
  if: ${{ failure() && needs.validate.result == 'success' }}
194
243
  permissions:
195
244
  issues: write
196
245
  steps:
197
246
  - name: Persist publication failure
198
- if: ${{ inputs.mode == 'publish' }}
247
+ if: ${{ inputs.mode == 'publish' || inputs.mode == 'resume' }}
199
248
  uses: vypdev/copilot@v3
200
249
  with:
201
250
  single-action: failed_deployment_action
@@ -8,7 +8,7 @@ on:
8
8
  required: true
9
9
  default: prepare
10
10
  type: choice
11
- options: [prepare, publish]
11
+ options: [prepare, publish, resume]
12
12
  operation-id:
13
13
  description: 'Internal durable operation identifier'
14
14
  required: false
@@ -66,10 +66,10 @@ jobs:
66
66
  if (operationId) errors.push('Operation ID must be empty in prepare mode.');
67
67
  if (!title || title.length > 1000) errors.push('Title must contain 1-1000 characters.');
68
68
  if (!changelog || changelog.length > 50000) errors.push('Changelog must contain 1-50000 characters.');
69
- } else if (mode === 'publish') {
69
+ } else if (mode === 'publish' || mode === 'resume') {
70
70
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{7,127}$/.test(operationId)) errors.push('Operation ID is invalid.');
71
- if (title || changelog) errors.push('Title and changelog must be empty in publish mode.');
72
- } else errors.push('Mode must be prepare or publish.');
71
+ if (title || changelog) errors.push('Title and changelog must be empty in publish or resume mode.');
72
+ } else errors.push('Mode must be prepare, publish, or resume.');
73
73
  if (errors.length) core.setFailed(errors.join(' '));
74
74
 
75
75
  prepare-version-files:
@@ -185,17 +185,66 @@ jobs:
185
185
  single-action-version: ${{ inputs.version }}
186
186
  token: ${{ secrets.PAT }}
187
187
 
188
+ resume-reconciliation:
189
+ name: Resume published deployment
190
+ if: ${{ inputs.mode == 'resume' }}
191
+ runs-on: ubuntu-latest
192
+ needs: validate
193
+ timeout-minutes: 15
194
+ permissions:
195
+ issues: write
196
+ pull-requests: write
197
+ steps:
198
+ - name: Verify published operation before recovery
199
+ uses: actions/github-script@v9
200
+ env:
201
+ ISSUE: ${{ inputs.issue }}
202
+ OPERATION_ID: ${{ inputs.operation-id }}
203
+ VERSION: ${{ inputs.version }}
204
+ with:
205
+ script: |
206
+ const response = await github.rest.issues.get({ ...context.repo, issue_number: Number(process.env.ISSUE) });
207
+ if (response.data.pull_request) throw new Error('The launcher must be an issue.');
208
+ const body = response.data.body || '';
209
+ const start = '<!-- copilot-configuration-start';
210
+ const end = 'copilot-configuration-end -->';
211
+ const first = body.indexOf(start);
212
+ const last = body.indexOf(end, first + start.length);
213
+ if (first < 0 || last < 0 || body.indexOf(start, first + start.length) >= 0) throw new Error('A unique durable configuration is required.');
214
+ const payload = JSON.parse(body.slice(first + start.length, last));
215
+ const state = payload?.deploymentOrchestration;
216
+ const receipt = state?.publicationReceipt;
217
+ const phase = state?.phase === 'blocked' && state?.lastFailure?.retryable === true
218
+ ? state.lastFailure.previousPhase : state?.phase;
219
+ if (payload?.schemaVersion !== 3 || state?.stateVersion !== 1
220
+ || !Number.isSafeInteger(state?.revision) || state.revision < 1
221
+ || state.operationId !== process.env.OPERATION_ID || state.version !== process.env.VERSION
222
+ || !['reconciliation_pending', 'completed'].includes(phase) || state.publicationVerified !== true
223
+ || !/^[a-f0-9]{40}$/i.test(state.productionSha || '') || state.tag !== `v${process.env.VERSION}`
224
+ || receipt?.operationId !== state.operationId || receipt?.productionSha !== state.productionSha
225
+ || receipt?.tag !== state.tag) throw new Error('Recovery requires the matching, already verified publication and reconciliation state.');
226
+ - name: Resume reconciliation from verified receipts
227
+ uses: vypdev/copilot@v3
228
+ with:
229
+ single-action: published_deployment_action
230
+ single-action-issue: ${{ inputs.issue }}
231
+ issue-workflow-profile: ${{ vars.COPILOT_ISSUE_WORKFLOW_PROFILE || '' }}
232
+ single-action-operation-id: ${{ inputs.operation-id }}
233
+ single-action-version: ${{ inputs.version }}
234
+ merge-queue-check-attestations: ${{ vars.MERGE_QUEUE_CHECK_ATTESTATIONS || '[]' }}
235
+ token: ${{ secrets.PAT }}
236
+
188
237
  report-failure:
189
238
  name: Report orchestration failure
190
239
  runs-on: ubuntu-latest
191
240
  timeout-minutes: 5
192
- needs: [validate, prepare-version-files, promote, publish]
241
+ needs: [validate, prepare-version-files, promote, publish, resume-reconciliation]
193
242
  if: ${{ failure() && needs.validate.result == 'success' }}
194
243
  permissions:
195
244
  issues: write
196
245
  steps:
197
246
  - name: Persist publication failure
198
- if: ${{ inputs.mode == 'publish' }}
247
+ if: ${{ inputs.mode == 'publish' || inputs.mode == 'resume' }}
199
248
  uses: vypdev/copilot@v3
200
249
  with:
201
250
  single-action: failed_deployment_action