qualty 0.1.25 → 0.1.27
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/qualty.js +325 -36
- package/package.json +1 -1
package/bin/qualty.js
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
const DEFAULT_QUALTY_API_URL = "https://qualty-api-production.up.railway.app";
|
|
31
31
|
|
|
32
32
|
const PROJECT_CONFIG_FILENAME = ".qualty.json";
|
|
33
|
+
const DOTENV_FILENAME = ".env";
|
|
33
34
|
const QUALTY_SHARED_CREDS_DIR = join(homedir(), ".qualty");
|
|
34
35
|
const QUALTY_SHARED_CREDS_PATH = join(QUALTY_SHARED_CREDS_DIR, "credentials");
|
|
35
36
|
|
|
@@ -48,6 +49,47 @@ function getProjectConfigWritePath() {
|
|
|
48
49
|
return findProjectConfigPath() || join(process.cwd(), PROJECT_CONFIG_FILENAME);
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
function findProjectDotenvPath() {
|
|
53
|
+
let current = process.cwd();
|
|
54
|
+
for (;;) {
|
|
55
|
+
const candidate = join(current, DOTENV_FILENAME);
|
|
56
|
+
if (existsSync(candidate)) return candidate;
|
|
57
|
+
const parent = dirname(current);
|
|
58
|
+
if (parent === current) return null;
|
|
59
|
+
current = parent;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseDotenvText(text) {
|
|
64
|
+
const out = {};
|
|
65
|
+
for (const raw of String(text || "").split("\n")) {
|
|
66
|
+
const line = raw.trim();
|
|
67
|
+
if (!line || line.startsWith("#") || !line.includes("=")) continue;
|
|
68
|
+
const eq = line.indexOf("=");
|
|
69
|
+
const key = line.slice(0, eq).trim();
|
|
70
|
+
let value = line.slice(eq + 1).trim();
|
|
71
|
+
if (
|
|
72
|
+
(value.startsWith('"') && value.endsWith('"'))
|
|
73
|
+
|| (value.startsWith("'") && value.endsWith("'"))
|
|
74
|
+
) {
|
|
75
|
+
value = value.slice(1, -1);
|
|
76
|
+
}
|
|
77
|
+
if (key) out[key] = value;
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Project .env walked up from cwd (same resolution as Qualty MCP). */
|
|
83
|
+
function loadProjectDotenv() {
|
|
84
|
+
const path = findProjectDotenvPath();
|
|
85
|
+
if (!path) return {};
|
|
86
|
+
try {
|
|
87
|
+
return parseDotenvText(readFileSync(path, "utf8"));
|
|
88
|
+
} catch {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
51
93
|
function readSharedCredentials() {
|
|
52
94
|
if (!existsSync(QUALTY_SHARED_CREDS_PATH)) return {};
|
|
53
95
|
try {
|
|
@@ -124,7 +166,14 @@ function normalizeApiUrl(raw, { label = "API URL" } = {}) {
|
|
|
124
166
|
}
|
|
125
167
|
|
|
126
168
|
function qualtyBaseUrlFromEnv() {
|
|
127
|
-
|
|
169
|
+
const dotenv = loadProjectDotenv();
|
|
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();
|
|
128
177
|
}
|
|
129
178
|
|
|
130
179
|
function resolveApiUrl(args = {}) {
|
|
@@ -142,8 +191,14 @@ function resolveApiUrl(args = {}) {
|
|
|
142
191
|
}
|
|
143
192
|
|
|
144
193
|
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;
|
|
145
200
|
const creds = readSharedCredentials();
|
|
146
|
-
return
|
|
201
|
+
return String(creds.api_token || "").trim();
|
|
147
202
|
}
|
|
148
203
|
|
|
149
204
|
function resolveOrgId(args = {}) {
|
|
@@ -187,13 +242,17 @@ function usage() {
|
|
|
187
242
|
"Usage:",
|
|
188
243
|
" qualty setup [--token <bearer-token>] [--org <org-id>]",
|
|
189
244
|
" qualty auth setup [--project <project-id>] [--profile <profile-name>] [--headed]",
|
|
190
|
-
" qualty auth export
|
|
191
|
-
"
|
|
192
|
-
"
|
|
193
|
-
"
|
|
194
|
-
"
|
|
195
|
-
"
|
|
196
|
-
"
|
|
245
|
+
" qualty auth export [--project <project-id>] [--profile <profile-name>] [--org <org-id>]",
|
|
246
|
+
" [--profiles <name1,name2>] [--repo <owner/repo>]",
|
|
247
|
+
" [--output <path>] [--no-file] [--copy] [--set-secret | --set-github]",
|
|
248
|
+
" Push login profile state to GitHub only (not API token, org, or project vars).",
|
|
249
|
+
" qualty ci setup [--project <project-id>] [--profile <profile-name>] [--org <org-id>]",
|
|
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",
|
|
197
256
|
" qualty auth restore-ci [--org <org-id>] [--project <project-id>] [--profile <fallback-profile>]",
|
|
198
257
|
" Reads QUALTY_AUTH_STATE_B64 (+ _2, _3, … shards) and writes ~/.qualty/local-contexts/…",
|
|
199
258
|
" qualty connect --project <project-id> [--port 3000] [--token <bearer-token>] [--org <org-id>]",
|
|
@@ -206,7 +265,7 @@ function usage() {
|
|
|
206
265
|
" qualty resolve --project <project-id> [--suite-id <suite-id>] [--ids <id1,id2>] [--json] [--token <bearer-token>]",
|
|
207
266
|
"",
|
|
208
267
|
"Env vars:",
|
|
209
|
-
" QUALTY_API_TOKEN
|
|
268
|
+
" QUALTY_API_TOKEN Project .env first (same as MCP), then process env, then ~/.qualty/credentials",
|
|
210
269
|
" QUALTY_BASE_URL API base URL when --url is not set (default: production)",
|
|
211
270
|
" QUALTY_LOCAL_CONCURRENCY Local parallel workers for --local runs (default: 4)",
|
|
212
271
|
"",
|
|
@@ -393,9 +452,7 @@ async function runSetup(args) {
|
|
|
393
452
|
|
|
394
453
|
try {
|
|
395
454
|
const currentOrgId = String(args.org || projectCfg.default_org_id || "");
|
|
396
|
-
const currentToken =
|
|
397
|
-
args.token || process.env.QUALTY_API_TOKEN || sharedCreds.api_token || ""
|
|
398
|
-
);
|
|
455
|
+
const currentToken = resolveToken(args);
|
|
399
456
|
|
|
400
457
|
let token = "";
|
|
401
458
|
if (args.token) {
|
|
@@ -542,7 +599,7 @@ function localAuthStatePath({ orgId, projectId, profile }) {
|
|
|
542
599
|
return join(QUALTY_SHARED_CREDS_DIR, "local-contexts", orgSeg, projectSeg, `${profileSeg}.json`);
|
|
543
600
|
}
|
|
544
601
|
|
|
545
|
-
function
|
|
602
|
+
function ciSetupAuthBundleOutputPath(statePath) {
|
|
546
603
|
return statePath.replace(/\.json$/i, ".b64");
|
|
547
604
|
}
|
|
548
605
|
|
|
@@ -674,6 +731,37 @@ function syncGithubCiConfig({
|
|
|
674
731
|
return results;
|
|
675
732
|
}
|
|
676
733
|
|
|
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
|
+
|
|
677
765
|
function printGithubSyncResults(results, { repo }) {
|
|
678
766
|
const target = repo ? ` (repo ${repo})` : "";
|
|
679
767
|
const printGroup = (title, rows) => {
|
|
@@ -754,7 +842,7 @@ function configStringValue(projectCfg, key, configPath) {
|
|
|
754
842
|
);
|
|
755
843
|
}
|
|
756
844
|
|
|
757
|
-
function
|
|
845
|
+
function missingCiSetupField(fieldLabel, { flag, configKey, configPath, hasFlag }) {
|
|
758
846
|
const configHint = configPath
|
|
759
847
|
? `${PROJECT_CONFIG_FILENAME} at ${configPath} (${configKey})`
|
|
760
848
|
: `${PROJECT_CONFIG_FILENAME} (not found walking up from ${process.cwd()})`;
|
|
@@ -770,7 +858,7 @@ function missingExportCiField(fieldLabel, { flag, configKey, configPath, hasFlag
|
|
|
770
858
|
].join(" ");
|
|
771
859
|
}
|
|
772
860
|
|
|
773
|
-
function
|
|
861
|
+
function resolveCiSetupConfig(args, { requireProfile = true } = {}) {
|
|
774
862
|
const { configPath, config: projectCfg } = readProjectConfigStrict();
|
|
775
863
|
|
|
776
864
|
const orgFromFlag = String(args.org || "").trim();
|
|
@@ -804,7 +892,7 @@ function resolveExportCiConfig(args, { requireProfile = true } = {}) {
|
|
|
804
892
|
|
|
805
893
|
if (!orgId) {
|
|
806
894
|
throw new Error(
|
|
807
|
-
|
|
895
|
+
missingCiSetupField("org ID", {
|
|
808
896
|
flag: "--org",
|
|
809
897
|
configKey: "default_org_id",
|
|
810
898
|
configPath,
|
|
@@ -814,7 +902,7 @@ function resolveExportCiConfig(args, { requireProfile = true } = {}) {
|
|
|
814
902
|
}
|
|
815
903
|
if (!projectId) {
|
|
816
904
|
throw new Error(
|
|
817
|
-
|
|
905
|
+
missingCiSetupField("project ID", {
|
|
818
906
|
flag: "--project",
|
|
819
907
|
configKey: "default_project_id",
|
|
820
908
|
configPath,
|
|
@@ -827,7 +915,7 @@ function resolveExportCiConfig(args, { requireProfile = true } = {}) {
|
|
|
827
915
|
allProfiles || profilesList.length > 0 || Boolean(suiteId) || explicitIds.length > 0;
|
|
828
916
|
if (requireProfile && !profile && !hasMultiProfileMode) {
|
|
829
917
|
throw new Error(
|
|
830
|
-
|
|
918
|
+
missingCiSetupField("auth profile", {
|
|
831
919
|
flag: "--profile",
|
|
832
920
|
configKey: "default_auth_profile",
|
|
833
921
|
configPath,
|
|
@@ -910,7 +998,7 @@ async function resolveRequiredProfileNamesFromTests({
|
|
|
910
998
|
return [...names];
|
|
911
999
|
}
|
|
912
1000
|
|
|
913
|
-
async function
|
|
1001
|
+
async function resolveCiSetupProfileEntries({
|
|
914
1002
|
orgId,
|
|
915
1003
|
projectId,
|
|
916
1004
|
profile,
|
|
@@ -1033,7 +1121,7 @@ async function runAuthRestoreCi(args) {
|
|
|
1033
1121
|
const payloads = collectAuthShardPayloadsFromEnv(process.env);
|
|
1034
1122
|
if (payloads.length === 0) {
|
|
1035
1123
|
throw new Error(
|
|
1036
|
-
`Missing ${AUTH_SECRET_PRIMARY}. Run qualty auth export
|
|
1124
|
+
`Missing ${AUTH_SECRET_PRIMARY}. Run qualty ci setup --set-github (first time) or qualty auth export --set-github (profile refresh).`
|
|
1037
1125
|
);
|
|
1038
1126
|
}
|
|
1039
1127
|
|
|
@@ -1052,7 +1140,198 @@ async function runAuthRestoreCi(args) {
|
|
|
1052
1140
|
console.log(`Restored ${restored.length} login profile(s).`);
|
|
1053
1141
|
}
|
|
1054
1142
|
|
|
1055
|
-
async function
|
|
1143
|
+
async function runAuthExport(args) {
|
|
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
|
+
) {
|
|
1152
|
+
throw new Error(
|
|
1153
|
+
"qualty auth export only updates login profile secrets. For full CI bootstrap use: qualty ci setup"
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
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
|
+
const setGithubFlag =
|
|
1185
|
+
parseBoolean(args["set-github"], false) || parseBoolean(args["set-secret"], false);
|
|
1186
|
+
const noFile = parseBoolean(args["no-file"], false);
|
|
1187
|
+
const outputPath = noFile
|
|
1188
|
+
? String(args.output || "").trim()
|
|
1189
|
+
: String(args.output || "").trim() ||
|
|
1190
|
+
ciSetupAuthBundleOutputPath(
|
|
1191
|
+
localAuthStatePath({ orgId, projectId, profile: profileEntries[0].localProfileName })
|
|
1192
|
+
);
|
|
1193
|
+
const copyToClipboardFlag = parseBoolean(args.copy, false);
|
|
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)`
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
// eslint-disable-next-line no-console
|
|
1238
|
+
console.log("");
|
|
1239
|
+
if (outputPath) {
|
|
1240
|
+
// eslint-disable-next-line no-console
|
|
1241
|
+
console.log(`Wrote ${AUTH_SECRET_PRIMARY} to ${outputPath}`);
|
|
1242
|
+
for (let index = 1; index < authShards.length; index += 1) {
|
|
1243
|
+
const shard = authShards[index];
|
|
1244
|
+
const shardPath = outputPath.replace(/(\.[^./]+)?$/, `_${index + 1}$1`);
|
|
1245
|
+
// eslint-disable-next-line no-console
|
|
1246
|
+
console.log(`Wrote ${shard.secretName} to ${shardPath}`);
|
|
1247
|
+
}
|
|
1248
|
+
// eslint-disable-next-line no-console
|
|
1249
|
+
console.log("");
|
|
1250
|
+
}
|
|
1251
|
+
if (setGithubFlag) {
|
|
1252
|
+
const syncResults = syncGithubAuthProfileOnly({
|
|
1253
|
+
repo: githubRepo,
|
|
1254
|
+
profile: primaryProfile,
|
|
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)."
|
|
1301
|
+
);
|
|
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
|
+
}
|
|
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
|
+
|
|
1334
|
+
async function runCiSetup(args) {
|
|
1056
1335
|
const {
|
|
1057
1336
|
orgId,
|
|
1058
1337
|
projectId,
|
|
@@ -1061,18 +1340,18 @@ async function runAuthExportCi(args) {
|
|
|
1061
1340
|
configPath,
|
|
1062
1341
|
allProfiles,
|
|
1063
1342
|
profilesList,
|
|
1064
|
-
suiteId:
|
|
1343
|
+
suiteId: configSuiteId,
|
|
1065
1344
|
explicitIds,
|
|
1066
|
-
} =
|
|
1345
|
+
} = resolveCiSetupConfig(args);
|
|
1067
1346
|
const token = resolveToken(args);
|
|
1068
1347
|
|
|
1069
|
-
const profileEntries = await
|
|
1348
|
+
const profileEntries = await resolveCiSetupProfileEntries({
|
|
1070
1349
|
orgId,
|
|
1071
1350
|
projectId,
|
|
1072
1351
|
profile,
|
|
1073
1352
|
allProfiles,
|
|
1074
1353
|
profilesList,
|
|
1075
|
-
suiteId:
|
|
1354
|
+
suiteId: configSuiteId,
|
|
1076
1355
|
explicitIds,
|
|
1077
1356
|
apiUrl,
|
|
1078
1357
|
token,
|
|
@@ -1090,12 +1369,12 @@ async function runAuthExportCi(args) {
|
|
|
1090
1369
|
const outputPath = noFile
|
|
1091
1370
|
? String(args.output || "").trim()
|
|
1092
1371
|
: String(args.output || "").trim() ||
|
|
1093
|
-
|
|
1372
|
+
ciSetupAuthBundleOutputPath(
|
|
1094
1373
|
localAuthStatePath({ orgId, projectId, profile: profileEntries[0].localProfileName })
|
|
1095
1374
|
);
|
|
1096
1375
|
const copyToClipboardFlag = parseBoolean(args.copy, false);
|
|
1097
1376
|
const githubRepo = String(args.repo || "").trim();
|
|
1098
|
-
const suiteId = String(args["suite-id"] || args.suite ||
|
|
1377
|
+
const suiteId = String(args["suite-id"] || args.suite || configSuiteId || "").trim();
|
|
1099
1378
|
const cliRepo = String(args["cli-repo"] || "").trim();
|
|
1100
1379
|
const cliRef = String(args["cli-ref"] || "").trim();
|
|
1101
1380
|
|
|
@@ -1202,10 +1481,10 @@ async function runAuthExportCi(args) {
|
|
|
1202
1481
|
console.log(
|
|
1203
1482
|
"1. GitHub Actions config was pushed via gh (secrets + repository variables — see sync above). Fix any ✗ manually."
|
|
1204
1483
|
);
|
|
1205
|
-
if (!suiteId && !
|
|
1484
|
+
if (!suiteId && !configSuiteId && explicitIds.length === 0) {
|
|
1206
1485
|
// eslint-disable-next-line no-console
|
|
1207
1486
|
console.log(
|
|
1208
|
-
" Tip: pass --suite-id or --ids to
|
|
1487
|
+
" Tip: pass --suite-id or --ids to bundle only login profiles required by those tests."
|
|
1209
1488
|
);
|
|
1210
1489
|
}
|
|
1211
1490
|
if (authShards.length > 1) {
|
|
@@ -1359,7 +1638,9 @@ async function runAuthExportCi(args) {
|
|
|
1359
1638
|
// eslint-disable-next-line no-console
|
|
1360
1639
|
console.log("");
|
|
1361
1640
|
// eslint-disable-next-line no-console
|
|
1362
|
-
console.log(
|
|
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
|
+
);
|
|
1363
1644
|
// eslint-disable-next-line no-console
|
|
1364
1645
|
console.log("");
|
|
1365
1646
|
}
|
|
@@ -2472,8 +2753,8 @@ async function runAuthSetup(args) {
|
|
|
2472
2753
|
console.log("");
|
|
2473
2754
|
// eslint-disable-next-line no-console
|
|
2474
2755
|
console.log(
|
|
2475
|
-
`[qualty][auth]
|
|
2476
|
-
` qualty auth export
|
|
2756
|
+
`[qualty][auth] Push this profile to GitHub Actions:\n` +
|
|
2757
|
+
` qualty auth export --project ${project.id} --profile "${profile}" --set-github`
|
|
2477
2758
|
);
|
|
2478
2759
|
} finally {
|
|
2479
2760
|
rl.close();
|
|
@@ -2495,21 +2776,29 @@ async function main() {
|
|
|
2495
2776
|
await runSetup(args);
|
|
2496
2777
|
return;
|
|
2497
2778
|
}
|
|
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
|
+
}
|
|
2498
2787
|
if (command === "auth") {
|
|
2499
2788
|
const sub = args._[1];
|
|
2500
2789
|
if (sub === "setup") {
|
|
2501
2790
|
await runAuthSetup(args);
|
|
2502
2791
|
return;
|
|
2503
2792
|
}
|
|
2504
|
-
if (sub === "export
|
|
2505
|
-
await
|
|
2793
|
+
if (sub === "export") {
|
|
2794
|
+
await runAuthExport(args);
|
|
2506
2795
|
return;
|
|
2507
2796
|
}
|
|
2508
2797
|
if (sub === "restore-ci") {
|
|
2509
2798
|
await runAuthRestoreCi(args);
|
|
2510
2799
|
return;
|
|
2511
2800
|
}
|
|
2512
|
-
throw new Error("Unknown auth subcommand. Use: qualty auth setup | qualty auth export
|
|
2801
|
+
throw new Error("Unknown auth subcommand. Use: qualty auth setup | qualty auth export | qualty auth restore-ci");
|
|
2513
2802
|
}
|
|
2514
2803
|
if (command === "run") {
|
|
2515
2804
|
if (parseBoolean(args.local, false)) {
|