@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.
- package/README.md +4 -4
- package/dist/automatify.cjs +668 -281
- package/package.json +4 -4
package/dist/automatify.cjs
CHANGED
|
@@ -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
|
-
(
|
|
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
|
|
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: [
|
|
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 = [
|
|
11510
|
-
|
|
11511
|
-
|
|
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(
|
|
13028
|
-
|
|
13029
|
-
|
|
13030
|
-
|
|
13031
|
-
|
|
13032
|
-
|
|
13033
|
-
|
|
13034
|
-
|
|
13035
|
-
|
|
13036
|
-
|
|
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(
|
|
13088
|
-
|
|
13089
|
-
|
|
13090
|
-
|
|
13091
|
-
|
|
13092
|
-
|
|
13093
|
-
|
|
13094
|
-
|
|
13095
|
-
|
|
13096
|
-
|
|
13097
|
-
|
|
13098
|
-
|
|
13099
|
-
|
|
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(
|
|
13389
|
-
|
|
13390
|
-
|
|
13391
|
-
|
|
13392
|
-
|
|
13393
|
-
|
|
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(
|
|
13422
|
-
|
|
13423
|
-
|
|
13424
|
-
|
|
13425
|
-
|
|
13426
|
-
|
|
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(
|
|
13476
|
-
|
|
13477
|
-
|
|
13478
|
-
|
|
13479
|
-
|
|
13480
|
-
|
|
13481
|
-
|
|
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(
|
|
13519
|
-
|
|
13520
|
-
|
|
13521
|
-
|
|
13522
|
-
|
|
13523
|
-
|
|
13524
|
-
|
|
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 = [
|
|
13864
|
-
|
|
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(
|
|
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(
|
|
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: [
|
|
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: [
|
|
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(
|
|
15217
|
-
|
|
15218
|
-
|
|
15219
|
-
|
|
15220
|
-
|
|
15221
|
-
|
|
15222
|
-
|
|
15223
|
-
|
|
15224
|
-
|
|
15225
|
-
|
|
15226
|
-
|
|
15227
|
-
|
|
15228
|
-
|
|
15229
|
-
|
|
15230
|
-
|
|
15231
|
-
|
|
15232
|
-
|
|
15233
|
-
|
|
15234
|
-
|
|
15235
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16556
|
-
["
|
|
16557
|
-
{
|
|
16558
|
-
|
|
16559
|
-
|
|
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
|
|
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
|
-
|
|
16648
|
-
|
|
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, {
|
|
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))
|
|
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()}`, {
|
|
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)
|
|
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 :
|
|
16713
|
-
|
|
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)
|
|
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 {
|
|
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)
|
|
16726
|
-
|
|
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 = [
|
|
16757
|
-
|
|
16758
|
-
|
|
16759
|
-
|
|
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(
|
|
16766
|
-
|
|
16767
|
-
|
|
16768
|
-
if (!exact(
|
|
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 ?? ""))
|
|
16771
|
-
|
|
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)
|
|
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(
|
|
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(
|
|
16779
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
17455
|
-
|
|
17456
|
-
|
|
17457
|
-
|
|
17458
|
-
|
|
17459
|
-
|
|
17460
|
-
|
|
17461
|
-
|
|
17462
|
-
|
|
17463
|
-
|
|
17464
|
-
|
|
17465
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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)
|
|
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({
|
|
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(
|
|
17639
|
-
|
|
17640
|
-
|
|
17641
|
-
|
|
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)
|
|
17645
|
-
|
|
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({
|
|
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(
|
|
17650
|
-
|
|
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({
|
|
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({
|
|
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(
|
|
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(
|
|
17681
|
-
|
|
17682
|
-
|
|
17683
|
-
|
|
17684
|
-
checks.push({
|
|
17685
|
-
|
|
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({
|
|
17690
|
-
|
|
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
|
-
|
|
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
|
"",
|