@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.
- package/package.json +1 -1
- package/skills/analyzing-test-failures/SKILL.md +159 -0
- package/skills/building-sf-integrations/SKILL.md +1 -1
- package/skills/checking-devops-prerequisites/SKILL.md +141 -0
- package/skills/configuring-code-analyzer/SKILL.md +482 -0
- package/skills/configuring-code-analyzer/examples/apex-project-config.yml +41 -0
- package/skills/configuring-code-analyzer/examples/ci-github-actions.yml +96 -0
- package/skills/configuring-code-analyzer/examples/fullstack-project-config.yml +46 -0
- package/skills/configuring-code-analyzer/examples/lwc-project-config.yml +26 -0
- package/skills/configuring-code-analyzer/references/ci-cd-templates.md +648 -0
- package/skills/configuring-code-analyzer/references/config-schema.md +257 -0
- package/skills/configuring-code-analyzer/references/diagnostic-flow.md +70 -0
- package/skills/configuring-code-analyzer/references/engine-prerequisites.md +276 -0
- package/skills/configuring-code-analyzer/references/rule-name-resolution.md +67 -0
- package/skills/configuring-code-analyzer/references/troubleshooting.md +298 -0
- package/skills/configuring-code-analyzer/scripts/check-prerequisites.sh +189 -0
- package/skills/configuring-code-analyzer/scripts/generate-config.sh +143 -0
- package/skills/configuring-code-analyzer/scripts/validate-config.sh +153 -0
- package/skills/configuring-quality-gate/SKILL.md +120 -0
- package/skills/configuring-test-provider/SKILL.md +113 -0
- package/skills/creating-fix-work-item/SKILL.md +66 -0
- package/skills/managing-cdc-enablement/SKILL.md +164 -0
- package/skills/managing-cdc-enablement/assets/PlatformEventChannel-template.xml +5 -0
- package/skills/managing-cdc-enablement/assets/PlatformEventChannelMember-template.xml +11 -0
- package/skills/managing-cdc-enablement/references/deploy-troubleshooting.md +73 -0
- package/skills/managing-cdc-enablement/references/filter-expressions.md +93 -0
- package/skills/managing-suite-assignments/SKILL.md +161 -0
- package/skills/polling-test-results/SKILL.md +72 -0
- package/skills/recommending-devops-tests/SKILL.md +137 -0
- package/skills/running-code-analyzer/SKILL.md +264 -267
- package/skills/running-code-analyzer/references/post-scan-workflows.md +286 -0
- package/skills/running-code-analyzer/scripts/describe-rule.js +382 -0
- package/skills/running-code-analyzer/scripts/list-rules.js +260 -0
- package/skills/running-code-analyzer/scripts/query-results.js +230 -0
- package/skills/running-devops-test-suite/SKILL.md +144 -0
- package/skills/syncing-test-providers/SKILL.md +108 -0
- package/skills/using-salesforce-archive/SKILL.md +121 -0
- package/skills/using-salesforce-archive/examples/monitor-failed-jobs.md +47 -0
- package/skills/using-salesforce-archive/references/archive-activity-entity.md +59 -0
- 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
|