@vypdev/copilot 3.10.5 → 3.10.6

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.
Files changed (28) hide show
  1. package/README.md +14 -0
  2. package/action.yml +16 -1
  3. package/build/api/index.js +1162 -76
  4. package/build/api/src/application/ports/bugbot_pull_request_resolution_ports.d.ts +4 -3
  5. package/build/api/src/application/ports/bugbot_pull_request_write_ports.d.ts +4 -3
  6. package/build/api/src/application/ports/pull_request_review_comment_ports.d.ts +4 -0
  7. package/build/api/src/data/model/application_error.d.ts +17 -2
  8. package/build/api/src/domain/change_size_reconciliation.d.ts +15 -0
  9. package/build/api/src/domain/copilot_command.d.ts +25 -0
  10. package/build/api/src/domain/copilot_command_validation.d.ts +10 -0
  11. package/build/api/src/domain/copilot_command_vocabulary.d.ts +3 -0
  12. package/build/api/src/domain/issue_workflow_diagnostic.d.ts +10 -0
  13. package/build/api/src/domain/issue_workflow_field_mapping.d.ts +10 -0
  14. package/build/api/src/domain/issue_workflow_profile.d.ts +67 -0
  15. package/build/api/src/domain/progress_binary_evidence_diagnostic.d.ts +9 -0
  16. package/build/api/src/domain/verification_failure.d.ts +15 -0
  17. package/build/cli/index.js +4299 -531
  18. package/build/deployment_recovery/index.js +41863 -0
  19. package/build/github_action/index.js +4877 -551
  20. package/deployment-recovery/action.yml +52 -0
  21. package/deployment-recovery/index.cjs +3 -0
  22. package/issue-admission/action.yml +41 -0
  23. package/issue-admission/index.cjs +13 -0
  24. package/package.json +19 -8
  25. package/setup/workflows/copilot_commit.yml +2 -0
  26. package/setup/workflows/copilot_issue.yml +57 -1
  27. package/setup/workflows/hotfix_workflow.yml +151 -7
  28. package/setup/workflows/release_workflow.yml +151 -7
@@ -0,0 +1,52 @@
1
+ name: Inspect unpublished candidate replacement
2
+ description: Read-only native prerequisite and replay inspection before project preparation.
3
+ inputs:
4
+ token:
5
+ description: Existing product token; this action performs only native reads.
6
+ required: true
7
+ issue:
8
+ description: Positive launcher issue.
9
+ required: true
10
+ operation-id:
11
+ description: Exact previous deployment operation.
12
+ required: true
13
+ expected-revision:
14
+ description: Exact previous durable revision.
15
+ required: true
16
+ version:
17
+ description: Exact unpublished version.
18
+ required: true
19
+ source-sha:
20
+ description: Reviewed corrected source before complete preparation.
21
+ required: true
22
+ confirm-unpublished:
23
+ description: Explicit maintainer confirmation after inspecting every project registry.
24
+ default: 'false'
25
+ issue-workflow-profile:
26
+ description: Same enabled workflows and field mappings as the ordinary preparation action.
27
+ default: ''
28
+ release-label:
29
+ description: Configured release-kind label.
30
+ default: release
31
+ hotfix-label:
32
+ description: Configured hotfix-kind label.
33
+ default: hotfix
34
+ repository-locale:
35
+ description: Repository Summary language; English and Spanish have static catalogs.
36
+ default: en-US
37
+ outputs:
38
+ mode:
39
+ description: prepare requires full validation; recover reuses the recorded candidate; recorded does not mutate.
40
+ source-branch:
41
+ description: Native operation-owned source branch.
42
+ candidate-sha:
43
+ description: Reviewed source for new preparation, or existing validated successor for recovery.
44
+ production-base-sha:
45
+ description: Native base for fresh replacement or the recorded replacement base for recovery.
46
+ operation-id:
47
+ description: Exact previous operation.
48
+ expected-revision:
49
+ description: Exact previous revision.
50
+ runs:
51
+ using: node24
52
+ main: index.cjs
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ void require('../build/deployment_recovery/index.js').runDeploymentCandidatePreflightAction();
@@ -0,0 +1,41 @@
1
+ name: Copilot issue event admission
2
+ description: Read-only issue admission and successful-reconciliation receipts, without agent setup.
3
+ inputs:
4
+ issue-event-admission-mode:
5
+ description: Isolated mode, inspect before work or record after successful work.
6
+ required: true
7
+ token:
8
+ description: Existing product credential for native identity and issue reads.
9
+ required: true
10
+ configuration:
11
+ description: Repository Variables snapshot used only in a credential-free state digest.
12
+ default: ''
13
+ repository-locale:
14
+ description: Job Summary locale, English or Spanish with regional and English fallback.
15
+ default: en-US
16
+ deploy-label:
17
+ description: Deployment intent label; it must never be coalesced.
18
+ default: deploy
19
+ release-label:
20
+ description: Release intent label; must match the normal Action.
21
+ default: release
22
+ hotfix-label:
23
+ description: Hotfix intent label; must match the normal Action.
24
+ default: hotfix
25
+ receipt-key:
26
+ description: Exact digest returned by this run's successful inspect step.
27
+ default: ''
28
+ outputs:
29
+ execute:
30
+ description: True only when the normal guarded issue pipeline should run.
31
+ reason:
32
+ description: Closed admission reason; no issue text or credentials.
33
+ receipt-key:
34
+ description: Opaque native state, workflow and runtime digest.
35
+ receipt-name:
36
+ description: Artifact name after an unchanged successful reconciliation.
37
+ receipt-path:
38
+ description: Private temporary receipt file for native artifact upload.
39
+ runs:
40
+ using: node24
41
+ main: index.cjs
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ // Metadata's required:true is advisory; never fall through to the mutation entry.
4
+ const mode = (process.env['INPUT_ISSUE-EVENT-ADMISSION-MODE'] ?? '').trim();
5
+ if (mode !== 'inspect' && mode !== 'record') {
6
+ process.stderr.write('Issue admission requires inspect or record mode. No pipeline was started.\n');
7
+ process.exitCode = 1;
8
+ } else {
9
+ // This isolated action has no JSON input-override channel or native worker mode.
10
+ delete process.env.INPUT_VARS_JSON;
11
+ process.env.COPILOT_ISSUE_ADMISSION_BOOTSTRAP = '1';
12
+ void require('../build/github_action/index.js').runGitHubActionEntry();
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vypdev/copilot",
3
- "version": "3.10.5",
3
+ "version": "3.10.6",
4
4
  "packageManager": "pnpm@10.12.4",
5
5
  "pnpm": {
6
6
  "overrides": {
@@ -34,9 +34,12 @@
34
34
  },
35
35
  "files": [
36
36
  "action.yml",
37
+ "issue-admission/",
38
+ "deployment-recovery/",
37
39
  "build/cli/index.js",
38
40
  "build/web/",
39
41
  "build/github_action/index.js",
42
+ "build/deployment_recovery/index.js",
40
43
  "build/api/index.js",
41
44
  "build/api/src/",
42
45
  "setup/ISSUE_TEMPLATE/",
@@ -55,7 +58,7 @@
55
58
  "node": ">=24"
56
59
  },
57
60
  "scripts": {
58
- "build": "node scripts/clean-generated-bundles.cjs && pnpm run build:web && ncc build src/actions/github_action.ts -o build/github_action && ncc build src/cli.ts -o build/cli && ncc build src/api.ts -o build/api && node scripts/prepare-generated-bundles.cjs && node scripts/set-cli-executable.cjs",
61
+ "build": "node scripts/clean-generated-bundles.cjs && pnpm run build:web && ncc build src/actions/github_action.ts -o build/github_action && ncc build src/actions/deployment_candidate_preflight_action.ts -o build/deployment_recovery && ncc build src/cli.ts -o build/cli && ncc build src/api.ts -o build/api && node scripts/prepare-generated-bundles.cjs && node scripts/set-cli-executable.cjs && pnpm run build:extension",
59
62
  "build:web": "vite build --config web/vite.config.mts",
60
63
  "check:web": "svelte-check --tsconfig web/tsconfig.json && vite build --config web/vite.config.mts",
61
64
  "validate:build": "node scripts/validate-build.cjs",
@@ -63,20 +66,27 @@
63
66
  "smoke:npm-package": "node scripts/smoke-test-npm-package.cjs",
64
67
  "test": "jest",
65
68
  "test:watch": "jest --watch",
66
- "test:coverage": "jest --coverage --json --outputFile=coverage/setup-acceptance-jest.json && node scripts/validate-coverage-budgets.cjs && node scripts/validate-setup-assistant-acceptance.cjs coverage/setup-acceptance-jest.json",
69
+ "test:coverage": "jest --coverage --json --outputFile=coverage/setup-acceptance-jest.json && node scripts/validate-coverage-budgets.cjs && node scripts/validate-setup-assistant-acceptance.cjs coverage/setup-acceptance-jest.json && pnpm run test:extension",
67
70
  "eval:bugbot": "jest src/tooling/__tests__/bugbot_quality_eval.test.ts src/tooling/__tests__/bugbot_benchmark.test.ts --runInBand",
68
- "typecheck": "tsc --noEmit",
71
+ "typecheck": "tsc --noEmit && pnpm run typecheck:extension",
69
72
  "metrics:architecture": "node scripts/collect-architecture-metrics.cjs",
70
- "lint": "eslint src",
73
+ "lint": "eslint src extension",
71
74
  "validate:agent-docs": "node scripts/validate-agent-documentation.cjs",
72
75
  "validate:docs-page": "node scripts/validate-docs-page-assets.cjs",
73
76
  "validate:documentation": "node scripts/validate-documentation-contract.cjs",
74
77
  "validate:workflows": "node scripts/validate-workflow-contract.cjs",
75
- "generate:specifications": "node scripts/validate-specification-catalog.cjs --write && node scripts/validate-github-communication-test-budget.cjs",
76
- "validate:specifications": "node scripts/validate-specification-catalog.cjs && node scripts/validate-github-communication-test-budget.cjs",
78
+ "generate:specifications": "node scripts/validate-specification-catalog.cjs --write && node scripts/validate-github-communication-test-budget.cjs && pnpm run validate:browser-extension-specifications",
79
+ "validate:specifications": "node scripts/validate-specification-catalog.cjs && node scripts/validate-github-communication-test-budget.cjs && pnpm run validate:browser-extension-specifications",
77
80
  "validate:setup-acceptance": "node scripts/validate-setup-assistant-acceptance.cjs",
78
81
  "lint:fix": "eslint src --fix",
79
- "postinstall": "node scripts/install-git-hooks.cjs"
82
+ "postinstall": "node scripts/install-git-hooks.cjs",
83
+ "validate:browser-extension-specifications": "node --test test-support/browser-extension-specifications.test.cjs && node scripts/validate-browser-extension-specifications.cjs",
84
+ "build:extension": "pnpm --dir extension run build",
85
+ "typecheck:extension": "pnpm --dir extension run typecheck",
86
+ "test:extension": "pnpm --dir extension run test",
87
+ "validate:extension": "pnpm --dir extension run validate",
88
+ "test:browsers:extension": "pnpm --dir extension run test:browsers",
89
+ "build:safari:extension": "pnpm --dir extension run build:safari"
80
90
  },
81
91
  "keywords": [],
82
92
  "author": "Efra Espada",
@@ -98,6 +108,7 @@
98
108
  "@types/jest": "^30.0.0",
99
109
  "@types/node": "^22.9.1",
100
110
  "@vercel/ncc": "^0.36.1",
111
+ "ajv": "6.15.0",
101
112
  "eslint": "^10.10.0",
102
113
  "jest": "^30.5.1",
103
114
  "svelte": "^5.57.1",
@@ -19,6 +19,7 @@ jobs:
19
19
  group: copilot-push-${{ github.repository }}-${{ github.ref_name }}
20
20
  cancel-in-progress: true
21
21
  permissions:
22
+ checks: write
22
23
  contents: read
23
24
  steps:
24
25
  - name: Checkout Repository
@@ -104,6 +105,7 @@ jobs:
104
105
  fixer-executable: ${{ vars.FIXER_EXECUTABLE }}
105
106
  token: ${{ secrets.PAT }}
106
107
  env:
108
+ COPILOT_EVIDENCE_TOKEN: ${{ github.token }}
107
109
  AGENT_PROVIDER: ${{ vars.AGENT_PROVIDER || 'codex' }}
108
110
  AGENT_MODEL_PROVIDER: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }}
109
111
  AGENT_MODEL: ${{ vars.AGENT_MODEL }}
@@ -4,9 +4,40 @@ on:
4
4
  issues:
5
5
  types: [opened, reopened, closed, edited, labeled, unlabeled, assigned, unassigned]
6
6
 
7
+ # Keep every pending lifecycle event; never replace a start/reopen with a label event.
8
+ # GitHub bounds this queue at 100 pending runs. Independent issues use different groups.
9
+ concurrency:
10
+ group: copilot-issue-${{ github.repository }}-${{ github.event.issue.number }}
11
+ cancel-in-progress: false
12
+ queue: max
13
+
7
14
  jobs:
15
+ issue-admission:
16
+ name: Admit issue event
17
+ runs-on: ubuntu-latest
18
+ timeout-minutes: 5
19
+ permissions:
20
+ contents: read
21
+ actions: read
22
+ outputs:
23
+ execute: ${{ steps.inspect.outputs.execute }}
24
+ receipt-key: ${{ steps.inspect.outputs.receipt-key }}
25
+ steps:
26
+ - name: Inspect native issue event
27
+ id: inspect
28
+ uses: vypdev/copilot/issue-admission@v3
29
+ with:
30
+ repository-locale: ${{ vars.REPOSITORY_LOCALE || 'en-US' }}
31
+ issue-event-admission-mode: inspect
32
+ token: ${{ secrets.PAT }}
33
+ configuration: ${{ format('[{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12},{13},{14},{15},{16},{17},{18},{19},{20},{21},{22},{23},{24},{25},{26},{27},{28},{29},{30},{31},{32},{33},{34},{35},{36},{37},{38},{39},{40},{41},{42},{43},{44},{45},{46},{47},{48},{49},{50},{51},{52},{53},{54},{55},{56},{57},{58},{59},{60},{61},{62},{63},{64}]', toJSON(vars.AGENT_ALLOWED_MODELS), toJSON(vars.AGENT_ALLOWED_MODEL_PROVIDERS), toJSON(vars.AGENT_EFFORT), toJSON(vars.AGENT_EXECUTABLE), toJSON(vars.AGENT_MODEL), toJSON(vars.AGENT_MODEL_PROVIDER), toJSON(vars.AGENT_PROVIDER), toJSON(vars.AI_IGNORE_FILES), toJSON(vars.AI_INCLUDE_REASONING), toJSON(vars.AI_MEMBERS_ONLY), toJSON(vars.AI_PULL_REQUEST_DESCRIPTION_MODE), toJSON(vars.BUGBOT_AUTOFIX_VERIFY_COMMANDS), toJSON(vars.BUGBOT_COMMENT_LIMIT), toJSON(vars.BUGBOT_DRY_RUN), toJSON(vars.BUGBOT_EFFORT), toJSON(vars.BUGBOT_FAIL_ON_UNRESOLVED), toJSON(vars.BUGBOT_ORGANIZATION_RULES), toJSON(vars.BUGBOT_REVIEW_DRAFTS), toJSON(vars.BUGBOT_SEVERITY), toJSON(vars.BUGBOT_SUGGESTED_CHANGES), toJSON(vars.BUGBOT_TELEMETRY), toJSON(vars.BUGBOT_TRACE_RULES), toJSON(vars.BUGFIX_TREE), toJSON(vars.CHORE_TREE), toJSON(vars.COMMIT_PREFIX_TRANSFORMS), toJSON(vars.COPILOT_ISSUE_WORKFLOW_PROFILE), toJSON(vars.DEBUG), toJSON(vars.DEPLOY_LABEL), toJSON(vars.DESIRED_ASSIGNEES_COUNT), toJSON(vars.DESIRED_REVIEWERS_COUNT), toJSON(vars.DEVELOPMENT_BRANCH), toJSON(vars.DOCS_TREE), toJSON(vars.FEATURE_TREE), toJSON(vars.FINDINGS_EFFORT), toJSON(vars.FINDINGS_EXECUTABLE), toJSON(vars.FINDINGS_MODEL), toJSON(vars.FINDINGS_MODEL_PROVIDER), toJSON(vars.FINDINGS_PROVIDER), toJSON(vars.FIXER_EFFORT), toJSON(vars.FIXER_EXECUTABLE), toJSON(vars.FIXER_MODEL), toJSON(vars.FIXER_MODEL_PROVIDER), toJSON(vars.FIXER_PROVIDER), toJSON(vars.HOTFIX_TREE), toJSON(vars.ISSUES_LOCALE), toJSON(vars.ISSUE_MANAGED_BRANCHES), toJSON(vars.MAIN_BRANCH), toJSON(vars.PLANNER_EFFORT), toJSON(vars.PLANNER_EXECUTABLE), toJSON(vars.PLANNER_MODEL), toJSON(vars.PLANNER_MODEL_PROVIDER), toJSON(vars.PLANNER_PROVIDER), toJSON(vars.PRE_BRANCH_SDD), toJSON(vars.PROJECT_COLUMN_ISSUE_CREATED), toJSON(vars.PROJECT_COLUMN_ISSUE_IN_PROGRESS), toJSON(vars.PROJECT_COLUMN_PULL_REQUEST_CREATED), toJSON(vars.PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS), toJSON(vars.PROJECT_IDS), toJSON(vars.PULL_REQUESTS_LOCALE), toJSON(vars.RELEASE_TREE), toJSON(vars.REOPEN_ISSUE_ON_PUSH), toJSON(vars.REPOSITORY_LOCALE), toJSON(vars.REVIEWER_PROVIDER), toJSON(vars.TESTER_PROVIDER), toJSON(vars.VERIFICATION_SETUP)) }}
34
+ deploy-label: ${{ vars.DEPLOY_LABEL || 'deploy' }}
35
+ env:
36
+ COPILOT_ISSUE_ADMISSION_TOKEN: ${{ github.token }}
37
+
8
38
  copilot-issues:
9
- if: ${{ (github.event.action == 'closed' && github.event.issue.state == 'closed') || vars.COPILOT_BOT_LOGIN == '' || github.actor != vars.COPILOT_BOT_LOGIN }}
39
+ needs: issue-admission
40
+ if: ${{ needs.issue-admission.outputs.execute == 'true' }}
10
41
  name: Copilot - Issue
11
42
  runs-on: ubuntu-latest
12
43
  timeout-minutes: 120
@@ -24,7 +55,9 @@ jobs:
24
55
  # Optional trusted-main setup: VERIFICATION_SETUP=true and .github/actions/copilot-verification-setup/action.yml.
25
56
  # https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites
26
57
  - uses: vypdev/copilot@v3
58
+ id: copilot
27
59
  with:
60
+ deploy-label: ${{ vars.DEPLOY_LABEL || 'deploy' }}
28
61
  repository-locale: ${{ vars.REPOSITORY_LOCALE || 'en-US' }}
29
62
  issues-locale: ${{ vars.ISSUES_LOCALE || '' }}
30
63
  pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || '' }}
@@ -101,3 +134,26 @@ jobs:
101
134
  OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
102
135
  CURSOR_API_KEY: ${{ (vars.AGENT_PROVIDER == 'cursor' || vars.FINDINGS_PROVIDER == 'cursor' || vars.FIXER_PROVIDER == 'cursor' || vars.PLANNER_PROVIDER == 'cursor' || vars.REVIEWER_PROVIDER == 'cursor' || vars.TESTER_PROVIDER == 'cursor') && secrets.CURSOR_API_KEY || '' }}
103
136
  CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }}
137
+
138
+ - name: Record unchanged successful reconciliation
139
+ id: receipt
140
+ if: ${{ success() && steps.copilot.outcome == 'success' && needs.issue-admission.outputs.receipt-key != '' }}
141
+ continue-on-error: true
142
+ uses: vypdev/copilot/issue-admission@v3
143
+ with:
144
+ repository-locale: ${{ vars.REPOSITORY_LOCALE || 'en-US' }}
145
+ issue-event-admission-mode: record
146
+ token: ${{ secrets.PAT }}
147
+ configuration: ${{ format('[{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12},{13},{14},{15},{16},{17},{18},{19},{20},{21},{22},{23},{24},{25},{26},{27},{28},{29},{30},{31},{32},{33},{34},{35},{36},{37},{38},{39},{40},{41},{42},{43},{44},{45},{46},{47},{48},{49},{50},{51},{52},{53},{54},{55},{56},{57},{58},{59},{60},{61},{62},{63},{64}]', toJSON(vars.AGENT_ALLOWED_MODELS), toJSON(vars.AGENT_ALLOWED_MODEL_PROVIDERS), toJSON(vars.AGENT_EFFORT), toJSON(vars.AGENT_EXECUTABLE), toJSON(vars.AGENT_MODEL), toJSON(vars.AGENT_MODEL_PROVIDER), toJSON(vars.AGENT_PROVIDER), toJSON(vars.AI_IGNORE_FILES), toJSON(vars.AI_INCLUDE_REASONING), toJSON(vars.AI_MEMBERS_ONLY), toJSON(vars.AI_PULL_REQUEST_DESCRIPTION_MODE), toJSON(vars.BUGBOT_AUTOFIX_VERIFY_COMMANDS), toJSON(vars.BUGBOT_COMMENT_LIMIT), toJSON(vars.BUGBOT_DRY_RUN), toJSON(vars.BUGBOT_EFFORT), toJSON(vars.BUGBOT_FAIL_ON_UNRESOLVED), toJSON(vars.BUGBOT_ORGANIZATION_RULES), toJSON(vars.BUGBOT_REVIEW_DRAFTS), toJSON(vars.BUGBOT_SEVERITY), toJSON(vars.BUGBOT_SUGGESTED_CHANGES), toJSON(vars.BUGBOT_TELEMETRY), toJSON(vars.BUGBOT_TRACE_RULES), toJSON(vars.BUGFIX_TREE), toJSON(vars.CHORE_TREE), toJSON(vars.COMMIT_PREFIX_TRANSFORMS), toJSON(vars.COPILOT_ISSUE_WORKFLOW_PROFILE), toJSON(vars.DEBUG), toJSON(vars.DEPLOY_LABEL), toJSON(vars.DESIRED_ASSIGNEES_COUNT), toJSON(vars.DESIRED_REVIEWERS_COUNT), toJSON(vars.DEVELOPMENT_BRANCH), toJSON(vars.DOCS_TREE), toJSON(vars.FEATURE_TREE), toJSON(vars.FINDINGS_EFFORT), toJSON(vars.FINDINGS_EXECUTABLE), toJSON(vars.FINDINGS_MODEL), toJSON(vars.FINDINGS_MODEL_PROVIDER), toJSON(vars.FINDINGS_PROVIDER), toJSON(vars.FIXER_EFFORT), toJSON(vars.FIXER_EXECUTABLE), toJSON(vars.FIXER_MODEL), toJSON(vars.FIXER_MODEL_PROVIDER), toJSON(vars.FIXER_PROVIDER), toJSON(vars.HOTFIX_TREE), toJSON(vars.ISSUES_LOCALE), toJSON(vars.ISSUE_MANAGED_BRANCHES), toJSON(vars.MAIN_BRANCH), toJSON(vars.PLANNER_EFFORT), toJSON(vars.PLANNER_EXECUTABLE), toJSON(vars.PLANNER_MODEL), toJSON(vars.PLANNER_MODEL_PROVIDER), toJSON(vars.PLANNER_PROVIDER), toJSON(vars.PRE_BRANCH_SDD), toJSON(vars.PROJECT_COLUMN_ISSUE_CREATED), toJSON(vars.PROJECT_COLUMN_ISSUE_IN_PROGRESS), toJSON(vars.PROJECT_COLUMN_PULL_REQUEST_CREATED), toJSON(vars.PROJECT_COLUMN_PULL_REQUEST_IN_PROGRESS), toJSON(vars.PROJECT_IDS), toJSON(vars.PULL_REQUESTS_LOCALE), toJSON(vars.RELEASE_TREE), toJSON(vars.REOPEN_ISSUE_ON_PUSH), toJSON(vars.REPOSITORY_LOCALE), toJSON(vars.REVIEWER_PROVIDER), toJSON(vars.TESTER_PROVIDER), toJSON(vars.VERIFICATION_SETUP)) }}
148
+ deploy-label: ${{ vars.DEPLOY_LABEL || 'deploy' }}
149
+ receipt-key: ${{ needs.issue-admission.outputs.receipt-key }}
150
+
151
+ - name: Retain successful reconciliation receipt
152
+ if: ${{ steps.receipt.outputs.receipt-path != '' }}
153
+ continue-on-error: true
154
+ uses: actions/upload-artifact@v5
155
+ with:
156
+ name: ${{ steps.receipt.outputs.receipt-name }}
157
+ path: ${{ steps.receipt.outputs.receipt-path }}
158
+ retention-days: 1
159
+ if-no-files-found: error
@@ -8,11 +8,25 @@ on:
8
8
  required: true
9
9
  default: prepare
10
10
  type: choice
11
+ # Add replace only after configuring the complete project verifier and enabling it below.
11
12
  options: [prepare, publish, resume]
12
13
  operation-id:
13
14
  description: 'Internal durable operation identifier'
14
15
  required: false
15
16
  default: ''
17
+ expected-revision:
18
+ description: 'Current durable revision; required only for replace'
19
+ required: false
20
+ default: ''
21
+ source-sha:
22
+ description: 'Reviewed corrected source before complete replacement preparation'
23
+ required: false
24
+ default: ''
25
+ confirm-unpublished:
26
+ description: 'Confirm the exact version is unpublished in every project registry'
27
+ type: boolean
28
+ required: false
29
+ default: false
16
30
  version:
17
31
  description: 'Hotfix version (MAJOR.MINOR.PATCH)'
18
32
  required: false
@@ -50,6 +64,11 @@ jobs:
50
64
  ISSUE: ${{ inputs.issue }}
51
65
  TITLE: ${{ inputs.title }}
52
66
  CHANGELOG: ${{ inputs.changelog }}
67
+ EXPECTED_REVISION: ${{ inputs.expected-revision }}
68
+ SOURCE_SHA: ${{ inputs.source-sha }}
69
+ CONFIRM_UNPUBLISHED: ${{ inputs.confirm-unpublished }}
70
+ # Workflow-owned opt-in; never supplied by an issue or dispatch input.
71
+ REPLACEMENT_ENABLED: 'false'
53
72
  with:
54
73
  script: |
55
74
  const mode = process.env.MODE;
@@ -59,6 +78,10 @@ jobs:
59
78
  const issue = Number(issueText);
60
79
  const title = process.env.TITLE || '';
61
80
  const changelog = process.env.CHANGELOG || '';
81
+ const revisionText = process.env.EXPECTED_REVISION || '';
82
+ const revision = Number(revisionText);
83
+ const sourceSha = process.env.SOURCE_SHA || '';
84
+ const confirmed = process.env.CONFIRM_UNPUBLISHED || 'false';
62
85
  const errors = [];
63
86
  if (!/^[1-9][0-9]*$/.test(issueText) || !Number.isSafeInteger(issue) || issue < 1) errors.push('Issue must be a positive safe integer.');
64
87
  if (!/^[0-9]+\.[0-9]+\.[0-9]+$/.test(version)) errors.push('Version must use MAJOR.MINOR.PATCH.');
@@ -66,25 +89,89 @@ jobs:
66
89
  if (operationId) errors.push('Operation ID must be empty in prepare mode.');
67
90
  if (!title || title.length > 1000) errors.push('Title must contain 1-1000 characters.');
68
91
  if (!changelog || changelog.length > 50000) errors.push('Changelog must contain 1-50000 characters.');
92
+ } else if (mode === 'replace') {
93
+ if (process.env.REPLACEMENT_ENABLED !== 'true') errors.push('Candidate replacement is disabled. Configure the complete project verifier, enable it in this workflow and then add replace to the choices.');
94
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{7,127}$/.test(operationId)) errors.push('Operation ID is invalid.');
95
+ if (!/^[1-9][0-9]*$/.test(revisionText) || !Number.isSafeInteger(revision) || revision < 1 || revision >= Number.MAX_SAFE_INTEGER) errors.push('Replacement revision is invalid.');
96
+ if (!/^[a-f0-9]{40}$/.test(sourceSha)) errors.push('Replacement source must be a full reviewed SHA.');
97
+ if (version.length > 64 || confirmed !== 'true') errors.push('Confirm the exact unpublished replacement version.');
98
+ if (title || changelog) errors.push('Replacement preserves the stored title and changelog.');
69
99
  } else if (mode === 'publish' || mode === 'resume') {
70
100
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{7,127}$/.test(operationId)) errors.push('Operation ID is invalid.');
71
101
  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.');
102
+ } else errors.push('Mode must be prepare, replace, publish, or resume.');
103
+ if (mode !== 'replace' && (revisionText || sourceSha || confirmed !== 'false')) errors.push('Replacement proof inputs are allowed only in replace mode.');
73
104
  if (errors.length) core.setFailed(errors.join(' '));
74
105
 
106
+ candidate-preflight:
107
+ name: Inspect unpublished candidate replacement
108
+ if: ${{ inputs.mode == 'replace' }}
109
+ runs-on: ubuntu-latest
110
+ needs: validate
111
+ timeout-minutes: 5
112
+ permissions:
113
+ contents: read
114
+ issues: read
115
+ pull-requests: read
116
+ actions: read
117
+ outputs:
118
+ mode: ${{ steps.inspect.outputs.mode }}
119
+ source-branch: ${{ steps.inspect.outputs.source-branch }}
120
+ candidate-sha: ${{ steps.inspect.outputs.candidate-sha }}
121
+ production-base-sha: ${{ steps.inspect.outputs.production-base-sha }}
122
+ operation-id: ${{ steps.inspect.outputs.operation-id }}
123
+ expected-revision: ${{ steps.inspect.outputs.expected-revision }}
124
+ steps:
125
+ - name: Read native candidate authority
126
+ id: inspect
127
+ uses: vypdev/copilot/deployment-recovery@v3
128
+ with:
129
+ issue: ${{ inputs.issue }}
130
+ operation-id: ${{ inputs.operation-id }}
131
+ expected-revision: ${{ inputs.expected-revision }}
132
+ version: ${{ inputs.version }}
133
+ source-sha: ${{ inputs.source-sha }}
134
+ confirm-unpublished: ${{ inputs.confirm-unpublished }}
135
+ issue-workflow-profile: ${{ vars.COPILOT_ISSUE_WORKFLOW_PROFILE || '' }}
136
+ release-label: release
137
+ hotfix-label: hotfix
138
+ repository-locale: ${{ vars.REPOSITORY_LOCALE || 'en-US' }}
139
+ token: ${{ secrets.PAT }}
140
+
75
141
  prepare-version-files:
76
142
  name: Prepare and validate hotfix
77
- if: ${{ inputs.mode == 'prepare' }}
143
+ if: ${{ !cancelled() && needs.validate.result == 'success' && (inputs.mode == 'prepare' || (inputs.mode == 'replace' && needs.candidate-preflight.result == 'success' && needs.candidate-preflight.outputs.mode == 'prepare')) }}
78
144
  runs-on: ubuntu-latest
79
- needs: validate
145
+ needs: [validate, candidate-preflight]
80
146
  timeout-minutes: 30
81
147
  permissions:
82
148
  contents: write
149
+ outputs:
150
+ candidate-sha: ${{ steps.candidate-identity.outputs.candidate-sha }}
83
151
  steps:
84
152
  - uses: actions/checkout@v5
85
153
  with:
86
154
  persist-credentials: true
155
+ ref: ${{ needs.candidate-preflight.outputs.source-branch || github.ref }}
156
+ - name: Verify reviewed replacement source
157
+ if: ${{ inputs.mode == 'replace' }}
158
+ uses: actions/github-script@v9
159
+ env:
160
+ EXPECTED_SHA: ${{ inputs.source-sha }}
161
+ with:
162
+ script: |
163
+ const { execFileSync } = require('node:child_process');
164
+ const current = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
165
+ if (!/^[a-f0-9]{40}$/.test(process.env.EXPECTED_SHA || '') || current !== process.env.EXPECTED_SHA) {
166
+ core.setFailed('The reviewed replacement source moved before preparation. No new candidate proof is available.');
167
+ }
87
168
  # Add project-specific version, build, validation, smoke-test and generated-file steps here.
169
+ - name: Complete fresh replacement verification
170
+ id: replacement-verification
171
+ if: ${{ inputs.mode == 'replace' }}
172
+ run: |
173
+ echo "Configure this step with every required project validator, including registry vacancy, before enabling candidate replacement."
174
+ exit 1
88
175
  - name: Commit prepared hotfix files
89
176
  uses: EndBug/add-and-commit@v9
90
177
  with:
@@ -93,12 +180,29 @@ jobs:
93
180
  committer_email: actions@github.com
94
181
  default_author: user_info
95
182
  message: 'gh-action: prepare hotfix ${{ inputs.version }}'
183
+ - name: Confirm prepared replacement identity
184
+ id: candidate-identity
185
+ if: ${{ inputs.mode == 'replace' }}
186
+ uses: actions/github-script@v9
187
+ env:
188
+ SOURCE_BRANCH: ${{ needs.candidate-preflight.outputs.source-branch }}
189
+ VERIFICATION_RESULT: ${{ steps.replacement-verification.outcome }}
190
+ with:
191
+ script: |
192
+ if (process.env.VERIFICATION_RESULT !== 'success') throw new Error('Complete replacement verification was not confirmed.');
193
+ const { execFileSync } = require('node:child_process');
194
+ const candidate = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
195
+ const dirty = execFileSync('git', ['status', '--porcelain'], { encoding: 'utf8' }).trim();
196
+ if (!/^[a-f0-9]{40}$/.test(candidate) || !process.env.SOURCE_BRANCH || dirty) throw new Error('The prepared candidate is not an exact clean commit.');
197
+ const response = await github.rest.git.getRef({ ...context.repo, ref: `heads/${process.env.SOURCE_BRANCH}` });
198
+ if (response.data.object?.type !== 'commit' || response.data.object.sha !== candidate) throw new Error('The prepared source moved before candidate proof was recorded.');
199
+ core.setOutput('candidate-sha', candidate);
96
200
 
97
201
  promote:
98
202
  name: Open production promotion
99
- if: ${{ inputs.mode == 'prepare' }}
203
+ if: ${{ !cancelled() && needs.validate.result == 'success' && ((inputs.mode == 'prepare' && needs.prepare-version-files.result == 'success') || (inputs.mode == 'replace' && needs.candidate-preflight.result == 'success' && ((needs.candidate-preflight.outputs.mode == 'prepare' && needs.prepare-version-files.result == 'success' && needs.prepare-version-files.outputs.candidate-sha != '') || needs.candidate-preflight.outputs.mode == 'recover'))) }}
100
204
  runs-on: ubuntu-latest
101
- needs: prepare-version-files
205
+ needs: [validate, candidate-preflight, prepare-version-files]
102
206
  timeout-minutes: 10
103
207
  permissions:
104
208
  contents: read
@@ -109,7 +213,9 @@ jobs:
109
213
  - uses: actions/checkout@v5
110
214
  with:
111
215
  persist-credentials: false
216
+ ref: ${{ inputs.mode == 'replace' && (needs.candidate-preflight.outputs.mode == 'recover' && needs.candidate-preflight.outputs.candidate-sha || needs.prepare-version-files.outputs.candidate-sha) || github.ref }}
112
217
  - name: Create or resume promotion PR
218
+ if: ${{ inputs.mode == 'prepare' }}
113
219
  uses: vypdev/copilot@v3
114
220
  with:
115
221
  single-action: prepare_deployment_action
@@ -140,6 +246,44 @@ jobs:
140
246
  issues-locale: ${{ vars.ISSUES_LOCALE || '' }}
141
247
  pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || '' }}
142
248
  token: ${{ secrets.PAT }}
249
+ - name: Replace or recover unpublished candidate
250
+ if: ${{ inputs.mode == 'replace' }}
251
+ uses: vypdev/copilot@v3
252
+ with:
253
+ single-action: replace_deployment_action
254
+ single-action-operation-id: ${{ needs.candidate-preflight.outputs.operation-id }}
255
+ single-action-expected-revision: ${{ needs.candidate-preflight.outputs.expected-revision }}
256
+ single-action-reviewed-source-sha: ${{ inputs.source-sha }}
257
+ single-action-candidate-sha: ${{ needs.candidate-preflight.outputs.mode == 'recover' && needs.candidate-preflight.outputs.candidate-sha || needs.prepare-version-files.outputs.candidate-sha }}
258
+ single-action-production-base-sha: ${{ needs.candidate-preflight.outputs.production-base-sha }}
259
+ single-action-confirm-unpublished: ${{ inputs.confirm-unpublished }}
260
+ single-action-issue: ${{ inputs.issue }}
261
+ issue-workflow-profile: ${{ vars.COPILOT_ISSUE_WORKFLOW_PROFILE || '' }}
262
+ single-action-version: ${{ inputs.version }}
263
+ single-action-title: ${{ inputs.title }}
264
+ single-action-changelog: ${{ inputs.changelog }}
265
+ main-branch: ${{ vars.MAIN_BRANCH || 'master' }}
266
+ development-branch: ${{ vars.DEVELOPMENT_BRANCH || 'develop' }}
267
+ release-tree: ${{ vars.RELEASE_TREE || 'release' }}
268
+ hotfix-tree: ${{ vars.HOTFIX_TREE || 'hotfix' }}
269
+ release-workflow: ${{ vars.RELEASE_WORKFLOW || 'release_workflow.yml' }}
270
+ hotfix-workflow: ${{ vars.HOTFIX_WORKFLOW || 'hotfix_workflow.yml' }}
271
+ release-reconciliation-strategy: ${{ vars.RELEASE_RECONCILIATION_STRATEGY || 'production-lineage' }}
272
+ hotfix-reconciliation-strategy: ${{ vars.HOTFIX_RECONCILIATION_STRATEGY || 'production-lineage' }}
273
+ reconciliation-pr-mode: ${{ vars.RECONCILIATION_PR_MODE || 'auto' }}
274
+ merge-queue-check-attestations: ${{ vars.MERGE_QUEUE_CHECK_ATTESTATIONS || '[]' }}
275
+ reconciliation-backmerge-mode: ${{ vars.RECONCILIATION_BACKMERGE_MODE || 'auto' }}
276
+ hotfix-active-release-policy: ${{ vars.HOTFIX_ACTIVE_RELEASE_POLICY || 'prefer-release' }}
277
+ reconciliation-tree: ${{ vars.RECONCILIATION_TREE || 'sync' }}
278
+ reconciliation-cleanup: ${{ vars.RECONCILIATION_CLEANUP || 'all' }}
279
+ reconciliation-issue-completion: ${{ vars.RECONCILIATION_ISSUE_COMPLETION || 'close' }}
280
+ orchestration-presentation-mode: ${{ vars.ORCHESTRATION_PRESENTATION_MODE || 'guided' }}
281
+ orchestration-diagrams: ${{ vars.ORCHESTRATION_DIAGRAMS || 'true' }}
282
+ orchestration-comment-mode: ${{ vars.ORCHESTRATION_COMMENT_MODE || 'update' }}
283
+ repository-locale: ${{ vars.REPOSITORY_LOCALE || 'en-US' }}
284
+ issues-locale: ${{ vars.ISSUES_LOCALE || '' }}
285
+ pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || '' }}
286
+ token: ${{ secrets.PAT }}
143
287
 
144
288
  publish:
145
289
  name: Publish accepted production commit
@@ -239,8 +383,8 @@ jobs:
239
383
  name: Report orchestration failure
240
384
  runs-on: ubuntu-latest
241
385
  timeout-minutes: 5
242
- needs: [validate, prepare-version-files, promote, publish, resume-reconciliation]
243
- if: ${{ failure() && needs.validate.result == 'success' }}
386
+ needs: [validate, candidate-preflight, prepare-version-files, promote, publish, resume-reconciliation]
387
+ if: ${{ failure() && needs.validate.result == 'success' && inputs.mode != 'replace' }}
244
388
  permissions:
245
389
  issues: write
246
390
  steps: