qualty 0.1.29 → 0.1.30

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