@copado/agentia-cli 1.3.0 → 1.4.0

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 (62) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +436 -427
  3. package/dist/commands/cicd/cloud/commit.js +4 -3
  4. package/dist/commands/cicd/cloud/promote.js +40 -18
  5. package/dist/commands/cicd/credential/list.js +2 -1
  6. package/dist/commands/cicd/environment/list.js +2 -1
  7. package/dist/commands/cicd/extension-configuration/list.js +2 -1
  8. package/dist/commands/cicd/function/list.js +10 -13
  9. package/dist/commands/cicd/job/list.js +2 -1
  10. package/dist/commands/cicd/job/result-file/get.js +11 -6
  11. package/dist/commands/cicd/pipeline/automation/list.js +2 -1
  12. package/dist/commands/cicd/pipeline/list.js +2 -1
  13. package/dist/commands/cicd/pipeline/quality-gate/list.js +2 -1
  14. package/dist/commands/cicd/project/list.js +2 -1
  15. package/dist/commands/cicd/promotion/list.js +2 -1
  16. package/dist/commands/cicd/promotion/status.js +29 -0
  17. package/dist/commands/cicd/repository/list.js +5 -8
  18. package/dist/commands/cicd/stage/list.js +2 -1
  19. package/dist/commands/cicd/template/list.js +9 -12
  20. package/dist/commands/cicd/work/delete.js +2 -2
  21. package/dist/commands/cicd/work/environment-sync.js +138 -62
  22. package/dist/commands/cicd/work/get.js +2 -2
  23. package/dist/commands/cicd/work/list.js +2 -1
  24. package/dist/commands/cicd/work/test/apex.js +18 -33
  25. package/dist/commands/cicd/work/test.js +114 -9
  26. package/dist/commands/cicd/work/update.js +2 -2
  27. package/dist/lib/defaults-store.js +3 -1
  28. package/dist/lib/function-list.js +3 -0
  29. package/dist/lib/git-worktree.js +4 -1
  30. package/dist/lib/job-get.js +2 -1
  31. package/dist/lib/job-types.js +2 -1
  32. package/dist/lib/list-pagination.js +6 -1
  33. package/dist/lib/local-quality-gates.js +4 -1
  34. package/dist/lib/mcp/testing-browser-tools.js +1 -16
  35. package/dist/lib/mcp/tool-annotations.js +0 -1
  36. package/dist/lib/mcp/tools.js +30 -25
  37. package/dist/lib/mcp/work-tools.js +100 -47
  38. package/dist/lib/promotion-run.js +3 -1
  39. package/dist/lib/promotion-status.js +12 -0
  40. package/dist/lib/quality-gates.js +13 -0
  41. package/dist/lib/repository-list.js +3 -0
  42. package/dist/lib/runtime.js +1 -1
  43. package/dist/lib/stock-quality-gates.js +2 -1
  44. package/dist/lib/template-list.js +3 -0
  45. package/dist/lib/testing-browser-operations.js +1 -2
  46. package/dist/lib/testing-browser-session.js +1 -2
  47. package/dist/lib/work-apex-test-failures.js +2 -1
  48. package/dist/lib/work-environment-sync-ready.js +7 -1
  49. package/dist/lib/work-environment-sync.js +8 -1
  50. package/dist/lib/work-local-run-tests.js +2 -1
  51. package/dist/lib/work-promote-deploy.js +3 -1
  52. package/dist/lib/work-types.js +3 -1
  53. package/oclif.manifest.json +477 -419
  54. package/package.json +1 -1
  55. package/templates/skills/agentia-cicd/SKILL.md +4 -3
  56. package/templates/skills/agentia-cicd/references/jobs.md +3 -2
  57. package/templates/skills/agentia-cicd/references/promotions.md +16 -4
  58. package/templates/skills/agentia-cicd/references/work.md +25 -11
  59. package/templates/skills/agentia-testing/references/browser-testing.md +0 -1
  60. package/templates/skills/agentia-testing/references/tests.md +1 -1
  61. package/vendor/java/var-replace-command.jar +0 -0
  62. package/dist/commands/testing/browser/reports.js +0 -33
@@ -1,18 +1,21 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { CicdCommand } from '../../../lib/cicd-command.js';
3
3
  import { commandStdioCaptureOptions, withCapturedCommandOutput } from '../../../lib/command-output-buffer.js';
4
- import { assertEnvironmentSyncDestOrgSelector, formatWorkEnvironmentSyncResult, performWorkEnvironmentSync, WorkEnvironmentSyncConflictError, WorkEnvironmentSyncValidationError, } from '../../../lib/work-environment-sync.js';
4
+ import { isPromotionRunExecutionError, PROMOTION_RUN_DEFAULT_TIMEOUT_SECONDS, promotionRunErrorJson, waitForPromotionDeploymentExecution, } from '../../../lib/promotion-run.js';
5
+ import { assertEnvironmentSyncDestOrgSelector, ENVIRONMENT_SYNC_DEPLOYMENT_WAIT_MESSAGE, ENVIRONMENT_SYNC_DEPLOYMENTS, formatWorkEnvironmentSyncDeploymentMonitor, formatWorkEnvironmentSyncResult, performWorkEnvironmentSync, shouldWaitForEnvironmentSyncDeployment, WorkEnvironmentSyncConflictError, WorkEnvironmentSyncValidationError, } from '../../../lib/work-environment-sync.js';
5
6
  export default class WorkEnvironmentSync extends CicdCommand {
6
7
  static description = [
7
8
  'Sync committed metadata from the next environment onto the destination environment (or current feature branch).',
8
9
  'Back-promotes every user story behind the destination environment from the next environment branch.',
9
- 'GET user stories ahead and behind. If none are behind, log that and stop. Otherwise create a back-promotion and POST those behind stories onto the promotion.',
10
+ 'On a feature branch, requires published commits first: stops when HEAD is ahead of origin/<feature> and suggests `cicd work publish`.',
11
+ 'GET user stories ahead and behind. If none are behind, log that and stop. Otherwise create a back-promotion via POST /agentia/headless/work/promote-action (PromoteAction with execute flags false) including those behind stories.',
10
12
  'Resolves the next-environment Git branch from --destination-environment or --dev-org-branch (the dest pipeline connection destinationBranch, or that destination environment\'s outbound branch).',
11
- 'Merges origin/<next-env> with --no-ff --no-commit, restores every other path from HEAD, commits the guessed pathspecs,',
12
- 'and pushes the dest org branch (and the feature branch when HEAD starts with feature/).',
13
- 'If guessed pathspecs still conflict, PATCH the back-promotion status to Merge Conflict, store lastEnvironmentSyncName, resolve the files, and re-run with --continue (optional --promotion). The promotion destination environment supplies the dest org branch and the next-env branch.',
14
- 'To discard a conflicted merge, re-run with --abort (optional --promotion); runs git merge --abort and sets the back-promotion status to Draft.',
15
- 'After a successful sync, PATCH the back-promotion status to Completed and clear lastEnvironmentSyncName.',
13
+ 'Creates promotion/<promotionName> from the dest org branch, merges origin/<next-env> pathspecs into that promotion branch (--no-ff --no-commit, restores non-pathspec paths), commits, merges the promotion branch into the dest org branch, and pushes both refs.',
14
+ 'If the run started on a feature branch, checks out that branch\'s changed files (vs lastBaseBranch) from the dest org, commits updates when needed, and tells you to re-check and publish again.',
15
+ 'If guessed pathspecs still conflict, PATCH the back-promotion status to Merge Conflict, store lastEnvironmentSyncName, resolve the files on promotion/<name>, and re-run with --continue (optional --promotion).',
16
+ 'To discard a conflicted merge, re-run with --abort (optional --promotion); runs git merge --abort and POST /agentia/headless/work/promote-action/complete with status Cancelled.',
17
+ 'After a successful sync, when --deployment resolves to local, runs `sf project deploy start --ignore-conflicts` against --sf-alias (Add-only changes), then POST /agentia/headless/work/promote-action/complete with status Completed and the resolved deployment (none|local|cloud; auto chooses local when every change is Add and --sf-alias is set, otherwise cloud).',
18
+ 'By default polls the cloud deployment JobExecution to a terminal status when deployment is cloud (--no-wait to skip).',
16
19
  'On git/merge failure (including conflicts) and after a successful --continue, uploads the captured command transcript as a Salesforce File on the back-promotion (`Environment Sync Logs <name-or-id> <datetime>.txt`) via `/agentia/headless/file/upload`. HTTP 404 is ignored until that endpoint is rolled out; other upload failures are warned and do not change the environment-sync outcome.',
17
20
  ].join(' ');
18
21
  static examples = [
@@ -21,12 +24,16 @@ export default class WorkEnvironmentSync extends CicdCommand {
21
24
  '<%= config.bin %> <%= command.id %> --continue',
22
25
  '<%= config.bin %> <%= command.id %> --abort',
23
26
  '<%= config.bin %> <%= command.id %> --promotion=P29334 --continue',
27
+ '<%= config.bin %> <%= command.id %> --destination-environment=dev1 --deployment=local --sf-alias=dev1',
28
+ '<%= config.bin %> <%= command.id %> --destination-environment=dev1 --deployment=cloud',
29
+ '<%= config.bin %> <%= command.id %> --destination-environment=dev1 --no-wait',
24
30
  '<%= config.bin %> <%= command.id %> --destination-environment=dev1 --json',
25
31
  ];
32
+ static experimental = true;
26
33
  static flags = {
27
34
  abort: Flags.boolean({
28
35
  default: false,
29
- description: 'Abort an in-progress merge (`git merge --abort`) and set the back-promotion status to Draft. Uses --promotion or lastEnvironmentSyncName. Exclusive with --continue.',
36
+ description: 'Abort an in-progress merge (`git merge --abort`) and cancel the back-promotion via promote-action/complete. Uses --promotion or lastEnvironmentSyncName. Exclusive with --continue.',
30
37
  exclusive: ['continue'],
31
38
  }),
32
39
  continue: Flags.boolean({
@@ -34,8 +41,13 @@ export default class WorkEnvironmentSync extends CicdCommand {
34
41
  description: 'Finish after merge conflicts. If the merge is still in progress (conflicts fixed and staged, but not committed), commits it; if you already committed, continues from there through the remaining git steps. Uses --promotion or lastEnvironmentSyncName.',
35
42
  exclusive: ['abort'],
36
43
  }),
44
+ deployment: Flags.string({
45
+ default: 'auto',
46
+ description: 'How promote-action/complete handles deployment after a successful sync: none (leave deployment job untouched), local (sf project deploy start --ignore-conflicts then mark deployment JobExecution Successful; Add-only changes; requires --sf-alias), cloud (start PromotionDeployAction), or auto (local when every change is Add and --sf-alias is set, otherwise cloud). Sent to the gateway as none|local|cloud (never auto).',
47
+ options: [...ENVIRONMENT_SYNC_DEPLOYMENTS],
48
+ }),
37
49
  'destination-credential': Flags.string({
38
- description: 'Credential ID posted as destinationCredentialId when creating the back-promotion. Copado requires a credential on the next/source environment (PRO-033 if a dest/dev credential is sent) and then stores the dest/dev credential. Defaults to that source environment defaultCredential, otherwise the first source credential.',
50
+ description: 'Credential ID recorded on the environment-sync result as destinationCredentialId. Defaults to the next/source environment defaultCredential, otherwise the first source credential. PromoteAction resolves deployment credentials itself.',
39
51
  }),
40
52
  'destination-environment': Flags.string({
41
53
  description: 'Destination org environment name or Id. Resolves to that environment\'s pipeline connection Git branch.',
@@ -51,42 +63,29 @@ export default class WorkEnvironmentSync extends CicdCommand {
51
63
  promotion: Flags.string({
52
64
  description: 'Back-promotion Id or name to PATCH when this run stops on merge conflicts, finishes successfully, continues, or aborts. Optional for --continue/--abort when lastEnvironmentSyncName was stored from a prior environment-sync run. Defaults to the back-promotion created in this run.',
53
65
  }),
66
+ 'sf-alias': Flags.string({
67
+ description: 'Salesforce CLI org alias or username for local deployment (`sf project deploy start -o`). Required with --deployment=local; with --deployment=auto enables local when changes are Add-only.',
68
+ }),
69
+ wait: Flags.boolean({
70
+ allowNo: true,
71
+ default: true,
72
+ description: 'Poll promotion and deployment executions to terminal status.',
73
+ }),
74
+ 'wait-timeout': Flags.integer({
75
+ default: PROMOTION_RUN_DEFAULT_TIMEOUT_SECONDS,
76
+ description: 'Maximum total seconds to wait for promotion and deployment executions.',
77
+ }),
54
78
  };
55
79
  async run() {
56
80
  const { flags } = await this.parse(WorkEnvironmentSync);
81
+ const syncFlags = flags;
82
+ if (!Number.isInteger(syncFlags['wait-timeout']) || syncFlags['wait-timeout'] <= 0) {
83
+ this.error('--wait-timeout must be a positive integer.', { exit: 2 });
84
+ }
57
85
  return withCapturedCommandOutput(async () => {
58
86
  const startedAt = Date.now();
59
87
  try {
60
- await assertEnvironmentSyncDestOrgSelector({
61
- abort: flags.abort === true,
62
- continue: flags.continue === true,
63
- destinationEnvironment: flags['destination-environment'],
64
- devOrgBranch: flags['dev-org-branch'],
65
- promotion: flags.promotion,
66
- });
67
- const result = await performWorkEnvironmentSync({
68
- abort: flags.abort === true,
69
- cliBin: this.config.bin,
70
- continue: flags.continue === true,
71
- hooks: {
72
- log: (message) => {
73
- this.display(() => {
74
- this.log(message);
75
- });
76
- },
77
- withSpinner: (message, action) => this.withSpinner(message, action),
78
- },
79
- json: this.jsonEnabled(),
80
- ...(flags['destination-credential'] === undefined
81
- ? {}
82
- : { destinationCredentialId: flags['destination-credential'] }),
83
- ...(flags['destination-environment'] === undefined
84
- ? {}
85
- : { destinationEnvironment: flags['destination-environment'] }),
86
- ...(flags['dev-org-branch'] === undefined ? {} : { devOrgBranch: flags['dev-org-branch'] }),
87
- ...(flags['pipeline-id'] === undefined ? {} : { pipelineId: flags['pipeline-id'] }),
88
- ...(flags.promotion === undefined ? {} : { promotion: flags.promotion }),
89
- });
88
+ const result = await this.runEnvironmentSync(syncFlags);
90
89
  this.display(() => {
91
90
  const formatted = formatWorkEnvironmentSyncResult(result);
92
91
  if (formatted.length > 0) {
@@ -94,34 +93,111 @@ export default class WorkEnvironmentSync extends CicdCommand {
94
93
  }
95
94
  });
96
95
  this.logElapsed(startedAt);
96
+ await this.waitForDeploymentIfNeeded(result, syncFlags, startedAt);
97
97
  return result;
98
98
  }
99
99
  catch (error) {
100
- if (error instanceof WorkEnvironmentSyncConflictError) {
101
- const { suggestions } = error;
102
- const suggestionOptions = suggestions === undefined ? {} : { suggestions };
103
- if (this.jsonEnabled()) {
104
- const message = error.gitStatus === undefined || error.gitStatus.length === 0
105
- ? error.message
106
- : `${error.message}\n\n${error.gitStatus}`;
107
- this.error(message, { exit: error.exit, ...suggestionOptions });
108
- }
109
- this.error(error.message, { exit: false, ...suggestionOptions });
110
- if (error.gitStatus !== undefined && error.gitStatus.length > 0) {
111
- this.log('');
112
- this.log(error.gitStatus);
113
- }
114
- this.exit(error.exit);
115
- }
116
- if (error instanceof WorkEnvironmentSyncValidationError) {
117
- this.error(error.message, {
118
- exit: error.exit,
119
- ...(error.suggestions === undefined ? {} : { suggestions: error.suggestions }),
120
- });
121
- }
122
- throw error;
100
+ this.handleEnvironmentSyncError(error);
123
101
  }
124
102
  }, commandStdioCaptureOptions(this));
125
103
  }
104
+ toErrorJson(err) {
105
+ const base = super.toErrorJson(err);
106
+ return promotionRunErrorJson(base, err);
107
+ }
108
+ handleEnvironmentSyncError(error) {
109
+ if (error instanceof WorkEnvironmentSyncConflictError) {
110
+ const { suggestions } = error;
111
+ const suggestionOptions = suggestions === undefined ? {} : { suggestions };
112
+ if (this.jsonEnabled()) {
113
+ const message = error.gitStatus === undefined || error.gitStatus.length === 0
114
+ ? error.message
115
+ : `${error.message}\n\n${error.gitStatus}`;
116
+ this.error(message, { exit: error.exit, ...suggestionOptions });
117
+ }
118
+ this.error(error.message, { exit: false, ...suggestionOptions });
119
+ if (error.gitStatus !== undefined && error.gitStatus.length > 0) {
120
+ this.log('');
121
+ this.log(error.gitStatus);
122
+ }
123
+ this.exit(error.exit);
124
+ }
125
+ if (error instanceof WorkEnvironmentSyncValidationError) {
126
+ this.error(error.message, {
127
+ exit: error.exit,
128
+ ...(error.suggestions === undefined ? {} : { suggestions: error.suggestions }),
129
+ });
130
+ }
131
+ throw error;
132
+ }
133
+ async runEnvironmentSync(syncFlags) {
134
+ await assertEnvironmentSyncDestOrgSelector({
135
+ abort: syncFlags.abort === true,
136
+ continue: syncFlags.continue === true,
137
+ destinationEnvironment: syncFlags['destination-environment'],
138
+ devOrgBranch: syncFlags['dev-org-branch'],
139
+ promotion: syncFlags.promotion,
140
+ });
141
+ return performWorkEnvironmentSync({
142
+ abort: syncFlags.abort === true,
143
+ cliBin: this.config.bin,
144
+ continue: syncFlags.continue === true,
145
+ deployment: syncFlags.deployment,
146
+ hooks: {
147
+ log: (message) => {
148
+ this.display(() => {
149
+ this.log(message);
150
+ });
151
+ },
152
+ withSpinner: (message, action) => this.withSpinner(message, action),
153
+ },
154
+ json: this.jsonEnabled(),
155
+ ...(syncFlags['destination-credential'] === undefined
156
+ ? {}
157
+ : { destinationCredentialId: syncFlags['destination-credential'] }),
158
+ ...(syncFlags['destination-environment'] === undefined
159
+ ? {}
160
+ : { destinationEnvironment: syncFlags['destination-environment'] }),
161
+ ...(syncFlags['dev-org-branch'] === undefined ? {} : { devOrgBranch: syncFlags['dev-org-branch'] }),
162
+ ...(syncFlags['pipeline-id'] === undefined ? {} : { pipelineId: syncFlags['pipeline-id'] }),
163
+ ...(syncFlags.promotion === undefined ? {} : { promotion: syncFlags.promotion }),
164
+ ...(syncFlags['sf-alias'] === undefined ? {} : { sfAlias: syncFlags['sf-alias'] }),
165
+ });
166
+ }
167
+ async waitForDeploymentIfNeeded(result, syncFlags, startedAt) {
168
+ if (!syncFlags.wait || !shouldWaitForEnvironmentSyncDeployment(result) || result.promotion?.id === undefined) {
169
+ return;
170
+ }
171
+ this.display(() => {
172
+ this.log(ENVIRONMENT_SYNC_DEPLOYMENT_WAIT_MESSAGE);
173
+ });
174
+ try {
175
+ const deploymentJobMonitor = await this.withSpinner('Waiting for deployment execution', () => waitForPromotionDeploymentExecution({
176
+ hooks: {
177
+ log: (message) => {
178
+ this.display(() => {
179
+ this.log(message);
180
+ });
181
+ },
182
+ withSpinner: (message, action) => this.withSpinner(message, action),
183
+ },
184
+ promotionId: result.promotion.id,
185
+ timeoutSeconds: syncFlags['wait-timeout'],
186
+ }));
187
+ result.deploymentJobMonitor = deploymentJobMonitor;
188
+ result.wait = true;
189
+ this.display(() => {
190
+ this.log(formatWorkEnvironmentSyncDeploymentMonitor(deploymentJobMonitor));
191
+ });
192
+ }
193
+ catch (error) {
194
+ if (isPromotionRunExecutionError(error)) {
195
+ ;
196
+ error.oclif = { exit: 1 };
197
+ }
198
+ throw error;
199
+ }
200
+ this.logElapsed(startedAt);
201
+ }
126
202
  }
127
203
  //# sourceMappingURL=environment-sync.js.map
@@ -8,11 +8,11 @@ import { formatUserStoryDetail, } from '../../../lib/work-types.js';
8
8
  export default class WorkGet extends CicdCommand {
9
9
  static args = {
10
10
  id: Args.string({
11
- description: 'User story ID (defaults to the active work item from `work set`)',
11
+ description: 'User story ID or name (defaults to the active work item from `work set`)',
12
12
  required: false,
13
13
  }),
14
14
  };
15
- static description = 'Get a Copado user story by ID';
15
+ static description = 'Get a Copado user story by ID or name';
16
16
  static examples = [
17
17
  '<%= config.bin %> <%= command.id %>',
18
18
  '<%= config.bin %> <%= command.id %> a0SKC000000CmZ2AU',
@@ -1,5 +1,5 @@
1
1
  import { CicdCommand } from '../../../lib/cicd-command.js';
2
- import { displayListPagination } from '../../../lib/list-pagination.js';
2
+ import { displayListPagination, emitPaginatedListWarnings } from '../../../lib/list-pagination.js';
3
3
  import { getUserContext } from '../../../lib/user-context.js';
4
4
  import { buildWorkListFilters, workListFilterFlags } from '../../../lib/work-flags.js';
5
5
  import { fetchWorkItems } from '../../../lib/work-get.js';
@@ -42,6 +42,7 @@ export default class WorkList extends CicdCommand {
42
42
  }
43
43
  }
44
44
  const page = await this.withSpinner('Fetching user stories', () => fetchWorkItems(filters));
45
+ emitPaginatedListWarnings(this.warn.bind(this), page);
45
46
  this.display(() => {
46
47
  if (page.data.length === 0) {
47
48
  this.log('No user stories found.');
@@ -1,11 +1,11 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { CicdCommand } from '../../../../lib/cicd-command.js';
3
- import { planWorkApexTestRun, recordWorkApexTestOutcome, WorkApexRerunError, } from '../../../../lib/work-apex-test-failures.js';
4
- import { DEFAULT_APEX_TEST_WAIT_MINUTES, formatWorkApexTestHuman, parseApexTestClassNames, runWorkApexTests, skippedWorkApexTestResult, validateApexTestWaitMinutes, WorkApexTestError, } from '../../../../lib/work-apex-test.js';
3
+ import { executeWorkApexTestRun, WorkApexRerunError } from '../../../../lib/work-apex-test-failures.js';
4
+ import { DEFAULT_APEX_TEST_WAIT_MINUTES, formatWorkApexTestHuman, parseApexTestClassNames, validateApexTestWaitMinutes, } from '../../../../lib/work-apex-test.js';
5
5
  export default class WorkTestApex extends CicdCommand {
6
6
  static description = [
7
+ 'Legacy alias of `work test --gate apex`; prefer that form (it is what the sample quality-gate scripts call).',
7
8
  'Run specified Apex test classes via Salesforce CLI (`sf apex run test`) with code coverage.',
8
- 'Usually run from project-root local quality gates (`.agentia_quality_gates.sh` / `.agentia_quality_gates.cmd`) when those scripts are configured.',
9
9
  'When `--apex-test-classes` is omitted or empty, logs that no Apex tests were specified and exits without running Salesforce CLI.',
10
10
  'Failed runs save `.agentia/apex.tests.<user-story>.json`; successful runs delete that file.',
11
11
  '`--rerun-failed-apex-tests` runs the previously failed classes (optionally filtered by `--apex-test-classes`).',
@@ -34,6 +34,7 @@ export default class WorkTestApex extends CicdCommand {
34
34
  description: 'Minutes to wait for Salesforce CLI Apex test results',
35
35
  }),
36
36
  };
37
+ static hidden = true;
37
38
  async run() {
38
39
  const { flags } = await this.parse(WorkTestApex);
39
40
  const startedAt = Date.now();
@@ -46,42 +47,26 @@ export default class WorkTestApex extends CicdCommand {
46
47
  catch (error) {
47
48
  this.error(error.message, { exit: 2 });
48
49
  }
49
- let plan;
50
50
  try {
51
- plan = await planWorkApexTestRun({
52
- requestedClassNames: classNames,
53
- rerunFailed: flags['rerun-failed-apex-tests'] === true,
54
- });
55
- }
56
- catch (error) {
57
- if (error instanceof WorkApexRerunError) {
58
- this.error(error.message, { exit: 2 });
59
- }
60
- throw error;
61
- }
62
- if (plan.kind === 'skip') {
63
- const result = skippedWorkApexTestResult(waitMinutes);
64
- this.display(() => {
65
- this.log(plan.message);
66
- });
67
- this.logElapsed(startedAt);
68
- return result;
69
- }
70
- try {
71
- const result = await this.withSpinner(`Running Apex tests (${plan.classNames.join(', ')}), waiting up to ${waitMinutes} minute(s)`, () => runWorkApexTests({ classNames: plan.classNames, waitMinutes }));
72
- await recordWorkApexTestOutcome({ failed: false, result, userStory: plan.userStory });
73
- this.display(() => {
74
- this.log(formatWorkApexTestHuman(result));
51
+ const result = await executeWorkApexTestRun({ requestedClassNames: classNames, rerunFailed: flags['rerun-failed-apex-tests'] === true, waitMinutes }, {
52
+ onResult: (apexResult) => {
53
+ this.display(() => {
54
+ this.log(formatWorkApexTestHuman(apexResult));
55
+ });
56
+ },
57
+ onSkip: (message) => {
58
+ this.display(() => {
59
+ this.log(message);
60
+ });
61
+ },
62
+ withSpinner: (message, action) => this.withSpinner(message, action),
75
63
  });
76
64
  this.logElapsed(startedAt);
77
65
  return result;
78
66
  }
79
67
  catch (error) {
80
- if (error instanceof WorkApexTestError) {
81
- await recordWorkApexTestOutcome({ failed: true, result: error.result, userStory: plan.userStory });
82
- this.display(() => {
83
- this.log(formatWorkApexTestHuman(error.result));
84
- });
68
+ if (error instanceof WorkApexRerunError) {
69
+ this.error(error.message, { exit: 2 });
85
70
  }
86
71
  throw error;
87
72
  }
@@ -2,27 +2,38 @@ import { Flags } from '@oclif/core';
2
2
  import path from 'node:path';
3
3
  import { CicdCommand } from '../../../lib/cicd-command.js';
4
4
  import { getCurrentBranch } from '../../../lib/git-worktree.js';
5
- import { LocalQualityGatesError, parseQualityGatesEnvAssignments, resolveLocalQualityGatesScript, } from '../../../lib/local-quality-gates.js';
6
- import { runWorkTestLocalQualityGates } from '../../../lib/stock-quality-gates.js';
7
- import { reportLocalQualityGatesFailure, reportLocalQualityGatesOutcome, } from '../../../lib/work-local-run-tests.js';
5
+ import { isInsideQualityGatesScript, LocalQualityGatesError, parseQualityGatesEnvAssignments, resolveLocalQualityGatesScript, } from '../../../lib/local-quality-gates.js';
6
+ import { formatDeclaredQualityGates, isApexOnlySelection, resolveQualityGateSelection, UnknownQualityGateError, } from '../../../lib/quality-gates.js';
7
+ import { listLocalQualityGates, runWorkTestLocalQualityGates } from '../../../lib/stock-quality-gates.js';
8
+ import { executeWorkApexTestRun, WorkApexRerunError } from '../../../lib/work-apex-test-failures.js';
9
+ import { DEFAULT_APEX_TEST_WAIT_MINUTES, formatWorkApexTestHuman, parseApexTestClassNames, validateApexTestWaitMinutes, WorkApexTestError, } from '../../../lib/work-apex-test.js';
10
+ import { reportApexGateRun, reportLocalQualityGatesFailure, reportLocalQualityGatesOutcome, } from '../../../lib/work-local-run-tests.js';
8
11
  import { extractUserStoryFromFeatureBranch } from '../../../lib/work-types.js';
9
12
  export default class WorkTest extends CicdCommand {
10
13
  static description = [
11
14
  'Run quality gates for the current work item.',
12
- 'With --local (the default) it runs project-root .agentia_quality_gates.sh (macOS/Linux) or',
13
- '.agentia_quality_gates.cmd (Windows) when present.',
14
- 'When no project script exists it warns, runs the stock Agentia setup sample from a temporary file, then deletes that file.',
15
+ 'Runs the project-root .agentia_quality_gates.sh (macOS/Linux) or .agentia_quality_gates.cmd (Windows);',
16
+ 'when none exists it warns and runs the stock Agentia sample from a temporary file.',
17
+ 'Every gate runs by default. Use --gate <id> (repeatable or comma-separated) to select gates; the selection is exported as AGENTIA_GATES.',
18
+ 'Standard ids are apex, lint and lwc; scripts declare gates with `# agentia-gate: <id> - <description>` comments (see --list-gates).',
19
+ '`--gate apex` alone skips the script, runs the Apex tests directly and uploads the result to the work item (nested inside a script it does not upload; the outer run reports).',
15
20
  'With --cloud it runs remote Copado quality gates (not implemented yet).',
16
21
  ].join(' ');
17
22
  static examples = [
18
23
  '<%= config.bin %> <%= command.id %>',
19
- '<%= config.bin %> <%= command.id %> --local --apex-test-classes=MyTest,OtherTest',
20
- '<%= config.bin %> <%= command.id %> --local --rerun-failed-apex-tests',
24
+ '<%= config.bin %> <%= command.id %> --gate lint --gate lwc',
25
+ '<%= config.bin %> <%= command.id %> --gate apex --apex-test-classes=MyTest,OtherTest',
26
+ '<%= config.bin %> <%= command.id %> --list-gates',
21
27
  '<%= config.bin %> <%= command.id %> -e SKIP_LWC_TESTS=1 -e TOOL_API_KEY=secret',
22
28
  '<%= config.bin %> <%= command.id %> --json',
23
29
  '<%= config.bin %> <%= command.id %> --cloud',
24
30
  ];
25
31
  static flags = {
32
+ all: Flags.boolean({
33
+ default: false,
34
+ description: 'Run every gate in the quality-gates script (the default when no --gate is given).',
35
+ exclusive: ['gate', 'list-gates'],
36
+ }),
26
37
  'apex-test-classes': Flags.string({
27
38
  description: 'Comma-separated Apex test class names passed to local quality gates as AGENTIA_APEX_TEST_CLASSES',
28
39
  }),
@@ -35,13 +46,27 @@ export default class WorkTest extends CicdCommand {
35
46
  description: 'Extra environment variable for the quality-gate script as NAME=value. Repeatable. Applied before reserved AGENTIA_APEX_* variables.',
36
47
  multiple: true,
37
48
  }),
49
+ gate: Flags.string({
50
+ description: 'Run only this gate id (for example apex, lint, lwc or a custom id declared in the script). Repeatable or comma-separated. Exported to the script as AGENTIA_GATES.',
51
+ exclusive: ['all', 'list-gates'],
52
+ multiple: true,
53
+ }),
54
+ 'list-gates': Flags.boolean({
55
+ default: false,
56
+ description: 'List the gates declared in the quality-gates script (or the stock sample) and exit.',
57
+ }),
38
58
  local: Flags.boolean({
39
59
  default: false,
40
60
  description: 'Run local project-root quality gates scripts. Used by default when --cloud is omitted.',
41
61
  }),
42
62
  'rerun-failed-apex-tests': Flags.boolean({
43
63
  default: false,
44
- description: 'Pass AGENTIA_APEX_RERUN_FAILED=--rerun-failed-apex-tests to local quality gates so sample scripts forward that flag to work test apex.',
64
+ description: 'Pass AGENTIA_APEX_RERUN_FAILED=--rerun-failed-apex-tests to local quality gates so sample scripts forward that flag to the apex gate.',
65
+ }),
66
+ wait: Flags.integer({
67
+ char: 'w',
68
+ default: DEFAULT_APEX_TEST_WAIT_MINUTES,
69
+ description: 'Minutes to wait for Apex test results (only with --gate apex alone).',
45
70
  }),
46
71
  };
47
72
  async run() {
@@ -62,6 +87,19 @@ export default class WorkTest extends CicdCommand {
62
87
  catch (error) {
63
88
  this.error(error.message, { exit: 2 });
64
89
  }
90
+ let selection;
91
+ try {
92
+ selection = resolveQualityGateSelection({ all: flags.all, gates: flags.gate });
93
+ }
94
+ catch (error) {
95
+ this.error(error.message, { exit: 2 });
96
+ }
97
+ if (flags['list-gates']) {
98
+ return this.listGates();
99
+ }
100
+ if (isApexOnlySelection(selection)) {
101
+ return this.runApexGate({ classNames: flags['apex-test-classes'], rerunFailed: flags['rerun-failed-apex-tests'] === true, wait: flags.wait }, startedAt);
102
+ }
65
103
  let qualityGates;
66
104
  try {
67
105
  const scriptPath = resolveLocalQualityGatesScript();
@@ -78,6 +116,7 @@ export default class WorkTest extends CicdCommand {
78
116
  qualityGates = await runWorkTestLocalQualityGates({
79
117
  apexTestClasses: flags['apex-test-classes'],
80
118
  env: flags.env,
119
+ gates: selection.gates,
81
120
  onWarning: (message) => this.warn(message),
82
121
  rerunFailed: flags['rerun-failed-apex-tests'] === true,
83
122
  });
@@ -100,6 +139,9 @@ export default class WorkTest extends CicdCommand {
100
139
  exit: error.exitCode || 1,
101
140
  });
102
141
  }
142
+ if (error instanceof UnknownQualityGateError) {
143
+ this.error(error.message, { exit: 2 });
144
+ }
103
145
  throw error;
104
146
  }
105
147
  this.logElapsed(startedAt);
@@ -108,6 +150,35 @@ export default class WorkTest extends CicdCommand {
108
150
  qualityGates,
109
151
  };
110
152
  }
153
+ async listGates() {
154
+ const catalog = await listLocalQualityGates();
155
+ this.display(() => {
156
+ if (catalog.gates.length === 0) {
157
+ this.log('No gates declared. Add `# agentia-gate: <id> - <description>` comments to the quality-gates script.');
158
+ return;
159
+ }
160
+ this.log(`Gates declared in ${catalog.stock ? 'the stock sample' : path.basename(catalog.scriptPath)}:`);
161
+ this.log(formatDeclaredQualityGates(catalog.gates));
162
+ });
163
+ return { gates: catalog.gates, local: true, qualityGates: { ran: false, skipped: false } };
164
+ }
165
+ async reportApexGate(result, failed) {
166
+ if (isInsideQualityGatesScript()) {
167
+ return;
168
+ }
169
+ const workItem = await this.resolveWorkItemForReport();
170
+ if (workItem === undefined) {
171
+ this.warn('Failed to report local quality gates result: not on a feature/US-XXXX branch.');
172
+ return;
173
+ }
174
+ await reportApexGateRun({
175
+ failed,
176
+ onLog: (message) => this.display(() => this.log(message)),
177
+ onWarning: (message) => this.warn(message),
178
+ result,
179
+ workItem,
180
+ });
181
+ }
111
182
  async reportLocalFailure(error) {
112
183
  const workItem = await this.resolveWorkItemForReport();
113
184
  if (workItem === undefined) {
@@ -138,5 +209,39 @@ export default class WorkTest extends CicdCommand {
138
209
  const branch = await getCurrentBranch();
139
210
  return extractUserStoryFromFeatureBranch(branch);
140
211
  }
212
+ async runApexGate(options, startedAt) {
213
+ let requestedClassNames;
214
+ let waitMinutes;
215
+ try {
216
+ requestedClassNames = parseApexTestClassNames(options.classNames);
217
+ waitMinutes = validateApexTestWaitMinutes(options.wait);
218
+ }
219
+ catch (error) {
220
+ this.error(error.message, { exit: 2 });
221
+ }
222
+ try {
223
+ const apex = await executeWorkApexTestRun({
224
+ requestedClassNames,
225
+ rerunFailed: options.rerunFailed,
226
+ waitMinutes,
227
+ }, {
228
+ onResult: (result) => this.display(() => this.log(formatWorkApexTestHuman(result))),
229
+ onSkip: (message) => this.display(() => this.log(message)),
230
+ withSpinner: (message, action) => this.withSpinner(message, action),
231
+ });
232
+ await this.reportApexGate(apex, false);
233
+ this.logElapsed(startedAt);
234
+ return { apex, local: true, qualityGates: { ran: apex.skipped !== true, skipped: false } };
235
+ }
236
+ catch (error) {
237
+ if (error instanceof WorkApexRerunError) {
238
+ this.error(error.message, { exit: 2 });
239
+ }
240
+ if (error instanceof WorkApexTestError) {
241
+ await this.reportApexGate(error.result, true);
242
+ }
243
+ throw error;
244
+ }
245
+ }
141
246
  }
142
247
  //# sourceMappingURL=test.js.map
@@ -7,11 +7,11 @@ import { workEndpoint } from '../../../lib/work-types.js';
7
7
  export default class WorkUpdate extends CicdCommand {
8
8
  static args = {
9
9
  id: Args.string({
10
- description: 'User story ID (defaults to the active work item from `work set`)',
10
+ description: 'User story ID or name (defaults to the active work item from `work set`)',
11
11
  required: false,
12
12
  }),
13
13
  };
14
- static description = 'Update a Copado user story by ID';
14
+ static description = 'Update a Copado user story by ID or name';
15
15
  static examples = [
16
16
  '<%= config.bin %> <%= command.id %> --title "Updated story"',
17
17
  '<%= config.bin %> <%= command.id %> --custom-field JIRA_Key__c=CICD-1',
@@ -14,6 +14,7 @@ const readGlobalConfigDocument = modules["defaults-store.js"]["readGlobalConfigD
14
14
  const readLastApexTestClassesSelected = modules["defaults-store.js"]["readLastApexTestClassesSelected"]
15
15
  const readLastBaseBranch = modules["defaults-store.js"]["readLastBaseBranch"]
16
16
  const readLastDevOrgBranch = modules["defaults-store.js"]["readLastDevOrgBranch"]
17
+ const readLastEnvironmentSyncFeatureBranch = modules["defaults-store.js"]["readLastEnvironmentSyncFeatureBranch"]
17
18
  const readLastEnvironmentSyncName = modules["defaults-store.js"]["readLastEnvironmentSyncName"]
18
19
  const readLastFullMetadataSelected = modules["defaults-store.js"]["readLastFullMetadataSelected"]
19
20
  const readLastPermissionSelected = modules["defaults-store.js"]["readLastPermissionSelected"]
@@ -35,8 +36,9 @@ const updateGlobalConfigDocument = modules["defaults-store.js"]["updateGlobalCon
35
36
  const writeCredentialSyncReceipt = modules["defaults-store.js"]["writeCredentialSyncReceipt"]
36
37
  const writeDefaults = modules["defaults-store.js"]["writeDefaults"]
37
38
  const writeLastCommitSelections = modules["defaults-store.js"]["writeLastCommitSelections"]
39
+ const writeLastEnvironmentSyncFeatureBranch = modules["defaults-store.js"]["writeLastEnvironmentSyncFeatureBranch"]
38
40
  const writeLastEnvironmentSyncName = modules["defaults-store.js"]["writeLastEnvironmentSyncName"]
39
41
  const writeLastWorkSelection = modules["defaults-store.js"]["writeLastWorkSelection"]
40
42
  const writeUpdateChannel = modules["defaults-store.js"]["writeUpdateChannel"]
41
43
  const writeUserContext = modules["defaults-store.js"]["writeUserContext"]
42
- export {DEFAULT_WORK_PUSH_RETRY_TIMEOUT_SECONDS, clearCredentialSyncReceipt, clearLastEnvironmentSyncName, clearLastKeysFromUserConfig, clearUserContext, getDefaultsStorePaths, isValidUserContext, migrateUserSettingsFromProjectConfig, readCredentialSyncReceipt, readDefaults, readEnableLocalPromote, readGlobalConfigDocument, readLastApexTestClassesSelected, readLastBaseBranch, readLastDevOrgBranch, readLastEnvironmentSyncName, readLastFullMetadataSelected, readLastPermissionSelected, readLastPullRequestBaseUrl, readLastWorkItem, readLastWorkItemId, readLastWorkItemTitle, readSkipNestedMetadataDetection, readSkipPullRequest, readUpdateChannel, readUserContext, readWorkPushRetryTimeoutSeconds, resetDefaultsStorePathsForTests, resolveDefaults, setDefaultsStorePathsForTests, setDefaultsStoreProjectRoot, unsetDefaults, updateGlobalConfigDocument, writeCredentialSyncReceipt, writeDefaults, writeLastCommitSelections, writeLastEnvironmentSyncName, writeLastWorkSelection, writeUpdateChannel, writeUserContext};
44
+ export {DEFAULT_WORK_PUSH_RETRY_TIMEOUT_SECONDS, clearCredentialSyncReceipt, clearLastEnvironmentSyncName, clearLastKeysFromUserConfig, clearUserContext, getDefaultsStorePaths, isValidUserContext, migrateUserSettingsFromProjectConfig, readCredentialSyncReceipt, readDefaults, readEnableLocalPromote, readGlobalConfigDocument, readLastApexTestClassesSelected, readLastBaseBranch, readLastDevOrgBranch, readLastEnvironmentSyncFeatureBranch, readLastEnvironmentSyncName, readLastFullMetadataSelected, readLastPermissionSelected, readLastPullRequestBaseUrl, readLastWorkItem, readLastWorkItemId, readLastWorkItemTitle, readSkipNestedMetadataDetection, readSkipPullRequest, readUpdateChannel, readUserContext, readWorkPushRetryTimeoutSeconds, resetDefaultsStorePathsForTests, resolveDefaults, setDefaultsStorePathsForTests, setDefaultsStoreProjectRoot, unsetDefaults, updateGlobalConfigDocument, writeCredentialSyncReceipt, writeDefaults, writeLastCommitSelections, writeLastEnvironmentSyncFeatureBranch, writeLastEnvironmentSyncName, writeLastWorkSelection, writeUpdateChannel, writeUserContext};
@@ -0,0 +1,3 @@
1
+ import {modules} from './runtime.js';
2
+ const fetchFunctions = modules["function-list.js"]["fetchFunctions"]
3
+ export {fetchFunctions};
@@ -3,6 +3,7 @@ const NAME_STATUS_PRETTY_FORMAT = modules["git-worktree.js"]["NAME_STATUS_PRETTY
3
3
  const abortMerge = modules["git-worktree.js"]["abortMerge"]
4
4
  const checkoutBranch = modules["git-worktree.js"]["checkoutBranch"]
5
5
  const checkoutFeatureBranch = modules["git-worktree.js"]["checkoutFeatureBranch"]
6
+ const checkoutPathsFromRef = modules["git-worktree.js"]["checkoutPathsFromRef"]
6
7
  const checkoutPreferredBranch = modules["git-worktree.js"]["checkoutPreferredBranch"]
7
8
  const commitAllTrackedChanges = modules["git-worktree.js"]["commitAllTrackedChanges"]
8
9
  const commitAllowEmpty = modules["git-worktree.js"]["commitAllowEmpty"]
@@ -27,9 +28,11 @@ const isAncestor = modules["git-worktree.js"]["isAncestor"]
27
28
  const isInsideGitWorkTree = modules["git-worktree.js"]["isInsideGitWorkTree"]
28
29
  const isMergeInProgress = modules["git-worktree.js"]["isMergeInProgress"]
29
30
  const isSafeGitBranchName = modules["git-worktree.js"]["isSafeGitBranchName"]
31
+ const listDiffNamesBetween = modules["git-worktree.js"]["listDiffNamesBetween"]
30
32
  const listDiffNamesVsHead = modules["git-worktree.js"]["listDiffNamesVsHead"]
31
33
  const listPendingChanges = modules["git-worktree.js"]["listPendingChanges"]
32
34
  const listStagedChanges = modules["git-worktree.js"]["listStagedChanges"]
35
+ const listTreeFileNames = modules["git-worktree.js"]["listTreeFileNames"]
33
36
  const listUnmergedFiles = modules["git-worktree.js"]["listUnmergedFiles"]
34
37
  const localBranchExists = modules["git-worktree.js"]["localBranchExists"]
35
38
  const mergeFfOnly = modules["git-worktree.js"]["mergeFfOnly"]
@@ -49,4 +52,4 @@ const restoreFeatureBranch = modules["git-worktree.js"]["restoreFeatureBranch"]
49
52
  const restoreHeadPaths = modules["git-worktree.js"]["restoreHeadPaths"]
50
53
  const setGitExecForTests = modules["git-worktree.js"]["setGitExecForTests"]
51
54
  const stageAndCommit = modules["git-worktree.js"]["stageAndCommit"]
52
- export {NAME_STATUS_PRETTY_FORMAT, abortMerge, checkoutBranch, checkoutFeatureBranch, checkoutPreferredBranch, commitAllTrackedChanges, commitAllowEmpty, commitCurrentMerge, commitStagedChanges, countCommitsSince, createAndCheckoutBranch, fetchAndCheckoutBranch, fetchOriginBranch, fetchOriginBranchIfExists, formatGitExecError, getCommitsNameStatus, getCurrentBranch, getFileAtRef, getGitRepoRoot, getHeadCommitId, getHumanGitStatus, getMergeBase, getRemoteUrl, hasUnpushedCommits, isAncestor, isInsideGitWorkTree, isMergeInProgress, isSafeGitBranchName, listDiffNamesVsHead, listPendingChanges, listStagedChanges, listUnmergedFiles, localBranchExists, mergeFfOnly, mergeNoFf, mergeNoFfNoCommit, mergeTheirsIgnoreSpaceChange, originBranchRef, originFetchRefspec, parseNameOnly, parsePorcelainPaths, pullOriginBranch, pushOriginBranch, pushOriginBranches, resetGitExecForTests, resetHardHead, restoreFeatureBranch, restoreHeadPaths, setGitExecForTests, stageAndCommit};
55
+ export {NAME_STATUS_PRETTY_FORMAT, abortMerge, checkoutBranch, checkoutFeatureBranch, checkoutPathsFromRef, checkoutPreferredBranch, commitAllTrackedChanges, commitAllowEmpty, commitCurrentMerge, commitStagedChanges, countCommitsSince, createAndCheckoutBranch, fetchAndCheckoutBranch, fetchOriginBranch, fetchOriginBranchIfExists, formatGitExecError, getCommitsNameStatus, getCurrentBranch, getFileAtRef, getGitRepoRoot, getHeadCommitId, getHumanGitStatus, getMergeBase, getRemoteUrl, hasUnpushedCommits, isAncestor, isInsideGitWorkTree, isMergeInProgress, isSafeGitBranchName, listDiffNamesBetween, listDiffNamesVsHead, listPendingChanges, listStagedChanges, listTreeFileNames, listUnmergedFiles, localBranchExists, mergeFfOnly, mergeNoFf, mergeNoFfNoCommit, mergeTheirsIgnoreSpaceChange, originBranchRef, originFetchRefspec, parseNameOnly, parsePorcelainPaths, pullOriginBranch, pushOriginBranch, pushOriginBranches, resetGitExecForTests, resetHardHead, restoreFeatureBranch, restoreHeadPaths, setGitExecForTests, stageAndCommit};