@automatify-au/cli 0.1.12 → 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 +5 -5
- package/dist/automatify.cjs +815 -226
- 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) {
|
|
@@ -16668,18 +16755,295 @@ var GitHubSetupProvider = class {
|
|
|
16668
16755
|
}
|
|
16669
16756
|
};
|
|
16670
16757
|
|
|
16758
|
+
// src/azureDevOpsSetupProvider.ts
|
|
16759
|
+
var isRecord3 = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
16760
|
+
async function adoGet(fetchImpl, url, token) {
|
|
16761
|
+
const response = await fetchImpl(url, {
|
|
16762
|
+
headers: {
|
|
16763
|
+
accept: "application/json",
|
|
16764
|
+
...token ? { authorization: `Basic ${Buffer.from(`:${token}`).toString("base64")}` } : {}
|
|
16765
|
+
}
|
|
16766
|
+
});
|
|
16767
|
+
const text = await response.text();
|
|
16768
|
+
let body = {};
|
|
16769
|
+
try {
|
|
16770
|
+
body = text ? JSON.parse(text) : {};
|
|
16771
|
+
} catch {
|
|
16772
|
+
}
|
|
16773
|
+
return { ok: response.ok, status: response.status, body };
|
|
16774
|
+
}
|
|
16775
|
+
async function setAzurePipelineVariableWithRest(input, fetchImpl) {
|
|
16776
|
+
const base = `https://dev.azure.com/${encodeURIComponent(input.organization)}/${encodeURIComponent(input.project)}/_apis/build/definitions/${encodeURIComponent(input.pipelineId)}`;
|
|
16777
|
+
const auth = { authorization: `Basic ${Buffer.from(`:${input.token}`).toString("base64")}` };
|
|
16778
|
+
const current = await adoGet(fetchImpl, `${base}?api-version=7.1`, input.token);
|
|
16779
|
+
if (!current.ok || !isRecord3(current.body))
|
|
16780
|
+
throw new Error(`Azure DevOps build definition could not be read (HTTP ${current.status}).`);
|
|
16781
|
+
const variables = isRecord3(current.body.variables) ? { ...current.body.variables } : {};
|
|
16782
|
+
variables[input.name] = { value: input.value, isSecret: true };
|
|
16783
|
+
const query = new URLSearchParams({
|
|
16784
|
+
"api-version": "7.1",
|
|
16785
|
+
secretsSourceDefinitionId: String(current.body.id),
|
|
16786
|
+
secretsSourceDefinitionRevision: String(current.body.revision)
|
|
16787
|
+
});
|
|
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
|
+
});
|
|
16793
|
+
if (!response.ok) throw new Error(`Azure DevOps pipeline variable update failed (HTTP ${response.status}).`);
|
|
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
|
+
}
|
|
16807
|
+
var AzureDevOpsSetupProvider = class {
|
|
16808
|
+
name = "azure-devops";
|
|
16809
|
+
fetchImpl;
|
|
16810
|
+
setter;
|
|
16811
|
+
constructor(deps = {}) {
|
|
16812
|
+
this.fetchImpl = deps.fetchImpl ?? fetch;
|
|
16813
|
+
this.setter = deps.setPipelineVariable ?? ((input) => setAzurePipelineVariableWithRest(input, this.fetchImpl));
|
|
16814
|
+
}
|
|
16815
|
+
async inspectExecutionDefinition(plan, token) {
|
|
16816
|
+
const a = plan.azureDevOps;
|
|
16817
|
+
const url = `https://dev.azure.com/${encodeURIComponent(a.organization)}/${encodeURIComponent(a.project)}/_apis/build/definitions/${encodeURIComponent(a.pipelineId)}?api-version=${encodeURIComponent(a.apiVersion)}`;
|
|
16818
|
+
const result = await adoGet(this.fetchImpl, url, token);
|
|
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
|
+
};
|
|
16824
|
+
const body = isRecord3(result.body) ? result.body : {};
|
|
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
|
+
};
|
|
16834
|
+
}
|
|
16835
|
+
async inspectCallbackSecretMetadata(plan, 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
|
+
};
|
|
16842
|
+
const a = plan.azureDevOps;
|
|
16843
|
+
const url = `https://dev.azure.com/${encodeURIComponent(a.organization)}/${encodeURIComponent(a.project)}/_apis/build/definitions/${encodeURIComponent(a.pipelineId)}?api-version=${encodeURIComponent(a.apiVersion)}`;
|
|
16844
|
+
const result = await adoGet(this.fetchImpl, url, token);
|
|
16845
|
+
if (!result.ok) throw new Error(`Azure DevOps pipeline metadata request failed with HTTP ${result.status}.`);
|
|
16846
|
+
const variables = isRecord3(result.body) && isRecord3(result.body.variables) ? result.body.variables : {};
|
|
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
|
+
};
|
|
16852
|
+
}
|
|
16853
|
+
async setCallbackSecret(plan, secret, value, 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
|
+
});
|
|
16864
|
+
}
|
|
16865
|
+
};
|
|
16866
|
+
|
|
16867
|
+
// src/azureDevOpsSetupPlan.ts
|
|
16868
|
+
var import_node_crypto2 = require("node:crypto");
|
|
16869
|
+
var digest = (v) => `sha256:${(0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(v)).digest("hex")}`;
|
|
16870
|
+
var required = (value, field) => {
|
|
16871
|
+
const result = value.trim();
|
|
16872
|
+
if (!result) throw new Error(`${field} is required.`);
|
|
16873
|
+
return result;
|
|
16874
|
+
};
|
|
16875
|
+
var variableName = (value, field) => {
|
|
16876
|
+
const result = required(value, field).toUpperCase();
|
|
16877
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(result)) throw new Error(`${field} is invalid.`);
|
|
16878
|
+
return result;
|
|
16879
|
+
};
|
|
16880
|
+
function buildAzureDevOpsSetupPlan(input) {
|
|
16881
|
+
const projectKey = required(input.projectKey, "project key").toUpperCase();
|
|
16882
|
+
if (!/^[A-Z][A-Z0-9_]{1,31}$/.test(projectKey)) throw new Error("project key is invalid.");
|
|
16883
|
+
const organization = required(input.organization, "organization");
|
|
16884
|
+
const azureProject = required(input.azureProject, "Azure project");
|
|
16885
|
+
const pipelineId = required(input.pipelineId, "pipeline id");
|
|
16886
|
+
if (!/^\d+$/.test(pipelineId)) throw new Error("pipeline id must be numeric.");
|
|
16887
|
+
const ref = required(input.ref, "ref");
|
|
16888
|
+
const apiVersion = required(input.apiVersion ?? "7.1", "api version");
|
|
16889
|
+
const profileLabel = required(input.profileLabel, "profile label");
|
|
16890
|
+
const endpointName = variableName(input.callbackEndpointVariableName, "callback endpoint variable name");
|
|
16891
|
+
const tokenName = variableName(input.callbackAuthTokenVariableName, "callback auth token variable name");
|
|
16892
|
+
if (endpointName === tokenName) throw new Error("callback variable names must be distinct.");
|
|
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
|
+
};
|
|
16974
|
+
return { ...withoutId, planId: digest(withoutId) };
|
|
16975
|
+
}
|
|
16976
|
+
function validateAzureDevOpsSetupPlan(value) {
|
|
16977
|
+
const errors = [];
|
|
16978
|
+
const record = value;
|
|
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.");
|
|
17000
|
+
const a = record?.azureDevOps;
|
|
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.");
|
|
17007
|
+
const p = record?.jiraProfile;
|
|
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.");
|
|
17010
|
+
const ids = p?.setProjectDefault ? ["set-provider-secrets", "upsert-jira-profile", "set-project-default"] : ["set-provider-secrets", "upsert-jira-profile"];
|
|
17011
|
+
const scopes = p?.setProjectDefault ? ["provider-secrets", "jira-profile", "project-default"] : ["provider-secrets", "jira-profile"];
|
|
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.");
|
|
17016
|
+
const smokeIds = ["pipeline-content", "pipeline-variable-metadata", "jira-profile"];
|
|
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.");
|
|
17025
|
+
if (errors.length === 0) {
|
|
17026
|
+
const { planId, ...rest } = record;
|
|
17027
|
+
if (planId !== digest(rest)) errors.push("planId does not match the plan contents.");
|
|
17028
|
+
}
|
|
17029
|
+
return errors.length ? { ok: false, errors } : { ok: true, errors, plan: value };
|
|
17030
|
+
}
|
|
17031
|
+
|
|
16671
17032
|
// src/setup.ts
|
|
16672
17033
|
var APPLY_RESULT_SCHEMA_VERSION = "automatify.testops.setup.apply/v1";
|
|
16673
17034
|
var DOCTOR_RESULT_SCHEMA_VERSION = "automatify.testops.setup.doctor/v1";
|
|
16674
17035
|
var SECRET_KEYS = [
|
|
16675
17036
|
"githubProviderToken",
|
|
16676
17037
|
"githubAdminToken",
|
|
17038
|
+
"azureDevOpsProviderToken",
|
|
17039
|
+
"azureDevOpsAdminToken",
|
|
16677
17040
|
"callbackEndpoint",
|
|
16678
17041
|
"callbackAuthToken"
|
|
16679
17042
|
];
|
|
16680
17043
|
var MUTATION_SCOPES = [
|
|
16681
17044
|
"workflow-file",
|
|
16682
17045
|
"github-secrets",
|
|
17046
|
+
"provider-secrets",
|
|
16683
17047
|
"jira-profile",
|
|
16684
17048
|
"project-default"
|
|
16685
17049
|
];
|
|
@@ -16704,7 +17068,13 @@ var PLAN_VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
16704
17068
|
"--workflow-source",
|
|
16705
17069
|
"--profile-label",
|
|
16706
17070
|
"--callback-endpoint-secret-name",
|
|
16707
|
-
"--callback-auth-token-secret-name"
|
|
17071
|
+
"--callback-auth-token-secret-name",
|
|
17072
|
+
"--organization",
|
|
17073
|
+
"--azure-project",
|
|
17074
|
+
"--pipeline-id",
|
|
17075
|
+
"--api-version",
|
|
17076
|
+
"--callback-endpoint-variable-name",
|
|
17077
|
+
"--callback-auth-token-variable-name"
|
|
16708
17078
|
]);
|
|
16709
17079
|
var APPLY_VALUE_FLAGS = /* @__PURE__ */ new Set(["--plan", "--repo-root"]);
|
|
16710
17080
|
var APPLY_BOOL_FLAGS = /* @__PURE__ */ new Set([
|
|
@@ -16815,7 +17185,7 @@ function loadPlan(planPath, cwd) {
|
|
|
16815
17185
|
ExitCode.ValidationError
|
|
16816
17186
|
);
|
|
16817
17187
|
}
|
|
16818
|
-
const validation = validateSetupPlan(parsed);
|
|
17188
|
+
const validation = isAzurePlanValue(parsed) ? validateAzureDevOpsSetupPlan(parsed) : validateSetupPlan(parsed);
|
|
16819
17189
|
if (!validation.ok || !validation.plan) {
|
|
16820
17190
|
throw new SetupCommandError(
|
|
16821
17191
|
"PLAN_VALIDATION_ERROR",
|
|
@@ -16825,6 +17195,9 @@ function loadPlan(planPath, cwd) {
|
|
|
16825
17195
|
}
|
|
16826
17196
|
return validation.plan;
|
|
16827
17197
|
}
|
|
17198
|
+
function isAzurePlanValue(value) {
|
|
17199
|
+
return Boolean(value && typeof value === "object" && value.provider === "azure-devops");
|
|
17200
|
+
}
|
|
16828
17201
|
function isSecretKey(value) {
|
|
16829
17202
|
return SECRET_KEYS.includes(value);
|
|
16830
17203
|
}
|
|
@@ -16844,14 +17217,26 @@ async function resolveSecrets(parsed, deps) {
|
|
|
16844
17217
|
);
|
|
16845
17218
|
}
|
|
16846
17219
|
if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
|
|
16847
|
-
throw new SetupCommandError(
|
|
17220
|
+
throw new SetupCommandError(
|
|
17221
|
+
"SECRET_INPUT_ERROR",
|
|
17222
|
+
"Secret stdin must be a JSON object.",
|
|
17223
|
+
ExitCode.ValidationError
|
|
17224
|
+
);
|
|
16848
17225
|
}
|
|
16849
17226
|
for (const [key, value] of Object.entries(envelope)) {
|
|
16850
17227
|
if (!isSecretKey(key)) {
|
|
16851
|
-
throw new SetupCommandError(
|
|
17228
|
+
throw new SetupCommandError(
|
|
17229
|
+
"SECRET_INPUT_ERROR",
|
|
17230
|
+
`Secret stdin contains unsupported key ${key}.`,
|
|
17231
|
+
ExitCode.ValidationError
|
|
17232
|
+
);
|
|
16852
17233
|
}
|
|
16853
17234
|
if (typeof value !== "string" || !value.trim()) {
|
|
16854
|
-
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
|
+
);
|
|
16855
17240
|
}
|
|
16856
17241
|
values[key] = value.trim();
|
|
16857
17242
|
sources[key] = "stdin";
|
|
@@ -16870,10 +17255,18 @@ async function resolveSecrets(parsed, deps) {
|
|
|
16870
17255
|
}
|
|
16871
17256
|
const envName = target.slice(4);
|
|
16872
17257
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(envName)) {
|
|
16873
|
-
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
|
+
);
|
|
16874
17263
|
}
|
|
16875
17264
|
if (sources[key]) {
|
|
16876
|
-
throw new SetupCommandError(
|
|
17265
|
+
throw new SetupCommandError(
|
|
17266
|
+
"SECRET_REFERENCE_ERROR",
|
|
17267
|
+
`Secret ${key} was provided more than once.`,
|
|
17268
|
+
ExitCode.ValidationError
|
|
17269
|
+
);
|
|
16877
17270
|
}
|
|
16878
17271
|
const value = deps.env[envName]?.trim();
|
|
16879
17272
|
if (!value) {
|
|
@@ -16908,7 +17301,11 @@ function approvedScopes(plan, parsed, dryRun) {
|
|
|
16908
17301
|
throw new SetupCommandError("APPROVAL_ERROR", `Unsupported approval scope ${scope}.`, ExitCode.UsageError);
|
|
16909
17302
|
}
|
|
16910
17303
|
if (!planScopes.includes(scope)) {
|
|
16911
|
-
throw new SetupCommandError(
|
|
17304
|
+
throw new SetupCommandError(
|
|
17305
|
+
"APPROVAL_ERROR",
|
|
17306
|
+
`Approval scope ${scope} is not present in this plan.`,
|
|
17307
|
+
ExitCode.UsageError
|
|
17308
|
+
);
|
|
16912
17309
|
}
|
|
16913
17310
|
expanded.add(scope);
|
|
16914
17311
|
}
|
|
@@ -16981,10 +17378,7 @@ async function prepareState(plan, scopes, secrets, context, deps, dryRun, rotate
|
|
|
16981
17378
|
);
|
|
16982
17379
|
}
|
|
16983
17380
|
if (secrets.values.githubAdminToken) {
|
|
16984
|
-
const inspection = await deps.githubProvider.inspectCallbackSecretMetadata(
|
|
16985
|
-
plan,
|
|
16986
|
-
secrets.values.githubAdminToken
|
|
16987
|
-
);
|
|
17381
|
+
const inspection = await deps.githubProvider.inspectCallbackSecretMetadata(plan, secrets.values.githubAdminToken);
|
|
16988
17382
|
repositorySecretNames = inspection.names;
|
|
16989
17383
|
const secretsToWrite = plan.github.requiredSecrets.filter(
|
|
16990
17384
|
(secret) => rotateSecrets || !repositorySecretNames?.has(secret.repositorySecretName)
|
|
@@ -17087,12 +17481,7 @@ async function applyGitHubSecrets(plan, state, secrets, deps, dryRun, rotateSecr
|
|
|
17087
17481
|
}
|
|
17088
17482
|
const adminToken = secrets.values.githubAdminToken;
|
|
17089
17483
|
for (const secret of pending) {
|
|
17090
|
-
await deps.githubProvider.setCallbackSecret(
|
|
17091
|
-
plan,
|
|
17092
|
-
secret,
|
|
17093
|
-
secrets.values[secret.valueKey],
|
|
17094
|
-
adminToken
|
|
17095
|
-
);
|
|
17484
|
+
await deps.githubProvider.setCallbackSecret(plan, secret, secrets.values[secret.valueKey], adminToken);
|
|
17096
17485
|
}
|
|
17097
17486
|
return {
|
|
17098
17487
|
id: action.id,
|
|
@@ -17318,11 +17707,39 @@ async function inspectPlan(plan, repoRoot, secrets, context, deps) {
|
|
|
17318
17707
|
function planCommand(args, deps) {
|
|
17319
17708
|
const parsed = parseArgs10(args, PLAN_VALUE_FLAGS, /* @__PURE__ */ new Set(["--set-default", "--disabled"]));
|
|
17320
17709
|
if (parsed.errors.length > 0) {
|
|
17321
|
-
return jsonResponse(
|
|
17710
|
+
return jsonResponse(
|
|
17711
|
+
errorPayload("USAGE_ERROR", "Invalid setup plan arguments.", parsed.errors),
|
|
17712
|
+
ExitCode.UsageError
|
|
17713
|
+
);
|
|
17322
17714
|
}
|
|
17323
|
-
|
|
17715
|
+
const provider = parsed.flags["--provider"] ?? "github-actions";
|
|
17716
|
+
if (provider === "azure-devops") {
|
|
17717
|
+
try {
|
|
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
|
+
);
|
|
17733
|
+
} catch (error) {
|
|
17734
|
+
return jsonResponse(errorPayload("VALIDATION_ERROR", safeErrorMessage(error)), ExitCode.ValidationError);
|
|
17735
|
+
}
|
|
17736
|
+
}
|
|
17737
|
+
if (provider !== "github-actions") {
|
|
17324
17738
|
return jsonResponse(
|
|
17325
|
-
errorPayload(
|
|
17739
|
+
errorPayload(
|
|
17740
|
+
"VALIDATION_ERROR",
|
|
17741
|
+
"Only github-actions is implemented in this stage; Azure DevOps is the documented next adapter."
|
|
17742
|
+
),
|
|
17326
17743
|
ExitCode.ValidationError
|
|
17327
17744
|
);
|
|
17328
17745
|
}
|
|
@@ -17343,16 +17760,16 @@ function planCommand(args, deps) {
|
|
|
17343
17760
|
});
|
|
17344
17761
|
return jsonResponse(plan);
|
|
17345
17762
|
} catch (error) {
|
|
17346
|
-
return jsonResponse(
|
|
17347
|
-
errorPayload("VALIDATION_ERROR", safeErrorMessage(error)),
|
|
17348
|
-
ExitCode.ValidationError
|
|
17349
|
-
);
|
|
17763
|
+
return jsonResponse(errorPayload("VALIDATION_ERROR", safeErrorMessage(error)), ExitCode.ValidationError);
|
|
17350
17764
|
}
|
|
17351
17765
|
}
|
|
17352
17766
|
async function applyCommand(args, context, deps) {
|
|
17353
17767
|
const parsed = parseArgs10(args, APPLY_VALUE_FLAGS, APPLY_BOOL_FLAGS, true);
|
|
17354
17768
|
if (parsed.errors.length > 0) {
|
|
17355
|
-
return jsonResponse(
|
|
17769
|
+
return jsonResponse(
|
|
17770
|
+
errorPayload("USAGE_ERROR", "Invalid setup apply arguments.", parsed.errors),
|
|
17771
|
+
ExitCode.UsageError
|
|
17772
|
+
);
|
|
17356
17773
|
}
|
|
17357
17774
|
const planPath = parsed.flags["--plan"];
|
|
17358
17775
|
if (!planPath) {
|
|
@@ -17369,21 +17786,15 @@ async function applyCommand(args, context, deps) {
|
|
|
17369
17786
|
const actionResults = [];
|
|
17370
17787
|
try {
|
|
17371
17788
|
const plan = loadPlan(planPath, deps.cwd);
|
|
17372
|
-
const scopes = approvedScopes(plan, parsed, dryRun);
|
|
17373
17789
|
secrets = await resolveSecrets(parsed, deps);
|
|
17790
|
+
if (plan.provider === "azure-devops") {
|
|
17791
|
+
return await applyAzureCommand(plan, parsed, context, deps, secrets);
|
|
17792
|
+
}
|
|
17793
|
+
const scopes = approvedScopes(plan, parsed, dryRun);
|
|
17374
17794
|
const repoRoot = import_node_path15.default.resolve(deps.cwd, parsed.flags["--repo-root"] ?? ".");
|
|
17375
17795
|
const rotateSecrets = parsed.boolFlags.has("--rotate-secrets");
|
|
17376
17796
|
const rotateProviderToken = parsed.boolFlags.has("--rotate-provider-token");
|
|
17377
|
-
const state = await prepareState(
|
|
17378
|
-
plan,
|
|
17379
|
-
scopes,
|
|
17380
|
-
secrets,
|
|
17381
|
-
context,
|
|
17382
|
-
deps,
|
|
17383
|
-
dryRun,
|
|
17384
|
-
rotateSecrets,
|
|
17385
|
-
rotateProviderToken
|
|
17386
|
-
);
|
|
17797
|
+
const state = await prepareState(plan, scopes, secrets, context, deps, dryRun, rotateSecrets, rotateProviderToken);
|
|
17387
17798
|
let appliedProfile = state.matchingProfile;
|
|
17388
17799
|
for (const action of plan.actions) {
|
|
17389
17800
|
if (!scopes.includes(action.scope)) {
|
|
@@ -17402,14 +17813,7 @@ async function applyCommand(args, context, deps) {
|
|
|
17402
17813
|
} else if (action.scope === "github-secrets") {
|
|
17403
17814
|
actionResults.push(await applyGitHubSecrets(plan, state, secrets, deps, dryRun, rotateSecrets));
|
|
17404
17815
|
} else if (action.scope === "jira-profile") {
|
|
17405
|
-
const applied = await applyJiraProfile(
|
|
17406
|
-
plan,
|
|
17407
|
-
state,
|
|
17408
|
-
secrets,
|
|
17409
|
-
context,
|
|
17410
|
-
dryRun,
|
|
17411
|
-
rotateProviderToken
|
|
17412
|
-
);
|
|
17816
|
+
const applied = await applyJiraProfile(plan, state, secrets, context, dryRun, rotateProviderToken);
|
|
17413
17817
|
appliedProfile = applied.profile;
|
|
17414
17818
|
actionResults.push(applied.result);
|
|
17415
17819
|
} else if (action.scope === "project-default") {
|
|
@@ -17469,18 +17873,149 @@ async function applyCommand(args, context, deps) {
|
|
|
17469
17873
|
);
|
|
17470
17874
|
}
|
|
17471
17875
|
}
|
|
17472
|
-
async function
|
|
17473
|
-
const
|
|
17474
|
-
|
|
17475
|
-
|
|
17476
|
-
|
|
17477
|
-
|
|
17876
|
+
async function applyAzureCommand(plan, parsed, context, deps, secrets) {
|
|
17877
|
+
const dryRun = parsed.boolFlags.has("--dry-run");
|
|
17878
|
+
const scopes = approvedScopes(plan, parsed, dryRun);
|
|
17879
|
+
const profiles = requireServiceData(
|
|
17880
|
+
await context.invokeForgeContract("listScenarioAutomationProfiles", { context: { projectKey: plan.project.key } }),
|
|
17881
|
+
"listScenarioAutomationProfiles"
|
|
17478
17882
|
);
|
|
17883
|
+
const current = profiles.find((item) => item.label === plan.jiraProfile.label);
|
|
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
|
+
);
|
|
17890
|
+
const results = [];
|
|
17891
|
+
const secretNames = scopes.includes("provider-secrets") && secrets.values.azureDevOpsAdminToken ? (await deps.azureProvider.inspectCallbackSecretMetadata(plan, secrets.values.azureDevOpsAdminToken)).names : void 0;
|
|
17892
|
+
let appliedProfile = current;
|
|
17893
|
+
for (const action of plan.actions) {
|
|
17894
|
+
if (!scopes.includes(action.scope)) {
|
|
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
|
+
});
|
|
17902
|
+
continue;
|
|
17903
|
+
}
|
|
17904
|
+
if (action.scope === "provider-secrets") {
|
|
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
|
+
});
|
|
17929
|
+
} else if (action.scope === "jira-profile") {
|
|
17930
|
+
const rotateProvider = parsed.boolFlags.has("--rotate-provider-token");
|
|
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
|
+
);
|
|
17964
|
+
appliedProfile = profile;
|
|
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
|
+
});
|
|
17972
|
+
} else if (action.scope === "project-default" && appliedProfile && !dryRun) {
|
|
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
|
+
});
|
|
17987
|
+
} else if (action.scope === "project-default") {
|
|
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
|
+
});
|
|
17995
|
+
}
|
|
17996
|
+
}
|
|
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
|
+
});
|
|
18008
|
+
}
|
|
18009
|
+
async function doctorCommand(args, context, deps) {
|
|
18010
|
+
const parsed = parseArgs10(args, APPLY_VALUE_FLAGS, /* @__PURE__ */ new Set(["--secrets-stdin"]), true);
|
|
17479
18011
|
if (parsed.approvals.length > 0) {
|
|
17480
18012
|
return jsonResponse(errorPayload("USAGE_ERROR", "setup doctor does not accept --approve."), ExitCode.UsageError);
|
|
17481
18013
|
}
|
|
17482
18014
|
if (parsed.errors.length > 0) {
|
|
17483
|
-
return jsonResponse(
|
|
18015
|
+
return jsonResponse(
|
|
18016
|
+
errorPayload("USAGE_ERROR", "Invalid setup doctor arguments.", parsed.errors),
|
|
18017
|
+
ExitCode.UsageError
|
|
18018
|
+
);
|
|
17484
18019
|
}
|
|
17485
18020
|
const planPath = parsed.flags["--plan"];
|
|
17486
18021
|
if (!planPath) {
|
|
@@ -17490,6 +18025,64 @@ async function doctorCommand(args, context, deps) {
|
|
|
17490
18025
|
try {
|
|
17491
18026
|
const plan = loadPlan(planPath, deps.cwd);
|
|
17492
18027
|
secrets = await resolveSecrets(parsed, deps);
|
|
18028
|
+
if (plan.provider === "azure-devops") {
|
|
18029
|
+
const checks = [];
|
|
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
|
+
);
|
|
18057
|
+
const profile = profiles.find((item) => item.label === plan.jiraProfile.label);
|
|
18058
|
+
const config = profile?.config;
|
|
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;
|
|
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
|
+
});
|
|
18071
|
+
const status = doctorStatus(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
|
+
);
|
|
18085
|
+
}
|
|
17493
18086
|
const repoRoot = import_node_path15.default.resolve(deps.cwd, parsed.flags["--repo-root"] ?? ".");
|
|
17494
18087
|
const summary = await inspectPlan(plan, repoRoot, secrets, context, deps);
|
|
17495
18088
|
return jsonResponse(summary, summary.exitCode);
|
|
@@ -17505,7 +18098,8 @@ function createSetupHandler(overrides = {}) {
|
|
|
17505
18098
|
cwd: overrides.cwd ?? process.cwd(),
|
|
17506
18099
|
env: overrides.env ?? process.env,
|
|
17507
18100
|
readStdin: overrides.readStdin ?? defaultReadStdin,
|
|
17508
|
-
githubProvider: overrides.githubProvider ?? new GitHubSetupProvider(overrides.githubProviderDeps)
|
|
18101
|
+
githubProvider: overrides.githubProvider ?? new GitHubSetupProvider(overrides.githubProviderDeps),
|
|
18102
|
+
azureProvider: overrides.azureProvider ?? new AzureDevOpsSetupProvider()
|
|
17509
18103
|
};
|
|
17510
18104
|
return async (request, context) => {
|
|
17511
18105
|
const [subcommand, ...args] = request.args;
|
|
@@ -17609,10 +18203,7 @@ function suitesToLines(items) {
|
|
|
17609
18203
|
if (items.length === 0) {
|
|
17610
18204
|
return ["Test suites: 0", "No test suites found for the selected context."];
|
|
17611
18205
|
}
|
|
17612
|
-
return [
|
|
17613
|
-
`Test suites: ${items.length}`,
|
|
17614
|
-
...items.map((item) => `- ${item.key} ${item.name}`)
|
|
17615
|
-
];
|
|
18206
|
+
return [`Test suites: ${items.length}`, ...items.map((item) => `- ${item.key} ${item.name}`)];
|
|
17616
18207
|
}
|
|
17617
18208
|
function suiteToLines(suite) {
|
|
17618
18209
|
return [
|
|
@@ -18179,9 +18770,7 @@ function buildHelpText(registry = COMMAND_REGISTRY, experimentalSyncEnabled = fa
|
|
|
18179
18770
|
"Available command groups:"
|
|
18180
18771
|
];
|
|
18181
18772
|
for (const command of visibleCommands) {
|
|
18182
|
-
lines.push(
|
|
18183
|
-
` ${command.name.padEnd(12)} ${command.description}${formatSubcommands(command.subcommands)}`
|
|
18184
|
-
);
|
|
18773
|
+
lines.push(` ${command.name.padEnd(12)} ${command.description}${formatSubcommands(command.subcommands)}`);
|
|
18185
18774
|
}
|
|
18186
18775
|
lines.push(
|
|
18187
18776
|
"",
|