@salesforce/afv-skills 1.17.0 → 1.19.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 (40) hide show
  1. package/package.json +1 -1
  2. package/skills/analyzing-test-failures/SKILL.md +159 -0
  3. package/skills/building-sf-integrations/SKILL.md +1 -1
  4. package/skills/checking-devops-prerequisites/SKILL.md +141 -0
  5. package/skills/configuring-code-analyzer/SKILL.md +482 -0
  6. package/skills/configuring-code-analyzer/examples/apex-project-config.yml +41 -0
  7. package/skills/configuring-code-analyzer/examples/ci-github-actions.yml +96 -0
  8. package/skills/configuring-code-analyzer/examples/fullstack-project-config.yml +46 -0
  9. package/skills/configuring-code-analyzer/examples/lwc-project-config.yml +26 -0
  10. package/skills/configuring-code-analyzer/references/ci-cd-templates.md +648 -0
  11. package/skills/configuring-code-analyzer/references/config-schema.md +257 -0
  12. package/skills/configuring-code-analyzer/references/diagnostic-flow.md +70 -0
  13. package/skills/configuring-code-analyzer/references/engine-prerequisites.md +276 -0
  14. package/skills/configuring-code-analyzer/references/rule-name-resolution.md +67 -0
  15. package/skills/configuring-code-analyzer/references/troubleshooting.md +298 -0
  16. package/skills/configuring-code-analyzer/scripts/check-prerequisites.sh +189 -0
  17. package/skills/configuring-code-analyzer/scripts/generate-config.sh +143 -0
  18. package/skills/configuring-code-analyzer/scripts/validate-config.sh +153 -0
  19. package/skills/configuring-quality-gate/SKILL.md +120 -0
  20. package/skills/configuring-test-provider/SKILL.md +113 -0
  21. package/skills/creating-fix-work-item/SKILL.md +66 -0
  22. package/skills/managing-cdc-enablement/SKILL.md +164 -0
  23. package/skills/managing-cdc-enablement/assets/PlatformEventChannel-template.xml +5 -0
  24. package/skills/managing-cdc-enablement/assets/PlatformEventChannelMember-template.xml +11 -0
  25. package/skills/managing-cdc-enablement/references/deploy-troubleshooting.md +73 -0
  26. package/skills/managing-cdc-enablement/references/filter-expressions.md +93 -0
  27. package/skills/managing-suite-assignments/SKILL.md +161 -0
  28. package/skills/polling-test-results/SKILL.md +72 -0
  29. package/skills/recommending-devops-tests/SKILL.md +137 -0
  30. package/skills/running-code-analyzer/SKILL.md +264 -267
  31. package/skills/running-code-analyzer/references/post-scan-workflows.md +286 -0
  32. package/skills/running-code-analyzer/scripts/describe-rule.js +382 -0
  33. package/skills/running-code-analyzer/scripts/list-rules.js +260 -0
  34. package/skills/running-code-analyzer/scripts/query-results.js +230 -0
  35. package/skills/running-devops-test-suite/SKILL.md +144 -0
  36. package/skills/syncing-test-providers/SKILL.md +108 -0
  37. package/skills/using-salesforce-archive/SKILL.md +121 -0
  38. package/skills/using-salesforce-archive/examples/monitor-failed-jobs.md +47 -0
  39. package/skills/using-salesforce-archive/references/archive-activity-entity.md +59 -0
  40. package/skills/using-salesforce-archive/references/connect-api-operations.md +157 -0
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env node
2
+ // Version: v1.0 | SHA256: placeholder
3
+ // List Code Analyzer rules matching a selector and return structured JSON
4
+ // Usage: node list-rules.js <selector> [options]
5
+ //
6
+ // This script runs `sf code-analyzer rules --rule-selector <selector>` and
7
+ // parses the table output into structured JSON for presentation.
8
+ //
9
+ // The CLI table format is:
10
+ // # Name Engine Severity Tags
11
+ // 1 @lwc/lwc/no-inner-html eslint 2 (High) Recommended, LWC, Security
12
+ //
13
+ // Output: JSON with {status, totalRules, rules: [{name, engine, severity, severityNum, tags}], engines, summary}
14
+
15
+ const { execSync } = require("child_process");
16
+
17
+ function printUsage() {
18
+ console.error(`Usage: node list-rules.js <selector> [options]
19
+
20
+ Arguments:
21
+ <selector> Rule selector (same syntax as --rule-selector)
22
+ Examples: "Security", "pmd", "eslint:Recommended",
23
+ "(pmd,eslint):Security:(1,2)", "Apex", "JavaScript"
24
+
25
+ Options:
26
+ --engine <name> Filter results to a specific engine after listing
27
+ --severity <n> Filter results to specific severity (1-5, comma-separated)
28
+ --top <n> Return at most N rules (default: 100)
29
+ --count-only Return only counts by engine/severity/category (no rule list)
30
+
31
+ Valid selector tokens:
32
+ Engines: eslint, regex, retire-js, flow, pmd, cpd, sfge
33
+ Severities: Critical/1, High/2, Moderate/3, Low/4, Info/5
34
+ Categories: Security, Performance, BestPractices, CodeStyle, Design, ErrorProne, Documentation
35
+ Languages: Apex, JavaScript, TypeScript, HTML, CSS, Visualforce, XML
36
+ Tags: Recommended, Custom, All, DevPreview, LWC, Fixable
37
+
38
+ Examples:
39
+ node list-rules.js "Security"
40
+ node list-rules.js "pmd:Security"
41
+ node list-rules.js "eslint:Recommended"
42
+ node list-rules.js "(pmd,eslint):Security:(1,2)"
43
+ node list-rules.js "Apex"
44
+ node list-rules.js "JavaScript:BestPractices"
45
+ node list-rules.js "Recommended" --count-only
46
+ node list-rules.js "all" --engine pmd --severity 1,2 --top 10`);
47
+ process.exit(1);
48
+ }
49
+
50
+ // Parse CLI arguments
51
+ const args = process.argv.slice(2);
52
+ if (args.length < 1 || args[0] === "--help" || args[0] === "-h") {
53
+ printUsage();
54
+ }
55
+
56
+ const selector = args[0];
57
+ const options = {
58
+ engine: null,
59
+ severity: null,
60
+ top: 100,
61
+ countOnly: false,
62
+ };
63
+
64
+ for (let i = 1; i < args.length; i++) {
65
+ switch (args[i]) {
66
+ case "--engine":
67
+ options.engine = (args[++i] || "").toLowerCase();
68
+ break;
69
+ case "--severity":
70
+ options.severity = (args[++i] || "")
71
+ .split(",")
72
+ .map((s) => parseInt(s.trim(), 10))
73
+ .filter((n) => n >= 1 && n <= 5);
74
+ break;
75
+ case "--top":
76
+ options.top = parseInt(args[++i] || "25", 10);
77
+ break;
78
+ case "--count-only":
79
+ options.countOnly = true;
80
+ break;
81
+ default:
82
+ console.error(`Unknown option: ${args[i]}`);
83
+ printUsage();
84
+ }
85
+ }
86
+
87
+ // Validate selector tokens before running CLI
88
+ const validationError = validateSelector(selector);
89
+ if (validationError) {
90
+ console.log(JSON.stringify({
91
+ status: "invalid_selector",
92
+ message: validationError,
93
+ hint: "Valid tokens: engines (pmd, eslint, cpd, retire-js, regex, flow, sfge), severities (1-5 or Critical/High/Moderate/Low/Info), categories (Security, Performance, BestPractices, CodeStyle, Design, ErrorProne, Documentation), languages (Apex, JavaScript, TypeScript, HTML, CSS, Visualforce, XML), tags (Recommended, Custom, All, DevPreview, LWC, Fixable)",
94
+ }));
95
+ process.exit(0);
96
+ }
97
+
98
+ // Run `sf code-analyzer rules`
99
+ let rawOutput;
100
+ try {
101
+ const cmd = `sf code-analyzer rules --rule-selector "${selector}" 2>&1`;
102
+ rawOutput = execSync(cmd, {
103
+ encoding: "utf8",
104
+ timeout: 60000,
105
+ maxBuffer: 2 * 1024 * 1024,
106
+ });
107
+ } catch (err) {
108
+ rawOutput = err.stdout || err.stderr || (err.output && err.output.join("")) || "";
109
+ if (!rawOutput) {
110
+ console.log(JSON.stringify({
111
+ status: "error",
112
+ message: `Failed to run sf code-analyzer rules: ${err.message}`,
113
+ }));
114
+ process.exit(0);
115
+ }
116
+ }
117
+
118
+ // Parse the table output
119
+ const rules = parseTableOutput(rawOutput);
120
+
121
+ if (rules.length === 0) {
122
+ console.log(JSON.stringify({
123
+ status: "no_rules_found",
124
+ message: `No rules matched selector "${selector}". Check the selector syntax or try a broader query.`,
125
+ hint: "Use tokens like: Security, pmd, eslint:Recommended, (1,2), Apex",
126
+ }));
127
+ process.exit(0);
128
+ }
129
+
130
+ // Apply post-filters
131
+ let filtered = rules;
132
+ if (options.engine) {
133
+ filtered = filtered.filter((r) => r.engine.toLowerCase() === options.engine);
134
+ }
135
+ if (options.severity) {
136
+ filtered = filtered.filter((r) => options.severity.includes(r.severityNum));
137
+ }
138
+
139
+ // Compute summary stats
140
+ const engineCounts = {};
141
+ const severityCounts = {};
142
+ const categoryCounts = {};
143
+ filtered.forEach((r) => {
144
+ engineCounts[r.engine] = (engineCounts[r.engine] || 0) + 1;
145
+ const sevKey = `${r.severityNum} (${severityName(r.severityNum)})`;
146
+ severityCounts[sevKey] = (severityCounts[sevKey] || 0) + 1;
147
+ (r.tags || []).forEach((tag) => {
148
+ const t = tag.trim();
149
+ if (["Security", "Performance", "BestPractices", "CodeStyle", "Design", "ErrorProne", "Documentation"].includes(t)) {
150
+ categoryCounts[t] = (categoryCounts[t] || 0) + 1;
151
+ }
152
+ });
153
+ });
154
+
155
+ // Build result
156
+ const result = {
157
+ status: "success",
158
+ selector,
159
+ totalRules: filtered.length,
160
+ summary: {
161
+ byEngine: engineCounts,
162
+ bySeverity: severityCounts,
163
+ byCategory: categoryCounts,
164
+ },
165
+ };
166
+
167
+ if (!options.countOnly) {
168
+ result.rules = filtered.slice(0, options.top).map((r) => ({
169
+ name: r.name,
170
+ engine: r.engine,
171
+ severity: r.severity,
172
+ severityNum: r.severityNum,
173
+ tags: r.tags,
174
+ }));
175
+ if (filtered.length > options.top) {
176
+ result.truncated = true;
177
+ result.showing = options.top;
178
+ }
179
+ }
180
+
181
+ console.log(JSON.stringify(result));
182
+
183
+ // --- Helper Functions ---
184
+
185
+ function parseTableOutput(output) {
186
+ const rules = [];
187
+ const lines = output.split("\n");
188
+
189
+ for (const line of lines) {
190
+ // Match table rows: starts with spaces + number
191
+ // Format: " 1 ruleName engine severity tags"
192
+ const match = line.match(/^\s+(\d+)\s{2,}(\S+)\s{2,}(\S+)\s{2,}(\d+\s*\([^)]+\))\s{2,}(.+)$/);
193
+ if (match) {
194
+ const severityStr = match[4].trim();
195
+ const sevNumMatch = severityStr.match(/^(\d)/);
196
+ rules.push({
197
+ name: match[2].trim(),
198
+ engine: match[3].trim(),
199
+ severity: severityStr,
200
+ severityNum: sevNumMatch ? parseInt(sevNumMatch[1], 10) : 0,
201
+ tags: match[5].trim().split(",").map((t) => t.trim()).filter(Boolean),
202
+ });
203
+ }
204
+ }
205
+
206
+ return rules;
207
+ }
208
+
209
+ function validateSelector(selector) {
210
+ if (!selector || !selector.trim()) {
211
+ return "Selector cannot be empty.";
212
+ }
213
+
214
+ const VALID_TOKENS = new Set([
215
+ // Engines
216
+ "eslint", "regex", "retire-js", "flow", "pmd", "cpd", "sfge",
217
+ // Severity names
218
+ "critical", "high", "moderate", "low", "info",
219
+ // Severity numbers
220
+ "1", "2", "3", "4", "5",
221
+ // General tags
222
+ "recommended", "custom", "all",
223
+ // Categories
224
+ "bestpractices", "codestyle", "design", "documentation", "errorprone", "security", "performance",
225
+ // Languages
226
+ "apex", "css", "html", "javascript", "typescript", "visualforce", "xml",
227
+ // Engine-specific
228
+ "devpreview", "lwc", "fixable",
229
+ ]);
230
+
231
+ // Split by : (AND), then handle () groups (OR)
232
+ const groups = selector.split(":").map((s) => s.trim()).filter(Boolean);
233
+ const invalid = [];
234
+
235
+ for (const group of groups) {
236
+ let tokens;
237
+ if (group.startsWith("(") && group.endsWith(")")) {
238
+ // OR group: (token1,token2)
239
+ tokens = group.slice(1, -1).split(",").map((t) => t.trim()).filter(Boolean);
240
+ } else {
241
+ tokens = [group];
242
+ }
243
+
244
+ for (const token of tokens) {
245
+ if (!VALID_TOKENS.has(token.toLowerCase())) {
246
+ invalid.push(token);
247
+ }
248
+ }
249
+ }
250
+
251
+ if (invalid.length > 0) {
252
+ return `Invalid selector token(s): ${invalid.join(", ")}. Did you misspell a token?`;
253
+ }
254
+ return null;
255
+ }
256
+
257
+ function severityName(num) {
258
+ const names = { 1: "Critical", 2: "High", 3: "Moderate", 4: "Low", 5: "Info" };
259
+ return names[num] || "Unknown";
260
+ }
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env node
2
+ // Version: v1.0 | SHA256: placeholder
3
+ // Query and filter Code Analyzer results JSON with rich filtering capabilities
4
+ // Usage: node query-results.js <results-file.json> [options]
5
+ //
6
+ // Options:
7
+ // --engine <name> Filter by engine (pmd, eslint, cpd, retire-js, etc.)
8
+ // --severity <n> Filter by severity (1-5, comma-separated for multiple)
9
+ // --category <tag> Filter by category/tag (Security, Performance, etc.)
10
+ // --rule <name> Filter by exact rule name (case-insensitive)
11
+ // --file <substring> Filter by file path substring
12
+ // --top <n> Return top N results (default: 10)
13
+ // --sort <field> Sort by: severity, rule, engine, file (default: severity)
14
+ // --sort-dir <dir> Sort direction: asc, desc (default: asc)
15
+ // --summary Show only summary counts (no individual violations)
16
+
17
+ const fs = require("fs");
18
+ const path = require("path");
19
+
20
+ function printUsage() {
21
+ console.error(`Usage: node query-results.js <results-file.json> [options]
22
+
23
+ Options:
24
+ --engine <name> Filter by engine (pmd, eslint, cpd, retire-js, etc.)
25
+ --severity <n> Filter by severity (1-5, comma-separated for multiple: 1,2)
26
+ --category <tag> Filter by category/tag (Security, Performance, BestPractices, etc.)
27
+ --rule <name> Filter by exact rule name (case-insensitive)
28
+ --file <substring> Filter by file path substring (case-insensitive)
29
+ --top <n> Return top N results (default: 10)
30
+ --sort <field> Sort by: severity, rule, engine, file (default: severity)
31
+ --sort-dir <dir> Sort direction: asc, desc (default: asc)
32
+ --summary Show only summary counts (no individual violations)
33
+
34
+ Examples:
35
+ node query-results.js results.json --engine pmd --severity 1,2
36
+ node query-results.js results.json --category Security --top 20
37
+ node query-results.js results.json --file AccountService.cls
38
+ node query-results.js results.json --rule ApexCRUDViolation
39
+ node query-results.js results.json --summary`);
40
+ process.exit(1);
41
+ }
42
+
43
+ // Parse CLI arguments
44
+ const args = process.argv.slice(2);
45
+ if (args.length < 1 || args[0] === "--help" || args[0] === "-h") {
46
+ printUsage();
47
+ }
48
+
49
+ const filePath = args[0];
50
+ const options = {
51
+ engine: null,
52
+ severity: null,
53
+ category: null,
54
+ rule: null,
55
+ file: null,
56
+ top: 10,
57
+ sort: "severity",
58
+ sortDir: "asc",
59
+ summary: false,
60
+ };
61
+
62
+ // Parse named options
63
+ for (let i = 1; i < args.length; i++) {
64
+ const arg = args[i];
65
+ switch (arg) {
66
+ case "--engine":
67
+ options.engine = (args[++i] || "").toLowerCase();
68
+ break;
69
+ case "--severity":
70
+ options.severity = (args[++i] || "")
71
+ .split(",")
72
+ .map((s) => parseInt(s.trim(), 10))
73
+ .filter((n) => n >= 1 && n <= 5);
74
+ break;
75
+ case "--category":
76
+ options.category = (args[++i] || "").toLowerCase();
77
+ break;
78
+ case "--rule":
79
+ options.rule = (args[++i] || "").toLowerCase();
80
+ break;
81
+ case "--file":
82
+ options.file = (args[++i] || "").toLowerCase();
83
+ break;
84
+ case "--top":
85
+ options.top = parseInt(args[++i] || "10", 10);
86
+ break;
87
+ case "--sort":
88
+ options.sort = args[++i] || "severity";
89
+ break;
90
+ case "--sort-dir":
91
+ options.sortDir = args[++i] || "asc";
92
+ break;
93
+ case "--summary":
94
+ options.summary = true;
95
+ break;
96
+ default:
97
+ console.error(`Unknown option: ${arg}`);
98
+ printUsage();
99
+ }
100
+ }
101
+
102
+ // Read and parse results file
103
+ let data;
104
+ try {
105
+ data = JSON.parse(fs.readFileSync(filePath, "utf8"));
106
+ } catch (err) {
107
+ console.error(`Error reading results file: ${err.message}`);
108
+ process.exit(1);
109
+ }
110
+
111
+ const runDir = data.runDir || "";
112
+ const violations = data.violations || [];
113
+
114
+ // Apply filters
115
+ let filtered = violations.filter((v) => {
116
+ if (options.engine && v.engine.toLowerCase() !== options.engine) return false;
117
+ if (options.severity && !options.severity.includes(v.severity)) return false;
118
+ if (options.category) {
119
+ const tags = (v.tags || []).map((t) => t.toLowerCase());
120
+ if (!tags.includes(options.category)) return false;
121
+ }
122
+ if (options.rule && v.rule.toLowerCase() !== options.rule) return false;
123
+ if (options.file) {
124
+ const loc = v.locations && v.locations[v.primaryLocationIndex || 0];
125
+ const fileLower = ((loc && loc.file) || "").toLowerCase();
126
+ if (!fileLower.includes(options.file)) return false;
127
+ }
128
+ return true;
129
+ });
130
+
131
+ // Sort
132
+ const sortMul = options.sortDir === "desc" ? -1 : 1;
133
+ filtered.sort((a, b) => {
134
+ let cmp = 0;
135
+ switch (options.sort) {
136
+ case "severity":
137
+ cmp = a.severity - b.severity;
138
+ break;
139
+ case "rule":
140
+ cmp = a.rule.localeCompare(b.rule);
141
+ break;
142
+ case "engine":
143
+ cmp = a.engine.localeCompare(b.engine);
144
+ break;
145
+ case "file": {
146
+ const aLoc = a.locations && a.locations[a.primaryLocationIndex || 0];
147
+ const bLoc = b.locations && b.locations[b.primaryLocationIndex || 0];
148
+ const aFile = (aLoc && aLoc.file) || "";
149
+ const bFile = (bLoc && bLoc.file) || "";
150
+ cmp = aFile.localeCompare(bFile);
151
+ break;
152
+ }
153
+ }
154
+ if (cmp !== 0) return cmp * sortMul;
155
+ // Secondary sort: severity ascending
156
+ return (a.severity - b.severity) * sortMul;
157
+ });
158
+
159
+ // Build output
160
+ const totalMatches = filtered.length;
161
+ const limited = filtered.slice(0, options.top);
162
+
163
+ // Compute severity breakdown of matches
164
+ const sevCounts = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
165
+ filtered.forEach((v) => {
166
+ if (sevCounts[v.severity] !== undefined) sevCounts[v.severity]++;
167
+ });
168
+
169
+ // Compute rule frequency for matches
170
+ const ruleCounts = {};
171
+ const ruleEngines = {};
172
+ filtered.forEach((v) => {
173
+ ruleCounts[v.rule] = (ruleCounts[v.rule] || 0) + 1;
174
+ if (!ruleEngines[v.rule]) ruleEngines[v.rule] = v.engine;
175
+ });
176
+ const topRules = Object.entries(ruleCounts)
177
+ .sort((a, b) => b[1] - a[1])
178
+ .slice(0, options.top)
179
+ .map(([rule, count]) => ({ rule, engine: ruleEngines[rule], count }));
180
+
181
+ // Compute file frequency for matches
182
+ const fileCounts = {};
183
+ filtered.forEach((v) => {
184
+ const loc = v.locations && v.locations[v.primaryLocationIndex || 0];
185
+ let file = (loc && loc.file) || "unknown";
186
+ if (runDir && file.startsWith(runDir)) file = file.substring(runDir.length + 1);
187
+ fileCounts[file] = (fileCounts[file] || 0) + 1;
188
+ });
189
+ const topFiles = Object.entries(fileCounts)
190
+ .sort((a, b) => b[1] - a[1])
191
+ .slice(0, options.top)
192
+ .map(([file, count]) => ({ file, count }));
193
+
194
+ // Build result object
195
+ const result = {
196
+ query: {
197
+ engine: options.engine,
198
+ severity: options.severity,
199
+ category: options.category,
200
+ rule: options.rule,
201
+ file: options.file,
202
+ top: options.top,
203
+ sort: options.sort,
204
+ sortDir: options.sortDir,
205
+ },
206
+ totalViolations: violations.length,
207
+ totalMatches,
208
+ severityCounts: sevCounts,
209
+ topRules,
210
+ topFiles,
211
+ };
212
+
213
+ if (!options.summary) {
214
+ result.violations = limited.map((v) => {
215
+ const loc = v.locations && v.locations[v.primaryLocationIndex || 0];
216
+ let file = (loc && loc.file) || "unknown";
217
+ if (runDir && file.startsWith(runDir)) file = file.substring(runDir.length + 1);
218
+ return {
219
+ rule: v.rule,
220
+ engine: v.engine,
221
+ severity: v.severity,
222
+ message: v.message,
223
+ file: file,
224
+ startLine: (loc && loc.startLine) || 0,
225
+ tags: v.tags || [],
226
+ };
227
+ });
228
+ }
229
+
230
+ console.log(JSON.stringify(result));
@@ -0,0 +1,144 @@
1
+ ---
2
+ name: running-devops-test-suite
3
+ description: "Triggers async execution of one or more DevOps Center test suites on a pipeline stage (Pre-Promote, Post-Promote, or Review event) via the Connect API, after an explicit user confirmation gate, then hands off to result polling via the `polling-test-results` skill. Also re-runs (retriggers) a quality gate after fixes — but only once validation confirms the coverage threshold is now met. TRIGGER when: the user wants to run, kick off, or launch test suites on a pipeline stage; execute tests before or after a promotion; trigger a Pre-Promote, Post-Promote, or Review-event run; re-run a quality gate after fixing failures; retry a failed gate once coverage is met; or unblock a blocked promotion after adding tests. DO NOT TRIGGER when: running sf apex run test directly (use running-apex-tests); or configuring a NEW gate or threshold (use configuring-quality-gate)."
4
+ metadata:
5
+ version: "1.0"
6
+ minApiVersion: "67.0"
7
+ ---
8
+
9
+ # Running a DevOps Center Test Suite
10
+
11
+ ## Prerequisites
12
+
13
+ Load and follow `checking-devops-prerequisites` first — run Prerequisites 1–4 AND Prerequisite 5 (pipeline stage), since this skill operates on a specific stage. You need the confirmed `doce-org-alias`, `pipelineId`, and `stageId` before proceeding.
14
+
15
+ ## Inputs required before calling this skill
16
+
17
+ | Input | How to obtain |
18
+ |---|---|
19
+ | `pipelineId` | From Prerequisite 4 (pipeline selection) |
20
+ | `stageId` | From Prerequisite 5 (pipeline stage confirmation) |
21
+ | `event` | Confirm with user: `Pre-Promote` or `Post-Promote` (or `Review` if the context is a review environment) |
22
+ | `testSuiteIds` | Confirmed suite IDs from the suite selection or recommendation step |
23
+ | `doce-org-alias` | Established in Prerequisite 1 |
24
+
25
+ ## Confirmation gate
26
+
27
+ **This call mutates org state — do not proceed without explicit user confirmation.**
28
+
29
+ Before calling the API, show the user:
30
+
31
+ > "I'm about to run tests with the following configuration:
32
+ > - Pipeline: `<pipelineName>`
33
+ > - Stage: `<stageName>`
34
+ > - Event: `<event>`
35
+ > - Suite(s): `<suiteName(s)>`
36
+ > - Org: `<doce-org-alias>`
37
+ >
38
+ > Shall I proceed?"
39
+
40
+ Do not make the API call until the user confirms.
41
+
42
+ ## API call
43
+
44
+ ```bash
45
+ sf api request rest \
46
+ "/services/data/v67.0/connect/devopstesting/pipeline/<pipelineId>/stage/execute" \
47
+ --method POST \
48
+ --body '{
49
+ "stageId": "<stageId>",
50
+ "event": "<event>",
51
+ "testSuiteIds": ["<suiteId1>", "<suiteId2>"]
52
+ }' \
53
+ --target-org <doce-org-alias>
54
+ ```
55
+
56
+ ### Body schema
57
+
58
+ | Field | Type | Description |
59
+ |---|---|---|
60
+ | `stageId` | string | The ID of the pipeline stage to execute tests on |
61
+ | `event` | string | `Pre-Promote`, `Post-Promote`, or `Review` |
62
+ | `testSuiteIds` | string[] | One or more test suite IDs to execute |
63
+
64
+ ## On success
65
+
66
+ Extract the `runId` (or execution ID) from the response. Inform the user:
67
+
68
+ > "Tests are running in `<doce-org-alias>`. I'll update you when results are ready."
69
+
70
+ Immediately hand off the `runId` to the `polling-test-results` skill to begin the polling loop.
71
+
72
+ ## On error
73
+
74
+ | Status | Message to user |
75
+ |---|---|
76
+ | 400 | "The test execution request was invalid. Check that the stage and suite IDs are correct." |
77
+ | 403 | "You don't have permission to run tests on this pipeline. Check your DevOps Testing API access." |
78
+ | 404 | "The pipeline or stage was not found. It may have been deleted." |
79
+ | 500 | "The DevOps Center org returned a server error. Try again in a few minutes." |
80
+
81
+ Never expose raw API errors to the user.
82
+
83
+ ---
84
+
85
+ ## Retrigger mode (re-running a quality gate)
86
+
87
+ Use this mode when a promotion was blocked by a quality gate failure and the coverage gap has since been addressed.
88
+
89
+ ### Extra preconditions — all must be true before proceeding
90
+
91
+ 1. The `Coverage` field on the latest `DevopsTestSuiteExecution` meets or exceeds the threshold defined in the `DevopsQualityGateRule`
92
+ 2. The user has explicitly asked to retrigger the gate
93
+ 3. The same `pipelineId`, `stageId`, and `event` from the blocked promotion are known
94
+
95
+ If coverage is still below threshold, do **not** retrigger. Instead, respond:
96
+
97
+ > "Coverage is still at `<X>%`, below the `<threshold>%` gate. The gate cannot be retriggered until the threshold is met. Here are the remaining uncovered methods: `<list>`."
98
+
99
+ Do not retry. Explain what must be resolved first and stop.
100
+
101
+ ### Inputs required for retrigger
102
+
103
+ | Input | Source |
104
+ |---|---|
105
+ | `pipelineId` | From the blocked promotion context |
106
+ | `stageId` | From the blocked promotion context |
107
+ | `event` | Same event type that originally blocked (`Pre-Promote` or `Post-Promote`) |
108
+ | `suiteIds` | Same suites that were originally run |
109
+ | `doce-org-alias` | Established in Prerequisite 1 |
110
+
111
+ ### Confirmation gate (retrigger)
112
+
113
+ Before executing the API call, present this confirmation prompt and wait for explicit user approval:
114
+
115
+ > "Coverage is confirmed at `<X>%`, which meets the `<threshold>%` gate. I'll retrigger the quality gate check for the `<stageName>` stage (`<event>`). Confirm?"
116
+
117
+ Only proceed after the user confirms. If the user declines, stop without making any API call.
118
+
119
+ ### API call (retrigger)
120
+
121
+ Uses the same Connect API stage/execute endpoint:
122
+
123
+ ```bash
124
+ sf api request rest "/services/data/v67.0/connect/devopstesting/pipeline/<pipelineId>/stage/execute" --method POST --body '{"stageId":"<stageId>","event":"<event>","testSuiteIds":["<suiteId1>"]}' --target-org <doce-org-alias>
125
+ ```
126
+
127
+ After the call returns a `runId`, hand off to the `polling-test-results` skill with the new `runId` to monitor the execution result.
128
+
129
+ ### Error handling (retrigger)
130
+
131
+ If the API returns an error indicating the gate cannot be retriggered, respond with:
132
+
133
+ > "The quality gate cannot be retriggered right now. Reason: `<plain-language summary>`. Here's what needs to be resolved first: `<list>`."
134
+
135
+ Never expose raw API error details to the user.
136
+
137
+ ---
138
+
139
+ ## Related skills
140
+
141
+ - **`polling-test-results`** — poll for async run results after receiving the `runId`
142
+ - **`recommending-devops-tests`** — recommend which suites to run first before triggering execution
143
+ - **`managing-suite-assignments`** — assign or map a suite to a pipeline stage if it isn't linked yet
144
+ - **`configuring-quality-gate`** — configure a new gate or threshold