@levr-one/cli 0.10.20 → 0.10.21

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.
Files changed (23) hide show
  1. package/dist/{addHandler-r0Yw1vu-.js → addHandler-DUulKXOX.js} +68 -68
  2. package/dist/appendHandler-DkbMvFOn.js +80 -0
  3. package/dist/artifact-upload-BnqeVY_b.js +296 -0
  4. package/dist/attachHandler-BOxIqxAw.js +151 -0
  5. package/dist/cli.js +237 -17
  6. package/dist/connect-CPybjrXT.js +36 -0
  7. package/dist/{currentHandler-BBHqC26D.js → currentHandler-B_sWXNeW.js} +1 -1
  8. package/dist/{importHandler-DAXXM42X.js → importHandler-oENkrali.js} +8 -8
  9. package/dist/{listHandler-qFOGgW2z.js → listHandler-E666F9uV.js} +5 -5
  10. package/dist/{loginHandler-DMjKtKj5.js → loginHandler-DS29PzLz.js} +5 -10
  11. package/dist/{logoutHandler-BFutKash.js → logoutHandler-TSQlZGpU.js} +2 -2
  12. package/dist/{pushHandler-C7nxljuh.js → pushHandler-CR5vu3ci.js} +142 -9
  13. package/dist/{resolve-token-DbQsmn03.js → resolve-token-7xb7kG7h.js} +2 -2
  14. package/dist/{resolve-workspace--4K7N4VU.js → resolve-workspace-Dsm8Xzut.js} +2 -2
  15. package/dist/{runHandler-gaYkPS79.js → runHandler-B2DlFleh.js} +8 -8
  16. package/dist/{sdk-client-vQsoMCez.js → sdk-client-B_urshDp.js} +309 -48
  17. package/dist/{selectHandler-CPeH8ILF.js → selectHandler-Cv5fJl-p.js} +5 -5
  18. package/dist/sleep-BpJ39Ypm.js +7 -0
  19. package/dist/{statusHandler-WkN652yu.js → statusHandler-CwO5kl3m.js} +3 -3
  20. package/dist/{token-refresh-Cu5RpkLJ.js → token-refresh-DeusLK8H.js} +1 -1
  21. package/package.json +1 -1
  22. /package/dist/{env-CHeKHu5S.js → env-CdwyPHGV.js} +0 -0
  23. /package/dist/{workspace-store-DDOxnut1.js → workspace-store-CnrxyYRB.js} +0 -0
package/dist/cli.js CHANGED
@@ -175,7 +175,7 @@ Examples:
175
175
  aliases: { y: "yes" }
176
176
  },
177
177
  loader: async () => {
178
- const { mcpAddHandler } = await import("./addHandler-r0Yw1vu-.js");
178
+ const { mcpAddHandler } = await import("./addHandler-DUulKXOX.js");
179
179
  return mcpAddHandler;
180
180
  }
181
181
  });
@@ -213,7 +213,7 @@ Examples:
213
213
  aliases: { d: "device-code" }
214
214
  },
215
215
  loader: async () => {
216
- const { loginHandler } = await import("./loginHandler-DMjKtKj5.js");
216
+ const { loginHandler } = await import("./loginHandler-DS29PzLz.js");
217
217
  return loginHandler;
218
218
  }
219
219
  });
@@ -232,7 +232,7 @@ Examples:
232
232
  },
233
233
  parameters: {},
234
234
  loader: async () => {
235
- const { logoutHandler } = await import("./logoutHandler-BFutKash.js");
235
+ const { logoutHandler } = await import("./logoutHandler-TSQlZGpU.js");
236
236
  return logoutHandler;
237
237
  }
238
238
  });
@@ -252,7 +252,7 @@ Examples:
252
252
  },
253
253
  parameters: {},
254
254
  loader: async () => {
255
- const { statusHandler } = await import("./statusHandler-WkN652yu.js");
255
+ const { statusHandler } = await import("./statusHandler-CwO5kl3m.js");
256
256
  return statusHandler;
257
257
  }
258
258
  });
@@ -280,7 +280,18 @@ Examples:
280
280
  levr push ./results.xml --source "backend-unit-tests"
281
281
  levr push ./results.xml --automation-source <uuid>
282
282
  levr push ./report.json --source e2e --team-id <uuid> # explicit team
283
- levr push ./test-results.xml # in CI: source auto-detected`
283
+ levr push ./test-results.xml # in CI: source auto-detected
284
+ levr push e2e-report.xml -s e2e --artifacts e2e-reports --output-results results.json
285
+
286
+ --artifacts <dir> uploads the screenshots, traces and videos the report
287
+ references (JUnit [[ATTACHMENT|path]], CTRF attachments[]) to their results.
288
+ Paths resolve against <dir> (then the report's directory) and must stay
289
+ inside the working directory. Files over 50 MiB are skipped. An artifact
290
+ problem prints a summary and never changes the push exit code.
291
+
292
+ --output-results <file> writes every imported result (id, name, suite,
293
+ classname, status, test_key, attempts, attachments) as JSON, for
294
+ levr attach --manifest or later uploads.`
284
295
  },
285
296
  parameters: {
286
297
  positional: {
@@ -339,6 +350,20 @@ Examples:
339
350
  brief: "File format (auto-detected if omitted)",
340
351
  optional: true
341
352
  },
353
+ artifacts: {
354
+ kind: "parsed",
355
+ parse: String,
356
+ brief: "Upload the artifacts the report references, resolved against this directory",
357
+ placeholder: "dir",
358
+ optional: true
359
+ },
360
+ "output-results": {
361
+ kind: "parsed",
362
+ parse: String,
363
+ brief: "Write the imported results (ids, test_key, attachments) to a JSON file",
364
+ placeholder: "file",
365
+ optional: true
366
+ },
342
367
  verbose: {
343
368
  kind: "boolean",
344
369
  default: false,
@@ -356,7 +381,7 @@ Examples:
356
381
  }
357
382
  },
358
383
  loader: async () => {
359
- const { pushHandler } = await import("./pushHandler-C7nxljuh.js");
384
+ const { pushHandler } = await import("./pushHandler-CR5vu3ci.js");
360
385
  return pushHandler;
361
386
  }
362
387
  });
@@ -476,7 +501,7 @@ Examples:
476
501
  }
477
502
  },
478
503
  loader: async () => {
479
- const { importHandler } = await import("./importHandler-DAXXM42X.js");
504
+ const { importHandler } = await import("./importHandler-oENkrali.js");
480
505
  return importHandler;
481
506
  }
482
507
  });
@@ -497,7 +522,7 @@ Examples:
497
522
  },
498
523
  parameters: {},
499
524
  loader: async () => {
500
- const { listHandler } = await import("./listHandler-qFOGgW2z.js");
525
+ const { listHandler } = await import("./listHandler-E666F9uV.js");
501
526
  return listHandler;
502
527
  }
503
528
  });
@@ -530,7 +555,7 @@ Examples:
530
555
  flags: {}
531
556
  },
532
557
  loader: async () => {
533
- const { selectHandler } = await import("./selectHandler-CPeH8ILF.js");
558
+ const { selectHandler } = await import("./selectHandler-Cv5fJl-p.js");
534
559
  return selectHandler;
535
560
  }
536
561
  });
@@ -547,7 +572,7 @@ Examples:
547
572
  },
548
573
  parameters: {},
549
574
  loader: async () => {
550
- const { currentHandler } = await import("./currentHandler-BBHqC26D.js");
575
+ const { currentHandler } = await import("./currentHandler-B_sWXNeW.js");
551
576
  return currentHandler;
552
577
  }
553
578
  });
@@ -616,14 +641,203 @@ Examples:
616
641
  }
617
642
  },
618
643
  loader: async () => {
619
- const { gatesRunHandler } = await import("./runHandler-gaYkPS79.js");
644
+ const { gatesRunHandler } = await import("./runHandler-B2DlFleh.js");
620
645
  return gatesRunHandler;
621
646
  }
622
647
  });
623
648
 
649
+ //#endregion
650
+ //#region src/commands/attach.ts
651
+ const ATTACHMENT_KINDS = [
652
+ "screenshot",
653
+ "video",
654
+ "trace",
655
+ "stdout_log",
656
+ "stderr_log",
657
+ "har",
658
+ "report",
659
+ "attachment",
660
+ "other"
661
+ ];
662
+ const attachCommand = buildCommand({
663
+ docs: {
664
+ brief: "Attach local files to a Levr entity",
665
+ fullDescription: `Upload local files (screenshots, trace.zip, videos, logs) and attach them to
666
+ an issue, test, run or execution result, without passing base64 through an
667
+ MCP call.
668
+
669
+ Targets:
670
+ ENG-42 an issue, by identifier
671
+ TC-5 a test case
672
+ TR-3 a run
673
+ <related_type>:<uuid> any entity with attachments, e.g.
674
+ run_result_variant:<uuid>
675
+ automation_run_result:<uuid>
676
+ <related_type>:<identifier> an explicit type, e.g. issue:TC-12 for a
677
+ team whose key is TC or TR
678
+ Spoke identifiers (ENG-JP-5) work in every form.
679
+
680
+ Files are private: only signed-in workspace members can open them. --embed
681
+ also adds a link to the entity so they render there: a new comment on an
682
+ issue, test or run, or an append to an execution result's actual result.
683
+ Files over 50 MiB are skipped.
684
+
685
+ --manifest <json> attaches a batch: [{ "target": "...", "files": ["..."],
686
+ "kind": "trace", "attempt": 1 }]. --kind and --attempt apply to
687
+ automation_run_result targets only.
688
+
689
+ Exit code 1 when any file fails, is skipped, or an embed write fails.
690
+
691
+ Examples:
692
+ levr attach ENG-42 screenshot.png
693
+ levr attach run_result_variant:<uuid> shot.png --embed
694
+ levr attach automation_run_result:<uuid> trace.zip --kind trace --attempt 0
695
+ levr attach --manifest extra-artifacts.json`
696
+ },
697
+ parameters: {
698
+ positional: {
699
+ kind: "array",
700
+ parameter: {
701
+ parse: String,
702
+ brief: "Target, then the files to attach",
703
+ placeholder: "target|file"
704
+ }
705
+ },
706
+ flags: {
707
+ "workspace-id": {
708
+ kind: "parsed",
709
+ parse: String,
710
+ brief: "Workspace ID (required for multi-workspace JWT auth)",
711
+ placeholder: "uuid",
712
+ optional: true
713
+ },
714
+ kind: {
715
+ kind: "enum",
716
+ values: ATTACHMENT_KINDS,
717
+ brief: "Artifact kind (automation_run_result targets only)",
718
+ optional: true
719
+ },
720
+ attempt: {
721
+ kind: "parsed",
722
+ parse: (v) => {
723
+ if (!/^\d+$/.test(v)) throw new Error("--attempt must be >= 0");
724
+ return Number(v);
725
+ },
726
+ brief: "Index of the earlier attempt (automation_run_result only; omit for the final attempt)",
727
+ placeholder: "n",
728
+ optional: true
729
+ },
730
+ embed: {
731
+ kind: "boolean",
732
+ default: false,
733
+ brief: "Also add a markdown link (comment, or actual-result append)"
734
+ },
735
+ manifest: {
736
+ kind: "parsed",
737
+ parse: String,
738
+ brief: "JSON file listing [{ target, files, kind?, attempt? }]",
739
+ placeholder: "file",
740
+ optional: true
741
+ },
742
+ verbose: {
743
+ kind: "boolean",
744
+ default: false,
745
+ brief: "Show detailed output"
746
+ }
747
+ },
748
+ aliases: {
749
+ w: "workspace-id",
750
+ k: "kind",
751
+ e: "embed",
752
+ m: "manifest",
753
+ v: "verbose"
754
+ }
755
+ },
756
+ loader: async () => {
757
+ const { attachHandler } = await import("./attachHandler-BOxIqxAw.js");
758
+ return attachHandler;
759
+ }
760
+ });
761
+
762
+ //#endregion
763
+ //#region src/commands/result/append.ts
764
+ const resultAppendCommand = buildCommand({
765
+ docs: {
766
+ brief: "Append to an execution result's actual result",
767
+ fullDescription: `Append text to an execution result's actual result, server-side (no
768
+ read-modify-write), optionally with local files attached first and embedded
769
+ as markdown links. Attached files are private: the links open for signed-in
770
+ workspace members.
771
+
772
+ The target is run_result_variant:<uuid> — the execution result id the levr
773
+ MCP tools and levr-browser return.
774
+
775
+ Examples:
776
+ levr result append run_result_variant:<uuid> --text "Checkout returns 500"
777
+ levr result append run_result_variant:<uuid> --file observed.md --attach shot.png trace.zip`
778
+ },
779
+ parameters: {
780
+ positional: {
781
+ kind: "tuple",
782
+ parameters: [{
783
+ parse: String,
784
+ brief: "run_result_variant:<uuid>",
785
+ placeholder: "target"
786
+ }]
787
+ },
788
+ flags: {
789
+ "workspace-id": {
790
+ kind: "parsed",
791
+ parse: String,
792
+ brief: "Workspace ID (required for multi-workspace JWT auth)",
793
+ placeholder: "uuid",
794
+ optional: true
795
+ },
796
+ text: {
797
+ kind: "parsed",
798
+ parse: String,
799
+ brief: "Text to append",
800
+ placeholder: "text",
801
+ optional: true
802
+ },
803
+ file: {
804
+ kind: "parsed",
805
+ parse: String,
806
+ brief: "Read the text to append from a file",
807
+ placeholder: "path",
808
+ optional: true
809
+ },
810
+ attach: {
811
+ kind: "parsed",
812
+ parse: String,
813
+ brief: "Files to attach and embed (repeatable)",
814
+ placeholder: "file",
815
+ variadic: true,
816
+ optional: true
817
+ },
818
+ verbose: {
819
+ kind: "boolean",
820
+ default: false,
821
+ brief: "Show detailed output"
822
+ }
823
+ },
824
+ aliases: {
825
+ w: "workspace-id",
826
+ t: "text",
827
+ f: "file",
828
+ a: "attach",
829
+ v: "verbose"
830
+ }
831
+ },
832
+ loader: async () => {
833
+ const { resultAppendHandler } = await import("./appendHandler-DkbMvFOn.js");
834
+ return resultAppendHandler;
835
+ }
836
+ });
837
+
624
838
  //#endregion
625
839
  //#region package.json
626
- var version = "0.10.20";
840
+ var version = "0.10.21";
627
841
 
628
842
  //#endregion
629
843
  //#region src/app.ts
@@ -647,6 +861,10 @@ const gatesRoutes = buildRouteMap({
647
861
  routes: { run: gatesRunCommand },
648
862
  docs: { brief: "Run a deliverable's gates and report the raw results" }
649
863
  });
864
+ const resultRoutes = buildRouteMap({
865
+ routes: { append: resultAppendCommand },
866
+ docs: { brief: "Work with execution results" }
867
+ });
650
868
  const routes = buildRouteMap({
651
869
  routes: {
652
870
  mcp: buildRouteMap({
@@ -658,6 +876,8 @@ const routes = buildRouteMap({
658
876
  gates: gatesRoutes,
659
877
  push: pushCommand,
660
878
  import: importCommand,
879
+ attach: attachCommand,
880
+ result: resultRoutes,
661
881
  completion: completionCommand
662
882
  },
663
883
  docs: { brief: "The command-line interface for Levr" }
@@ -738,10 +958,10 @@ function envFilePath(cwd, env) {
738
958
  * getting silence is the one case where quiet failure hides a real mistake.
739
959
  */
740
960
  function loadEnvFile(cwd = process.cwd(), env = process.env) {
741
- const path = envFilePath(cwd, env);
961
+ const path$1 = envFilePath(cwd, env);
742
962
  const explicitlyRequested = Boolean(env["LEVR_ENV_FILE"]);
743
- if (!existsSync(path)) {
744
- if (explicitlyRequested) throw new Error(`LEVR_ENV_FILE points at "${path}", which does not exist.`);
963
+ if (!existsSync(path$1)) {
964
+ if (explicitlyRequested) throw new Error(`LEVR_ENV_FILE points at "${path$1}", which does not exist.`);
745
965
  return {
746
966
  path: null,
747
967
  applied: []
@@ -749,7 +969,7 @@ function loadEnvFile(cwd = process.cwd(), env = process.env) {
749
969
  }
750
970
  let parsed;
751
971
  try {
752
- parsed = parse(readFileSync(path, "utf8"));
972
+ parsed = parse(readFileSync(path$1, "utf8"));
753
973
  } catch (error) {
754
974
  if (explicitlyRequested) throw error;
755
975
  return {
@@ -765,7 +985,7 @@ function loadEnvFile(cwd = process.cwd(), env = process.env) {
765
985
  applied.push(key);
766
986
  }
767
987
  return {
768
- path,
988
+ path: path$1,
769
989
  applied
770
990
  };
771
991
  }
@@ -0,0 +1,36 @@
1
+ import { client, configureClient } from "./sdk-client-B_urshDp.js";
2
+ import { resolveToken } from "./resolve-token-7xb7kG7h.js";
3
+ import { resolveWorkspace } from "./resolve-workspace-Dsm8Xzut.js";
4
+
5
+ //#region src/utils/connect.ts
6
+ /**
7
+ * Authenticate the SDK client for a command (ENG-6164): resolve the token,
8
+ * and for a JWT the workspace. Reports the failure and sets exit code 1;
9
+ * returns false when the command must stop.
10
+ */
11
+ async function connect(ctx, workspaceIdFlag) {
12
+ let auth;
13
+ try {
14
+ auth = await resolveToken();
15
+ } catch (err) {
16
+ ctx.logger.error(err instanceof Error ? err.message : "Authentication failed.");
17
+ ctx.process.exitCode = 1;
18
+ return false;
19
+ }
20
+ configureClient(auth);
21
+ if (auth.type === "jwt") try {
22
+ const ws = await resolveWorkspace(workspaceIdFlag);
23
+ client.setConfig({
24
+ ...client.getConfig(),
25
+ workspaceId: ws.workspaceId
26
+ });
27
+ } catch (err) {
28
+ ctx.logger.error(err instanceof Error ? err.message : "Workspace resolution failed.");
29
+ ctx.process.exitCode = 1;
30
+ return false;
31
+ }
32
+ return true;
33
+ }
34
+
35
+ //#endregion
36
+ export { connect };
@@ -1,4 +1,4 @@
1
- import { loadWorkspace } from "./workspace-store-DDOxnut1.js";
1
+ import { loadWorkspace } from "./workspace-store-CnrxyYRB.js";
2
2
 
3
3
  //#region src/commands/workspace/currentHandler.ts
4
4
  function currentHandler() {
@@ -1,9 +1,9 @@
1
- import { getApiUrl, getTeamId } from "./env-CHeKHu5S.js";
2
- import { authGetProfileV1, configureClient, teamFindAllV1, testCaseImportCommitV1, testCaseImportPreviewV1 } from "./sdk-client-vQsoMCez.js";
3
- import "./workspace-store-DDOxnut1.js";
4
- import { resolveWorkspace } from "./resolve-workspace--4K7N4VU.js";
5
- import "./token-refresh-Cu5RpkLJ.js";
6
- import { resolveToken } from "./resolve-token-DbQsmn03.js";
1
+ import { authGetProfileV1, configureClient, teamFindAllV1, testCaseImportCommitV1, testCaseImportPreviewV1 } from "./sdk-client-B_urshDp.js";
2
+ import { getApiUrl, getTeamId } from "./env-CdwyPHGV.js";
3
+ import "./token-refresh-DeusLK8H.js";
4
+ import { resolveToken } from "./resolve-token-7xb7kG7h.js";
5
+ import "./workspace-store-CnrxyYRB.js";
6
+ import { resolveWorkspace } from "./resolve-workspace-Dsm8Xzut.js";
7
7
  import chalk from "chalk";
8
8
  import { createReadStream } from "node:fs";
9
9
  import { basename } from "node:path";
@@ -269,9 +269,9 @@ const MATCH_COLORS = {
269
269
  manual: chalk.green
270
270
  };
271
271
  /** Stream the file into a Blob — no readFileSync, no sync stall. */
272
- async function fileToBlob(path) {
272
+ async function fileToBlob(path$1) {
273
273
  const chunks = [];
274
- for await (const chunk of createReadStream(path)) chunks.push(chunk);
274
+ for await (const chunk of createReadStream(path$1)) chunks.push(chunk);
275
275
  return new Blob(chunks);
276
276
  }
277
277
  function printMapping(logger, mapping) {
@@ -1,8 +1,8 @@
1
- import "./env-CHeKHu5S.js";
2
- import { authGetSitesV1, configureClient } from "./sdk-client-vQsoMCez.js";
3
- import { loadWorkspace } from "./workspace-store-DDOxnut1.js";
4
- import "./token-refresh-Cu5RpkLJ.js";
5
- import { resolveToken } from "./resolve-token-DbQsmn03.js";
1
+ import { authGetSitesV1, configureClient } from "./sdk-client-B_urshDp.js";
2
+ import "./env-CdwyPHGV.js";
3
+ import "./token-refresh-DeusLK8H.js";
4
+ import { resolveToken } from "./resolve-token-7xb7kG7h.js";
5
+ import { loadWorkspace } from "./workspace-store-CnrxyYRB.js";
6
6
 
7
7
  //#region src/commands/workspace/listHandler.ts
8
8
  async function listHandler() {
@@ -1,7 +1,8 @@
1
- import { CLI_CLIENT_ID, getApiUrl, getAuthUrl, setSessionApiUrl, writeCredentials } from "./env-CHeKHu5S.js";
2
- import { configureClient } from "./sdk-client-vQsoMCez.js";
3
- import "./workspace-store-DDOxnut1.js";
4
- import { autoSelectWorkspace } from "./resolve-workspace--4K7N4VU.js";
1
+ import { configureClient } from "./sdk-client-B_urshDp.js";
2
+ import { CLI_CLIENT_ID, getApiUrl, getAuthUrl, setSessionApiUrl, writeCredentials } from "./env-CdwyPHGV.js";
3
+ import "./workspace-store-CnrxyYRB.js";
4
+ import { autoSelectWorkspace } from "./resolve-workspace-Dsm8Xzut.js";
5
+ import { sleep } from "./sleep-BpJ39Ypm.js";
5
6
  import chalk from "chalk";
6
7
  import { createHash, randomBytes } from "node:crypto";
7
8
  import { createServer } from "node:http";
@@ -153,12 +154,6 @@ function startCallbackServer(options = {}) {
153
154
  };
154
155
  }
155
156
 
156
- //#endregion
157
- //#region src/utils/sleep.ts
158
- function sleep(ms) {
159
- return new Promise((resolve) => setTimeout(resolve, ms));
160
- }
161
-
162
157
  //#endregion
163
158
  //#region src/auth/device-flow.ts
164
159
  /**
@@ -1,5 +1,5 @@
1
- import { deleteCredentials, getPatToken } from "./env-CHeKHu5S.js";
2
- import { clearWorkspace } from "./workspace-store-DDOxnut1.js";
1
+ import { deleteCredentials, getPatToken } from "./env-CdwyPHGV.js";
2
+ import { clearWorkspace } from "./workspace-store-CnrxyYRB.js";
3
3
 
4
4
  //#region src/commands/auth/logoutHandler.ts
5
5
  function logoutHandler() {
@@ -1,11 +1,13 @@
1
- import { getApiUrl, getAutomationSourceIdOverride, getSourceOverride, getTeamId } from "./env-CHeKHu5S.js";
2
- import { client, configureClient, uploadImport } from "./sdk-client-vQsoMCez.js";
3
- import "./workspace-store-DDOxnut1.js";
4
- import { resolveWorkspace } from "./resolve-workspace--4K7N4VU.js";
5
- import "./token-refresh-Cu5RpkLJ.js";
6
- import { resolveToken } from "./resolve-token-DbQsmn03.js";
7
- import { readFileSync, statSync } from "node:fs";
8
- import { basename } from "node:path";
1
+ import { client, configureClient, uploadImport } from "./sdk-client-B_urshDp.js";
2
+ import { getApiUrl, getAutomationSourceIdOverride, getSourceOverride, getTeamId } from "./env-CdwyPHGV.js";
3
+ import "./token-refresh-DeusLK8H.js";
4
+ import { resolveToken } from "./resolve-token-7xb7kG7h.js";
5
+ import "./workspace-store-CnrxyYRB.js";
6
+ import { resolveWorkspace } from "./resolve-workspace-Dsm8Xzut.js";
7
+ import "./sleep-BpJ39Ypm.js";
8
+ import { resolveArtifactPath, uploadArtifactFile } from "./artifact-upload-BnqeVY_b.js";
9
+ import { readFileSync, statSync, writeFileSync } from "node:fs";
10
+ import { basename, dirname, resolve } from "node:path";
9
11
  import { execSync } from "node:child_process";
10
12
  import ora from "ora";
11
13
 
@@ -356,6 +358,98 @@ function getCiMetadata() {
356
358
  return detectCiEnvironment() ?? void 0;
357
359
  }
358
360
 
361
+ //#endregion
362
+ //#region src/utils/push-artifacts.ts
363
+ const ARTIFACT_UPLOAD_CONCURRENCY = 4;
364
+ function formatArtifactSummary(s) {
365
+ return `Artifacts: uploaded ${s.uploaded}, deduplicated ${s.deduplicated}, skipped ${s.skipped} (too large), missing ${s.missing}, rejected ${s.rejected} (outside the working directory), failed ${s.failed}`;
366
+ }
367
+ async function uploadReportArtifacts(options) {
368
+ const summary = {
369
+ uploaded: 0,
370
+ deduplicated: 0,
371
+ skipped: 0,
372
+ missing: 0,
373
+ rejected: 0,
374
+ failed: 0
375
+ };
376
+ const tasks = [];
377
+ for (const result of options.results) for (const att of result.attachments) {
378
+ if (att.stored || !att.path) continue;
379
+ const ref = att.path;
380
+ const upload = async () => {
381
+ let resolved = resolveArtifactPath(ref, options.artifactsDir, options.root);
382
+ if (resolved && resolved.status === "missing") resolved = resolveArtifactPath(ref, options.reportDir, options.root) ?? resolved;
383
+ if (resolved === null) {
384
+ summary.rejected += 1;
385
+ options.onProblem?.(ref, "rejected");
386
+ return;
387
+ }
388
+ if (resolved.status === "missing") {
389
+ summary.missing += 1;
390
+ options.onProblem?.(ref, { status: "missing" });
391
+ return;
392
+ }
393
+ const outcome = await uploadArtifactFile({
394
+ target: {
395
+ relatedType: "automation_run_result",
396
+ relatedId: result.id
397
+ },
398
+ filePath: resolved.path,
399
+ validated: resolved,
400
+ name: att.name ?? void 0,
401
+ kind: att.kind,
402
+ attemptIndex: att.attempt_index
403
+ });
404
+ switch (outcome.status) {
405
+ case "uploaded":
406
+ summary.uploaded += 1;
407
+ return;
408
+ case "deduplicated":
409
+ summary.deduplicated += 1;
410
+ return;
411
+ case "skipped":
412
+ summary.skipped += 1;
413
+ break;
414
+ case "missing":
415
+ summary.missing += 1;
416
+ break;
417
+ case "rejected":
418
+ summary.rejected += 1;
419
+ break;
420
+ case "failed":
421
+ summary.failed += 1;
422
+ break;
423
+ }
424
+ options.onProblem?.(ref, outcome);
425
+ };
426
+ tasks.push(async () => {
427
+ try {
428
+ await upload();
429
+ } catch (err) {
430
+ summary.failed += 1;
431
+ options.onProblem?.(ref, {
432
+ status: "failed",
433
+ message: err instanceof Error ? err.message : String(err)
434
+ });
435
+ }
436
+ });
437
+ }
438
+ await runBounded(tasks, options.concurrency ?? ARTIFACT_UPLOAD_CONCURRENCY);
439
+ return summary;
440
+ }
441
+ /** Run tasks with at most `limit` in flight. Tasks must not throw. */
442
+ async function runBounded(tasks, limit) {
443
+ let next = 0;
444
+ const worker = async () => {
445
+ while (next < tasks.length) {
446
+ const task = tasks[next++];
447
+ if (task) await task();
448
+ }
449
+ };
450
+ await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
451
+ }
452
+
359
453
  //#endregion
360
454
  //#region src/commands/pushHandler.ts
361
455
  const MAX_FILE_SIZE = 10 * 1024 * 1024;
@@ -425,6 +519,9 @@ async function pushHandler(flags, file) {
425
519
  if (automationSourceId && sourceName && sourceOrigin !== "auto-detected") this.logger.warning(`--automation-source (${automationSourceOrigin}) takes precedence; ignoring source name "${sourceName}" (${sourceOrigin}).`);
426
520
  const uploadTeamId = automationSourceId && !flags["team-id"] ? void 0 : teamId;
427
521
  const ciMeta = getCiMetadata();
522
+ const outputResults = flags["output-results"];
523
+ const artifactsDir = flags.artifacts;
524
+ const includeResults = Boolean(outputResults || artifactsDir);
428
525
  if (flags.verbose) {
429
526
  this.logger.debug(`Team: ${uploadTeamId ?? (automationSourceId ? "(automation source's team)" : "(server default)")}`);
430
527
  this.logger.debug(`File: ${file} (${formatBytes(fileStat.size)})`);
@@ -453,7 +550,8 @@ async function pushHandler(flags, file) {
453
550
  format: flags.format,
454
551
  runName: flags["run-name"],
455
552
  ...automationSourceId ? { automationSourceId } : { automationSource: sourceName },
456
- importMetadata: ciMeta
553
+ importMetadata: ciMeta,
554
+ includeResults
457
555
  });
458
556
  spinner.stop();
459
557
  if (result?.status === "failed") {
@@ -489,6 +587,11 @@ async function pushHandler(flags, file) {
489
587
  const ciLabel = ciMeta.ci_build_id ? `${prettyProvider} #${ciMeta.ci_build_id}` : prettyProvider;
490
588
  this.process.stdout.write(` CI: ${ciLabel}\n`);
491
589
  }
590
+ if (includeResults) await handleResultArtifacts(this, result, {
591
+ outputResults,
592
+ artifactsDir,
593
+ reportFile: file
594
+ });
492
595
  if (flags.verbose && result.result?.stats) {
493
596
  const s = result.result.stats;
494
597
  this.process.stdout.write("\n Details:\n");
@@ -506,6 +609,36 @@ async function pushHandler(flags, file) {
506
609
  this.process.exitCode = 1;
507
610
  }
508
611
  }
612
+ /**
613
+ * ENG-6164: write the results file and upload the report's artifacts.
614
+ * fail_fallback(warn) throughout — nothing here changes the push exit code
615
+ * (DD5, ENG-591): every problem becomes a warning or a summary count.
616
+ */
617
+ async function handleResultArtifacts(ctx, result, options) {
618
+ const results = result.result?.results ?? [];
619
+ if (options.outputResults) try {
620
+ writeFileSync(options.outputResults, `${JSON.stringify(results, null, 2)}\n`);
621
+ ctx.process.stdout.write(` Results: ${results.length} written to ${options.outputResults}\n`);
622
+ } catch (err) {
623
+ ctx.logger.warning(`Could not write ${options.outputResults}: ${err instanceof Error ? err.message : String(err)}`);
624
+ }
625
+ if (options.artifactsDir) try {
626
+ const summary = await uploadReportArtifacts({
627
+ results,
628
+ artifactsDir: resolve(options.artifactsDir),
629
+ reportDir: dirname(resolve(options.reportFile)),
630
+ onProblem: (path$1, outcome) => {
631
+ const why = outcome === "rejected" ? "outside the working directory" : outcome.status === "skipped" ? "larger than 50 MiB" : "message" in outcome ? outcome.message : outcome.status;
632
+ ctx.logger.debug(`artifact ${path$1}: ${why}`);
633
+ }
634
+ });
635
+ const line = formatArtifactSummary(summary);
636
+ if (summary.skipped + summary.missing + summary.rejected + summary.failed > 0) ctx.logger.warning(line);
637
+ else ctx.process.stdout.write(` ${line}\n`);
638
+ } catch (err) {
639
+ ctx.logger.warning(`Artifact upload stopped: ${err instanceof Error ? err.message : String(err)}`);
640
+ }
641
+ }
509
642
 
510
643
  //#endregion
511
644
  export { pushHandler };