@automatify-au/cli 0.1.13 → 0.1.15
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 +20 -6
- package/dist/automatify.cjs +1285 -358
- package/package.json +4 -4
package/dist/automatify.cjs
CHANGED
|
@@ -10765,10 +10765,170 @@ var Priority;
|
|
|
10765
10765
|
|
|
10766
10766
|
// src/config.ts
|
|
10767
10767
|
var import_node_fs = require("node:fs");
|
|
10768
|
-
var import_node_child_process = require("node:child_process");
|
|
10769
10768
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
10770
|
-
|
|
10769
|
+
|
|
10770
|
+
// src/secureStore.ts
|
|
10771
|
+
var import_node_child_process = require("node:child_process");
|
|
10771
10772
|
var KEYCHAIN_SERVICE = "automatify-testops-cli";
|
|
10773
|
+
function errorText(error) {
|
|
10774
|
+
return error instanceof Error && error.message ? error.message : "unknown secure-storage error";
|
|
10775
|
+
}
|
|
10776
|
+
function windowsProtectScript() {
|
|
10777
|
+
return [
|
|
10778
|
+
"Add-Type -AssemblyName System.Security",
|
|
10779
|
+
"$plain = [Console]::In.ReadToEnd()",
|
|
10780
|
+
"$bytes = [Text.Encoding]::UTF8.GetBytes($plain)",
|
|
10781
|
+
"$cipher = [System.Security.Cryptography.ProtectedData]::Protect($bytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)",
|
|
10782
|
+
"[Console]::Out.Write([Convert]::ToBase64String($cipher))"
|
|
10783
|
+
].join("; ");
|
|
10784
|
+
}
|
|
10785
|
+
function windowsUnprotectScript() {
|
|
10786
|
+
return [
|
|
10787
|
+
"Add-Type -AssemblyName System.Security",
|
|
10788
|
+
"$encoded = [Console]::In.ReadToEnd()",
|
|
10789
|
+
"$cipher = [Convert]::FromBase64String($encoded)",
|
|
10790
|
+
"$bytes = [System.Security.Cryptography.ProtectedData]::Unprotect($cipher, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)",
|
|
10791
|
+
"[Console]::Out.Write([Text.Encoding]::UTF8.GetString($bytes))"
|
|
10792
|
+
].join("; ");
|
|
10793
|
+
}
|
|
10794
|
+
function quoteSecurityInteractiveArg(value) {
|
|
10795
|
+
if (/\r|\n|\0/.test(value)) {
|
|
10796
|
+
throw new Error("macOS Keychain values cannot contain newline or NUL characters.");
|
|
10797
|
+
}
|
|
10798
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
10799
|
+
}
|
|
10800
|
+
function macOSKeychainWriteInput(account, value) {
|
|
10801
|
+
return [
|
|
10802
|
+
"add-generic-password",
|
|
10803
|
+
"-U",
|
|
10804
|
+
"-s",
|
|
10805
|
+
quoteSecurityInteractiveArg(KEYCHAIN_SERVICE),
|
|
10806
|
+
"-a",
|
|
10807
|
+
quoteSecurityInteractiveArg(account),
|
|
10808
|
+
"-w",
|
|
10809
|
+
quoteSecurityInteractiveArg(value)
|
|
10810
|
+
].join(" ") + "\n";
|
|
10811
|
+
}
|
|
10812
|
+
function readSecureSecret(locator, deps = {}) {
|
|
10813
|
+
const platform = deps.platform ?? process.platform;
|
|
10814
|
+
const run = deps.execFileSync ?? import_node_child_process.execFileSync;
|
|
10815
|
+
if (platform === "darwin") {
|
|
10816
|
+
if (!locator.account) {
|
|
10817
|
+
return locator.protectedValue ? {
|
|
10818
|
+
status: "error",
|
|
10819
|
+
message: "This config contains a Windows DPAPI value, but it is being read on macOS. Re-store the secret on this machine."
|
|
10820
|
+
} : { status: "missing" };
|
|
10821
|
+
}
|
|
10822
|
+
try {
|
|
10823
|
+
const value = run("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", locator.account, "-w"], {
|
|
10824
|
+
encoding: "utf8",
|
|
10825
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
10826
|
+
}).trim();
|
|
10827
|
+
return value ? { status: "available", value, source: "keychain" } : {
|
|
10828
|
+
status: "error",
|
|
10829
|
+
message: `macOS Keychain returned an empty value for account ${locator.account}. Re-store the secret.`
|
|
10830
|
+
};
|
|
10831
|
+
} catch (error) {
|
|
10832
|
+
return {
|
|
10833
|
+
status: "error",
|
|
10834
|
+
message: `Unable to read macOS Keychain account ${locator.account}: ${errorText(error)}. Re-store the secret.`
|
|
10835
|
+
};
|
|
10836
|
+
}
|
|
10837
|
+
}
|
|
10838
|
+
if (platform === "win32") {
|
|
10839
|
+
if (!locator.protectedValue) {
|
|
10840
|
+
return locator.account ? {
|
|
10841
|
+
status: "error",
|
|
10842
|
+
message: "This config references a macOS Keychain secret, but it is being read on Windows. Re-store the secret for the current Windows user."
|
|
10843
|
+
} : { status: "missing" };
|
|
10844
|
+
}
|
|
10845
|
+
try {
|
|
10846
|
+
const value = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", windowsUnprotectScript()], {
|
|
10847
|
+
input: locator.protectedValue,
|
|
10848
|
+
encoding: "utf8",
|
|
10849
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
10850
|
+
}).trim();
|
|
10851
|
+
return value ? { status: "available", value, source: "secure-store" } : {
|
|
10852
|
+
status: "error",
|
|
10853
|
+
message: "Windows DPAPI returned an empty value. Re-store the secret for the current Windows user."
|
|
10854
|
+
};
|
|
10855
|
+
} catch (error) {
|
|
10856
|
+
return {
|
|
10857
|
+
status: "error",
|
|
10858
|
+
message: `Unable to decrypt the Windows DPAPI value for the current user: ${errorText(error)}. Re-store the secret.`
|
|
10859
|
+
};
|
|
10860
|
+
}
|
|
10861
|
+
}
|
|
10862
|
+
if (locator.account || locator.protectedValue) {
|
|
10863
|
+
return {
|
|
10864
|
+
status: "error",
|
|
10865
|
+
message: "This config contains local secure-store metadata, but local secure storage is supported only on macOS and Windows. Use an environment variable on Linux/CI."
|
|
10866
|
+
};
|
|
10867
|
+
}
|
|
10868
|
+
return { status: "missing" };
|
|
10869
|
+
}
|
|
10870
|
+
function writeSecureSecret(account, value, deps = {}) {
|
|
10871
|
+
const platform = deps.platform ?? process.platform;
|
|
10872
|
+
const run = deps.execFileSync ?? import_node_child_process.execFileSync;
|
|
10873
|
+
if (platform === "darwin") {
|
|
10874
|
+
try {
|
|
10875
|
+
run("security", ["-q", "-i"], {
|
|
10876
|
+
input: macOSKeychainWriteInput(account, value),
|
|
10877
|
+
encoding: "utf8",
|
|
10878
|
+
stdio: ["pipe", "ignore", "pipe"]
|
|
10879
|
+
});
|
|
10880
|
+
} catch {
|
|
10881
|
+
throw new Error("Unable to store the secret in macOS Keychain.");
|
|
10882
|
+
}
|
|
10883
|
+
return { source: "keychain", account };
|
|
10884
|
+
}
|
|
10885
|
+
if (platform === "win32") {
|
|
10886
|
+
let protectedValue;
|
|
10887
|
+
try {
|
|
10888
|
+
protectedValue = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", windowsProtectScript()], {
|
|
10889
|
+
input: value,
|
|
10890
|
+
encoding: "utf8",
|
|
10891
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
10892
|
+
}).trim();
|
|
10893
|
+
} catch {
|
|
10894
|
+
throw new Error("Unable to protect the secret with Windows DPAPI for the current user.");
|
|
10895
|
+
}
|
|
10896
|
+
if (!protectedValue) {
|
|
10897
|
+
throw new Error("Windows DPAPI returned an empty protected value.");
|
|
10898
|
+
}
|
|
10899
|
+
return { source: "secure-store", protectedValue };
|
|
10900
|
+
}
|
|
10901
|
+
throw new Error(
|
|
10902
|
+
"Secure local secret storage is supported on macOS and Windows only. On Linux/CI, keep secrets in environment variables."
|
|
10903
|
+
);
|
|
10904
|
+
}
|
|
10905
|
+
function deleteSecureSecret(locator, deps = {}) {
|
|
10906
|
+
const platform = deps.platform ?? process.platform;
|
|
10907
|
+
const run = deps.execFileSync ?? import_node_child_process.execFileSync;
|
|
10908
|
+
if (platform === "darwin") {
|
|
10909
|
+
if (!locator.account) {
|
|
10910
|
+
return { status: "missing" };
|
|
10911
|
+
}
|
|
10912
|
+
try {
|
|
10913
|
+
run("security", ["delete-generic-password", "-s", KEYCHAIN_SERVICE, "-a", locator.account], {
|
|
10914
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
10915
|
+
});
|
|
10916
|
+
return { status: "deleted" };
|
|
10917
|
+
} catch (error) {
|
|
10918
|
+
return {
|
|
10919
|
+
status: "error",
|
|
10920
|
+
message: `Unable to delete macOS Keychain account ${locator.account}: ${errorText(error)}.`
|
|
10921
|
+
};
|
|
10922
|
+
}
|
|
10923
|
+
}
|
|
10924
|
+
if (platform === "win32") {
|
|
10925
|
+
return locator.protectedValue ? { status: "deleted" } : { status: "missing" };
|
|
10926
|
+
}
|
|
10927
|
+
return locator.account || locator.protectedValue ? { status: "deleted" } : { status: "missing" };
|
|
10928
|
+
}
|
|
10929
|
+
|
|
10930
|
+
// src/config.ts
|
|
10931
|
+
var DEFAULT_CONFIG_FILENAME = ".testops-cli.json";
|
|
10772
10932
|
function parseFlags(args) {
|
|
10773
10933
|
const flags = {};
|
|
10774
10934
|
const unknownFlags = [];
|
|
@@ -10802,6 +10962,15 @@ function parseFlags(args) {
|
|
|
10802
10962
|
function toStringValue(value) {
|
|
10803
10963
|
return typeof value === "string" ? value.trim() : "";
|
|
10804
10964
|
}
|
|
10965
|
+
function firstStringValue(...values) {
|
|
10966
|
+
for (const value of values) {
|
|
10967
|
+
const normalized = toStringValue(value);
|
|
10968
|
+
if (normalized) {
|
|
10969
|
+
return normalized;
|
|
10970
|
+
}
|
|
10971
|
+
}
|
|
10972
|
+
return "";
|
|
10973
|
+
}
|
|
10805
10974
|
function readConfigFile(configPath) {
|
|
10806
10975
|
if (!(0, import_node_fs.existsSync)(configPath)) {
|
|
10807
10976
|
return {};
|
|
@@ -10814,10 +10983,33 @@ function readConfigFile(configPath) {
|
|
|
10814
10983
|
return {};
|
|
10815
10984
|
}
|
|
10816
10985
|
}
|
|
10986
|
+
function readConfigFileForMutation(configPath) {
|
|
10987
|
+
if (!(0, import_node_fs.existsSync)(configPath)) {
|
|
10988
|
+
return { file: {} };
|
|
10989
|
+
}
|
|
10990
|
+
try {
|
|
10991
|
+
const raw = (0, import_node_fs.readFileSync)(configPath, "utf8");
|
|
10992
|
+
const parsed = JSON.parse(raw);
|
|
10993
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
10994
|
+
return {
|
|
10995
|
+
file: {},
|
|
10996
|
+
error: `Unable to update config at ${configPath}: existing config must contain a JSON object.`
|
|
10997
|
+
};
|
|
10998
|
+
}
|
|
10999
|
+
return { file: parsed };
|
|
11000
|
+
} catch {
|
|
11001
|
+
return {
|
|
11002
|
+
file: {},
|
|
11003
|
+
error: `Unable to update config at ${configPath}: existing config is not valid JSON. Fix or remove it before retrying.`
|
|
11004
|
+
};
|
|
11005
|
+
}
|
|
11006
|
+
}
|
|
10817
11007
|
function writeConfigFile(configPath, file) {
|
|
10818
11008
|
(0, import_node_fs.writeFileSync)(configPath, `${JSON.stringify(file, null, 2)}
|
|
10819
11009
|
`, "utf8");
|
|
10820
|
-
(
|
|
11010
|
+
if (process.platform !== "win32") {
|
|
11011
|
+
(0, import_node_fs.chmodSync)(configPath, 384);
|
|
11012
|
+
}
|
|
10821
11013
|
}
|
|
10822
11014
|
function normalizeBaseUrl(value) {
|
|
10823
11015
|
if (value.endsWith("/")) {
|
|
@@ -10831,28 +11023,46 @@ function readAuthMode(input) {
|
|
|
10831
11023
|
function defaultKeychainAccount(configPath) {
|
|
10832
11024
|
return import_node_path.default.resolve(configPath);
|
|
10833
11025
|
}
|
|
10834
|
-
function
|
|
10835
|
-
|
|
10836
|
-
|
|
11026
|
+
function defaultJiraApiTokenKeychainAccount(configPath) {
|
|
11027
|
+
return `${import_node_path.default.resolve(configPath)}:jiraApiToken`;
|
|
11028
|
+
}
|
|
11029
|
+
function valueFromPrecedence(key, flagValue, envValue, fileValue, fallback = "") {
|
|
11030
|
+
if (flagValue) {
|
|
11031
|
+
return { value: flagValue, source: "flag" };
|
|
10837
11032
|
}
|
|
10838
|
-
|
|
10839
|
-
return
|
|
10840
|
-
encoding: "utf8",
|
|
10841
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
10842
|
-
}).trim();
|
|
10843
|
-
} catch {
|
|
10844
|
-
return "";
|
|
11033
|
+
if (envValue) {
|
|
11034
|
+
return { value: envValue, source: "env" };
|
|
10845
11035
|
}
|
|
11036
|
+
if (fileValue) {
|
|
11037
|
+
return { value: fileValue, source: "file" };
|
|
11038
|
+
}
|
|
11039
|
+
if (fallback) {
|
|
11040
|
+
return { value: fallback, source: "default" };
|
|
11041
|
+
}
|
|
11042
|
+
return { value: "", source: "default" };
|
|
10846
11043
|
}
|
|
10847
|
-
function
|
|
10848
|
-
if (
|
|
10849
|
-
|
|
11044
|
+
function secureAccountForKey(key, file, configPath) {
|
|
11045
|
+
if (key === "forgeAuthToken") {
|
|
11046
|
+
return toStringValue(file.forgeAuthTokenKeychainAccount) || defaultKeychainAccount(configPath);
|
|
10850
11047
|
}
|
|
10851
|
-
(
|
|
10852
|
-
stdio: ["ignore", "ignore", "pipe"]
|
|
10853
|
-
});
|
|
11048
|
+
return toStringValue(file.jiraApiTokenKeychainAccount) || defaultJiraApiTokenKeychainAccount(configPath);
|
|
10854
11049
|
}
|
|
10855
|
-
function
|
|
11050
|
+
function secureLocatorForKey(key, file) {
|
|
11051
|
+
if (key === "forgeAuthToken") {
|
|
11052
|
+
return {
|
|
11053
|
+
account: toStringValue(file.forgeAuthTokenKeychainAccount) || void 0,
|
|
11054
|
+
protectedValue: toStringValue(file.forgeAuthTokenProtected) || void 0
|
|
11055
|
+
};
|
|
11056
|
+
}
|
|
11057
|
+
return {
|
|
11058
|
+
account: toStringValue(file.jiraApiTokenKeychainAccount) || void 0,
|
|
11059
|
+
protectedValue: toStringValue(file.jiraApiTokenProtected) || void 0
|
|
11060
|
+
};
|
|
11061
|
+
}
|
|
11062
|
+
function secretEnvironmentName(key) {
|
|
11063
|
+
return key === "jiraApiToken" ? "JIRA_API_TOKEN" : "TESTOPS_FORGE_AUTH_TOKEN";
|
|
11064
|
+
}
|
|
11065
|
+
function resolveSecretValue(key, flagValue, envValue, fileValue, file) {
|
|
10856
11066
|
if (flagValue) {
|
|
10857
11067
|
return { value: flagValue, source: "flag" };
|
|
10858
11068
|
}
|
|
@@ -10862,11 +11072,33 @@ function valueFromPrecedence(key, flagValue, envValue, fileValue, fallback = "")
|
|
|
10862
11072
|
if (fileValue) {
|
|
10863
11073
|
return { value: fileValue, source: "file" };
|
|
10864
11074
|
}
|
|
10865
|
-
|
|
10866
|
-
|
|
11075
|
+
const secure = readSecureSecret(secureLocatorForKey(key, file));
|
|
11076
|
+
if (secure.status === "available") {
|
|
11077
|
+
return { value: secure.value, source: secure.source };
|
|
11078
|
+
}
|
|
11079
|
+
if (secure.status === "error") {
|
|
11080
|
+
return { value: "", source: "default", error: `${key}: ${secure.message}` };
|
|
10867
11081
|
}
|
|
10868
11082
|
return { value: "", source: "default" };
|
|
10869
11083
|
}
|
|
11084
|
+
function clearSecretFields(file, key) {
|
|
11085
|
+
if (key === "forgeAuthToken") {
|
|
11086
|
+
return {
|
|
11087
|
+
...file,
|
|
11088
|
+
forgeAuthToken: void 0,
|
|
11089
|
+
TESTOPS_FORGE_AUTH_TOKEN: void 0,
|
|
11090
|
+
forgeAuthTokenKeychainAccount: void 0,
|
|
11091
|
+
forgeAuthTokenProtected: void 0
|
|
11092
|
+
};
|
|
11093
|
+
}
|
|
11094
|
+
return {
|
|
11095
|
+
...file,
|
|
11096
|
+
jiraApiToken: void 0,
|
|
11097
|
+
JIRA_API_TOKEN: void 0,
|
|
11098
|
+
jiraApiTokenKeychainAccount: void 0,
|
|
11099
|
+
jiraApiTokenProtected: void 0
|
|
11100
|
+
};
|
|
11101
|
+
}
|
|
10870
11102
|
function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
|
|
10871
11103
|
const { flags, unknownFlags } = parseFlags(args);
|
|
10872
11104
|
const configPathFlag = flags["--config"] ? import_node_path.default.resolve(cwd, flags["--config"]) : "";
|
|
@@ -10905,11 +11137,12 @@ function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
|
|
|
10905
11137
|
toStringValue(env.JIRA_EMAIL),
|
|
10906
11138
|
toStringValue(file.jiraEmail ?? file.JIRA_EMAIL)
|
|
10907
11139
|
);
|
|
10908
|
-
const jiraApiTokenResolved =
|
|
11140
|
+
const jiraApiTokenResolved = resolveSecretValue(
|
|
10909
11141
|
"jiraApiToken",
|
|
10910
11142
|
toStringValue(flags["--jira-api-token"]),
|
|
10911
11143
|
toStringValue(env.JIRA_API_TOKEN),
|
|
10912
|
-
|
|
11144
|
+
firstStringValue(file.jiraApiToken, file.JIRA_API_TOKEN),
|
|
11145
|
+
file
|
|
10913
11146
|
);
|
|
10914
11147
|
const forgeEndpointResolved = valueFromPrecedence(
|
|
10915
11148
|
"forgeEndpoint",
|
|
@@ -10923,10 +11156,13 @@ function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
|
|
|
10923
11156
|
"",
|
|
10924
11157
|
toStringValue(file.forgeAuthTokenKeychainAccount)
|
|
10925
11158
|
);
|
|
10926
|
-
const
|
|
10927
|
-
|
|
10928
|
-
|
|
10929
|
-
|
|
11159
|
+
const forgeAuthTokenResolved = resolveSecretValue(
|
|
11160
|
+
"forgeAuthToken",
|
|
11161
|
+
"",
|
|
11162
|
+
toStringValue(env.TESTOPS_FORGE_AUTH_TOKEN),
|
|
11163
|
+
firstStringValue(file.forgeAuthToken, file.TESTOPS_FORGE_AUTH_TOKEN),
|
|
11164
|
+
file
|
|
11165
|
+
);
|
|
10930
11166
|
const config = {
|
|
10931
11167
|
baseUrl: normalizeBaseUrl(baseUrlResolved.value),
|
|
10932
11168
|
projectKey: projectKeyResolved.value,
|
|
@@ -10952,13 +11188,16 @@ function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
|
|
|
10952
11188
|
configPath: configPathSource
|
|
10953
11189
|
};
|
|
10954
11190
|
const warnings = [];
|
|
11191
|
+
const errors = [jiraApiTokenResolved.error, forgeAuthTokenResolved.error].filter(
|
|
11192
|
+
(value) => Boolean(value)
|
|
11193
|
+
);
|
|
10955
11194
|
if (unknownFlags.length > 0) {
|
|
10956
11195
|
warnings.push(`Ignored unknown config flags: ${unknownFlags.join(", ")}`);
|
|
10957
11196
|
}
|
|
10958
11197
|
if (!(0, import_node_fs.existsSync)(configPath) && sources.configPath !== "default") {
|
|
10959
11198
|
warnings.push(`Config file not found at ${configPath}; using env/flags/defaults.`);
|
|
10960
11199
|
}
|
|
10961
|
-
return { values: config, sources, warnings };
|
|
11200
|
+
return { values: config, sources, warnings, errors };
|
|
10962
11201
|
}
|
|
10963
11202
|
function maskSecret(value) {
|
|
10964
11203
|
if (!value) {
|
|
@@ -10971,7 +11210,7 @@ function maskSecret(value) {
|
|
|
10971
11210
|
}
|
|
10972
11211
|
function validateResolvedConfig(resolution) {
|
|
10973
11212
|
const { values } = resolution;
|
|
10974
|
-
const errors = [];
|
|
11213
|
+
const errors = [...resolution.errors];
|
|
10975
11214
|
const warnings = [...resolution.warnings];
|
|
10976
11215
|
if (!values.baseUrl) {
|
|
10977
11216
|
errors.push("Missing required setting: JIRA_BASE_URL (flag/env/file).");
|
|
@@ -10986,10 +11225,8 @@ function validateResolvedConfig(resolution) {
|
|
|
10986
11225
|
if (!values.jiraApiToken) {
|
|
10987
11226
|
errors.push("Auth mode api-token requires JIRA_API_TOKEN.");
|
|
10988
11227
|
}
|
|
10989
|
-
} else {
|
|
10990
|
-
|
|
10991
|
-
warnings.push("JIRA_EMAIL/JIRA_API_TOKEN were provided but auth mode is 'none'.");
|
|
10992
|
-
}
|
|
11228
|
+
} else if (values.jiraApiToken || values.jiraEmail) {
|
|
11229
|
+
warnings.push("JIRA_EMAIL/JIRA_API_TOKEN were provided but auth mode is 'none'.");
|
|
10993
11230
|
}
|
|
10994
11231
|
return { ok: errors.length === 0, errors, warnings };
|
|
10995
11232
|
}
|
|
@@ -11052,21 +11289,33 @@ function applyConfigSet(configPath, rawKey, value) {
|
|
|
11052
11289
|
]
|
|
11053
11290
|
};
|
|
11054
11291
|
}
|
|
11055
|
-
const
|
|
11056
|
-
if (
|
|
11057
|
-
|
|
11058
|
-
|
|
11059
|
-
|
|
11060
|
-
|
|
11061
|
-
|
|
11062
|
-
|
|
11063
|
-
|
|
11064
|
-
|
|
11065
|
-
|
|
11066
|
-
|
|
11067
|
-
|
|
11068
|
-
|
|
11069
|
-
|
|
11292
|
+
const mutable = readConfigFileForMutation(configPath);
|
|
11293
|
+
if (mutable.error) {
|
|
11294
|
+
return { exitCode: ExitCode.ValidationError, stderr: [mutable.error] };
|
|
11295
|
+
}
|
|
11296
|
+
const file = mutable.file;
|
|
11297
|
+
if (key === "forgeAuthToken" || key === "jiraApiToken") {
|
|
11298
|
+
const secretKey = key;
|
|
11299
|
+
const account = secureAccountForKey(secretKey, file, configPath);
|
|
11300
|
+
try {
|
|
11301
|
+
const stored = writeSecureSecret(account, value);
|
|
11302
|
+
const cleared = clearSecretFields(file, secretKey);
|
|
11303
|
+
const nextFile2 = stored.source === "keychain" ? secretKey === "forgeAuthToken" ? { ...cleared, forgeAuthTokenKeychainAccount: stored.account } : { ...cleared, jiraApiTokenKeychainAccount: stored.account } : secretKey === "forgeAuthToken" ? { ...cleared, forgeAuthTokenProtected: stored.protectedValue } : { ...cleared, jiraApiTokenProtected: stored.protectedValue };
|
|
11304
|
+
writeConfigFile(configPath, nextFile2);
|
|
11305
|
+
const backend = stored.source === "keychain" ? "macOS Keychain" : "Windows DPAPI for the current user";
|
|
11306
|
+
return {
|
|
11307
|
+
exitCode: ExitCode.Success,
|
|
11308
|
+
stdout: [`Config updated: ${secretKey} stored securely with ${backend}.`]
|
|
11309
|
+
};
|
|
11310
|
+
} catch (error) {
|
|
11311
|
+
const unsupported = process.platform !== "darwin" && process.platform !== "win32";
|
|
11312
|
+
return {
|
|
11313
|
+
exitCode: unsupported ? ExitCode.UsageError : ExitCode.ValidationError,
|
|
11314
|
+
stderr: [
|
|
11315
|
+
`Unable to store ${secretKey} securely: ${error instanceof Error ? error.message : "unknown secure-storage error"} Use ${secretEnvironmentName(secretKey)} on Linux/CI.`
|
|
11316
|
+
]
|
|
11317
|
+
};
|
|
11318
|
+
}
|
|
11070
11319
|
}
|
|
11071
11320
|
const nextFile = {
|
|
11072
11321
|
...file,
|
|
@@ -11078,6 +11327,101 @@ function applyConfigSet(configPath, rawKey, value) {
|
|
|
11078
11327
|
stdout: [`Config updated: ${rawKey}.`]
|
|
11079
11328
|
};
|
|
11080
11329
|
}
|
|
11330
|
+
function configFileKeysFor(key) {
|
|
11331
|
+
switch (key) {
|
|
11332
|
+
case "baseUrl":
|
|
11333
|
+
return ["baseUrl", "JIRA_BASE_URL"];
|
|
11334
|
+
case "projectKey":
|
|
11335
|
+
return ["projectKey", "JIRA_PROJECT_KEY"];
|
|
11336
|
+
case "issueKey":
|
|
11337
|
+
return ["issueKey", "JIRA_ISSUE_KEY"];
|
|
11338
|
+
case "authMode":
|
|
11339
|
+
return ["authMode", "TESTOPS_AUTH_MODE"];
|
|
11340
|
+
case "jiraEmail":
|
|
11341
|
+
return ["jiraEmail", "JIRA_EMAIL"];
|
|
11342
|
+
case "forgeEndpoint":
|
|
11343
|
+
return ["forgeEndpoint", "TESTOPS_FORGE_ENDPOINT"];
|
|
11344
|
+
default:
|
|
11345
|
+
return [key];
|
|
11346
|
+
}
|
|
11347
|
+
}
|
|
11348
|
+
function applyConfigUnset(configPath, rawKey) {
|
|
11349
|
+
const key = normalizeConfigSetKey(rawKey);
|
|
11350
|
+
if (!key) {
|
|
11351
|
+
return {
|
|
11352
|
+
exitCode: ExitCode.UsageError,
|
|
11353
|
+
stderr: ["Usage: automatify testops config unset <key>"]
|
|
11354
|
+
};
|
|
11355
|
+
}
|
|
11356
|
+
const mutable = readConfigFileForMutation(configPath);
|
|
11357
|
+
if (mutable.error) {
|
|
11358
|
+
return { exitCode: ExitCode.ValidationError, stderr: [mutable.error] };
|
|
11359
|
+
}
|
|
11360
|
+
const file = mutable.file;
|
|
11361
|
+
if (key === "forgeAuthToken" || key === "jiraApiToken") {
|
|
11362
|
+
const secretKey = key;
|
|
11363
|
+
const deleted = deleteSecureSecret(secureLocatorForKey(secretKey, file));
|
|
11364
|
+
if (deleted.status === "error") {
|
|
11365
|
+
return { exitCode: ExitCode.ValidationError, stderr: [deleted.message] };
|
|
11366
|
+
}
|
|
11367
|
+
writeConfigFile(configPath, clearSecretFields(file, secretKey));
|
|
11368
|
+
return { exitCode: ExitCode.Success, stdout: [`Config removed: ${secretKey}.`] };
|
|
11369
|
+
}
|
|
11370
|
+
const next = { ...file };
|
|
11371
|
+
for (const fileKey of configFileKeysFor(key)) {
|
|
11372
|
+
delete next[fileKey];
|
|
11373
|
+
}
|
|
11374
|
+
writeConfigFile(configPath, next);
|
|
11375
|
+
return { exitCode: ExitCode.Success, stdout: [`Config removed: ${rawKey}.`] };
|
|
11376
|
+
}
|
|
11377
|
+
function applyConfigMigrateSecrets(configPath) {
|
|
11378
|
+
const mutable = readConfigFileForMutation(configPath);
|
|
11379
|
+
if (mutable.error) {
|
|
11380
|
+
return { exitCode: ExitCode.ValidationError, stderr: [mutable.error] };
|
|
11381
|
+
}
|
|
11382
|
+
const file = mutable.file;
|
|
11383
|
+
const pending = [];
|
|
11384
|
+
const jiraApiToken = firstStringValue(file.jiraApiToken, file.JIRA_API_TOKEN);
|
|
11385
|
+
const forgeAuthToken = firstStringValue(file.forgeAuthToken, file.TESTOPS_FORGE_AUTH_TOKEN);
|
|
11386
|
+
if (jiraApiToken) {
|
|
11387
|
+
pending.push({ key: "jiraApiToken", value: jiraApiToken });
|
|
11388
|
+
}
|
|
11389
|
+
if (forgeAuthToken) {
|
|
11390
|
+
pending.push({ key: "forgeAuthToken", value: forgeAuthToken });
|
|
11391
|
+
}
|
|
11392
|
+
if (pending.length === 0) {
|
|
11393
|
+
return { exitCode: ExitCode.Success, stdout: ["No plaintext local secrets require migration."] };
|
|
11394
|
+
}
|
|
11395
|
+
const migrated = [];
|
|
11396
|
+
for (const item of pending) {
|
|
11397
|
+
const response = applyConfigSet(configPath, item.key, item.value);
|
|
11398
|
+
if (response.exitCode !== ExitCode.Success) {
|
|
11399
|
+
return {
|
|
11400
|
+
exitCode: response.exitCode,
|
|
11401
|
+
stdout: migrated.length > 0 ? [`Migrated before failure: ${migrated.join(", ")}.`] : void 0,
|
|
11402
|
+
stderr: response.stderr
|
|
11403
|
+
};
|
|
11404
|
+
}
|
|
11405
|
+
migrated.push(item.key);
|
|
11406
|
+
}
|
|
11407
|
+
return {
|
|
11408
|
+
exitCode: ExitCode.Success,
|
|
11409
|
+
stdout: [`Migrated plaintext secrets to secure storage: ${migrated.join(", ")}.`]
|
|
11410
|
+
};
|
|
11411
|
+
}
|
|
11412
|
+
function stripConfigPathArgs(args) {
|
|
11413
|
+
const operands = [];
|
|
11414
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
11415
|
+
if (args[i] === "--config") {
|
|
11416
|
+
if (args[i + 1] && !args[i + 1].startsWith("--")) {
|
|
11417
|
+
i += 1;
|
|
11418
|
+
}
|
|
11419
|
+
continue;
|
|
11420
|
+
}
|
|
11421
|
+
operands.push(args[i]);
|
|
11422
|
+
}
|
|
11423
|
+
return operands;
|
|
11424
|
+
}
|
|
11081
11425
|
function createConfigHandler(env = process.env, cwd = process.cwd()) {
|
|
11082
11426
|
return (request) => {
|
|
11083
11427
|
const [subcommand, ...subArgs] = request.args;
|
|
@@ -11086,14 +11430,22 @@ function createConfigHandler(env = process.env, cwd = process.cwd()) {
|
|
|
11086
11430
|
if (subcommand === "show") {
|
|
11087
11431
|
return {
|
|
11088
11432
|
exitCode: ExitCode.Success,
|
|
11089
|
-
stdout: [
|
|
11433
|
+
stdout: [
|
|
11434
|
+
...toDisplayLines(resolution),
|
|
11435
|
+
...validation.warnings.map((line) => `WARN: ${line}`),
|
|
11436
|
+
...resolution.errors.map((line) => `ERROR: ${line}`)
|
|
11437
|
+
]
|
|
11090
11438
|
};
|
|
11091
11439
|
}
|
|
11092
11440
|
if (subcommand === "validate") {
|
|
11093
11441
|
if (validation.ok) {
|
|
11094
11442
|
return {
|
|
11095
11443
|
exitCode: ExitCode.Success,
|
|
11096
|
-
stdout: [
|
|
11444
|
+
stdout: [
|
|
11445
|
+
"Config validation: PASS",
|
|
11446
|
+
...toDisplayLines(resolution),
|
|
11447
|
+
...validation.warnings.map((line) => `WARN: ${line}`)
|
|
11448
|
+
]
|
|
11097
11449
|
};
|
|
11098
11450
|
}
|
|
11099
11451
|
return {
|
|
@@ -11103,17 +11455,25 @@ function createConfigHandler(env = process.env, cwd = process.cwd()) {
|
|
|
11103
11455
|
};
|
|
11104
11456
|
}
|
|
11105
11457
|
if (subcommand === "set") {
|
|
11106
|
-
const [key, ...valueParts] = subArgs;
|
|
11107
|
-
const
|
|
11458
|
+
const [key, ...valueParts] = stripConfigPathArgs(subArgs);
|
|
11459
|
+
const normalizedKey = normalizeConfigSetKey(key ?? "");
|
|
11460
|
+
const readValueFromStdin = (normalizedKey === "forgeAuthToken" || normalizedKey === "jiraApiToken") && valueParts.length === 1 && valueParts[0] === "--stdin";
|
|
11108
11461
|
const value = readValueFromStdin ? (0, import_node_fs.readFileSync)(0, "utf8").trim() : valueParts.join(" ").trim();
|
|
11109
11462
|
if (!key || !value) {
|
|
11110
11463
|
return {
|
|
11111
11464
|
exitCode: ExitCode.UsageError,
|
|
11112
|
-
stderr: ["Usage: automatify testops config set <key> <value>"]
|
|
11465
|
+
stderr: ["Usage: automatify testops config set <key> <value|--stdin>"]
|
|
11113
11466
|
};
|
|
11114
11467
|
}
|
|
11115
11468
|
return applyConfigSet(resolution.values.configPath, key, value);
|
|
11116
11469
|
}
|
|
11470
|
+
if (subcommand === "unset") {
|
|
11471
|
+
const [key] = stripConfigPathArgs(subArgs);
|
|
11472
|
+
return applyConfigUnset(resolution.values.configPath, key ?? "");
|
|
11473
|
+
}
|
|
11474
|
+
if (subcommand === "migrate-secrets") {
|
|
11475
|
+
return applyConfigMigrateSecrets(resolution.values.configPath);
|
|
11476
|
+
}
|
|
11117
11477
|
return {
|
|
11118
11478
|
exitCode: ExitCode.UsageError,
|
|
11119
11479
|
stderr: [`Unsupported config subcommand: ${subcommand}`]
|
|
@@ -11506,9 +11866,11 @@ function normalizePath(pathValue) {
|
|
|
11506
11866
|
return pathValue.replaceAll("\\", "/");
|
|
11507
11867
|
}
|
|
11508
11868
|
function discoverFeatureFiles(scannedFiles) {
|
|
11509
|
-
const featurePaths = [
|
|
11510
|
-
|
|
11511
|
-
|
|
11869
|
+
const featurePaths = [
|
|
11870
|
+
...new Set(
|
|
11871
|
+
scannedFiles.map((filePath) => normalizePath(filePath)).filter((filePath) => filePath.toLowerCase().endsWith(".feature"))
|
|
11872
|
+
)
|
|
11873
|
+
].sort((a, b) => a.localeCompare(b));
|
|
11512
11874
|
return {
|
|
11513
11875
|
total: featurePaths.length,
|
|
11514
11876
|
paths: featurePaths,
|
|
@@ -11747,12 +12109,7 @@ function scanProjectFiles(options) {
|
|
|
11747
12109
|
}
|
|
11748
12110
|
|
|
11749
12111
|
// src/auto.ts
|
|
11750
|
-
var DETERMINISTIC_STEPS = [
|
|
11751
|
-
"detect",
|
|
11752
|
-
"discover",
|
|
11753
|
-
"map-preview",
|
|
11754
|
-
"upload"
|
|
11755
|
-
];
|
|
12112
|
+
var DETERMINISTIC_STEPS = ["detect", "discover", "map-preview", "upload"];
|
|
11756
12113
|
function parseArgs(args) {
|
|
11757
12114
|
const flags = {};
|
|
11758
12115
|
const boolFlags = /* @__PURE__ */ new Set();
|
|
@@ -12400,10 +12757,7 @@ function createAutoHandler(deps = {}) {
|
|
|
12400
12757
|
const diagnostic = diagMissingProjectContext();
|
|
12401
12758
|
return {
|
|
12402
12759
|
exitCode: ExitCode.ValidationError,
|
|
12403
|
-
stderr: [
|
|
12404
|
-
`ERROR: ${diagnostic.code}: ${diagnostic.message}`,
|
|
12405
|
-
`SUGGESTION: ${diagnostic.suggestion}`
|
|
12406
|
-
]
|
|
12760
|
+
stderr: [`ERROR: ${diagnostic.code}: ${diagnostic.message}`, `SUGGESTION: ${diagnostic.suggestion}`]
|
|
12407
12761
|
};
|
|
12408
12762
|
}
|
|
12409
12763
|
execution = await executeUploadFlow({
|
|
@@ -13024,17 +13378,19 @@ function createAllureOpenHandler(deps = {}) {
|
|
|
13024
13378
|
if (useJson) {
|
|
13025
13379
|
return {
|
|
13026
13380
|
exitCode: ExitCode.Success,
|
|
13027
|
-
stdout: toJsonLine(
|
|
13028
|
-
|
|
13029
|
-
|
|
13030
|
-
|
|
13031
|
-
|
|
13032
|
-
|
|
13033
|
-
|
|
13034
|
-
|
|
13035
|
-
|
|
13036
|
-
|
|
13037
|
-
|
|
13381
|
+
stdout: toJsonLine(
|
|
13382
|
+
buildJsonOutput({
|
|
13383
|
+
zipPath,
|
|
13384
|
+
dryRun: true,
|
|
13385
|
+
served: false,
|
|
13386
|
+
opened: false,
|
|
13387
|
+
keep,
|
|
13388
|
+
extractDir: extractDirRaw,
|
|
13389
|
+
rootDir: extractDirRaw,
|
|
13390
|
+
indexPath,
|
|
13391
|
+
summary
|
|
13392
|
+
})
|
|
13393
|
+
)
|
|
13038
13394
|
};
|
|
13039
13395
|
}
|
|
13040
13396
|
return {
|
|
@@ -13084,20 +13440,24 @@ function createAllureOpenHandler(deps = {}) {
|
|
|
13084
13440
|
}
|
|
13085
13441
|
}
|
|
13086
13442
|
if (useJson) {
|
|
13087
|
-
logger.log(
|
|
13088
|
-
|
|
13089
|
-
|
|
13090
|
-
|
|
13091
|
-
|
|
13092
|
-
|
|
13093
|
-
|
|
13094
|
-
|
|
13095
|
-
|
|
13096
|
-
|
|
13097
|
-
|
|
13098
|
-
|
|
13099
|
-
|
|
13100
|
-
|
|
13443
|
+
logger.log(
|
|
13444
|
+
JSON.stringify(
|
|
13445
|
+
buildJsonOutput({
|
|
13446
|
+
zipPath,
|
|
13447
|
+
dryRun: false,
|
|
13448
|
+
served: true,
|
|
13449
|
+
opened,
|
|
13450
|
+
keep,
|
|
13451
|
+
extractDir: extractResult.extractDir,
|
|
13452
|
+
rootDir: extractResult.rootDir,
|
|
13453
|
+
indexPath: extractResult.indexPath,
|
|
13454
|
+
url: serverHandle.url,
|
|
13455
|
+
port: serverHandle.port,
|
|
13456
|
+
host: serverHandle.host,
|
|
13457
|
+
summary
|
|
13458
|
+
})
|
|
13459
|
+
)
|
|
13460
|
+
);
|
|
13101
13461
|
} else {
|
|
13102
13462
|
logger.log(`Allure report extracted to ${extractResult.extractDir}.`);
|
|
13103
13463
|
if (verbose) {
|
|
@@ -13385,13 +13745,15 @@ function createAllureHandler(deps = {}) {
|
|
|
13385
13745
|
if (useJson) {
|
|
13386
13746
|
return {
|
|
13387
13747
|
exitCode: ExitCode.Success,
|
|
13388
|
-
stdout: toJsonLine(
|
|
13389
|
-
|
|
13390
|
-
|
|
13391
|
-
|
|
13392
|
-
|
|
13393
|
-
|
|
13394
|
-
|
|
13748
|
+
stdout: toJsonLine(
|
|
13749
|
+
buildDownloadJsonOutput({
|
|
13750
|
+
issueKey: issueKey2,
|
|
13751
|
+
dryRun: true,
|
|
13752
|
+
downloaded: false,
|
|
13753
|
+
attachment: selected,
|
|
13754
|
+
outputPath
|
|
13755
|
+
})
|
|
13756
|
+
)
|
|
13395
13757
|
};
|
|
13396
13758
|
}
|
|
13397
13759
|
return {
|
|
@@ -13418,13 +13780,15 @@ function createAllureHandler(deps = {}) {
|
|
|
13418
13780
|
if (useJson) {
|
|
13419
13781
|
return {
|
|
13420
13782
|
exitCode: ExitCode.Success,
|
|
13421
|
-
stdout: toJsonLine(
|
|
13422
|
-
|
|
13423
|
-
|
|
13424
|
-
|
|
13425
|
-
|
|
13426
|
-
|
|
13427
|
-
|
|
13783
|
+
stdout: toJsonLine(
|
|
13784
|
+
buildDownloadJsonOutput({
|
|
13785
|
+
issueKey: issueKey2,
|
|
13786
|
+
dryRun: false,
|
|
13787
|
+
downloaded: true,
|
|
13788
|
+
attachment: downloaded,
|
|
13789
|
+
outputPath
|
|
13790
|
+
})
|
|
13791
|
+
)
|
|
13428
13792
|
};
|
|
13429
13793
|
}
|
|
13430
13794
|
return {
|
|
@@ -13472,14 +13836,16 @@ function createAllureHandler(deps = {}) {
|
|
|
13472
13836
|
if (useJson) {
|
|
13473
13837
|
return {
|
|
13474
13838
|
exitCode: ExitCode.Success,
|
|
13475
|
-
stdout: toJsonLine(
|
|
13476
|
-
|
|
13477
|
-
|
|
13478
|
-
|
|
13479
|
-
|
|
13480
|
-
|
|
13481
|
-
|
|
13482
|
-
|
|
13839
|
+
stdout: toJsonLine(
|
|
13840
|
+
buildJsonOutput2({
|
|
13841
|
+
issueKey,
|
|
13842
|
+
uploaded: false,
|
|
13843
|
+
commentAdded: false,
|
|
13844
|
+
dryRun: true,
|
|
13845
|
+
summary,
|
|
13846
|
+
attachment: { filename: attachmentFilename }
|
|
13847
|
+
})
|
|
13848
|
+
)
|
|
13483
13849
|
};
|
|
13484
13850
|
}
|
|
13485
13851
|
return {
|
|
@@ -13515,14 +13881,16 @@ function createAllureHandler(deps = {}) {
|
|
|
13515
13881
|
if (useJson) {
|
|
13516
13882
|
return {
|
|
13517
13883
|
exitCode: ExitCode.Success,
|
|
13518
|
-
stdout: toJsonLine(
|
|
13519
|
-
|
|
13520
|
-
|
|
13521
|
-
|
|
13522
|
-
|
|
13523
|
-
|
|
13524
|
-
|
|
13525
|
-
|
|
13884
|
+
stdout: toJsonLine(
|
|
13885
|
+
buildJsonOutput2({
|
|
13886
|
+
issueKey,
|
|
13887
|
+
uploaded: true,
|
|
13888
|
+
commentAdded,
|
|
13889
|
+
dryRun: false,
|
|
13890
|
+
summary,
|
|
13891
|
+
attachment
|
|
13892
|
+
})
|
|
13893
|
+
)
|
|
13526
13894
|
};
|
|
13527
13895
|
}
|
|
13528
13896
|
return {
|
|
@@ -13545,8 +13913,255 @@ function createAllureHandler(deps = {}) {
|
|
|
13545
13913
|
};
|
|
13546
13914
|
}
|
|
13547
13915
|
|
|
13916
|
+
// src/auth.ts
|
|
13917
|
+
var import_node_fs10 = require("node:fs");
|
|
13918
|
+
function parseAuthArgs(args, allowLoginFlags) {
|
|
13919
|
+
const configArgs = [];
|
|
13920
|
+
const unknown = [];
|
|
13921
|
+
let baseUrl = "";
|
|
13922
|
+
let jiraEmail = "";
|
|
13923
|
+
let tokenStdin = false;
|
|
13924
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
13925
|
+
const token = args[i];
|
|
13926
|
+
if (token === "--config") {
|
|
13927
|
+
const value = args[i + 1];
|
|
13928
|
+
if (!value || value.startsWith("--")) {
|
|
13929
|
+
unknown.push(token);
|
|
13930
|
+
continue;
|
|
13931
|
+
}
|
|
13932
|
+
configArgs.push(token, value);
|
|
13933
|
+
i += 1;
|
|
13934
|
+
continue;
|
|
13935
|
+
}
|
|
13936
|
+
if (allowLoginFlags && token === "--base-url") {
|
|
13937
|
+
const value = args[i + 1];
|
|
13938
|
+
if (!value || value.startsWith("--")) {
|
|
13939
|
+
unknown.push(token);
|
|
13940
|
+
continue;
|
|
13941
|
+
}
|
|
13942
|
+
baseUrl = value.trim();
|
|
13943
|
+
i += 1;
|
|
13944
|
+
continue;
|
|
13945
|
+
}
|
|
13946
|
+
if (allowLoginFlags && token === "--jira-email") {
|
|
13947
|
+
const value = args[i + 1];
|
|
13948
|
+
if (!value || value.startsWith("--")) {
|
|
13949
|
+
unknown.push(token);
|
|
13950
|
+
continue;
|
|
13951
|
+
}
|
|
13952
|
+
jiraEmail = value.trim();
|
|
13953
|
+
i += 1;
|
|
13954
|
+
continue;
|
|
13955
|
+
}
|
|
13956
|
+
if (allowLoginFlags && token === "--token-stdin") {
|
|
13957
|
+
tokenStdin = true;
|
|
13958
|
+
continue;
|
|
13959
|
+
}
|
|
13960
|
+
unknown.push(token);
|
|
13961
|
+
}
|
|
13962
|
+
return { configArgs, baseUrl, jiraEmail, tokenStdin, unknown };
|
|
13963
|
+
}
|
|
13964
|
+
function runConfigMutation(handler, args, context) {
|
|
13965
|
+
const response = handler(
|
|
13966
|
+
{
|
|
13967
|
+
group: "config",
|
|
13968
|
+
args,
|
|
13969
|
+
rawArgs: ["config", ...args]
|
|
13970
|
+
},
|
|
13971
|
+
context
|
|
13972
|
+
);
|
|
13973
|
+
if (response instanceof Promise) {
|
|
13974
|
+
throw new Error("Config mutations must remain synchronous.");
|
|
13975
|
+
}
|
|
13976
|
+
return response;
|
|
13977
|
+
}
|
|
13978
|
+
function sourceLabel(source) {
|
|
13979
|
+
switch (source) {
|
|
13980
|
+
case "keychain":
|
|
13981
|
+
return "macOS Keychain";
|
|
13982
|
+
case "secure-store":
|
|
13983
|
+
return "Windows DPAPI";
|
|
13984
|
+
case "env":
|
|
13985
|
+
return "environment";
|
|
13986
|
+
case "file":
|
|
13987
|
+
return "legacy plaintext config";
|
|
13988
|
+
case "flag":
|
|
13989
|
+
return "command flag";
|
|
13990
|
+
default:
|
|
13991
|
+
return "not configured";
|
|
13992
|
+
}
|
|
13993
|
+
}
|
|
13994
|
+
function authState(parts) {
|
|
13995
|
+
const populated = parts.filter(Boolean).length;
|
|
13996
|
+
if (populated === 0) {
|
|
13997
|
+
return "not configured";
|
|
13998
|
+
}
|
|
13999
|
+
return populated === parts.length ? "configured" : "incomplete";
|
|
14000
|
+
}
|
|
14001
|
+
function createStatusResponse(args, env, cwd) {
|
|
14002
|
+
const parsed = parseAuthArgs(args, false);
|
|
14003
|
+
if (parsed.unknown.length > 0) {
|
|
14004
|
+
return {
|
|
14005
|
+
exitCode: ExitCode.UsageError,
|
|
14006
|
+
stderr: ["Unsupported auth status arguments. Only --config <path> is supported."]
|
|
14007
|
+
};
|
|
14008
|
+
}
|
|
14009
|
+
const resolution = resolveCliConfig(parsed.configArgs, env, cwd);
|
|
14010
|
+
const { values, sources } = resolution;
|
|
14011
|
+
const jiraState = authState([values.baseUrl, values.jiraEmail, values.jiraApiToken]);
|
|
14012
|
+
const forgeState = authState([values.forgeEndpoint, values.forgeAuthToken]);
|
|
14013
|
+
const stdout = [
|
|
14014
|
+
"Auth status (secret-safe):",
|
|
14015
|
+
` Jira: ${jiraState}`,
|
|
14016
|
+
` baseUrl: ${values.baseUrl || "<unset>"}`,
|
|
14017
|
+
` email: ${values.jiraEmail || "<unset>"}`,
|
|
14018
|
+
` apiToken: ${values.jiraApiToken ? `configured (${sourceLabel(sources.jiraApiToken)})` : "<unset>"}`,
|
|
14019
|
+
` Forge transport: ${forgeState}`,
|
|
14020
|
+
` endpoint: ${values.forgeEndpoint || "<unset>"}`,
|
|
14021
|
+
` authToken: ${values.forgeAuthToken ? `configured (${sourceLabel(sources.forgeAuthToken)})` : "<unset>"}`,
|
|
14022
|
+
` configPath: ${values.configPath}`
|
|
14023
|
+
];
|
|
14024
|
+
if (resolution.errors.length > 0) {
|
|
14025
|
+
return {
|
|
14026
|
+
exitCode: ExitCode.ValidationError,
|
|
14027
|
+
stdout,
|
|
14028
|
+
stderr: resolution.errors.map((error) => `ERROR: ${error}`)
|
|
14029
|
+
};
|
|
14030
|
+
}
|
|
14031
|
+
return { exitCode: ExitCode.Success, stdout };
|
|
14032
|
+
}
|
|
14033
|
+
function createLoginResponse(args, env, cwd, readStdin, platform, context) {
|
|
14034
|
+
const parsed = parseAuthArgs(args, true);
|
|
14035
|
+
if (parsed.unknown.length > 0) {
|
|
14036
|
+
return {
|
|
14037
|
+
exitCode: ExitCode.UsageError,
|
|
14038
|
+
stderr: [
|
|
14039
|
+
"Unsupported auth login arguments. Use --base-url, --jira-email, --token-stdin, and optional --config only.",
|
|
14040
|
+
"Secrets must be provided through --token-stdin or environment variables, never as command-line values."
|
|
14041
|
+
]
|
|
14042
|
+
};
|
|
14043
|
+
}
|
|
14044
|
+
const current = resolveCliConfig(parsed.configArgs, env, cwd);
|
|
14045
|
+
const baseUrl = parsed.baseUrl || env.JIRA_BASE_URL?.trim() || current.values.baseUrl;
|
|
14046
|
+
const jiraEmail = parsed.jiraEmail || env.JIRA_EMAIL?.trim() || current.values.jiraEmail;
|
|
14047
|
+
const stdinToken = parsed.tokenStdin ? readStdin().trim() : "";
|
|
14048
|
+
const effectiveToken = stdinToken || env.JIRA_API_TOKEN?.trim() || current.values.jiraApiToken;
|
|
14049
|
+
const missing = [];
|
|
14050
|
+
if (!baseUrl) missing.push("--base-url or JIRA_BASE_URL");
|
|
14051
|
+
if (!jiraEmail) missing.push("--jira-email or JIRA_EMAIL");
|
|
14052
|
+
if (!effectiveToken) missing.push("--token-stdin, JIRA_API_TOKEN, or an existing stored token");
|
|
14053
|
+
if (missing.length > 0) {
|
|
14054
|
+
return {
|
|
14055
|
+
exitCode: ExitCode.UsageError,
|
|
14056
|
+
stderr: [
|
|
14057
|
+
`Jira login is incomplete. Provide: ${missing.join(", ")}.`,
|
|
14058
|
+
"Secrets are never accepted as normal command-line values."
|
|
14059
|
+
]
|
|
14060
|
+
};
|
|
14061
|
+
}
|
|
14062
|
+
if (parsed.tokenStdin && platform !== "darwin" && platform !== "win32") {
|
|
14063
|
+
return {
|
|
14064
|
+
exitCode: ExitCode.UsageError,
|
|
14065
|
+
stderr: [
|
|
14066
|
+
"Local secure secret storage is supported only on macOS and Windows.",
|
|
14067
|
+
"On Linux/CI, set JIRA_API_TOKEN in the environment and run auth login without --token-stdin."
|
|
14068
|
+
]
|
|
14069
|
+
};
|
|
14070
|
+
}
|
|
14071
|
+
const configHandler = createConfigHandler(env, cwd);
|
|
14072
|
+
const configSuffix = parsed.configArgs;
|
|
14073
|
+
const steps = [];
|
|
14074
|
+
if (stdinToken) {
|
|
14075
|
+
steps.push({ label: "Jira API token", args: ["set", "jiraApiToken", stdinToken, ...configSuffix] });
|
|
14076
|
+
}
|
|
14077
|
+
steps.push(
|
|
14078
|
+
{ label: "Jira base URL", args: ["set", "baseUrl", baseUrl, ...configSuffix] },
|
|
14079
|
+
{ label: "Jira email", args: ["set", "jiraEmail", jiraEmail, ...configSuffix] },
|
|
14080
|
+
{ label: "auth mode", args: ["set", "authMode", "api-token", ...configSuffix] }
|
|
14081
|
+
);
|
|
14082
|
+
const completed = [];
|
|
14083
|
+
for (const step of steps) {
|
|
14084
|
+
const response = runConfigMutation(configHandler, step.args, context);
|
|
14085
|
+
if (response.exitCode !== ExitCode.Success) {
|
|
14086
|
+
return {
|
|
14087
|
+
exitCode: response.exitCode,
|
|
14088
|
+
stdout: completed.length > 0 ? [`Configured before failure: ${completed.join(", ")}.`] : void 0,
|
|
14089
|
+
stderr: response.stderr
|
|
14090
|
+
};
|
|
14091
|
+
}
|
|
14092
|
+
completed.push(step.label);
|
|
14093
|
+
}
|
|
14094
|
+
const tokenSource = stdinToken ? platform === "darwin" ? "macOS Keychain" : "Windows DPAPI" : sourceLabel(
|
|
14095
|
+
current.sources.jiraApiToken === "default" && env.JIRA_API_TOKEN ? "env" : current.sources.jiraApiToken
|
|
14096
|
+
);
|
|
14097
|
+
return {
|
|
14098
|
+
exitCode: ExitCode.Success,
|
|
14099
|
+
stdout: [
|
|
14100
|
+
"Jira login configured.",
|
|
14101
|
+
` baseUrl: ${baseUrl.replace(/\/$/, "")}`,
|
|
14102
|
+
` email: ${jiraEmail}`,
|
|
14103
|
+
` apiToken: configured (${tokenSource})`,
|
|
14104
|
+
"Run `automatify testops auth status` to verify the resolved credentials."
|
|
14105
|
+
]
|
|
14106
|
+
};
|
|
14107
|
+
}
|
|
14108
|
+
function createLogoutResponse(args, env, cwd, context) {
|
|
14109
|
+
const parsed = parseAuthArgs(args, false);
|
|
14110
|
+
if (parsed.unknown.length > 0) {
|
|
14111
|
+
return {
|
|
14112
|
+
exitCode: ExitCode.UsageError,
|
|
14113
|
+
stderr: ["Unsupported auth logout arguments. Only --config <path> is supported."]
|
|
14114
|
+
};
|
|
14115
|
+
}
|
|
14116
|
+
const configHandler = createConfigHandler(env, cwd);
|
|
14117
|
+
const failures = [];
|
|
14118
|
+
const removed = [];
|
|
14119
|
+
for (const key of ["jiraApiToken", "forgeAuthToken"]) {
|
|
14120
|
+
const response = runConfigMutation(configHandler, ["unset", key, ...parsed.configArgs], context);
|
|
14121
|
+
if (response.exitCode === ExitCode.Success) {
|
|
14122
|
+
removed.push(key);
|
|
14123
|
+
} else {
|
|
14124
|
+
failures.push(...response.stderr ?? [`Unable to remove ${key}.`]);
|
|
14125
|
+
}
|
|
14126
|
+
}
|
|
14127
|
+
if (failures.length > 0) {
|
|
14128
|
+
return {
|
|
14129
|
+
exitCode: ExitCode.ValidationError,
|
|
14130
|
+
stdout: removed.length > 0 ? [`Removed before failure: ${removed.join(", ")}.`] : void 0,
|
|
14131
|
+
stderr: failures
|
|
14132
|
+
};
|
|
14133
|
+
}
|
|
14134
|
+
const stdout = ["Local Jira and Forge credentials removed.", "Non-secret URL/email settings were preserved."];
|
|
14135
|
+
if (env.JIRA_API_TOKEN || env.TESTOPS_FORGE_AUTH_TOKEN) {
|
|
14136
|
+
stdout.push("Environment-provided credentials are unchanged and may still resolve as configured.");
|
|
14137
|
+
}
|
|
14138
|
+
return { exitCode: ExitCode.Success, stdout };
|
|
14139
|
+
}
|
|
14140
|
+
function createAuthHandler(deps = {}) {
|
|
14141
|
+
const env = deps.env ?? process.env;
|
|
14142
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
14143
|
+
const readStdin = deps.readStdin ?? (() => (0, import_node_fs10.readFileSync)(0, "utf8"));
|
|
14144
|
+
const platform = deps.platform ?? process.platform;
|
|
14145
|
+
return (request, context) => {
|
|
14146
|
+
const [subcommand, ...args] = request.args;
|
|
14147
|
+
if (subcommand === "status") {
|
|
14148
|
+
return createStatusResponse(args, env, cwd);
|
|
14149
|
+
}
|
|
14150
|
+
if (subcommand === "login") {
|
|
14151
|
+
return createLoginResponse(args, env, cwd, readStdin, platform, context);
|
|
14152
|
+
}
|
|
14153
|
+
if (subcommand === "logout") {
|
|
14154
|
+
return createLogoutResponse(args, env, cwd, context);
|
|
14155
|
+
}
|
|
14156
|
+
return {
|
|
14157
|
+
exitCode: ExitCode.UsageError,
|
|
14158
|
+
stderr: ["Unsupported auth subcommand. Use login, status, or logout."]
|
|
14159
|
+
};
|
|
14160
|
+
};
|
|
14161
|
+
}
|
|
14162
|
+
|
|
13548
14163
|
// src/bdd.ts
|
|
13549
|
-
var
|
|
14164
|
+
var import_node_fs11 = require("node:fs");
|
|
13550
14165
|
var import_node_path11 = __toESM(require("node:path"), 1);
|
|
13551
14166
|
var import_yazl = __toESM(require_yazl(), 1);
|
|
13552
14167
|
|
|
@@ -13860,9 +14475,11 @@ function parseScenarioFeatureFile(raw, filePath) {
|
|
|
13860
14475
|
if (!featureName || !scenarioName || !scenarioKey || steps.length === 0 || scenarioCount !== 1) {
|
|
13861
14476
|
return null;
|
|
13862
14477
|
}
|
|
13863
|
-
const linkedIssueKeys = [
|
|
13864
|
-
|
|
13865
|
-
|
|
14478
|
+
const linkedIssueKeys = [
|
|
14479
|
+
...new Set(
|
|
14480
|
+
tags.map((tag) => tag.replace(/^@/, "").trim().toUpperCase()).filter((tag) => ISSUE_KEY_PATTERN.test(tag))
|
|
14481
|
+
)
|
|
14482
|
+
];
|
|
13866
14483
|
return {
|
|
13867
14484
|
scenarioKey,
|
|
13868
14485
|
featureName,
|
|
@@ -13969,7 +14586,7 @@ function toJsonExportManifestItems(items) {
|
|
|
13969
14586
|
async function defaultCreateZipArchive(archivePath, entries) {
|
|
13970
14587
|
await new Promise((resolve, reject) => {
|
|
13971
14588
|
const zip = new import_yazl.ZipFile();
|
|
13972
|
-
const output = zip.outputStream.pipe((0,
|
|
14589
|
+
const output = zip.outputStream.pipe((0, import_node_fs11.createWriteStream)(archivePath));
|
|
13973
14590
|
output.on("close", () => resolve());
|
|
13974
14591
|
output.on("error", reject);
|
|
13975
14592
|
zip.outputStream.on("error", reject);
|
|
@@ -13999,10 +14616,10 @@ function missingProjectResponse() {
|
|
|
13999
14616
|
function createBddHandler(deps = {}) {
|
|
14000
14617
|
const cwd = deps.cwd ?? process.cwd();
|
|
14001
14618
|
const env = deps.env ?? process.env;
|
|
14002
|
-
const mkdir = deps.mkdir ?? ((targetPath, options) => (0,
|
|
14003
|
-
const readFile = deps.readFile ?? ((filePath) => (0,
|
|
14004
|
-
const readDir = deps.readDir ?? ((dirPath) => (0,
|
|
14005
|
-
const writeFile = deps.writeFile ?? ((filePath, content) => (0,
|
|
14619
|
+
const mkdir = deps.mkdir ?? ((targetPath, options) => (0, import_node_fs11.mkdirSync)(targetPath, options));
|
|
14620
|
+
const readFile = deps.readFile ?? ((filePath) => (0, import_node_fs11.readFileSync)(filePath, "utf8"));
|
|
14621
|
+
const readDir = deps.readDir ?? ((dirPath) => (0, import_node_fs11.readdirSync)(dirPath));
|
|
14622
|
+
const writeFile = deps.writeFile ?? ((filePath, content) => (0, import_node_fs11.writeFileSync)(filePath, content, "utf8"));
|
|
14006
14623
|
const createZipArchive = deps.createZipArchive ?? defaultCreateZipArchive;
|
|
14007
14624
|
return async (request, context) => {
|
|
14008
14625
|
const [subcommand, ...restArgs] = request.args;
|
|
@@ -14060,7 +14677,9 @@ function createBddHandler(deps = {}) {
|
|
|
14060
14677
|
stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
|
|
14061
14678
|
};
|
|
14062
14679
|
}
|
|
14063
|
-
const features = (featureId ? result.data.filter((feature) => feature.id === featureId) : result.data).sort(
|
|
14680
|
+
const features = (featureId ? result.data.filter((feature) => feature.id === featureId) : result.data).sort(
|
|
14681
|
+
(left, right) => left.payload.name.localeCompare(right.payload.name) || left.createdAt.localeCompare(right.createdAt)
|
|
14682
|
+
);
|
|
14064
14683
|
if (featureId && features.length === 0) {
|
|
14065
14684
|
return {
|
|
14066
14685
|
exitCode: ExitCode.RemoteError,
|
|
@@ -14180,7 +14799,9 @@ function createBddHandler(deps = {}) {
|
|
|
14180
14799
|
stderr: [`ERROR: ${scenariosResult.error.code}: ${scenariosResult.error.message}`]
|
|
14181
14800
|
};
|
|
14182
14801
|
}
|
|
14183
|
-
const selectedScenarios = exportAll ? [...scenariosResult.data].sort(
|
|
14802
|
+
const selectedScenarios = exportAll ? [...scenariosResult.data].sort(
|
|
14803
|
+
(left, right) => (left.key ?? left.id).localeCompare(right.key ?? right.id)
|
|
14804
|
+
) : scenariosResult.data.filter(
|
|
14184
14805
|
(item) => item.id === scenarioSelector2 || (item.key ?? "").toUpperCase() === scenarioSelector2.toUpperCase()
|
|
14185
14806
|
);
|
|
14186
14807
|
if (!exportAll && selectedScenarios.length === 0) {
|
|
@@ -14326,7 +14947,9 @@ function createBddHandler(deps = {}) {
|
|
|
14326
14947
|
if (!scenario) {
|
|
14327
14948
|
return {
|
|
14328
14949
|
exitCode: ExitCode.RemoteError,
|
|
14329
|
-
stderr: [
|
|
14950
|
+
stderr: [
|
|
14951
|
+
`ERROR: NOT_FOUND: BDD scenario ${parsedScenario.scenarioKey} was not found in this project.`
|
|
14952
|
+
]
|
|
14330
14953
|
};
|
|
14331
14954
|
}
|
|
14332
14955
|
const nextTags = parsedScenario.tags.filter((tag) => !SCENARIO_METADATA_PATTERN.test(tag));
|
|
@@ -14432,7 +15055,9 @@ function createBddHandler(deps = {}) {
|
|
|
14432
15055
|
if (!scenarioId && !testCaseKey) {
|
|
14433
15056
|
return {
|
|
14434
15057
|
exitCode: ExitCode.ValidationError,
|
|
14435
|
-
stderr: [
|
|
15058
|
+
stderr: [
|
|
15059
|
+
"ERROR: Provide either --id <SCENARIO_ID> or --test-case-key <TC-197> for testops bdd scenarios run."
|
|
15060
|
+
]
|
|
14436
15061
|
};
|
|
14437
15062
|
}
|
|
14438
15063
|
try {
|
|
@@ -14706,10 +15331,7 @@ function listToLines(items) {
|
|
|
14706
15331
|
if (items.length === 0) {
|
|
14707
15332
|
return ["Test cases: 0", "No test cases found for the selected context."];
|
|
14708
15333
|
}
|
|
14709
|
-
return [
|
|
14710
|
-
`Test cases: ${items.length}`,
|
|
14711
|
-
...items.map((item) => `- ${item.key} [${item.status}] ${item.title}`)
|
|
14712
|
-
];
|
|
15334
|
+
return [`Test cases: ${items.length}`, ...items.map((item) => `- ${item.key} [${item.status}] ${item.title}`)];
|
|
14713
15335
|
}
|
|
14714
15336
|
function showToLines(testCase) {
|
|
14715
15337
|
return [
|
|
@@ -15009,11 +15631,7 @@ var CONFIG_FLAGS5 = /* @__PURE__ */ new Set([
|
|
|
15009
15631
|
"--jira-email",
|
|
15010
15632
|
"--jira-api-token"
|
|
15011
15633
|
]);
|
|
15012
|
-
var DIRECT_JIRA_FLAGS = /* @__PURE__ */ new Set([
|
|
15013
|
-
"--site",
|
|
15014
|
-
"--email",
|
|
15015
|
-
"--api-token"
|
|
15016
|
-
]);
|
|
15634
|
+
var DIRECT_JIRA_FLAGS = /* @__PURE__ */ new Set(["--site", "--email", "--api-token"]);
|
|
15017
15635
|
function parseArgs6(args) {
|
|
15018
15636
|
const flags = {};
|
|
15019
15637
|
const boolFlags = /* @__PURE__ */ new Set();
|
|
@@ -15094,9 +15712,10 @@ function resolveAllureJiraConfig(parsed, env, cwd) {
|
|
|
15094
15712
|
const resolution = resolveCliConfig(pickConfigArgs5(parsed), env, cwd);
|
|
15095
15713
|
const site = normalizeSite3(parsed.flags["--site"] ?? env.JIRA_SITE ?? resolution.values.baseUrl);
|
|
15096
15714
|
const email = (parsed.flags["--email"] ?? resolution.values.jiraEmail).trim();
|
|
15097
|
-
const
|
|
15715
|
+
const directApiToken = (parsed.flags["--api-token"] ?? "").trim();
|
|
15716
|
+
const apiToken = (directApiToken || resolution.values.jiraApiToken).trim();
|
|
15098
15717
|
const issueKey = (parsed.flags["--issue-key"] ?? "").trim();
|
|
15099
|
-
const errors = [];
|
|
15718
|
+
const errors = directApiToken ? [] : resolution.errors.filter((line) => line.startsWith("jiraApiToken:"));
|
|
15100
15719
|
if (!hasValue(site)) {
|
|
15101
15720
|
errors.push("Missing Jira site. Pass --site, set JIRA_SITE/JIRA_BASE_URL, or configure baseUrl.");
|
|
15102
15721
|
}
|
|
@@ -15213,27 +15832,33 @@ function createDoctorHandler(deps = {}) {
|
|
|
15213
15832
|
message: "Issue access check skipped. Pass --issue-key to verify Jira read/download access for a specific issue."
|
|
15214
15833
|
});
|
|
15215
15834
|
}
|
|
15216
|
-
checks.push(
|
|
15217
|
-
|
|
15218
|
-
|
|
15219
|
-
|
|
15220
|
-
|
|
15221
|
-
|
|
15222
|
-
|
|
15223
|
-
|
|
15224
|
-
|
|
15225
|
-
|
|
15226
|
-
|
|
15227
|
-
|
|
15228
|
-
|
|
15229
|
-
|
|
15230
|
-
|
|
15231
|
-
|
|
15232
|
-
|
|
15233
|
-
|
|
15234
|
-
|
|
15235
|
-
|
|
15236
|
-
|
|
15835
|
+
checks.push(
|
|
15836
|
+
commandCheck(
|
|
15837
|
+
{ execFileSync: runCommand },
|
|
15838
|
+
"unzip",
|
|
15839
|
+
["-v"],
|
|
15840
|
+
"unzip is available for manual ZIP inspection.",
|
|
15841
|
+
"unzip was not found. The future built-in opener can avoid this, but manual inspection will be less convenient."
|
|
15842
|
+
)
|
|
15843
|
+
);
|
|
15844
|
+
checks.push(
|
|
15845
|
+
commandCheck(
|
|
15846
|
+
{ execFileSync: runCommand },
|
|
15847
|
+
"java",
|
|
15848
|
+
["-version"],
|
|
15849
|
+
"Java is available for optional raw allure-results workflows.",
|
|
15850
|
+
"Java was not found. This does not block generated Allure HTML ZIP upload/download/open; it only matters for raw allure-results generation."
|
|
15851
|
+
)
|
|
15852
|
+
);
|
|
15853
|
+
checks.push(
|
|
15854
|
+
commandCheck(
|
|
15855
|
+
{ execFileSync: runCommand },
|
|
15856
|
+
"allure",
|
|
15857
|
+
["--version"],
|
|
15858
|
+
"Allure CLI is available for optional raw allure-results workflows.",
|
|
15859
|
+
"Allure CLI was not found. This does not block generated Allure HTML ZIP upload/download/open."
|
|
15860
|
+
)
|
|
15861
|
+
);
|
|
15237
15862
|
const summary2 = {
|
|
15238
15863
|
status: computeOverallStatus(checks),
|
|
15239
15864
|
checks
|
|
@@ -15272,9 +15897,7 @@ function createDoctorHandler(deps = {}) {
|
|
|
15272
15897
|
message: "Forge endpoint is configured."
|
|
15273
15898
|
});
|
|
15274
15899
|
}
|
|
15275
|
-
const hasContext = Boolean(
|
|
15276
|
-
resolution.values.projectKey || resolution.values.issueKey
|
|
15277
|
-
);
|
|
15900
|
+
const hasContext = Boolean(resolution.values.projectKey || resolution.values.issueKey);
|
|
15278
15901
|
if (enforceContextCheck || hasContext) {
|
|
15279
15902
|
if (!hasContext) {
|
|
15280
15903
|
checks.push({
|
|
@@ -15331,7 +15954,7 @@ function createDoctorHandler(deps = {}) {
|
|
|
15331
15954
|
}
|
|
15332
15955
|
|
|
15333
15956
|
// src/ingestFeature.ts
|
|
15334
|
-
var
|
|
15957
|
+
var import_node_fs12 = require("node:fs");
|
|
15335
15958
|
var import_node_path12 = __toESM(require("node:path"), 1);
|
|
15336
15959
|
var CONFIG_FLAGS6 = /* @__PURE__ */ new Set([
|
|
15337
15960
|
"--config",
|
|
@@ -15418,8 +16041,8 @@ function normalizeError5(error) {
|
|
|
15418
16041
|
return "Unknown ingest error.";
|
|
15419
16042
|
}
|
|
15420
16043
|
function createIngestFeatureHandler(deps = {}) {
|
|
15421
|
-
const readFile = deps.readFile ?? ((filePath) => (0,
|
|
15422
|
-
const readStdin = deps.readStdin ?? (() => (0,
|
|
16044
|
+
const readFile = deps.readFile ?? ((filePath) => (0, import_node_fs12.readFileSync)(filePath, "utf8"));
|
|
16045
|
+
const readStdin = deps.readStdin ?? (() => (0, import_node_fs12.readFileSync)(0, "utf8"));
|
|
15423
16046
|
const cwd = deps.cwd ?? process.cwd();
|
|
15424
16047
|
const env = deps.env ?? process.env;
|
|
15425
16048
|
return async (request, context) => {
|
|
@@ -15501,11 +16124,7 @@ function createIngestFeatureHandler(deps = {}) {
|
|
|
15501
16124
|
}
|
|
15502
16125
|
return {
|
|
15503
16126
|
exitCode: ExitCode.RemoteError,
|
|
15504
|
-
stdout: [
|
|
15505
|
-
"Feature ingestion: FAILED",
|
|
15506
|
-
`Feature: ${name}`,
|
|
15507
|
-
`Source: ${useStdin ? "stdin" : sourceFile}`
|
|
15508
|
-
],
|
|
16127
|
+
stdout: ["Feature ingestion: FAILED", `Feature: ${name}`, `Source: ${useStdin ? "stdin" : sourceFile}`],
|
|
15509
16128
|
stderr: [`ERROR: ${result.error.code}: ${result.error.message}`]
|
|
15510
16129
|
};
|
|
15511
16130
|
}
|
|
@@ -15546,11 +16165,7 @@ function createIngestFeatureHandler(deps = {}) {
|
|
|
15546
16165
|
}
|
|
15547
16166
|
return {
|
|
15548
16167
|
exitCode: ExitCode.TransportError,
|
|
15549
|
-
stdout: [
|
|
15550
|
-
"Feature ingestion: FAILED",
|
|
15551
|
-
`Feature: ${name}`,
|
|
15552
|
-
`Source: ${useStdin ? "stdin" : sourceFile}`
|
|
15553
|
-
],
|
|
16168
|
+
stdout: ["Feature ingestion: FAILED", `Feature: ${name}`, `Source: ${useStdin ? "stdin" : sourceFile}`],
|
|
15554
16169
|
stderr: [`ERROR: ${normalizeError5(error)}`]
|
|
15555
16170
|
};
|
|
15556
16171
|
}
|
|
@@ -15558,7 +16173,7 @@ function createIngestFeatureHandler(deps = {}) {
|
|
|
15558
16173
|
}
|
|
15559
16174
|
|
|
15560
16175
|
// src/runUpload.ts
|
|
15561
|
-
var
|
|
16176
|
+
var import_node_fs13 = require("node:fs");
|
|
15562
16177
|
var import_node_path13 = __toESM(require("node:path"), 1);
|
|
15563
16178
|
var STEP_RESULTS = [
|
|
15564
16179
|
StepResult.Passed,
|
|
@@ -15700,8 +16315,8 @@ function summarizeRunResult(result) {
|
|
|
15700
16315
|
return lines;
|
|
15701
16316
|
}
|
|
15702
16317
|
function createRunUploadHandler(deps = {}) {
|
|
15703
|
-
const readFile = deps.readFile ?? ((filePath) => (0,
|
|
15704
|
-
const readStdin = deps.readStdin ?? (() => (0,
|
|
16318
|
+
const readFile = deps.readFile ?? ((filePath) => (0, import_node_fs13.readFileSync)(filePath, "utf8"));
|
|
16319
|
+
const readStdin = deps.readStdin ?? (() => (0, import_node_fs13.readFileSync)(0, "utf8"));
|
|
15705
16320
|
const cwd = deps.cwd ?? process.cwd();
|
|
15706
16321
|
const env = deps.env ?? process.env;
|
|
15707
16322
|
return async (request, context) => {
|
|
@@ -16046,7 +16661,7 @@ function createRunsHandler(deps = {}) {
|
|
|
16046
16661
|
}
|
|
16047
16662
|
|
|
16048
16663
|
// src/setup.ts
|
|
16049
|
-
var
|
|
16664
|
+
var import_node_fs15 = require("node:fs");
|
|
16050
16665
|
var import_node_path15 = __toESM(require("node:path"), 1);
|
|
16051
16666
|
|
|
16052
16667
|
// src/githubSetupProvider.ts
|
|
@@ -16054,7 +16669,7 @@ var import_node_child_process4 = require("node:child_process");
|
|
|
16054
16669
|
|
|
16055
16670
|
// src/setupPlan.ts
|
|
16056
16671
|
var import_node_crypto = require("node:crypto");
|
|
16057
|
-
var
|
|
16672
|
+
var import_node_fs14 = require("node:fs");
|
|
16058
16673
|
var import_node_path14 = __toESM(require("node:path"), 1);
|
|
16059
16674
|
var SETUP_PLAN_SCHEMA_VERSION = "automatify.testops.setup/v1";
|
|
16060
16675
|
var SETUP_PLAN_KIND = "AutomatifyTestOpsSetupPlan";
|
|
@@ -16145,18 +16760,12 @@ function buildGitHubSetupPlan(input) {
|
|
|
16145
16760
|
const workflowId = requireNonEmpty(input.workflowId, "workflow id");
|
|
16146
16761
|
const workflowPath = normalizeWorkflowPath(input.workflowPath);
|
|
16147
16762
|
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
|
-
);
|
|
16763
|
+
const endpointSecretName = normalizeSecretName(input.callbackEndpointSecretName, "callback endpoint secret name");
|
|
16764
|
+
const tokenSecretName = normalizeSecretName(input.callbackAuthTokenSecretName, "callback auth token secret name");
|
|
16156
16765
|
if (endpointSecretName === tokenSecretName) {
|
|
16157
16766
|
throw new Error("callback endpoint and auth token secret names must be different.");
|
|
16158
16767
|
}
|
|
16159
|
-
const workflowContent = (0,
|
|
16768
|
+
const workflowContent = (0, import_node_fs14.readFileSync)(input.workflowSourcePath, "utf8");
|
|
16160
16769
|
if (!workflowContent.trim()) {
|
|
16161
16770
|
throw new Error("workflow source file is empty.");
|
|
16162
16771
|
}
|
|
@@ -16211,13 +16820,7 @@ function buildGitHubSetupPlan(input) {
|
|
|
16211
16820
|
"Secrets: read/write metadata only for the explicit github-secrets apply scope.",
|
|
16212
16821
|
"Contents: read for doctor; this command never creates commits or pull requests."
|
|
16213
16822
|
],
|
|
16214
|
-
remoteChangesExcluded: [
|
|
16215
|
-
"accounts",
|
|
16216
|
-
"credentials",
|
|
16217
|
-
"commits",
|
|
16218
|
-
"pull requests",
|
|
16219
|
-
"workflow runs"
|
|
16220
|
-
]
|
|
16823
|
+
remoteChangesExcluded: ["accounts", "credentials", "commits", "pull requests", "workflow runs"]
|
|
16221
16824
|
}
|
|
16222
16825
|
},
|
|
16223
16826
|
jiraProfile: {
|
|
@@ -16435,15 +17038,7 @@ function validateSetupPlan(value) {
|
|
|
16435
17038
|
errors.push("project.key is required.");
|
|
16436
17039
|
}
|
|
16437
17040
|
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
|
-
])) {
|
|
17041
|
+
if (!isRecord(github) || !hasExactKeys(github, ["owner", "repository", "ref", "workflowId", "workflowFile", "requiredSecrets", "guidance"])) {
|
|
16447
17042
|
errors.push("github has an invalid shape.");
|
|
16448
17043
|
} else {
|
|
16449
17044
|
for (const field of ["owner", "repository", "ref", "workflowId"]) {
|
|
@@ -16520,6 +17115,8 @@ function hashSetupContent(value) {
|
|
|
16520
17115
|
}
|
|
16521
17116
|
|
|
16522
17117
|
// src/githubSetupProvider.ts
|
|
17118
|
+
var GITHUB_SECRET_PAGE_SIZE = 100;
|
|
17119
|
+
var MAX_GITHUB_SECRET_PAGES = 100;
|
|
16523
17120
|
function isRecord2(value) {
|
|
16524
17121
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
16525
17122
|
}
|
|
@@ -16551,18 +17148,14 @@ async function githubApi(fetchImpl, url, token) {
|
|
|
16551
17148
|
}
|
|
16552
17149
|
async function setGitHubRepositorySecretWithCli(input) {
|
|
16553
17150
|
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
|
-
}
|
|
17151
|
+
const child = (0, import_node_child_process4.spawn)("gh", ["secret", "set", input.name, "--repo", `${input.owner}/${input.repository}`], {
|
|
17152
|
+
shell: false,
|
|
17153
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
17154
|
+
env: {
|
|
17155
|
+
...process.env,
|
|
17156
|
+
GH_TOKEN: input.adminToken
|
|
16564
17157
|
}
|
|
16565
|
-
);
|
|
17158
|
+
});
|
|
16566
17159
|
let stderr = "";
|
|
16567
17160
|
child.stderr.setEncoding("utf8");
|
|
16568
17161
|
child.stderr.on("data", (chunk) => {
|
|
@@ -16622,6 +17215,7 @@ var GitHubSetupProvider = class {
|
|
|
16622
17215
|
return {
|
|
16623
17216
|
found: true,
|
|
16624
17217
|
active: state === "active",
|
|
17218
|
+
state,
|
|
16625
17219
|
path: workflowPath,
|
|
16626
17220
|
contentSha256,
|
|
16627
17221
|
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 +17230,52 @@ var GitHubSetupProvider = class {
|
|
|
16636
17230
|
};
|
|
16637
17231
|
}
|
|
16638
17232
|
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
|
-
}
|
|
17233
|
+
const baseUrl = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/actions/secrets`;
|
|
16646
17234
|
const names = /* @__PURE__ */ new Set();
|
|
16647
|
-
|
|
16648
|
-
|
|
17235
|
+
let totalCount;
|
|
17236
|
+
let pagesRead = 0;
|
|
17237
|
+
for (let page = 1; page <= MAX_GITHUB_SECRET_PAGES; page += 1) {
|
|
17238
|
+
const url = `${baseUrl}?per_page=${GITHUB_SECRET_PAGE_SIZE}&page=${page}`;
|
|
17239
|
+
const response = await githubApi(this.fetchImpl, url, adminToken);
|
|
17240
|
+
if (!response.ok) {
|
|
17241
|
+
throw new Error(
|
|
17242
|
+
`GitHub repository secret metadata request failed on page ${page} with HTTP ${response.status}: ${safeGitHubMessage(response.body, "request failed")}`
|
|
17243
|
+
);
|
|
17244
|
+
}
|
|
17245
|
+
const body = isRecord2(response.body) ? response.body : {};
|
|
17246
|
+
if (totalCount === void 0 && typeof body.total_count === "number" && Number.isFinite(body.total_count)) {
|
|
17247
|
+
totalCount = Math.max(0, Math.trunc(body.total_count));
|
|
17248
|
+
}
|
|
17249
|
+
const secrets = Array.isArray(body.secrets) ? body.secrets : [];
|
|
17250
|
+
const before = names.size;
|
|
17251
|
+
for (const item of secrets) {
|
|
16649
17252
|
if (isRecord2(item) && typeof item.name === "string") {
|
|
16650
17253
|
names.add(item.name);
|
|
16651
17254
|
}
|
|
16652
17255
|
}
|
|
17256
|
+
pagesRead = page;
|
|
17257
|
+
if (totalCount !== void 0 && names.size >= totalCount) {
|
|
17258
|
+
break;
|
|
17259
|
+
}
|
|
17260
|
+
if (secrets.length < GITHUB_SECRET_PAGE_SIZE) {
|
|
17261
|
+
if (totalCount !== void 0 && names.size < totalCount) {
|
|
17262
|
+
throw new Error(
|
|
17263
|
+
`GitHub repository secret metadata pagination ended early after ${names.size} of ${totalCount} secret name(s).`
|
|
17264
|
+
);
|
|
17265
|
+
}
|
|
17266
|
+
break;
|
|
17267
|
+
}
|
|
17268
|
+
if (names.size === before) {
|
|
17269
|
+
throw new Error("GitHub repository secret metadata pagination made no progress.");
|
|
17270
|
+
}
|
|
17271
|
+
if (page === MAX_GITHUB_SECRET_PAGES) {
|
|
17272
|
+
throw new Error(`GitHub repository secret metadata exceeded ${MAX_GITHUB_SECRET_PAGES} pages.`);
|
|
17273
|
+
}
|
|
16653
17274
|
}
|
|
16654
17275
|
return {
|
|
16655
17276
|
verified: true,
|
|
16656
17277
|
names,
|
|
16657
|
-
message: `Verified ${names.size} GitHub repository secret name(s); values remain unreadable.`
|
|
17278
|
+
message: `Verified ${names.size} GitHub repository secret name(s) across ${pagesRead} page(s); values remain unreadable.`
|
|
16658
17279
|
};
|
|
16659
17280
|
}
|
|
16660
17281
|
async setCallbackSecret(plan, secret, value, adminToken) {
|
|
@@ -16671,7 +17292,12 @@ var GitHubSetupProvider = class {
|
|
|
16671
17292
|
// src/azureDevOpsSetupProvider.ts
|
|
16672
17293
|
var isRecord3 = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
16673
17294
|
async function adoGet(fetchImpl, url, token) {
|
|
16674
|
-
const response = await fetchImpl(url, {
|
|
17295
|
+
const response = await fetchImpl(url, {
|
|
17296
|
+
headers: {
|
|
17297
|
+
accept: "application/json",
|
|
17298
|
+
...token ? { authorization: `Basic ${Buffer.from(`:${token}`).toString("base64")}` } : {}
|
|
17299
|
+
}
|
|
17300
|
+
});
|
|
16675
17301
|
const text = await response.text();
|
|
16676
17302
|
let body = {};
|
|
16677
17303
|
try {
|
|
@@ -16684,7 +17310,8 @@ async function setAzurePipelineVariableWithRest(input, fetchImpl) {
|
|
|
16684
17310
|
const base = `https://dev.azure.com/${encodeURIComponent(input.organization)}/${encodeURIComponent(input.project)}/_apis/build/definitions/${encodeURIComponent(input.pipelineId)}`;
|
|
16685
17311
|
const auth = { authorization: `Basic ${Buffer.from(`:${input.token}`).toString("base64")}` };
|
|
16686
17312
|
const current = await adoGet(fetchImpl, `${base}?api-version=7.1`, input.token);
|
|
16687
|
-
if (!current.ok || !isRecord3(current.body))
|
|
17313
|
+
if (!current.ok || !isRecord3(current.body))
|
|
17314
|
+
throw new Error(`Azure DevOps build definition could not be read (HTTP ${current.status}).`);
|
|
16688
17315
|
const variables = isRecord3(current.body.variables) ? { ...current.body.variables } : {};
|
|
16689
17316
|
variables[input.name] = { value: input.value, isSecret: true };
|
|
16690
17317
|
const query = new URLSearchParams({
|
|
@@ -16692,9 +17319,25 @@ async function setAzurePipelineVariableWithRest(input, fetchImpl) {
|
|
|
16692
17319
|
secretsSourceDefinitionId: String(current.body.id),
|
|
16693
17320
|
secretsSourceDefinitionRevision: String(current.body.revision)
|
|
16694
17321
|
});
|
|
16695
|
-
const response = await fetchImpl(`${base}?${query.toString()}`, {
|
|
17322
|
+
const response = await fetchImpl(`${base}?${query.toString()}`, {
|
|
17323
|
+
method: "PUT",
|
|
17324
|
+
headers: { ...auth, "content-type": "application/json", accept: "application/json" },
|
|
17325
|
+
body: JSON.stringify({ ...current.body, variables })
|
|
17326
|
+
});
|
|
16696
17327
|
if (!response.ok) throw new Error(`Azure DevOps pipeline variable update failed (HTTP ${response.status}).`);
|
|
16697
17328
|
}
|
|
17329
|
+
function azurePipelineStateMessage(name, queueStatus) {
|
|
17330
|
+
switch (queueStatus) {
|
|
17331
|
+
case "enabled":
|
|
17332
|
+
return `Azure DevOps pipeline ${name} is reachable and enabled. Inspection never queues a run.`;
|
|
17333
|
+
case "paused":
|
|
17334
|
+
return `Azure DevOps pipeline ${name} is reachable but paused; builds may be queued but will not start until the definition is enabled.`;
|
|
17335
|
+
case "disabled":
|
|
17336
|
+
return `Azure DevOps pipeline ${name} is reachable but disabled; new builds cannot be queued until the definition is enabled.`;
|
|
17337
|
+
default:
|
|
17338
|
+
return `Azure DevOps pipeline ${name} is reachable, but its queue status is ${queueStatus ?? "unknown"}.`;
|
|
17339
|
+
}
|
|
17340
|
+
}
|
|
16698
17341
|
var AzureDevOpsSetupProvider = class {
|
|
16699
17342
|
name = "azure-devops";
|
|
16700
17343
|
fetchImpl;
|
|
@@ -16707,23 +17350,51 @@ var AzureDevOpsSetupProvider = class {
|
|
|
16707
17350
|
const a = plan.azureDevOps;
|
|
16708
17351
|
const url = `https://dev.azure.com/${encodeURIComponent(a.organization)}/${encodeURIComponent(a.project)}/_apis/build/definitions/${encodeURIComponent(a.pipelineId)}?api-version=${encodeURIComponent(a.apiVersion)}`;
|
|
16709
17352
|
const result = await adoGet(this.fetchImpl, url, token);
|
|
16710
|
-
if (!result.ok)
|
|
17353
|
+
if (!result.ok)
|
|
17354
|
+
return {
|
|
17355
|
+
found: false,
|
|
17356
|
+
message: result.status === 404 ? "Azure DevOps pipeline was not found." : `Azure DevOps pipeline metadata could not be verified (HTTP ${result.status}).`
|
|
17357
|
+
};
|
|
16711
17358
|
const body = isRecord3(result.body) ? result.body : {};
|
|
16712
|
-
const name = typeof body.name === "string" ? body.name :
|
|
16713
|
-
|
|
17359
|
+
const name = typeof body.name === "string" ? body.name : a.pipelineId;
|
|
17360
|
+
const queueStatus = typeof body.queueStatus === "string" ? body.queueStatus : void 0;
|
|
17361
|
+
return {
|
|
17362
|
+
found: true,
|
|
17363
|
+
active: queueStatus === "enabled",
|
|
17364
|
+
state: queueStatus,
|
|
17365
|
+
path: name,
|
|
17366
|
+
message: azurePipelineStateMessage(name, queueStatus)
|
|
17367
|
+
};
|
|
16714
17368
|
}
|
|
16715
17369
|
async inspectCallbackSecretMetadata(plan, token) {
|
|
16716
|
-
if (!token)
|
|
17370
|
+
if (!token)
|
|
17371
|
+
return {
|
|
17372
|
+
verified: false,
|
|
17373
|
+
names: /* @__PURE__ */ new Set(),
|
|
17374
|
+
message: "Azure DevOps pipeline variable metadata was not verified because azureDevOpsAdminToken was not provided. Secret values are never readable."
|
|
17375
|
+
};
|
|
16717
17376
|
const a = plan.azureDevOps;
|
|
16718
17377
|
const url = `https://dev.azure.com/${encodeURIComponent(a.organization)}/${encodeURIComponent(a.project)}/_apis/build/definitions/${encodeURIComponent(a.pipelineId)}?api-version=${encodeURIComponent(a.apiVersion)}`;
|
|
16719
17378
|
const result = await adoGet(this.fetchImpl, url, token);
|
|
16720
17379
|
if (!result.ok) throw new Error(`Azure DevOps pipeline metadata request failed with HTTP ${result.status}.`);
|
|
16721
17380
|
const variables = isRecord3(result.body) && isRecord3(result.body.variables) ? result.body.variables : {};
|
|
16722
|
-
return {
|
|
17381
|
+
return {
|
|
17382
|
+
verified: true,
|
|
17383
|
+
names: new Set(Object.keys(variables)),
|
|
17384
|
+
message: `Verified ${Object.keys(variables).length} Azure DevOps pipeline variable name(s); secret values remain unreadable.`
|
|
17385
|
+
};
|
|
16723
17386
|
}
|
|
16724
17387
|
async setCallbackSecret(plan, secret, value, adminToken) {
|
|
16725
|
-
if (!this.setter)
|
|
16726
|
-
|
|
17388
|
+
if (!this.setter)
|
|
17389
|
+
throw new Error("Azure DevOps pipeline variable setter is not configured; use the safe Azure CLI adapter.");
|
|
17390
|
+
await this.setter({
|
|
17391
|
+
organization: plan.azureDevOps.organization,
|
|
17392
|
+
project: plan.azureDevOps.project,
|
|
17393
|
+
pipelineId: plan.azureDevOps.pipelineId,
|
|
17394
|
+
name: secret.repositorySecretName,
|
|
17395
|
+
value,
|
|
17396
|
+
token: adminToken
|
|
17397
|
+
});
|
|
16727
17398
|
}
|
|
16728
17399
|
};
|
|
16729
17400
|
|
|
@@ -16753,30 +17424,138 @@ function buildAzureDevOpsSetupPlan(input) {
|
|
|
16753
17424
|
const endpointName = variableName(input.callbackEndpointVariableName, "callback endpoint variable name");
|
|
16754
17425
|
const tokenName = variableName(input.callbackAuthTokenVariableName, "callback auth token variable name");
|
|
16755
17426
|
if (endpointName === tokenName) throw new Error("callback variable names must be distinct.");
|
|
16756
|
-
const requiredSecrets = [
|
|
16757
|
-
|
|
16758
|
-
|
|
16759
|
-
|
|
17427
|
+
const requiredSecrets = [
|
|
17428
|
+
{
|
|
17429
|
+
repositorySecretName: endpointName,
|
|
17430
|
+
valueKey: "callbackEndpoint",
|
|
17431
|
+
purpose: "Forge callback endpoint pipeline variable."
|
|
17432
|
+
},
|
|
17433
|
+
{
|
|
17434
|
+
repositorySecretName: tokenName,
|
|
17435
|
+
valueKey: "callbackAuthToken",
|
|
17436
|
+
purpose: "Forge callback auth token pipeline variable."
|
|
17437
|
+
}
|
|
17438
|
+
];
|
|
17439
|
+
const scopes = [
|
|
17440
|
+
"provider-secrets",
|
|
17441
|
+
"jira-profile",
|
|
17442
|
+
...input.setProjectDefault ? ["project-default"] : []
|
|
17443
|
+
];
|
|
17444
|
+
const actions = scopes.map((scope) => ({
|
|
17445
|
+
id: scope === "provider-secrets" ? "set-provider-secrets" : scope === "jira-profile" ? "upsert-jira-profile" : "set-project-default",
|
|
17446
|
+
scope,
|
|
17447
|
+
summary: `Apply ${scope} for Azure DevOps.`,
|
|
17448
|
+
mutates: "jira"
|
|
17449
|
+
}));
|
|
17450
|
+
const withoutId = {
|
|
17451
|
+
schemaVersion: "automatify.testops.setup/v1",
|
|
17452
|
+
kind: "AutomatifyTestOpsSetupPlan",
|
|
17453
|
+
provider: "azure-devops",
|
|
17454
|
+
project: { key: projectKey },
|
|
17455
|
+
azureDevOps: {
|
|
17456
|
+
organization,
|
|
17457
|
+
project: azureProject,
|
|
17458
|
+
pipelineId,
|
|
17459
|
+
ref,
|
|
17460
|
+
apiVersion,
|
|
17461
|
+
requiredSecrets,
|
|
17462
|
+
guidance: {
|
|
17463
|
+
authentication: "Use an Azure DevOps PAT with least-privilege pipeline read/manage-variable access.",
|
|
17464
|
+
permissions: ["Pipelines: read for doctor; manage variables only for explicit provider-secrets apply."],
|
|
17465
|
+
remoteChangesExcluded: ["accounts", "credentials", "commits", "pull requests", "pipeline runs"]
|
|
17466
|
+
}
|
|
17467
|
+
},
|
|
17468
|
+
jiraProfile: {
|
|
17469
|
+
label: profileLabel,
|
|
17470
|
+
enabled: input.enabled,
|
|
17471
|
+
provider: "azureDevops",
|
|
17472
|
+
config: {
|
|
17473
|
+
organization,
|
|
17474
|
+
project: azureProject,
|
|
17475
|
+
pipelineId,
|
|
17476
|
+
apiVersion,
|
|
17477
|
+
bodyTemplate: JSON.stringify({ resources: { repositories: { self: { refName: ref } } } })
|
|
17478
|
+
},
|
|
17479
|
+
setProjectDefault: input.setProjectDefault
|
|
17480
|
+
},
|
|
17481
|
+
actions,
|
|
17482
|
+
smokeValidation: [
|
|
17483
|
+
{
|
|
17484
|
+
id: "pipeline-content",
|
|
17485
|
+
verifies: "Azure DevOps pipeline metadata without queueing a run.",
|
|
17486
|
+
triggersExternalRun: false
|
|
17487
|
+
},
|
|
17488
|
+
{
|
|
17489
|
+
id: "pipeline-variable-metadata",
|
|
17490
|
+
verifies: "Required variable names; values remain unreadable.",
|
|
17491
|
+
triggersExternalRun: false
|
|
17492
|
+
},
|
|
17493
|
+
{
|
|
17494
|
+
id: "jira-profile",
|
|
17495
|
+
verifies: "Forge automation profile and optional default.",
|
|
17496
|
+
triggersExternalRun: false
|
|
17497
|
+
}
|
|
17498
|
+
],
|
|
17499
|
+
rollback: actions.map((a) => ({
|
|
17500
|
+
actionId: a.id,
|
|
17501
|
+
strategy: "Restore the previous state manually; secret values cannot be read back.",
|
|
17502
|
+
automatic: false
|
|
17503
|
+
})),
|
|
17504
|
+
followUp: {
|
|
17505
|
+
azureDevOps: "Azure DevOps setup is implemented through this provider-neutral plan/apply/doctor boundary."
|
|
17506
|
+
}
|
|
17507
|
+
};
|
|
16760
17508
|
return { ...withoutId, planId: digest(withoutId) };
|
|
16761
17509
|
}
|
|
16762
17510
|
function validateAzureDevOpsSetupPlan(value) {
|
|
16763
17511
|
const errors = [];
|
|
16764
17512
|
const record = value;
|
|
16765
|
-
const exact = (v, keys) => Boolean(
|
|
16766
|
-
|
|
16767
|
-
|
|
16768
|
-
if (!exact(
|
|
17513
|
+
const exact = (v, keys) => Boolean(
|
|
17514
|
+
v && typeof v === "object" && !Array.isArray(v) && Object.keys(v).sort().join() === [...keys].sort().join()
|
|
17515
|
+
);
|
|
17516
|
+
if (!exact(value, [
|
|
17517
|
+
"schemaVersion",
|
|
17518
|
+
"kind",
|
|
17519
|
+
"planId",
|
|
17520
|
+
"provider",
|
|
17521
|
+
"project",
|
|
17522
|
+
"azureDevOps",
|
|
17523
|
+
"jiraProfile",
|
|
17524
|
+
"actions",
|
|
17525
|
+
"smokeValidation",
|
|
17526
|
+
"rollback",
|
|
17527
|
+
"followUp"
|
|
17528
|
+
]))
|
|
17529
|
+
errors.push("Plan has missing or unsupported top-level fields.");
|
|
17530
|
+
if (record?.schemaVersion !== "automatify.testops.setup/v1" || record?.kind !== "AutomatifyTestOpsSetupPlan" || record?.provider !== "azure-devops")
|
|
17531
|
+
errors.push("Plan schema, kind, or provider is invalid.");
|
|
17532
|
+
if (!exact(record?.project, ["key"]) || !/^[A-Z][A-Z0-9_]{1,31}$/.test(record?.project?.key ?? ""))
|
|
17533
|
+
errors.push("project.key is invalid.");
|
|
16769
17534
|
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
|
-
|
|
17535
|
+
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 ?? ""))
|
|
17536
|
+
errors.push("azureDevOps has an invalid shape or fields.");
|
|
17537
|
+
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(
|
|
17538
|
+
(s) => exact(s, ["repositorySecretName", "valueKey", "purpose"]) && /^[A-Z_][A-Z0-9_]*$/.test(s.repositorySecretName) && s.purpose
|
|
17539
|
+
))
|
|
17540
|
+
errors.push("azureDevOps.requiredSecrets is invalid.");
|
|
16772
17541
|
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)
|
|
17542
|
+
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)
|
|
17543
|
+
errors.push("jiraProfile/config is invalid or inconsistent.");
|
|
16774
17544
|
const ids = p?.setProjectDefault ? ["set-provider-secrets", "upsert-jira-profile", "set-project-default"] : ["set-provider-secrets", "upsert-jira-profile"];
|
|
16775
17545
|
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(
|
|
17546
|
+
if (!Array.isArray(record?.actions) || record.actions.map((x) => x.id).join() !== ids.join() || !record.actions.every(
|
|
17547
|
+
(x, i) => exact(x, ["id", "scope", "summary", "mutates"]) && x.scope === scopes[i] && x.mutates === "jira"
|
|
17548
|
+
))
|
|
17549
|
+
errors.push("actions are invalid.");
|
|
16777
17550
|
const smokeIds = ["pipeline-content", "pipeline-variable-metadata", "jira-profile"];
|
|
16778
|
-
if (!Array.isArray(record?.smokeValidation) || record.smokeValidation.length !== 3 || !record.smokeValidation.every(
|
|
16779
|
-
|
|
17551
|
+
if (!Array.isArray(record?.smokeValidation) || record.smokeValidation.length !== 3 || !record.smokeValidation.every(
|
|
17552
|
+
(x, i) => exact(x, ["id", "verifies", "triggersExternalRun"]) && x.id === smokeIds[i] && x.triggersExternalRun === false
|
|
17553
|
+
))
|
|
17554
|
+
errors.push("smokeValidation is invalid.");
|
|
17555
|
+
if (!Array.isArray(record?.rollback) || record.rollback.length !== ids.length || !record.rollback.every(
|
|
17556
|
+
(x, i) => exact(x, ["actionId", "strategy", "automatic"]) && x.actionId === ids[i] && x.automatic === false
|
|
17557
|
+
))
|
|
17558
|
+
errors.push("rollback is invalid.");
|
|
16780
17559
|
if (errors.length === 0) {
|
|
16781
17560
|
const { planId, ...rest } = record;
|
|
16782
17561
|
if (planId !== digest(rest)) errors.push("planId does not match the plan contents.");
|
|
@@ -16932,7 +17711,7 @@ function loadPlan(planPath, cwd) {
|
|
|
16932
17711
|
const absolute = import_node_path15.default.resolve(cwd, planPath);
|
|
16933
17712
|
let parsed;
|
|
16934
17713
|
try {
|
|
16935
|
-
parsed = JSON.parse((0,
|
|
17714
|
+
parsed = JSON.parse((0, import_node_fs15.readFileSync)(absolute, "utf8"));
|
|
16936
17715
|
} catch (error) {
|
|
16937
17716
|
throw new SetupCommandError(
|
|
16938
17717
|
"PLAN_READ_ERROR",
|
|
@@ -16972,14 +17751,26 @@ async function resolveSecrets(parsed, deps) {
|
|
|
16972
17751
|
);
|
|
16973
17752
|
}
|
|
16974
17753
|
if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
|
|
16975
|
-
throw new SetupCommandError(
|
|
17754
|
+
throw new SetupCommandError(
|
|
17755
|
+
"SECRET_INPUT_ERROR",
|
|
17756
|
+
"Secret stdin must be a JSON object.",
|
|
17757
|
+
ExitCode.ValidationError
|
|
17758
|
+
);
|
|
16976
17759
|
}
|
|
16977
17760
|
for (const [key, value] of Object.entries(envelope)) {
|
|
16978
17761
|
if (!isSecretKey(key)) {
|
|
16979
|
-
throw new SetupCommandError(
|
|
17762
|
+
throw new SetupCommandError(
|
|
17763
|
+
"SECRET_INPUT_ERROR",
|
|
17764
|
+
`Secret stdin contains unsupported key ${key}.`,
|
|
17765
|
+
ExitCode.ValidationError
|
|
17766
|
+
);
|
|
16980
17767
|
}
|
|
16981
17768
|
if (typeof value !== "string" || !value.trim()) {
|
|
16982
|
-
throw new SetupCommandError(
|
|
17769
|
+
throw new SetupCommandError(
|
|
17770
|
+
"SECRET_INPUT_ERROR",
|
|
17771
|
+
`Secret stdin key ${key} must be a non-empty string.`,
|
|
17772
|
+
ExitCode.ValidationError
|
|
17773
|
+
);
|
|
16983
17774
|
}
|
|
16984
17775
|
values[key] = value.trim();
|
|
16985
17776
|
sources[key] = "stdin";
|
|
@@ -16998,10 +17789,18 @@ async function resolveSecrets(parsed, deps) {
|
|
|
16998
17789
|
}
|
|
16999
17790
|
const envName = target.slice(4);
|
|
17000
17791
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(envName)) {
|
|
17001
|
-
throw new SetupCommandError(
|
|
17792
|
+
throw new SetupCommandError(
|
|
17793
|
+
"SECRET_REFERENCE_ERROR",
|
|
17794
|
+
`Secret reference for ${key} has an invalid environment variable name.`,
|
|
17795
|
+
ExitCode.ValidationError
|
|
17796
|
+
);
|
|
17002
17797
|
}
|
|
17003
17798
|
if (sources[key]) {
|
|
17004
|
-
throw new SetupCommandError(
|
|
17799
|
+
throw new SetupCommandError(
|
|
17800
|
+
"SECRET_REFERENCE_ERROR",
|
|
17801
|
+
`Secret ${key} was provided more than once.`,
|
|
17802
|
+
ExitCode.ValidationError
|
|
17803
|
+
);
|
|
17005
17804
|
}
|
|
17006
17805
|
const value = deps.env[envName]?.trim();
|
|
17007
17806
|
if (!value) {
|
|
@@ -17036,7 +17835,11 @@ function approvedScopes(plan, parsed, dryRun) {
|
|
|
17036
17835
|
throw new SetupCommandError("APPROVAL_ERROR", `Unsupported approval scope ${scope}.`, ExitCode.UsageError);
|
|
17037
17836
|
}
|
|
17038
17837
|
if (!planScopes.includes(scope)) {
|
|
17039
|
-
throw new SetupCommandError(
|
|
17838
|
+
throw new SetupCommandError(
|
|
17839
|
+
"APPROVAL_ERROR",
|
|
17840
|
+
`Approval scope ${scope} is not present in this plan.`,
|
|
17841
|
+
ExitCode.UsageError
|
|
17842
|
+
);
|
|
17040
17843
|
}
|
|
17041
17844
|
expanded.add(scope);
|
|
17042
17845
|
}
|
|
@@ -17109,10 +17912,7 @@ async function prepareState(plan, scopes, secrets, context, deps, dryRun, rotate
|
|
|
17109
17912
|
);
|
|
17110
17913
|
}
|
|
17111
17914
|
if (secrets.values.githubAdminToken) {
|
|
17112
|
-
const inspection = await deps.githubProvider.inspectCallbackSecretMetadata(
|
|
17113
|
-
plan,
|
|
17114
|
-
secrets.values.githubAdminToken
|
|
17115
|
-
);
|
|
17915
|
+
const inspection = await deps.githubProvider.inspectCallbackSecretMetadata(plan, secrets.values.githubAdminToken);
|
|
17116
17916
|
repositorySecretNames = inspection.names;
|
|
17117
17917
|
const secretsToWrite = plan.github.requiredSecrets.filter(
|
|
17118
17918
|
(secret) => rotateSecrets || !repositorySecretNames?.has(secret.repositorySecretName)
|
|
@@ -17150,7 +17950,7 @@ function resolveWorkflowTarget(plan, repoRoot) {
|
|
|
17150
17950
|
function applyWorkflowFile(plan, repoRoot, dryRun) {
|
|
17151
17951
|
const action = plan.actions.find((item) => item.scope === "workflow-file");
|
|
17152
17952
|
const target = resolveWorkflowTarget(plan, repoRoot);
|
|
17153
|
-
const sourceContent = (0,
|
|
17953
|
+
const sourceContent = (0, import_node_fs15.readFileSync)(plan.github.workflowFile.sourcePath, "utf8");
|
|
17154
17954
|
if (hashSetupContent(sourceContent) !== plan.github.workflowFile.sha256) {
|
|
17155
17955
|
throw new SetupCommandError(
|
|
17156
17956
|
"WORKFLOW_SOURCE_CHANGED",
|
|
@@ -17158,8 +17958,8 @@ function applyWorkflowFile(plan, repoRoot, dryRun) {
|
|
|
17158
17958
|
ExitCode.ValidationError
|
|
17159
17959
|
);
|
|
17160
17960
|
}
|
|
17161
|
-
const existed = (0,
|
|
17162
|
-
const previousContent = existed ? (0,
|
|
17961
|
+
const existed = (0, import_node_fs15.existsSync)(target);
|
|
17962
|
+
const previousContent = existed ? (0, import_node_fs15.readFileSync)(target, "utf8") : void 0;
|
|
17163
17963
|
const matches = previousContent !== void 0 && hashSetupContent(previousContent) === plan.github.workflowFile.sha256;
|
|
17164
17964
|
if (matches) {
|
|
17165
17965
|
return {
|
|
@@ -17171,8 +17971,8 @@ function applyWorkflowFile(plan, repoRoot, dryRun) {
|
|
|
17171
17971
|
};
|
|
17172
17972
|
}
|
|
17173
17973
|
if (!dryRun) {
|
|
17174
|
-
(0,
|
|
17175
|
-
(0,
|
|
17974
|
+
(0, import_node_fs15.mkdirSync)(import_node_path15.default.dirname(target), { recursive: true });
|
|
17975
|
+
(0, import_node_fs15.writeFileSync)(target, sourceContent, { encoding: "utf8", mode: 420 });
|
|
17176
17976
|
}
|
|
17177
17977
|
return {
|
|
17178
17978
|
id: action.id,
|
|
@@ -17215,12 +18015,7 @@ async function applyGitHubSecrets(plan, state, secrets, deps, dryRun, rotateSecr
|
|
|
17215
18015
|
}
|
|
17216
18016
|
const adminToken = secrets.values.githubAdminToken;
|
|
17217
18017
|
for (const secret of pending) {
|
|
17218
|
-
await deps.githubProvider.setCallbackSecret(
|
|
17219
|
-
plan,
|
|
17220
|
-
secret,
|
|
17221
|
-
secrets.values[secret.valueKey],
|
|
17222
|
-
adminToken
|
|
17223
|
-
);
|
|
18018
|
+
await deps.githubProvider.setCallbackSecret(plan, secret, secrets.values[secret.valueKey], adminToken);
|
|
17224
18019
|
}
|
|
17225
18020
|
return {
|
|
17226
18021
|
id: action.id,
|
|
@@ -17347,14 +18142,14 @@ async function inspectPlan(plan, repoRoot, secrets, context, deps) {
|
|
|
17347
18142
|
const checks = [];
|
|
17348
18143
|
let forgeTransportFailure = false;
|
|
17349
18144
|
const target = resolveWorkflowTarget(plan, repoRoot);
|
|
17350
|
-
if (!(0,
|
|
18145
|
+
if (!(0, import_node_fs15.existsSync)(target)) {
|
|
17351
18146
|
checks.push({
|
|
17352
18147
|
id: "workflow-local",
|
|
17353
18148
|
status: "fail",
|
|
17354
18149
|
message: `Local workflow file ${plan.github.workflowFile.path} does not exist.`
|
|
17355
18150
|
});
|
|
17356
18151
|
} else {
|
|
17357
|
-
const localHash = hashSetupContent((0,
|
|
18152
|
+
const localHash = hashSetupContent((0, import_node_fs15.readFileSync)(target, "utf8"));
|
|
17358
18153
|
checks.push({
|
|
17359
18154
|
id: "workflow-local",
|
|
17360
18155
|
status: localHash === plan.github.workflowFile.sha256 ? "pass" : "fail",
|
|
@@ -17446,31 +18241,39 @@ async function inspectPlan(plan, repoRoot, secrets, context, deps) {
|
|
|
17446
18241
|
function planCommand(args, deps) {
|
|
17447
18242
|
const parsed = parseArgs10(args, PLAN_VALUE_FLAGS, /* @__PURE__ */ new Set(["--set-default", "--disabled"]));
|
|
17448
18243
|
if (parsed.errors.length > 0) {
|
|
17449
|
-
return jsonResponse(
|
|
18244
|
+
return jsonResponse(
|
|
18245
|
+
errorPayload("USAGE_ERROR", "Invalid setup plan arguments.", parsed.errors),
|
|
18246
|
+
ExitCode.UsageError
|
|
18247
|
+
);
|
|
17450
18248
|
}
|
|
17451
18249
|
const provider = parsed.flags["--provider"] ?? "github-actions";
|
|
17452
18250
|
if (provider === "azure-devops") {
|
|
17453
18251
|
try {
|
|
17454
|
-
return jsonResponse(
|
|
17455
|
-
|
|
17456
|
-
|
|
17457
|
-
|
|
17458
|
-
|
|
17459
|
-
|
|
17460
|
-
|
|
17461
|
-
|
|
17462
|
-
|
|
17463
|
-
|
|
17464
|
-
|
|
17465
|
-
|
|
17466
|
-
|
|
18252
|
+
return jsonResponse(
|
|
18253
|
+
buildAzureDevOpsSetupPlan({
|
|
18254
|
+
projectKey: parsed.flags["--project-key"] ?? "",
|
|
18255
|
+
organization: parsed.flags["--organization"] ?? "",
|
|
18256
|
+
azureProject: parsed.flags["--azure-project"] ?? "",
|
|
18257
|
+
pipelineId: parsed.flags["--pipeline-id"] ?? "",
|
|
18258
|
+
ref: parsed.flags["--ref"] ?? "main",
|
|
18259
|
+
apiVersion: parsed.flags["--api-version"],
|
|
18260
|
+
profileLabel: parsed.flags["--profile-label"] ?? "Azure DevOps",
|
|
18261
|
+
enabled: !parsed.boolFlags.has("--disabled"),
|
|
18262
|
+
setProjectDefault: parsed.boolFlags.has("--set-default"),
|
|
18263
|
+
callbackEndpointVariableName: parsed.flags["--callback-endpoint-variable-name"] ?? "TESTOPS_FORGE_ENDPOINT",
|
|
18264
|
+
callbackAuthTokenVariableName: parsed.flags["--callback-auth-token-variable-name"] ?? "TESTOPS_FORGE_AUTH_TOKEN"
|
|
18265
|
+
})
|
|
18266
|
+
);
|
|
17467
18267
|
} catch (error) {
|
|
17468
18268
|
return jsonResponse(errorPayload("VALIDATION_ERROR", safeErrorMessage(error)), ExitCode.ValidationError);
|
|
17469
18269
|
}
|
|
17470
18270
|
}
|
|
17471
18271
|
if (provider !== "github-actions") {
|
|
17472
18272
|
return jsonResponse(
|
|
17473
|
-
errorPayload(
|
|
18273
|
+
errorPayload(
|
|
18274
|
+
"VALIDATION_ERROR",
|
|
18275
|
+
"Only github-actions is implemented in this stage; Azure DevOps is the documented next adapter."
|
|
18276
|
+
),
|
|
17474
18277
|
ExitCode.ValidationError
|
|
17475
18278
|
);
|
|
17476
18279
|
}
|
|
@@ -17491,16 +18294,16 @@ function planCommand(args, deps) {
|
|
|
17491
18294
|
});
|
|
17492
18295
|
return jsonResponse(plan);
|
|
17493
18296
|
} catch (error) {
|
|
17494
|
-
return jsonResponse(
|
|
17495
|
-
errorPayload("VALIDATION_ERROR", safeErrorMessage(error)),
|
|
17496
|
-
ExitCode.ValidationError
|
|
17497
|
-
);
|
|
18297
|
+
return jsonResponse(errorPayload("VALIDATION_ERROR", safeErrorMessage(error)), ExitCode.ValidationError);
|
|
17498
18298
|
}
|
|
17499
18299
|
}
|
|
17500
18300
|
async function applyCommand(args, context, deps) {
|
|
17501
18301
|
const parsed = parseArgs10(args, APPLY_VALUE_FLAGS, APPLY_BOOL_FLAGS, true);
|
|
17502
18302
|
if (parsed.errors.length > 0) {
|
|
17503
|
-
return jsonResponse(
|
|
18303
|
+
return jsonResponse(
|
|
18304
|
+
errorPayload("USAGE_ERROR", "Invalid setup apply arguments.", parsed.errors),
|
|
18305
|
+
ExitCode.UsageError
|
|
18306
|
+
);
|
|
17504
18307
|
}
|
|
17505
18308
|
const planPath = parsed.flags["--plan"];
|
|
17506
18309
|
if (!planPath) {
|
|
@@ -17525,16 +18328,7 @@ async function applyCommand(args, context, deps) {
|
|
|
17525
18328
|
const repoRoot = import_node_path15.default.resolve(deps.cwd, parsed.flags["--repo-root"] ?? ".");
|
|
17526
18329
|
const rotateSecrets = parsed.boolFlags.has("--rotate-secrets");
|
|
17527
18330
|
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
|
-
);
|
|
18331
|
+
const state = await prepareState(plan, scopes, secrets, context, deps, dryRun, rotateSecrets, rotateProviderToken);
|
|
17538
18332
|
let appliedProfile = state.matchingProfile;
|
|
17539
18333
|
for (const action of plan.actions) {
|
|
17540
18334
|
if (!scopes.includes(action.scope)) {
|
|
@@ -17553,14 +18347,7 @@ async function applyCommand(args, context, deps) {
|
|
|
17553
18347
|
} else if (action.scope === "github-secrets") {
|
|
17554
18348
|
actionResults.push(await applyGitHubSecrets(plan, state, secrets, deps, dryRun, rotateSecrets));
|
|
17555
18349
|
} else if (action.scope === "jira-profile") {
|
|
17556
|
-
const applied = await applyJiraProfile(
|
|
17557
|
-
plan,
|
|
17558
|
-
state,
|
|
17559
|
-
secrets,
|
|
17560
|
-
context,
|
|
17561
|
-
dryRun,
|
|
17562
|
-
rotateProviderToken
|
|
17563
|
-
);
|
|
18350
|
+
const applied = await applyJiraProfile(plan, state, secrets, context, dryRun, rotateProviderToken);
|
|
17564
18351
|
appliedProfile = applied.profile;
|
|
17565
18352
|
actionResults.push(applied.result);
|
|
17566
18353
|
} else if (action.scope === "project-default") {
|
|
@@ -17623,49 +18410,146 @@ async function applyCommand(args, context, deps) {
|
|
|
17623
18410
|
async function applyAzureCommand(plan, parsed, context, deps, secrets) {
|
|
17624
18411
|
const dryRun = parsed.boolFlags.has("--dry-run");
|
|
17625
18412
|
const scopes = approvedScopes(plan, parsed, dryRun);
|
|
17626
|
-
const profiles = requireServiceData(
|
|
18413
|
+
const profiles = requireServiceData(
|
|
18414
|
+
await context.invokeForgeContract("listScenarioAutomationProfiles", { context: { projectKey: plan.project.key } }),
|
|
18415
|
+
"listScenarioAutomationProfiles"
|
|
18416
|
+
);
|
|
17627
18417
|
const current = profiles.find((item) => item.label === plan.jiraProfile.label);
|
|
17628
|
-
if (!dryRun && scopes.includes("provider-secrets") && !secrets.values.azureDevOpsAdminToken)
|
|
18418
|
+
if (!dryRun && scopes.includes("provider-secrets") && !secrets.values.azureDevOpsAdminToken)
|
|
18419
|
+
throw new SetupCommandError(
|
|
18420
|
+
"SECRET_INPUT_REQUIRED",
|
|
18421
|
+
"azureDevOpsAdminToken is required for the approved Azure DevOps pipeline-variable scope.",
|
|
18422
|
+
ExitCode.ValidationError
|
|
18423
|
+
);
|
|
17629
18424
|
const results = [];
|
|
17630
18425
|
const secretNames = scopes.includes("provider-secrets") && secrets.values.azureDevOpsAdminToken ? (await deps.azureProvider.inspectCallbackSecretMetadata(plan, secrets.values.azureDevOpsAdminToken)).names : void 0;
|
|
17631
18426
|
let appliedProfile = current;
|
|
17632
18427
|
for (const action of plan.actions) {
|
|
17633
18428
|
if (!scopes.includes(action.scope)) {
|
|
17634
|
-
results.push({
|
|
18429
|
+
results.push({
|
|
18430
|
+
id: action.id,
|
|
18431
|
+
scope: action.scope,
|
|
18432
|
+
status: "skipped",
|
|
18433
|
+
message: "Action was not in the explicit approval scope.",
|
|
18434
|
+
rollback: { available: false, guidance: "No mutation occurred." }
|
|
18435
|
+
});
|
|
17635
18436
|
continue;
|
|
17636
18437
|
}
|
|
17637
18438
|
if (action.scope === "provider-secrets") {
|
|
17638
|
-
const pending = plan.azureDevOps.requiredSecrets.filter(
|
|
17639
|
-
|
|
17640
|
-
|
|
17641
|
-
|
|
18439
|
+
const pending = plan.azureDevOps.requiredSecrets.filter(
|
|
18440
|
+
(item) => parsed.boolFlags.has("--rotate-secrets") || !secretNames?.has(item.repositorySecretName)
|
|
18441
|
+
);
|
|
18442
|
+
if (!dryRun && pending.some((item) => !secrets.values[item.valueKey]))
|
|
18443
|
+
throw new SetupCommandError(
|
|
18444
|
+
"SECRET_INPUT_REQUIRED",
|
|
18445
|
+
"Azure DevOps callback secrets are required for approved pipeline variable changes.",
|
|
18446
|
+
ExitCode.ValidationError
|
|
18447
|
+
);
|
|
18448
|
+
if (!dryRun)
|
|
18449
|
+
for (const item of pending)
|
|
18450
|
+
await deps.azureProvider.setCallbackSecret(
|
|
18451
|
+
plan,
|
|
18452
|
+
item,
|
|
18453
|
+
secrets.values[item.valueKey],
|
|
18454
|
+
secrets.values.azureDevOpsAdminToken
|
|
18455
|
+
);
|
|
18456
|
+
results.push({
|
|
18457
|
+
id: action.id,
|
|
18458
|
+
scope: action.scope,
|
|
18459
|
+
status: dryRun ? "planned" : pending.length ? "updated" : "skipped",
|
|
18460
|
+
message: "Azure DevOps pipeline variable names are managed without printing secret values.",
|
|
18461
|
+
rollback: { available: false, guidance: rollbackFor(plan, action.id) }
|
|
18462
|
+
});
|
|
17642
18463
|
} else if (action.scope === "jira-profile") {
|
|
17643
18464
|
const rotateProvider = parsed.boolFlags.has("--rotate-provider-token");
|
|
17644
|
-
if (!dryRun && (!current?.hasSecret || rotateProvider) && !secrets.values.azureDevOpsProviderToken)
|
|
17645
|
-
|
|
18465
|
+
if (!dryRun && (!current?.hasSecret || rotateProvider) && !secrets.values.azureDevOpsProviderToken)
|
|
18466
|
+
throw new SetupCommandError(
|
|
18467
|
+
"SECRET_INPUT_REQUIRED",
|
|
18468
|
+
"azureDevOpsProviderToken is required to create or rotate the Azure provider PAT.",
|
|
18469
|
+
ExitCode.ValidationError
|
|
18470
|
+
);
|
|
18471
|
+
const profile = dryRun ? current ?? {
|
|
18472
|
+
id: "<created-by-forge>",
|
|
18473
|
+
projectKey: plan.project.key,
|
|
18474
|
+
label: plan.jiraProfile.label,
|
|
18475
|
+
provider: "azureDevops",
|
|
18476
|
+
authType: "basicPat",
|
|
18477
|
+
method: "POST",
|
|
18478
|
+
endpointSummary: "<computed-by-forge>",
|
|
18479
|
+
config: plan.jiraProfile.config,
|
|
18480
|
+
enabled: plan.jiraProfile.enabled,
|
|
18481
|
+
hasSecret: false,
|
|
18482
|
+
createdAt: "",
|
|
18483
|
+
updatedAt: ""
|
|
18484
|
+
} : requireServiceData(
|
|
18485
|
+
await context.invokeForgeContract("upsertScenarioAutomationProfile", {
|
|
18486
|
+
context: { projectKey: plan.project.key },
|
|
18487
|
+
input: {
|
|
18488
|
+
profileId: current?.id,
|
|
18489
|
+
label: plan.jiraProfile.label,
|
|
18490
|
+
provider: "azureDevops",
|
|
18491
|
+
config: plan.jiraProfile.config,
|
|
18492
|
+
enabled: plan.jiraProfile.enabled,
|
|
18493
|
+
secretToken: !current?.hasSecret || rotateProvider ? secrets.values.azureDevOpsProviderToken : void 0
|
|
18494
|
+
}
|
|
18495
|
+
}),
|
|
18496
|
+
"upsertScenarioAutomationProfile"
|
|
18497
|
+
);
|
|
17646
18498
|
appliedProfile = profile;
|
|
17647
|
-
results.push({
|
|
18499
|
+
results.push({
|
|
18500
|
+
id: action.id,
|
|
18501
|
+
scope: action.scope,
|
|
18502
|
+
status: dryRun ? "planned" : current ? "updated" : "created",
|
|
18503
|
+
message: "Azure DevOps Jira automation profile applied through the existing Forge contract.",
|
|
18504
|
+
rollback: { available: !dryRun, guidance: rollbackFor(plan, action.id) }
|
|
18505
|
+
});
|
|
17648
18506
|
} else if (action.scope === "project-default" && appliedProfile && !dryRun) {
|
|
17649
|
-
requireServiceData(
|
|
17650
|
-
|
|
18507
|
+
requireServiceData(
|
|
18508
|
+
await context.invokeForgeContract("setScenarioAutomationDefaultProfile", {
|
|
18509
|
+
context: { projectKey: plan.project.key },
|
|
18510
|
+
profileId: appliedProfile.id
|
|
18511
|
+
}),
|
|
18512
|
+
"setScenarioAutomationDefaultProfile"
|
|
18513
|
+
);
|
|
18514
|
+
results.push({
|
|
18515
|
+
id: action.id,
|
|
18516
|
+
scope: action.scope,
|
|
18517
|
+
status: "updated",
|
|
18518
|
+
message: "Set the Azure DevOps Jira automation profile as project default.",
|
|
18519
|
+
rollback: { available: true, guidance: rollbackFor(plan, action.id) }
|
|
18520
|
+
});
|
|
17651
18521
|
} else if (action.scope === "project-default") {
|
|
17652
|
-
results.push({
|
|
18522
|
+
results.push({
|
|
18523
|
+
id: action.id,
|
|
18524
|
+
scope: action.scope,
|
|
18525
|
+
status: "planned",
|
|
18526
|
+
message: "Set the Azure DevOps Jira automation profile as project default.",
|
|
18527
|
+
rollback: { available: false, guidance: rollbackFor(plan, action.id) }
|
|
18528
|
+
});
|
|
17653
18529
|
}
|
|
17654
18530
|
}
|
|
17655
|
-
return jsonResponse({
|
|
18531
|
+
return jsonResponse({
|
|
18532
|
+
schemaVersion: APPLY_RESULT_SCHEMA_VERSION,
|
|
18533
|
+
planId: plan.planId,
|
|
18534
|
+
provider: plan.provider,
|
|
18535
|
+
status: dryRun ? "dry-run" : "applied",
|
|
18536
|
+
dryRun,
|
|
18537
|
+
confirmed: !dryRun,
|
|
18538
|
+
approvedScopes: scopes,
|
|
18539
|
+
actions: results,
|
|
18540
|
+
smokeValidation: { status: "not-run", externalRunTriggered: false }
|
|
18541
|
+
});
|
|
17656
18542
|
}
|
|
17657
18543
|
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
|
-
);
|
|
18544
|
+
const parsed = parseArgs10(args, APPLY_VALUE_FLAGS, /* @__PURE__ */ new Set(["--secrets-stdin"]), true);
|
|
17664
18545
|
if (parsed.approvals.length > 0) {
|
|
17665
18546
|
return jsonResponse(errorPayload("USAGE_ERROR", "setup doctor does not accept --approve."), ExitCode.UsageError);
|
|
17666
18547
|
}
|
|
17667
18548
|
if (parsed.errors.length > 0) {
|
|
17668
|
-
return jsonResponse(
|
|
18549
|
+
return jsonResponse(
|
|
18550
|
+
errorPayload("USAGE_ERROR", "Invalid setup doctor arguments.", parsed.errors),
|
|
18551
|
+
ExitCode.UsageError
|
|
18552
|
+
);
|
|
17669
18553
|
}
|
|
17670
18554
|
const planPath = parsed.flags["--plan"];
|
|
17671
18555
|
if (!planPath) {
|
|
@@ -17677,19 +18561,61 @@ async function doctorCommand(args, context, deps) {
|
|
|
17677
18561
|
secrets = await resolveSecrets(parsed, deps);
|
|
17678
18562
|
if (plan.provider === "azure-devops") {
|
|
17679
18563
|
const checks = [];
|
|
17680
|
-
const inspection = await deps.azureProvider.inspectExecutionDefinition(
|
|
17681
|
-
|
|
17682
|
-
|
|
17683
|
-
|
|
17684
|
-
checks.push({
|
|
17685
|
-
|
|
18564
|
+
const inspection = await deps.azureProvider.inspectExecutionDefinition(
|
|
18565
|
+
plan,
|
|
18566
|
+
secrets.values.azureDevOpsProviderToken ?? secrets.values.azureDevOpsAdminToken
|
|
18567
|
+
);
|
|
18568
|
+
checks.push({
|
|
18569
|
+
id: "pipeline-azure",
|
|
18570
|
+
status: inspection.found && inspection.active === true ? "pass" : "fail",
|
|
18571
|
+
message: inspection.message
|
|
18572
|
+
});
|
|
18573
|
+
const variables = await deps.azureProvider.inspectCallbackSecretMetadata(
|
|
18574
|
+
plan,
|
|
18575
|
+
secrets.values.azureDevOpsAdminToken
|
|
18576
|
+
);
|
|
18577
|
+
const missing = plan.azureDevOps.requiredSecrets.filter(
|
|
18578
|
+
(item) => !variables.names.has(item.repositorySecretName)
|
|
18579
|
+
);
|
|
18580
|
+
checks.push({
|
|
18581
|
+
id: "callback-secret-metadata",
|
|
18582
|
+
status: variables.verified && missing.length === 0 ? "pass" : variables.verified ? "fail" : "warn",
|
|
18583
|
+
message: variables.message
|
|
18584
|
+
});
|
|
18585
|
+
const profiles = requireServiceData(
|
|
18586
|
+
await context.invokeForgeContract("listScenarioAutomationProfiles", {
|
|
18587
|
+
context: { projectKey: plan.project.key }
|
|
18588
|
+
}),
|
|
18589
|
+
"listScenarioAutomationProfiles"
|
|
18590
|
+
);
|
|
17686
18591
|
const profile = profiles.find((item) => item.label === plan.jiraProfile.label);
|
|
17687
18592
|
const config = profile?.config;
|
|
17688
18593
|
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
|
-
|
|
18594
|
+
checks.push({
|
|
18595
|
+
id: "jira-profile",
|
|
18596
|
+
status: matches ? "pass" : "fail",
|
|
18597
|
+
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."
|
|
18598
|
+
});
|
|
18599
|
+
if (plan.jiraProfile.setProjectDefault)
|
|
18600
|
+
checks.push({
|
|
18601
|
+
id: "jira-project-default",
|
|
18602
|
+
status: profile?.isProjectDefault ? "pass" : "fail",
|
|
18603
|
+
message: profile?.isProjectDefault ? "Jira automation profile is the project default." : "Jira automation profile is not the project default."
|
|
18604
|
+
});
|
|
17691
18605
|
const status = doctorStatus(checks);
|
|
17692
|
-
|
|
18606
|
+
const exitCode = status === "fail" ? ExitCode.ValidationError : ExitCode.Success;
|
|
18607
|
+
return jsonResponse(
|
|
18608
|
+
{
|
|
18609
|
+
schemaVersion: DOCTOR_RESULT_SCHEMA_VERSION,
|
|
18610
|
+
planId: plan.planId,
|
|
18611
|
+
provider: plan.provider,
|
|
18612
|
+
status,
|
|
18613
|
+
exitCode,
|
|
18614
|
+
externalRunTriggered: false,
|
|
18615
|
+
checks
|
|
18616
|
+
},
|
|
18617
|
+
exitCode
|
|
18618
|
+
);
|
|
17693
18619
|
}
|
|
17694
18620
|
const repoRoot = import_node_path15.default.resolve(deps.cwd, parsed.flags["--repo-root"] ?? ".");
|
|
17695
18621
|
const summary = await inspectPlan(plan, repoRoot, secrets, context, deps);
|
|
@@ -17811,10 +18737,7 @@ function suitesToLines(items) {
|
|
|
17811
18737
|
if (items.length === 0) {
|
|
17812
18738
|
return ["Test suites: 0", "No test suites found for the selected context."];
|
|
17813
18739
|
}
|
|
17814
|
-
return [
|
|
17815
|
-
`Test suites: ${items.length}`,
|
|
17816
|
-
...items.map((item) => `- ${item.key} ${item.name}`)
|
|
17817
|
-
];
|
|
18740
|
+
return [`Test suites: ${items.length}`, ...items.map((item) => `- ${item.key} ${item.name}`)];
|
|
17818
18741
|
}
|
|
17819
18742
|
function suiteToLines(suite) {
|
|
17820
18743
|
return [
|
|
@@ -18237,6 +19160,12 @@ var COMMAND_REGISTRY = [
|
|
|
18237
19160
|
subcommands: ["upload", "download", "open"],
|
|
18238
19161
|
handler: createAllureHandler()
|
|
18239
19162
|
},
|
|
19163
|
+
{
|
|
19164
|
+
name: "auth",
|
|
19165
|
+
description: "Secret-safe Jira authentication login, status, and logout commands",
|
|
19166
|
+
subcommands: ["login", "status", "logout"],
|
|
19167
|
+
handler: createAuthHandler()
|
|
19168
|
+
},
|
|
18240
19169
|
{
|
|
18241
19170
|
name: "auto",
|
|
18242
19171
|
description: "Smart auto-ingestion command contract (MVP staged)",
|
|
@@ -18259,7 +19188,7 @@ var COMMAND_REGISTRY = [
|
|
|
18259
19188
|
{
|
|
18260
19189
|
name: "config",
|
|
18261
19190
|
description: "Configuration and auth bootstrap commands",
|
|
18262
|
-
subcommands: ["show", "validate", "set"],
|
|
19191
|
+
subcommands: ["show", "validate", "set", "unset", "migrate-secrets"],
|
|
18263
19192
|
handler: createConfigHandler()
|
|
18264
19193
|
},
|
|
18265
19194
|
{
|
|
@@ -18381,9 +19310,7 @@ function buildHelpText(registry = COMMAND_REGISTRY, experimentalSyncEnabled = fa
|
|
|
18381
19310
|
"Available command groups:"
|
|
18382
19311
|
];
|
|
18383
19312
|
for (const command of visibleCommands) {
|
|
18384
|
-
lines.push(
|
|
18385
|
-
` ${command.name.padEnd(12)} ${command.description}${formatSubcommands(command.subcommands)}`
|
|
18386
|
-
);
|
|
19313
|
+
lines.push(` ${command.name.padEnd(12)} ${command.description}${formatSubcommands(command.subcommands)}`);
|
|
18387
19314
|
}
|
|
18388
19315
|
lines.push(
|
|
18389
19316
|
"",
|