@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.
- package/CHANGELOG.md +21 -0
- package/index.d.mts +3 -1
- package/index.d.mts.map +1 -1
- package/index.d.ts +3 -1
- package/index.d.ts.map +1 -1
- package/index.js +17 -1
- package/index.js.map +1 -1
- package/index.mjs +2 -0
- package/index.mjs.map +1 -1
- package/internal/direct-instance-errors.d.mts +9 -0
- package/internal/direct-instance-errors.d.mts.map +1 -1
- package/internal/direct-instance-errors.d.ts +9 -0
- package/internal/direct-instance-errors.d.ts.map +1 -1
- package/internal/direct-instance-errors.js +6 -1
- package/internal/direct-instance-errors.js.map +1 -1
- package/internal/direct-instance-errors.mjs +5 -1
- package/internal/direct-instance-errors.mjs.map +1 -1
- package/internal/proxy-transport.d.mts +9 -0
- package/internal/proxy-transport.d.mts.map +1 -1
- package/internal/proxy-transport.d.ts +9 -0
- package/internal/proxy-transport.d.ts.map +1 -1
- package/internal/proxy-transport.js +19 -0
- package/internal/proxy-transport.js.map +1 -1
- package/internal/proxy-transport.mjs +19 -0
- package/internal/proxy-transport.mjs.map +1 -1
- package/internal/sse-fetch.d.mts +11 -0
- package/internal/sse-fetch.d.mts.map +1 -0
- package/internal/sse-fetch.d.ts +11 -0
- package/internal/sse-fetch.d.ts.map +1 -0
- package/internal/sse-fetch.js +23 -0
- package/internal/sse-fetch.js.map +1 -0
- package/internal/sse-fetch.mjs +20 -0
- package/internal/sse-fetch.mjs.map +1 -0
- package/package.json +21 -1
- package/rbe-session.d.mts +35 -0
- package/rbe-session.d.mts.map +1 -0
- package/rbe-session.d.ts +35 -0
- package/rbe-session.d.ts.map +1 -0
- package/rbe-session.js +72 -0
- package/rbe-session.js.map +1 -0
- package/rbe-session.mjs +65 -0
- package/rbe-session.mjs.map +1 -0
- package/rbe-workspace.d.mts +163 -0
- package/rbe-workspace.d.mts.map +1 -0
- package/rbe-workspace.d.ts +163 -0
- package/rbe-workspace.d.ts.map +1 -0
- package/rbe-workspace.js +394 -0
- package/rbe-workspace.js.map +1 -0
- package/rbe-workspace.mjs +382 -0
- package/rbe-workspace.mjs.map +1 -0
- package/resources/xcode-instances-helpers.d.mts +61 -0
- package/resources/xcode-instances-helpers.d.mts.map +1 -1
- package/resources/xcode-instances-helpers.d.ts +61 -0
- package/resources/xcode-instances-helpers.d.ts.map +1 -1
- package/resources/xcode-instances-helpers.js +126 -7
- package/resources/xcode-instances-helpers.js.map +1 -1
- package/resources/xcode-instances-helpers.mjs +127 -8
- package/resources/xcode-instances-helpers.mjs.map +1 -1
- package/src/index.ts +25 -0
- package/src/internal/direct-instance-errors.ts +11 -1
- package/src/internal/proxy-transport.ts +22 -0
- package/src/internal/sse-fetch.ts +20 -0
- package/src/rbe-session.ts +92 -0
- package/src/rbe-workspace.ts +407 -0
- package/src/resources/xcode-instances-helpers.ts +223 -13
- package/src/version.ts +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- 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
|
|
534
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
1
|
+
export declare const VERSION = "0.36.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/version.js
CHANGED
package/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.36.0'; // x-release-please-version
|
|
2
2
|
//# sourceMappingURL=version.mjs.map
|