qualty 0.1.20 → 0.1.21
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 +144 -15
- package/bin/qualty.js +333 -145
- package/bin/test-pw.mjs +4 -0
- package/package.json +1 -1
package/bin/local-runner.js
CHANGED
|
@@ -78,6 +78,25 @@ function localAuthStatePath({ orgId, projectId, profile }) {
|
|
|
78
78
|
return join(homedir(), ".qualty", "local-contexts", orgSeg, projectSeg, `${profileSeg}.json`);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
function jobUsesSavedLogin(job) {
|
|
82
|
+
if (!job) return false;
|
|
83
|
+
if (job.use_saved_login) return true;
|
|
84
|
+
if (job.saved_login_profile_id) return true;
|
|
85
|
+
const profile = job.saved_login_profile;
|
|
86
|
+
return Boolean(profile && typeof profile === "object" && (profile.id || profile.name));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function localProfileLabel(profile) {
|
|
90
|
+
if (!profile || typeof profile !== "object") return "";
|
|
91
|
+
return String(profile.local_profile_name || profile.name || "").trim();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function authSetupCommand(projectId, profileName) {
|
|
95
|
+
const pid = String(projectId || "").trim();
|
|
96
|
+
const name = String(profileName || "").trim();
|
|
97
|
+
return `qualty auth setup --project ${pid} --profile "${name}"`;
|
|
98
|
+
}
|
|
99
|
+
|
|
81
100
|
export function summarizeStorageStateFile(statePath) {
|
|
82
101
|
const path = String(statePath || "").trim();
|
|
83
102
|
if (!path || !existsSync(path)) {
|
|
@@ -233,6 +252,31 @@ export async function logAuthStateAfterInjection(tag, context, { testUrl = "" }
|
|
|
233
252
|
}
|
|
234
253
|
}
|
|
235
254
|
|
|
255
|
+
export async function logAuthStateAfterNavigation(tag, page) {
|
|
256
|
+
try {
|
|
257
|
+
const snapshot = await page.evaluate(() => ({
|
|
258
|
+
origin: window.location.origin,
|
|
259
|
+
href: window.location.href,
|
|
260
|
+
localStorageKeys: Object.keys(localStorage),
|
|
261
|
+
cookieCount: document.cookie ? document.cookie.split(";").filter(Boolean).length : 0,
|
|
262
|
+
}));
|
|
263
|
+
const keys = snapshot.localStorageKeys.length ? snapshot.localStorageKeys.join(", ") : "none";
|
|
264
|
+
// eslint-disable-next-line no-console
|
|
265
|
+
console.log(
|
|
266
|
+
`${tag} after navigation (${snapshot.origin}): localStorage keys [${keys}], document.cookie entries ~${snapshot.cookieCount}`
|
|
267
|
+
);
|
|
268
|
+
if (snapshot.localStorageKeys.length === 0 && snapshot.cookieCount === 0) {
|
|
269
|
+
// eslint-disable-next-line no-console
|
|
270
|
+
console.warn(
|
|
271
|
+
`${tag} warning: page has no localStorage keys and no cookies after navigation — app will likely appear logged out`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
} catch (err) {
|
|
275
|
+
// eslint-disable-next-line no-console
|
|
276
|
+
console.warn(`${tag} could not read auth state after navigation: ${err?.message || err}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
236
280
|
const localBindingSetupPromises = new Map();
|
|
237
281
|
|
|
238
282
|
async function promptYesNo(question, defaultYes = true) {
|
|
@@ -340,11 +384,21 @@ async function maybeSetupMissingLocalBinding({
|
|
|
340
384
|
? claim.saved_login_profile
|
|
341
385
|
: null;
|
|
342
386
|
if (!profile || !profile.name) return null;
|
|
343
|
-
if (profile.has_local_binding) return null;
|
|
344
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) return null;
|
|
345
|
-
|
|
346
387
|
const profileName = String(profile.name || "").trim();
|
|
347
388
|
if (!profileName) return null;
|
|
389
|
+
const localProfileName = localProfileLabel(profile) || profileName;
|
|
390
|
+
const statePath = localAuthStatePath({
|
|
391
|
+
orgId,
|
|
392
|
+
projectId: claim?.project_id,
|
|
393
|
+
profile: localProfileName,
|
|
394
|
+
});
|
|
395
|
+
if (existsSync(statePath)) return null;
|
|
396
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
`Local login state missing for profile "${profileName}". Run: ${authSetupCommand(claim?.project_id, profileName)}`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
348
402
|
const key = `${String(orgId || "")}:${String(claim?.project_id || "")}:${profileName}`;
|
|
349
403
|
if (localBindingSetupPromises.has(key)) {
|
|
350
404
|
return localBindingSetupPromises.get(key);
|
|
@@ -398,20 +452,21 @@ async function preflightMissingLocalBindings({
|
|
|
398
452
|
}) {
|
|
399
453
|
if (noAuthState) return;
|
|
400
454
|
if (explicitAuthProfile) return;
|
|
401
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) return;
|
|
402
455
|
const missingByProfile = new Map();
|
|
403
456
|
for (const job of savedJobs || []) {
|
|
404
|
-
|
|
457
|
+
if (!jobUsesSavedLogin(job)) continue;
|
|
405
458
|
const profileName = String(job?.saved_login_profile?.name || "").trim();
|
|
406
|
-
if (!
|
|
459
|
+
if (!profileName) continue;
|
|
460
|
+
const localProfileName =
|
|
461
|
+
String(job?.saved_login_profile?.local_profile_name || "").trim() || profileName;
|
|
407
462
|
const statePath = localAuthStatePath({
|
|
408
463
|
orgId,
|
|
409
464
|
projectId: String(job?.project_id || projectId || "").trim(),
|
|
410
|
-
profile:
|
|
465
|
+
profile: localProfileName,
|
|
411
466
|
});
|
|
412
467
|
if (existsSync(statePath)) continue;
|
|
413
468
|
const entry = missingByProfile.get(profileName) || {
|
|
414
|
-
localProfileName
|
|
469
|
+
localProfileName,
|
|
415
470
|
projectId: String(job?.project_id || projectId || "").trim(),
|
|
416
471
|
startUrl: rewriteUrlForLocalRun(String(job?.url || "").trim(), localPort),
|
|
417
472
|
tests: [],
|
|
@@ -427,6 +482,13 @@ async function preflightMissingLocalBindings({
|
|
|
427
482
|
}
|
|
428
483
|
if (missingByProfile.size === 0) return;
|
|
429
484
|
|
|
485
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
486
|
+
const [profileName, entry] = missingByProfile.entries().next().value;
|
|
487
|
+
throw new Error(
|
|
488
|
+
`Local login state missing for profile "${profileName}". Run: ${authSetupCommand(entry.projectId, profileName)}`
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
430
492
|
// eslint-disable-next-line no-console
|
|
431
493
|
console.log("[qualty][auth] Missing local login state for attached dashboard profiles:");
|
|
432
494
|
for (const [profileName, entry] of missingByProfile.entries()) {
|
|
@@ -1228,6 +1290,51 @@ export async function runLocalExecution({
|
|
|
1228
1290
|
authProfile,
|
|
1229
1291
|
noAuthState,
|
|
1230
1292
|
storageStatePath,
|
|
1293
|
+
}) {
|
|
1294
|
+
let overallSuccess = true;
|
|
1295
|
+
let lastError = null;
|
|
1296
|
+
let claimDevice = device || undefined;
|
|
1297
|
+
|
|
1298
|
+
while (true) {
|
|
1299
|
+
let once;
|
|
1300
|
+
try {
|
|
1301
|
+
once = await runLocalSingleCombination({
|
|
1302
|
+
apiRequest,
|
|
1303
|
+
apiUrl,
|
|
1304
|
+
token,
|
|
1305
|
+
orgId,
|
|
1306
|
+
executionId,
|
|
1307
|
+
device: claimDevice,
|
|
1308
|
+
headless,
|
|
1309
|
+
authProfile,
|
|
1310
|
+
noAuthState,
|
|
1311
|
+
storageStatePath,
|
|
1312
|
+
});
|
|
1313
|
+
} catch (err) {
|
|
1314
|
+
const msg = String(err?.message || err);
|
|
1315
|
+
if (msg.includes("No queued combinations")) break;
|
|
1316
|
+
throw err;
|
|
1317
|
+
}
|
|
1318
|
+
claimDevice = undefined;
|
|
1319
|
+
overallSuccess = overallSuccess && once.success;
|
|
1320
|
+
if (!once.success) lastError = once.error;
|
|
1321
|
+
if (device || !once.combinationsRemaining) break;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
return { success: overallSuccess, error: lastError };
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
async function runLocalSingleCombination({
|
|
1328
|
+
apiRequest,
|
|
1329
|
+
apiUrl,
|
|
1330
|
+
token,
|
|
1331
|
+
orgId,
|
|
1332
|
+
executionId,
|
|
1333
|
+
device,
|
|
1334
|
+
headless,
|
|
1335
|
+
authProfile,
|
|
1336
|
+
noAuthState,
|
|
1337
|
+
storageStatePath,
|
|
1231
1338
|
}) {
|
|
1232
1339
|
const { chromium } = await import("playwright");
|
|
1233
1340
|
|
|
@@ -1238,7 +1345,7 @@ export async function runLocalExecution({
|
|
|
1238
1345
|
orgId,
|
|
1239
1346
|
path: `/api/v1/cli/executions/${executionId}/claim`,
|
|
1240
1347
|
method: "POST",
|
|
1241
|
-
body: { device:
|
|
1348
|
+
body: device ? { device: String(device).trim() } : {},
|
|
1242
1349
|
});
|
|
1243
1350
|
const effectiveDevice = String(claim.device || runDevice).trim() || runDevice;
|
|
1244
1351
|
|
|
@@ -1267,7 +1374,7 @@ export async function runLocalExecution({
|
|
|
1267
1374
|
explicitAuthProfile: String(authProfile || "").trim(),
|
|
1268
1375
|
noAuthState: Boolean(noAuthState),
|
|
1269
1376
|
});
|
|
1270
|
-
const claimProfile =
|
|
1377
|
+
const claimProfile = localProfileLabel(claimSavedLogin);
|
|
1271
1378
|
const effectiveProfile = String(authProfile || "").trim() || setupFromClaim?.profile || claimProfile;
|
|
1272
1379
|
const resolvedStoragePath =
|
|
1273
1380
|
!noAuthState && effectiveProfile
|
|
@@ -1309,12 +1416,22 @@ export async function runLocalExecution({
|
|
|
1309
1416
|
});
|
|
1310
1417
|
|
|
1311
1418
|
if (!noAuthState && effectiveProfile && !finalStoragePath) {
|
|
1419
|
+
const profileName = String(claimSavedLogin?.name || effectiveProfile).trim();
|
|
1312
1420
|
const candidates = [storageStatePath, resolvedStoragePath].filter(Boolean);
|
|
1421
|
+
const setupCmd = authSetupCommand(claim.project_id, profileName);
|
|
1422
|
+
const detail =
|
|
1423
|
+
`Local login state missing for profile "${profileName}".` +
|
|
1424
|
+
(candidates.length ? ` Checked: ${candidates.join(", ")}.` : "") +
|
|
1425
|
+
` Run: ${setupCmd}`;
|
|
1426
|
+
const failLoud =
|
|
1427
|
+
!process.stdin.isTTY ||
|
|
1428
|
+
!process.stdout.isTTY ||
|
|
1429
|
+
(claimSavedLogin?.name && !String(authProfile || "").trim());
|
|
1430
|
+
if (failLoud) {
|
|
1431
|
+
throw new Error(detail);
|
|
1432
|
+
}
|
|
1313
1433
|
// eslint-disable-next-line no-console
|
|
1314
|
-
console.warn(
|
|
1315
|
-
`[qualty][auth] profile "${effectiveProfile}" was requested but no auth state file exists` +
|
|
1316
|
-
(candidates.length ? ` (checked: ${candidates.join(", ")})` : "")
|
|
1317
|
-
);
|
|
1434
|
+
console.warn(`[qualty][auth] ${detail}`);
|
|
1318
1435
|
}
|
|
1319
1436
|
|
|
1320
1437
|
const browserServer = await chromium.launchServer({
|
|
@@ -1379,6 +1496,9 @@ export async function runLocalExecution({
|
|
|
1379
1496
|
try {
|
|
1380
1497
|
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 60000 });
|
|
1381
1498
|
await page.waitForTimeout(500);
|
|
1499
|
+
if (finalStoragePath) {
|
|
1500
|
+
await logAuthStateAfterNavigation("[qualty][auth]", page);
|
|
1501
|
+
}
|
|
1382
1502
|
|
|
1383
1503
|
for (let stepIndex = 0; stepIndex < totalSteps; stepIndex += 1) {
|
|
1384
1504
|
const step = steps[stepIndex] || {};
|
|
@@ -1544,7 +1664,12 @@ export async function runLocalExecution({
|
|
|
1544
1664
|
}),
|
|
1545
1665
|
});
|
|
1546
1666
|
|
|
1547
|
-
|
|
1667
|
+
if (completeResp && completeResp.status) {
|
|
1668
|
+
runSuccess = completeResp.status === "completed" || runSuccess;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
const combinationsRemaining = Number(completeResp?.combinations_remaining ?? 0);
|
|
1672
|
+
return { success: runSuccess, stepsJson, error: runError, combinationsRemaining, device: effectiveDevice };
|
|
1548
1673
|
}
|
|
1549
1674
|
|
|
1550
1675
|
async function fetchProjectLocalPort({ apiRequest, apiUrl, token, orgId, projectId }) {
|
|
@@ -1604,6 +1729,10 @@ export async function runLocalCi({
|
|
|
1604
1729
|
const started = runs.filter((r) => r.execution_job_id);
|
|
1605
1730
|
if (started.length === 0) throw new Error("Failed to start any local runs.");
|
|
1606
1731
|
|
|
1732
|
+
const executionIds = started.map((r) => r.execution_job_id);
|
|
1733
|
+
// eslint-disable-next-line no-console
|
|
1734
|
+
console.log(`QUALTY_EXECUTION_IDS=${JSON.stringify(executionIds)}`);
|
|
1735
|
+
|
|
1607
1736
|
await preflightMissingLocalBindings({
|
|
1608
1737
|
apiRequest,
|
|
1609
1738
|
apiUrl,
|
package/bin/qualty.js
CHANGED
|
@@ -12,40 +12,32 @@ import {
|
|
|
12
12
|
import { gzipSync } from "node:zlib";
|
|
13
13
|
import { spawn, spawnSync } from "node:child_process";
|
|
14
14
|
import { homedir, hostname } from "node:os";
|
|
15
|
-
import { join } from "node:path";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
16
|
import { createInterface } from "node:readline/promises";
|
|
17
17
|
import process from "node:process";
|
|
18
18
|
import { rewriteUrlForLocalRun, runLocalCi, summarizeStorageStateFile, logAuthStateDiagnostics } from "./local-runner.js";
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
const DEFAULT_QUALTY_API_URL = "https://qualty-api-production.up.railway.app";
|
|
20
|
+
/** Default Qualty API base URL (no trailing slash). Override with QUALTY_API_URL env. */
|
|
21
|
+
//const DEFAULT_QUALTY_API_URL = "https://qualty-api-production.up.railway.app";
|
|
22
|
+
const DEFAULT_QUALTY_API_URL = "http://127.0.0.1:8000";
|
|
22
23
|
|
|
23
|
-
const
|
|
24
|
-
const QUALTY_CONFIG_PATH = join(QUALTY_CONFIG_DIR, "config.json");
|
|
24
|
+
const PROJECT_CONFIG_FILENAME = ".qualty.json";
|
|
25
25
|
const QUALTY_SHARED_CREDS_DIR = join(homedir(), ".qualty");
|
|
26
26
|
const QUALTY_SHARED_CREDS_PATH = join(QUALTY_SHARED_CREDS_DIR, "credentials");
|
|
27
|
-
const QUALTY_PROJECT_CONFIG_PATH = join(process.cwd(), ".qualty.json");
|
|
28
27
|
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
return {};
|
|
28
|
+
function findProjectConfigPath() {
|
|
29
|
+
let current = process.cwd();
|
|
30
|
+
for (;;) {
|
|
31
|
+
const candidate = join(current, PROJECT_CONFIG_FILENAME);
|
|
32
|
+
if (existsSync(candidate)) return candidate;
|
|
33
|
+
const parent = dirname(current);
|
|
34
|
+
if (parent === current) return null;
|
|
35
|
+
current = parent;
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
38
|
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
writeFileSync(QUALTY_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
44
|
-
try {
|
|
45
|
-
chmodSync(QUALTY_CONFIG_PATH, 0o600);
|
|
46
|
-
} catch {
|
|
47
|
-
// best effort on non-posix systems
|
|
48
|
-
}
|
|
39
|
+
function getProjectConfigWritePath() {
|
|
40
|
+
return findProjectConfigPath() || join(process.cwd(), PROJECT_CONFIG_FILENAME);
|
|
49
41
|
}
|
|
50
42
|
|
|
51
43
|
function readSharedCredentials() {
|
|
@@ -70,10 +62,19 @@ function writeSharedCredentials(token) {
|
|
|
70
62
|
}
|
|
71
63
|
}
|
|
72
64
|
|
|
65
|
+
function projectIdFromConfig(projectCfg, fallback = "") {
|
|
66
|
+
return String(fallback || projectCfg.default_project_id || "").trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function authProfileFromConfig(projectCfg, fallback = "") {
|
|
70
|
+
return String(fallback || projectCfg.default_auth_profile || "").trim();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
73
|
function readProjectConfig() {
|
|
74
|
-
|
|
74
|
+
const configPath = findProjectConfigPath();
|
|
75
|
+
if (!configPath) return {};
|
|
75
76
|
try {
|
|
76
|
-
const raw = readFileSync(
|
|
77
|
+
const raw = readFileSync(configPath, "utf8");
|
|
77
78
|
const parsed = JSON.parse(raw);
|
|
78
79
|
if (!parsed || typeof parsed !== "object") return {};
|
|
79
80
|
return parsed;
|
|
@@ -83,29 +84,29 @@ function readProjectConfig() {
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
function writeProjectConfig(updates) {
|
|
87
|
+
const configPath = getProjectConfigWritePath();
|
|
86
88
|
const current = readProjectConfig();
|
|
87
89
|
const next = { ...current };
|
|
88
90
|
for (const [k, v] of Object.entries(updates || {})) {
|
|
89
91
|
if (v === undefined || v === null || v === "") delete next[k];
|
|
90
92
|
else next[k] = v;
|
|
91
93
|
}
|
|
92
|
-
writeFileSync(
|
|
94
|
+
writeFileSync(configPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
function resolveApiUrl(args
|
|
97
|
+
function resolveApiUrl(args = {}) {
|
|
96
98
|
void args;
|
|
97
|
-
|
|
98
|
-
return String(DEFAULT_QUALTY_API_URL).replace(/\/$/, "");
|
|
99
|
+
return String(process.env.QUALTY_API_URL || DEFAULT_QUALTY_API_URL).replace(/\/$/, "");
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
function resolveToken(args
|
|
102
|
+
function resolveToken(args = {}) {
|
|
102
103
|
const creds = readSharedCredentials();
|
|
103
|
-
return args.token || process.env.QUALTY_API_TOKEN || creds.api_token ||
|
|
104
|
+
return args.token || process.env.QUALTY_API_TOKEN || creds.api_token || "";
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
function resolveOrgId(args
|
|
107
|
+
function resolveOrgId(args = {}) {
|
|
107
108
|
const projectCfg = readProjectConfig();
|
|
108
|
-
return args.org || process.env.QUALTY_ORG_ID ||
|
|
109
|
+
return args.org || process.env.QUALTY_ORG_ID || projectCfg.default_org_id || "";
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
function maskSecret(secret) {
|
|
@@ -145,7 +146,10 @@ function usage() {
|
|
|
145
146
|
" qualty setup [--token <bearer-token>] [--org <org-id>]",
|
|
146
147
|
" qualty auth setup [--project <project-id>] [--profile <profile-name>] [--headed]",
|
|
147
148
|
" qualty auth export-ci --project <project-id> [--profile <profile-name>] [--org <org-id>]",
|
|
148
|
-
" [--
|
|
149
|
+
" [--suite-id <suite-uuid>] [--repo <owner/repo>]",
|
|
150
|
+
" [--output <path>] [--no-file] [--copy] [--gzip] [--include-token]",
|
|
151
|
+
" [--set-secret | --set-github] gh: secrets (token, API URL, auth) + variables (org, project, profile, …)",
|
|
152
|
+
" [--cli-repo <owner/repo>] [--cli-ref <branch>] Optional CI variables",
|
|
149
153
|
" qualty connect --project <project-id> [--port 3000] [--token <bearer-token>] [--org <org-id>]",
|
|
150
154
|
" qualty run --project <project-id> [--suite-id <suite-id>] [--ids <id1,id2>] [--local] [--token <bearer-token>] [--org <org-id>]",
|
|
151
155
|
" [--poll-interval 5] [--timeout 30] [--fail-on-failure true] [--no-view-logs]",
|
|
@@ -157,7 +161,8 @@ function usage() {
|
|
|
157
161
|
"",
|
|
158
162
|
"Env vars:",
|
|
159
163
|
" QUALTY_API_TOKEN Bearer token used for auth",
|
|
160
|
-
"
|
|
164
|
+
" QUALTY_API_URL API base URL (default: bundled CLI default)",
|
|
165
|
+
" QUALTY_ORG_ID Org context override (default: .qualty.json default_org_id)",
|
|
161
166
|
" QUALTY_LOCAL_CONCURRENCY Local parallel workers for --local runs (default: 4)",
|
|
162
167
|
].join("\n")
|
|
163
168
|
);
|
|
@@ -228,12 +233,11 @@ function startCloudflared(token, port) {
|
|
|
228
233
|
}
|
|
229
234
|
|
|
230
235
|
async function runConnect(args) {
|
|
231
|
-
const config = readQualtyConfig();
|
|
232
236
|
const projectId = args.project;
|
|
233
237
|
const port = Number(args.port || 3000);
|
|
234
|
-
const apiUrl = resolveApiUrl(args
|
|
235
|
-
const token = resolveToken(args
|
|
236
|
-
const orgId = resolveOrgId(args
|
|
238
|
+
const apiUrl = resolveApiUrl(args);
|
|
239
|
+
const token = resolveToken(args);
|
|
240
|
+
const orgId = resolveOrgId(args);
|
|
237
241
|
|
|
238
242
|
if (!projectId || !token) {
|
|
239
243
|
usage();
|
|
@@ -305,7 +309,6 @@ async function runConnect(args) {
|
|
|
305
309
|
}
|
|
306
310
|
|
|
307
311
|
async function runSetup(args) {
|
|
308
|
-
const existing = readQualtyConfig();
|
|
309
312
|
const sharedCreds = readSharedCredentials();
|
|
310
313
|
const projectCfg = readProjectConfig();
|
|
311
314
|
const rl = createInterface({
|
|
@@ -341,8 +344,10 @@ async function runSetup(args) {
|
|
|
341
344
|
};
|
|
342
345
|
|
|
343
346
|
try {
|
|
344
|
-
const currentOrgId = String(args.org ||
|
|
345
|
-
const currentToken = String(
|
|
347
|
+
const currentOrgId = String(args.org || projectCfg.default_org_id || "");
|
|
348
|
+
const currentToken = String(
|
|
349
|
+
args.token || process.env.QUALTY_API_TOKEN || sharedCreds.api_token || ""
|
|
350
|
+
);
|
|
346
351
|
|
|
347
352
|
let token = "";
|
|
348
353
|
if (args.token) {
|
|
@@ -438,20 +443,13 @@ async function runSetup(args) {
|
|
|
438
443
|
throw new Error("Org ID is required. Re-run setup and provide QUALTY_ORG_ID.");
|
|
439
444
|
}
|
|
440
445
|
|
|
441
|
-
const next = {
|
|
442
|
-
org_id: orgId,
|
|
443
|
-
};
|
|
444
|
-
// Keep token in the same place MCP uses.
|
|
445
446
|
writeSharedCredentials(token);
|
|
446
|
-
writeQualtyConfig(next);
|
|
447
|
-
// Keep org in MCP-compatible per-project config too.
|
|
448
447
|
writeProjectConfig({ default_org_id: orgId });
|
|
449
|
-
|
|
450
|
-
console.log(`[qualty] Saved config to ${QUALTY_CONFIG_PATH}`);
|
|
448
|
+
const projectConfigPath = getProjectConfigWritePath();
|
|
451
449
|
// eslint-disable-next-line no-console
|
|
452
450
|
console.log(`[qualty] Saved shared token to ${QUALTY_SHARED_CREDS_PATH} (used by MCP)`);
|
|
453
451
|
// eslint-disable-next-line no-console
|
|
454
|
-
console.log(`[qualty] Saved project
|
|
452
|
+
console.log(`[qualty] Saved project config to ${projectConfigPath} (MCP-compatible)`);
|
|
455
453
|
// eslint-disable-next-line no-console
|
|
456
454
|
console.log(`[qualty] Org: ${orgName || "configured"}`);
|
|
457
455
|
// eslint-disable-next-line no-console
|
|
@@ -524,8 +522,17 @@ function copyToClipboard(text) {
|
|
|
524
522
|
return false;
|
|
525
523
|
}
|
|
526
524
|
|
|
527
|
-
function
|
|
528
|
-
const
|
|
525
|
+
function ghRepoArgs(repo) {
|
|
526
|
+
const target = String(repo || "").trim();
|
|
527
|
+
return target ? ["--repo", target] : [];
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function setGithubSecret(name, value, { repo } = {}) {
|
|
531
|
+
const result = spawnSync(
|
|
532
|
+
"gh",
|
|
533
|
+
["secret", "set", name, ...ghRepoArgs(repo)],
|
|
534
|
+
{ input: value, encoding: "utf8" }
|
|
535
|
+
);
|
|
529
536
|
if (result.error?.code === "ENOENT") {
|
|
530
537
|
return { ok: false, reason: "GitHub CLI (gh) is not installed." };
|
|
531
538
|
}
|
|
@@ -536,6 +543,112 @@ function setGithubSecret(name, value) {
|
|
|
536
543
|
return { ok: true };
|
|
537
544
|
}
|
|
538
545
|
|
|
546
|
+
function setGithubVariable(name, value, { repo } = {}) {
|
|
547
|
+
const result = spawnSync("gh", ["variable", "set", name, "--body", value, ...ghRepoArgs(repo)], {
|
|
548
|
+
encoding: "utf8",
|
|
549
|
+
});
|
|
550
|
+
if (result.error?.code === "ENOENT") {
|
|
551
|
+
return { ok: false, reason: "GitHub CLI (gh) is not installed." };
|
|
552
|
+
}
|
|
553
|
+
if (result.status !== 0) {
|
|
554
|
+
const detail = String(result.stderr || result.stdout || "").trim();
|
|
555
|
+
return { ok: false, reason: detail || `gh variable set failed (exit ${result.status})` };
|
|
556
|
+
}
|
|
557
|
+
return { ok: true };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Push Qualty CI secrets + repository variables via gh (run from target app repo root).
|
|
562
|
+
*/
|
|
563
|
+
function syncGithubCiConfig({
|
|
564
|
+
repo,
|
|
565
|
+
orgId,
|
|
566
|
+
projectId,
|
|
567
|
+
profile,
|
|
568
|
+
authPayload,
|
|
569
|
+
apiUrl,
|
|
570
|
+
token,
|
|
571
|
+
suiteId,
|
|
572
|
+
cliRepo,
|
|
573
|
+
cliRef,
|
|
574
|
+
}) {
|
|
575
|
+
const results = [];
|
|
576
|
+
const record = (kind, name, outcome) => {
|
|
577
|
+
results.push({ kind, name, ...outcome });
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const authResult = setGithubSecret("QUALTY_AUTH_STATE_B64", authPayload, { repo });
|
|
581
|
+
record("secret", "QUALTY_AUTH_STATE_B64", authResult);
|
|
582
|
+
|
|
583
|
+
if (token) {
|
|
584
|
+
record("secret", "QUALTY_API_TOKEN", setGithubSecret("QUALTY_API_TOKEN", token, { repo }));
|
|
585
|
+
} else {
|
|
586
|
+
record("secret", "QUALTY_API_TOKEN", {
|
|
587
|
+
ok: false,
|
|
588
|
+
reason: "No token locally — run qualty setup or set QUALTY_API_TOKEN.",
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const apiUrlNorm = String(apiUrl || "").replace(/\/$/, "");
|
|
593
|
+
const isLocalDefault = apiUrlNorm === "http://127.0.0.1:8000";
|
|
594
|
+
if (apiUrlNorm && !isLocalDefault) {
|
|
595
|
+
record("secret", "QUALTY_API_URL", setGithubSecret("QUALTY_API_URL", apiUrlNorm, { repo }));
|
|
596
|
+
} else {
|
|
597
|
+
record("secret", "QUALTY_API_URL", {
|
|
598
|
+
ok: false,
|
|
599
|
+
reason: "Set QUALTY_API_URL to your dev/prod API (not localhost) before export-ci.",
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
record("variable", "QUALTY_ORG_ID", setGithubVariable("QUALTY_ORG_ID", orgId, { repo }));
|
|
604
|
+
record("variable", "QUALTY_PROJECT_ID", setGithubVariable("QUALTY_PROJECT_ID", String(projectId), { repo }));
|
|
605
|
+
record("variable", "QUALTY_AUTH_PROFILE", setGithubVariable("QUALTY_AUTH_PROFILE", profile, { repo }));
|
|
606
|
+
|
|
607
|
+
if (suiteId) {
|
|
608
|
+
record("variable", "QUALTY_SUITE_ID", setGithubVariable("QUALTY_SUITE_ID", String(suiteId), { repo }));
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (cliRepo) {
|
|
612
|
+
record("variable", "QUALTY_CLI_REPO", setGithubVariable("QUALTY_CLI_REPO", cliRepo, { repo }));
|
|
613
|
+
}
|
|
614
|
+
if (cliRef) {
|
|
615
|
+
record("variable", "QUALTY_CLI_REF", setGithubVariable("QUALTY_CLI_REF", cliRef, { repo }));
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return results;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function printGithubSyncResults(results, { repo }) {
|
|
622
|
+
const target = repo ? ` (repo ${repo})` : "";
|
|
623
|
+
const printGroup = (title, rows) => {
|
|
624
|
+
if (!rows.length) return;
|
|
625
|
+
// eslint-disable-next-line no-console
|
|
626
|
+
console.log(title);
|
|
627
|
+
for (const row of rows) {
|
|
628
|
+
if (row.ok) {
|
|
629
|
+
// eslint-disable-next-line no-console
|
|
630
|
+
console.log(` ✓ ${row.name}`);
|
|
631
|
+
} else {
|
|
632
|
+
// eslint-disable-next-line no-console
|
|
633
|
+
console.log(` ✗ ${row.name} — ${row.reason || "failed"}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// eslint-disable-next-line no-console
|
|
639
|
+
console.log(`GitHub sync${target} (gh secret set + gh variable set):`);
|
|
640
|
+
printGroup(
|
|
641
|
+
" Repository secrets:",
|
|
642
|
+
results.filter((r) => r.kind === "secret")
|
|
643
|
+
);
|
|
644
|
+
printGroup(
|
|
645
|
+
" Repository variables:",
|
|
646
|
+
results.filter((r) => r.kind === "variable")
|
|
647
|
+
);
|
|
648
|
+
// eslint-disable-next-line no-console
|
|
649
|
+
console.log("");
|
|
650
|
+
}
|
|
651
|
+
|
|
539
652
|
function encodeAuthStateForGithubSecret(statePath, { gzip = false } = {}) {
|
|
540
653
|
const raw = readFileSync(statePath);
|
|
541
654
|
if (gzip) {
|
|
@@ -552,14 +665,10 @@ function encodeAuthStateForGithubSecret(statePath, { gzip = false } = {}) {
|
|
|
552
665
|
};
|
|
553
666
|
}
|
|
554
667
|
|
|
555
|
-
function githubAuthRestoreShell({ orgId, projectId, profile
|
|
668
|
+
function githubAuthRestoreShell({ orgId, projectId, profile }) {
|
|
556
669
|
const orgSeg = safePathSegment(orgId, "default-org");
|
|
557
670
|
const projectSeg = safePathSegment(projectId, "default-project");
|
|
558
671
|
const profileSeg = safePathSegment(profile, "default");
|
|
559
|
-
const decodeCmd =
|
|
560
|
-
encoding === "gzip+base64"
|
|
561
|
-
? 'echo "$QUALTY_AUTH_STATE_B64" | base64 -d | gunzip'
|
|
562
|
-
: 'echo "$QUALTY_AUTH_STATE_B64" | base64 -d';
|
|
563
672
|
return [
|
|
564
673
|
`- name: Restore Qualty login state`,
|
|
565
674
|
` env:`,
|
|
@@ -567,13 +676,13 @@ function githubAuthRestoreShell({ orgId, projectId, profile, encoding }) {
|
|
|
567
676
|
` run: |`,
|
|
568
677
|
` AUTH_DIR="$HOME/.qualty/local-contexts/${orgSeg}/${projectSeg}"`,
|
|
569
678
|
` mkdir -p "$AUTH_DIR"`,
|
|
570
|
-
` $
|
|
679
|
+
` echo "$QUALTY_AUTH_STATE_B64" | base64 -d | gunzip > "$AUTH_DIR/${profileSeg}.json"`,
|
|
571
680
|
` test -s "$AUTH_DIR/${profileSeg}.json"`,
|
|
572
681
|
``,
|
|
573
682
|
`- name: Run Qualty (local)`,
|
|
574
683
|
` env:`,
|
|
575
684
|
` QUALTY_API_TOKEN: \${{ secrets.QUALTY_API_TOKEN }}`,
|
|
576
|
-
` QUALTY_ORG_ID: \${{
|
|
685
|
+
` QUALTY_ORG_ID: \${{ vars.QUALTY_ORG_ID }}`,
|
|
577
686
|
` run: |`,
|
|
578
687
|
` npx qualty@latest run --local \\`,
|
|
579
688
|
` --project ${projectId} \\`,
|
|
@@ -584,12 +693,14 @@ function githubAuthRestoreShell({ orgId, projectId, profile, encoding }) {
|
|
|
584
693
|
}
|
|
585
694
|
|
|
586
695
|
async function runAuthExportCi(args) {
|
|
587
|
-
const
|
|
588
|
-
const
|
|
589
|
-
const token = resolveToken(args, config);
|
|
696
|
+
const orgId = resolveOrgId(args);
|
|
697
|
+
const token = resolveToken(args);
|
|
590
698
|
const projectCfg = readProjectConfig();
|
|
591
|
-
const projectId =
|
|
592
|
-
const profile =
|
|
699
|
+
const projectId = projectIdFromConfig(projectCfg, args.project);
|
|
700
|
+
const profile = authProfileFromConfig(
|
|
701
|
+
projectCfg,
|
|
702
|
+
args.profile || args["auth-profile"]
|
|
703
|
+
);
|
|
593
704
|
|
|
594
705
|
if (!orgId) {
|
|
595
706
|
throw new Error("Missing org. Pass --org, set QUALTY_ORG_ID, or add default_org_id to .qualty.json.");
|
|
@@ -614,7 +725,10 @@ async function runAuthExportCi(args) {
|
|
|
614
725
|
);
|
|
615
726
|
}
|
|
616
727
|
|
|
617
|
-
|
|
728
|
+
const setGithubFlag =
|
|
729
|
+
parseBoolean(args["set-github"], false) || parseBoolean(args["set-secret"], false);
|
|
730
|
+
// CI path: one format only — gzip then base64 in QUALTY_AUTH_STATE_B64 (no separate gzip flag).
|
|
731
|
+
let gzip = parseBoolean(args.gzip, false) || setGithubFlag;
|
|
618
732
|
let encoded = encodeAuthStateForGithubSecret(statePath, { gzip });
|
|
619
733
|
let secretBytes = Buffer.byteLength(encoded.payload, "utf8");
|
|
620
734
|
if (!gzip && secretBytes > GITHUB_SECRET_MAX_BYTES - 1024) {
|
|
@@ -634,7 +748,12 @@ async function runAuthExportCi(args) {
|
|
|
634
748
|
? String(args.output || "").trim()
|
|
635
749
|
: String(args.output || "").trim() || authExportCiOutputPath(statePath);
|
|
636
750
|
const copyToClipboardFlag = parseBoolean(args.copy, false);
|
|
637
|
-
const
|
|
751
|
+
const githubRepo = String(args.repo || "").trim();
|
|
752
|
+
const suiteId = String(args["suite-id"] || args.suite || "").trim();
|
|
753
|
+
const cliRepo = String(args["cli-repo"] || "").trim();
|
|
754
|
+
const cliRef = String(args["cli-ref"] || "").trim();
|
|
755
|
+
const apiUrl = resolveApiUrl(args);
|
|
756
|
+
|
|
638
757
|
if (outputPath) {
|
|
639
758
|
writeFileSync(outputPath, encoded.payload, "utf8");
|
|
640
759
|
try {
|
|
@@ -646,12 +765,12 @@ async function runAuthExportCi(args) {
|
|
|
646
765
|
|
|
647
766
|
const includeToken = parseBoolean(args["include-token"], false);
|
|
648
767
|
const profileSeg = safePathSegment(profile, "default");
|
|
649
|
-
const skipPayloadPrint = Boolean(outputPath || copyToClipboardFlag ||
|
|
768
|
+
const skipPayloadPrint = Boolean(outputPath || copyToClipboardFlag || setGithubFlag);
|
|
650
769
|
|
|
651
770
|
// eslint-disable-next-line no-console
|
|
652
771
|
console.log("");
|
|
653
772
|
// eslint-disable-next-line no-console
|
|
654
|
-
console.log("=== Qualty → GitHub Actions secrets ===");
|
|
773
|
+
console.log("=== Qualty → GitHub Actions (secrets & variables) ===");
|
|
655
774
|
// eslint-disable-next-line no-console
|
|
656
775
|
console.log("");
|
|
657
776
|
if (encoded.encoding === "gzip+base64") {
|
|
@@ -671,21 +790,27 @@ async function runAuthExportCi(args) {
|
|
|
671
790
|
// eslint-disable-next-line no-console
|
|
672
791
|
console.log("");
|
|
673
792
|
}
|
|
674
|
-
if (
|
|
675
|
-
const
|
|
676
|
-
|
|
793
|
+
if (setGithubFlag) {
|
|
794
|
+
const syncResults = syncGithubCiConfig({
|
|
795
|
+
repo: githubRepo,
|
|
796
|
+
orgId,
|
|
797
|
+
projectId,
|
|
798
|
+
profile,
|
|
799
|
+
authPayload: encoded.payload,
|
|
800
|
+
apiUrl,
|
|
801
|
+
token,
|
|
802
|
+
suiteId,
|
|
803
|
+
cliRepo,
|
|
804
|
+
cliRef,
|
|
805
|
+
});
|
|
806
|
+
printGithubSyncResults(syncResults, { repo: githubRepo });
|
|
807
|
+
const authRow = syncResults.find((r) => r.name === "QUALTY_AUTH_STATE_B64");
|
|
808
|
+
if (!authRow?.ok && outputPath) {
|
|
677
809
|
// eslint-disable-next-line no-console
|
|
678
|
-
console.log(
|
|
679
|
-
} else {
|
|
810
|
+
console.log(` Manual auth secret: gh secret set QUALTY_AUTH_STATE_B64 < ${outputPath}`);
|
|
680
811
|
// eslint-disable-next-line no-console
|
|
681
|
-
console.log(
|
|
682
|
-
if (outputPath) {
|
|
683
|
-
// eslint-disable-next-line no-console
|
|
684
|
-
console.log(` gh secret set QUALTY_AUTH_STATE_B64 < ${outputPath}`);
|
|
685
|
-
}
|
|
812
|
+
console.log("");
|
|
686
813
|
}
|
|
687
|
-
// eslint-disable-next-line no-console
|
|
688
|
-
console.log("");
|
|
689
814
|
}
|
|
690
815
|
if (copyToClipboardFlag) {
|
|
691
816
|
if (copyToClipboard(encoded.payload)) {
|
|
@@ -702,61 +827,124 @@ async function runAuthExportCi(args) {
|
|
|
702
827
|
// eslint-disable-next-line no-console
|
|
703
828
|
console.log("");
|
|
704
829
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
if (includeToken && token) {
|
|
830
|
+
if (setGithubFlag) {
|
|
831
|
+
// eslint-disable-next-line no-console
|
|
832
|
+
console.log(
|
|
833
|
+
"1. GitHub Actions config was pushed via gh (secrets + repository variables — see sync above). Fix any ✗ manually."
|
|
834
|
+
);
|
|
835
|
+
if (!suiteId) {
|
|
836
|
+
// eslint-disable-next-line no-console
|
|
837
|
+
console.log(
|
|
838
|
+
" Tip: re-run with --suite-id <uuid> to set QUALTY_SUITE_ID (or set it in the GitHub UI)."
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
// eslint-disable-next-line no-console
|
|
842
|
+
console.log("");
|
|
843
|
+
} else {
|
|
844
|
+
// eslint-disable-next-line no-console
|
|
845
|
+
console.log(
|
|
846
|
+
"1. Add repository variables (or re-run with --set-github from this repo's git root):"
|
|
847
|
+
);
|
|
724
848
|
// eslint-disable-next-line no-console
|
|
725
|
-
console.log(
|
|
726
|
-
} else if (token) {
|
|
849
|
+
console.log("");
|
|
727
850
|
// eslint-disable-next-line no-console
|
|
728
|
-
console.log(
|
|
851
|
+
console.log("---");
|
|
729
852
|
// eslint-disable-next-line no-console
|
|
730
|
-
console.log("
|
|
853
|
+
console.log("Name: QUALTY_ORG_ID");
|
|
731
854
|
// eslint-disable-next-line no-console
|
|
732
|
-
console.log("
|
|
733
|
-
|
|
855
|
+
console.log("Value:");
|
|
856
|
+
// eslint-disable-next-line no-console
|
|
857
|
+
console.log(orgId);
|
|
858
|
+
// eslint-disable-next-line no-console
|
|
859
|
+
console.log(` gh variable set QUALTY_ORG_ID --body "${orgId}"`);
|
|
860
|
+
// eslint-disable-next-line no-console
|
|
861
|
+
console.log("---");
|
|
862
|
+
// eslint-disable-next-line no-console
|
|
863
|
+
console.log("Name: QUALTY_PROJECT_ID");
|
|
864
|
+
// eslint-disable-next-line no-console
|
|
865
|
+
console.log("Value:");
|
|
866
|
+
// eslint-disable-next-line no-console
|
|
867
|
+
console.log(projectId);
|
|
868
|
+
// eslint-disable-next-line no-console
|
|
869
|
+
console.log(` gh variable set QUALTY_PROJECT_ID --body "${projectId}"`);
|
|
870
|
+
// eslint-disable-next-line no-console
|
|
871
|
+
console.log("---");
|
|
872
|
+
// eslint-disable-next-line no-console
|
|
873
|
+
console.log("Name: QUALTY_AUTH_PROFILE");
|
|
874
|
+
// eslint-disable-next-line no-console
|
|
875
|
+
console.log("Value:");
|
|
876
|
+
// eslint-disable-next-line no-console
|
|
877
|
+
console.log(profile);
|
|
878
|
+
// eslint-disable-next-line no-console
|
|
879
|
+
console.log(` gh variable set QUALTY_AUTH_PROFILE --body "${profile}"`);
|
|
734
880
|
// eslint-disable-next-line no-console
|
|
735
|
-
console.log("
|
|
881
|
+
console.log("---");
|
|
882
|
+
// eslint-disable-next-line no-console
|
|
883
|
+
console.log("Name: QUALTY_SUITE_ID");
|
|
884
|
+
// eslint-disable-next-line no-console
|
|
885
|
+
console.log("Value:");
|
|
886
|
+
// eslint-disable-next-line no-console
|
|
887
|
+
console.log(suiteId || "(suite UUID from Qualty dashboard — pass --suite-id)");
|
|
888
|
+
if (suiteId) {
|
|
889
|
+
// eslint-disable-next-line no-console
|
|
890
|
+
console.log(` gh variable set QUALTY_SUITE_ID --body "${suiteId}"`);
|
|
891
|
+
}
|
|
892
|
+
// eslint-disable-next-line no-console
|
|
893
|
+
console.log("");
|
|
894
|
+
// eslint-disable-next-line no-console
|
|
895
|
+
console.log("2. Add these GitHub secrets:");
|
|
736
896
|
}
|
|
737
|
-
|
|
738
|
-
console.log("---");
|
|
739
|
-
// eslint-disable-next-line no-console
|
|
740
|
-
console.log("Name: QUALTY_AUTH_STATE_B64");
|
|
741
|
-
if (skipPayloadPrint) {
|
|
897
|
+
if (!setGithubFlag) {
|
|
742
898
|
// eslint-disable-next-line no-console
|
|
743
|
-
console.log(
|
|
744
|
-
|
|
899
|
+
console.log("");
|
|
900
|
+
// eslint-disable-next-line no-console
|
|
901
|
+
console.log("---");
|
|
902
|
+
// eslint-disable-next-line no-console
|
|
903
|
+
console.log("Name: QUALTY_API_TOKEN");
|
|
745
904
|
// eslint-disable-next-line no-console
|
|
746
905
|
console.log("Value:");
|
|
906
|
+
if (includeToken && token) {
|
|
907
|
+
// eslint-disable-next-line no-console
|
|
908
|
+
console.log(token);
|
|
909
|
+
} else if (token) {
|
|
910
|
+
// eslint-disable-next-line no-console
|
|
911
|
+
console.log(`(your qlty_ci_ token — already configured locally as ${maskSecret(token)})`);
|
|
912
|
+
// eslint-disable-next-line no-console
|
|
913
|
+
console.log("Paste the full token from Qualty → Account → API tokens.");
|
|
914
|
+
// eslint-disable-next-line no-console
|
|
915
|
+
console.log("Re-run with --set-github to push via gh (or --include-token to print).");
|
|
916
|
+
} else {
|
|
917
|
+
// eslint-disable-next-line no-console
|
|
918
|
+
console.log("(create in Qualty → Account → API tokens; must start with qlty_ci_)");
|
|
919
|
+
}
|
|
920
|
+
// eslint-disable-next-line no-console
|
|
921
|
+
console.log("---");
|
|
922
|
+
// eslint-disable-next-line no-console
|
|
923
|
+
console.log("Name: QUALTY_AUTH_STATE_B64");
|
|
924
|
+
if (skipPayloadPrint) {
|
|
925
|
+
// eslint-disable-next-line no-console
|
|
926
|
+
console.log(
|
|
927
|
+
`Value: (see file above${copyToClipboardFlag ? " or clipboard" : ""})`
|
|
928
|
+
);
|
|
929
|
+
} else {
|
|
930
|
+
// eslint-disable-next-line no-console
|
|
931
|
+
console.log("Value:");
|
|
932
|
+
// eslint-disable-next-line no-console
|
|
933
|
+
console.log(encoded.payload);
|
|
934
|
+
}
|
|
935
|
+
// eslint-disable-next-line no-console
|
|
936
|
+
console.log("---");
|
|
747
937
|
// eslint-disable-next-line no-console
|
|
748
|
-
console.log(
|
|
938
|
+
console.log("");
|
|
749
939
|
}
|
|
940
|
+
|
|
941
|
+
const stepLabel = setGithubFlag ? "2" : "3";
|
|
750
942
|
// eslint-disable-next-line no-console
|
|
751
|
-
console.log(
|
|
752
|
-
// eslint-disable-next-line no-console
|
|
753
|
-
console.log("");
|
|
754
|
-
// eslint-disable-next-line no-console
|
|
755
|
-
console.log("3. Paste this workflow snippet before qualty run:");
|
|
943
|
+
console.log(`${stepLabel}. Workflow snippet (see templates/github-actions/qualty-local-pr.yml):`);
|
|
756
944
|
// eslint-disable-next-line no-console
|
|
757
945
|
console.log("");
|
|
758
946
|
// eslint-disable-next-line no-console
|
|
759
|
-
console.log(githubAuthRestoreShell({ orgId, projectId, profile
|
|
947
|
+
console.log(githubAuthRestoreShell({ orgId, projectId, profile }));
|
|
760
948
|
// eslint-disable-next-line no-console
|
|
761
949
|
console.log("");
|
|
762
950
|
// eslint-disable-next-line no-console
|
|
@@ -1113,10 +1301,9 @@ function printQualtyViewLogsReport(executionId, status) {
|
|
|
1113
1301
|
}
|
|
1114
1302
|
|
|
1115
1303
|
async function runCi(args) {
|
|
1116
|
-
const
|
|
1117
|
-
const
|
|
1118
|
-
const
|
|
1119
|
-
const orgId = resolveOrgId(args, config);
|
|
1304
|
+
const apiUrl = resolveApiUrl(args);
|
|
1305
|
+
const token = resolveToken(args);
|
|
1306
|
+
const orgId = resolveOrgId(args);
|
|
1120
1307
|
const projectId = args.project;
|
|
1121
1308
|
const suiteId = args["suite-id"];
|
|
1122
1309
|
const explicitIds = String(args.ids || "")
|
|
@@ -1460,10 +1647,9 @@ async function runLocalSuiteWithSequences({
|
|
|
1460
1647
|
}
|
|
1461
1648
|
|
|
1462
1649
|
async function runResolve(args) {
|
|
1463
|
-
const
|
|
1464
|
-
const
|
|
1465
|
-
const
|
|
1466
|
-
const orgId = resolveOrgId(args, config);
|
|
1650
|
+
const apiUrl = resolveApiUrl(args);
|
|
1651
|
+
const token = resolveToken(args);
|
|
1652
|
+
const orgId = resolveOrgId(args);
|
|
1467
1653
|
const projectId = args.project;
|
|
1468
1654
|
const suiteId = args["suite-id"];
|
|
1469
1655
|
const explicitIds = String(args.ids || "")
|
|
@@ -1626,8 +1812,8 @@ async function maybePromptProjectIdForLocalRun({ args, apiUrl, token, orgId }) {
|
|
|
1626
1812
|
|
|
1627
1813
|
function resolveLocalAuthStateForRun({ args, orgId, projectId }) {
|
|
1628
1814
|
if (parseBoolean(args["no-auth-state"], false)) return null;
|
|
1629
|
-
const
|
|
1630
|
-
const profile =
|
|
1815
|
+
const projectCfg = readProjectConfig();
|
|
1816
|
+
const profile = authProfileFromConfig(projectCfg, args["auth-profile"]);
|
|
1631
1817
|
if (!profile) return null;
|
|
1632
1818
|
const path = localAuthStatePath({ orgId, projectId, profile });
|
|
1633
1819
|
const summary = summarizeStorageStateFile(path);
|
|
@@ -1640,17 +1826,21 @@ function resolveLocalAuthStateForRun({ args, orgId, projectId }) {
|
|
|
1640
1826
|
}
|
|
1641
1827
|
|
|
1642
1828
|
async function maybePromptAuthProfileForLocalRun({ args, orgId, projectId }) {
|
|
1643
|
-
const
|
|
1829
|
+
const projectCfg = readProjectConfig();
|
|
1830
|
+
const requestedProfile = authProfileFromConfig(projectCfg, args["auth-profile"]);
|
|
1644
1831
|
if (requestedProfile) return { authProfile: requestedProfile, noAuthState: false };
|
|
1645
1832
|
if (parseBoolean(args["no-auth-state"], false)) return { authProfile: "", noAuthState: true };
|
|
1646
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
1833
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1834
|
+
const defaultProfile = authProfileFromConfig(projectCfg);
|
|
1835
|
+
if (defaultProfile) return { authProfile: defaultProfile, noAuthState: false };
|
|
1836
|
+
return { authProfile: "", noAuthState: false };
|
|
1837
|
+
}
|
|
1647
1838
|
if (!projectId) return { authProfile: "", noAuthState: false };
|
|
1648
1839
|
|
|
1649
1840
|
const profiles = listLocalAuthProfiles({ orgId, projectId });
|
|
1650
1841
|
if (profiles.length === 0) return { authProfile: "", noAuthState: false };
|
|
1651
1842
|
|
|
1652
|
-
const
|
|
1653
|
-
const defaultProfile = String(projectCfg.local_auth_profile || "").trim();
|
|
1843
|
+
const defaultProfile = authProfileFromConfig(projectCfg);
|
|
1654
1844
|
const defaultIndex = profiles.findIndex((p) => p === defaultProfile);
|
|
1655
1845
|
|
|
1656
1846
|
// eslint-disable-next-line no-console
|
|
@@ -1692,10 +1882,9 @@ async function maybePromptAuthProfileForLocalRun({ args, orgId, projectId }) {
|
|
|
1692
1882
|
}
|
|
1693
1883
|
|
|
1694
1884
|
async function runAuthSetup(args) {
|
|
1695
|
-
const
|
|
1696
|
-
const
|
|
1697
|
-
const
|
|
1698
|
-
const orgId = resolveOrgId(args, config);
|
|
1885
|
+
const apiUrl = resolveApiUrl(args);
|
|
1886
|
+
const token = resolveToken(args);
|
|
1887
|
+
const orgId = resolveOrgId(args);
|
|
1699
1888
|
if (!token) {
|
|
1700
1889
|
throw new Error("Missing auth token. Run `qualty setup` first.");
|
|
1701
1890
|
}
|
|
@@ -1849,9 +2038,9 @@ async function runAuthSetup(args) {
|
|
|
1849
2038
|
await browser.close();
|
|
1850
2039
|
|
|
1851
2040
|
writeProjectConfig({
|
|
1852
|
-
local_auth_project_id: project.id,
|
|
1853
|
-
local_auth_profile: profile,
|
|
1854
2041
|
default_org_id: orgId,
|
|
2042
|
+
default_project_id: project.id,
|
|
2043
|
+
default_auth_profile: profile,
|
|
1855
2044
|
});
|
|
1856
2045
|
try {
|
|
1857
2046
|
const synced = await registerLocalLoginProfileBinding({
|
|
@@ -1914,11 +2103,10 @@ async function main() {
|
|
|
1914
2103
|
throw new Error("Unknown auth subcommand. Use: qualty auth setup | qualty auth export-ci");
|
|
1915
2104
|
}
|
|
1916
2105
|
if (command === "run") {
|
|
1917
|
-
const config = readQualtyConfig();
|
|
1918
2106
|
if (parseBoolean(args.local, false)) {
|
|
1919
|
-
const apiUrl = resolveApiUrl(args
|
|
1920
|
-
const token = resolveToken(args
|
|
1921
|
-
const orgId = resolveOrgId(args
|
|
2107
|
+
const apiUrl = resolveApiUrl(args);
|
|
2108
|
+
const token = resolveToken(args);
|
|
2109
|
+
const orgId = resolveOrgId(args);
|
|
1922
2110
|
const projectId =
|
|
1923
2111
|
(await maybePromptProjectIdForLocalRun({
|
|
1924
2112
|
args,
|
package/bin/test-pw.mjs
ADDED