@automatify-au/cli 0.1.14 → 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 +17 -3
- package/dist/automatify.cjs +679 -139
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,15 +77,25 @@ Minimum:
|
|
|
77
77
|
- `TESTOPS_FORGE_ENDPOINT` for commands that call Forge contracts (`doctor`, `ingest`, `run`, `sync`)
|
|
78
78
|
- `TESTOPS_FORGE_AUTH_TOKEN` from the one-time token shown in Forge `Operations -> CLI access`
|
|
79
79
|
|
|
80
|
-
Local
|
|
80
|
+
Local Jira auth helper on macOS/Windows:
|
|
81
|
+
```bash
|
|
82
|
+
printf "%s" "$JIRA_API_TOKEN" | automatify testops auth login \
|
|
83
|
+
--base-url https://automatify-com-au.atlassian.net \
|
|
84
|
+
--jira-email you@example.com \
|
|
85
|
+
--token-stdin
|
|
86
|
+
automatify testops auth status
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
On Linux/CI, keep `JIRA_API_TOKEN` in the environment and run `auth login` without `--token-stdin`; the CLI stores only non-secret Jira settings. Use `automatify testops auth logout` to remove local Jira and Forge credentials while preserving non-secret URL/email settings. Environment variables are not modified by logout.
|
|
90
|
+
|
|
91
|
+
Forge transport auth remains an advanced setting:
|
|
81
92
|
```bash
|
|
82
|
-
automatify testops config set baseUrl https://automatify-com-au.atlassian.net
|
|
83
93
|
automatify testops config set projectKey DEV
|
|
84
94
|
automatify testops config set forgeEndpoint "<webtrigger-url>"
|
|
85
95
|
printf "%s" "$TESTOPS_FORGE_AUTH_TOKEN" | automatify testops config set forgeAuthToken --stdin
|
|
86
96
|
```
|
|
87
97
|
|
|
88
|
-
|
|
98
|
+
Jira and Forge secrets use macOS Keychain on macOS and current-user Windows DPAPI on Windows. Linux and CI keep secrets in `JIRA_API_TOKEN` and `TESTOPS_FORGE_AUTH_TOKEN`. Existing plaintext local tokens remain readable for compatibility and can be migrated with `automatify testops config migrate-secrets`. See [AUTH.md](./AUTH.md) for the normal Jira auth workflow and [WINDOWS.md](./WINDOWS.md) for PowerShell setup and verification.
|
|
89
99
|
|
|
90
100
|
Optional:
|
|
91
101
|
- `TESTOPS_FORGE_TIMEOUT_MS`
|
|
@@ -114,6 +124,10 @@ All TestOps commands are invoked as `automatify testops <command>`:
|
|
|
114
124
|
- `automatify testops bdd features export --output-dir ./features-export --feature-id <FEATURE_ID>`
|
|
115
125
|
- `runs`
|
|
116
126
|
- `automatify testops runs show --id <RUN_ID>`
|
|
127
|
+
- `auth`
|
|
128
|
+
- `automatify testops auth login --base-url <JIRA_URL> --jira-email <EMAIL> --token-stdin`
|
|
129
|
+
- `automatify testops auth status`
|
|
130
|
+
- `automatify testops auth logout`
|
|
117
131
|
- `config`
|
|
118
132
|
- `automatify testops config show`
|
|
119
133
|
- `automatify testops config validate`
|
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,6 +10983,27 @@ 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");
|
|
@@ -10833,63 +11023,46 @@ function readAuthMode(input) {
|
|
|
10833
11023
|
function defaultKeychainAccount(configPath) {
|
|
10834
11024
|
return import_node_path.default.resolve(configPath);
|
|
10835
11025
|
}
|
|
10836
|
-
function
|
|
10837
|
-
|
|
10838
|
-
|
|
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" };
|
|
10839
11032
|
}
|
|
10840
|
-
|
|
10841
|
-
return
|
|
10842
|
-
encoding: "utf8",
|
|
10843
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
10844
|
-
}).trim();
|
|
10845
|
-
} catch {
|
|
10846
|
-
return "";
|
|
11033
|
+
if (envValue) {
|
|
11034
|
+
return { value: envValue, source: "env" };
|
|
10847
11035
|
}
|
|
10848
|
-
|
|
10849
|
-
|
|
10850
|
-
if (process.platform !== "darwin") {
|
|
10851
|
-
throw new Error("Keychain-backed secrets are only supported on macOS.");
|
|
11036
|
+
if (fileValue) {
|
|
11037
|
+
return { value: fileValue, source: "file" };
|
|
10852
11038
|
}
|
|
10853
|
-
(
|
|
10854
|
-
|
|
10855
|
-
}
|
|
11039
|
+
if (fallback) {
|
|
11040
|
+
return { value: fallback, source: "default" };
|
|
11041
|
+
}
|
|
11042
|
+
return { value: "", source: "default" };
|
|
10856
11043
|
}
|
|
10857
|
-
function
|
|
10858
|
-
|
|
10859
|
-
|
|
10860
|
-
"$plain = [Console]::In.ReadToEnd()",
|
|
10861
|
-
"$bytes = [Text.Encoding]::UTF8.GetBytes($plain)",
|
|
10862
|
-
"$cipher = [System.Security.Cryptography.ProtectedData]::Protect($bytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)",
|
|
10863
|
-
"[Console]::Out.Write([Convert]::ToBase64String($cipher))"
|
|
10864
|
-
].join("; ");
|
|
10865
|
-
return (0, import_node_child_process.execFileSync)("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
10866
|
-
input: value,
|
|
10867
|
-
encoding: "utf8",
|
|
10868
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
10869
|
-
}).trim();
|
|
10870
|
-
}
|
|
10871
|
-
function unprotectWindowsSecret(protectedValue) {
|
|
10872
|
-
if (!protectedValue || process.platform !== "win32") {
|
|
10873
|
-
return "";
|
|
11044
|
+
function secureAccountForKey(key, file, configPath) {
|
|
11045
|
+
if (key === "forgeAuthToken") {
|
|
11046
|
+
return toStringValue(file.forgeAuthTokenKeychainAccount) || defaultKeychainAccount(configPath);
|
|
10874
11047
|
}
|
|
10875
|
-
|
|
10876
|
-
|
|
10877
|
-
|
|
10878
|
-
|
|
10879
|
-
|
|
10880
|
-
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
return (0, import_node_child_process.execFileSync)("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
10884
|
-
input: protectedValue,
|
|
10885
|
-
encoding: "utf8",
|
|
10886
|
-
stdio: ["pipe", "pipe", "ignore"]
|
|
10887
|
-
}).trim();
|
|
10888
|
-
} catch {
|
|
10889
|
-
return "";
|
|
11048
|
+
return toStringValue(file.jiraApiTokenKeychainAccount) || defaultJiraApiTokenKeychainAccount(configPath);
|
|
11049
|
+
}
|
|
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
|
+
};
|
|
10890
11056
|
}
|
|
11057
|
+
return {
|
|
11058
|
+
account: toStringValue(file.jiraApiTokenKeychainAccount) || void 0,
|
|
11059
|
+
protectedValue: toStringValue(file.jiraApiTokenProtected) || void 0
|
|
11060
|
+
};
|
|
10891
11061
|
}
|
|
10892
|
-
function
|
|
11062
|
+
function secretEnvironmentName(key) {
|
|
11063
|
+
return key === "jiraApiToken" ? "JIRA_API_TOKEN" : "TESTOPS_FORGE_AUTH_TOKEN";
|
|
11064
|
+
}
|
|
11065
|
+
function resolveSecretValue(key, flagValue, envValue, fileValue, file) {
|
|
10893
11066
|
if (flagValue) {
|
|
10894
11067
|
return { value: flagValue, source: "flag" };
|
|
10895
11068
|
}
|
|
@@ -10899,11 +11072,33 @@ function valueFromPrecedence(key, flagValue, envValue, fileValue, fallback = "")
|
|
|
10899
11072
|
if (fileValue) {
|
|
10900
11073
|
return { value: fileValue, source: "file" };
|
|
10901
11074
|
}
|
|
10902
|
-
|
|
10903
|
-
|
|
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}` };
|
|
10904
11081
|
}
|
|
10905
11082
|
return { value: "", source: "default" };
|
|
10906
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
|
+
}
|
|
10907
11102
|
function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
|
|
10908
11103
|
const { flags, unknownFlags } = parseFlags(args);
|
|
10909
11104
|
const configPathFlag = flags["--config"] ? import_node_path.default.resolve(cwd, flags["--config"]) : "";
|
|
@@ -10942,11 +11137,12 @@ function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
|
|
|
10942
11137
|
toStringValue(env.JIRA_EMAIL),
|
|
10943
11138
|
toStringValue(file.jiraEmail ?? file.JIRA_EMAIL)
|
|
10944
11139
|
);
|
|
10945
|
-
const jiraApiTokenResolved =
|
|
11140
|
+
const jiraApiTokenResolved = resolveSecretValue(
|
|
10946
11141
|
"jiraApiToken",
|
|
10947
11142
|
toStringValue(flags["--jira-api-token"]),
|
|
10948
11143
|
toStringValue(env.JIRA_API_TOKEN),
|
|
10949
|
-
|
|
11144
|
+
firstStringValue(file.jiraApiToken, file.JIRA_API_TOKEN),
|
|
11145
|
+
file
|
|
10950
11146
|
);
|
|
10951
11147
|
const forgeEndpointResolved = valueFromPrecedence(
|
|
10952
11148
|
"forgeEndpoint",
|
|
@@ -10960,11 +11156,13 @@ function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
|
|
|
10960
11156
|
"",
|
|
10961
11157
|
toStringValue(file.forgeAuthTokenKeychainAccount)
|
|
10962
11158
|
);
|
|
10963
|
-
const
|
|
10964
|
-
|
|
10965
|
-
|
|
10966
|
-
|
|
10967
|
-
|
|
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
|
+
);
|
|
10968
11166
|
const config = {
|
|
10969
11167
|
baseUrl: normalizeBaseUrl(baseUrlResolved.value),
|
|
10970
11168
|
projectKey: projectKeyResolved.value,
|
|
@@ -10990,13 +11188,16 @@ function resolveCliConfig(args, env = process.env, cwd = process.cwd()) {
|
|
|
10990
11188
|
configPath: configPathSource
|
|
10991
11189
|
};
|
|
10992
11190
|
const warnings = [];
|
|
11191
|
+
const errors = [jiraApiTokenResolved.error, forgeAuthTokenResolved.error].filter(
|
|
11192
|
+
(value) => Boolean(value)
|
|
11193
|
+
);
|
|
10993
11194
|
if (unknownFlags.length > 0) {
|
|
10994
11195
|
warnings.push(`Ignored unknown config flags: ${unknownFlags.join(", ")}`);
|
|
10995
11196
|
}
|
|
10996
11197
|
if (!(0, import_node_fs.existsSync)(configPath) && sources.configPath !== "default") {
|
|
10997
11198
|
warnings.push(`Config file not found at ${configPath}; using env/flags/defaults.`);
|
|
10998
11199
|
}
|
|
10999
|
-
return { values: config, sources, warnings };
|
|
11200
|
+
return { values: config, sources, warnings, errors };
|
|
11000
11201
|
}
|
|
11001
11202
|
function maskSecret(value) {
|
|
11002
11203
|
if (!value) {
|
|
@@ -11009,7 +11210,7 @@ function maskSecret(value) {
|
|
|
11009
11210
|
}
|
|
11010
11211
|
function validateResolvedConfig(resolution) {
|
|
11011
11212
|
const { values } = resolution;
|
|
11012
|
-
const errors = [];
|
|
11213
|
+
const errors = [...resolution.errors];
|
|
11013
11214
|
const warnings = [...resolution.warnings];
|
|
11014
11215
|
if (!values.baseUrl) {
|
|
11015
11216
|
errors.push("Missing required setting: JIRA_BASE_URL (flag/env/file).");
|
|
@@ -11024,10 +11225,8 @@ function validateResolvedConfig(resolution) {
|
|
|
11024
11225
|
if (!values.jiraApiToken) {
|
|
11025
11226
|
errors.push("Auth mode api-token requires JIRA_API_TOKEN.");
|
|
11026
11227
|
}
|
|
11027
|
-
} else {
|
|
11028
|
-
|
|
11029
|
-
warnings.push("JIRA_EMAIL/JIRA_API_TOKEN were provided but auth mode is 'none'.");
|
|
11030
|
-
}
|
|
11228
|
+
} else if (values.jiraApiToken || values.jiraEmail) {
|
|
11229
|
+
warnings.push("JIRA_EMAIL/JIRA_API_TOKEN were provided but auth mode is 'none'.");
|
|
11031
11230
|
}
|
|
11032
11231
|
return { ok: errors.length === 0, errors, warnings };
|
|
11033
11232
|
}
|
|
@@ -11090,53 +11289,33 @@ function applyConfigSet(configPath, rawKey, value) {
|
|
|
11090
11289
|
]
|
|
11091
11290
|
};
|
|
11092
11291
|
}
|
|
11093
|
-
const
|
|
11094
|
-
if (
|
|
11095
|
-
|
|
11096
|
-
|
|
11097
|
-
|
|
11098
|
-
|
|
11099
|
-
|
|
11100
|
-
|
|
11101
|
-
|
|
11102
|
-
|
|
11103
|
-
|
|
11104
|
-
|
|
11105
|
-
|
|
11106
|
-
|
|
11107
|
-
stdout: ["Config updated: forgeAuthToken protected with Windows DPAPI for the current user."]
|
|
11108
|
-
};
|
|
11109
|
-
} catch (error) {
|
|
11110
|
-
return {
|
|
11111
|
-
exitCode: ExitCode.ValidationError,
|
|
11112
|
-
stderr: [
|
|
11113
|
-
`Unable to store forgeAuthToken securely: ${error instanceof Error ? error.message : "unknown secure-storage error"}`
|
|
11114
|
-
]
|
|
11115
|
-
};
|
|
11116
|
-
}
|
|
11117
|
-
}
|
|
11118
|
-
if (process.platform !== "darwin") {
|
|
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";
|
|
11119
11306
|
return {
|
|
11120
|
-
exitCode: ExitCode.
|
|
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,
|
|
11121
11314
|
stderr: [
|
|
11122
|
-
|
|
11315
|
+
`Unable to store ${secretKey} securely: ${error instanceof Error ? error.message : "unknown secure-storage error"} Use ${secretEnvironmentName(secretKey)} on Linux/CI.`
|
|
11123
11316
|
]
|
|
11124
11317
|
};
|
|
11125
11318
|
}
|
|
11126
|
-
const account = toStringValue(file.forgeAuthTokenKeychainAccount) || defaultKeychainAccount(configPath);
|
|
11127
|
-
setKeychainSecret(account, value);
|
|
11128
|
-
const nextFile2 = {
|
|
11129
|
-
...file,
|
|
11130
|
-
forgeAuthToken: void 0,
|
|
11131
|
-
TESTOPS_FORGE_AUTH_TOKEN: void 0,
|
|
11132
|
-
forgeAuthTokenProtected: void 0,
|
|
11133
|
-
forgeAuthTokenKeychainAccount: account
|
|
11134
|
-
};
|
|
11135
|
-
writeConfigFile(configPath, nextFile2);
|
|
11136
|
-
return {
|
|
11137
|
-
exitCode: ExitCode.Success,
|
|
11138
|
-
stdout: [`Config updated: forgeAuthToken stored in macOS Keychain (${account}).`]
|
|
11139
|
-
};
|
|
11140
11319
|
}
|
|
11141
11320
|
const nextFile = {
|
|
11142
11321
|
...file,
|
|
@@ -11148,6 +11327,101 @@ function applyConfigSet(configPath, rawKey, value) {
|
|
|
11148
11327
|
stdout: [`Config updated: ${rawKey}.`]
|
|
11149
11328
|
};
|
|
11150
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
|
+
}
|
|
11151
11425
|
function createConfigHandler(env = process.env, cwd = process.cwd()) {
|
|
11152
11426
|
return (request) => {
|
|
11153
11427
|
const [subcommand, ...subArgs] = request.args;
|
|
@@ -11156,7 +11430,11 @@ function createConfigHandler(env = process.env, cwd = process.cwd()) {
|
|
|
11156
11430
|
if (subcommand === "show") {
|
|
11157
11431
|
return {
|
|
11158
11432
|
exitCode: ExitCode.Success,
|
|
11159
|
-
stdout: [
|
|
11433
|
+
stdout: [
|
|
11434
|
+
...toDisplayLines(resolution),
|
|
11435
|
+
...validation.warnings.map((line) => `WARN: ${line}`),
|
|
11436
|
+
...resolution.errors.map((line) => `ERROR: ${line}`)
|
|
11437
|
+
]
|
|
11160
11438
|
};
|
|
11161
11439
|
}
|
|
11162
11440
|
if (subcommand === "validate") {
|
|
@@ -11177,17 +11455,25 @@ function createConfigHandler(env = process.env, cwd = process.cwd()) {
|
|
|
11177
11455
|
};
|
|
11178
11456
|
}
|
|
11179
11457
|
if (subcommand === "set") {
|
|
11180
|
-
const [key, ...valueParts] = subArgs;
|
|
11181
|
-
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";
|
|
11182
11461
|
const value = readValueFromStdin ? (0, import_node_fs.readFileSync)(0, "utf8").trim() : valueParts.join(" ").trim();
|
|
11183
11462
|
if (!key || !value) {
|
|
11184
11463
|
return {
|
|
11185
11464
|
exitCode: ExitCode.UsageError,
|
|
11186
|
-
stderr: ["Usage: automatify testops config set <key> <value>"]
|
|
11465
|
+
stderr: ["Usage: automatify testops config set <key> <value|--stdin>"]
|
|
11187
11466
|
};
|
|
11188
11467
|
}
|
|
11189
11468
|
return applyConfigSet(resolution.values.configPath, key, value);
|
|
11190
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
|
+
}
|
|
11191
11477
|
return {
|
|
11192
11478
|
exitCode: ExitCode.UsageError,
|
|
11193
11479
|
stderr: [`Unsupported config subcommand: ${subcommand}`]
|
|
@@ -13627,8 +13913,255 @@ function createAllureHandler(deps = {}) {
|
|
|
13627
13913
|
};
|
|
13628
13914
|
}
|
|
13629
13915
|
|
|
13630
|
-
// src/
|
|
13916
|
+
// src/auth.ts
|
|
13631
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
|
+
|
|
14163
|
+
// src/bdd.ts
|
|
14164
|
+
var import_node_fs11 = require("node:fs");
|
|
13632
14165
|
var import_node_path11 = __toESM(require("node:path"), 1);
|
|
13633
14166
|
var import_yazl = __toESM(require_yazl(), 1);
|
|
13634
14167
|
|
|
@@ -14053,7 +14586,7 @@ function toJsonExportManifestItems(items) {
|
|
|
14053
14586
|
async function defaultCreateZipArchive(archivePath, entries) {
|
|
14054
14587
|
await new Promise((resolve, reject) => {
|
|
14055
14588
|
const zip = new import_yazl.ZipFile();
|
|
14056
|
-
const output = zip.outputStream.pipe((0,
|
|
14589
|
+
const output = zip.outputStream.pipe((0, import_node_fs11.createWriteStream)(archivePath));
|
|
14057
14590
|
output.on("close", () => resolve());
|
|
14058
14591
|
output.on("error", reject);
|
|
14059
14592
|
zip.outputStream.on("error", reject);
|
|
@@ -14083,10 +14616,10 @@ function missingProjectResponse() {
|
|
|
14083
14616
|
function createBddHandler(deps = {}) {
|
|
14084
14617
|
const cwd = deps.cwd ?? process.cwd();
|
|
14085
14618
|
const env = deps.env ?? process.env;
|
|
14086
|
-
const mkdir = deps.mkdir ?? ((targetPath, options) => (0,
|
|
14087
|
-
const readFile = deps.readFile ?? ((filePath) => (0,
|
|
14088
|
-
const readDir = deps.readDir ?? ((dirPath) => (0,
|
|
14089
|
-
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"));
|
|
14090
14623
|
const createZipArchive = deps.createZipArchive ?? defaultCreateZipArchive;
|
|
14091
14624
|
return async (request, context) => {
|
|
14092
14625
|
const [subcommand, ...restArgs] = request.args;
|
|
@@ -15179,9 +15712,10 @@ function resolveAllureJiraConfig(parsed, env, cwd) {
|
|
|
15179
15712
|
const resolution = resolveCliConfig(pickConfigArgs5(parsed), env, cwd);
|
|
15180
15713
|
const site = normalizeSite3(parsed.flags["--site"] ?? env.JIRA_SITE ?? resolution.values.baseUrl);
|
|
15181
15714
|
const email = (parsed.flags["--email"] ?? resolution.values.jiraEmail).trim();
|
|
15182
|
-
const
|
|
15715
|
+
const directApiToken = (parsed.flags["--api-token"] ?? "").trim();
|
|
15716
|
+
const apiToken = (directApiToken || resolution.values.jiraApiToken).trim();
|
|
15183
15717
|
const issueKey = (parsed.flags["--issue-key"] ?? "").trim();
|
|
15184
|
-
const errors = [];
|
|
15718
|
+
const errors = directApiToken ? [] : resolution.errors.filter((line) => line.startsWith("jiraApiToken:"));
|
|
15185
15719
|
if (!hasValue(site)) {
|
|
15186
15720
|
errors.push("Missing Jira site. Pass --site, set JIRA_SITE/JIRA_BASE_URL, or configure baseUrl.");
|
|
15187
15721
|
}
|
|
@@ -15420,7 +15954,7 @@ function createDoctorHandler(deps = {}) {
|
|
|
15420
15954
|
}
|
|
15421
15955
|
|
|
15422
15956
|
// src/ingestFeature.ts
|
|
15423
|
-
var
|
|
15957
|
+
var import_node_fs12 = require("node:fs");
|
|
15424
15958
|
var import_node_path12 = __toESM(require("node:path"), 1);
|
|
15425
15959
|
var CONFIG_FLAGS6 = /* @__PURE__ */ new Set([
|
|
15426
15960
|
"--config",
|
|
@@ -15507,8 +16041,8 @@ function normalizeError5(error) {
|
|
|
15507
16041
|
return "Unknown ingest error.";
|
|
15508
16042
|
}
|
|
15509
16043
|
function createIngestFeatureHandler(deps = {}) {
|
|
15510
|
-
const readFile = deps.readFile ?? ((filePath) => (0,
|
|
15511
|
-
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"));
|
|
15512
16046
|
const cwd = deps.cwd ?? process.cwd();
|
|
15513
16047
|
const env = deps.env ?? process.env;
|
|
15514
16048
|
return async (request, context) => {
|
|
@@ -15639,7 +16173,7 @@ function createIngestFeatureHandler(deps = {}) {
|
|
|
15639
16173
|
}
|
|
15640
16174
|
|
|
15641
16175
|
// src/runUpload.ts
|
|
15642
|
-
var
|
|
16176
|
+
var import_node_fs13 = require("node:fs");
|
|
15643
16177
|
var import_node_path13 = __toESM(require("node:path"), 1);
|
|
15644
16178
|
var STEP_RESULTS = [
|
|
15645
16179
|
StepResult.Passed,
|
|
@@ -15781,8 +16315,8 @@ function summarizeRunResult(result) {
|
|
|
15781
16315
|
return lines;
|
|
15782
16316
|
}
|
|
15783
16317
|
function createRunUploadHandler(deps = {}) {
|
|
15784
|
-
const readFile = deps.readFile ?? ((filePath) => (0,
|
|
15785
|
-
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"));
|
|
15786
16320
|
const cwd = deps.cwd ?? process.cwd();
|
|
15787
16321
|
const env = deps.env ?? process.env;
|
|
15788
16322
|
return async (request, context) => {
|
|
@@ -16127,7 +16661,7 @@ function createRunsHandler(deps = {}) {
|
|
|
16127
16661
|
}
|
|
16128
16662
|
|
|
16129
16663
|
// src/setup.ts
|
|
16130
|
-
var
|
|
16664
|
+
var import_node_fs15 = require("node:fs");
|
|
16131
16665
|
var import_node_path15 = __toESM(require("node:path"), 1);
|
|
16132
16666
|
|
|
16133
16667
|
// src/githubSetupProvider.ts
|
|
@@ -16135,7 +16669,7 @@ var import_node_child_process4 = require("node:child_process");
|
|
|
16135
16669
|
|
|
16136
16670
|
// src/setupPlan.ts
|
|
16137
16671
|
var import_node_crypto = require("node:crypto");
|
|
16138
|
-
var
|
|
16672
|
+
var import_node_fs14 = require("node:fs");
|
|
16139
16673
|
var import_node_path14 = __toESM(require("node:path"), 1);
|
|
16140
16674
|
var SETUP_PLAN_SCHEMA_VERSION = "automatify.testops.setup/v1";
|
|
16141
16675
|
var SETUP_PLAN_KIND = "AutomatifyTestOpsSetupPlan";
|
|
@@ -16231,7 +16765,7 @@ function buildGitHubSetupPlan(input) {
|
|
|
16231
16765
|
if (endpointSecretName === tokenSecretName) {
|
|
16232
16766
|
throw new Error("callback endpoint and auth token secret names must be different.");
|
|
16233
16767
|
}
|
|
16234
|
-
const workflowContent = (0,
|
|
16768
|
+
const workflowContent = (0, import_node_fs14.readFileSync)(input.workflowSourcePath, "utf8");
|
|
16235
16769
|
if (!workflowContent.trim()) {
|
|
16236
16770
|
throw new Error("workflow source file is empty.");
|
|
16237
16771
|
}
|
|
@@ -17177,7 +17711,7 @@ function loadPlan(planPath, cwd) {
|
|
|
17177
17711
|
const absolute = import_node_path15.default.resolve(cwd, planPath);
|
|
17178
17712
|
let parsed;
|
|
17179
17713
|
try {
|
|
17180
|
-
parsed = JSON.parse((0,
|
|
17714
|
+
parsed = JSON.parse((0, import_node_fs15.readFileSync)(absolute, "utf8"));
|
|
17181
17715
|
} catch (error) {
|
|
17182
17716
|
throw new SetupCommandError(
|
|
17183
17717
|
"PLAN_READ_ERROR",
|
|
@@ -17416,7 +17950,7 @@ function resolveWorkflowTarget(plan, repoRoot) {
|
|
|
17416
17950
|
function applyWorkflowFile(plan, repoRoot, dryRun) {
|
|
17417
17951
|
const action = plan.actions.find((item) => item.scope === "workflow-file");
|
|
17418
17952
|
const target = resolveWorkflowTarget(plan, repoRoot);
|
|
17419
|
-
const sourceContent = (0,
|
|
17953
|
+
const sourceContent = (0, import_node_fs15.readFileSync)(plan.github.workflowFile.sourcePath, "utf8");
|
|
17420
17954
|
if (hashSetupContent(sourceContent) !== plan.github.workflowFile.sha256) {
|
|
17421
17955
|
throw new SetupCommandError(
|
|
17422
17956
|
"WORKFLOW_SOURCE_CHANGED",
|
|
@@ -17424,8 +17958,8 @@ function applyWorkflowFile(plan, repoRoot, dryRun) {
|
|
|
17424
17958
|
ExitCode.ValidationError
|
|
17425
17959
|
);
|
|
17426
17960
|
}
|
|
17427
|
-
const existed = (0,
|
|
17428
|
-
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;
|
|
17429
17963
|
const matches = previousContent !== void 0 && hashSetupContent(previousContent) === plan.github.workflowFile.sha256;
|
|
17430
17964
|
if (matches) {
|
|
17431
17965
|
return {
|
|
@@ -17437,8 +17971,8 @@ function applyWorkflowFile(plan, repoRoot, dryRun) {
|
|
|
17437
17971
|
};
|
|
17438
17972
|
}
|
|
17439
17973
|
if (!dryRun) {
|
|
17440
|
-
(0,
|
|
17441
|
-
(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 });
|
|
17442
17976
|
}
|
|
17443
17977
|
return {
|
|
17444
17978
|
id: action.id,
|
|
@@ -17608,14 +18142,14 @@ async function inspectPlan(plan, repoRoot, secrets, context, deps) {
|
|
|
17608
18142
|
const checks = [];
|
|
17609
18143
|
let forgeTransportFailure = false;
|
|
17610
18144
|
const target = resolveWorkflowTarget(plan, repoRoot);
|
|
17611
|
-
if (!(0,
|
|
18145
|
+
if (!(0, import_node_fs15.existsSync)(target)) {
|
|
17612
18146
|
checks.push({
|
|
17613
18147
|
id: "workflow-local",
|
|
17614
18148
|
status: "fail",
|
|
17615
18149
|
message: `Local workflow file ${plan.github.workflowFile.path} does not exist.`
|
|
17616
18150
|
});
|
|
17617
18151
|
} else {
|
|
17618
|
-
const localHash = hashSetupContent((0,
|
|
18152
|
+
const localHash = hashSetupContent((0, import_node_fs15.readFileSync)(target, "utf8"));
|
|
17619
18153
|
checks.push({
|
|
17620
18154
|
id: "workflow-local",
|
|
17621
18155
|
status: localHash === plan.github.workflowFile.sha256 ? "pass" : "fail",
|
|
@@ -18626,6 +19160,12 @@ var COMMAND_REGISTRY = [
|
|
|
18626
19160
|
subcommands: ["upload", "download", "open"],
|
|
18627
19161
|
handler: createAllureHandler()
|
|
18628
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
|
+
},
|
|
18629
19169
|
{
|
|
18630
19170
|
name: "auto",
|
|
18631
19171
|
description: "Smart auto-ingestion command contract (MVP staged)",
|
|
@@ -18648,7 +19188,7 @@ var COMMAND_REGISTRY = [
|
|
|
18648
19188
|
{
|
|
18649
19189
|
name: "config",
|
|
18650
19190
|
description: "Configuration and auth bootstrap commands",
|
|
18651
|
-
subcommands: ["show", "validate", "set"],
|
|
19191
|
+
subcommands: ["show", "validate", "set", "unset", "migrate-secrets"],
|
|
18652
19192
|
handler: createConfigHandler()
|
|
18653
19193
|
},
|
|
18654
19194
|
{
|