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.
Files changed (52) hide show
  1. package/.next-steps.md +179 -0
  2. package/README.md +13 -0
  3. package/dist/ai/summarizer.js +6 -3
  4. package/dist/ai/summarizer.js.map +1 -1
  5. package/dist/azure/test-cases.js +28 -10
  6. package/dist/azure/test-cases.js.map +1 -1
  7. package/dist/azure/work-items.d.ts +28 -0
  8. package/dist/azure/work-items.js +96 -0
  9. package/dist/azure/work-items.js.map +1 -1
  10. package/dist/cli.js +362 -25
  11. package/dist/cli.js.map +1 -1
  12. package/dist/config.d.ts +0 -5
  13. package/dist/config.js +17 -3
  14. package/dist/config.js.map +1 -1
  15. package/dist/issues/ado-bugs.d.ts +23 -0
  16. package/dist/issues/ado-bugs.js +59 -0
  17. package/dist/issues/ado-bugs.js.map +1 -0
  18. package/dist/issues/create-issues.d.ts +32 -0
  19. package/dist/issues/create-issues.js +236 -0
  20. package/dist/issues/create-issues.js.map +1 -0
  21. package/dist/issues/github.d.ts +22 -0
  22. package/dist/issues/github.js +95 -0
  23. package/dist/issues/github.js.map +1 -0
  24. package/dist/mcp-server.d.ts +3 -0
  25. package/dist/mcp-server.js +154 -20
  26. package/dist/mcp-server.js.map +1 -1
  27. package/dist/parsers/gherkin.js +2 -1
  28. package/dist/parsers/gherkin.js.map +1 -1
  29. package/dist/sync/engine.d.ts +41 -0
  30. package/dist/sync/engine.js +176 -10
  31. package/dist/sync/engine.js.map +1 -1
  32. package/dist/sync/manifest.d.ts +69 -0
  33. package/dist/sync/manifest.js +197 -0
  34. package/dist/sync/manifest.js.map +1 -0
  35. package/dist/sync/publish-results.d.ts +8 -1
  36. package/dist/sync/publish-results.js +167 -5
  37. package/dist/sync/publish-results.js.map +1 -1
  38. package/dist/sync/writeback.js +41 -20
  39. package/dist/sync/writeback.js.map +1 -1
  40. package/dist/types.d.ts +53 -0
  41. package/docs/mcp-server.md +131 -29
  42. package/docs/publish-test-results.md +136 -2
  43. package/docs/vscode-extension.md +139 -0
  44. package/llms.txt +28 -3
  45. package/package.json +2 -2
  46. package/.cucumber/.ado-sync-state.json +0 -282
  47. package/.cucumber/ado-sync.yaml +0 -21
  48. package/.cucumber/features/cart.feature +0 -62
  49. package/.cucumber/features/checkout.feature +0 -100
  50. package/.cucumber/features/homepage.feature +0 -7
  51. package/.cucumber/features/inventory.feature +0 -59
  52. 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 { SyncConfig } from '../types';
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
- sources.push({ filePath: path.resolve(configDir, f), format: opts.resultFormat });
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
- sources.push({ filePath: path.resolve(configDir, src.value), format: src.format });
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
- return { runId: 0, runUrl: '', totalResults: allResults.length, passed, failed, other };
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
- return { runId, runUrl, totalResults: allResults.length, passed, failed, other };
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