qualty 0.1.29 → 0.1.30
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 +246 -1029
- package/bin/qualty.js +153 -690
- package/package.json +1 -1
- package/bin/auth-ci-bundle.js +0 -348
package/bin/qualty.js
CHANGED
|
@@ -9,28 +9,19 @@ import {
|
|
|
9
9
|
readdirSync,
|
|
10
10
|
writeFileSync,
|
|
11
11
|
} from "node:fs";
|
|
12
|
+
import { gzipSync } from "node:zlib";
|
|
12
13
|
import { spawn, spawnSync } from "node:child_process";
|
|
13
14
|
import { homedir, hostname } from "node:os";
|
|
14
15
|
import { dirname, join } from "node:path";
|
|
15
16
|
import { createInterface } from "node:readline/promises";
|
|
16
17
|
import process from "node:process";
|
|
17
18
|
import { rewriteUrlForLocalRun, runLocalCi, summarizeStorageStateFile, logAuthStateDiagnostics } from "./local-runner.js";
|
|
18
|
-
import {
|
|
19
|
-
AUTH_PROFILES_VARIABLE,
|
|
20
|
-
AUTH_SECRET_PRIMARY,
|
|
21
|
-
AUTH_SHARDS_VARIABLE,
|
|
22
|
-
authShardSecretName,
|
|
23
|
-
buildAuthBundleManifest,
|
|
24
|
-
collectAuthShardPayloadsFromEnv,
|
|
25
|
-
restoreAuthPayloadsToDisk,
|
|
26
|
-
shardAuthBundleManifest,
|
|
27
|
-
} from "./auth-ci-bundle.js";
|
|
28
19
|
|
|
29
20
|
/** CLI backend URL is fixed by Qualty distribution. */
|
|
30
21
|
const DEFAULT_QUALTY_API_URL = "https://qualty-api-production.up.railway.app";
|
|
22
|
+
// const DEFAULT_QUALTY_API_URL = "http://localhost:8000";
|
|
31
23
|
|
|
32
24
|
const PROJECT_CONFIG_FILENAME = ".qualty.json";
|
|
33
|
-
const DOTENV_FILENAME = ".env";
|
|
34
25
|
const QUALTY_SHARED_CREDS_DIR = join(homedir(), ".qualty");
|
|
35
26
|
const QUALTY_SHARED_CREDS_PATH = join(QUALTY_SHARED_CREDS_DIR, "credentials");
|
|
36
27
|
|
|
@@ -49,47 +40,6 @@ function getProjectConfigWritePath() {
|
|
|
49
40
|
return findProjectConfigPath() || join(process.cwd(), PROJECT_CONFIG_FILENAME);
|
|
50
41
|
}
|
|
51
42
|
|
|
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
|
-
|
|
93
43
|
function readSharedCredentials() {
|
|
94
44
|
if (!existsSync(QUALTY_SHARED_CREDS_PATH)) return {};
|
|
95
45
|
try {
|
|
@@ -124,11 +74,6 @@ function authProfileFromConfig(projectCfg, fallback = "") {
|
|
|
124
74
|
return String(fallback || projectCfg.default_auth_profile || "").trim();
|
|
125
75
|
}
|
|
126
76
|
|
|
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
|
-
|
|
132
77
|
function readProjectConfig() {
|
|
133
78
|
const configPath = findProjectConfigPath();
|
|
134
79
|
if (!configPath) return {};
|
|
@@ -171,14 +116,7 @@ function normalizeApiUrl(raw, { label = "API URL" } = {}) {
|
|
|
171
116
|
}
|
|
172
117
|
|
|
173
118
|
function qualtyBaseUrlFromEnv() {
|
|
174
|
-
|
|
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();
|
|
119
|
+
return String(process.env.QUALTY_BASE_URL || process.env.QUALTY_API_URL || "").trim();
|
|
182
120
|
}
|
|
183
121
|
|
|
184
122
|
function resolveApiUrl(args = {}) {
|
|
@@ -196,14 +134,8 @@ function resolveApiUrl(args = {}) {
|
|
|
196
134
|
}
|
|
197
135
|
|
|
198
136
|
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;
|
|
205
137
|
const creds = readSharedCredentials();
|
|
206
|
-
return
|
|
138
|
+
return args.token || process.env.QUALTY_API_TOKEN || creds.api_token || "";
|
|
207
139
|
}
|
|
208
140
|
|
|
209
141
|
function resolveOrgId(args = {}) {
|
|
@@ -247,19 +179,11 @@ function usage() {
|
|
|
247
179
|
"Usage:",
|
|
248
180
|
" qualty setup [--token <bearer-token>] [--org <org-id>]",
|
|
249
181
|
" qualty auth setup [--project <project-id>] [--profile <profile-name>] [--headed]",
|
|
250
|
-
" qualty auth export [--project <project-id>] [--profile <profile-name>] [--org <org-id>]",
|
|
251
|
-
"
|
|
252
|
-
"
|
|
253
|
-
"
|
|
254
|
-
"
|
|
255
|
-
" [--all-profiles] [--profiles <name1,name2>]",
|
|
256
|
-
" [--suite-id <suite-uuid>] [--ids <id1,id2>] bundle login profiles required by tests",
|
|
257
|
-
" [--url <api-base-url>] [--repo <owner/repo>]",
|
|
258
|
-
" [--output <path>] [--no-file] [--copy] [--gzip] [--include-token]",
|
|
259
|
-
" [--set-secret | --set-github] gh: secrets (token, API URL, auth bundle) + variables",
|
|
260
|
-
" [--cli-repo <owner/repo>] [--cli-ref <branch>] Optional CI variables",
|
|
261
|
-
" qualty auth restore-ci [--org <org-id>] [--project <project-id>] [--profile <fallback-profile>]",
|
|
262
|
-
" Reads QUALTY_AUTH_STATE_B64 (+ _2, _3, … shards) and writes ~/.qualty/local-contexts/…",
|
|
182
|
+
" qualty auth export-ci [--project <project-id>] [--profile <profile-name>] [--org <org-id>]",
|
|
183
|
+
" [--url <api-base-url>] [--suite-id <suite-uuid>] [--repo <owner/repo>]",
|
|
184
|
+
" [--output <path>] [--no-file] [--copy] [--gzip] [--include-token]",
|
|
185
|
+
" [--set-secret | --set-github] gh: secrets (token, API URL, auth) + variables (org, project, profile, …)",
|
|
186
|
+
" [--cli-repo <owner/repo>] [--cli-ref <branch>] Optional CI variables",
|
|
263
187
|
" qualty connect --project <project-id> [--port 3000] [--token <bearer-token>] [--org <org-id>]",
|
|
264
188
|
" qualty run --project <project-id> [--suite-id <suite-id>] [--ids <id1,id2>] [--local] [--token <bearer-token>] [--org <org-id>]",
|
|
265
189
|
" [--poll-interval 5] [--timeout 30] [--fail-on-failure true] [--no-view-logs]",
|
|
@@ -270,11 +194,11 @@ function usage() {
|
|
|
270
194
|
" qualty resolve --project <project-id> [--suite-id <suite-id>] [--ids <id1,id2>] [--json] [--token <bearer-token>]",
|
|
271
195
|
"",
|
|
272
196
|
"Env vars:",
|
|
273
|
-
" QUALTY_API_TOKEN
|
|
197
|
+
" QUALTY_API_TOKEN Bearer token used for auth",
|
|
274
198
|
" QUALTY_BASE_URL API base URL when --url is not set (default: production)",
|
|
275
199
|
" QUALTY_LOCAL_CONCURRENCY Local parallel workers for --local runs (default: 4)",
|
|
276
200
|
"",
|
|
277
|
-
"Config (.qualty.json): default_org_id, default_project_id, default_auth_profile
|
|
201
|
+
"Config (.qualty.json): default_org_id, default_project_id, default_auth_profile",
|
|
278
202
|
" CLI flags (--org, --project, --profile, --url) override config values.",
|
|
279
203
|
].join("\n")
|
|
280
204
|
);
|
|
@@ -457,7 +381,9 @@ async function runSetup(args) {
|
|
|
457
381
|
|
|
458
382
|
try {
|
|
459
383
|
const currentOrgId = String(args.org || projectCfg.default_org_id || "");
|
|
460
|
-
const currentToken =
|
|
384
|
+
const currentToken = String(
|
|
385
|
+
args.token || process.env.QUALTY_API_TOKEN || sharedCreds.api_token || ""
|
|
386
|
+
);
|
|
461
387
|
|
|
462
388
|
let token = "";
|
|
463
389
|
if (args.token) {
|
|
@@ -604,10 +530,12 @@ function localAuthStatePath({ orgId, projectId, profile }) {
|
|
|
604
530
|
return join(QUALTY_SHARED_CREDS_DIR, "local-contexts", orgSeg, projectSeg, `${profileSeg}.json`);
|
|
605
531
|
}
|
|
606
532
|
|
|
607
|
-
function
|
|
533
|
+
function authExportCiOutputPath(statePath) {
|
|
608
534
|
return statePath.replace(/\.json$/i, ".b64");
|
|
609
535
|
}
|
|
610
536
|
|
|
537
|
+
const GITHUB_SECRET_MAX_BYTES = 64 * 1024;
|
|
538
|
+
|
|
611
539
|
function copyToClipboard(text) {
|
|
612
540
|
if (process.platform === "darwin") {
|
|
613
541
|
const result = spawnSync("pbcopy", { input: text, encoding: "utf8" });
|
|
@@ -673,8 +601,7 @@ function syncGithubCiConfig({
|
|
|
673
601
|
orgId,
|
|
674
602
|
projectId,
|
|
675
603
|
profile,
|
|
676
|
-
|
|
677
|
-
authShards,
|
|
604
|
+
authPayload,
|
|
678
605
|
apiUrl,
|
|
679
606
|
token,
|
|
680
607
|
suiteId,
|
|
@@ -686,9 +613,8 @@ function syncGithubCiConfig({
|
|
|
686
613
|
results.push({ kind, name, ...outcome });
|
|
687
614
|
};
|
|
688
615
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
}
|
|
616
|
+
const authResult = setGithubSecret("QUALTY_AUTH_STATE_B64", authPayload, { repo });
|
|
617
|
+
record("secret", "QUALTY_AUTH_STATE_B64", authResult);
|
|
692
618
|
|
|
693
619
|
if (token) {
|
|
694
620
|
record("secret", "QUALTY_API_TOKEN", setGithubSecret("QUALTY_API_TOKEN", token, { repo }));
|
|
@@ -706,21 +632,7 @@ function syncGithubCiConfig({
|
|
|
706
632
|
|
|
707
633
|
record("variable", "QUALTY_ORG_ID", setGithubVariable("QUALTY_ORG_ID", orgId, { repo }));
|
|
708
634
|
record("variable", "QUALTY_PROJECT_ID", setGithubVariable("QUALTY_PROJECT_ID", String(projectId), { repo }));
|
|
709
|
-
|
|
710
|
-
const names = (profileNames || []).filter(Boolean);
|
|
711
|
-
if (names.length > 0) {
|
|
712
|
-
record("variable", AUTH_PROFILES_VARIABLE, setGithubVariable(AUTH_PROFILES_VARIABLE, names.join(","), { repo }));
|
|
713
|
-
}
|
|
714
|
-
if (profile) {
|
|
715
|
-
record("variable", "QUALTY_AUTH_PROFILE", setGithubVariable("QUALTY_AUTH_PROFILE", profile, { repo }));
|
|
716
|
-
}
|
|
717
|
-
if ((authShards || []).length > 1) {
|
|
718
|
-
record(
|
|
719
|
-
"variable",
|
|
720
|
-
AUTH_SHARDS_VARIABLE,
|
|
721
|
-
setGithubVariable(AUTH_SHARDS_VARIABLE, String(authShards.length), { repo })
|
|
722
|
-
);
|
|
723
|
-
}
|
|
635
|
+
record("variable", "QUALTY_AUTH_PROFILE", setGithubVariable("QUALTY_AUTH_PROFILE", profile, { repo }));
|
|
724
636
|
|
|
725
637
|
if (suiteId) {
|
|
726
638
|
record("variable", "QUALTY_SUITE_ID", setGithubVariable("QUALTY_SUITE_ID", String(suiteId), { repo }));
|
|
@@ -736,37 +648,6 @@ function syncGithubCiConfig({
|
|
|
736
648
|
return results;
|
|
737
649
|
}
|
|
738
650
|
|
|
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
|
-
|
|
770
651
|
function printGithubSyncResults(results, { repo }) {
|
|
771
652
|
const target = repo ? ` (repo ${repo})` : "";
|
|
772
653
|
const printGroup = (title, rows) => {
|
|
@@ -847,7 +728,7 @@ function configStringValue(projectCfg, key, configPath) {
|
|
|
847
728
|
);
|
|
848
729
|
}
|
|
849
730
|
|
|
850
|
-
function
|
|
731
|
+
function missingExportCiField(fieldLabel, { flag, configKey, configPath, hasFlag }) {
|
|
851
732
|
const configHint = configPath
|
|
852
733
|
? `${PROJECT_CONFIG_FILENAME} at ${configPath} (${configKey})`
|
|
853
734
|
: `${PROJECT_CONFIG_FILENAME} (not found walking up from ${process.cwd()})`;
|
|
@@ -863,22 +744,12 @@ function missingCiSetupField(fieldLabel, { flag, configKey, configPath, hasFlag
|
|
|
863
744
|
].join(" ");
|
|
864
745
|
}
|
|
865
746
|
|
|
866
|
-
function
|
|
747
|
+
function resolveExportCiConfig(args) {
|
|
867
748
|
const { configPath, config: projectCfg } = readProjectConfigStrict();
|
|
868
749
|
|
|
869
750
|
const orgFromFlag = String(args.org || "").trim();
|
|
870
751
|
const projectFromFlag = String(args.project || "").trim();
|
|
871
752
|
const profileFromFlag = String(args.profile || args["auth-profile"] || "").trim();
|
|
872
|
-
const allProfiles = parseBoolean(args["all-profiles"], false);
|
|
873
|
-
const profilesList = String(args.profiles || "")
|
|
874
|
-
.split(",")
|
|
875
|
-
.map((part) => part.trim())
|
|
876
|
-
.filter(Boolean);
|
|
877
|
-
const suiteId = String(args["suite-id"] || args.suite || "").trim();
|
|
878
|
-
const explicitIds = String(args.ids || "")
|
|
879
|
-
.split(",")
|
|
880
|
-
.map((part) => part.trim())
|
|
881
|
-
.filter(Boolean);
|
|
882
753
|
|
|
883
754
|
let orgId = orgFromFlag;
|
|
884
755
|
if (!orgId) {
|
|
@@ -897,7 +768,7 @@ function resolveCiSetupConfig(args, { requireProfile = true } = {}) {
|
|
|
897
768
|
|
|
898
769
|
if (!orgId) {
|
|
899
770
|
throw new Error(
|
|
900
|
-
|
|
771
|
+
missingExportCiField("org ID", {
|
|
901
772
|
flag: "--org",
|
|
902
773
|
configKey: "default_org_id",
|
|
903
774
|
configPath,
|
|
@@ -907,7 +778,7 @@ function resolveCiSetupConfig(args, { requireProfile = true } = {}) {
|
|
|
907
778
|
}
|
|
908
779
|
if (!projectId) {
|
|
909
780
|
throw new Error(
|
|
910
|
-
|
|
781
|
+
missingExportCiField("project ID", {
|
|
911
782
|
flag: "--project",
|
|
912
783
|
configKey: "default_project_id",
|
|
913
784
|
configPath,
|
|
@@ -915,12 +786,9 @@ function resolveCiSetupConfig(args, { requireProfile = true } = {}) {
|
|
|
915
786
|
})
|
|
916
787
|
);
|
|
917
788
|
}
|
|
918
|
-
|
|
919
|
-
const hasMultiProfileMode =
|
|
920
|
-
allProfiles || profilesList.length > 0 || Boolean(suiteId) || explicitIds.length > 0;
|
|
921
|
-
if (requireProfile && !profile && !hasMultiProfileMode) {
|
|
789
|
+
if (!profile) {
|
|
922
790
|
throw new Error(
|
|
923
|
-
|
|
791
|
+
missingExportCiField("auth profile", {
|
|
924
792
|
flag: "--profile",
|
|
925
793
|
configKey: "default_auth_profile",
|
|
926
794
|
configPath,
|
|
@@ -936,163 +804,43 @@ function resolveCiSetupConfig(args, { requireProfile = true } = {}) {
|
|
|
936
804
|
throw new Error(`Invalid API URL: ${err.message}`);
|
|
937
805
|
}
|
|
938
806
|
|
|
939
|
-
return {
|
|
940
|
-
orgId,
|
|
941
|
-
projectId,
|
|
942
|
-
profile,
|
|
943
|
-
apiUrl,
|
|
944
|
-
configPath,
|
|
945
|
-
allProfiles,
|
|
946
|
-
profilesList,
|
|
947
|
-
suiteId,
|
|
948
|
-
explicitIds,
|
|
949
|
-
};
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
function jobUsesSavedLogin(job) {
|
|
953
|
-
if (!job) return false;
|
|
954
|
-
if (job.use_saved_login) return true;
|
|
955
|
-
if (job.saved_login_profile_id) return true;
|
|
956
|
-
const profile = job.saved_login_profile;
|
|
957
|
-
return Boolean(profile && typeof profile === "object" && (profile.id || profile.name));
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
async function fetchSavedLoginProfilesForProject({ apiUrl, token, orgId, projectId }) {
|
|
961
|
-
const payload = await apiRequest({
|
|
962
|
-
apiUrl,
|
|
963
|
-
token,
|
|
964
|
-
orgId,
|
|
965
|
-
path: `/api/v1/projects/${encodeURIComponent(String(projectId))}/saved-login-profiles`,
|
|
966
|
-
}).catch(() => []);
|
|
967
|
-
return Array.isArray(payload) ? payload : [];
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
function localProfileLabelFromApiRow(row, fallbackName = "") {
|
|
971
|
-
return String(row?.local_profile_name || row?.name || fallbackName || "").trim();
|
|
807
|
+
return { orgId, projectId, profile, apiUrl, configPath };
|
|
972
808
|
}
|
|
973
809
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
}
|
|
982
|
-
const jobsPayload = await resolveSavedJobs({
|
|
983
|
-
apiUrl,
|
|
984
|
-
token,
|
|
985
|
-
orgId,
|
|
986
|
-
projectId,
|
|
987
|
-
suiteId,
|
|
988
|
-
explicitIds,
|
|
989
|
-
});
|
|
990
|
-
const jobs = Array.isArray(jobsPayload)
|
|
991
|
-
? jobsPayload
|
|
992
|
-
: Array.isArray(jobsPayload?.results)
|
|
993
|
-
? jobsPayload.results
|
|
994
|
-
: Array.isArray(jobsPayload?.jobs)
|
|
995
|
-
? jobsPayload.jobs
|
|
996
|
-
: [];
|
|
997
|
-
const names = new Set();
|
|
998
|
-
for (const job of jobs) {
|
|
999
|
-
if (!jobUsesSavedLogin(job)) continue;
|
|
1000
|
-
const name = String(job?.saved_login_profile?.name || "").trim();
|
|
1001
|
-
if (name) names.add(name);
|
|
1002
|
-
}
|
|
1003
|
-
return [...names];
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
async function resolveCiSetupProfileEntries({
|
|
1007
|
-
orgId,
|
|
1008
|
-
projectId,
|
|
1009
|
-
profile,
|
|
1010
|
-
allProfiles,
|
|
1011
|
-
profilesList,
|
|
1012
|
-
suiteId,
|
|
1013
|
-
explicitIds,
|
|
1014
|
-
apiUrl,
|
|
1015
|
-
token,
|
|
1016
|
-
}) {
|
|
1017
|
-
const apiProfiles = token
|
|
1018
|
-
? await fetchSavedLoginProfilesForProject({ apiUrl, token, orgId, projectId })
|
|
1019
|
-
: [];
|
|
1020
|
-
|
|
1021
|
-
let requestedNames = [];
|
|
1022
|
-
if (allProfiles) {
|
|
1023
|
-
requestedNames = apiProfiles.map((row) => String(row?.name || "").trim()).filter(Boolean);
|
|
1024
|
-
if (requestedNames.length === 0) {
|
|
1025
|
-
requestedNames = listLocalAuthProfiles({ orgId, projectId });
|
|
1026
|
-
}
|
|
1027
|
-
} else if (profilesList.length > 0) {
|
|
1028
|
-
requestedNames = profilesList;
|
|
1029
|
-
} else if (suiteId || explicitIds.length > 0) {
|
|
1030
|
-
if (!token) {
|
|
1031
|
-
throw new Error("QUALTY_API_TOKEN is required to resolve profiles from --suite-id or --ids.");
|
|
1032
|
-
}
|
|
1033
|
-
requestedNames = await resolveRequiredProfileNamesFromTests({
|
|
1034
|
-
apiUrl,
|
|
1035
|
-
token,
|
|
1036
|
-
orgId,
|
|
1037
|
-
projectId,
|
|
1038
|
-
suiteId,
|
|
1039
|
-
explicitIds,
|
|
1040
|
-
});
|
|
1041
|
-
if (requestedNames.length === 0 && profile) {
|
|
1042
|
-
requestedNames = [profile];
|
|
1043
|
-
}
|
|
1044
|
-
} else if (profile) {
|
|
1045
|
-
requestedNames = [profile];
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
requestedNames = [...new Set(requestedNames.filter(Boolean))];
|
|
1049
|
-
if (requestedNames.length === 0) {
|
|
1050
|
-
throw new Error(
|
|
1051
|
-
"No auth profiles selected. Pass --profile, --profiles, --all-profiles, or --suite-id / --ids."
|
|
1052
|
-
);
|
|
810
|
+
function encodeAuthStateForGithubSecret(statePath, { gzip = false } = {}) {
|
|
811
|
+
const raw = readFileSync(statePath);
|
|
812
|
+
if (gzip) {
|
|
813
|
+
return {
|
|
814
|
+
encoding: "gzip+base64",
|
|
815
|
+
payload: gzipSync(raw).toString("base64"),
|
|
816
|
+
rawBytes: raw.length,
|
|
817
|
+
};
|
|
1053
818
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
const localProfileName = localProfileLabelFromApiRow(apiRow, dashboardName) || dashboardName;
|
|
1060
|
-
const statePath = localAuthStatePath({ orgId, projectId, profile: localProfileName });
|
|
1061
|
-
if (!existsSync(statePath)) {
|
|
1062
|
-
missing.push({ dashboardName, localProfileName, statePath });
|
|
1063
|
-
continue;
|
|
1064
|
-
}
|
|
1065
|
-
entries.push({
|
|
1066
|
-
name: dashboardName,
|
|
1067
|
-
localProfileName,
|
|
1068
|
-
statePath,
|
|
1069
|
-
});
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
if (missing.length > 0) {
|
|
1073
|
-
const lines = missing.map(
|
|
1074
|
-
(row) =>
|
|
1075
|
-
` - "${row.dashboardName}" → ${row.statePath}\n qualty auth setup --project ${projectId} --profile "${row.dashboardName}"`
|
|
1076
|
-
);
|
|
1077
|
-
throw new Error(["Local auth state missing for:", ...lines].join("\n"));
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
return entries;
|
|
819
|
+
return {
|
|
820
|
+
encoding: "base64",
|
|
821
|
+
payload: raw.toString("base64"),
|
|
822
|
+
rawBytes: raw.length,
|
|
823
|
+
};
|
|
1081
824
|
}
|
|
1082
825
|
|
|
1083
|
-
function githubAuthRestoreShell({
|
|
1084
|
-
const shardEnvLines = [];
|
|
1085
|
-
for (let index = 1; index <= Math.max(1, shardCount); index += 1) {
|
|
1086
|
-
const secretName = authShardSecretName(index);
|
|
1087
|
-
shardEnvLines.push(` ${secretName}: \${{ secrets.${secretName} }}`);
|
|
1088
|
-
}
|
|
826
|
+
function githubAuthRestoreShell({ orgId, projectId, profile }) {
|
|
1089
827
|
return [
|
|
1090
|
-
`- name: Restore Qualty login
|
|
828
|
+
`- name: Restore Qualty login state`,
|
|
1091
829
|
` env:`,
|
|
1092
|
-
|
|
1093
|
-
`
|
|
1094
|
-
`
|
|
1095
|
-
`
|
|
830
|
+
` QUALTY_AUTH_STATE_B64: \${{ secrets.QUALTY_AUTH_STATE_B64 }}`,
|
|
831
|
+
` run: |`,
|
|
832
|
+
` set -euo pipefail`,
|
|
833
|
+
` slug_segment() {`,
|
|
834
|
+
` printf '%s' "$1" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g; s/[^a-zA-Z0-9._-]+/_/g; s/^_+|_+$//g'`,
|
|
835
|
+
` }`,
|
|
836
|
+
` org_seg=$(slug_segment "$QUALTY_ORG_ID"); [ -n "$org_seg" ] || org_seg="default-org"`,
|
|
837
|
+
` project_seg=$(slug_segment "$QUALTY_PROJECT_ID"); [ -n "$project_seg" ] || project_seg="default-project"`,
|
|
838
|
+
` profile_seg=$(slug_segment "$QUALTY_AUTH_PROFILE"); [ -n "$profile_seg" ] || profile_seg="default"`,
|
|
839
|
+
` auth_dir="$HOME/.qualty/local-contexts/\${org_seg}/\${project_seg}"`,
|
|
840
|
+
` auth_file="\${auth_dir}/\${profile_seg}.json"`,
|
|
841
|
+
` mkdir -p "$auth_dir"`,
|
|
842
|
+
` echo "$QUALTY_AUTH_STATE_B64" | base64 -d | gunzip > "$auth_file"`,
|
|
843
|
+
` test -s "$auth_file"`,
|
|
1096
844
|
``,
|
|
1097
845
|
`- name: Run Qualty tests (local)`,
|
|
1098
846
|
` env:`,
|
|
@@ -1103,311 +851,84 @@ function githubAuthRestoreShell({ shardCount = 1 } = {}) {
|
|
|
1103
851
|
` --org "$QUALTY_ORG_ID" \\`,
|
|
1104
852
|
` --project "$QUALTY_PROJECT_ID" \\`,
|
|
1105
853
|
` --suite-id "YOUR-SUITE-UUID" \\`,
|
|
854
|
+
` --auth-profile "$QUALTY_AUTH_PROFILE" \\`,
|
|
1106
855
|
` --fail-on-failure true`,
|
|
1107
|
-
` # Omit --auth-profile when tests use different saved login profiles.`,
|
|
1108
|
-
` # Add --auth-profile "Profile Name" only to force one profile for the whole run.`,
|
|
1109
856
|
].join("\n");
|
|
1110
857
|
}
|
|
1111
858
|
|
|
1112
|
-
async function
|
|
1113
|
-
const orgId
|
|
1114
|
-
const
|
|
1115
|
-
const fallbackProfile = String(
|
|
1116
|
-
args.profile || args["auth-profile"] || process.env.QUALTY_AUTH_PROFILE || ""
|
|
1117
|
-
).trim();
|
|
1118
|
-
|
|
1119
|
-
if (!orgId) {
|
|
1120
|
-
throw new Error("Missing org ID. Set QUALTY_ORG_ID or pass --org.");
|
|
1121
|
-
}
|
|
1122
|
-
if (!projectId) {
|
|
1123
|
-
throw new Error("Missing project ID. Set QUALTY_PROJECT_ID or pass --project.");
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
const payloads = collectAuthShardPayloadsFromEnv(process.env);
|
|
1127
|
-
if (payloads.length === 0) {
|
|
1128
|
-
throw new Error(
|
|
1129
|
-
`Missing ${AUTH_SECRET_PRIMARY}. Run qualty ci setup --set-github (first time) or qualty auth export --set-github (profile refresh).`
|
|
1130
|
-
);
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
const restored = restoreAuthPayloadsToDisk({
|
|
1134
|
-
payloads,
|
|
1135
|
-
orgId,
|
|
1136
|
-
projectId,
|
|
1137
|
-
fallbackProfile,
|
|
1138
|
-
});
|
|
1139
|
-
|
|
1140
|
-
for (const row of restored) {
|
|
1141
|
-
// eslint-disable-next-line no-console
|
|
1142
|
-
console.log(`Restored login state: ${row.path} (profile "${row.name}")`);
|
|
1143
|
-
}
|
|
1144
|
-
// eslint-disable-next-line no-console
|
|
1145
|
-
console.log(`Restored ${restored.length} login profile(s).`);
|
|
1146
|
-
}
|
|
859
|
+
async function runAuthExportCi(args) {
|
|
860
|
+
const { orgId, projectId, profile, apiUrl, configPath } = resolveExportCiConfig(args);
|
|
861
|
+
const token = resolveToken(args);
|
|
1147
862
|
|
|
1148
|
-
|
|
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
|
-
) {
|
|
863
|
+
const statePath = localAuthStatePath({ orgId, projectId, profile });
|
|
864
|
+
if (!existsSync(statePath)) {
|
|
1157
865
|
throw new Error(
|
|
1158
|
-
|
|
866
|
+
[
|
|
867
|
+
"Local auth state not found:",
|
|
868
|
+
` ${statePath}`,
|
|
869
|
+
"",
|
|
870
|
+
"Create it first:",
|
|
871
|
+
` qualty auth setup --project ${projectId} --profile "${profile}"`,
|
|
872
|
+
configPath ? ` (org/project/profile from ${configPath})` : "",
|
|
873
|
+
]
|
|
874
|
+
.filter(Boolean)
|
|
875
|
+
.join("\n")
|
|
1159
876
|
);
|
|
1160
877
|
}
|
|
1161
878
|
|
|
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
879
|
const setGithubFlag =
|
|
1190
880
|
parseBoolean(args["set-github"], false) || parseBoolean(args["set-secret"], false);
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
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)`
|
|
881
|
+
// CI path: one format only — gzip then base64 in QUALTY_AUTH_STATE_B64 (no separate gzip flag).
|
|
882
|
+
let gzip = parseBoolean(args.gzip, false) || setGithubFlag;
|
|
883
|
+
let encoded;
|
|
884
|
+
try {
|
|
885
|
+
encoded = encodeAuthStateForGithubSecret(statePath, { gzip });
|
|
886
|
+
} catch (err) {
|
|
887
|
+
throw new Error(
|
|
888
|
+
`Could not read auth state at ${statePath}: ${err.message}`
|
|
1240
889
|
);
|
|
1241
890
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
console.log(`Wrote ${shard.secretName} to ${shardPath}`);
|
|
891
|
+
let secretBytes = Buffer.byteLength(encoded.payload, "utf8");
|
|
892
|
+
if (!gzip && secretBytes > GITHUB_SECRET_MAX_BYTES - 1024) {
|
|
893
|
+
gzip = true;
|
|
894
|
+
try {
|
|
895
|
+
encoded = encodeAuthStateForGithubSecret(statePath, { gzip: true });
|
|
896
|
+
} catch (err) {
|
|
897
|
+
throw new Error(
|
|
898
|
+
`Could not gzip auth state at ${statePath}: ${err.message}`
|
|
899
|
+
);
|
|
1252
900
|
}
|
|
1253
|
-
|
|
1254
|
-
console.log("");
|
|
901
|
+
secretBytes = Buffer.byteLength(encoded.payload, "utf8");
|
|
1255
902
|
}
|
|
1256
|
-
if (
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
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)."
|
|
903
|
+
if (secretBytes > GITHUB_SECRET_MAX_BYTES - 1024) {
|
|
904
|
+
throw new Error(
|
|
905
|
+
`Auth state is too large for a GitHub secret (${secretBytes} bytes; limit ~64 KB). ` +
|
|
906
|
+
"Try a shorter-lived session, fewer cookies, or contact Qualty support."
|
|
1306
907
|
);
|
|
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
908
|
}
|
|
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
909
|
|
|
1339
|
-
async function runCiSetup(args) {
|
|
1340
|
-
const {
|
|
1341
|
-
orgId,
|
|
1342
|
-
projectId,
|
|
1343
|
-
profile,
|
|
1344
|
-
apiUrl,
|
|
1345
|
-
configPath,
|
|
1346
|
-
allProfiles,
|
|
1347
|
-
profilesList,
|
|
1348
|
-
suiteId: configSuiteId,
|
|
1349
|
-
explicitIds,
|
|
1350
|
-
} = resolveCiSetupConfig(args);
|
|
1351
|
-
const token = resolveToken(args);
|
|
1352
|
-
|
|
1353
|
-
const profileEntries = await resolveCiSetupProfileEntries({
|
|
1354
|
-
orgId,
|
|
1355
|
-
projectId,
|
|
1356
|
-
profile,
|
|
1357
|
-
allProfiles,
|
|
1358
|
-
profilesList,
|
|
1359
|
-
suiteId: configSuiteId,
|
|
1360
|
-
explicitIds,
|
|
1361
|
-
apiUrl,
|
|
1362
|
-
token,
|
|
1363
|
-
});
|
|
1364
|
-
const profileNames = profileEntries.map((entry) => entry.name);
|
|
1365
|
-
const primaryProfile = profile || profileNames[0] || "";
|
|
1366
|
-
|
|
1367
|
-
const manifest = buildAuthBundleManifest({ orgId, projectId, profileEntries });
|
|
1368
|
-
const authShards = shardAuthBundleManifest(manifest);
|
|
1369
|
-
const primaryShard = authShards[0];
|
|
1370
|
-
|
|
1371
|
-
const setGithubFlag =
|
|
1372
|
-
parseBoolean(args["set-github"], false) || parseBoolean(args["set-secret"], false);
|
|
1373
910
|
const noFile = parseBoolean(args["no-file"], false);
|
|
1374
911
|
const outputPath = noFile
|
|
1375
912
|
? String(args.output || "").trim()
|
|
1376
|
-
: String(args.output || "").trim() ||
|
|
1377
|
-
ciSetupAuthBundleOutputPath(
|
|
1378
|
-
localAuthStatePath({ orgId, projectId, profile: profileEntries[0].localProfileName })
|
|
1379
|
-
);
|
|
913
|
+
: String(args.output || "").trim() || authExportCiOutputPath(statePath);
|
|
1380
914
|
const copyToClipboardFlag = parseBoolean(args.copy, false);
|
|
1381
915
|
const githubRepo = String(args.repo || "").trim();
|
|
1382
|
-
const suiteId = String(args["suite-id"] || args.suite ||
|
|
916
|
+
const suiteId = String(args["suite-id"] || args.suite || "").trim();
|
|
1383
917
|
const cliRepo = String(args["cli-repo"] || "").trim();
|
|
1384
918
|
const cliRef = String(args["cli-ref"] || "").trim();
|
|
1385
919
|
|
|
1386
920
|
if (outputPath) {
|
|
1387
|
-
writeFileSync(outputPath,
|
|
921
|
+
writeFileSync(outputPath, encoded.payload, "utf8");
|
|
1388
922
|
try {
|
|
1389
923
|
chmodSync(outputPath, 0o600);
|
|
1390
924
|
} catch {
|
|
1391
925
|
// ignore platforms that do not support chmod
|
|
1392
926
|
}
|
|
1393
|
-
for (let index = 1; index < authShards.length; index += 1) {
|
|
1394
|
-
const shard = authShards[index];
|
|
1395
|
-
const shardPath = outputPath.replace(/(\.[^./]+)?$/, `_${index + 1}$1`);
|
|
1396
|
-
writeFileSync(shardPath, shard.payload, "utf8");
|
|
1397
|
-
try {
|
|
1398
|
-
chmodSync(shardPath, 0o600);
|
|
1399
|
-
} catch {
|
|
1400
|
-
// ignore
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
927
|
}
|
|
1404
928
|
|
|
1405
929
|
const includeToken = parseBoolean(args["include-token"], false);
|
|
930
|
+
const profileSeg = safePathSegment(profile, "default");
|
|
1406
931
|
const skipPayloadPrint = Boolean(outputPath || copyToClipboardFlag || setGithubFlag);
|
|
1407
|
-
const totalSecretBytes = authShards.reduce(
|
|
1408
|
-
(sum, shard) => sum + Buffer.byteLength(shard.payload, "utf8"),
|
|
1409
|
-
0
|
|
1410
|
-
);
|
|
1411
932
|
|
|
1412
933
|
// eslint-disable-next-line no-console
|
|
1413
934
|
console.log("");
|
|
@@ -1415,29 +936,20 @@ async function runCiSetup(args) {
|
|
|
1415
936
|
console.log("=== Qualty → GitHub Actions (secrets & variables) ===");
|
|
1416
937
|
// eslint-disable-next-line no-console
|
|
1417
938
|
console.log("");
|
|
1418
|
-
|
|
1419
|
-
console.log(
|
|
1420
|
-
`Auth bundle: ${profileNames.length} profile(s), ${authShards.length} secret shard(s), ${totalSecretBytes} bytes total`
|
|
1421
|
-
);
|
|
1422
|
-
for (const shard of authShards) {
|
|
939
|
+
if (encoded.encoding === "gzip+base64") {
|
|
1423
940
|
// eslint-disable-next-line no-console
|
|
1424
|
-
console.log(
|
|
1425
|
-
|
|
1426
|
-
|
|
941
|
+
console.log(`Auth state: gzip+base64 (${encoded.rawBytes} bytes raw → ${secretBytes} bytes secret)`);
|
|
942
|
+
} else {
|
|
943
|
+
// eslint-disable-next-line no-console
|
|
944
|
+
console.log(`Auth state: base64 (${encoded.rawBytes} bytes raw → ${secretBytes} bytes secret)`);
|
|
1427
945
|
}
|
|
1428
946
|
// eslint-disable-next-line no-console
|
|
1429
947
|
console.log("");
|
|
1430
948
|
if (outputPath) {
|
|
1431
949
|
// eslint-disable-next-line no-console
|
|
1432
|
-
console.log(`Wrote
|
|
1433
|
-
for (let index = 1; index < authShards.length; index += 1) {
|
|
1434
|
-
const shard = authShards[index];
|
|
1435
|
-
const shardPath = outputPath.replace(/(\.[^./]+)?$/, `_${index + 1}$1`);
|
|
1436
|
-
// eslint-disable-next-line no-console
|
|
1437
|
-
console.log(`Wrote ${shard.secretName} to ${shardPath}`);
|
|
1438
|
-
}
|
|
950
|
+
console.log(`Wrote QUALTY_AUTH_STATE_B64 to ${outputPath}`);
|
|
1439
951
|
// eslint-disable-next-line no-console
|
|
1440
|
-
console.log(` gh secret set
|
|
952
|
+
console.log(` gh secret set QUALTY_AUTH_STATE_B64 < ${outputPath}`);
|
|
1441
953
|
// eslint-disable-next-line no-console
|
|
1442
954
|
console.log("");
|
|
1443
955
|
}
|
|
@@ -1446,9 +958,8 @@ async function runCiSetup(args) {
|
|
|
1446
958
|
repo: githubRepo,
|
|
1447
959
|
orgId,
|
|
1448
960
|
projectId,
|
|
1449
|
-
profile
|
|
1450
|
-
|
|
1451
|
-
authShards,
|
|
961
|
+
profile,
|
|
962
|
+
authPayload: encoded.payload,
|
|
1452
963
|
apiUrl,
|
|
1453
964
|
token,
|
|
1454
965
|
suiteId,
|
|
@@ -1456,27 +967,25 @@ async function runCiSetup(args) {
|
|
|
1456
967
|
cliRef,
|
|
1457
968
|
});
|
|
1458
969
|
printGithubSyncResults(syncResults, { repo: githubRepo });
|
|
1459
|
-
const
|
|
1460
|
-
if (
|
|
970
|
+
const authRow = syncResults.find((r) => r.name === "QUALTY_AUTH_STATE_B64");
|
|
971
|
+
if (!authRow?.ok && outputPath) {
|
|
1461
972
|
// eslint-disable-next-line no-console
|
|
1462
|
-
console.log(
|
|
1463
|
-
authShards.forEach((shard, index) => {
|
|
1464
|
-
const shardPath =
|
|
1465
|
-
index === 0 ? outputPath : outputPath.replace(/(\.[^./]+)?$/, `_${index + 1}$1`);
|
|
1466
|
-
// eslint-disable-next-line no-console
|
|
1467
|
-
console.log(` gh secret set ${shard.secretName} < ${shardPath}`);
|
|
1468
|
-
});
|
|
973
|
+
console.log(` Manual auth secret: gh secret set QUALTY_AUTH_STATE_B64 < ${outputPath}`);
|
|
1469
974
|
// eslint-disable-next-line no-console
|
|
1470
975
|
console.log("");
|
|
1471
976
|
}
|
|
1472
977
|
}
|
|
1473
978
|
if (copyToClipboardFlag) {
|
|
1474
|
-
if (copyToClipboard(
|
|
979
|
+
if (copyToClipboard(encoded.payload)) {
|
|
1475
980
|
// eslint-disable-next-line no-console
|
|
1476
|
-
console.log(
|
|
981
|
+
console.log("✓ Copied QUALTY_AUTH_STATE_B64 to clipboard. Paste into GitHub → Settings → Secrets.");
|
|
1477
982
|
} else {
|
|
1478
983
|
// eslint-disable-next-line no-console
|
|
1479
984
|
console.log("Could not copy to clipboard on this machine.");
|
|
985
|
+
if (outputPath) {
|
|
986
|
+
// eslint-disable-next-line no-console
|
|
987
|
+
console.log(` Use the file instead: ${outputPath}`);
|
|
988
|
+
}
|
|
1480
989
|
}
|
|
1481
990
|
// eslint-disable-next-line no-console
|
|
1482
991
|
console.log("");
|
|
@@ -1486,16 +995,10 @@ async function runCiSetup(args) {
|
|
|
1486
995
|
console.log(
|
|
1487
996
|
"1. GitHub Actions config was pushed via gh (secrets + repository variables — see sync above). Fix any ✗ manually."
|
|
1488
997
|
);
|
|
1489
|
-
if (!suiteId
|
|
1490
|
-
// eslint-disable-next-line no-console
|
|
1491
|
-
console.log(
|
|
1492
|
-
" Tip: pass --suite-id or --ids to bundle only login profiles required by those tests."
|
|
1493
|
-
);
|
|
1494
|
-
}
|
|
1495
|
-
if (authShards.length > 1) {
|
|
998
|
+
if (!suiteId) {
|
|
1496
999
|
// eslint-disable-next-line no-console
|
|
1497
1000
|
console.log(
|
|
1498
|
-
|
|
1001
|
+
" Tip: hardcode --suite-id or --ids in your workflow (see templates/github-actions/qualty-local-pr.yml)."
|
|
1499
1002
|
);
|
|
1500
1003
|
}
|
|
1501
1004
|
// eslint-disable-next-line no-console
|
|
@@ -1530,35 +1033,13 @@ async function runCiSetup(args) {
|
|
|
1530
1033
|
// eslint-disable-next-line no-console
|
|
1531
1034
|
console.log("---");
|
|
1532
1035
|
// eslint-disable-next-line no-console
|
|
1533
|
-
console.log(
|
|
1036
|
+
console.log("Name: QUALTY_AUTH_PROFILE");
|
|
1534
1037
|
// eslint-disable-next-line no-console
|
|
1535
1038
|
console.log("Value:");
|
|
1536
1039
|
// eslint-disable-next-line no-console
|
|
1537
|
-
console.log(
|
|
1040
|
+
console.log(profile);
|
|
1538
1041
|
// eslint-disable-next-line no-console
|
|
1539
|
-
console.log(` gh variable set
|
|
1540
|
-
if (primaryProfile) {
|
|
1541
|
-
// eslint-disable-next-line no-console
|
|
1542
|
-
console.log("---");
|
|
1543
|
-
// eslint-disable-next-line no-console
|
|
1544
|
-
console.log("Name: QUALTY_AUTH_PROFILE");
|
|
1545
|
-
// eslint-disable-next-line no-console
|
|
1546
|
-
console.log("Value:");
|
|
1547
|
-
// eslint-disable-next-line no-console
|
|
1548
|
-
console.log(primaryProfile);
|
|
1549
|
-
// eslint-disable-next-line no-console
|
|
1550
|
-
console.log(` gh variable set QUALTY_AUTH_PROFILE --body "${primaryProfile}"`);
|
|
1551
|
-
}
|
|
1552
|
-
if (authShards.length > 1) {
|
|
1553
|
-
// eslint-disable-next-line no-console
|
|
1554
|
-
console.log("---");
|
|
1555
|
-
// eslint-disable-next-line no-console
|
|
1556
|
-
console.log(`Name: ${AUTH_SHARDS_VARIABLE}`);
|
|
1557
|
-
// eslint-disable-next-line no-console
|
|
1558
|
-
console.log("Value:");
|
|
1559
|
-
// eslint-disable-next-line no-console
|
|
1560
|
-
console.log(String(authShards.length));
|
|
1561
|
-
}
|
|
1042
|
+
console.log(` gh variable set QUALTY_AUTH_PROFILE --body "${profile}"`);
|
|
1562
1043
|
// eslint-disable-next-line no-console
|
|
1563
1044
|
console.log("---");
|
|
1564
1045
|
// eslint-disable-next-line no-console
|
|
@@ -1599,20 +1080,20 @@ async function runCiSetup(args) {
|
|
|
1599
1080
|
// eslint-disable-next-line no-console
|
|
1600
1081
|
console.log("(create in Qualty → Account → API tokens; must start with qlty_ci_)");
|
|
1601
1082
|
}
|
|
1602
|
-
|
|
1083
|
+
// eslint-disable-next-line no-console
|
|
1084
|
+
console.log("---");
|
|
1085
|
+
// eslint-disable-next-line no-console
|
|
1086
|
+
console.log("Name: QUALTY_AUTH_STATE_B64");
|
|
1087
|
+
if (skipPayloadPrint) {
|
|
1603
1088
|
// eslint-disable-next-line no-console
|
|
1604
|
-
console.log(
|
|
1089
|
+
console.log(
|
|
1090
|
+
`Value: (see file above${copyToClipboardFlag ? " or clipboard" : ""})`
|
|
1091
|
+
);
|
|
1092
|
+
} else {
|
|
1605
1093
|
// eslint-disable-next-line no-console
|
|
1606
|
-
console.log(
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
console.log(`Value: (see file above${copyToClipboardFlag && shard.secretName === AUTH_SECRET_PRIMARY ? " or clipboard" : ""})`);
|
|
1610
|
-
} else {
|
|
1611
|
-
// eslint-disable-next-line no-console
|
|
1612
|
-
console.log("Value:");
|
|
1613
|
-
// eslint-disable-next-line no-console
|
|
1614
|
-
console.log(shard.payload);
|
|
1615
|
-
}
|
|
1094
|
+
console.log("Value:");
|
|
1095
|
+
// eslint-disable-next-line no-console
|
|
1096
|
+
console.log(encoded.payload);
|
|
1616
1097
|
}
|
|
1617
1098
|
// eslint-disable-next-line no-console
|
|
1618
1099
|
console.log("---");
|
|
@@ -1626,26 +1107,17 @@ async function runCiSetup(args) {
|
|
|
1626
1107
|
// eslint-disable-next-line no-console
|
|
1627
1108
|
console.log("");
|
|
1628
1109
|
// eslint-disable-next-line no-console
|
|
1629
|
-
console.log(githubAuthRestoreShell({
|
|
1110
|
+
console.log(githubAuthRestoreShell({ orgId, projectId, profile }));
|
|
1630
1111
|
// eslint-disable-next-line no-console
|
|
1631
1112
|
console.log("");
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
}
|
|
1636
|
-
if (profileNames.length === 1) {
|
|
1637
|
-
// eslint-disable-next-line no-console
|
|
1638
|
-
console.log(`Optional override: --auth-profile "${profileNames[0]}"`);
|
|
1639
|
-
} else {
|
|
1640
|
-
// eslint-disable-next-line no-console
|
|
1641
|
-
console.log("Omit --auth-profile in CI so each test uses its dashboard saved login profile.");
|
|
1642
|
-
}
|
|
1113
|
+
// eslint-disable-next-line no-console
|
|
1114
|
+
console.log(`Local file: ${statePath}`);
|
|
1115
|
+
// eslint-disable-next-line no-console
|
|
1116
|
+
console.log(`Profile flag: --auth-profile "${profile}" (file on disk: ${profileSeg}.json)`);
|
|
1643
1117
|
// eslint-disable-next-line no-console
|
|
1644
1118
|
console.log("");
|
|
1645
1119
|
// eslint-disable-next-line no-console
|
|
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
|
-
);
|
|
1120
|
+
console.log("When login expires, re-run qualty auth setup and qualty auth export-ci to refresh the secret.");
|
|
1649
1121
|
// eslint-disable-next-line no-console
|
|
1650
1122
|
console.log("");
|
|
1651
1123
|
}
|
|
@@ -2503,7 +1975,8 @@ async function maybePromptProjectIdForLocalRun({ args, apiUrl, token, orgId }) {
|
|
|
2503
1975
|
|
|
2504
1976
|
function resolveLocalAuthStateForRun({ args, orgId, projectId }) {
|
|
2505
1977
|
if (parseBoolean(args["no-auth-state"], false)) return null;
|
|
2506
|
-
const
|
|
1978
|
+
const projectCfg = readProjectConfig();
|
|
1979
|
+
const profile = authProfileFromConfig(projectCfg, args["auth-profile"]);
|
|
2507
1980
|
if (!profile) return null;
|
|
2508
1981
|
const path = localAuthStatePath({ orgId, projectId, profile });
|
|
2509
1982
|
const summary = summarizeStorageStateFile(path);
|
|
@@ -2517,10 +1990,12 @@ function resolveLocalAuthStateForRun({ args, orgId, projectId }) {
|
|
|
2517
1990
|
|
|
2518
1991
|
async function maybePromptAuthProfileForLocalRun({ args, orgId, projectId }) {
|
|
2519
1992
|
const projectCfg = readProjectConfig();
|
|
2520
|
-
const requestedProfile =
|
|
1993
|
+
const requestedProfile = authProfileFromConfig(projectCfg, args["auth-profile"]);
|
|
2521
1994
|
if (requestedProfile) return { authProfile: requestedProfile, noAuthState: false };
|
|
2522
1995
|
if (parseBoolean(args["no-auth-state"], false)) return { authProfile: "", noAuthState: true };
|
|
2523
1996
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1997
|
+
const defaultProfile = authProfileFromConfig(projectCfg);
|
|
1998
|
+
if (defaultProfile) return { authProfile: defaultProfile, noAuthState: false };
|
|
2524
1999
|
return { authProfile: "", noAuthState: false };
|
|
2525
2000
|
}
|
|
2526
2001
|
if (!projectId) return { authProfile: "", noAuthState: false };
|
|
@@ -2755,8 +2230,8 @@ async function runAuthSetup(args) {
|
|
|
2755
2230
|
console.log("");
|
|
2756
2231
|
// eslint-disable-next-line no-console
|
|
2757
2232
|
console.log(
|
|
2758
|
-
`[qualty][auth]
|
|
2759
|
-
` qualty auth export --project ${project.id} --profile "${profile}"
|
|
2233
|
+
`[qualty][auth] For GitHub Actions secrets, run:\n` +
|
|
2234
|
+
` qualty auth export-ci --project ${project.id} --profile "${profile}"`
|
|
2760
2235
|
);
|
|
2761
2236
|
} finally {
|
|
2762
2237
|
rl.close();
|
|
@@ -2778,29 +2253,17 @@ async function main() {
|
|
|
2778
2253
|
await runSetup(args);
|
|
2779
2254
|
return;
|
|
2780
2255
|
}
|
|
2781
|
-
if (command === "ci") {
|
|
2782
|
-
const sub = args._[1];
|
|
2783
|
-
if (sub === "setup") {
|
|
2784
|
-
await runCiSetup(args);
|
|
2785
|
-
return;
|
|
2786
|
-
}
|
|
2787
|
-
throw new Error("Unknown ci subcommand. Use: qualty ci setup");
|
|
2788
|
-
}
|
|
2789
2256
|
if (command === "auth") {
|
|
2790
2257
|
const sub = args._[1];
|
|
2791
2258
|
if (sub === "setup") {
|
|
2792
2259
|
await runAuthSetup(args);
|
|
2793
2260
|
return;
|
|
2794
2261
|
}
|
|
2795
|
-
if (sub === "export") {
|
|
2796
|
-
await
|
|
2797
|
-
return;
|
|
2798
|
-
}
|
|
2799
|
-
if (sub === "restore-ci") {
|
|
2800
|
-
await runAuthRestoreCi(args);
|
|
2262
|
+
if (sub === "export-ci") {
|
|
2263
|
+
await runAuthExportCi(args);
|
|
2801
2264
|
return;
|
|
2802
2265
|
}
|
|
2803
|
-
throw new Error("Unknown auth subcommand. Use: qualty auth setup | qualty auth export
|
|
2266
|
+
throw new Error("Unknown auth subcommand. Use: qualty auth setup | qualty auth export-ci");
|
|
2804
2267
|
}
|
|
2805
2268
|
if (command === "run") {
|
|
2806
2269
|
if (parseBoolean(args.local, false)) {
|
|
@@ -2833,7 +2296,7 @@ async function main() {
|
|
|
2833
2296
|
projectId,
|
|
2834
2297
|
})
|
|
2835
2298
|
: {
|
|
2836
|
-
authProfile:
|
|
2299
|
+
authProfile: String(args["auth-profile"] || "").trim(),
|
|
2837
2300
|
noAuthState: parseBoolean(args["no-auth-state"], false),
|
|
2838
2301
|
};
|
|
2839
2302
|
const localAuthArgs = {
|
|
@@ -2856,9 +2319,9 @@ async function main() {
|
|
|
2856
2319
|
device: args.device ? String(args.device).trim() : undefined,
|
|
2857
2320
|
headless: !parseBoolean(args.headed, false),
|
|
2858
2321
|
localConcurrency: resolveLocalConcurrency(args),
|
|
2859
|
-
authProfile: authChoice.authProfile || "",
|
|
2322
|
+
authProfile: localAuth?.profile || authChoice.authProfile || "",
|
|
2860
2323
|
noAuthState: Boolean(authChoice.noAuthState || parseBoolean(args["no-auth-state"], false)),
|
|
2861
|
-
storageStatePath:
|
|
2324
|
+
storageStatePath: localAuth?.path,
|
|
2862
2325
|
};
|
|
2863
2326
|
if (suiteId && explicitIds.length === 0) {
|
|
2864
2327
|
await runLocalSuiteWithSequences(localRunArgs);
|