ado-sync 0.1.47 → 0.1.49
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/.next-steps.md +179 -0
- package/README.md +13 -0
- package/dist/ai/summarizer.js +6 -3
- package/dist/ai/summarizer.js.map +1 -1
- package/dist/azure/test-cases.js +28 -10
- package/dist/azure/test-cases.js.map +1 -1
- package/dist/azure/work-items.d.ts +28 -0
- package/dist/azure/work-items.js +96 -0
- package/dist/azure/work-items.js.map +1 -1
- package/dist/cli.js +362 -25
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +0 -5
- package/dist/config.js +17 -3
- package/dist/config.js.map +1 -1
- package/dist/issues/ado-bugs.d.ts +23 -0
- package/dist/issues/ado-bugs.js +59 -0
- package/dist/issues/ado-bugs.js.map +1 -0
- package/dist/issues/create-issues.d.ts +32 -0
- package/dist/issues/create-issues.js +236 -0
- package/dist/issues/create-issues.js.map +1 -0
- package/dist/issues/github.d.ts +22 -0
- package/dist/issues/github.js +95 -0
- package/dist/issues/github.js.map +1 -0
- package/dist/mcp-server.d.ts +3 -0
- package/dist/mcp-server.js +154 -20
- package/dist/mcp-server.js.map +1 -1
- package/dist/parsers/gherkin.js +2 -1
- package/dist/parsers/gherkin.js.map +1 -1
- package/dist/sync/engine.d.ts +41 -0
- package/dist/sync/engine.js +176 -10
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync/manifest.d.ts +69 -0
- package/dist/sync/manifest.js +197 -0
- package/dist/sync/manifest.js.map +1 -0
- package/dist/sync/publish-results.d.ts +8 -1
- package/dist/sync/publish-results.js +167 -5
- package/dist/sync/publish-results.js.map +1 -1
- package/dist/sync/writeback.js +41 -20
- package/dist/sync/writeback.js.map +1 -1
- package/dist/types.d.ts +53 -0
- package/docs/mcp-server.md +131 -29
- package/docs/publish-test-results.md +136 -2
- package/docs/vscode-extension.md +139 -0
- package/llms.txt +28 -3
- package/package.json +2 -2
- package/.cucumber/.ado-sync-state.json +0 -282
- package/.cucumber/ado-sync.yaml +0 -21
- package/.cucumber/features/cart.feature +0 -62
- package/.cucumber/features/checkout.feature +0 -100
- package/.cucumber/features/homepage.feature +0 -7
- package/.cucumber/features/inventory.feature +0 -59
- package/.cucumber/features/login.feature +0 -74
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* .ai-workflow-manifest.json generator
|
|
4
|
+
*
|
|
5
|
+
* Produces a structured JSON manifest that gives AI agents (Claude Code, Copilot,
|
|
6
|
+
* Cursor) the full context needed to drive the Planner → Generator → Push → CI →
|
|
7
|
+
* Publish cycle for a given User Story — the materia "AI-assisted BDD" pattern.
|
|
8
|
+
*
|
|
9
|
+
* Command: ado-sync generate --manifest --story-ids 1234
|
|
10
|
+
* MCP tool: generate_manifest({ storyIds: [1234] })
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.generateManifests = generateManifests;
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const client_1 = require("../azure/client");
|
|
50
|
+
const work_items_1 = require("../azure/work-items");
|
|
51
|
+
// ─── Main function ────────────────────────────────────────────────────────────
|
|
52
|
+
async function generateManifests(config, configDir, opts) {
|
|
53
|
+
if (!opts.storyIds.length) {
|
|
54
|
+
throw new Error('--story-ids is required for --manifest');
|
|
55
|
+
}
|
|
56
|
+
const client = await client_1.AzureClient.create(config);
|
|
57
|
+
const outputFolder = opts.outputFolder
|
|
58
|
+
? path.resolve(configDir, opts.outputFolder)
|
|
59
|
+
: configDir;
|
|
60
|
+
if (!opts.dryRun) {
|
|
61
|
+
fs.mkdirSync(outputFolder, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
const format = resolveManifestFormat(opts.format, config.local.type);
|
|
64
|
+
const specExt = specExtension(format, config.local.type);
|
|
65
|
+
const results = [];
|
|
66
|
+
for (const storyId of opts.storyIds) {
|
|
67
|
+
const ctx = await (0, work_items_1.getStoryContext)(client, config.project, storyId, config.orgUrl);
|
|
68
|
+
const manifest = buildManifest(ctx, outputFolder, specExt, format);
|
|
69
|
+
const filePath = path.join(outputFolder, `.ai-workflow-manifest-${storyId}.json`);
|
|
70
|
+
if (!opts.force && fs.existsSync(filePath)) {
|
|
71
|
+
results.push({ action: 'skipped', filePath, storyId, title: ctx.title });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!opts.dryRun) {
|
|
75
|
+
fs.writeFileSync(filePath, JSON.stringify(manifest, null, 2), 'utf8');
|
|
76
|
+
}
|
|
77
|
+
results.push({ action: 'created', filePath, storyId, title: ctx.title, manifest });
|
|
78
|
+
}
|
|
79
|
+
return results;
|
|
80
|
+
}
|
|
81
|
+
// ─── Manifest builder ─────────────────────────────────────────────────────────
|
|
82
|
+
function buildManifest(ctx, outputFolder, specExt, format) {
|
|
83
|
+
const slug = toKebabCase(ctx.title) || `story-${ctx.storyId}`;
|
|
84
|
+
const specFile = path.join(outputFolder, `${ctx.storyId}-${slug}${specExt}`);
|
|
85
|
+
const manifest = path.join(outputFolder, `.ai-workflow-manifest-${ctx.storyId}.json`);
|
|
86
|
+
const steps = [
|
|
87
|
+
{
|
|
88
|
+
step: 1, tool: 'validate_config', action: 'validate_config',
|
|
89
|
+
description: 'Verify Azure DevOps connection, project, and test plan are reachable.',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
step: 2, tool: 'get_story_context', action: 'get_story_context',
|
|
93
|
+
description: 'Fetch AC items, related test cases, and suggested tags for the story.',
|
|
94
|
+
input: { storyId: ctx.storyId },
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
step: 3, tool: 'generate_specs', action: 'generate_spec',
|
|
98
|
+
description: `Write a ${format} spec skeleton from AC items. One Scenario per AC bullet.`,
|
|
99
|
+
input: { storyIds: [ctx.storyId], format, dryRun: false },
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
step: 4, tool: 'editor', action: 'fill_steps',
|
|
103
|
+
description: 'Fill in the Given/When/Then steps. Apply suggested tags. Reference page objects.',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
step: 5, tool: 'push_specs', action: 'push_dry_run',
|
|
107
|
+
description: 'Preview test case creation — verify titles and step count before committing.',
|
|
108
|
+
input: { dryRun: true },
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
step: 6, tool: 'push_specs', action: 'push_specs',
|
|
112
|
+
description: 'Create test cases in Azure DevOps and write @tc:ID back into the spec file.',
|
|
113
|
+
input: { dryRun: false },
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
step: 7, tool: 'ci', action: 'run_tests',
|
|
117
|
+
description: 'Run tests in CI. Collect CTRF or Playwright JSON results.',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
step: 8, tool: 'publish_test_results', action: 'publish_results',
|
|
121
|
+
description: 'Publish results to ADO test run. File GitHub Issues for any failures.',
|
|
122
|
+
input: { createIssuesOnFailure: true },
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
const requiredDocuments = [
|
|
126
|
+
{
|
|
127
|
+
name: `ADO User Story #${ctx.storyId}`,
|
|
128
|
+
source: ctx.url,
|
|
129
|
+
status: 'available',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'Spec file',
|
|
133
|
+
path: specFile,
|
|
134
|
+
status: ctx.relatedTestCases.length > 0 ? 'available' : 'pending',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'Test results',
|
|
138
|
+
path: 'results/playwright.json',
|
|
139
|
+
status: 'pending',
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
const validationChecklist = [
|
|
143
|
+
`Spec covers all ${ctx.acItems.length} AC items (one Scenario per bullet)`,
|
|
144
|
+
'Each Scenario has filled Given/When/Then steps',
|
|
145
|
+
`Suggested tags applied: ${ctx.suggestedTags.join(', ') || '(none detected)'}`,
|
|
146
|
+
'ado-sync push --dry-run shows 0 errors',
|
|
147
|
+
'Test run pass rate ≥ 80%',
|
|
148
|
+
];
|
|
149
|
+
if (ctx.relatedTestCases.length > 0) {
|
|
150
|
+
validationChecklist.push(`Existing TCs verified: ${ctx.relatedTestCases.join(', ')}`);
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
version: '1.0',
|
|
154
|
+
generatedAt: new Date().toISOString(),
|
|
155
|
+
storyId: ctx.storyId,
|
|
156
|
+
title: ctx.title,
|
|
157
|
+
context: {
|
|
158
|
+
storyUrl: ctx.url,
|
|
159
|
+
acceptanceCriteria: ctx.acItems,
|
|
160
|
+
suggestedTags: ctx.suggestedTags,
|
|
161
|
+
suggestedActors: ctx.suggestedActors,
|
|
162
|
+
relatedTestCases: ctx.relatedTestCases,
|
|
163
|
+
},
|
|
164
|
+
workflow: { steps },
|
|
165
|
+
requiredDocuments,
|
|
166
|
+
validationChecklist,
|
|
167
|
+
outputPaths: {
|
|
168
|
+
specFile,
|
|
169
|
+
manifest,
|
|
170
|
+
testResults: 'results/playwright.json',
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
175
|
+
function resolveManifestFormat(explicit, localType) {
|
|
176
|
+
if (explicit)
|
|
177
|
+
return explicit;
|
|
178
|
+
if (localType === 'gherkin' || localType === 'reqnroll')
|
|
179
|
+
return 'gherkin';
|
|
180
|
+
return 'markdown';
|
|
181
|
+
}
|
|
182
|
+
function specExtension(format, localType) {
|
|
183
|
+
if (format === 'gherkin')
|
|
184
|
+
return '.feature';
|
|
185
|
+
if (localType === 'markdown')
|
|
186
|
+
return '.md';
|
|
187
|
+
return '.md';
|
|
188
|
+
}
|
|
189
|
+
function toKebabCase(str) {
|
|
190
|
+
return str
|
|
191
|
+
.toLowerCase()
|
|
192
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
193
|
+
.replace(/\s+/g, '-')
|
|
194
|
+
.replace(/-+/g, '-')
|
|
195
|
+
.replace(/^-|-$/g, '');
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/sync/manifest.ts"],"names":[],"mappings":"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"}
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
* NUnit TRX (via NUnit3TestAdapter) does NOT include [Property] values in the TRX format.
|
|
22
22
|
* Use `--logger "nunit3;LogFileName=results.xml"` to get the native NUnit XML with properties.
|
|
23
23
|
*/
|
|
24
|
-
import {
|
|
24
|
+
import { CreateIssuesResult } from '../issues/create-issues';
|
|
25
|
+
import { CreateIssuesConfig, SyncConfig } from '../types';
|
|
25
26
|
/**
|
|
26
27
|
* Azure DevOps AttachmentType enum values (from TestInterfaces.AttachmentType).
|
|
27
28
|
* Only GeneralAttachment and ConsoleLog are universally supported for test result
|
|
@@ -52,6 +53,8 @@ export interface PublishResult {
|
|
|
52
53
|
passed: number;
|
|
53
54
|
failed: number;
|
|
54
55
|
other: number;
|
|
56
|
+
/** Summary of issues filed for failures. Present only when createIssuesOnFailure is configured. */
|
|
57
|
+
issuesSummary?: CreateIssuesResult;
|
|
55
58
|
}
|
|
56
59
|
export declare function publishTestResults(config: SyncConfig, configDir: string, opts?: {
|
|
57
60
|
dryRun?: boolean;
|
|
@@ -61,4 +64,8 @@ export declare function publishTestResults(config: SyncConfig, configDir: string
|
|
|
61
64
|
buildId?: number;
|
|
62
65
|
/** Extra folder to scan for screenshots/videos/logs to attach to test results. */
|
|
63
66
|
attachmentsFolder?: string;
|
|
67
|
+
/** When true, file GitHub Issues or ADO Bugs for failures (uses config + CLI overrides). */
|
|
68
|
+
createIssuesOnFailure?: boolean;
|
|
69
|
+
/** CLI overrides for issue-creation config. */
|
|
70
|
+
issueOverrides?: Partial<CreateIssuesConfig>;
|
|
64
71
|
}): Promise<PublishResult>;
|
|
@@ -62,6 +62,7 @@ const fs = __importStar(require("fs"));
|
|
|
62
62
|
const glob_1 = require("glob");
|
|
63
63
|
const path = __importStar(require("path"));
|
|
64
64
|
const client_1 = require("../azure/client");
|
|
65
|
+
const create_issues_1 = require("../issues/create-issues");
|
|
65
66
|
// ─── Attachment helpers ───────────────────────────────────────────────────────
|
|
66
67
|
/** Map a file extension to the Azure DevOps attachment type. */
|
|
67
68
|
function extToAttachmentType(ext) {
|
|
@@ -962,6 +963,119 @@ function parseRustTestJson(content, tagPrefix, treatInconclusiveAs) {
|
|
|
962
963
|
}
|
|
963
964
|
return results;
|
|
964
965
|
}
|
|
966
|
+
// ─── CTRF JSON parser ─────────────────────────────────────────────────────────
|
|
967
|
+
//
|
|
968
|
+
// Common Test Report Format — https://ctrf.io
|
|
969
|
+
// Produced by @ctrf/playwright-ctrf-json-reporter, jest-ctrf-json-reporter,
|
|
970
|
+
// cypress-ctrf-json-reporter, and others.
|
|
971
|
+
//
|
|
972
|
+
// {
|
|
973
|
+
// "results": {
|
|
974
|
+
// "tool": { "name": "playwright" },
|
|
975
|
+
// "summary": { "tests": 10, "passed": 8, "failed": 1, "skipped": 1, ... },
|
|
976
|
+
// "tests": [
|
|
977
|
+
// {
|
|
978
|
+
// "name": "should login",
|
|
979
|
+
// "status": "passed", // passed | failed | skipped | pending | other
|
|
980
|
+
// "duration": 1234, // milliseconds
|
|
981
|
+
// "suite": "Login Tests", // optional — becomes "suite > name"
|
|
982
|
+
// "message": "Error message", // populated on failure
|
|
983
|
+
// "trace": "Stack trace", // populated on failure
|
|
984
|
+
// "tags": ["@tc:12345", "@smoke"],
|
|
985
|
+
// "filepath": "tests/login.spec.ts",
|
|
986
|
+
// "retries": 2,
|
|
987
|
+
// "flaky": false,
|
|
988
|
+
// "attachments": [
|
|
989
|
+
// { "name": "screenshot", "contentType": "image/png", "path": "/abs/path/..." }
|
|
990
|
+
// ],
|
|
991
|
+
// "stdout": ["line1", "line2"],
|
|
992
|
+
// "stderr": ["line1"]
|
|
993
|
+
// }
|
|
994
|
+
// ],
|
|
995
|
+
// "environment": { "appName": "MyApp", "buildNumber": "42" }
|
|
996
|
+
// }
|
|
997
|
+
// }
|
|
998
|
+
//
|
|
999
|
+
// TC ID extraction:
|
|
1000
|
+
// 1. tags[] — look for "@tc:12345" or "tc:12345" entries
|
|
1001
|
+
// 2. name — fallback: match @tc:12345 in the test name string
|
|
1002
|
+
function parseCtrfJson(content, tagPrefix, treatInconclusiveAs, resultFileDir = '') {
|
|
1003
|
+
const report = JSON.parse(content);
|
|
1004
|
+
const ctrfResults = report?.results;
|
|
1005
|
+
if (!ctrfResults)
|
|
1006
|
+
return [];
|
|
1007
|
+
const tests = ctrfResults.tests ?? [];
|
|
1008
|
+
const idRe = new RegExp(`(?:@)?${tagPrefix}:(\\d+)`, 'i');
|
|
1009
|
+
const results = [];
|
|
1010
|
+
for (const t of tests) {
|
|
1011
|
+
const status = t.status ?? 'other';
|
|
1012
|
+
let outcome;
|
|
1013
|
+
if (status === 'passed')
|
|
1014
|
+
outcome = 'Passed';
|
|
1015
|
+
else if (status === 'failed')
|
|
1016
|
+
outcome = 'Failed';
|
|
1017
|
+
else
|
|
1018
|
+
outcome = 'NotExecuted'; // skipped | pending | other
|
|
1019
|
+
outcome = normaliseOutcome(outcome, treatInconclusiveAs);
|
|
1020
|
+
// Build test name: "Suite > name" when suite is present
|
|
1021
|
+
const suite = t.suite ?? '';
|
|
1022
|
+
const name = t.name ?? '';
|
|
1023
|
+
const testName = suite ? `${suite} > ${name}` : name;
|
|
1024
|
+
// TC ID — tags first, then name
|
|
1025
|
+
let testCaseId;
|
|
1026
|
+
for (const tag of t.tags ?? []) {
|
|
1027
|
+
const m = String(tag).match(idRe);
|
|
1028
|
+
if (m) {
|
|
1029
|
+
testCaseId = parseInt(m[1], 10);
|
|
1030
|
+
break;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (testCaseId === undefined) {
|
|
1034
|
+
const nameMatch = name.match(idRe);
|
|
1035
|
+
if (nameMatch)
|
|
1036
|
+
testCaseId = parseInt(nameMatch[1], 10);
|
|
1037
|
+
}
|
|
1038
|
+
// Attachments (path-based files)
|
|
1039
|
+
const ctrfAttachments = [];
|
|
1040
|
+
for (const att of t.attachments ?? []) {
|
|
1041
|
+
const filePath = att.path ?? '';
|
|
1042
|
+
const contentType = att.contentType ?? '';
|
|
1043
|
+
if (!filePath)
|
|
1044
|
+
continue;
|
|
1045
|
+
const ext = path.extname(filePath);
|
|
1046
|
+
const absPath = resultFileDir && !path.isAbsolute(filePath)
|
|
1047
|
+
? path.resolve(resultFileDir, filePath)
|
|
1048
|
+
: filePath;
|
|
1049
|
+
const data = safeReadFile(absPath);
|
|
1050
|
+
if (data) {
|
|
1051
|
+
const attName = att.name ?? path.basename(filePath, ext);
|
|
1052
|
+
const fileName = `${attName}${ext || mimeToExt(contentType)}`;
|
|
1053
|
+
ctrfAttachments.push({
|
|
1054
|
+
fileName,
|
|
1055
|
+
data,
|
|
1056
|
+
attachmentType: mimeToAttachmentType(contentType) || extToAttachmentType(ext),
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
// stdout / stderr arrays → log attachments
|
|
1061
|
+
const stdoutText = (t.stdout ?? []).join('').trim();
|
|
1062
|
+
const stderrText = (t.stderr ?? []).join('').trim();
|
|
1063
|
+
if (stdoutText)
|
|
1064
|
+
ctrfAttachments.push({ fileName: 'stdout.log', data: Buffer.from(stdoutText, 'utf8'), attachmentType: 'ConsoleLog' });
|
|
1065
|
+
if (stderrText)
|
|
1066
|
+
ctrfAttachments.push({ fileName: 'stderr.log', data: Buffer.from(stderrText, 'utf8'), attachmentType: 'ConsoleLog' });
|
|
1067
|
+
results.push({
|
|
1068
|
+
testName,
|
|
1069
|
+
outcome,
|
|
1070
|
+
durationMs: t.duration ?? 0,
|
|
1071
|
+
errorMessage: t.message || undefined,
|
|
1072
|
+
stackTrace: t.trace || undefined,
|
|
1073
|
+
testCaseId,
|
|
1074
|
+
attachments: ctrfAttachments.length ? ctrfAttachments : undefined,
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
return results;
|
|
1078
|
+
}
|
|
965
1079
|
// ─── Auto-detect format ───────────────────────────────────────────────────────
|
|
966
1080
|
function detectFormat(filePath, content) {
|
|
967
1081
|
const ext = path.extname(filePath).toLowerCase();
|
|
@@ -996,6 +1110,10 @@ function detectFormat(filePath, content) {
|
|
|
996
1110
|
// RSpec JSON has top-level "examples" array and "summary" object
|
|
997
1111
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && Array.isArray(parsed.examples) && parsed.summary)
|
|
998
1112
|
return 'rspecJson';
|
|
1113
|
+
// CTRF JSON has top-level "results" object with a "tests" array
|
|
1114
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) &&
|
|
1115
|
+
parsed.results && typeof parsed.results === 'object' && Array.isArray(parsed.results.tests))
|
|
1116
|
+
return 'ctrfJson';
|
|
999
1117
|
}
|
|
1000
1118
|
catch { /* fall through */ }
|
|
1001
1119
|
return 'cucumberJson';
|
|
@@ -1074,16 +1192,41 @@ async function publishTestResults(config, configDir, opts = {}) {
|
|
|
1074
1192
|
throw new Error('No publishTestResults configuration and no --testResult files specified.');
|
|
1075
1193
|
}
|
|
1076
1194
|
const tagPrefix = config.sync?.tagPrefix ?? 'tc';
|
|
1077
|
-
// Gather result files
|
|
1195
|
+
// Gather result files — entries may be literal paths or glob patterns
|
|
1078
1196
|
const sources = [];
|
|
1197
|
+
function isGlobPattern(p) {
|
|
1198
|
+
return p.includes('*') || p.includes('?') || p.includes('{');
|
|
1199
|
+
}
|
|
1079
1200
|
if (opts.resultFiles?.length) {
|
|
1080
1201
|
for (const f of opts.resultFiles) {
|
|
1081
|
-
|
|
1202
|
+
if (isGlobPattern(f)) {
|
|
1203
|
+
const matches = await (0, glob_1.glob)(f, { cwd: configDir, absolute: true });
|
|
1204
|
+
if (matches.length === 0) {
|
|
1205
|
+
process.stderr.write(` [warn] No files matched glob pattern: ${f}\n`);
|
|
1206
|
+
}
|
|
1207
|
+
for (const match of matches.sort()) {
|
|
1208
|
+
sources.push({ filePath: match, format: opts.resultFormat });
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
else {
|
|
1212
|
+
sources.push({ filePath: path.resolve(configDir, f), format: opts.resultFormat });
|
|
1213
|
+
}
|
|
1082
1214
|
}
|
|
1083
1215
|
}
|
|
1084
1216
|
else if (pubConfig?.testResult?.sources) {
|
|
1085
1217
|
for (const src of pubConfig.testResult.sources) {
|
|
1086
|
-
|
|
1218
|
+
if (isGlobPattern(src.value)) {
|
|
1219
|
+
const matches = await (0, glob_1.glob)(src.value, { cwd: configDir, absolute: true });
|
|
1220
|
+
if (matches.length === 0) {
|
|
1221
|
+
process.stderr.write(` [warn] No files matched glob pattern: ${src.value}\n`);
|
|
1222
|
+
}
|
|
1223
|
+
for (const match of matches.sort()) {
|
|
1224
|
+
sources.push({ filePath: match, format: src.format });
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
else {
|
|
1228
|
+
sources.push({ filePath: path.resolve(configDir, src.value), format: src.format });
|
|
1229
|
+
}
|
|
1087
1230
|
}
|
|
1088
1231
|
}
|
|
1089
1232
|
// Parse all result files
|
|
@@ -1124,6 +1267,9 @@ async function publishTestResults(config, configDir, opts = {}) {
|
|
|
1124
1267
|
case 'rspecJson':
|
|
1125
1268
|
allResults.push(...parseRspecJson(content, tagPrefix, treatInconclusiveAs));
|
|
1126
1269
|
break;
|
|
1270
|
+
case 'ctrfJson':
|
|
1271
|
+
allResults.push(...parseCtrfJson(content, tagPrefix, treatInconclusiveAs, fileDir));
|
|
1272
|
+
break;
|
|
1127
1273
|
case 'rustTest':
|
|
1128
1274
|
allResults.push(...parseRustTestJson(content, tagPrefix, treatInconclusiveAs));
|
|
1129
1275
|
break;
|
|
@@ -1138,7 +1284,15 @@ async function publishTestResults(config, configDir, opts = {}) {
|
|
|
1138
1284
|
const failed = allResults.filter((r) => r.outcome === 'Failed').length;
|
|
1139
1285
|
const other = allResults.length - passed - failed;
|
|
1140
1286
|
if (opts.dryRun) {
|
|
1141
|
-
|
|
1287
|
+
// Still run issue creation logic in dry-run so the caller gets a preview
|
|
1288
|
+
const issueConfig = opts.createIssuesOnFailure || pubConfig?.createIssuesOnFailure
|
|
1289
|
+
? { ...(pubConfig?.createIssuesOnFailure ?? {}), ...(opts.issueOverrides ?? {}) }
|
|
1290
|
+
: undefined;
|
|
1291
|
+
const issuesSummary = issueConfig && failed > 0
|
|
1292
|
+
? await (0, create_issues_1.createIssuesFromResults)(allResults, config, issueConfig, { totalResults: allResults.length },
|
|
1293
|
+
/* dryRun */ true)
|
|
1294
|
+
: undefined;
|
|
1295
|
+
return { runId: 0, runUrl: '', totalResults: allResults.length, passed, failed, other, issuesSummary };
|
|
1142
1296
|
}
|
|
1143
1297
|
const client = await client_1.AzureClient.create(config);
|
|
1144
1298
|
const testApi = await client.getTestApi();
|
|
@@ -1232,6 +1386,14 @@ async function publishTestResults(config, configDir, opts = {}) {
|
|
|
1232
1386
|
}
|
|
1233
1387
|
await testApi.updateTestRun({ state: 'Completed' }, config.project, runId);
|
|
1234
1388
|
const runUrl = `${config.orgUrl}/${config.project}/_testManagement/runs?runId=${runId}`;
|
|
1235
|
-
|
|
1389
|
+
// ── Create issues for failures ────────────────────────────────────────────
|
|
1390
|
+
let issuesSummary;
|
|
1391
|
+
const issueConfig = opts.createIssuesOnFailure || pubConfig?.createIssuesOnFailure
|
|
1392
|
+
? { ...(pubConfig?.createIssuesOnFailure ?? {}), ...(opts.issueOverrides ?? {}) }
|
|
1393
|
+
: undefined;
|
|
1394
|
+
if (issueConfig && failed > 0) {
|
|
1395
|
+
issuesSummary = await (0, create_issues_1.createIssuesFromResults)(allResults, config, issueConfig, { runId, runUrl, buildId: opts.buildId, totalResults: allResults.length }, opts.dryRun);
|
|
1396
|
+
}
|
|
1397
|
+
return { runId, runUrl, totalResults: allResults.length, passed, failed, other, issuesSummary };
|
|
1236
1398
|
}
|
|
1237
1399
|
//# sourceMappingURL=publish-results.js.map
|