qualty 0.1.26 → 0.1.29
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/bin/local-runner.js +11 -7
- package/bin/qualty.js +307 -20
- package/package.json +1 -1
package/bin/local-runner.js
CHANGED
|
@@ -1374,8 +1374,9 @@ async function runLocalSingleCombination({
|
|
|
1374
1374
|
explicitAuthProfile: String(authProfile || "").trim(),
|
|
1375
1375
|
noAuthState: Boolean(noAuthState),
|
|
1376
1376
|
});
|
|
1377
|
+
const explicitAuthProfile = String(authProfile || "").trim();
|
|
1377
1378
|
const claimProfile = localProfileLabel(claimSavedLogin);
|
|
1378
|
-
const effectiveProfile =
|
|
1379
|
+
const effectiveProfile = explicitAuthProfile || setupFromClaim?.profile || claimProfile;
|
|
1379
1380
|
const resolvedStoragePath =
|
|
1380
1381
|
!noAuthState && effectiveProfile
|
|
1381
1382
|
? localAuthStatePath({
|
|
@@ -1387,17 +1388,20 @@ async function runLocalSingleCombination({
|
|
|
1387
1388
|
const finalStoragePath =
|
|
1388
1389
|
!noAuthState && setupFromClaim?.path && existsSync(setupFromClaim.path)
|
|
1389
1390
|
? setupFromClaim.path
|
|
1390
|
-
: !noAuthState &&
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1391
|
+
: !noAuthState &&
|
|
1392
|
+
explicitAuthProfile &&
|
|
1393
|
+
storageStatePath &&
|
|
1394
|
+
existsSync(storageStatePath)
|
|
1395
|
+
? storageStatePath
|
|
1396
|
+
: !noAuthState && resolvedStoragePath && existsSync(resolvedStoragePath)
|
|
1397
|
+
? resolvedStoragePath
|
|
1398
|
+
: null;
|
|
1395
1399
|
|
|
1396
1400
|
let authSource = "none";
|
|
1397
1401
|
if (!noAuthState && finalStoragePath) {
|
|
1398
1402
|
if (setupFromClaim?.path && finalStoragePath === setupFromClaim.path) {
|
|
1399
1403
|
authSource = "interactive setup during run";
|
|
1400
|
-
} else if (storageStatePath && finalStoragePath === storageStatePath) {
|
|
1404
|
+
} else if (explicitAuthProfile && storageStatePath && finalStoragePath === storageStatePath) {
|
|
1401
1405
|
authSource = "--auth-profile flag";
|
|
1402
1406
|
} else if (resolvedStoragePath && finalStoragePath === resolvedStoragePath) {
|
|
1403
1407
|
authSource = claimProfile ? "dashboard saved login profile" : "resolved profile path";
|
package/bin/qualty.js
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
const DEFAULT_QUALTY_API_URL = "https://qualty-api-production.up.railway.app";
|
|
31
31
|
|
|
32
32
|
const PROJECT_CONFIG_FILENAME = ".qualty.json";
|
|
33
|
+
const DOTENV_FILENAME = ".env";
|
|
33
34
|
const QUALTY_SHARED_CREDS_DIR = join(homedir(), ".qualty");
|
|
34
35
|
const QUALTY_SHARED_CREDS_PATH = join(QUALTY_SHARED_CREDS_DIR, "credentials");
|
|
35
36
|
|
|
@@ -48,6 +49,47 @@ function getProjectConfigWritePath() {
|
|
|
48
49
|
return findProjectConfigPath() || join(process.cwd(), PROJECT_CONFIG_FILENAME);
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
function findProjectDotenvPath() {
|
|
53
|
+
let current = process.cwd();
|
|
54
|
+
for (;;) {
|
|
55
|
+
const candidate = join(current, DOTENV_FILENAME);
|
|
56
|
+
if (existsSync(candidate)) return candidate;
|
|
57
|
+
const parent = dirname(current);
|
|
58
|
+
if (parent === current) return null;
|
|
59
|
+
current = parent;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseDotenvText(text) {
|
|
64
|
+
const out = {};
|
|
65
|
+
for (const raw of String(text || "").split("\n")) {
|
|
66
|
+
const line = raw.trim();
|
|
67
|
+
if (!line || line.startsWith("#") || !line.includes("=")) continue;
|
|
68
|
+
const eq = line.indexOf("=");
|
|
69
|
+
const key = line.slice(0, eq).trim();
|
|
70
|
+
let value = line.slice(eq + 1).trim();
|
|
71
|
+
if (
|
|
72
|
+
(value.startsWith('"') && value.endsWith('"'))
|
|
73
|
+
|| (value.startsWith("'") && value.endsWith("'"))
|
|
74
|
+
) {
|
|
75
|
+
value = value.slice(1, -1);
|
|
76
|
+
}
|
|
77
|
+
if (key) out[key] = value;
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Project .env walked up from cwd (same resolution as Qualty MCP). */
|
|
83
|
+
function loadProjectDotenv() {
|
|
84
|
+
const path = findProjectDotenvPath();
|
|
85
|
+
if (!path) return {};
|
|
86
|
+
try {
|
|
87
|
+
return parseDotenvText(readFileSync(path, "utf8"));
|
|
88
|
+
} catch {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
51
93
|
function readSharedCredentials() {
|
|
52
94
|
if (!existsSync(QUALTY_SHARED_CREDS_PATH)) return {};
|
|
53
95
|
try {
|
|
@@ -82,6 +124,11 @@ function authProfileFromConfig(projectCfg, fallback = "") {
|
|
|
82
124
|
return String(fallback || projectCfg.default_auth_profile || "").trim();
|
|
83
125
|
}
|
|
84
126
|
|
|
127
|
+
/** Only --auth-profile on the CLI; config default is not a run-wide override. */
|
|
128
|
+
function explicitAuthProfileFromArgs(args) {
|
|
129
|
+
return String(args["auth-profile"] || "").trim();
|
|
130
|
+
}
|
|
131
|
+
|
|
85
132
|
function readProjectConfig() {
|
|
86
133
|
const configPath = findProjectConfigPath();
|
|
87
134
|
if (!configPath) return {};
|
|
@@ -124,7 +171,14 @@ function normalizeApiUrl(raw, { label = "API URL" } = {}) {
|
|
|
124
171
|
}
|
|
125
172
|
|
|
126
173
|
function qualtyBaseUrlFromEnv() {
|
|
127
|
-
|
|
174
|
+
const dotenv = loadProjectDotenv();
|
|
175
|
+
return String(
|
|
176
|
+
dotenv.QUALTY_BASE_URL
|
|
177
|
+
|| dotenv.QUALTY_API_URL
|
|
178
|
+
|| process.env.QUALTY_BASE_URL
|
|
179
|
+
|| process.env.QUALTY_API_URL
|
|
180
|
+
|| ""
|
|
181
|
+
).trim();
|
|
128
182
|
}
|
|
129
183
|
|
|
130
184
|
function resolveApiUrl(args = {}) {
|
|
@@ -142,8 +196,14 @@ function resolveApiUrl(args = {}) {
|
|
|
142
196
|
}
|
|
143
197
|
|
|
144
198
|
function resolveToken(args = {}) {
|
|
199
|
+
if (args.token) return String(args.token).trim();
|
|
200
|
+
const dotenv = loadProjectDotenv();
|
|
201
|
+
const fromDotenv = String(dotenv.QUALTY_API_TOKEN || "").trim();
|
|
202
|
+
if (fromDotenv) return fromDotenv;
|
|
203
|
+
const fromProcessEnv = String(process.env.QUALTY_API_TOKEN || "").trim();
|
|
204
|
+
if (fromProcessEnv) return fromProcessEnv;
|
|
145
205
|
const creds = readSharedCredentials();
|
|
146
|
-
return
|
|
206
|
+
return String(creds.api_token || "").trim();
|
|
147
207
|
}
|
|
148
208
|
|
|
149
209
|
function resolveOrgId(args = {}) {
|
|
@@ -187,6 +247,10 @@ function usage() {
|
|
|
187
247
|
"Usage:",
|
|
188
248
|
" qualty setup [--token <bearer-token>] [--org <org-id>]",
|
|
189
249
|
" qualty auth setup [--project <project-id>] [--profile <profile-name>] [--headed]",
|
|
250
|
+
" qualty auth export [--project <project-id>] [--profile <profile-name>] [--org <org-id>]",
|
|
251
|
+
" [--profiles <name1,name2>] [--repo <owner/repo>]",
|
|
252
|
+
" [--output <path>] [--no-file] [--copy] [--set-secret | --set-github]",
|
|
253
|
+
" Push login profile state to GitHub only (not API token, org, or project vars).",
|
|
190
254
|
" qualty ci setup [--project <project-id>] [--profile <profile-name>] [--org <org-id>]",
|
|
191
255
|
" [--all-profiles] [--profiles <name1,name2>]",
|
|
192
256
|
" [--suite-id <suite-uuid>] [--ids <id1,id2>] bundle login profiles required by tests",
|
|
@@ -206,11 +270,11 @@ function usage() {
|
|
|
206
270
|
" qualty resolve --project <project-id> [--suite-id <suite-id>] [--ids <id1,id2>] [--json] [--token <bearer-token>]",
|
|
207
271
|
"",
|
|
208
272
|
"Env vars:",
|
|
209
|
-
" QUALTY_API_TOKEN
|
|
273
|
+
" QUALTY_API_TOKEN Project .env first (same as MCP), then process env, then ~/.qualty/credentials",
|
|
210
274
|
" QUALTY_BASE_URL API base URL when --url is not set (default: production)",
|
|
211
275
|
" QUALTY_LOCAL_CONCURRENCY Local parallel workers for --local runs (default: 4)",
|
|
212
276
|
"",
|
|
213
|
-
"Config (.qualty.json): default_org_id, default_project_id, default_auth_profile",
|
|
277
|
+
"Config (.qualty.json): default_org_id, default_project_id, default_auth_profile (ci/auth setup only; local runs use each test's saved login profile unless --auth-profile is set)",
|
|
214
278
|
" CLI flags (--org, --project, --profile, --url) override config values.",
|
|
215
279
|
].join("\n")
|
|
216
280
|
);
|
|
@@ -393,9 +457,7 @@ async function runSetup(args) {
|
|
|
393
457
|
|
|
394
458
|
try {
|
|
395
459
|
const currentOrgId = String(args.org || projectCfg.default_org_id || "");
|
|
396
|
-
const currentToken =
|
|
397
|
-
args.token || process.env.QUALTY_API_TOKEN || sharedCreds.api_token || ""
|
|
398
|
-
);
|
|
460
|
+
const currentToken = resolveToken(args);
|
|
399
461
|
|
|
400
462
|
let token = "";
|
|
401
463
|
if (args.token) {
|
|
@@ -674,6 +736,37 @@ function syncGithubCiConfig({
|
|
|
674
736
|
return results;
|
|
675
737
|
}
|
|
676
738
|
|
|
739
|
+
/**
|
|
740
|
+
* Push only auth bundle secrets and auth-related repository variables (no API token or org/project).
|
|
741
|
+
*/
|
|
742
|
+
function syncGithubAuthProfileOnly({ repo, profile, profileNames, authShards }) {
|
|
743
|
+
const results = [];
|
|
744
|
+
const record = (kind, name, outcome) => {
|
|
745
|
+
results.push({ kind, name, ...outcome });
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
for (const shard of authShards || []) {
|
|
749
|
+
record("secret", shard.secretName, setGithubSecret(shard.secretName, shard.payload, { repo }));
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const names = (profileNames || []).filter(Boolean);
|
|
753
|
+
if (names.length > 0) {
|
|
754
|
+
record("variable", AUTH_PROFILES_VARIABLE, setGithubVariable(AUTH_PROFILES_VARIABLE, names.join(","), { repo }));
|
|
755
|
+
}
|
|
756
|
+
if (profile) {
|
|
757
|
+
record("variable", "QUALTY_AUTH_PROFILE", setGithubVariable("QUALTY_AUTH_PROFILE", profile, { repo }));
|
|
758
|
+
}
|
|
759
|
+
if ((authShards || []).length > 1) {
|
|
760
|
+
record(
|
|
761
|
+
"variable",
|
|
762
|
+
AUTH_SHARDS_VARIABLE,
|
|
763
|
+
setGithubVariable(AUTH_SHARDS_VARIABLE, String(authShards.length), { repo })
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return results;
|
|
768
|
+
}
|
|
769
|
+
|
|
677
770
|
function printGithubSyncResults(results, { repo }) {
|
|
678
771
|
const target = repo ? ` (repo ${repo})` : "";
|
|
679
772
|
const printGroup = (title, rows) => {
|
|
@@ -1033,7 +1126,7 @@ async function runAuthRestoreCi(args) {
|
|
|
1033
1126
|
const payloads = collectAuthShardPayloadsFromEnv(process.env);
|
|
1034
1127
|
if (payloads.length === 0) {
|
|
1035
1128
|
throw new Error(
|
|
1036
|
-
`Missing ${AUTH_SECRET_PRIMARY}. Run qualty ci setup --set-github
|
|
1129
|
+
`Missing ${AUTH_SECRET_PRIMARY}. Run qualty ci setup --set-github (first time) or qualty auth export --set-github (profile refresh).`
|
|
1037
1130
|
);
|
|
1038
1131
|
}
|
|
1039
1132
|
|
|
@@ -1052,6 +1145,197 @@ async function runAuthRestoreCi(args) {
|
|
|
1052
1145
|
console.log(`Restored ${restored.length} login profile(s).`);
|
|
1053
1146
|
}
|
|
1054
1147
|
|
|
1148
|
+
async function runAuthExport(args) {
|
|
1149
|
+
if (
|
|
1150
|
+
parseBoolean(args["all-profiles"], false) ||
|
|
1151
|
+
String(args["suite-id"] || args.suite || "").trim() ||
|
|
1152
|
+
String(args.ids || "")
|
|
1153
|
+
.split(",")
|
|
1154
|
+
.map((part) => part.trim())
|
|
1155
|
+
.filter(Boolean).length > 0
|
|
1156
|
+
) {
|
|
1157
|
+
throw new Error(
|
|
1158
|
+
"qualty auth export only updates login profile secrets. For full CI bootstrap use: qualty ci setup"
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const profilesList = String(args.profiles || "")
|
|
1163
|
+
.split(",")
|
|
1164
|
+
.map((part) => part.trim())
|
|
1165
|
+
.filter(Boolean);
|
|
1166
|
+
const { orgId, projectId, profile, apiUrl, configPath } = resolveCiSetupConfig(args, {
|
|
1167
|
+
requireProfile: profilesList.length === 0,
|
|
1168
|
+
});
|
|
1169
|
+
const token = resolveToken(args);
|
|
1170
|
+
|
|
1171
|
+
const profileEntries = await resolveCiSetupProfileEntries({
|
|
1172
|
+
orgId,
|
|
1173
|
+
projectId,
|
|
1174
|
+
profile,
|
|
1175
|
+
allProfiles: false,
|
|
1176
|
+
profilesList,
|
|
1177
|
+
suiteId: "",
|
|
1178
|
+
explicitIds: [],
|
|
1179
|
+
apiUrl,
|
|
1180
|
+
token,
|
|
1181
|
+
});
|
|
1182
|
+
const profileNames = profileEntries.map((entry) => entry.name);
|
|
1183
|
+
const primaryProfile = profile || profileNames[0] || "";
|
|
1184
|
+
|
|
1185
|
+
const manifest = buildAuthBundleManifest({ orgId, projectId, profileEntries });
|
|
1186
|
+
const authShards = shardAuthBundleManifest(manifest);
|
|
1187
|
+
const primaryShard = authShards[0];
|
|
1188
|
+
|
|
1189
|
+
const setGithubFlag =
|
|
1190
|
+
parseBoolean(args["set-github"], false) || parseBoolean(args["set-secret"], false);
|
|
1191
|
+
const noFile = parseBoolean(args["no-file"], false);
|
|
1192
|
+
const outputPath = noFile
|
|
1193
|
+
? String(args.output || "").trim()
|
|
1194
|
+
: String(args.output || "").trim() ||
|
|
1195
|
+
ciSetupAuthBundleOutputPath(
|
|
1196
|
+
localAuthStatePath({ orgId, projectId, profile: profileEntries[0].localProfileName })
|
|
1197
|
+
);
|
|
1198
|
+
const copyToClipboardFlag = parseBoolean(args.copy, false);
|
|
1199
|
+
const githubRepo = String(args.repo || "").trim();
|
|
1200
|
+
|
|
1201
|
+
if (outputPath) {
|
|
1202
|
+
writeFileSync(outputPath, primaryShard.payload, "utf8");
|
|
1203
|
+
try {
|
|
1204
|
+
chmodSync(outputPath, 0o600);
|
|
1205
|
+
} catch {
|
|
1206
|
+
// ignore platforms that do not support chmod
|
|
1207
|
+
}
|
|
1208
|
+
for (let index = 1; index < authShards.length; index += 1) {
|
|
1209
|
+
const shard = authShards[index];
|
|
1210
|
+
const shardPath = outputPath.replace(/(\.[^./]+)?$/, `_${index + 1}$1`);
|
|
1211
|
+
writeFileSync(shardPath, shard.payload, "utf8");
|
|
1212
|
+
try {
|
|
1213
|
+
chmodSync(shardPath, 0o600);
|
|
1214
|
+
} catch {
|
|
1215
|
+
// ignore
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const skipPayloadPrint = Boolean(outputPath || copyToClipboardFlag || setGithubFlag);
|
|
1221
|
+
const totalSecretBytes = authShards.reduce(
|
|
1222
|
+
(sum, shard) => sum + Buffer.byteLength(shard.payload, "utf8"),
|
|
1223
|
+
0
|
|
1224
|
+
);
|
|
1225
|
+
|
|
1226
|
+
// eslint-disable-next-line no-console
|
|
1227
|
+
console.log("");
|
|
1228
|
+
// eslint-disable-next-line no-console
|
|
1229
|
+
console.log("=== Qualty → GitHub Actions (login profiles only) ===");
|
|
1230
|
+
// eslint-disable-next-line no-console
|
|
1231
|
+
console.log("");
|
|
1232
|
+
// eslint-disable-next-line no-console
|
|
1233
|
+
console.log(
|
|
1234
|
+
`Auth bundle: ${profileNames.length} profile(s), ${authShards.length} secret shard(s), ${totalSecretBytes} bytes total`
|
|
1235
|
+
);
|
|
1236
|
+
for (const shard of authShards) {
|
|
1237
|
+
// eslint-disable-next-line no-console
|
|
1238
|
+
console.log(
|
|
1239
|
+
` - ${shard.secretName}: ${shard.profileNames.join(", ")} (${Buffer.byteLength(shard.payload, "utf8")} bytes)`
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
// eslint-disable-next-line no-console
|
|
1243
|
+
console.log("");
|
|
1244
|
+
if (outputPath) {
|
|
1245
|
+
// eslint-disable-next-line no-console
|
|
1246
|
+
console.log(`Wrote ${AUTH_SECRET_PRIMARY} to ${outputPath}`);
|
|
1247
|
+
for (let index = 1; index < authShards.length; index += 1) {
|
|
1248
|
+
const shard = authShards[index];
|
|
1249
|
+
const shardPath = outputPath.replace(/(\.[^./]+)?$/, `_${index + 1}$1`);
|
|
1250
|
+
// eslint-disable-next-line no-console
|
|
1251
|
+
console.log(`Wrote ${shard.secretName} to ${shardPath}`);
|
|
1252
|
+
}
|
|
1253
|
+
// eslint-disable-next-line no-console
|
|
1254
|
+
console.log("");
|
|
1255
|
+
}
|
|
1256
|
+
if (setGithubFlag) {
|
|
1257
|
+
const syncResults = syncGithubAuthProfileOnly({
|
|
1258
|
+
repo: githubRepo,
|
|
1259
|
+
profile: primaryProfile,
|
|
1260
|
+
profileNames,
|
|
1261
|
+
authShards,
|
|
1262
|
+
});
|
|
1263
|
+
printGithubSyncResults(syncResults, { repo: githubRepo });
|
|
1264
|
+
const failedAuth = syncResults.filter((row) => row.kind === "secret" && !row.ok);
|
|
1265
|
+
if (failedAuth.length > 0 && outputPath) {
|
|
1266
|
+
// eslint-disable-next-line no-console
|
|
1267
|
+
console.log(" Manual auth secrets:");
|
|
1268
|
+
authShards.forEach((shard, index) => {
|
|
1269
|
+
const shardPath =
|
|
1270
|
+
index === 0 ? outputPath : outputPath.replace(/(\.[^./]+)?$/, `_${index + 1}$1`);
|
|
1271
|
+
// eslint-disable-next-line no-console
|
|
1272
|
+
console.log(` gh secret set ${shard.secretName} < ${shardPath}`);
|
|
1273
|
+
});
|
|
1274
|
+
// eslint-disable-next-line no-console
|
|
1275
|
+
console.log("");
|
|
1276
|
+
}
|
|
1277
|
+
} else {
|
|
1278
|
+
// eslint-disable-next-line no-console
|
|
1279
|
+
console.log("GitHub secrets (login state only — does not update QUALTY_API_TOKEN or org/project):");
|
|
1280
|
+
// eslint-disable-next-line no-console
|
|
1281
|
+
console.log("");
|
|
1282
|
+
for (const shard of authShards) {
|
|
1283
|
+
// eslint-disable-next-line no-console
|
|
1284
|
+
console.log("---");
|
|
1285
|
+
// eslint-disable-next-line no-console
|
|
1286
|
+
console.log(`Name: ${shard.secretName}`);
|
|
1287
|
+
if (skipPayloadPrint) {
|
|
1288
|
+
// eslint-disable-next-line no-console
|
|
1289
|
+
console.log(
|
|
1290
|
+
`Value: (see file above${copyToClipboardFlag && shard.secretName === AUTH_SECRET_PRIMARY ? " or clipboard" : ""})`
|
|
1291
|
+
);
|
|
1292
|
+
} else {
|
|
1293
|
+
// eslint-disable-next-line no-console
|
|
1294
|
+
console.log("Value:");
|
|
1295
|
+
// eslint-disable-next-line no-console
|
|
1296
|
+
console.log(shard.payload);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
// eslint-disable-next-line no-console
|
|
1300
|
+
console.log("---");
|
|
1301
|
+
// eslint-disable-next-line no-console
|
|
1302
|
+
console.log("");
|
|
1303
|
+
// eslint-disable-next-line no-console
|
|
1304
|
+
console.log(
|
|
1305
|
+
"Re-run with --set-github from this repo's git root to push via gh (after qualty ci setup for first-time CI)."
|
|
1306
|
+
);
|
|
1307
|
+
// eslint-disable-next-line no-console
|
|
1308
|
+
console.log("");
|
|
1309
|
+
}
|
|
1310
|
+
if (copyToClipboardFlag) {
|
|
1311
|
+
if (copyToClipboard(primaryShard.payload)) {
|
|
1312
|
+
// eslint-disable-next-line no-console
|
|
1313
|
+
console.log(`✓ Copied ${AUTH_SECRET_PRIMARY} to clipboard.`);
|
|
1314
|
+
} else {
|
|
1315
|
+
// eslint-disable-next-line no-console
|
|
1316
|
+
console.log("Could not copy to clipboard on this machine.");
|
|
1317
|
+
}
|
|
1318
|
+
// eslint-disable-next-line no-console
|
|
1319
|
+
console.log("");
|
|
1320
|
+
}
|
|
1321
|
+
for (const entry of profileEntries) {
|
|
1322
|
+
// eslint-disable-next-line no-console
|
|
1323
|
+
console.log(`Profile "${entry.name}" → ${entry.statePath}`);
|
|
1324
|
+
}
|
|
1325
|
+
if (configPath) {
|
|
1326
|
+
// eslint-disable-next-line no-console
|
|
1327
|
+
console.log(`Config: ${configPath}`);
|
|
1328
|
+
}
|
|
1329
|
+
// eslint-disable-next-line no-console
|
|
1330
|
+
console.log("");
|
|
1331
|
+
// eslint-disable-next-line no-console
|
|
1332
|
+
console.log(
|
|
1333
|
+
"When login expires, re-run qualty auth setup then qualty auth export --set-github (or qualty ci setup for a full refresh)."
|
|
1334
|
+
);
|
|
1335
|
+
// eslint-disable-next-line no-console
|
|
1336
|
+
console.log("");
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1055
1339
|
async function runCiSetup(args) {
|
|
1056
1340
|
const {
|
|
1057
1341
|
orgId,
|
|
@@ -1359,7 +1643,9 @@ async function runCiSetup(args) {
|
|
|
1359
1643
|
// eslint-disable-next-line no-console
|
|
1360
1644
|
console.log("");
|
|
1361
1645
|
// eslint-disable-next-line no-console
|
|
1362
|
-
console.log(
|
|
1646
|
+
console.log(
|
|
1647
|
+
"When login expires, re-run qualty auth setup and qualty auth export --set-github (or qualty ci setup --set-github for a full refresh)."
|
|
1648
|
+
);
|
|
1363
1649
|
// eslint-disable-next-line no-console
|
|
1364
1650
|
console.log("");
|
|
1365
1651
|
}
|
|
@@ -2217,8 +2503,7 @@ async function maybePromptProjectIdForLocalRun({ args, apiUrl, token, orgId }) {
|
|
|
2217
2503
|
|
|
2218
2504
|
function resolveLocalAuthStateForRun({ args, orgId, projectId }) {
|
|
2219
2505
|
if (parseBoolean(args["no-auth-state"], false)) return null;
|
|
2220
|
-
const
|
|
2221
|
-
const profile = authProfileFromConfig(projectCfg, args["auth-profile"]);
|
|
2506
|
+
const profile = explicitAuthProfileFromArgs(args);
|
|
2222
2507
|
if (!profile) return null;
|
|
2223
2508
|
const path = localAuthStatePath({ orgId, projectId, profile });
|
|
2224
2509
|
const summary = summarizeStorageStateFile(path);
|
|
@@ -2232,12 +2517,10 @@ function resolveLocalAuthStateForRun({ args, orgId, projectId }) {
|
|
|
2232
2517
|
|
|
2233
2518
|
async function maybePromptAuthProfileForLocalRun({ args, orgId, projectId }) {
|
|
2234
2519
|
const projectCfg = readProjectConfig();
|
|
2235
|
-
const requestedProfile =
|
|
2520
|
+
const requestedProfile = explicitAuthProfileFromArgs(args);
|
|
2236
2521
|
if (requestedProfile) return { authProfile: requestedProfile, noAuthState: false };
|
|
2237
2522
|
if (parseBoolean(args["no-auth-state"], false)) return { authProfile: "", noAuthState: true };
|
|
2238
2523
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2239
|
-
const defaultProfile = authProfileFromConfig(projectCfg);
|
|
2240
|
-
if (defaultProfile) return { authProfile: defaultProfile, noAuthState: false };
|
|
2241
2524
|
return { authProfile: "", noAuthState: false };
|
|
2242
2525
|
}
|
|
2243
2526
|
if (!projectId) return { authProfile: "", noAuthState: false };
|
|
@@ -2472,8 +2755,8 @@ async function runAuthSetup(args) {
|
|
|
2472
2755
|
console.log("");
|
|
2473
2756
|
// eslint-disable-next-line no-console
|
|
2474
2757
|
console.log(
|
|
2475
|
-
`[qualty][auth]
|
|
2476
|
-
` qualty
|
|
2758
|
+
`[qualty][auth] Push this profile to GitHub Actions:\n` +
|
|
2759
|
+
` qualty auth export --project ${project.id} --profile "${profile}" --set-github`
|
|
2477
2760
|
);
|
|
2478
2761
|
} finally {
|
|
2479
2762
|
rl.close();
|
|
@@ -2509,11 +2792,15 @@ async function main() {
|
|
|
2509
2792
|
await runAuthSetup(args);
|
|
2510
2793
|
return;
|
|
2511
2794
|
}
|
|
2795
|
+
if (sub === "export") {
|
|
2796
|
+
await runAuthExport(args);
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2512
2799
|
if (sub === "restore-ci") {
|
|
2513
2800
|
await runAuthRestoreCi(args);
|
|
2514
2801
|
return;
|
|
2515
2802
|
}
|
|
2516
|
-
throw new Error("Unknown auth subcommand. Use: qualty auth setup | qualty auth restore-ci");
|
|
2803
|
+
throw new Error("Unknown auth subcommand. Use: qualty auth setup | qualty auth export | qualty auth restore-ci");
|
|
2517
2804
|
}
|
|
2518
2805
|
if (command === "run") {
|
|
2519
2806
|
if (parseBoolean(args.local, false)) {
|
|
@@ -2546,7 +2833,7 @@ async function main() {
|
|
|
2546
2833
|
projectId,
|
|
2547
2834
|
})
|
|
2548
2835
|
: {
|
|
2549
|
-
authProfile:
|
|
2836
|
+
authProfile: explicitAuthProfileFromArgs(args),
|
|
2550
2837
|
noAuthState: parseBoolean(args["no-auth-state"], false),
|
|
2551
2838
|
};
|
|
2552
2839
|
const localAuthArgs = {
|
|
@@ -2569,9 +2856,9 @@ async function main() {
|
|
|
2569
2856
|
device: args.device ? String(args.device).trim() : undefined,
|
|
2570
2857
|
headless: !parseBoolean(args.headed, false),
|
|
2571
2858
|
localConcurrency: resolveLocalConcurrency(args),
|
|
2572
|
-
authProfile:
|
|
2859
|
+
authProfile: authChoice.authProfile || "",
|
|
2573
2860
|
noAuthState: Boolean(authChoice.noAuthState || parseBoolean(args["no-auth-state"], false)),
|
|
2574
|
-
storageStatePath: localAuth?.path,
|
|
2861
|
+
storageStatePath: authChoice.authProfile ? localAuth?.path : undefined,
|
|
2575
2862
|
};
|
|
2576
2863
|
if (suiteId && explicitIds.length === 0) {
|
|
2577
2864
|
await runLocalSuiteWithSequences(localRunArgs);
|