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/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
- 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();
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 String(creds.api_token || "").trim();
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
- " [--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",
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 Project .env first (same as MCP), then process env, then ~/.qualty/credentials",
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 = resolveToken(args);
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 ciSetupAuthBundleOutputPath(statePath) {
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
- profileNames,
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
- for (const shard of authShards || []) {
685
- record("secret", shard.secretName, setGithubSecret(shard.secretName, shard.payload, { repo }));
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 missingCiSetupField(fieldLabel, { flag, configKey, configPath, hasFlag }) {
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 resolveCiSetupConfig(args, { requireProfile = true } = {}) {
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
- missingCiSetupField("org ID", {
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
- missingCiSetupField("project ID", {
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
- missingCiSetupField("auth profile", {
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
- async function resolveRequiredProfileNamesFromTests({
970
- apiUrl,
971
- token,
972
- orgId,
973
- projectId,
974
- suiteId,
975
- explicitIds,
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
- return entries;
819
+ return {
820
+ encoding: "base64",
821
+ payload: raw.toString("base64"),
822
+ rawBytes: raw.length,
823
+ };
1076
824
  }
1077
825
 
1078
- function githubAuthRestoreShell({ shardCount = 1 } = {}) {
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 states`,
828
+ `- name: Restore Qualty login state`,
1086
829
  ` env:`,
1087
- ...shardEnvLines,
1088
- ` QUALTY_ORG_ID: \${{ vars.QUALTY_ORG_ID }}`,
1089
- ` QUALTY_PROJECT_ID: \${{ vars.QUALTY_PROJECT_ID }}`,
1090
- ` run: npx --yes qualty auth restore-ci`,
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 runAuthRestoreCi(args) {
1108
- const orgId = String(args.org || process.env.QUALTY_ORG_ID || "").trim();
1109
- const projectId = String(args.project || process.env.QUALTY_PROJECT_ID || "").trim();
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
- 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
- ) {
863
+ const statePath = localAuthStatePath({ orgId, projectId, profile });
864
+ if (!existsSync(statePath)) {
1152
865
  throw new Error(
1153
- "qualty auth export only updates login profile secrets. For full CI bootstrap use: qualty ci setup"
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
- 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)`
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
- // 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}`);
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
- // eslint-disable-next-line no-console
1249
- console.log("");
901
+ secretBytes = Buffer.byteLength(encoded.payload, "utf8");
1250
902
  }
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)."
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 || configSuiteId || "").trim();
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, primaryShard.payload, "utf8");
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
- // eslint-disable-next-line no-console
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
- ` - ${shard.secretName}: ${shard.profileNames.join(", ")} (${Buffer.byteLength(shard.payload, "utf8")} bytes)`
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 ${AUTH_SECRET_PRIMARY} to ${outputPath}`);
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 ${AUTH_SECRET_PRIMARY} < ${outputPath}`);
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: primaryProfile,
1445
- profileNames,
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 failedAuth = syncResults.filter((row) => row.kind === "secret" && !row.ok);
1455
- if (failedAuth.length > 0 && outputPath) {
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(" Manual auth secrets:");
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(primaryShard.payload)) {
979
+ if (copyToClipboard(encoded.payload)) {
1470
980
  // eslint-disable-next-line no-console
1471
- console.log(`✓ Copied ${AUTH_SECRET_PRIMARY} to clipboard.`);
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 && !configSuiteId && explicitIds.length === 0) {
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
- ` Auth was split across ${authShards.length} secrets. Add optional shard env vars to your workflow restore step.`
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(`Name: ${AUTH_PROFILES_VARIABLE}`);
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(profileNames.join(","));
1040
+ console.log(profile);
1533
1041
  // eslint-disable-next-line no-console
1534
- console.log(` gh variable set ${AUTH_PROFILES_VARIABLE} --body "${profileNames.join(",")}"`);
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
- for (const shard of authShards) {
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(`Name: ${shard.secretName}`);
1602
- if (skipPayloadPrint) {
1603
- // eslint-disable-next-line no-console
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({ shardCount: authShards.length }));
1110
+ console.log(githubAuthRestoreShell({ orgId, projectId, profile }));
1625
1111
  // eslint-disable-next-line no-console
1626
1112
  console.log("");
1627
- for (const entry of profileEntries) {
1628
- // eslint-disable-next-line no-console
1629
- console.log(`Profile "${entry.name}" → ${entry.statePath}`);
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] Push this profile to GitHub Actions:\n` +
2757
- ` qualty auth export --project ${project.id} --profile "${profile}" --set-github`
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 runAuthExport(args);
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 | qualty auth restore-ci");
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)) {