@automatify-au/cli 0.1.13 → 0.1.14

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.
@@ -10817,7 +10817,9 @@ function readConfigFile(configPath) {
10817
10817
  function writeConfigFile(configPath, file) {
10818
10818
  (0, import_node_fs.writeFileSync)(configPath, `${JSON.stringify(file, null, 2)}
10819
10819
  `, "utf8");
10820
- (0, import_node_fs.chmodSync)(configPath, 384);
10820
+ if (process.platform !== "win32") {
10821
+ (0, import_node_fs.chmodSync)(configPath, 384);
10822
+ }
10821
10823
  }
10822
10824
  function normalizeBaseUrl(value) {
10823
10825
  if (value.endsWith("/")) {
@@ -10852,6 +10854,41 @@ function setKeychainSecret(account, value) {
10852
10854
  stdio: ["ignore", "ignore", "pipe"]
10853
10855
  });
10854
10856
  }
10857
+ function protectWindowsSecret(value) {
10858
+ const script = [
10859
+ "Add-Type -AssemblyName System.Security",
10860
+ "$plain = [Console]::In.ReadToEnd()",
10861
+ "$bytes = [Text.Encoding]::UTF8.GetBytes($plain)",
10862
+ "$cipher = [System.Security.Cryptography.ProtectedData]::Protect($bytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)",
10863
+ "[Console]::Out.Write([Convert]::ToBase64String($cipher))"
10864
+ ].join("; ");
10865
+ return (0, import_node_child_process.execFileSync)("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
10866
+ input: value,
10867
+ encoding: "utf8",
10868
+ stdio: ["pipe", "pipe", "pipe"]
10869
+ }).trim();
10870
+ }
10871
+ function unprotectWindowsSecret(protectedValue) {
10872
+ if (!protectedValue || process.platform !== "win32") {
10873
+ return "";
10874
+ }
10875
+ const script = [
10876
+ "Add-Type -AssemblyName System.Security",
10877
+ "$encoded = [Console]::In.ReadToEnd()",
10878
+ "$cipher = [Convert]::FromBase64String($encoded)",
10879
+ "$bytes = [System.Security.Cryptography.ProtectedData]::Unprotect($cipher, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)",
10880
+ "[Console]::Out.Write([Text.Encoding]::UTF8.GetString($bytes))"
10881
+ ].join("; ");
10882
+ try {
10883
+ return (0, import_node_child_process.execFileSync)("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
10884
+ input: protectedValue,
10885
+ encoding: "utf8",
10886
+ stdio: ["pipe", "pipe", "ignore"]
10887
+ }).trim();
10888
+ } catch {
10889
+ return "";
10890
+ }
10891
+ }
10855
10892
  function valueFromPrecedence(key, flagValue, envValue, fileValue, fallback = "") {
10856
10893
  if (flagValue) {
10857
10894
  return { value: flagValue, source: "flag" };
@@ -10926,7 +10963,8 @@ function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
10926
10963
  const forgeAuthTokenFromEnv = toStringValue(env.TESTOPS_FORGE_AUTH_TOKEN);
10927
10964
  const forgeAuthTokenFromFile = toStringValue(file.forgeAuthToken ?? file.TESTOPS_FORGE_AUTH_TOKEN);
10928
10965
  const forgeAuthTokenFromKeychain = forgeAuthTokenFromEnv || forgeAuthTokenFromFile || !forgeAuthTokenKeychainAccountResolved.value ? "" : readKeychainSecret(forgeAuthTokenKeychainAccountResolved.value);
10929
- const forgeAuthTokenResolved = forgeAuthTokenFromEnv ? { value: forgeAuthTokenFromEnv, source: "env" } : forgeAuthTokenFromFile ? { value: forgeAuthTokenFromFile, source: "file" } : forgeAuthTokenFromKeychain ? { value: forgeAuthTokenFromKeychain, source: "keychain" } : { value: "", source: "default" };
10966
+ const forgeAuthTokenFromWindowsStore = forgeAuthTokenFromEnv || forgeAuthTokenFromFile || forgeAuthTokenFromKeychain ? "" : unprotectWindowsSecret(toStringValue(file.forgeAuthTokenProtected));
10967
+ const forgeAuthTokenResolved = forgeAuthTokenFromEnv ? { value: forgeAuthTokenFromEnv, source: "env" } : forgeAuthTokenFromFile ? { value: forgeAuthTokenFromFile, source: "file" } : forgeAuthTokenFromKeychain ? { value: forgeAuthTokenFromKeychain, source: "keychain" } : forgeAuthTokenFromWindowsStore ? { value: forgeAuthTokenFromWindowsStore, source: "secure-store" } : { value: "", source: "default" };
10930
10968
  const config = {
10931
10969
  baseUrl: normalizeBaseUrl(baseUrlResolved.value),
10932
10970
  projectKey: projectKeyResolved.value,
@@ -11054,12 +11092,44 @@ function applyConfigSet(configPath, rawKey, value) {
11054
11092
  }
11055
11093
  const file = readConfigFile(configPath);
11056
11094
  if (key === "forgeAuthToken") {
11095
+ if (process.platform === "win32") {
11096
+ try {
11097
+ const protectedValue = protectWindowsSecret(value);
11098
+ writeConfigFile(configPath, {
11099
+ ...file,
11100
+ forgeAuthToken: void 0,
11101
+ TESTOPS_FORGE_AUTH_TOKEN: void 0,
11102
+ forgeAuthTokenKeychainAccount: void 0,
11103
+ forgeAuthTokenProtected: protectedValue
11104
+ });
11105
+ return {
11106
+ exitCode: ExitCode.Success,
11107
+ stdout: ["Config updated: forgeAuthToken protected with Windows DPAPI for the current user."]
11108
+ };
11109
+ } catch (error) {
11110
+ return {
11111
+ exitCode: ExitCode.ValidationError,
11112
+ stderr: [
11113
+ `Unable to store forgeAuthToken securely: ${error instanceof Error ? error.message : "unknown secure-storage error"}`
11114
+ ]
11115
+ };
11116
+ }
11117
+ }
11118
+ if (process.platform !== "darwin") {
11119
+ return {
11120
+ exitCode: ExitCode.UsageError,
11121
+ stderr: [
11122
+ "Secure local forgeAuthToken storage is currently supported on macOS and Windows. On Linux/CI, set TESTOPS_FORGE_AUTH_TOKEN in the environment."
11123
+ ]
11124
+ };
11125
+ }
11057
11126
  const account = toStringValue(file.forgeAuthTokenKeychainAccount) || defaultKeychainAccount(configPath);
11058
11127
  setKeychainSecret(account, value);
11059
11128
  const nextFile2 = {
11060
11129
  ...file,
11061
11130
  forgeAuthToken: void 0,
11062
11131
  TESTOPS_FORGE_AUTH_TOKEN: void 0,
11132
+ forgeAuthTokenProtected: void 0,
11063
11133
  forgeAuthTokenKeychainAccount: account
11064
11134
  };
11065
11135
  writeConfigFile(configPath, nextFile2);
@@ -11093,7 +11163,11 @@ function createConfigHandler(env = process.env, cwd = process.cwd()) {
11093
11163
  if (validation.ok) {
11094
11164
  return {
11095
11165
  exitCode: ExitCode.Success,
11096
- stdout: ["Config validation: PASS", ...toDisplayLines(resolution), ...validation.warnings.map((line) => `WARN: ${line}`)]
11166
+ stdout: [
11167
+ "Config validation: PASS",
11168
+ ...toDisplayLines(resolution),
11169
+ ...validation.warnings.map((line) => `WARN: ${line}`)
11170
+ ]
11097
11171
  };
11098
11172
  }
11099
11173
  return {
@@ -11506,9 +11580,11 @@ function normalizePath(pathValue) {
11506
11580
  return pathValue.replaceAll("\\", "/");
11507
11581
  }
11508
11582
  function discoverFeatureFiles(scannedFiles) {
11509
- const featurePaths = [...new Set(
11510
- scannedFiles.map((filePath) => normalizePath(filePath)).filter((filePath) => filePath.toLowerCase().endsWith(".feature"))
11511
- )].sort((a, b) => a.localeCompare(b));
11583
+ const featurePaths = [
11584
+ ...new Set(
11585
+ scannedFiles.map((filePath) => normalizePath(filePath)).filter((filePath) => filePath.toLowerCase().endsWith(".feature"))
11586
+ )
11587
+ ].sort((a, b) => a.localeCompare(b));
11512
11588
  return {
11513
11589
  total: featurePaths.length,
11514
11590
  paths: featurePaths,
@@ -11747,12 +11823,7 @@ function scanProjectFiles(options) {
11747
11823
  }
11748
11824
 
11749
11825
  // src/auto.ts
11750
- var DETERMINISTIC_STEPS = [
11751
- "detect",
11752
- "discover",
11753
- "map-preview",
11754
- "upload"
11755
- ];
11826
+ var DETERMINISTIC_STEPS = ["detect", "discover", "map-preview", "upload"];
11756
11827
  function parseArgs(args) {
11757
11828
  const flags = {};
11758
11829
  const boolFlags = /* @__PURE__ */ new Set();
@@ -12400,10 +12471,7 @@ function createAutoHandler(deps = {}) {
12400
12471
  const diagnostic = diagMissingProjectContext();
12401
12472
  return {
12402
12473
  exitCode: ExitCode.ValidationError,
12403
- stderr: [
12404
- `ERROR: ${diagnostic.code}: ${diagnostic.message}`,
12405
- `SUGGESTION: ${diagnostic.suggestion}`
12406
- ]
12474
+ stderr: [`ERROR: ${diagnostic.code}: ${diagnostic.message}`, `SUGGESTION: ${diagnostic.suggestion}`]
12407
12475
  };
12408
12476
  }
12409
12477
  execution = await executeUploadFlow({
@@ -13024,17 +13092,19 @@ function createAllureOpenHandler(deps = {}) {
13024
13092
  if (useJson) {
13025
13093
  return {
13026
13094
  exitCode: ExitCode.Success,
13027
- stdout: toJsonLine(buildJsonOutput({
13028
- zipPath,
13029
- dryRun: true,
13030
- served: false,
13031
- opened: false,
13032
- keep,
13033
- extractDir: extractDirRaw,
13034
- rootDir: extractDirRaw,
13035
- indexPath,
13036
- summary
13037
- }))
13095
+ stdout: toJsonLine(
13096
+ buildJsonOutput({
13097
+ zipPath,
13098
+ dryRun: true,
13099
+ served: false,
13100
+ opened: false,
13101
+ keep,
13102
+ extractDir: extractDirRaw,
13103
+ rootDir: extractDirRaw,
13104
+ indexPath,
13105
+ summary
13106
+ })
13107
+ )
13038
13108
  };
13039
13109
  }
13040
13110
  return {
@@ -13084,20 +13154,24 @@ function createAllureOpenHandler(deps = {}) {
13084
13154
  }
13085
13155
  }
13086
13156
  if (useJson) {
13087
- logger.log(JSON.stringify(buildJsonOutput({
13088
- zipPath,
13089
- dryRun: false,
13090
- served: true,
13091
- opened,
13092
- keep,
13093
- extractDir: extractResult.extractDir,
13094
- rootDir: extractResult.rootDir,
13095
- indexPath: extractResult.indexPath,
13096
- url: serverHandle.url,
13097
- port: serverHandle.port,
13098
- host: serverHandle.host,
13099
- summary
13100
- })));
13157
+ logger.log(
13158
+ JSON.stringify(
13159
+ buildJsonOutput({
13160
+ zipPath,
13161
+ dryRun: false,
13162
+ served: true,
13163
+ opened,
13164
+ keep,
13165
+ extractDir: extractResult.extractDir,
13166
+ rootDir: extractResult.rootDir,
13167
+ indexPath: extractResult.indexPath,
13168
+ url: serverHandle.url,
13169
+ port: serverHandle.port,
13170
+ host: serverHandle.host,
13171
+ summary
13172
+ })
13173
+ )
13174
+ );
13101
13175
  } else {
13102
13176
  logger.log(`Allure report extracted to ${extractResult.extractDir}.`);
13103
13177
  if (verbose) {
@@ -13385,13 +13459,15 @@ function createAllureHandler(deps = {}) {
13385
13459
  if (useJson) {
13386
13460
  return {
13387
13461
  exitCode: ExitCode.Success,
13388
- stdout: toJsonLine(buildDownloadJsonOutput({
13389
- issueKey: issueKey2,
13390
- dryRun: true,
13391
- downloaded: false,
13392
- attachment: selected,
13393
- outputPath
13394
- }))
13462
+ stdout: toJsonLine(
13463
+ buildDownloadJsonOutput({
13464
+ issueKey: issueKey2,
13465
+ dryRun: true,
13466
+ downloaded: false,
13467
+ attachment: selected,
13468
+ outputPath
13469
+ })
13470
+ )
13395
13471
  };
13396
13472
  }
13397
13473
  return {
@@ -13418,13 +13494,15 @@ function createAllureHandler(deps = {}) {
13418
13494
  if (useJson) {
13419
13495
  return {
13420
13496
  exitCode: ExitCode.Success,
13421
- stdout: toJsonLine(buildDownloadJsonOutput({
13422
- issueKey: issueKey2,
13423
- dryRun: false,
13424
- downloaded: true,
13425
- attachment: downloaded,
13426
- outputPath
13427
- }))
13497
+ stdout: toJsonLine(
13498
+ buildDownloadJsonOutput({
13499
+ issueKey: issueKey2,
13500
+ dryRun: false,
13501
+ downloaded: true,
13502
+ attachment: downloaded,
13503
+ outputPath
13504
+ })
13505
+ )
13428
13506
  };
13429
13507
  }
13430
13508
  return {
@@ -13472,14 +13550,16 @@ function createAllureHandler(deps = {}) {
13472
13550
  if (useJson) {
13473
13551
  return {
13474
13552
  exitCode: ExitCode.Success,
13475
- stdout: toJsonLine(buildJsonOutput2({
13476
- issueKey,
13477
- uploaded: false,
13478
- commentAdded: false,
13479
- dryRun: true,
13480
- summary,
13481
- attachment: { filename: attachmentFilename }
13482
- }))
13553
+ stdout: toJsonLine(
13554
+ buildJsonOutput2({
13555
+ issueKey,
13556
+ uploaded: false,
13557
+ commentAdded: false,
13558
+ dryRun: true,
13559
+ summary,
13560
+ attachment: { filename: attachmentFilename }
13561
+ })
13562
+ )
13483
13563
  };
13484
13564
  }
13485
13565
  return {
@@ -13515,14 +13595,16 @@ function createAllureHandler(deps = {}) {
13515
13595
  if (useJson) {
13516
13596
  return {
13517
13597
  exitCode: ExitCode.Success,
13518
- stdout: toJsonLine(buildJsonOutput2({
13519
- issueKey,
13520
- uploaded: true,
13521
- commentAdded,
13522
- dryRun: false,
13523
- summary,
13524
- attachment
13525
- }))
13598
+ stdout: toJsonLine(
13599
+ buildJsonOutput2({
13600
+ issueKey,
13601
+ uploaded: true,
13602
+ commentAdded,
13603
+ dryRun: false,
13604
+ summary,
13605
+ attachment
13606
+ })
13607
+ )
13526
13608
  };
13527
13609
  }
13528
13610
  return {
@@ -13860,9 +13942,11 @@ function parseScenarioFeatureFile(raw, filePath) {
13860
13942
  if (!featureName || !scenarioName || !scenarioKey || steps.length === 0 || scenarioCount !== 1) {
13861
13943
  return null;
13862
13944
  }
13863
- const linkedIssueKeys = [...new Set(
13864
- tags.map((tag) => tag.replace(/^@/, "").trim().toUpperCase()).filter((tag) => ISSUE_KEY_PATTERN.test(tag))
13865
- )];
13945
+ const linkedIssueKeys = [
13946
+ ...new Set(
13947
+ tags.map((tag) => tag.replace(/^@/, "").trim().toUpperCase()).filter((tag) => ISSUE_KEY_PATTERN.test(tag))
13948
+ )
13949
+ ];
13866
13950
  return {
13867
13951
  scenarioKey,
13868
13952
  featureName,
@@ -14060,7 +14144,9 @@ function createBddHandler(deps = {}) {
14060
14144
  stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
14061
14145
  };
14062
14146
  }
14063
- const features = (featureId ? result.data.filter((feature) => feature.id === featureId) : result.data).sort((left, right) => left.payload.name.localeCompare(right.payload.name) || left.createdAt.localeCompare(right.createdAt));
14147
+ const features = (featureId ? result.data.filter((feature) => feature.id === featureId) : result.data).sort(
14148
+ (left, right) => left.payload.name.localeCompare(right.payload.name) || left.createdAt.localeCompare(right.createdAt)
14149
+ );
14064
14150
  if (featureId && features.length === 0) {
14065
14151
  return {
14066
14152
  exitCode: ExitCode.RemoteError,
@@ -14180,7 +14266,9 @@ function createBddHandler(deps = {}) {
14180
14266
  stderr: [`ERROR: ${scenariosResult.error.code}: ${scenariosResult.error.message}`]
14181
14267
  };
14182
14268
  }
14183
- const selectedScenarios = exportAll ? [...scenariosResult.data].sort((left, right) => (left.key ?? left.id).localeCompare(right.key ?? right.id)) : scenariosResult.data.filter(
14269
+ const selectedScenarios = exportAll ? [...scenariosResult.data].sort(
14270
+ (left, right) => (left.key ?? left.id).localeCompare(right.key ?? right.id)
14271
+ ) : scenariosResult.data.filter(
14184
14272
  (item) => item.id === scenarioSelector2 || (item.key ?? "").toUpperCase() === scenarioSelector2.toUpperCase()
14185
14273
  );
14186
14274
  if (!exportAll && selectedScenarios.length === 0) {
@@ -14326,7 +14414,9 @@ function createBddHandler(deps = {}) {
14326
14414
  if (!scenario) {
14327
14415
  return {
14328
14416
  exitCode: ExitCode.RemoteError,
14329
- stderr: [`ERROR: NOT_FOUND: BDD scenario ${parsedScenario.scenarioKey} was not found in this project.`]
14417
+ stderr: [
14418
+ `ERROR: NOT_FOUND: BDD scenario ${parsedScenario.scenarioKey} was not found in this project.`
14419
+ ]
14330
14420
  };
14331
14421
  }
14332
14422
  const nextTags = parsedScenario.tags.filter((tag) => !SCENARIO_METADATA_PATTERN.test(tag));
@@ -14432,7 +14522,9 @@ function createBddHandler(deps = {}) {
14432
14522
  if (!scenarioId && !testCaseKey) {
14433
14523
  return {
14434
14524
  exitCode: ExitCode.ValidationError,
14435
- stderr: ["ERROR: Provide either --id <SCENARIO_ID> or --test-case-key <TC-197> for testops bdd scenarios run."]
14525
+ stderr: [
14526
+ "ERROR: Provide either --id <SCENARIO_ID> or --test-case-key <TC-197> for testops bdd scenarios run."
14527
+ ]
14436
14528
  };
14437
14529
  }
14438
14530
  try {
@@ -14706,10 +14798,7 @@ function listToLines(items) {
14706
14798
  if (items.length === 0) {
14707
14799
  return ["Test cases: 0", "No test cases found for the selected context."];
14708
14800
  }
14709
- return [
14710
- `Test cases: ${items.length}`,
14711
- ...items.map((item) => `- ${item.key} [${item.status}] ${item.title}`)
14712
- ];
14801
+ return [`Test cases: ${items.length}`, ...items.map((item) => `- ${item.key} [${item.status}] ${item.title}`)];
14713
14802
  }
14714
14803
  function showToLines(testCase) {
14715
14804
  return [
@@ -15009,11 +15098,7 @@ var CONFIG_FLAGS5 = /* @__PURE__ */ new Set([
15009
15098
  "--jira-email",
15010
15099
  "--jira-api-token"
15011
15100
  ]);
15012
- var DIRECT_JIRA_FLAGS = /* @__PURE__ */ new Set([
15013
- "--site",
15014
- "--email",
15015
- "--api-token"
15016
- ]);
15101
+ var DIRECT_JIRA_FLAGS = /* @__PURE__ */ new Set(["--site", "--email", "--api-token"]);
15017
15102
  function parseArgs6(args) {
15018
15103
  const flags = {};
15019
15104
  const boolFlags = /* @__PURE__ */ new Set();
@@ -15213,27 +15298,33 @@ function createDoctorHandler(deps = {}) {
15213
15298
  message: "Issue access check skipped. Pass --issue-key to verify Jira read/download access for a specific issue."
15214
15299
  });
15215
15300
  }
15216
- checks.push(commandCheck(
15217
- { execFileSync: runCommand },
15218
- "unzip",
15219
- ["-v"],
15220
- "unzip is available for manual ZIP inspection.",
15221
- "unzip was not found. The future built-in opener can avoid this, but manual inspection will be less convenient."
15222
- ));
15223
- checks.push(commandCheck(
15224
- { execFileSync: runCommand },
15225
- "java",
15226
- ["-version"],
15227
- "Java is available for optional raw allure-results workflows.",
15228
- "Java was not found. This does not block generated Allure HTML ZIP upload/download/open; it only matters for raw allure-results generation."
15229
- ));
15230
- checks.push(commandCheck(
15231
- { execFileSync: runCommand },
15232
- "allure",
15233
- ["--version"],
15234
- "Allure CLI is available for optional raw allure-results workflows.",
15235
- "Allure CLI was not found. This does not block generated Allure HTML ZIP upload/download/open."
15236
- ));
15301
+ checks.push(
15302
+ commandCheck(
15303
+ { execFileSync: runCommand },
15304
+ "unzip",
15305
+ ["-v"],
15306
+ "unzip is available for manual ZIP inspection.",
15307
+ "unzip was not found. The future built-in opener can avoid this, but manual inspection will be less convenient."
15308
+ )
15309
+ );
15310
+ checks.push(
15311
+ commandCheck(
15312
+ { execFileSync: runCommand },
15313
+ "java",
15314
+ ["-version"],
15315
+ "Java is available for optional raw allure-results workflows.",
15316
+ "Java was not found. This does not block generated Allure HTML ZIP upload/download/open; it only matters for raw allure-results generation."
15317
+ )
15318
+ );
15319
+ checks.push(
15320
+ commandCheck(
15321
+ { execFileSync: runCommand },
15322
+ "allure",
15323
+ ["--version"],
15324
+ "Allure CLI is available for optional raw allure-results workflows.",
15325
+ "Allure CLI was not found. This does not block generated Allure HTML ZIP upload/download/open."
15326
+ )
15327
+ );
15237
15328
  const summary2 = {
15238
15329
  status: computeOverallStatus(checks),
15239
15330
  checks
@@ -15272,9 +15363,7 @@ function createDoctorHandler(deps = {}) {
15272
15363
  message: "Forge endpoint is configured."
15273
15364
  });
15274
15365
  }
15275
- const hasContext = Boolean(
15276
- resolution.values.projectKey || resolution.values.issueKey
15277
- );
15366
+ const hasContext = Boolean(resolution.values.projectKey || resolution.values.issueKey);
15278
15367
  if (enforceContextCheck || hasContext) {
15279
15368
  if (!hasContext) {
15280
15369
  checks.push({
@@ -15501,11 +15590,7 @@ function createIngestFeatureHandler(deps = {}) {
15501
15590
  }
15502
15591
  return {
15503
15592
  exitCode: ExitCode.RemoteError,
15504
- stdout: [
15505
- "Feature ingestion: FAILED",
15506
- `Feature: ${name}`,
15507
- `Source: ${useStdin ? "stdin" : sourceFile}`
15508
- ],
15593
+ stdout: ["Feature ingestion: FAILED", `Feature: ${name}`, `Source: ${useStdin ? "stdin" : sourceFile}`],
15509
15594
  stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
15510
15595
  };
15511
15596
  }
@@ -15546,11 +15631,7 @@ function createIngestFeatureHandler(deps = {}) {
15546
15631
  }
15547
15632
  return {
15548
15633
  exitCode: ExitCode.TransportError,
15549
- stdout: [
15550
- "Feature ingestion: FAILED",
15551
- `Feature: ${name}`,
15552
- `Source: ${useStdin ? "stdin" : sourceFile}`
15553
- ],
15634
+ stdout: ["Feature ingestion: FAILED", `Feature: ${name}`, `Source: ${useStdin ? "stdin" : sourceFile}`],
15554
15635
  stderr: [`ERROR: ${normalizeError5(error)}`]
15555
15636
  };
15556
15637
  }
@@ -16145,14 +16226,8 @@ function buildGitHubSetupPlan(input) {
16145
16226
  const workflowId = requireNonEmpty(input.workflowId, "workflow id");
16146
16227
  const workflowPath = normalizeWorkflowPath(input.workflowPath);
16147
16228
  const profileLabel = requireNonEmpty(input.profileLabel, "profile label");
16148
- const endpointSecretName = normalizeSecretName(
16149
- input.callbackEndpointSecretName,
16150
- "callback endpoint secret name"
16151
- );
16152
- const tokenSecretName = normalizeSecretName(
16153
- input.callbackAuthTokenSecretName,
16154
- "callback auth token secret name"
16155
- );
16229
+ const endpointSecretName = normalizeSecretName(input.callbackEndpointSecretName, "callback endpoint secret name");
16230
+ const tokenSecretName = normalizeSecretName(input.callbackAuthTokenSecretName, "callback auth token secret name");
16156
16231
  if (endpointSecretName === tokenSecretName) {
16157
16232
  throw new Error("callback endpoint and auth token secret names must be different.");
16158
16233
  }
@@ -16211,13 +16286,7 @@ function buildGitHubSetupPlan(input) {
16211
16286
  "Secrets: read/write metadata only for the explicit github-secrets apply scope.",
16212
16287
  "Contents: read for doctor; this command never creates commits or pull requests."
16213
16288
  ],
16214
- remoteChangesExcluded: [
16215
- "accounts",
16216
- "credentials",
16217
- "commits",
16218
- "pull requests",
16219
- "workflow runs"
16220
- ]
16289
+ remoteChangesExcluded: ["accounts", "credentials", "commits", "pull requests", "workflow runs"]
16221
16290
  }
16222
16291
  },
16223
16292
  jiraProfile: {
@@ -16435,15 +16504,7 @@ function validateSetupPlan(value) {
16435
16504
  errors.push("project.key is required.");
16436
16505
  }
16437
16506
  const github = value.github;
16438
- if (!isRecord(github) || !hasExactKeys(github, [
16439
- "owner",
16440
- "repository",
16441
- "ref",
16442
- "workflowId",
16443
- "workflowFile",
16444
- "requiredSecrets",
16445
- "guidance"
16446
- ])) {
16507
+ if (!isRecord(github) || !hasExactKeys(github, ["owner", "repository", "ref", "workflowId", "workflowFile", "requiredSecrets", "guidance"])) {
16447
16508
  errors.push("github has an invalid shape.");
16448
16509
  } else {
16449
16510
  for (const field of ["owner", "repository", "ref", "workflowId"]) {
@@ -16520,6 +16581,8 @@ function hashSetupContent(value) {
16520
16581
  }
16521
16582
 
16522
16583
  // src/githubSetupProvider.ts
16584
+ var GITHUB_SECRET_PAGE_SIZE = 100;
16585
+ var MAX_GITHUB_SECRET_PAGES = 100;
16523
16586
  function isRecord2(value) {
16524
16587
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
16525
16588
  }
@@ -16551,18 +16614,14 @@ async function githubApi(fetchImpl, url, token) {
16551
16614
  }
16552
16615
  async function setGitHubRepositorySecretWithCli(input) {
16553
16616
  await new Promise((resolve, reject) => {
16554
- const child = (0, import_node_child_process4.spawn)(
16555
- "gh",
16556
- ["secret", "set", input.name, "--repo", `${input.owner}/${input.repository}`],
16557
- {
16558
- shell: false,
16559
- stdio: ["pipe", "ignore", "pipe"],
16560
- env: {
16561
- ...process.env,
16562
- GH_TOKEN: input.adminToken
16563
- }
16617
+ const child = (0, import_node_child_process4.spawn)("gh", ["secret", "set", input.name, "--repo", `${input.owner}/${input.repository}`], {
16618
+ shell: false,
16619
+ stdio: ["pipe", "ignore", "pipe"],
16620
+ env: {
16621
+ ...process.env,
16622
+ GH_TOKEN: input.adminToken
16564
16623
  }
16565
- );
16624
+ });
16566
16625
  let stderr = "";
16567
16626
  child.stderr.setEncoding("utf8");
16568
16627
  child.stderr.on("data", (chunk) => {
@@ -16622,6 +16681,7 @@ var GitHubSetupProvider = class {
16622
16681
  return {
16623
16682
  found: true,
16624
16683
  active: state === "active",
16684
+ state,
16625
16685
  path: workflowPath,
16626
16686
  contentSha256,
16627
16687
  message: state !== "active" ? `GitHub workflow is registered but its state is ${state ?? "unknown"}.` : workflowPath !== workflowFile.path ? `GitHub workflow path ${workflowPath ?? "<unknown>"} does not match ${workflowFile.path}.` : contentSha256 === workflowFile.sha256 ? "GitHub workflow is active and its content matches the approved plan." : "GitHub workflow metadata is reachable, but the approved ref content could not be confirmed as an exact plan match."
@@ -16636,25 +16696,52 @@ var GitHubSetupProvider = class {
16636
16696
  };
16637
16697
  }
16638
16698
  const { owner, repository } = plan.github;
16639
- const url = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/actions/secrets?per_page=100`;
16640
- const response = await githubApi(this.fetchImpl, url, adminToken);
16641
- if (!response.ok) {
16642
- throw new Error(
16643
- `GitHub repository secret metadata request failed with HTTP ${response.status}: ${safeGitHubMessage(response.body, "request failed")}`
16644
- );
16645
- }
16699
+ const baseUrl = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/actions/secrets`;
16646
16700
  const names = /* @__PURE__ */ new Set();
16647
- if (isRecord2(response.body) && Array.isArray(response.body.secrets)) {
16648
- for (const item of response.body.secrets) {
16701
+ let totalCount;
16702
+ let pagesRead = 0;
16703
+ for (let page = 1; page <= MAX_GITHUB_SECRET_PAGES; page += 1) {
16704
+ const url = `${baseUrl}?per_page=${GITHUB_SECRET_PAGE_SIZE}&page=${page}`;
16705
+ const response = await githubApi(this.fetchImpl, url, adminToken);
16706
+ if (!response.ok) {
16707
+ throw new Error(
16708
+ `GitHub repository secret metadata request failed on page ${page} with HTTP ${response.status}: ${safeGitHubMessage(response.body, "request failed")}`
16709
+ );
16710
+ }
16711
+ const body = isRecord2(response.body) ? response.body : {};
16712
+ if (totalCount === void 0 && typeof body.total_count === "number" && Number.isFinite(body.total_count)) {
16713
+ totalCount = Math.max(0, Math.trunc(body.total_count));
16714
+ }
16715
+ const secrets = Array.isArray(body.secrets) ? body.secrets : [];
16716
+ const before = names.size;
16717
+ for (const item of secrets) {
16649
16718
  if (isRecord2(item) && typeof item.name === "string") {
16650
16719
  names.add(item.name);
16651
16720
  }
16652
16721
  }
16722
+ pagesRead = page;
16723
+ if (totalCount !== void 0 && names.size >= totalCount) {
16724
+ break;
16725
+ }
16726
+ if (secrets.length < GITHUB_SECRET_PAGE_SIZE) {
16727
+ if (totalCount !== void 0 && names.size < totalCount) {
16728
+ throw new Error(
16729
+ `GitHub repository secret metadata pagination ended early after ${names.size} of ${totalCount} secret name(s).`
16730
+ );
16731
+ }
16732
+ break;
16733
+ }
16734
+ if (names.size === before) {
16735
+ throw new Error("GitHub repository secret metadata pagination made no progress.");
16736
+ }
16737
+ if (page === MAX_GITHUB_SECRET_PAGES) {
16738
+ throw new Error(`GitHub repository secret metadata exceeded ${MAX_GITHUB_SECRET_PAGES} pages.`);
16739
+ }
16653
16740
  }
16654
16741
  return {
16655
16742
  verified: true,
16656
16743
  names,
16657
- message: `Verified ${names.size} GitHub repository secret name(s); values remain unreadable.`
16744
+ message: `Verified ${names.size} GitHub repository secret name(s) across ${pagesRead} page(s); values remain unreadable.`
16658
16745
  };
16659
16746
  }
16660
16747
  async setCallbackSecret(plan, secret, value, adminToken) {
@@ -16671,7 +16758,12 @@ var GitHubSetupProvider = class {
16671
16758
  // src/azureDevOpsSetupProvider.ts
16672
16759
  var isRecord3 = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
16673
16760
  async function adoGet(fetchImpl, url, token) {
16674
- const response = await fetchImpl(url, { headers: { accept: "application/json", ...token ? { authorization: `Basic ${Buffer.from(`:${token}`).toString("base64")}` } : {} } });
16761
+ const response = await fetchImpl(url, {
16762
+ headers: {
16763
+ accept: "application/json",
16764
+ ...token ? { authorization: `Basic ${Buffer.from(`:${token}`).toString("base64")}` } : {}
16765
+ }
16766
+ });
16675
16767
  const text = await response.text();
16676
16768
  let body = {};
16677
16769
  try {
@@ -16684,7 +16776,8 @@ async function setAzurePipelineVariableWithRest(input, fetchImpl) {
16684
16776
  const base = `https://dev.azure.com/${encodeURIComponent(input.organization)}/${encodeURIComponent(input.project)}/_apis/build/definitions/${encodeURIComponent(input.pipelineId)}`;
16685
16777
  const auth = { authorization: `Basic ${Buffer.from(`:${input.token}`).toString("base64")}` };
16686
16778
  const current = await adoGet(fetchImpl, `${base}?api-version=7.1`, input.token);
16687
- if (!current.ok || !isRecord3(current.body)) throw new Error(`Azure DevOps build definition could not be read (HTTP ${current.status}).`);
16779
+ if (!current.ok || !isRecord3(current.body))
16780
+ throw new Error(`Azure DevOps build definition could not be read (HTTP ${current.status}).`);
16688
16781
  const variables = isRecord3(current.body.variables) ? { ...current.body.variables } : {};
16689
16782
  variables[input.name] = { value: input.value, isSecret: true };
16690
16783
  const query = new URLSearchParams({
@@ -16692,9 +16785,25 @@ async function setAzurePipelineVariableWithRest(input, fetchImpl) {
16692
16785
  secretsSourceDefinitionId: String(current.body.id),
16693
16786
  secretsSourceDefinitionRevision: String(current.body.revision)
16694
16787
  });
16695
- const response = await fetchImpl(`${base}?${query.toString()}`, { method: "PUT", headers: { ...auth, "content-type": "application/json", accept: "application/json" }, body: JSON.stringify({ ...current.body, variables }) });
16788
+ const response = await fetchImpl(`${base}?${query.toString()}`, {
16789
+ method: "PUT",
16790
+ headers: { ...auth, "content-type": "application/json", accept: "application/json" },
16791
+ body: JSON.stringify({ ...current.body, variables })
16792
+ });
16696
16793
  if (!response.ok) throw new Error(`Azure DevOps pipeline variable update failed (HTTP ${response.status}).`);
16697
16794
  }
16795
+ function azurePipelineStateMessage(name, queueStatus) {
16796
+ switch (queueStatus) {
16797
+ case "enabled":
16798
+ return `Azure DevOps pipeline ${name} is reachable and enabled. Inspection never queues a run.`;
16799
+ case "paused":
16800
+ return `Azure DevOps pipeline ${name} is reachable but paused; builds may be queued but will not start until the definition is enabled.`;
16801
+ case "disabled":
16802
+ return `Azure DevOps pipeline ${name} is reachable but disabled; new builds cannot be queued until the definition is enabled.`;
16803
+ default:
16804
+ return `Azure DevOps pipeline ${name} is reachable, but its queue status is ${queueStatus ?? "unknown"}.`;
16805
+ }
16806
+ }
16698
16807
  var AzureDevOpsSetupProvider = class {
16699
16808
  name = "azure-devops";
16700
16809
  fetchImpl;
@@ -16707,23 +16816,51 @@ var AzureDevOpsSetupProvider = class {
16707
16816
  const a = plan.azureDevOps;
16708
16817
  const url = `https://dev.azure.com/${encodeURIComponent(a.organization)}/${encodeURIComponent(a.project)}/_apis/build/definitions/${encodeURIComponent(a.pipelineId)}?api-version=${encodeURIComponent(a.apiVersion)}`;
16709
16818
  const result = await adoGet(this.fetchImpl, url, token);
16710
- if (!result.ok) return { found: false, message: result.status === 404 ? "Azure DevOps pipeline was not found." : `Azure DevOps pipeline metadata could not be verified (HTTP ${result.status}).` };
16819
+ if (!result.ok)
16820
+ return {
16821
+ found: false,
16822
+ message: result.status === 404 ? "Azure DevOps pipeline was not found." : `Azure DevOps pipeline metadata could not be verified (HTTP ${result.status}).`
16823
+ };
16711
16824
  const body = isRecord3(result.body) ? result.body : {};
16712
- const name = typeof body.name === "string" ? body.name : void 0;
16713
- return { found: true, active: true, path: name, message: `Azure DevOps pipeline ${name ?? a.pipelineId} is reachable. Inspection never queues a run.` };
16825
+ const name = typeof body.name === "string" ? body.name : a.pipelineId;
16826
+ const queueStatus = typeof body.queueStatus === "string" ? body.queueStatus : void 0;
16827
+ return {
16828
+ found: true,
16829
+ active: queueStatus === "enabled",
16830
+ state: queueStatus,
16831
+ path: name,
16832
+ message: azurePipelineStateMessage(name, queueStatus)
16833
+ };
16714
16834
  }
16715
16835
  async inspectCallbackSecretMetadata(plan, token) {
16716
- if (!token) return { verified: false, names: /* @__PURE__ */ new Set(), message: "Azure DevOps pipeline variable metadata was not verified because azureDevOpsAdminToken was not provided. Secret values are never readable." };
16836
+ if (!token)
16837
+ return {
16838
+ verified: false,
16839
+ names: /* @__PURE__ */ new Set(),
16840
+ message: "Azure DevOps pipeline variable metadata was not verified because azureDevOpsAdminToken was not provided. Secret values are never readable."
16841
+ };
16717
16842
  const a = plan.azureDevOps;
16718
16843
  const url = `https://dev.azure.com/${encodeURIComponent(a.organization)}/${encodeURIComponent(a.project)}/_apis/build/definitions/${encodeURIComponent(a.pipelineId)}?api-version=${encodeURIComponent(a.apiVersion)}`;
16719
16844
  const result = await adoGet(this.fetchImpl, url, token);
16720
16845
  if (!result.ok) throw new Error(`Azure DevOps pipeline metadata request failed with HTTP ${result.status}.`);
16721
16846
  const variables = isRecord3(result.body) && isRecord3(result.body.variables) ? result.body.variables : {};
16722
- return { verified: true, names: new Set(Object.keys(variables)), message: `Verified ${Object.keys(variables).length} Azure DevOps pipeline variable name(s); secret values remain unreadable.` };
16847
+ return {
16848
+ verified: true,
16849
+ names: new Set(Object.keys(variables)),
16850
+ message: `Verified ${Object.keys(variables).length} Azure DevOps pipeline variable name(s); secret values remain unreadable.`
16851
+ };
16723
16852
  }
16724
16853
  async setCallbackSecret(plan, secret, value, adminToken) {
16725
- if (!this.setter) throw new Error("Azure DevOps pipeline variable setter is not configured; use the safe Azure CLI adapter.");
16726
- await this.setter({ organization: plan.azureDevOps.organization, project: plan.azureDevOps.project, pipelineId: plan.azureDevOps.pipelineId, name: secret.repositorySecretName, value, token: adminToken });
16854
+ if (!this.setter)
16855
+ throw new Error("Azure DevOps pipeline variable setter is not configured; use the safe Azure CLI adapter.");
16856
+ await this.setter({
16857
+ organization: plan.azureDevOps.organization,
16858
+ project: plan.azureDevOps.project,
16859
+ pipelineId: plan.azureDevOps.pipelineId,
16860
+ name: secret.repositorySecretName,
16861
+ value,
16862
+ token: adminToken
16863
+ });
16727
16864
  }
16728
16865
  };
16729
16866
 
@@ -16753,30 +16890,138 @@ function buildAzureDevOpsSetupPlan(input) {
16753
16890
  const endpointName = variableName(input.callbackEndpointVariableName, "callback endpoint variable name");
16754
16891
  const tokenName = variableName(input.callbackAuthTokenVariableName, "callback auth token variable name");
16755
16892
  if (endpointName === tokenName) throw new Error("callback variable names must be distinct.");
16756
- const requiredSecrets = [{ repositorySecretName: endpointName, valueKey: "callbackEndpoint", purpose: "Forge callback endpoint pipeline variable." }, { repositorySecretName: tokenName, valueKey: "callbackAuthToken", purpose: "Forge callback auth token pipeline variable." }];
16757
- const scopes = ["provider-secrets", "jira-profile", ...input.setProjectDefault ? ["project-default"] : []];
16758
- const actions = scopes.map((scope) => ({ id: scope === "provider-secrets" ? "set-provider-secrets" : scope === "jira-profile" ? "upsert-jira-profile" : "set-project-default", scope, summary: `Apply ${scope} for Azure DevOps.`, mutates: "jira" }));
16759
- const withoutId = { schemaVersion: "automatify.testops.setup/v1", kind: "AutomatifyTestOpsSetupPlan", provider: "azure-devops", project: { key: projectKey }, azureDevOps: { organization, project: azureProject, pipelineId, ref, apiVersion, requiredSecrets, guidance: { authentication: "Use an Azure DevOps PAT with least-privilege pipeline read/manage-variable access.", permissions: ["Pipelines: read for doctor; manage variables only for explicit provider-secrets apply."], remoteChangesExcluded: ["accounts", "credentials", "commits", "pull requests", "pipeline runs"] } }, jiraProfile: { label: profileLabel, enabled: input.enabled, provider: "azureDevops", config: { organization, project: azureProject, pipelineId, apiVersion, bodyTemplate: JSON.stringify({ resources: { repositories: { self: { refName: ref } } } }) }, setProjectDefault: input.setProjectDefault }, actions, smokeValidation: [{ id: "pipeline-content", verifies: "Azure DevOps pipeline metadata without queueing a run.", triggersExternalRun: false }, { id: "pipeline-variable-metadata", verifies: "Required variable names; values remain unreadable.", triggersExternalRun: false }, { id: "jira-profile", verifies: "Forge automation profile and optional default.", triggersExternalRun: false }], rollback: actions.map((a) => ({ actionId: a.id, strategy: "Restore the previous state manually; secret values cannot be read back.", automatic: false })), followUp: { azureDevOps: "Azure DevOps setup is implemented through this provider-neutral plan/apply/doctor boundary." } };
16893
+ const requiredSecrets = [
16894
+ {
16895
+ repositorySecretName: endpointName,
16896
+ valueKey: "callbackEndpoint",
16897
+ purpose: "Forge callback endpoint pipeline variable."
16898
+ },
16899
+ {
16900
+ repositorySecretName: tokenName,
16901
+ valueKey: "callbackAuthToken",
16902
+ purpose: "Forge callback auth token pipeline variable."
16903
+ }
16904
+ ];
16905
+ const scopes = [
16906
+ "provider-secrets",
16907
+ "jira-profile",
16908
+ ...input.setProjectDefault ? ["project-default"] : []
16909
+ ];
16910
+ const actions = scopes.map((scope) => ({
16911
+ id: scope === "provider-secrets" ? "set-provider-secrets" : scope === "jira-profile" ? "upsert-jira-profile" : "set-project-default",
16912
+ scope,
16913
+ summary: `Apply ${scope} for Azure DevOps.`,
16914
+ mutates: "jira"
16915
+ }));
16916
+ const withoutId = {
16917
+ schemaVersion: "automatify.testops.setup/v1",
16918
+ kind: "AutomatifyTestOpsSetupPlan",
16919
+ provider: "azure-devops",
16920
+ project: { key: projectKey },
16921
+ azureDevOps: {
16922
+ organization,
16923
+ project: azureProject,
16924
+ pipelineId,
16925
+ ref,
16926
+ apiVersion,
16927
+ requiredSecrets,
16928
+ guidance: {
16929
+ authentication: "Use an Azure DevOps PAT with least-privilege pipeline read/manage-variable access.",
16930
+ permissions: ["Pipelines: read for doctor; manage variables only for explicit provider-secrets apply."],
16931
+ remoteChangesExcluded: ["accounts", "credentials", "commits", "pull requests", "pipeline runs"]
16932
+ }
16933
+ },
16934
+ jiraProfile: {
16935
+ label: profileLabel,
16936
+ enabled: input.enabled,
16937
+ provider: "azureDevops",
16938
+ config: {
16939
+ organization,
16940
+ project: azureProject,
16941
+ pipelineId,
16942
+ apiVersion,
16943
+ bodyTemplate: JSON.stringify({ resources: { repositories: { self: { refName: ref } } } })
16944
+ },
16945
+ setProjectDefault: input.setProjectDefault
16946
+ },
16947
+ actions,
16948
+ smokeValidation: [
16949
+ {
16950
+ id: "pipeline-content",
16951
+ verifies: "Azure DevOps pipeline metadata without queueing a run.",
16952
+ triggersExternalRun: false
16953
+ },
16954
+ {
16955
+ id: "pipeline-variable-metadata",
16956
+ verifies: "Required variable names; values remain unreadable.",
16957
+ triggersExternalRun: false
16958
+ },
16959
+ {
16960
+ id: "jira-profile",
16961
+ verifies: "Forge automation profile and optional default.",
16962
+ triggersExternalRun: false
16963
+ }
16964
+ ],
16965
+ rollback: actions.map((a) => ({
16966
+ actionId: a.id,
16967
+ strategy: "Restore the previous state manually; secret values cannot be read back.",
16968
+ automatic: false
16969
+ })),
16970
+ followUp: {
16971
+ azureDevOps: "Azure DevOps setup is implemented through this provider-neutral plan/apply/doctor boundary."
16972
+ }
16973
+ };
16760
16974
  return { ...withoutId, planId: digest(withoutId) };
16761
16975
  }
16762
16976
  function validateAzureDevOpsSetupPlan(value) {
16763
16977
  const errors = [];
16764
16978
  const record = value;
16765
- const exact = (v, keys) => Boolean(v && typeof v === "object" && !Array.isArray(v) && Object.keys(v).sort().join() === [...keys].sort().join());
16766
- if (!exact(value, ["schemaVersion", "kind", "planId", "provider", "project", "azureDevOps", "jiraProfile", "actions", "smokeValidation", "rollback", "followUp"])) errors.push("Plan has missing or unsupported top-level fields.");
16767
- if (record?.schemaVersion !== "automatify.testops.setup/v1" || record?.kind !== "AutomatifyTestOpsSetupPlan" || record?.provider !== "azure-devops") errors.push("Plan schema, kind, or provider is invalid.");
16768
- if (!exact(record?.project, ["key"]) || !/^[A-Z][A-Z0-9_]{1,31}$/.test(record?.project?.key ?? "")) errors.push("project.key is invalid.");
16979
+ const exact = (v, keys) => Boolean(
16980
+ v && typeof v === "object" && !Array.isArray(v) && Object.keys(v).sort().join() === [...keys].sort().join()
16981
+ );
16982
+ if (!exact(value, [
16983
+ "schemaVersion",
16984
+ "kind",
16985
+ "planId",
16986
+ "provider",
16987
+ "project",
16988
+ "azureDevOps",
16989
+ "jiraProfile",
16990
+ "actions",
16991
+ "smokeValidation",
16992
+ "rollback",
16993
+ "followUp"
16994
+ ]))
16995
+ errors.push("Plan has missing or unsupported top-level fields.");
16996
+ if (record?.schemaVersion !== "automatify.testops.setup/v1" || record?.kind !== "AutomatifyTestOpsSetupPlan" || record?.provider !== "azure-devops")
16997
+ errors.push("Plan schema, kind, or provider is invalid.");
16998
+ if (!exact(record?.project, ["key"]) || !/^[A-Z][A-Z0-9_]{1,31}$/.test(record?.project?.key ?? ""))
16999
+ errors.push("project.key is invalid.");
16769
17000
  const a = record?.azureDevOps;
16770
- if (!exact(a, ["organization", "project", "pipelineId", "ref", "apiVersion", "requiredSecrets", "guidance"]) || ![a?.organization, a?.project, a?.ref, a?.apiVersion].every((x) => typeof x === "string" && x.trim()) || !/^\d+$/.test(a?.pipelineId ?? "")) errors.push("azureDevOps has an invalid shape or fields.");
16771
- if (!Array.isArray(a?.requiredSecrets) || a.requiredSecrets.length !== 2 || new Set(a.requiredSecrets.map((s) => s.repositorySecretName)).size !== 2 || new Set(a.requiredSecrets.map((s) => s.valueKey)).size !== 2 || !a.requiredSecrets.some((s) => s.valueKey === "callbackEndpoint") || !a.requiredSecrets.some((s) => s.valueKey === "callbackAuthToken") || !a.requiredSecrets.every((s) => exact(s, ["repositorySecretName", "valueKey", "purpose"]) && /^[A-Z_][A-Z0-9_]*$/.test(s.repositorySecretName) && s.purpose)) errors.push("azureDevOps.requiredSecrets is invalid.");
17001
+ if (!exact(a, ["organization", "project", "pipelineId", "ref", "apiVersion", "requiredSecrets", "guidance"]) || ![a?.organization, a?.project, a?.ref, a?.apiVersion].every((x) => typeof x === "string" && x.trim()) || !/^\d+$/.test(a?.pipelineId ?? ""))
17002
+ errors.push("azureDevOps has an invalid shape or fields.");
17003
+ if (!Array.isArray(a?.requiredSecrets) || a.requiredSecrets.length !== 2 || new Set(a.requiredSecrets.map((s) => s.repositorySecretName)).size !== 2 || new Set(a.requiredSecrets.map((s) => s.valueKey)).size !== 2 || !a.requiredSecrets.some((s) => s.valueKey === "callbackEndpoint") || !a.requiredSecrets.some((s) => s.valueKey === "callbackAuthToken") || !a.requiredSecrets.every(
17004
+ (s) => exact(s, ["repositorySecretName", "valueKey", "purpose"]) && /^[A-Z_][A-Z0-9_]*$/.test(s.repositorySecretName) && s.purpose
17005
+ ))
17006
+ errors.push("azureDevOps.requiredSecrets is invalid.");
16772
17007
  const p = record?.jiraProfile;
16773
- if (!exact(p, ["label", "enabled", "provider", "config", "setProjectDefault"]) || p?.provider !== "azureDevops" || !exact(p?.config, ["organization", "project", "pipelineId", "apiVersion", "bodyTemplate"]) || p.config.organization !== a?.organization || p.config.project !== a?.project || p.config.pipelineId !== a?.pipelineId || p.config.apiVersion !== a?.apiVersion) errors.push("jiraProfile/config is invalid or inconsistent.");
17008
+ if (!exact(p, ["label", "enabled", "provider", "config", "setProjectDefault"]) || p?.provider !== "azureDevops" || !exact(p?.config, ["organization", "project", "pipelineId", "apiVersion", "bodyTemplate"]) || p.config.organization !== a?.organization || p.config.project !== a?.project || p.config.pipelineId !== a?.pipelineId || p.config.apiVersion !== a?.apiVersion)
17009
+ errors.push("jiraProfile/config is invalid or inconsistent.");
16774
17010
  const ids = p?.setProjectDefault ? ["set-provider-secrets", "upsert-jira-profile", "set-project-default"] : ["set-provider-secrets", "upsert-jira-profile"];
16775
17011
  const scopes = p?.setProjectDefault ? ["provider-secrets", "jira-profile", "project-default"] : ["provider-secrets", "jira-profile"];
16776
- if (!Array.isArray(record?.actions) || record.actions.map((x) => x.id).join() !== ids.join() || !record.actions.every((x, i) => exact(x, ["id", "scope", "summary", "mutates"]) && x.scope === scopes[i] && x.mutates === "jira")) errors.push("actions are invalid.");
17012
+ if (!Array.isArray(record?.actions) || record.actions.map((x) => x.id).join() !== ids.join() || !record.actions.every(
17013
+ (x, i) => exact(x, ["id", "scope", "summary", "mutates"]) && x.scope === scopes[i] && x.mutates === "jira"
17014
+ ))
17015
+ errors.push("actions are invalid.");
16777
17016
  const smokeIds = ["pipeline-content", "pipeline-variable-metadata", "jira-profile"];
16778
- if (!Array.isArray(record?.smokeValidation) || record.smokeValidation.length !== 3 || !record.smokeValidation.every((x, i) => exact(x, ["id", "verifies", "triggersExternalRun"]) && x.id === smokeIds[i] && x.triggersExternalRun === false)) errors.push("smokeValidation is invalid.");
16779
- if (!Array.isArray(record?.rollback) || record.rollback.length !== ids.length || !record.rollback.every((x, i) => exact(x, ["actionId", "strategy", "automatic"]) && x.actionId === ids[i] && x.automatic === false)) errors.push("rollback is invalid.");
17017
+ if (!Array.isArray(record?.smokeValidation) || record.smokeValidation.length !== 3 || !record.smokeValidation.every(
17018
+ (x, i) => exact(x, ["id", "verifies", "triggersExternalRun"]) && x.id === smokeIds[i] && x.triggersExternalRun === false
17019
+ ))
17020
+ errors.push("smokeValidation is invalid.");
17021
+ if (!Array.isArray(record?.rollback) || record.rollback.length !== ids.length || !record.rollback.every(
17022
+ (x, i) => exact(x, ["actionId", "strategy", "automatic"]) && x.actionId === ids[i] && x.automatic === false
17023
+ ))
17024
+ errors.push("rollback is invalid.");
16780
17025
  if (errors.length === 0) {
16781
17026
  const { planId, ...rest } = record;
16782
17027
  if (planId !== digest(rest)) errors.push("planId does not match the plan contents.");
@@ -16972,14 +17217,26 @@ async function resolveSecrets(parsed, deps) {
16972
17217
  );
16973
17218
  }
16974
17219
  if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
16975
- throw new SetupCommandError("SECRET_INPUT_ERROR", "Secret stdin must be a JSON object.", ExitCode.ValidationError);
17220
+ throw new SetupCommandError(
17221
+ "SECRET_INPUT_ERROR",
17222
+ "Secret stdin must be a JSON object.",
17223
+ ExitCode.ValidationError
17224
+ );
16976
17225
  }
16977
17226
  for (const [key, value] of Object.entries(envelope)) {
16978
17227
  if (!isSecretKey(key)) {
16979
- throw new SetupCommandError("SECRET_INPUT_ERROR", `Secret stdin contains unsupported key ${key}.`, ExitCode.ValidationError);
17228
+ throw new SetupCommandError(
17229
+ "SECRET_INPUT_ERROR",
17230
+ `Secret stdin contains unsupported key ${key}.`,
17231
+ ExitCode.ValidationError
17232
+ );
16980
17233
  }
16981
17234
  if (typeof value !== "string" || !value.trim()) {
16982
- throw new SetupCommandError("SECRET_INPUT_ERROR", `Secret stdin key ${key} must be a non-empty string.`, ExitCode.ValidationError);
17235
+ throw new SetupCommandError(
17236
+ "SECRET_INPUT_ERROR",
17237
+ `Secret stdin key ${key} must be a non-empty string.`,
17238
+ ExitCode.ValidationError
17239
+ );
16983
17240
  }
16984
17241
  values[key] = value.trim();
16985
17242
  sources[key] = "stdin";
@@ -16998,10 +17255,18 @@ async function resolveSecrets(parsed, deps) {
16998
17255
  }
16999
17256
  const envName = target.slice(4);
17000
17257
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(envName)) {
17001
- throw new SetupCommandError("SECRET_REFERENCE_ERROR", `Secret reference for ${key} has an invalid environment variable name.`, ExitCode.ValidationError);
17258
+ throw new SetupCommandError(
17259
+ "SECRET_REFERENCE_ERROR",
17260
+ `Secret reference for ${key} has an invalid environment variable name.`,
17261
+ ExitCode.ValidationError
17262
+ );
17002
17263
  }
17003
17264
  if (sources[key]) {
17004
- throw new SetupCommandError("SECRET_REFERENCE_ERROR", `Secret ${key} was provided more than once.`, ExitCode.ValidationError);
17265
+ throw new SetupCommandError(
17266
+ "SECRET_REFERENCE_ERROR",
17267
+ `Secret ${key} was provided more than once.`,
17268
+ ExitCode.ValidationError
17269
+ );
17005
17270
  }
17006
17271
  const value = deps.env[envName]?.trim();
17007
17272
  if (!value) {
@@ -17036,7 +17301,11 @@ function approvedScopes(plan, parsed, dryRun) {
17036
17301
  throw new SetupCommandError("APPROVAL_ERROR", `Unsupported approval scope ${scope}.`, ExitCode.UsageError);
17037
17302
  }
17038
17303
  if (!planScopes.includes(scope)) {
17039
- throw new SetupCommandError("APPROVAL_ERROR", `Approval scope ${scope} is not present in this plan.`, ExitCode.UsageError);
17304
+ throw new SetupCommandError(
17305
+ "APPROVAL_ERROR",
17306
+ `Approval scope ${scope} is not present in this plan.`,
17307
+ ExitCode.UsageError
17308
+ );
17040
17309
  }
17041
17310
  expanded.add(scope);
17042
17311
  }
@@ -17109,10 +17378,7 @@ async function prepareState(plan, scopes, secrets, context, deps, dryRun, rotate
17109
17378
  );
17110
17379
  }
17111
17380
  if (secrets.values.githubAdminToken) {
17112
- const inspection = await deps.githubProvider.inspectCallbackSecretMetadata(
17113
- plan,
17114
- secrets.values.githubAdminToken
17115
- );
17381
+ const inspection = await deps.githubProvider.inspectCallbackSecretMetadata(plan, secrets.values.githubAdminToken);
17116
17382
  repositorySecretNames = inspection.names;
17117
17383
  const secretsToWrite = plan.github.requiredSecrets.filter(
17118
17384
  (secret) => rotateSecrets || !repositorySecretNames?.has(secret.repositorySecretName)
@@ -17215,12 +17481,7 @@ async function applyGitHubSecrets(plan, state, secrets, deps, dryRun, rotateSecr
17215
17481
  }
17216
17482
  const adminToken = secrets.values.githubAdminToken;
17217
17483
  for (const secret of pending) {
17218
- await deps.githubProvider.setCallbackSecret(
17219
- plan,
17220
- secret,
17221
- secrets.values[secret.valueKey],
17222
- adminToken
17223
- );
17484
+ await deps.githubProvider.setCallbackSecret(plan, secret, secrets.values[secret.valueKey], adminToken);
17224
17485
  }
17225
17486
  return {
17226
17487
  id: action.id,
@@ -17446,31 +17707,39 @@ async function inspectPlan(plan, repoRoot, secrets, context, deps) {
17446
17707
  function planCommand(args, deps) {
17447
17708
  const parsed = parseArgs10(args, PLAN_VALUE_FLAGS, /* @__PURE__ */ new Set(["--set-default", "--disabled"]));
17448
17709
  if (parsed.errors.length > 0) {
17449
- return jsonResponse(errorPayload("USAGE_ERROR", "Invalid setup plan arguments.", parsed.errors), ExitCode.UsageError);
17710
+ return jsonResponse(
17711
+ errorPayload("USAGE_ERROR", "Invalid setup plan arguments.", parsed.errors),
17712
+ ExitCode.UsageError
17713
+ );
17450
17714
  }
17451
17715
  const provider = parsed.flags["--provider"] ?? "github-actions";
17452
17716
  if (provider === "azure-devops") {
17453
17717
  try {
17454
- return jsonResponse(buildAzureDevOpsSetupPlan({
17455
- projectKey: parsed.flags["--project-key"] ?? "",
17456
- organization: parsed.flags["--organization"] ?? "",
17457
- azureProject: parsed.flags["--azure-project"] ?? "",
17458
- pipelineId: parsed.flags["--pipeline-id"] ?? "",
17459
- ref: parsed.flags["--ref"] ?? "main",
17460
- apiVersion: parsed.flags["--api-version"],
17461
- profileLabel: parsed.flags["--profile-label"] ?? "Azure DevOps",
17462
- enabled: !parsed.boolFlags.has("--disabled"),
17463
- setProjectDefault: parsed.boolFlags.has("--set-default"),
17464
- callbackEndpointVariableName: parsed.flags["--callback-endpoint-variable-name"] ?? "TESTOPS_FORGE_ENDPOINT",
17465
- callbackAuthTokenVariableName: parsed.flags["--callback-auth-token-variable-name"] ?? "TESTOPS_FORGE_AUTH_TOKEN"
17466
- }));
17718
+ return jsonResponse(
17719
+ buildAzureDevOpsSetupPlan({
17720
+ projectKey: parsed.flags["--project-key"] ?? "",
17721
+ organization: parsed.flags["--organization"] ?? "",
17722
+ azureProject: parsed.flags["--azure-project"] ?? "",
17723
+ pipelineId: parsed.flags["--pipeline-id"] ?? "",
17724
+ ref: parsed.flags["--ref"] ?? "main",
17725
+ apiVersion: parsed.flags["--api-version"],
17726
+ profileLabel: parsed.flags["--profile-label"] ?? "Azure DevOps",
17727
+ enabled: !parsed.boolFlags.has("--disabled"),
17728
+ setProjectDefault: parsed.boolFlags.has("--set-default"),
17729
+ callbackEndpointVariableName: parsed.flags["--callback-endpoint-variable-name"] ?? "TESTOPS_FORGE_ENDPOINT",
17730
+ callbackAuthTokenVariableName: parsed.flags["--callback-auth-token-variable-name"] ?? "TESTOPS_FORGE_AUTH_TOKEN"
17731
+ })
17732
+ );
17467
17733
  } catch (error) {
17468
17734
  return jsonResponse(errorPayload("VALIDATION_ERROR", safeErrorMessage(error)), ExitCode.ValidationError);
17469
17735
  }
17470
17736
  }
17471
17737
  if (provider !== "github-actions") {
17472
17738
  return jsonResponse(
17473
- errorPayload("VALIDATION_ERROR", "Only github-actions is implemented in this stage; Azure DevOps is the documented next adapter."),
17739
+ errorPayload(
17740
+ "VALIDATION_ERROR",
17741
+ "Only github-actions is implemented in this stage; Azure DevOps is the documented next adapter."
17742
+ ),
17474
17743
  ExitCode.ValidationError
17475
17744
  );
17476
17745
  }
@@ -17491,16 +17760,16 @@ function planCommand(args, deps) {
17491
17760
  });
17492
17761
  return jsonResponse(plan);
17493
17762
  } catch (error) {
17494
- return jsonResponse(
17495
- errorPayload("VALIDATION_ERROR", safeErrorMessage(error)),
17496
- ExitCode.ValidationError
17497
- );
17763
+ return jsonResponse(errorPayload("VALIDATION_ERROR", safeErrorMessage(error)), ExitCode.ValidationError);
17498
17764
  }
17499
17765
  }
17500
17766
  async function applyCommand(args, context, deps) {
17501
17767
  const parsed = parseArgs10(args, APPLY_VALUE_FLAGS, APPLY_BOOL_FLAGS, true);
17502
17768
  if (parsed.errors.length > 0) {
17503
- return jsonResponse(errorPayload("USAGE_ERROR", "Invalid setup apply arguments.", parsed.errors), ExitCode.UsageError);
17769
+ return jsonResponse(
17770
+ errorPayload("USAGE_ERROR", "Invalid setup apply arguments.", parsed.errors),
17771
+ ExitCode.UsageError
17772
+ );
17504
17773
  }
17505
17774
  const planPath = parsed.flags["--plan"];
17506
17775
  if (!planPath) {
@@ -17525,16 +17794,7 @@ async function applyCommand(args, context, deps) {
17525
17794
  const repoRoot = import_node_path15.default.resolve(deps.cwd, parsed.flags["--repo-root"] ?? ".");
17526
17795
  const rotateSecrets = parsed.boolFlags.has("--rotate-secrets");
17527
17796
  const rotateProviderToken = parsed.boolFlags.has("--rotate-provider-token");
17528
- const state = await prepareState(
17529
- plan,
17530
- scopes,
17531
- secrets,
17532
- context,
17533
- deps,
17534
- dryRun,
17535
- rotateSecrets,
17536
- rotateProviderToken
17537
- );
17797
+ const state = await prepareState(plan, scopes, secrets, context, deps, dryRun, rotateSecrets, rotateProviderToken);
17538
17798
  let appliedProfile = state.matchingProfile;
17539
17799
  for (const action of plan.actions) {
17540
17800
  if (!scopes.includes(action.scope)) {
@@ -17553,14 +17813,7 @@ async function applyCommand(args, context, deps) {
17553
17813
  } else if (action.scope === "github-secrets") {
17554
17814
  actionResults.push(await applyGitHubSecrets(plan, state, secrets, deps, dryRun, rotateSecrets));
17555
17815
  } else if (action.scope === "jira-profile") {
17556
- const applied = await applyJiraProfile(
17557
- plan,
17558
- state,
17559
- secrets,
17560
- context,
17561
- dryRun,
17562
- rotateProviderToken
17563
- );
17816
+ const applied = await applyJiraProfile(plan, state, secrets, context, dryRun, rotateProviderToken);
17564
17817
  appliedProfile = applied.profile;
17565
17818
  actionResults.push(applied.result);
17566
17819
  } else if (action.scope === "project-default") {
@@ -17623,49 +17876,146 @@ async function applyCommand(args, context, deps) {
17623
17876
  async function applyAzureCommand(plan, parsed, context, deps, secrets) {
17624
17877
  const dryRun = parsed.boolFlags.has("--dry-run");
17625
17878
  const scopes = approvedScopes(plan, parsed, dryRun);
17626
- const profiles = requireServiceData(await context.invokeForgeContract("listScenarioAutomationProfiles", { context: { projectKey: plan.project.key } }), "listScenarioAutomationProfiles");
17879
+ const profiles = requireServiceData(
17880
+ await context.invokeForgeContract("listScenarioAutomationProfiles", { context: { projectKey: plan.project.key } }),
17881
+ "listScenarioAutomationProfiles"
17882
+ );
17627
17883
  const current = profiles.find((item) => item.label === plan.jiraProfile.label);
17628
- if (!dryRun && scopes.includes("provider-secrets") && !secrets.values.azureDevOpsAdminToken) throw new SetupCommandError("SECRET_INPUT_REQUIRED", "azureDevOpsAdminToken is required for the approved Azure DevOps pipeline-variable scope.", ExitCode.ValidationError);
17884
+ if (!dryRun && scopes.includes("provider-secrets") && !secrets.values.azureDevOpsAdminToken)
17885
+ throw new SetupCommandError(
17886
+ "SECRET_INPUT_REQUIRED",
17887
+ "azureDevOpsAdminToken is required for the approved Azure DevOps pipeline-variable scope.",
17888
+ ExitCode.ValidationError
17889
+ );
17629
17890
  const results = [];
17630
17891
  const secretNames = scopes.includes("provider-secrets") && secrets.values.azureDevOpsAdminToken ? (await deps.azureProvider.inspectCallbackSecretMetadata(plan, secrets.values.azureDevOpsAdminToken)).names : void 0;
17631
17892
  let appliedProfile = current;
17632
17893
  for (const action of plan.actions) {
17633
17894
  if (!scopes.includes(action.scope)) {
17634
- results.push({ id: action.id, scope: action.scope, status: "skipped", message: "Action was not in the explicit approval scope.", rollback: { available: false, guidance: "No mutation occurred." } });
17895
+ results.push({
17896
+ id: action.id,
17897
+ scope: action.scope,
17898
+ status: "skipped",
17899
+ message: "Action was not in the explicit approval scope.",
17900
+ rollback: { available: false, guidance: "No mutation occurred." }
17901
+ });
17635
17902
  continue;
17636
17903
  }
17637
17904
  if (action.scope === "provider-secrets") {
17638
- const pending = plan.azureDevOps.requiredSecrets.filter((item) => parsed.boolFlags.has("--rotate-secrets") || !secretNames?.has(item.repositorySecretName));
17639
- if (!dryRun && pending.some((item) => !secrets.values[item.valueKey])) throw new SetupCommandError("SECRET_INPUT_REQUIRED", "Azure DevOps callback secrets are required for approved pipeline variable changes.", ExitCode.ValidationError);
17640
- if (!dryRun) for (const item of pending) await deps.azureProvider.setCallbackSecret(plan, item, secrets.values[item.valueKey], secrets.values.azureDevOpsAdminToken);
17641
- results.push({ id: action.id, scope: action.scope, status: dryRun ? "planned" : pending.length ? "updated" : "skipped", message: "Azure DevOps pipeline variable names are managed without printing secret values.", rollback: { available: false, guidance: rollbackFor(plan, action.id) } });
17905
+ const pending = plan.azureDevOps.requiredSecrets.filter(
17906
+ (item) => parsed.boolFlags.has("--rotate-secrets") || !secretNames?.has(item.repositorySecretName)
17907
+ );
17908
+ if (!dryRun && pending.some((item) => !secrets.values[item.valueKey]))
17909
+ throw new SetupCommandError(
17910
+ "SECRET_INPUT_REQUIRED",
17911
+ "Azure DevOps callback secrets are required for approved pipeline variable changes.",
17912
+ ExitCode.ValidationError
17913
+ );
17914
+ if (!dryRun)
17915
+ for (const item of pending)
17916
+ await deps.azureProvider.setCallbackSecret(
17917
+ plan,
17918
+ item,
17919
+ secrets.values[item.valueKey],
17920
+ secrets.values.azureDevOpsAdminToken
17921
+ );
17922
+ results.push({
17923
+ id: action.id,
17924
+ scope: action.scope,
17925
+ status: dryRun ? "planned" : pending.length ? "updated" : "skipped",
17926
+ message: "Azure DevOps pipeline variable names are managed without printing secret values.",
17927
+ rollback: { available: false, guidance: rollbackFor(plan, action.id) }
17928
+ });
17642
17929
  } else if (action.scope === "jira-profile") {
17643
17930
  const rotateProvider = parsed.boolFlags.has("--rotate-provider-token");
17644
- if (!dryRun && (!current?.hasSecret || rotateProvider) && !secrets.values.azureDevOpsProviderToken) throw new SetupCommandError("SECRET_INPUT_REQUIRED", "azureDevOpsProviderToken is required to create or rotate the Azure provider PAT.", ExitCode.ValidationError);
17645
- const profile = dryRun ? current ?? { id: "<created-by-forge>", projectKey: plan.project.key, label: plan.jiraProfile.label, provider: "azureDevops", authType: "basicPat", method: "POST", endpointSummary: "<computed-by-forge>", config: plan.jiraProfile.config, enabled: plan.jiraProfile.enabled, hasSecret: false, createdAt: "", updatedAt: "" } : requireServiceData(await context.invokeForgeContract("upsertScenarioAutomationProfile", { context: { projectKey: plan.project.key }, input: { profileId: current?.id, label: plan.jiraProfile.label, provider: "azureDevops", config: plan.jiraProfile.config, enabled: plan.jiraProfile.enabled, secretToken: !current?.hasSecret || rotateProvider ? secrets.values.azureDevOpsProviderToken : void 0 } }), "upsertScenarioAutomationProfile");
17931
+ if (!dryRun && (!current?.hasSecret || rotateProvider) && !secrets.values.azureDevOpsProviderToken)
17932
+ throw new SetupCommandError(
17933
+ "SECRET_INPUT_REQUIRED",
17934
+ "azureDevOpsProviderToken is required to create or rotate the Azure provider PAT.",
17935
+ ExitCode.ValidationError
17936
+ );
17937
+ const profile = dryRun ? current ?? {
17938
+ id: "<created-by-forge>",
17939
+ projectKey: plan.project.key,
17940
+ label: plan.jiraProfile.label,
17941
+ provider: "azureDevops",
17942
+ authType: "basicPat",
17943
+ method: "POST",
17944
+ endpointSummary: "<computed-by-forge>",
17945
+ config: plan.jiraProfile.config,
17946
+ enabled: plan.jiraProfile.enabled,
17947
+ hasSecret: false,
17948
+ createdAt: "",
17949
+ updatedAt: ""
17950
+ } : requireServiceData(
17951
+ await context.invokeForgeContract("upsertScenarioAutomationProfile", {
17952
+ context: { projectKey: plan.project.key },
17953
+ input: {
17954
+ profileId: current?.id,
17955
+ label: plan.jiraProfile.label,
17956
+ provider: "azureDevops",
17957
+ config: plan.jiraProfile.config,
17958
+ enabled: plan.jiraProfile.enabled,
17959
+ secretToken: !current?.hasSecret || rotateProvider ? secrets.values.azureDevOpsProviderToken : void 0
17960
+ }
17961
+ }),
17962
+ "upsertScenarioAutomationProfile"
17963
+ );
17646
17964
  appliedProfile = profile;
17647
- results.push({ id: action.id, scope: action.scope, status: dryRun ? "planned" : current ? "updated" : "created", message: "Azure DevOps Jira automation profile applied through the existing Forge contract.", rollback: { available: !dryRun, guidance: rollbackFor(plan, action.id) } });
17965
+ results.push({
17966
+ id: action.id,
17967
+ scope: action.scope,
17968
+ status: dryRun ? "planned" : current ? "updated" : "created",
17969
+ message: "Azure DevOps Jira automation profile applied through the existing Forge contract.",
17970
+ rollback: { available: !dryRun, guidance: rollbackFor(plan, action.id) }
17971
+ });
17648
17972
  } else if (action.scope === "project-default" && appliedProfile && !dryRun) {
17649
- requireServiceData(await context.invokeForgeContract("setScenarioAutomationDefaultProfile", { context: { projectKey: plan.project.key }, profileId: appliedProfile.id }), "setScenarioAutomationDefaultProfile");
17650
- results.push({ id: action.id, scope: action.scope, status: "updated", message: "Set the Azure DevOps Jira automation profile as project default.", rollback: { available: true, guidance: rollbackFor(plan, action.id) } });
17973
+ requireServiceData(
17974
+ await context.invokeForgeContract("setScenarioAutomationDefaultProfile", {
17975
+ context: { projectKey: plan.project.key },
17976
+ profileId: appliedProfile.id
17977
+ }),
17978
+ "setScenarioAutomationDefaultProfile"
17979
+ );
17980
+ results.push({
17981
+ id: action.id,
17982
+ scope: action.scope,
17983
+ status: "updated",
17984
+ message: "Set the Azure DevOps Jira automation profile as project default.",
17985
+ rollback: { available: true, guidance: rollbackFor(plan, action.id) }
17986
+ });
17651
17987
  } else if (action.scope === "project-default") {
17652
- results.push({ id: action.id, scope: action.scope, status: "planned", message: "Set the Azure DevOps Jira automation profile as project default.", rollback: { available: false, guidance: rollbackFor(plan, action.id) } });
17988
+ results.push({
17989
+ id: action.id,
17990
+ scope: action.scope,
17991
+ status: "planned",
17992
+ message: "Set the Azure DevOps Jira automation profile as project default.",
17993
+ rollback: { available: false, guidance: rollbackFor(plan, action.id) }
17994
+ });
17653
17995
  }
17654
17996
  }
17655
- return jsonResponse({ schemaVersion: APPLY_RESULT_SCHEMA_VERSION, planId: plan.planId, provider: plan.provider, status: dryRun ? "dry-run" : "applied", dryRun, confirmed: !dryRun, approvedScopes: scopes, actions: results, smokeValidation: { status: "not-run", externalRunTriggered: false } });
17997
+ return jsonResponse({
17998
+ schemaVersion: APPLY_RESULT_SCHEMA_VERSION,
17999
+ planId: plan.planId,
18000
+ provider: plan.provider,
18001
+ status: dryRun ? "dry-run" : "applied",
18002
+ dryRun,
18003
+ confirmed: !dryRun,
18004
+ approvedScopes: scopes,
18005
+ actions: results,
18006
+ smokeValidation: { status: "not-run", externalRunTriggered: false }
18007
+ });
17656
18008
  }
17657
18009
  async function doctorCommand(args, context, deps) {
17658
- const parsed = parseArgs10(
17659
- args,
17660
- APPLY_VALUE_FLAGS,
17661
- /* @__PURE__ */ new Set(["--secrets-stdin"]),
17662
- true
17663
- );
18010
+ const parsed = parseArgs10(args, APPLY_VALUE_FLAGS, /* @__PURE__ */ new Set(["--secrets-stdin"]), true);
17664
18011
  if (parsed.approvals.length > 0) {
17665
18012
  return jsonResponse(errorPayload("USAGE_ERROR", "setup doctor does not accept --approve."), ExitCode.UsageError);
17666
18013
  }
17667
18014
  if (parsed.errors.length > 0) {
17668
- return jsonResponse(errorPayload("USAGE_ERROR", "Invalid setup doctor arguments.", parsed.errors), ExitCode.UsageError);
18015
+ return jsonResponse(
18016
+ errorPayload("USAGE_ERROR", "Invalid setup doctor arguments.", parsed.errors),
18017
+ ExitCode.UsageError
18018
+ );
17669
18019
  }
17670
18020
  const planPath = parsed.flags["--plan"];
17671
18021
  if (!planPath) {
@@ -17677,19 +18027,61 @@ async function doctorCommand(args, context, deps) {
17677
18027
  secrets = await resolveSecrets(parsed, deps);
17678
18028
  if (plan.provider === "azure-devops") {
17679
18029
  const checks = [];
17680
- const inspection = await deps.azureProvider.inspectExecutionDefinition(plan, secrets.values.azureDevOpsProviderToken ?? secrets.values.azureDevOpsAdminToken);
17681
- checks.push({ id: "pipeline-azure", status: inspection.found ? "pass" : "fail", message: inspection.message });
17682
- const variables = await deps.azureProvider.inspectCallbackSecretMetadata(plan, secrets.values.azureDevOpsAdminToken);
17683
- const missing = plan.azureDevOps.requiredSecrets.filter((item) => !variables.names.has(item.repositorySecretName));
17684
- checks.push({ id: "callback-secret-metadata", status: variables.verified && missing.length === 0 ? "pass" : variables.verified ? "fail" : "warn", message: variables.message });
17685
- const profiles = requireServiceData(await context.invokeForgeContract("listScenarioAutomationProfiles", { context: { projectKey: plan.project.key } }), "listScenarioAutomationProfiles");
18030
+ const inspection = await deps.azureProvider.inspectExecutionDefinition(
18031
+ plan,
18032
+ secrets.values.azureDevOpsProviderToken ?? secrets.values.azureDevOpsAdminToken
18033
+ );
18034
+ checks.push({
18035
+ id: "pipeline-azure",
18036
+ status: inspection.found && inspection.active === true ? "pass" : "fail",
18037
+ message: inspection.message
18038
+ });
18039
+ const variables = await deps.azureProvider.inspectCallbackSecretMetadata(
18040
+ plan,
18041
+ secrets.values.azureDevOpsAdminToken
18042
+ );
18043
+ const missing = plan.azureDevOps.requiredSecrets.filter(
18044
+ (item) => !variables.names.has(item.repositorySecretName)
18045
+ );
18046
+ checks.push({
18047
+ id: "callback-secret-metadata",
18048
+ status: variables.verified && missing.length === 0 ? "pass" : variables.verified ? "fail" : "warn",
18049
+ message: variables.message
18050
+ });
18051
+ const profiles = requireServiceData(
18052
+ await context.invokeForgeContract("listScenarioAutomationProfiles", {
18053
+ context: { projectKey: plan.project.key }
18054
+ }),
18055
+ "listScenarioAutomationProfiles"
18056
+ );
17686
18057
  const profile = profiles.find((item) => item.label === plan.jiraProfile.label);
17687
18058
  const config = profile?.config;
17688
18059
  const matches = profile?.provider === "azureDevops" && profile.enabled === plan.jiraProfile.enabled && profile.hasSecret && config?.organization === plan.jiraProfile.config.organization && config?.project === plan.jiraProfile.config.project && config?.pipelineId === plan.jiraProfile.config.pipelineId && config?.apiVersion === plan.jiraProfile.config.apiVersion && config?.bodyTemplate === plan.jiraProfile.config.bodyTemplate;
17689
- checks.push({ id: "jira-profile", status: matches ? "pass" : "fail", message: matches ? "Jira automation profile fields match and Forge reports a stored provider secret." : "Jira automation profile is missing, differs, or lacks provider secret metadata." });
17690
- if (plan.jiraProfile.setProjectDefault) checks.push({ id: "jira-project-default", status: profile?.isProjectDefault ? "pass" : "fail", message: profile?.isProjectDefault ? "Jira automation profile is the project default." : "Jira automation profile is not the project default." });
18060
+ checks.push({
18061
+ id: "jira-profile",
18062
+ status: matches ? "pass" : "fail",
18063
+ message: matches ? "Jira automation profile fields match and Forge reports a stored provider secret." : "Jira automation profile is missing, differs, or lacks provider secret metadata."
18064
+ });
18065
+ if (plan.jiraProfile.setProjectDefault)
18066
+ checks.push({
18067
+ id: "jira-project-default",
18068
+ status: profile?.isProjectDefault ? "pass" : "fail",
18069
+ message: profile?.isProjectDefault ? "Jira automation profile is the project default." : "Jira automation profile is not the project default."
18070
+ });
17691
18071
  const status = doctorStatus(checks);
17692
- return jsonResponse({ schemaVersion: DOCTOR_RESULT_SCHEMA_VERSION, planId: plan.planId, provider: plan.provider, status, exitCode: status === "fail" ? ExitCode.ValidationError : ExitCode.Success, externalRunTriggered: false, checks });
18072
+ const exitCode = status === "fail" ? ExitCode.ValidationError : ExitCode.Success;
18073
+ return jsonResponse(
18074
+ {
18075
+ schemaVersion: DOCTOR_RESULT_SCHEMA_VERSION,
18076
+ planId: plan.planId,
18077
+ provider: plan.provider,
18078
+ status,
18079
+ exitCode,
18080
+ externalRunTriggered: false,
18081
+ checks
18082
+ },
18083
+ exitCode
18084
+ );
17693
18085
  }
17694
18086
  const repoRoot = import_node_path15.default.resolve(deps.cwd, parsed.flags["--repo-root"] ?? ".");
17695
18087
  const summary = await inspectPlan(plan, repoRoot, secrets, context, deps);
@@ -17811,10 +18203,7 @@ function suitesToLines(items) {
17811
18203
  if (items.length === 0) {
17812
18204
  return ["Test suites: 0", "No test suites found for the selected context."];
17813
18205
  }
17814
- return [
17815
- `Test suites: ${items.length}`,
17816
- ...items.map((item) => `- ${item.key} ${item.name}`)
17817
- ];
18206
+ return [`Test suites: ${items.length}`, ...items.map((item) => `- ${item.key} ${item.name}`)];
17818
18207
  }
17819
18208
  function suiteToLines(suite) {
17820
18209
  return [
@@ -18381,9 +18770,7 @@ function buildHelpText(registry = COMMAND_REGISTRY, experimentalSyncEnabled = fa
18381
18770
  "Available command groups:"
18382
18771
  ];
18383
18772
  for (const command of visibleCommands) {
18384
- lines.push(
18385
- ` ${command.name.padEnd(12)} ${command.description}${formatSubcommands(command.subcommands)}`
18386
- );
18773
+ lines.push(` ${command.name.padEnd(12)} ${command.description}${formatSubcommands(command.subcommands)}`);
18387
18774
  }
18388
18775
  lines.push(
18389
18776
  "",