qualty 0.1.27 → 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 +239 -1018
- package/bin/qualty.js +144 -679
- 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 {
|
|
@@ -166,14 +116,7 @@ function normalizeApiUrl(raw, { label = "API URL" } = {}) {
|
|
|
166
116
|
}
|
|
167
117
|
|
|
168
118
|
function qualtyBaseUrlFromEnv() {
|
|
169
|
-
|
|
170
|
-
return String(
|
|
171
|
-
dotenv.QUALTY_BASE_URL
|
|
172
|
-
|| dotenv.QUALTY_API_URL
|
|
173
|
-
|| process.env.QUALTY_BASE_URL
|
|
174
|
-
|| process.env.QUALTY_API_URL
|
|
175
|
-
|| ""
|
|
176
|
-
).trim();
|
|
119
|
+
return String(process.env.QUALTY_BASE_URL || process.env.QUALTY_API_URL || "").trim();
|
|
177
120
|
}
|
|
178
121
|
|
|
179
122
|
function resolveApiUrl(args = {}) {
|
|
@@ -191,14 +134,8 @@ function resolveApiUrl(args = {}) {
|
|
|
191
134
|
}
|
|
192
135
|
|
|
193
136
|
function resolveToken(args = {}) {
|
|
194
|
-
if (args.token) return String(args.token).trim();
|
|
195
|
-
const dotenv = loadProjectDotenv();
|
|
196
|
-
const fromDotenv = String(dotenv.QUALTY_API_TOKEN || "").trim();
|
|
197
|
-
if (fromDotenv) return fromDotenv;
|
|
198
|
-
const fromProcessEnv = String(process.env.QUALTY_API_TOKEN || "").trim();
|
|
199
|
-
if (fromProcessEnv) return fromProcessEnv;
|
|
200
137
|
const creds = readSharedCredentials();
|
|
201
|
-
return
|
|
138
|
+
return args.token || process.env.QUALTY_API_TOKEN || creds.api_token || "";
|
|
202
139
|
}
|
|
203
140
|
|
|
204
141
|
function resolveOrgId(args = {}) {
|
|
@@ -242,19 +179,11 @@ function usage() {
|
|
|
242
179
|
"Usage:",
|
|
243
180
|
" qualty setup [--token <bearer-token>] [--org <org-id>]",
|
|
244
181
|
" qualty auth setup [--project <project-id>] [--profile <profile-name>] [--headed]",
|
|
245
|
-
" qualty auth export [--project <project-id>] [--profile <profile-name>] [--org <org-id>]",
|
|
246
|
-
"
|
|
247
|
-
"
|
|
248
|
-
"
|
|
249
|
-
"
|
|
250
|
-
" [--all-profiles] [--profiles <name1,name2>]",
|
|
251
|
-
" [--suite-id <suite-uuid>] [--ids <id1,id2>] bundle login profiles required by tests",
|
|
252
|
-
" [--url <api-base-url>] [--repo <owner/repo>]",
|
|
253
|
-
" [--output <path>] [--no-file] [--copy] [--gzip] [--include-token]",
|
|
254
|
-
" [--set-secret | --set-github] gh: secrets (token, API URL, auth bundle) + variables",
|
|
255
|
-
" [--cli-repo <owner/repo>] [--cli-ref <branch>] Optional CI variables",
|
|
256
|
-
" qualty auth restore-ci [--org <org-id>] [--project <project-id>] [--profile <fallback-profile>]",
|
|
257
|
-
" 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",
|
|
258
187
|
" qualty connect --project <project-id> [--port 3000] [--token <bearer-token>] [--org <org-id>]",
|
|
259
188
|
" qualty run --project <project-id> [--suite-id <suite-id>] [--ids <id1,id2>] [--local] [--token <bearer-token>] [--org <org-id>]",
|
|
260
189
|
" [--poll-interval 5] [--timeout 30] [--fail-on-failure true] [--no-view-logs]",
|
|
@@ -265,7 +194,7 @@ function usage() {
|
|
|
265
194
|
" qualty resolve --project <project-id> [--suite-id <suite-id>] [--ids <id1,id2>] [--json] [--token <bearer-token>]",
|
|
266
195
|
"",
|
|
267
196
|
"Env vars:",
|
|
268
|
-
" QUALTY_API_TOKEN
|
|
197
|
+
" QUALTY_API_TOKEN Bearer token used for auth",
|
|
269
198
|
" QUALTY_BASE_URL API base URL when --url is not set (default: production)",
|
|
270
199
|
" QUALTY_LOCAL_CONCURRENCY Local parallel workers for --local runs (default: 4)",
|
|
271
200
|
"",
|
|
@@ -452,7 +381,9 @@ async function runSetup(args) {
|
|
|
452
381
|
|
|
453
382
|
try {
|
|
454
383
|
const currentOrgId = String(args.org || projectCfg.default_org_id || "");
|
|
455
|
-
const currentToken =
|
|
384
|
+
const currentToken = String(
|
|
385
|
+
args.token || process.env.QUALTY_API_TOKEN || sharedCreds.api_token || ""
|
|
386
|
+
);
|
|
456
387
|
|
|
457
388
|
let token = "";
|
|
458
389
|
if (args.token) {
|
|
@@ -599,10 +530,12 @@ function localAuthStatePath({ orgId, projectId, profile }) {
|
|
|
599
530
|
return join(QUALTY_SHARED_CREDS_DIR, "local-contexts", orgSeg, projectSeg, `${profileSeg}.json`);
|
|
600
531
|
}
|
|
601
532
|
|
|
602
|
-
function
|
|
533
|
+
function authExportCiOutputPath(statePath) {
|
|
603
534
|
return statePath.replace(/\.json$/i, ".b64");
|
|
604
535
|
}
|
|
605
536
|
|
|
537
|
+
const GITHUB_SECRET_MAX_BYTES = 64 * 1024;
|
|
538
|
+
|
|
606
539
|
function copyToClipboard(text) {
|
|
607
540
|
if (process.platform === "darwin") {
|
|
608
541
|
const result = spawnSync("pbcopy", { input: text, encoding: "utf8" });
|
|
@@ -668,8 +601,7 @@ function syncGithubCiConfig({
|
|
|
668
601
|
orgId,
|
|
669
602
|
projectId,
|
|
670
603
|
profile,
|
|
671
|
-
|
|
672
|
-
authShards,
|
|
604
|
+
authPayload,
|
|
673
605
|
apiUrl,
|
|
674
606
|
token,
|
|
675
607
|
suiteId,
|
|
@@ -681,9 +613,8 @@ function syncGithubCiConfig({
|
|
|
681
613
|
results.push({ kind, name, ...outcome });
|
|
682
614
|
};
|
|
683
615
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
616
|
+
const authResult = setGithubSecret("QUALTY_AUTH_STATE_B64", authPayload, { repo });
|
|
617
|
+
record("secret", "QUALTY_AUTH_STATE_B64", authResult);
|
|
687
618
|
|
|
688
619
|
if (token) {
|
|
689
620
|
record("secret", "QUALTY_API_TOKEN", setGithubSecret("QUALTY_API_TOKEN", token, { repo }));
|
|
@@ -701,21 +632,7 @@ function syncGithubCiConfig({
|
|
|
701
632
|
|
|
702
633
|
record("variable", "QUALTY_ORG_ID", setGithubVariable("QUALTY_ORG_ID", orgId, { repo }));
|
|
703
634
|
record("variable", "QUALTY_PROJECT_ID", setGithubVariable("QUALTY_PROJECT_ID", String(projectId), { repo }));
|
|
704
|
-
|
|
705
|
-
const names = (profileNames || []).filter(Boolean);
|
|
706
|
-
if (names.length > 0) {
|
|
707
|
-
record("variable", AUTH_PROFILES_VARIABLE, setGithubVariable(AUTH_PROFILES_VARIABLE, names.join(","), { repo }));
|
|
708
|
-
}
|
|
709
|
-
if (profile) {
|
|
710
|
-
record("variable", "QUALTY_AUTH_PROFILE", setGithubVariable("QUALTY_AUTH_PROFILE", profile, { repo }));
|
|
711
|
-
}
|
|
712
|
-
if ((authShards || []).length > 1) {
|
|
713
|
-
record(
|
|
714
|
-
"variable",
|
|
715
|
-
AUTH_SHARDS_VARIABLE,
|
|
716
|
-
setGithubVariable(AUTH_SHARDS_VARIABLE, String(authShards.length), { repo })
|
|
717
|
-
);
|
|
718
|
-
}
|
|
635
|
+
record("variable", "QUALTY_AUTH_PROFILE", setGithubVariable("QUALTY_AUTH_PROFILE", profile, { repo }));
|
|
719
636
|
|
|
720
637
|
if (suiteId) {
|
|
721
638
|
record("variable", "QUALTY_SUITE_ID", setGithubVariable("QUALTY_SUITE_ID", String(suiteId), { repo }));
|
|
@@ -731,37 +648,6 @@ function syncGithubCiConfig({
|
|
|
731
648
|
return results;
|
|
732
649
|
}
|
|
733
650
|
|
|
734
|
-
/**
|
|
735
|
-
* Push only auth bundle secrets and auth-related repository variables (no API token or org/project).
|
|
736
|
-
*/
|
|
737
|
-
function syncGithubAuthProfileOnly({ repo, profile, profileNames, authShards }) {
|
|
738
|
-
const results = [];
|
|
739
|
-
const record = (kind, name, outcome) => {
|
|
740
|
-
results.push({ kind, name, ...outcome });
|
|
741
|
-
};
|
|
742
|
-
|
|
743
|
-
for (const shard of authShards || []) {
|
|
744
|
-
record("secret", shard.secretName, setGithubSecret(shard.secretName, shard.payload, { repo }));
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
const names = (profileNames || []).filter(Boolean);
|
|
748
|
-
if (names.length > 0) {
|
|
749
|
-
record("variable", AUTH_PROFILES_VARIABLE, setGithubVariable(AUTH_PROFILES_VARIABLE, names.join(","), { repo }));
|
|
750
|
-
}
|
|
751
|
-
if (profile) {
|
|
752
|
-
record("variable", "QUALTY_AUTH_PROFILE", setGithubVariable("QUALTY_AUTH_PROFILE", profile, { repo }));
|
|
753
|
-
}
|
|
754
|
-
if ((authShards || []).length > 1) {
|
|
755
|
-
record(
|
|
756
|
-
"variable",
|
|
757
|
-
AUTH_SHARDS_VARIABLE,
|
|
758
|
-
setGithubVariable(AUTH_SHARDS_VARIABLE, String(authShards.length), { repo })
|
|
759
|
-
);
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
return results;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
651
|
function printGithubSyncResults(results, { repo }) {
|
|
766
652
|
const target = repo ? ` (repo ${repo})` : "";
|
|
767
653
|
const printGroup = (title, rows) => {
|
|
@@ -842,7 +728,7 @@ function configStringValue(projectCfg, key, configPath) {
|
|
|
842
728
|
);
|
|
843
729
|
}
|
|
844
730
|
|
|
845
|
-
function
|
|
731
|
+
function missingExportCiField(fieldLabel, { flag, configKey, configPath, hasFlag }) {
|
|
846
732
|
const configHint = configPath
|
|
847
733
|
? `${PROJECT_CONFIG_FILENAME} at ${configPath} (${configKey})`
|
|
848
734
|
: `${PROJECT_CONFIG_FILENAME} (not found walking up from ${process.cwd()})`;
|
|
@@ -858,22 +744,12 @@ function missingCiSetupField(fieldLabel, { flag, configKey, configPath, hasFlag
|
|
|
858
744
|
].join(" ");
|
|
859
745
|
}
|
|
860
746
|
|
|
861
|
-
function
|
|
747
|
+
function resolveExportCiConfig(args) {
|
|
862
748
|
const { configPath, config: projectCfg } = readProjectConfigStrict();
|
|
863
749
|
|
|
864
750
|
const orgFromFlag = String(args.org || "").trim();
|
|
865
751
|
const projectFromFlag = String(args.project || "").trim();
|
|
866
752
|
const profileFromFlag = String(args.profile || args["auth-profile"] || "").trim();
|
|
867
|
-
const allProfiles = parseBoolean(args["all-profiles"], false);
|
|
868
|
-
const profilesList = String(args.profiles || "")
|
|
869
|
-
.split(",")
|
|
870
|
-
.map((part) => part.trim())
|
|
871
|
-
.filter(Boolean);
|
|
872
|
-
const suiteId = String(args["suite-id"] || args.suite || "").trim();
|
|
873
|
-
const explicitIds = String(args.ids || "")
|
|
874
|
-
.split(",")
|
|
875
|
-
.map((part) => part.trim())
|
|
876
|
-
.filter(Boolean);
|
|
877
753
|
|
|
878
754
|
let orgId = orgFromFlag;
|
|
879
755
|
if (!orgId) {
|
|
@@ -892,7 +768,7 @@ function resolveCiSetupConfig(args, { requireProfile = true } = {}) {
|
|
|
892
768
|
|
|
893
769
|
if (!orgId) {
|
|
894
770
|
throw new Error(
|
|
895
|
-
|
|
771
|
+
missingExportCiField("org ID", {
|
|
896
772
|
flag: "--org",
|
|
897
773
|
configKey: "default_org_id",
|
|
898
774
|
configPath,
|
|
@@ -902,7 +778,7 @@ function resolveCiSetupConfig(args, { requireProfile = true } = {}) {
|
|
|
902
778
|
}
|
|
903
779
|
if (!projectId) {
|
|
904
780
|
throw new Error(
|
|
905
|
-
|
|
781
|
+
missingExportCiField("project ID", {
|
|
906
782
|
flag: "--project",
|
|
907
783
|
configKey: "default_project_id",
|
|
908
784
|
configPath,
|
|
@@ -910,12 +786,9 @@ function resolveCiSetupConfig(args, { requireProfile = true } = {}) {
|
|
|
910
786
|
})
|
|
911
787
|
);
|
|
912
788
|
}
|
|
913
|
-
|
|
914
|
-
const hasMultiProfileMode =
|
|
915
|
-
allProfiles || profilesList.length > 0 || Boolean(suiteId) || explicitIds.length > 0;
|
|
916
|
-
if (requireProfile && !profile && !hasMultiProfileMode) {
|
|
789
|
+
if (!profile) {
|
|
917
790
|
throw new Error(
|
|
918
|
-
|
|
791
|
+
missingExportCiField("auth profile", {
|
|
919
792
|
flag: "--profile",
|
|
920
793
|
configKey: "default_auth_profile",
|
|
921
794
|
configPath,
|
|
@@ -931,163 +804,43 @@ function resolveCiSetupConfig(args, { requireProfile = true } = {}) {
|
|
|
931
804
|
throw new Error(`Invalid API URL: ${err.message}`);
|
|
932
805
|
}
|
|
933
806
|
|
|
934
|
-
return {
|
|
935
|
-
orgId,
|
|
936
|
-
projectId,
|
|
937
|
-
profile,
|
|
938
|
-
apiUrl,
|
|
939
|
-
configPath,
|
|
940
|
-
allProfiles,
|
|
941
|
-
profilesList,
|
|
942
|
-
suiteId,
|
|
943
|
-
explicitIds,
|
|
944
|
-
};
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
function jobUsesSavedLogin(job) {
|
|
948
|
-
if (!job) return false;
|
|
949
|
-
if (job.use_saved_login) return true;
|
|
950
|
-
if (job.saved_login_profile_id) return true;
|
|
951
|
-
const profile = job.saved_login_profile;
|
|
952
|
-
return Boolean(profile && typeof profile === "object" && (profile.id || profile.name));
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
async function fetchSavedLoginProfilesForProject({ apiUrl, token, orgId, projectId }) {
|
|
956
|
-
const payload = await apiRequest({
|
|
957
|
-
apiUrl,
|
|
958
|
-
token,
|
|
959
|
-
orgId,
|
|
960
|
-
path: `/api/v1/projects/${encodeURIComponent(String(projectId))}/saved-login-profiles`,
|
|
961
|
-
}).catch(() => []);
|
|
962
|
-
return Array.isArray(payload) ? payload : [];
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
function localProfileLabelFromApiRow(row, fallbackName = "") {
|
|
966
|
-
return String(row?.local_profile_name || row?.name || fallbackName || "").trim();
|
|
807
|
+
return { orgId, projectId, profile, apiUrl, configPath };
|
|
967
808
|
}
|
|
968
809
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
}
|
|
977
|
-
const jobsPayload = await resolveSavedJobs({
|
|
978
|
-
apiUrl,
|
|
979
|
-
token,
|
|
980
|
-
orgId,
|
|
981
|
-
projectId,
|
|
982
|
-
suiteId,
|
|
983
|
-
explicitIds,
|
|
984
|
-
});
|
|
985
|
-
const jobs = Array.isArray(jobsPayload)
|
|
986
|
-
? jobsPayload
|
|
987
|
-
: Array.isArray(jobsPayload?.results)
|
|
988
|
-
? jobsPayload.results
|
|
989
|
-
: Array.isArray(jobsPayload?.jobs)
|
|
990
|
-
? jobsPayload.jobs
|
|
991
|
-
: [];
|
|
992
|
-
const names = new Set();
|
|
993
|
-
for (const job of jobs) {
|
|
994
|
-
if (!jobUsesSavedLogin(job)) continue;
|
|
995
|
-
const name = String(job?.saved_login_profile?.name || "").trim();
|
|
996
|
-
if (name) names.add(name);
|
|
997
|
-
}
|
|
998
|
-
return [...names];
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
async function resolveCiSetupProfileEntries({
|
|
1002
|
-
orgId,
|
|
1003
|
-
projectId,
|
|
1004
|
-
profile,
|
|
1005
|
-
allProfiles,
|
|
1006
|
-
profilesList,
|
|
1007
|
-
suiteId,
|
|
1008
|
-
explicitIds,
|
|
1009
|
-
apiUrl,
|
|
1010
|
-
token,
|
|
1011
|
-
}) {
|
|
1012
|
-
const apiProfiles = token
|
|
1013
|
-
? await fetchSavedLoginProfilesForProject({ apiUrl, token, orgId, projectId })
|
|
1014
|
-
: [];
|
|
1015
|
-
|
|
1016
|
-
let requestedNames = [];
|
|
1017
|
-
if (allProfiles) {
|
|
1018
|
-
requestedNames = apiProfiles.map((row) => String(row?.name || "").trim()).filter(Boolean);
|
|
1019
|
-
if (requestedNames.length === 0) {
|
|
1020
|
-
requestedNames = listLocalAuthProfiles({ orgId, projectId });
|
|
1021
|
-
}
|
|
1022
|
-
} else if (profilesList.length > 0) {
|
|
1023
|
-
requestedNames = profilesList;
|
|
1024
|
-
} else if (suiteId || explicitIds.length > 0) {
|
|
1025
|
-
if (!token) {
|
|
1026
|
-
throw new Error("QUALTY_API_TOKEN is required to resolve profiles from --suite-id or --ids.");
|
|
1027
|
-
}
|
|
1028
|
-
requestedNames = await resolveRequiredProfileNamesFromTests({
|
|
1029
|
-
apiUrl,
|
|
1030
|
-
token,
|
|
1031
|
-
orgId,
|
|
1032
|
-
projectId,
|
|
1033
|
-
suiteId,
|
|
1034
|
-
explicitIds,
|
|
1035
|
-
});
|
|
1036
|
-
if (requestedNames.length === 0 && profile) {
|
|
1037
|
-
requestedNames = [profile];
|
|
1038
|
-
}
|
|
1039
|
-
} else if (profile) {
|
|
1040
|
-
requestedNames = [profile];
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
requestedNames = [...new Set(requestedNames.filter(Boolean))];
|
|
1044
|
-
if (requestedNames.length === 0) {
|
|
1045
|
-
throw new Error(
|
|
1046
|
-
"No auth profiles selected. Pass --profile, --profiles, --all-profiles, or --suite-id / --ids."
|
|
1047
|
-
);
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
const entries = [];
|
|
1051
|
-
const missing = [];
|
|
1052
|
-
for (const dashboardName of requestedNames) {
|
|
1053
|
-
const apiRow = apiProfiles.find((row) => String(row?.name || "").trim() === dashboardName);
|
|
1054
|
-
const localProfileName = localProfileLabelFromApiRow(apiRow, dashboardName) || dashboardName;
|
|
1055
|
-
const statePath = localAuthStatePath({ orgId, projectId, profile: localProfileName });
|
|
1056
|
-
if (!existsSync(statePath)) {
|
|
1057
|
-
missing.push({ dashboardName, localProfileName, statePath });
|
|
1058
|
-
continue;
|
|
1059
|
-
}
|
|
1060
|
-
entries.push({
|
|
1061
|
-
name: dashboardName,
|
|
1062
|
-
localProfileName,
|
|
1063
|
-
statePath,
|
|
1064
|
-
});
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
if (missing.length > 0) {
|
|
1068
|
-
const lines = missing.map(
|
|
1069
|
-
(row) =>
|
|
1070
|
-
` - "${row.dashboardName}" → ${row.statePath}\n qualty auth setup --project ${projectId} --profile "${row.dashboardName}"`
|
|
1071
|
-
);
|
|
1072
|
-
throw new Error(["Local auth state missing for:", ...lines].join("\n"));
|
|
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
|
+
};
|
|
1073
818
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
819
|
+
return {
|
|
820
|
+
encoding: "base64",
|
|
821
|
+
payload: raw.toString("base64"),
|
|
822
|
+
rawBytes: raw.length,
|
|
823
|
+
};
|
|
1076
824
|
}
|
|
1077
825
|
|
|
1078
|
-
function githubAuthRestoreShell({
|
|
1079
|
-
const shardEnvLines = [];
|
|
1080
|
-
for (let index = 1; index <= Math.max(1, shardCount); index += 1) {
|
|
1081
|
-
const secretName = authShardSecretName(index);
|
|
1082
|
-
shardEnvLines.push(` ${secretName}: \${{ secrets.${secretName} }}`);
|
|
1083
|
-
}
|
|
826
|
+
function githubAuthRestoreShell({ orgId, projectId, profile }) {
|
|
1084
827
|
return [
|
|
1085
|
-
`- name: Restore Qualty login
|
|
828
|
+
`- name: Restore Qualty login state`,
|
|
1086
829
|
` env:`,
|
|
1087
|
-
|
|
1088
|
-
`
|
|
1089
|
-
`
|
|
1090
|
-
`
|
|
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"`,
|
|
1091
844
|
``,
|
|
1092
845
|
`- name: Run Qualty tests (local)`,
|
|
1093
846
|
` env:`,
|
|
@@ -1098,311 +851,84 @@ function githubAuthRestoreShell({ shardCount = 1 } = {}) {
|
|
|
1098
851
|
` --org "$QUALTY_ORG_ID" \\`,
|
|
1099
852
|
` --project "$QUALTY_PROJECT_ID" \\`,
|
|
1100
853
|
` --suite-id "YOUR-SUITE-UUID" \\`,
|
|
854
|
+
` --auth-profile "$QUALTY_AUTH_PROFILE" \\`,
|
|
1101
855
|
` --fail-on-failure true`,
|
|
1102
|
-
` # Omit --auth-profile when tests use different saved login profiles.`,
|
|
1103
|
-
` # Add --auth-profile "Profile Name" only to force one profile for the whole run.`,
|
|
1104
856
|
].join("\n");
|
|
1105
857
|
}
|
|
1106
858
|
|
|
1107
|
-
async function
|
|
1108
|
-
const orgId
|
|
1109
|
-
const
|
|
1110
|
-
const fallbackProfile = String(
|
|
1111
|
-
args.profile || args["auth-profile"] || process.env.QUALTY_AUTH_PROFILE || ""
|
|
1112
|
-
).trim();
|
|
1113
|
-
|
|
1114
|
-
if (!orgId) {
|
|
1115
|
-
throw new Error("Missing org ID. Set QUALTY_ORG_ID or pass --org.");
|
|
1116
|
-
}
|
|
1117
|
-
if (!projectId) {
|
|
1118
|
-
throw new Error("Missing project ID. Set QUALTY_PROJECT_ID or pass --project.");
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
const payloads = collectAuthShardPayloadsFromEnv(process.env);
|
|
1122
|
-
if (payloads.length === 0) {
|
|
1123
|
-
throw new Error(
|
|
1124
|
-
`Missing ${AUTH_SECRET_PRIMARY}. Run qualty ci setup --set-github (first time) or qualty auth export --set-github (profile refresh).`
|
|
1125
|
-
);
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
const restored = restoreAuthPayloadsToDisk({
|
|
1129
|
-
payloads,
|
|
1130
|
-
orgId,
|
|
1131
|
-
projectId,
|
|
1132
|
-
fallbackProfile,
|
|
1133
|
-
});
|
|
1134
|
-
|
|
1135
|
-
for (const row of restored) {
|
|
1136
|
-
// eslint-disable-next-line no-console
|
|
1137
|
-
console.log(`Restored login state: ${row.path} (profile "${row.name}")`);
|
|
1138
|
-
}
|
|
1139
|
-
// eslint-disable-next-line no-console
|
|
1140
|
-
console.log(`Restored ${restored.length} login profile(s).`);
|
|
1141
|
-
}
|
|
859
|
+
async function runAuthExportCi(args) {
|
|
860
|
+
const { orgId, projectId, profile, apiUrl, configPath } = resolveExportCiConfig(args);
|
|
861
|
+
const token = resolveToken(args);
|
|
1142
862
|
|
|
1143
|
-
|
|
1144
|
-
if (
|
|
1145
|
-
parseBoolean(args["all-profiles"], false) ||
|
|
1146
|
-
String(args["suite-id"] || args.suite || "").trim() ||
|
|
1147
|
-
String(args.ids || "")
|
|
1148
|
-
.split(",")
|
|
1149
|
-
.map((part) => part.trim())
|
|
1150
|
-
.filter(Boolean).length > 0
|
|
1151
|
-
) {
|
|
863
|
+
const statePath = localAuthStatePath({ orgId, projectId, profile });
|
|
864
|
+
if (!existsSync(statePath)) {
|
|
1152
865
|
throw new Error(
|
|
1153
|
-
|
|
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")
|
|
1154
876
|
);
|
|
1155
877
|
}
|
|
1156
878
|
|
|
1157
|
-
const profilesList = String(args.profiles || "")
|
|
1158
|
-
.split(",")
|
|
1159
|
-
.map((part) => part.trim())
|
|
1160
|
-
.filter(Boolean);
|
|
1161
|
-
const { orgId, projectId, profile, apiUrl, configPath } = resolveCiSetupConfig(args, {
|
|
1162
|
-
requireProfile: profilesList.length === 0,
|
|
1163
|
-
});
|
|
1164
|
-
const token = resolveToken(args);
|
|
1165
|
-
|
|
1166
|
-
const profileEntries = await resolveCiSetupProfileEntries({
|
|
1167
|
-
orgId,
|
|
1168
|
-
projectId,
|
|
1169
|
-
profile,
|
|
1170
|
-
allProfiles: false,
|
|
1171
|
-
profilesList,
|
|
1172
|
-
suiteId: "",
|
|
1173
|
-
explicitIds: [],
|
|
1174
|
-
apiUrl,
|
|
1175
|
-
token,
|
|
1176
|
-
});
|
|
1177
|
-
const profileNames = profileEntries.map((entry) => entry.name);
|
|
1178
|
-
const primaryProfile = profile || profileNames[0] || "";
|
|
1179
|
-
|
|
1180
|
-
const manifest = buildAuthBundleManifest({ orgId, projectId, profileEntries });
|
|
1181
|
-
const authShards = shardAuthBundleManifest(manifest);
|
|
1182
|
-
const primaryShard = authShards[0];
|
|
1183
|
-
|
|
1184
879
|
const setGithubFlag =
|
|
1185
880
|
parseBoolean(args["set-github"], false) || parseBoolean(args["set-secret"], false);
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
const githubRepo = String(args.repo || "").trim();
|
|
1195
|
-
|
|
1196
|
-
if (outputPath) {
|
|
1197
|
-
writeFileSync(outputPath, primaryShard.payload, "utf8");
|
|
1198
|
-
try {
|
|
1199
|
-
chmodSync(outputPath, 0o600);
|
|
1200
|
-
} catch {
|
|
1201
|
-
// ignore platforms that do not support chmod
|
|
1202
|
-
}
|
|
1203
|
-
for (let index = 1; index < authShards.length; index += 1) {
|
|
1204
|
-
const shard = authShards[index];
|
|
1205
|
-
const shardPath = outputPath.replace(/(\.[^./]+)?$/, `_${index + 1}$1`);
|
|
1206
|
-
writeFileSync(shardPath, shard.payload, "utf8");
|
|
1207
|
-
try {
|
|
1208
|
-
chmodSync(shardPath, 0o600);
|
|
1209
|
-
} catch {
|
|
1210
|
-
// ignore
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
const skipPayloadPrint = Boolean(outputPath || copyToClipboardFlag || setGithubFlag);
|
|
1216
|
-
const totalSecretBytes = authShards.reduce(
|
|
1217
|
-
(sum, shard) => sum + Buffer.byteLength(shard.payload, "utf8"),
|
|
1218
|
-
0
|
|
1219
|
-
);
|
|
1220
|
-
|
|
1221
|
-
// eslint-disable-next-line no-console
|
|
1222
|
-
console.log("");
|
|
1223
|
-
// eslint-disable-next-line no-console
|
|
1224
|
-
console.log("=== Qualty → GitHub Actions (login profiles only) ===");
|
|
1225
|
-
// eslint-disable-next-line no-console
|
|
1226
|
-
console.log("");
|
|
1227
|
-
// eslint-disable-next-line no-console
|
|
1228
|
-
console.log(
|
|
1229
|
-
`Auth bundle: ${profileNames.length} profile(s), ${authShards.length} secret shard(s), ${totalSecretBytes} bytes total`
|
|
1230
|
-
);
|
|
1231
|
-
for (const shard of authShards) {
|
|
1232
|
-
// eslint-disable-next-line no-console
|
|
1233
|
-
console.log(
|
|
1234
|
-
` - ${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}`
|
|
1235
889
|
);
|
|
1236
890
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
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
|
+
);
|
|
1247
900
|
}
|
|
1248
|
-
|
|
1249
|
-
console.log("");
|
|
901
|
+
secretBytes = Buffer.byteLength(encoded.payload, "utf8");
|
|
1250
902
|
}
|
|
1251
|
-
if (
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
profileNames,
|
|
1256
|
-
authShards,
|
|
1257
|
-
});
|
|
1258
|
-
printGithubSyncResults(syncResults, { repo: githubRepo });
|
|
1259
|
-
const failedAuth = syncResults.filter((row) => row.kind === "secret" && !row.ok);
|
|
1260
|
-
if (failedAuth.length > 0 && outputPath) {
|
|
1261
|
-
// eslint-disable-next-line no-console
|
|
1262
|
-
console.log(" Manual auth secrets:");
|
|
1263
|
-
authShards.forEach((shard, index) => {
|
|
1264
|
-
const shardPath =
|
|
1265
|
-
index === 0 ? outputPath : outputPath.replace(/(\.[^./]+)?$/, `_${index + 1}$1`);
|
|
1266
|
-
// eslint-disable-next-line no-console
|
|
1267
|
-
console.log(` gh secret set ${shard.secretName} < ${shardPath}`);
|
|
1268
|
-
});
|
|
1269
|
-
// eslint-disable-next-line no-console
|
|
1270
|
-
console.log("");
|
|
1271
|
-
}
|
|
1272
|
-
} else {
|
|
1273
|
-
// eslint-disable-next-line no-console
|
|
1274
|
-
console.log("GitHub secrets (login state only — does not update QUALTY_API_TOKEN or org/project):");
|
|
1275
|
-
// eslint-disable-next-line no-console
|
|
1276
|
-
console.log("");
|
|
1277
|
-
for (const shard of authShards) {
|
|
1278
|
-
// eslint-disable-next-line no-console
|
|
1279
|
-
console.log("---");
|
|
1280
|
-
// eslint-disable-next-line no-console
|
|
1281
|
-
console.log(`Name: ${shard.secretName}`);
|
|
1282
|
-
if (skipPayloadPrint) {
|
|
1283
|
-
// eslint-disable-next-line no-console
|
|
1284
|
-
console.log(
|
|
1285
|
-
`Value: (see file above${copyToClipboardFlag && shard.secretName === AUTH_SECRET_PRIMARY ? " or clipboard" : ""})`
|
|
1286
|
-
);
|
|
1287
|
-
} else {
|
|
1288
|
-
// eslint-disable-next-line no-console
|
|
1289
|
-
console.log("Value:");
|
|
1290
|
-
// eslint-disable-next-line no-console
|
|
1291
|
-
console.log(shard.payload);
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
// eslint-disable-next-line no-console
|
|
1295
|
-
console.log("---");
|
|
1296
|
-
// eslint-disable-next-line no-console
|
|
1297
|
-
console.log("");
|
|
1298
|
-
// eslint-disable-next-line no-console
|
|
1299
|
-
console.log(
|
|
1300
|
-
"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."
|
|
1301
907
|
);
|
|
1302
|
-
// eslint-disable-next-line no-console
|
|
1303
|
-
console.log("");
|
|
1304
|
-
}
|
|
1305
|
-
if (copyToClipboardFlag) {
|
|
1306
|
-
if (copyToClipboard(primaryShard.payload)) {
|
|
1307
|
-
// eslint-disable-next-line no-console
|
|
1308
|
-
console.log(`✓ Copied ${AUTH_SECRET_PRIMARY} to clipboard.`);
|
|
1309
|
-
} else {
|
|
1310
|
-
// eslint-disable-next-line no-console
|
|
1311
|
-
console.log("Could not copy to clipboard on this machine.");
|
|
1312
|
-
}
|
|
1313
|
-
// eslint-disable-next-line no-console
|
|
1314
|
-
console.log("");
|
|
1315
|
-
}
|
|
1316
|
-
for (const entry of profileEntries) {
|
|
1317
|
-
// eslint-disable-next-line no-console
|
|
1318
|
-
console.log(`Profile "${entry.name}" → ${entry.statePath}`);
|
|
1319
908
|
}
|
|
1320
|
-
if (configPath) {
|
|
1321
|
-
// eslint-disable-next-line no-console
|
|
1322
|
-
console.log(`Config: ${configPath}`);
|
|
1323
|
-
}
|
|
1324
|
-
// eslint-disable-next-line no-console
|
|
1325
|
-
console.log("");
|
|
1326
|
-
// eslint-disable-next-line no-console
|
|
1327
|
-
console.log(
|
|
1328
|
-
"When login expires, re-run qualty auth setup then qualty auth export --set-github (or qualty ci setup for a full refresh)."
|
|
1329
|
-
);
|
|
1330
|
-
// eslint-disable-next-line no-console
|
|
1331
|
-
console.log("");
|
|
1332
|
-
}
|
|
1333
909
|
|
|
1334
|
-
async function runCiSetup(args) {
|
|
1335
|
-
const {
|
|
1336
|
-
orgId,
|
|
1337
|
-
projectId,
|
|
1338
|
-
profile,
|
|
1339
|
-
apiUrl,
|
|
1340
|
-
configPath,
|
|
1341
|
-
allProfiles,
|
|
1342
|
-
profilesList,
|
|
1343
|
-
suiteId: configSuiteId,
|
|
1344
|
-
explicitIds,
|
|
1345
|
-
} = resolveCiSetupConfig(args);
|
|
1346
|
-
const token = resolveToken(args);
|
|
1347
|
-
|
|
1348
|
-
const profileEntries = await resolveCiSetupProfileEntries({
|
|
1349
|
-
orgId,
|
|
1350
|
-
projectId,
|
|
1351
|
-
profile,
|
|
1352
|
-
allProfiles,
|
|
1353
|
-
profilesList,
|
|
1354
|
-
suiteId: configSuiteId,
|
|
1355
|
-
explicitIds,
|
|
1356
|
-
apiUrl,
|
|
1357
|
-
token,
|
|
1358
|
-
});
|
|
1359
|
-
const profileNames = profileEntries.map((entry) => entry.name);
|
|
1360
|
-
const primaryProfile = profile || profileNames[0] || "";
|
|
1361
|
-
|
|
1362
|
-
const manifest = buildAuthBundleManifest({ orgId, projectId, profileEntries });
|
|
1363
|
-
const authShards = shardAuthBundleManifest(manifest);
|
|
1364
|
-
const primaryShard = authShards[0];
|
|
1365
|
-
|
|
1366
|
-
const setGithubFlag =
|
|
1367
|
-
parseBoolean(args["set-github"], false) || parseBoolean(args["set-secret"], false);
|
|
1368
910
|
const noFile = parseBoolean(args["no-file"], false);
|
|
1369
911
|
const outputPath = noFile
|
|
1370
912
|
? String(args.output || "").trim()
|
|
1371
|
-
: String(args.output || "").trim() ||
|
|
1372
|
-
ciSetupAuthBundleOutputPath(
|
|
1373
|
-
localAuthStatePath({ orgId, projectId, profile: profileEntries[0].localProfileName })
|
|
1374
|
-
);
|
|
913
|
+
: String(args.output || "").trim() || authExportCiOutputPath(statePath);
|
|
1375
914
|
const copyToClipboardFlag = parseBoolean(args.copy, false);
|
|
1376
915
|
const githubRepo = String(args.repo || "").trim();
|
|
1377
|
-
const suiteId = String(args["suite-id"] || args.suite ||
|
|
916
|
+
const suiteId = String(args["suite-id"] || args.suite || "").trim();
|
|
1378
917
|
const cliRepo = String(args["cli-repo"] || "").trim();
|
|
1379
918
|
const cliRef = String(args["cli-ref"] || "").trim();
|
|
1380
919
|
|
|
1381
920
|
if (outputPath) {
|
|
1382
|
-
writeFileSync(outputPath,
|
|
921
|
+
writeFileSync(outputPath, encoded.payload, "utf8");
|
|
1383
922
|
try {
|
|
1384
923
|
chmodSync(outputPath, 0o600);
|
|
1385
924
|
} catch {
|
|
1386
925
|
// ignore platforms that do not support chmod
|
|
1387
926
|
}
|
|
1388
|
-
for (let index = 1; index < authShards.length; index += 1) {
|
|
1389
|
-
const shard = authShards[index];
|
|
1390
|
-
const shardPath = outputPath.replace(/(\.[^./]+)?$/, `_${index + 1}$1`);
|
|
1391
|
-
writeFileSync(shardPath, shard.payload, "utf8");
|
|
1392
|
-
try {
|
|
1393
|
-
chmodSync(shardPath, 0o600);
|
|
1394
|
-
} catch {
|
|
1395
|
-
// ignore
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
927
|
}
|
|
1399
928
|
|
|
1400
929
|
const includeToken = parseBoolean(args["include-token"], false);
|
|
930
|
+
const profileSeg = safePathSegment(profile, "default");
|
|
1401
931
|
const skipPayloadPrint = Boolean(outputPath || copyToClipboardFlag || setGithubFlag);
|
|
1402
|
-
const totalSecretBytes = authShards.reduce(
|
|
1403
|
-
(sum, shard) => sum + Buffer.byteLength(shard.payload, "utf8"),
|
|
1404
|
-
0
|
|
1405
|
-
);
|
|
1406
932
|
|
|
1407
933
|
// eslint-disable-next-line no-console
|
|
1408
934
|
console.log("");
|
|
@@ -1410,29 +936,20 @@ async function runCiSetup(args) {
|
|
|
1410
936
|
console.log("=== Qualty → GitHub Actions (secrets & variables) ===");
|
|
1411
937
|
// eslint-disable-next-line no-console
|
|
1412
938
|
console.log("");
|
|
1413
|
-
|
|
1414
|
-
console.log(
|
|
1415
|
-
`Auth bundle: ${profileNames.length} profile(s), ${authShards.length} secret shard(s), ${totalSecretBytes} bytes total`
|
|
1416
|
-
);
|
|
1417
|
-
for (const shard of authShards) {
|
|
939
|
+
if (encoded.encoding === "gzip+base64") {
|
|
1418
940
|
// eslint-disable-next-line no-console
|
|
1419
|
-
console.log(
|
|
1420
|
-
|
|
1421
|
-
|
|
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)`);
|
|
1422
945
|
}
|
|
1423
946
|
// eslint-disable-next-line no-console
|
|
1424
947
|
console.log("");
|
|
1425
948
|
if (outputPath) {
|
|
1426
949
|
// eslint-disable-next-line no-console
|
|
1427
|
-
console.log(`Wrote
|
|
1428
|
-
for (let index = 1; index < authShards.length; index += 1) {
|
|
1429
|
-
const shard = authShards[index];
|
|
1430
|
-
const shardPath = outputPath.replace(/(\.[^./]+)?$/, `_${index + 1}$1`);
|
|
1431
|
-
// eslint-disable-next-line no-console
|
|
1432
|
-
console.log(`Wrote ${shard.secretName} to ${shardPath}`);
|
|
1433
|
-
}
|
|
950
|
+
console.log(`Wrote QUALTY_AUTH_STATE_B64 to ${outputPath}`);
|
|
1434
951
|
// eslint-disable-next-line no-console
|
|
1435
|
-
console.log(` gh secret set
|
|
952
|
+
console.log(` gh secret set QUALTY_AUTH_STATE_B64 < ${outputPath}`);
|
|
1436
953
|
// eslint-disable-next-line no-console
|
|
1437
954
|
console.log("");
|
|
1438
955
|
}
|
|
@@ -1441,9 +958,8 @@ async function runCiSetup(args) {
|
|
|
1441
958
|
repo: githubRepo,
|
|
1442
959
|
orgId,
|
|
1443
960
|
projectId,
|
|
1444
|
-
profile
|
|
1445
|
-
|
|
1446
|
-
authShards,
|
|
961
|
+
profile,
|
|
962
|
+
authPayload: encoded.payload,
|
|
1447
963
|
apiUrl,
|
|
1448
964
|
token,
|
|
1449
965
|
suiteId,
|
|
@@ -1451,27 +967,25 @@ async function runCiSetup(args) {
|
|
|
1451
967
|
cliRef,
|
|
1452
968
|
});
|
|
1453
969
|
printGithubSyncResults(syncResults, { repo: githubRepo });
|
|
1454
|
-
const
|
|
1455
|
-
if (
|
|
970
|
+
const authRow = syncResults.find((r) => r.name === "QUALTY_AUTH_STATE_B64");
|
|
971
|
+
if (!authRow?.ok && outputPath) {
|
|
1456
972
|
// eslint-disable-next-line no-console
|
|
1457
|
-
console.log(
|
|
1458
|
-
authShards.forEach((shard, index) => {
|
|
1459
|
-
const shardPath =
|
|
1460
|
-
index === 0 ? outputPath : outputPath.replace(/(\.[^./]+)?$/, `_${index + 1}$1`);
|
|
1461
|
-
// eslint-disable-next-line no-console
|
|
1462
|
-
console.log(` gh secret set ${shard.secretName} < ${shardPath}`);
|
|
1463
|
-
});
|
|
973
|
+
console.log(` Manual auth secret: gh secret set QUALTY_AUTH_STATE_B64 < ${outputPath}`);
|
|
1464
974
|
// eslint-disable-next-line no-console
|
|
1465
975
|
console.log("");
|
|
1466
976
|
}
|
|
1467
977
|
}
|
|
1468
978
|
if (copyToClipboardFlag) {
|
|
1469
|
-
if (copyToClipboard(
|
|
979
|
+
if (copyToClipboard(encoded.payload)) {
|
|
1470
980
|
// eslint-disable-next-line no-console
|
|
1471
|
-
console.log(
|
|
981
|
+
console.log("✓ Copied QUALTY_AUTH_STATE_B64 to clipboard. Paste into GitHub → Settings → Secrets.");
|
|
1472
982
|
} else {
|
|
1473
983
|
// eslint-disable-next-line no-console
|
|
1474
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
|
+
}
|
|
1475
989
|
}
|
|
1476
990
|
// eslint-disable-next-line no-console
|
|
1477
991
|
console.log("");
|
|
@@ -1481,16 +995,10 @@ async function runCiSetup(args) {
|
|
|
1481
995
|
console.log(
|
|
1482
996
|
"1. GitHub Actions config was pushed via gh (secrets + repository variables — see sync above). Fix any ✗ manually."
|
|
1483
997
|
);
|
|
1484
|
-
if (!suiteId
|
|
1485
|
-
// eslint-disable-next-line no-console
|
|
1486
|
-
console.log(
|
|
1487
|
-
" Tip: pass --suite-id or --ids to bundle only login profiles required by those tests."
|
|
1488
|
-
);
|
|
1489
|
-
}
|
|
1490
|
-
if (authShards.length > 1) {
|
|
998
|
+
if (!suiteId) {
|
|
1491
999
|
// eslint-disable-next-line no-console
|
|
1492
1000
|
console.log(
|
|
1493
|
-
|
|
1001
|
+
" Tip: hardcode --suite-id or --ids in your workflow (see templates/github-actions/qualty-local-pr.yml)."
|
|
1494
1002
|
);
|
|
1495
1003
|
}
|
|
1496
1004
|
// eslint-disable-next-line no-console
|
|
@@ -1525,35 +1033,13 @@ async function runCiSetup(args) {
|
|
|
1525
1033
|
// eslint-disable-next-line no-console
|
|
1526
1034
|
console.log("---");
|
|
1527
1035
|
// eslint-disable-next-line no-console
|
|
1528
|
-
console.log(
|
|
1036
|
+
console.log("Name: QUALTY_AUTH_PROFILE");
|
|
1529
1037
|
// eslint-disable-next-line no-console
|
|
1530
1038
|
console.log("Value:");
|
|
1531
1039
|
// eslint-disable-next-line no-console
|
|
1532
|
-
console.log(
|
|
1040
|
+
console.log(profile);
|
|
1533
1041
|
// eslint-disable-next-line no-console
|
|
1534
|
-
console.log(` gh variable set
|
|
1535
|
-
if (primaryProfile) {
|
|
1536
|
-
// eslint-disable-next-line no-console
|
|
1537
|
-
console.log("---");
|
|
1538
|
-
// eslint-disable-next-line no-console
|
|
1539
|
-
console.log("Name: QUALTY_AUTH_PROFILE");
|
|
1540
|
-
// eslint-disable-next-line no-console
|
|
1541
|
-
console.log("Value:");
|
|
1542
|
-
// eslint-disable-next-line no-console
|
|
1543
|
-
console.log(primaryProfile);
|
|
1544
|
-
// eslint-disable-next-line no-console
|
|
1545
|
-
console.log(` gh variable set QUALTY_AUTH_PROFILE --body "${primaryProfile}"`);
|
|
1546
|
-
}
|
|
1547
|
-
if (authShards.length > 1) {
|
|
1548
|
-
// eslint-disable-next-line no-console
|
|
1549
|
-
console.log("---");
|
|
1550
|
-
// eslint-disable-next-line no-console
|
|
1551
|
-
console.log(`Name: ${AUTH_SHARDS_VARIABLE}`);
|
|
1552
|
-
// eslint-disable-next-line no-console
|
|
1553
|
-
console.log("Value:");
|
|
1554
|
-
// eslint-disable-next-line no-console
|
|
1555
|
-
console.log(String(authShards.length));
|
|
1556
|
-
}
|
|
1042
|
+
console.log(` gh variable set QUALTY_AUTH_PROFILE --body "${profile}"`);
|
|
1557
1043
|
// eslint-disable-next-line no-console
|
|
1558
1044
|
console.log("---");
|
|
1559
1045
|
// eslint-disable-next-line no-console
|
|
@@ -1594,20 +1080,20 @@ async function runCiSetup(args) {
|
|
|
1594
1080
|
// eslint-disable-next-line no-console
|
|
1595
1081
|
console.log("(create in Qualty → Account → API tokens; must start with qlty_ci_)");
|
|
1596
1082
|
}
|
|
1597
|
-
|
|
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) {
|
|
1598
1088
|
// eslint-disable-next-line no-console
|
|
1599
|
-
console.log(
|
|
1089
|
+
console.log(
|
|
1090
|
+
`Value: (see file above${copyToClipboardFlag ? " or clipboard" : ""})`
|
|
1091
|
+
);
|
|
1092
|
+
} else {
|
|
1600
1093
|
// eslint-disable-next-line no-console
|
|
1601
|
-
console.log(
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
console.log(`Value: (see file above${copyToClipboardFlag && shard.secretName === AUTH_SECRET_PRIMARY ? " or clipboard" : ""})`);
|
|
1605
|
-
} else {
|
|
1606
|
-
// eslint-disable-next-line no-console
|
|
1607
|
-
console.log("Value:");
|
|
1608
|
-
// eslint-disable-next-line no-console
|
|
1609
|
-
console.log(shard.payload);
|
|
1610
|
-
}
|
|
1094
|
+
console.log("Value:");
|
|
1095
|
+
// eslint-disable-next-line no-console
|
|
1096
|
+
console.log(encoded.payload);
|
|
1611
1097
|
}
|
|
1612
1098
|
// eslint-disable-next-line no-console
|
|
1613
1099
|
console.log("---");
|
|
@@ -1621,26 +1107,17 @@ async function runCiSetup(args) {
|
|
|
1621
1107
|
// eslint-disable-next-line no-console
|
|
1622
1108
|
console.log("");
|
|
1623
1109
|
// eslint-disable-next-line no-console
|
|
1624
|
-
console.log(githubAuthRestoreShell({
|
|
1110
|
+
console.log(githubAuthRestoreShell({ orgId, projectId, profile }));
|
|
1625
1111
|
// eslint-disable-next-line no-console
|
|
1626
1112
|
console.log("");
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
}
|
|
1631
|
-
if (profileNames.length === 1) {
|
|
1632
|
-
// eslint-disable-next-line no-console
|
|
1633
|
-
console.log(`Optional override: --auth-profile "${profileNames[0]}"`);
|
|
1634
|
-
} else {
|
|
1635
|
-
// eslint-disable-next-line no-console
|
|
1636
|
-
console.log("Omit --auth-profile in CI so each test uses its dashboard saved login profile.");
|
|
1637
|
-
}
|
|
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)`);
|
|
1638
1117
|
// eslint-disable-next-line no-console
|
|
1639
1118
|
console.log("");
|
|
1640
1119
|
// eslint-disable-next-line no-console
|
|
1641
|
-
console.log(
|
|
1642
|
-
"When login expires, re-run qualty auth setup and qualty auth export --set-github (or qualty ci setup --set-github for a full refresh)."
|
|
1643
|
-
);
|
|
1120
|
+
console.log("When login expires, re-run qualty auth setup and qualty auth export-ci to refresh the secret.");
|
|
1644
1121
|
// eslint-disable-next-line no-console
|
|
1645
1122
|
console.log("");
|
|
1646
1123
|
}
|
|
@@ -2753,8 +2230,8 @@ async function runAuthSetup(args) {
|
|
|
2753
2230
|
console.log("");
|
|
2754
2231
|
// eslint-disable-next-line no-console
|
|
2755
2232
|
console.log(
|
|
2756
|
-
`[qualty][auth]
|
|
2757
|
-
` 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}"`
|
|
2758
2235
|
);
|
|
2759
2236
|
} finally {
|
|
2760
2237
|
rl.close();
|
|
@@ -2776,29 +2253,17 @@ async function main() {
|
|
|
2776
2253
|
await runSetup(args);
|
|
2777
2254
|
return;
|
|
2778
2255
|
}
|
|
2779
|
-
if (command === "ci") {
|
|
2780
|
-
const sub = args._[1];
|
|
2781
|
-
if (sub === "setup") {
|
|
2782
|
-
await runCiSetup(args);
|
|
2783
|
-
return;
|
|
2784
|
-
}
|
|
2785
|
-
throw new Error("Unknown ci subcommand. Use: qualty ci setup");
|
|
2786
|
-
}
|
|
2787
2256
|
if (command === "auth") {
|
|
2788
2257
|
const sub = args._[1];
|
|
2789
2258
|
if (sub === "setup") {
|
|
2790
2259
|
await runAuthSetup(args);
|
|
2791
2260
|
return;
|
|
2792
2261
|
}
|
|
2793
|
-
if (sub === "export") {
|
|
2794
|
-
await
|
|
2795
|
-
return;
|
|
2796
|
-
}
|
|
2797
|
-
if (sub === "restore-ci") {
|
|
2798
|
-
await runAuthRestoreCi(args);
|
|
2262
|
+
if (sub === "export-ci") {
|
|
2263
|
+
await runAuthExportCi(args);
|
|
2799
2264
|
return;
|
|
2800
2265
|
}
|
|
2801
|
-
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");
|
|
2802
2267
|
}
|
|
2803
2268
|
if (command === "run") {
|
|
2804
2269
|
if (parseBoolean(args.local, false)) {
|