@limrun/api 0.34.0 → 0.36.0

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 (70) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/index.d.mts +3 -1
  3. package/index.d.mts.map +1 -1
  4. package/index.d.ts +3 -1
  5. package/index.d.ts.map +1 -1
  6. package/index.js +17 -1
  7. package/index.js.map +1 -1
  8. package/index.mjs +2 -0
  9. package/index.mjs.map +1 -1
  10. package/internal/direct-instance-errors.d.mts +9 -0
  11. package/internal/direct-instance-errors.d.mts.map +1 -1
  12. package/internal/direct-instance-errors.d.ts +9 -0
  13. package/internal/direct-instance-errors.d.ts.map +1 -1
  14. package/internal/direct-instance-errors.js +6 -1
  15. package/internal/direct-instance-errors.js.map +1 -1
  16. package/internal/direct-instance-errors.mjs +5 -1
  17. package/internal/direct-instance-errors.mjs.map +1 -1
  18. package/internal/proxy-transport.d.mts +9 -0
  19. package/internal/proxy-transport.d.mts.map +1 -1
  20. package/internal/proxy-transport.d.ts +9 -0
  21. package/internal/proxy-transport.d.ts.map +1 -1
  22. package/internal/proxy-transport.js +19 -0
  23. package/internal/proxy-transport.js.map +1 -1
  24. package/internal/proxy-transport.mjs +19 -0
  25. package/internal/proxy-transport.mjs.map +1 -1
  26. package/internal/sse-fetch.d.mts +11 -0
  27. package/internal/sse-fetch.d.mts.map +1 -0
  28. package/internal/sse-fetch.d.ts +11 -0
  29. package/internal/sse-fetch.d.ts.map +1 -0
  30. package/internal/sse-fetch.js +23 -0
  31. package/internal/sse-fetch.js.map +1 -0
  32. package/internal/sse-fetch.mjs +20 -0
  33. package/internal/sse-fetch.mjs.map +1 -0
  34. package/package.json +21 -1
  35. package/rbe-session.d.mts +35 -0
  36. package/rbe-session.d.mts.map +1 -0
  37. package/rbe-session.d.ts +35 -0
  38. package/rbe-session.d.ts.map +1 -0
  39. package/rbe-session.js +72 -0
  40. package/rbe-session.js.map +1 -0
  41. package/rbe-session.mjs +65 -0
  42. package/rbe-session.mjs.map +1 -0
  43. package/rbe-workspace.d.mts +163 -0
  44. package/rbe-workspace.d.mts.map +1 -0
  45. package/rbe-workspace.d.ts +163 -0
  46. package/rbe-workspace.d.ts.map +1 -0
  47. package/rbe-workspace.js +394 -0
  48. package/rbe-workspace.js.map +1 -0
  49. package/rbe-workspace.mjs +382 -0
  50. package/rbe-workspace.mjs.map +1 -0
  51. package/resources/xcode-instances-helpers.d.mts +61 -0
  52. package/resources/xcode-instances-helpers.d.mts.map +1 -1
  53. package/resources/xcode-instances-helpers.d.ts +61 -0
  54. package/resources/xcode-instances-helpers.d.ts.map +1 -1
  55. package/resources/xcode-instances-helpers.js +126 -7
  56. package/resources/xcode-instances-helpers.js.map +1 -1
  57. package/resources/xcode-instances-helpers.mjs +127 -8
  58. package/resources/xcode-instances-helpers.mjs.map +1 -1
  59. package/src/index.ts +25 -0
  60. package/src/internal/direct-instance-errors.ts +11 -1
  61. package/src/internal/proxy-transport.ts +22 -0
  62. package/src/internal/sse-fetch.ts +20 -0
  63. package/src/rbe-session.ts +92 -0
  64. package/src/rbe-workspace.ts +407 -0
  65. package/src/resources/xcode-instances-helpers.ts +223 -13
  66. package/src/version.ts +1 -1
  67. package/version.d.mts +1 -1
  68. package/version.d.ts +1 -1
  69. package/version.js +1 -1
  70. package/version.mjs +1 -1
@@ -12,10 +12,13 @@ import {
12
12
  } from '../folder-sync';
13
13
  import { createIgnoreFn } from '../folder-sync-ignore';
14
14
  import { nodeProxyTransport } from '../internal/proxy-transport';
15
- import { directInstanceHttpError } from '../internal/direct-instance-errors';
15
+ import { directInstanceHttpError, isDirectInstanceHttpError } from '../internal/direct-instance-errors';
16
16
  import { LimrunError } from '../core/error';
17
17
  import { validateBuildSettings } from '../build-settings';
18
18
  import { startTcpTunnel, type Tunnel } from '../tunnel';
19
+ import { isTransientError, retryTransient } from '../rbe-session';
20
+ import { sseFetch } from '../internal/sse-fetch';
21
+ import { createEventSource } from 'eventsource-client';
19
22
 
20
23
  export type { Tunnel } from '../tunnel';
21
24
 
@@ -192,6 +195,46 @@ export type RbeInstallResult = {
192
195
  installDurationMs?: number;
193
196
  };
194
197
 
198
+ export type RbeUploadOptions =
199
+ | {
200
+ /** Upload as a named asset: the SDK mints the presigned upload URL via assets.getOrCreate. */
201
+ assetName: string;
202
+ /** Optional asset TTL as a Go duration (e.g. "24h", "30m"; "1d" is invalid), forwarded to assets.getOrCreate. */
203
+ ttl?: string;
204
+ signedUploadUrl?: never;
205
+ }
206
+ | {
207
+ /** Upload to a caller-minted presigned Limrun asset storage URL. */
208
+ signedUploadUrl: string;
209
+ assetName?: never;
210
+ ttl?: never;
211
+ };
212
+
213
+ export type RbeUploadResult = {
214
+ /** The .app bundle name of the uploaded build; it is the root directory of the uploaded tar.gz. */
215
+ appName: string;
216
+ /** CFBundleIdentifier of the uploaded app, when known. */
217
+ bundleId?: string;
218
+ /** Presigned download URL of the asset; present when uploading by assetName. */
219
+ signedDownloadUrl?: string;
220
+ };
221
+
222
+ /** An in-flight Bazel invocation on the instance's RBE stack. */
223
+ export type RbeActiveBuild = {
224
+ invocationId: string;
225
+ /** RUNNING while in flight; terminal statuses appear only on the build-end event. */
226
+ status: 'RUNNING' | (string & {});
227
+ /** The bazel target pattern(s) of the invocation, when known. */
228
+ pattern?: string[];
229
+ };
230
+
231
+ /** The terminal summary of a Bazel invocation, from the build stream's end event. */
232
+ export type RbeBuildEnd = {
233
+ invocationId: string;
234
+ status: 'SUCCEEDED' | 'FAILED' | 'CANCELLED' | 'INCOMPLETE' | (string & {});
235
+ error?: string;
236
+ };
237
+
195
238
  export type XcodeClient = {
196
239
  /**
197
240
  * Sync source code to the xcode instance. In watch mode, keeps syncing on changes.
@@ -243,6 +286,34 @@ export type XcodeClient = {
243
286
  */
244
287
  startRbeTunnel: (opts?: RbeTunnelOptions) => Promise<Tunnel>;
245
288
 
289
+ /**
290
+ * Upload the latest successful RBE build's app as an asset: the daemon
291
+ * packages the build's .app as a tar.gz (the same artifact format
292
+ * xcodebuild uploads produce) and pushes it to asset storage. Call after
293
+ * `bazel build --config=limrun` succeeds. The build is recorded on the
294
+ * instance asynchronously moments after bazel reports success, so the
295
+ * brief no-build window right after bazel exits is retried automatically.
296
+ */
297
+ uploadLatestRbeBuild: (opts: RbeUploadOptions) => Promise<RbeUploadResult>;
298
+
299
+ /**
300
+ * List the Bazel invocations currently building on the RBE stack. Poll this
301
+ * to discover new builds (e.g. to auto-upload on build end).
302
+ */
303
+ getActiveRbeBuilds: () => Promise<RbeActiveBuild[]>;
304
+
305
+ /**
306
+ * Wait for a Bazel invocation to finish and return its terminal summary.
307
+ * Subscribes to the invocation's build event stream (which replays from the
308
+ * start, so subscribing any time before the build ends is safe) and resolves
309
+ * on the terminal event. Rejects when the stream drops or cannot be reached,
310
+ * typically because the build already ended and its live stream was removed,
311
+ * and on abort via the optional signal. There is no internal retry; the
312
+ * caller owns that policy (re-subscribe while the invocation is still listed
313
+ * active, else treat the outcome as unknown).
314
+ */
315
+ waitForRbeBuildEnd: (invocationId: string, opts?: { signal?: AbortSignal }) => Promise<RbeBuildEnd>;
316
+
246
317
  /**
247
318
  * Create a new iOS simulator instance and attach it to this xcode instance.
248
319
  * Deletes the simulator if attach fails so it is never leaked. Returns the new
@@ -368,6 +439,27 @@ async function readJsonResponse<T>(res: Response, operation: string): Promise<T>
368
439
  return JSON.parse(text) as T;
369
440
  }
370
441
 
442
+ /**
443
+ * Mints presigned upload/download URLs for a named asset via assets.getOrCreate,
444
+ * wrapping failures with the asset name (and the original error as cause).
445
+ * Shared by the xcodebuild `--upload` path and `uploadLatestRbeBuild`.
446
+ */
447
+ function mintAssetUploadUrls(
448
+ assets: { getOrCreate: (body: { name: string; ttl?: string }) => Promise<AssetUploadUrls> },
449
+ name: string,
450
+ ttl?: string,
451
+ ): Promise<AssetUploadUrls> {
452
+ return assets.getOrCreate({ name, ...(ttl && { ttl }) }).catch((err) => {
453
+ const message = `Failed to create upload URL for asset '${name}': ${
454
+ err instanceof Error ? err.message : err
455
+ }`;
456
+ // @ts-ignore - not all envs have native support for cause yet
457
+ throw new Error(message, { cause: err });
458
+ });
459
+ }
460
+
461
+ type AssetUploadUrls = { signedUploadUrl: string; signedDownloadUrl: string };
462
+
371
463
  export class XcodeInstances extends GeneratedXcodeInstances {
372
464
  async createClient(params: XcodeCreateClientParams): Promise<XcodeClient> {
373
465
  let apiUrl: string;
@@ -530,21 +622,13 @@ export class XcodeInstances extends GeneratedXcodeInstances {
530
622
  };
531
623
 
532
624
  if (options?.upload && 'assetName' in options.upload) {
533
- const uploadName = options.upload.assetName;
534
- const requestPromise = client.assets
535
- .getOrCreate({ name: uploadName })
536
- .then((asset) => {
625
+ const requestPromise = mintAssetUploadUrls(client.assets, options.upload.assetName).then(
626
+ (asset) => {
537
627
  request.signedUploadUrl = asset.signedUploadUrl;
538
628
  request.additionalMetadata = { signedDownloadUrl: asset.signedDownloadUrl };
539
629
  return request;
540
- })
541
- .catch((err) => {
542
- throw new Error(
543
- `Failed to create upload URL for artifact '${uploadName}': ${
544
- err instanceof Error ? err.message : err
545
- }`,
546
- );
547
- });
630
+ },
631
+ );
548
632
  return exec(requestPromise, { apiUrl, token, log });
549
633
  }
550
634
 
@@ -613,6 +697,132 @@ export class XcodeInstances extends GeneratedXcodeInstances {
613
697
  },
614
698
  );
615
699
  },
700
+
701
+ async uploadLatestRbeBuild(opts: RbeUploadOptions): Promise<RbeUploadResult> {
702
+ let signedUploadUrl: string;
703
+ let signedDownloadUrl: string | undefined;
704
+ if (opts.assetName !== undefined) {
705
+ if (!opts.assetName) {
706
+ throw new Error('assetName must not be empty');
707
+ }
708
+ const asset = await mintAssetUploadUrls(client.assets, opts.assetName, opts.ttl);
709
+ signedUploadUrl = asset.signedUploadUrl;
710
+ signedDownloadUrl = asset.signedDownloadUrl;
711
+ } else {
712
+ signedUploadUrl = opts.signedUploadUrl;
713
+ }
714
+
715
+ const post = async (): Promise<{ appName: string; bundleId?: string }> => {
716
+ // The daemon uploads to asset storage before responding, so this
717
+ // request can go minutes without response bytes.
718
+ const res = await nodeProxyTransport.fetchLongRequest(`${apiUrl}/rbe/upload`, {
719
+ method: 'POST',
720
+ headers: {
721
+ 'Content-Type': 'application/json',
722
+ Authorization: `Bearer ${token}`,
723
+ },
724
+ body: JSON.stringify({ signedUploadUrl }),
725
+ });
726
+ if (res.status === 404) {
727
+ // A vanished instance and a limbuild that predates the endpoint
728
+ // 404 indistinguishably (identical bodies), and neither is the
729
+ // RbeUnsupportedError case (the daemon may well support /rbe),
730
+ // hence a plain two-cause error.
731
+ await res.text().catch(() => undefined);
732
+ throw new Error(
733
+ 'POST /rbe/upload returned 404: the instance may no longer exist, or its limbuild ' +
734
+ 'predates RBE upload support. Recreate the instance and retry.',
735
+ );
736
+ }
737
+ return readJsonResponse<{ appName: string; bundleId?: string }>(res, 'POST /rbe/upload');
738
+ };
739
+
740
+ // Build recording is asynchronous on the daemon (it lands moments
741
+ // after bazel reports success), so the no-build 400 fired right at
742
+ // build end is as transient as a gateway blip; retry both classes.
743
+ const retryOn = (err: unknown) =>
744
+ isTransientError(err) ||
745
+ (isDirectInstanceHttpError(err, 400) && /no successful RBE app build/i.test(err.body));
746
+ const result = await retryTransient(post, { retryOn });
747
+ return { ...result, ...(signedDownloadUrl && { signedDownloadUrl }) };
748
+ },
749
+
750
+ async getActiveRbeBuilds(): Promise<RbeActiveBuild[]> {
751
+ const res = await nodeProxyTransport.fetch(`${apiUrl}/rbe/builds/active`, {
752
+ method: 'GET',
753
+ headers: {
754
+ Authorization: `Bearer ${token}`,
755
+ },
756
+ });
757
+ // readRbeResponse: a 404 here means a limbuild that predates this
758
+ // route, not a vanished instance (same trap as the other /rbe routes).
759
+ return readRbeResponse<RbeActiveBuild[]>(res, 'GET /rbe/builds/active');
760
+ },
761
+
762
+ waitForRbeBuildEnd(invocationId: string, opts?: { signal?: AbortSignal }): Promise<RbeBuildEnd> {
763
+ return new Promise<RbeBuildEnd>((resolve, reject) => {
764
+ if (opts?.signal?.aborted) {
765
+ reject(new Error(`waiting for build ${invocationId} was aborted`));
766
+ return;
767
+ }
768
+ // Both settle paths close the source: eventsource-client
769
+ // auto-reconnects otherwise, which would retry-loop against the
770
+ // stream once the daemon removes it.
771
+ let settled = false;
772
+ const cleanup = () => {
773
+ eventSource.close();
774
+ opts?.signal?.removeEventListener('abort', onAbort);
775
+ };
776
+ const settleResolve = (end: RbeBuildEnd) => {
777
+ if (settled) return;
778
+ settled = true;
779
+ cleanup();
780
+ resolve(end);
781
+ };
782
+ const settleReject = (err: Error) => {
783
+ if (settled) return;
784
+ settled = true;
785
+ cleanup();
786
+ reject(err);
787
+ };
788
+ const onAbort = () => settleReject(new Error(`waiting for build ${invocationId} was aborted`));
789
+ const eventSource = createEventSource({
790
+ url: `${apiUrl}/exec/${invocationId}/events`,
791
+ fetch: sseFetch(nodeProxyTransport.fetch, (err) =>
792
+ settleReject(
793
+ new Error(
794
+ `build event stream for ${invocationId} is unreachable: ${
795
+ err instanceof Error ? err.message : err
796
+ }`,
797
+ ),
798
+ ),
799
+ ),
800
+ headers: { Authorization: `Bearer ${token}` },
801
+ onMessage: (message) => {
802
+ if (message.event !== 'end') {
803
+ return; // meta and log frames
804
+ }
805
+ let end: RbeBuildEnd;
806
+ try {
807
+ end = JSON.parse(message.data) as RbeBuildEnd;
808
+ } catch (err) {
809
+ settleReject(new Error(`invalid build end event for ${invocationId}: ${err}`));
810
+ return;
811
+ }
812
+ settleResolve(end);
813
+ },
814
+ onDisconnect: () => {
815
+ settleReject(
816
+ new Error(
817
+ `build event stream for ${invocationId} ended without a terminal event ` +
818
+ '(the build may have finished and its live stream been removed)',
819
+ ),
820
+ );
821
+ },
822
+ });
823
+ opts?.signal?.addEventListener('abort', onAbort, { once: true });
824
+ });
825
+ },
616
826
  };
617
827
  }
618
828
  }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.34.0'; // x-release-please-version
1
+ export const VERSION = '0.36.0'; // x-release-please-version
package/version.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.34.0";
1
+ export declare const VERSION = "0.36.0";
2
2
  //# sourceMappingURL=version.d.mts.map
package/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.34.0";
1
+ export declare const VERSION = "0.36.0";
2
2
  //# sourceMappingURL=version.d.ts.map
package/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
- exports.VERSION = '0.34.0'; // x-release-please-version
4
+ exports.VERSION = '0.36.0'; // x-release-please-version
5
5
  //# sourceMappingURL=version.js.map
package/version.mjs CHANGED
@@ -1,2 +1,2 @@
1
- export const VERSION = '0.34.0'; // x-release-please-version
1
+ export const VERSION = '0.36.0'; // x-release-please-version
2
2
  //# sourceMappingURL=version.mjs.map