@stim-cli/cache-s3 1.21.0 → 1.23.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/README.md CHANGED
@@ -33,6 +33,12 @@ SDK's standard credential chain. For a named profile in `~/.aws/credentials`,
33
33
  set `cache.options.profile` to `stim-r2`, or run commands with
34
34
  `AWS_PROFILE=stim-r2`, for example `AWS_PROFILE=stim-r2 stim ios`. An explicit
35
35
  cache profile overrides the ambient AWS profile. Credentials never belong in `.stim.json`.
36
+ Programmatic Stim clients use their captured environment, including profile files
37
+ and `credential_process`. The optional provider resolves credentials and environment-backed request settings
38
+ in a cancellable helper process, keeping the calling process environment unchanged.
39
+ Captured endpoint settings are pinned for the request, with no fallback to the
40
+ calling process's endpoint. Credentials and settings return over private IPC and
41
+ are reused until the credentials need refreshing.
36
42
 
37
43
  Create a private R2 bucket and an R2 API token with Object Read & Write access
38
44
  scoped to that bucket. Copy the S3 endpoint shown by Cloudflare; jurisdiction
@@ -47,6 +53,11 @@ existing build entries unless overwrite was requested. Use matching native
47
53
  toolchains for the initial test; the provider does not add toolchain information
48
54
  to Stim's existing build key.
49
55
 
56
+ Archive extraction runs in an owned helper. Cancellation waits for that process
57
+ to exit before removing download staging, so extraction cannot keep writing
58
+ after the operation settles. A cleanup failure reports the retained directory
59
+ and filesystem cause; bounded plans refuse instead of counting it as a miss.
60
+
50
61
  The file transport is also exported as `createS3FileCache()` from
51
62
  `@stim-cli/cache-s3`, with the `FileCacheCapability` interface exported from
52
63
  `@stim-cli/cache`. A cache adapter supplies a compatible key and a completed
@@ -61,5 +72,7 @@ Objects use `<prefix>/builds/<platform>/<key>` and
61
72
  `<prefix>/native-android/v1/<key>`. A prefix defaults to `stim/v1`. Keep
62
73
  incompatible snapshot formats in separate key namespaces. Retention belongs to
63
74
  the bucket lifecycle configuration; local GC never removes shared objects.
64
- Uploads use a single S3 PUT, so entries larger than S3's 5 GiB limit are not
65
- supported by this initial provider.
75
+ Uploads use a single-object S3 PUT, so entries larger than S3's 5 GiB limit are
76
+ not supported. A connection reset retries the completed file with a fresh stream,
77
+ up to three PUT attempts with short, cancellable delays. Conditional writes still
78
+ preserve an existing entry if the first upload committed but its reply was lost.
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,46 @@
1
+ import { S3Client } from "@aws-sdk/client-s3";
2
+ //#region credentials-worker.ts
3
+ process.once("message", async (input) => {
4
+ const client = new S3Client({
5
+ ...input,
6
+ logger: {
7
+ debug() {},
8
+ info() {},
9
+ warn() {},
10
+ error() {}
11
+ }
12
+ });
13
+ try {
14
+ const config = client.config;
15
+ const credentials = await config.credentials();
16
+ const endpoint = input.endpoint ?? await config.serviceConfiguredEndpoint?.();
17
+ const message = {
18
+ credentials: {
19
+ ...credentials,
20
+ expiration: credentials.expiration?.toISOString()
21
+ },
22
+ requestConfig: {
23
+ ...endpoint === void 0 ? {} : { endpoint },
24
+ disableClockSkewCorrection: await config.disableClockSkewCorrection(),
25
+ useFipsEndpoint: await config.useFipsEndpoint(),
26
+ useDualstackEndpoint: await config.useDualstackEndpoint(),
27
+ useArnRegion: typeof config.useArnRegion === "function" ? await config.useArnRegion() : config.useArnRegion,
28
+ disableS3ExpressSessionAuth: typeof config.disableS3ExpressSessionAuth === "function" ? await config.disableS3ExpressSessionAuth() : config.disableS3ExpressSessionAuth,
29
+ authSchemePreference: typeof config.authSchemePreference === "function" ? await config.authSchemePreference() : config.authSchemePreference,
30
+ sigv4aSigningRegionSet: typeof config.sigv4aSigningRegionSet === "function" ? await config.sigv4aSigningRegionSet() : config.sigv4aSigningRegionSet,
31
+ maxAttempts: typeof config.maxAttempts === "function" ? await config.maxAttempts() : config.maxAttempts,
32
+ retryMode: typeof config.retryMode === "function" ? await config.retryMode() : config.retryMode,
33
+ defaultsMode: typeof config.defaultsMode === "function" ? await config.defaultsMode() : config.defaultsMode,
34
+ userAgentAppId: typeof config.userAgentAppId === "function" ? await config.userAgentAppId() : config.userAgentAppId
35
+ }
36
+ };
37
+ process.send?.({ value: message }, () => process.exit(0));
38
+ } catch {
39
+ process.exitCode = 1;
40
+ process.disconnect?.();
41
+ } finally {
42
+ client.destroy();
43
+ }
44
+ });
45
+ //#endregion
46
+ export {};
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,36 @@
1
+ import { createReadStream } from "node:fs";
2
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { pipeline } from "node:stream/promises";
4
+ import { extract } from "tar";
5
+ //#region extract-worker.ts
6
+ process.once("message", async ({ archive, staging }) => {
7
+ try {
8
+ let invalidLink = false;
9
+ await pipeline(createReadStream(archive), extract({
10
+ cwd: staging,
11
+ strict: true,
12
+ filter(path, entry) {
13
+ if (!("type" in entry)) return true;
14
+ if (entry.type !== "SymbolicLink" && entry.type !== "Link") return true;
15
+ const link = entry.linkpath ?? "";
16
+ const target = resolve(staging, entry.type === "SymbolicLink" ? dirname(path) : "", link);
17
+ const root = relative(staging, resolve(staging, path)).split(sep)[0];
18
+ const outside = relative(resolve(staging, root ?? ""), target);
19
+ if (isAbsolute(link) || outside === ".." || outside.startsWith(`..${sep}`)) {
20
+ invalidLink = true;
21
+ return false;
22
+ }
23
+ return true;
24
+ }
25
+ }));
26
+ if (invalidLink) throw new Error("The S3 build entry contains a link outside the artifact.");
27
+ process.send?.({ value: true }, () => process.exit(0));
28
+ } catch (error) {
29
+ process.send?.({ error: {
30
+ message: error instanceof Error ? error.message : String(error),
31
+ ...error instanceof Error && "code" in error && typeof error.code === "string" ? { code: error.code } : {}
32
+ } }, () => process.exit(1));
33
+ }
34
+ });
35
+ //#endregion
36
+ export {};
package/dist/index.d.mts CHANGED
@@ -8,12 +8,15 @@ interface S3FileCacheOptions {
8
8
  region?: string | undefined;
9
9
  profile?: string | undefined;
10
10
  forcePathStyle?: boolean | undefined;
11
+ env?: NodeJS.ProcessEnv | undefined;
12
+ projectRoot?: string | undefined;
11
13
  }
12
14
  /** Stores opaque files; each cache adapter owns its key and serialization. Credentials use the AWS SDK chain. */
13
15
  declare function createS3FileCache(options: S3FileCacheOptions): FileCacheCapability & Required<Pick<FileCacheCapability, "stat">>;
14
- declare function createCacheProvider({ options }: {
16
+ declare function createCacheProvider({ options, projectRoot, env }: {
15
17
  projectRoot: string;
16
18
  options: Record<string, unknown>;
19
+ env?: NodeJS.ProcessEnv;
17
20
  }): CacheProvider;
18
21
  //#endregion
19
22
  export { S3FileCacheOptions, apiVersion, createCacheProvider, createS3FileCache };
package/dist/index.mjs CHANGED
@@ -3,12 +3,118 @@ import { createHash, randomUUID } from "node:crypto";
3
3
  import { createReadStream, createWriteStream } from "node:fs";
4
4
  import { lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
5
5
  import { tmpdir } from "node:os";
6
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
+ import { basename, dirname, join } from "node:path";
7
7
  import { Readable, Transform } from "node:stream";
8
8
  import { pipeline } from "node:stream/promises";
9
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
9
10
  import { deserialize, serialize } from "node:v8";
10
- import { create, extract } from "tar";
11
- import { CACHE_PROVIDER_API_VERSION } from "@stim-cli/cache";
11
+ import { create } from "tar";
12
+ import { CACHE_PROVIDER_API_VERSION, buildResolveTimeoutMs } from "@stim-cli/cache";
13
+ import { spawn } from "node:child_process";
14
+ import { fileURLToPath } from "node:url";
15
+ //#region worker.ts
16
+ function scopedWorker(worker, input, { env, cwd, signal, timeoutMs, name }) {
17
+ signal.throwIfAborted();
18
+ return new Promise((resolve, reject) => {
19
+ const child = spawn(process.execPath, [fileURLToPath(worker)], {
20
+ cwd,
21
+ env,
22
+ detached: process.platform !== "win32",
23
+ stdio: [
24
+ "ignore",
25
+ "ignore",
26
+ "ignore",
27
+ "ipc"
28
+ ]
29
+ });
30
+ let reply;
31
+ let failure;
32
+ let closed = false;
33
+ let stopping;
34
+ const stop = () => {
35
+ if (closed || !child.pid) return;
36
+ if (process.platform === "win32") stopping ??= new Promise((done) => {
37
+ const killer = spawn(join(process.env.SystemRoot || "C:\\Windows", "System32", "taskkill.exe"), [
38
+ "/PID",
39
+ String(child.pid),
40
+ "/T",
41
+ "/F"
42
+ ], {
43
+ stdio: "ignore",
44
+ windowsHide: true
45
+ });
46
+ killer.once("error", (error) => {
47
+ failure ??= error;
48
+ });
49
+ killer.once("close", (code) => {
50
+ if (code !== 0) failure ??= /* @__PURE__ */ new Error(`${name} process cleanup failed.`);
51
+ done();
52
+ });
53
+ });
54
+ else try {
55
+ process.kill(-child.pid, "SIGKILL");
56
+ } catch (error) {
57
+ if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) failure ??= error;
58
+ }
59
+ };
60
+ const timer = setTimeout(() => {
61
+ failure = /* @__PURE__ */ new Error(`${name} did not finish within ${timeoutMs / 1e3} seconds.`);
62
+ stop();
63
+ }, timeoutMs);
64
+ signal.addEventListener("abort", stop, { once: true });
65
+ if (signal.aborted) stop();
66
+ child.once("spawn", () => {
67
+ if (signal.aborted) stop();
68
+ else child.send(input, (error) => {
69
+ if (error) {
70
+ failure = error;
71
+ stop();
72
+ }
73
+ });
74
+ });
75
+ child.once("message", (message) => {
76
+ reply = message;
77
+ });
78
+ child.once("error", (error) => {
79
+ failure = error;
80
+ });
81
+ child.once("close", async (code) => {
82
+ closed = true;
83
+ clearTimeout(timer);
84
+ signal.removeEventListener("abort", stop);
85
+ await stopping;
86
+ if (signal.aborted) reject(signal.reason);
87
+ else if (failure) reject(failure);
88
+ else if (reply?.error) reject(Object.assign(new Error(reply.error.message), { code: reply.error.code }));
89
+ else if (code !== 0 || reply?.value === void 0) reject(/* @__PURE__ */ new Error(`Could not complete ${name}.`));
90
+ else resolve(reply.value);
91
+ });
92
+ });
93
+ }
94
+ //#endregion
95
+ //#region credentials.ts
96
+ function scopedClientContext(env, cwd, options, signal) {
97
+ return scopedWorker(new URL(import.meta.url.endsWith(".ts") ? "./credentials-worker.ts" : "./credentials-worker.mjs", import.meta.url), options, {
98
+ env: {
99
+ ...env,
100
+ ...options.profile ? { AWS_PROFILE: options.profile } : {}
101
+ },
102
+ cwd,
103
+ signal,
104
+ timeoutMs: 1e4,
105
+ name: "S3 credential resolution"
106
+ }).then((context) => {
107
+ const { expiration, ...credentials } = context.credentials;
108
+ return {
109
+ credentials: {
110
+ ...credentials,
111
+ ...expiration ? { expiration: new Date(expiration) } : {}
112
+ },
113
+ requestConfig: context.requestConfig
114
+ };
115
+ });
116
+ }
117
+ //#endregion
12
118
  //#region index.ts
13
119
  const apiVersion = CACHE_PROVIDER_API_VERSION;
14
120
  async function fileDigests(path, signal) {
@@ -46,93 +152,147 @@ function createS3FileCache(options) {
46
152
  error() {}
47
153
  }
48
154
  };
49
- const client = new S3Client(config);
155
+ const environment = options.env === void 0 ? void 0 : { ...options.env };
156
+ const defaultClient = environment ? void 0 : new S3Client(config);
157
+ let context;
158
+ async function withClient(signal, run) {
159
+ signal.throwIfAborted();
160
+ if (defaultClient) return run(defaultClient);
161
+ if (!context || context.credentials.expiration && context.credentials.expiration.getTime() - Date.now() < 3e5) context = await scopedClientContext(environment ?? {}, options.projectRoot ?? process.cwd(), {
162
+ profile: options.profile,
163
+ region: options.region ?? "us-east-1",
164
+ endpoint: options.endpoint
165
+ }, signal);
166
+ signal.throwIfAborted();
167
+ const captured = context;
168
+ const scoped = new S3Client({
169
+ ...config,
170
+ ...captured.requestConfig,
171
+ credentials: captured.credentials,
172
+ ignoreConfiguredEndpointUrls: true,
173
+ useArnRegion: async () => captured.requestConfig.useArnRegion,
174
+ disableS3ExpressSessionAuth: async () => captured.requestConfig.disableS3ExpressSessionAuth,
175
+ sigv4aSigningRegionSet: async () => captured.requestConfig.sigv4aSigningRegionSet,
176
+ userAgentAppId: async () => captured.requestConfig.userAgentAppId
177
+ });
178
+ try {
179
+ return await run(scoped);
180
+ } finally {
181
+ scoped.destroy();
182
+ }
183
+ }
50
184
  const prefix = (options.prefix ?? "stim/v1").replace(/^\/+|\/+$/g, "");
51
185
  const objectKey = (key) => `${prefix}/${key}`;
52
186
  return {
53
187
  async stat({ key, signal }) {
54
- signal.throwIfAborted();
55
- try {
56
- const object = await client.send(new HeadObjectCommand({
57
- Bucket: options.bucket,
58
- Key: objectKey(key)
59
- }), { abortSignal: signal });
60
- return typeof object.ContentLength === "number" ? { size: object.ContentLength } : null;
61
- } catch (error) {
62
- if (status(error) === 404) return null;
63
- throw error;
64
- }
188
+ return withClient(signal, async (client) => {
189
+ signal.throwIfAborted();
190
+ try {
191
+ const object = await client.send(new HeadObjectCommand({
192
+ Bucket: options.bucket,
193
+ Key: objectKey(key)
194
+ }), { abortSignal: signal });
195
+ return typeof object.ContentLength === "number" ? { size: object.ContentLength } : null;
196
+ } catch (error) {
197
+ if (status(error) === 404) return null;
198
+ throw error;
199
+ }
200
+ });
65
201
  },
66
202
  async resolve({ key, destinationDir, signal }) {
67
- signal.throwIfAborted();
68
- let object;
69
- try {
70
- object = await client.send(new GetObjectCommand({
71
- Bucket: options.bucket,
72
- Key: objectKey(key)
73
- }), { abortSignal: signal });
74
- } catch (error) {
75
- if (status(error) === 404) return null;
76
- throw error;
77
- }
78
- const expected = object.Metadata?.sha256;
79
- if (!(object.Body instanceof Readable) || !expected || !/^[a-f0-9]{64}$/.test(expected)) {
80
- if (object.Body instanceof Readable) object.Body.destroy();
81
- throw new Error("The S3 cache entry has no SHA-256 metadata.");
82
- }
83
- const temporary = join(destinationDir, `${randomUUID()}.partial`);
84
- const complete = join(destinationDir, `${randomUUID()}.cache`);
85
- const digest = createHash("sha256");
86
- try {
87
- await mkdir(destinationDir, { recursive: true });
88
- await pipeline(object.Body, new Transform({ transform(chunk, _encoding, callback) {
89
- digest.update(chunk);
90
- callback(null, chunk);
91
- } }), createWriteStream(temporary, { flags: "wx" }), { signal });
92
- if (digest.digest("hex") !== expected) throw new Error("The S3 cache entry failed SHA-256 verification.");
203
+ return withClient(signal, async (client) => {
93
204
  signal.throwIfAborted();
94
- await rename(temporary, complete);
95
- return complete;
96
- } finally {
97
- object.Body.destroy();
98
- await rm(temporary, { force: true });
99
- }
205
+ let object;
206
+ try {
207
+ object = await client.send(new GetObjectCommand({
208
+ Bucket: options.bucket,
209
+ Key: objectKey(key)
210
+ }), { abortSignal: signal });
211
+ } catch (error) {
212
+ if (status(error) === 404) return null;
213
+ throw error;
214
+ }
215
+ const expected = object.Metadata?.sha256;
216
+ if (!(object.Body instanceof Readable) || !expected || !/^[a-f0-9]{64}$/.test(expected)) {
217
+ if (object.Body instanceof Readable) object.Body.destroy();
218
+ throw new Error("The S3 cache entry has no SHA-256 metadata.");
219
+ }
220
+ const temporary = join(destinationDir, `${randomUUID()}.partial`);
221
+ const complete = join(destinationDir, `${randomUUID()}.cache`);
222
+ const digest = createHash("sha256");
223
+ try {
224
+ await mkdir(destinationDir, { recursive: true });
225
+ await pipeline(object.Body, new Transform({ transform(chunk, _encoding, callback) {
226
+ digest.update(chunk);
227
+ callback(null, chunk);
228
+ } }), createWriteStream(temporary, { flags: "wx" }), { signal });
229
+ if (digest.digest("hex") !== expected) throw new Error("The S3 cache entry failed SHA-256 verification.");
230
+ signal.throwIfAborted();
231
+ await rename(temporary, complete);
232
+ return complete;
233
+ } finally {
234
+ object.Body.destroy();
235
+ await rm(temporary, { force: true });
236
+ }
237
+ });
100
238
  },
101
239
  async store({ key, sourcePath, overwrite, signal }) {
102
- signal.throwIfAborted();
103
- const digests = await fileDigests(sourcePath, signal);
104
- const body = createReadStream(sourcePath, { signal });
105
- try {
106
- await client.send(new PutObjectCommand({
107
- Bucket: options.bucket,
108
- Key: objectKey(key),
109
- Body: body,
110
- ContentLength: (await stat(sourcePath)).size,
111
- ContentMD5: digests.md5,
112
- Metadata: { sha256: digests.sha256 },
113
- IfNoneMatch: overwrite ? void 0 : "*"
114
- }), { abortSignal: signal });
115
- } catch (error) {
116
- if (overwrite || status(error) !== 412) throw error;
117
- } finally {
118
- body.destroy();
119
- }
240
+ return withClient(signal, async (client) => {
241
+ signal.throwIfAborted();
242
+ const digests = await fileDigests(sourcePath, signal);
243
+ const size = (await stat(sourcePath)).size;
244
+ for (let attempt = 0;; attempt++) {
245
+ signal.throwIfAborted();
246
+ const body = createReadStream(sourcePath, { signal });
247
+ try {
248
+ await client.send(new PutObjectCommand({
249
+ Bucket: options.bucket,
250
+ Key: objectKey(key),
251
+ Body: body,
252
+ ContentLength: size,
253
+ ContentMD5: digests.md5,
254
+ Metadata: { sha256: digests.sha256 },
255
+ IfNoneMatch: overwrite ? void 0 : "*"
256
+ }), { abortSignal: signal });
257
+ return;
258
+ } catch (error) {
259
+ if (!overwrite && status(error) === 412) return;
260
+ if (attempt === 2 || signal.aborted || !(error instanceof Error) || !("code" in error) || error.code !== "ECONNRESET") throw error;
261
+ } finally {
262
+ body.destroy();
263
+ }
264
+ await setTimeout$1(100 * 2 ** attempt, void 0, { signal });
265
+ }
266
+ });
120
267
  }
121
268
  };
122
269
  }
123
- async function scratch(run) {
124
- const dir = await mkdtemp(join(tmpdir(), "stim-s3-cache-"));
125
- try {
126
- return await run(dir);
127
- } finally {
128
- await rm(dir, {
129
- recursive: true,
130
- force: true
131
- });
132
- }
270
+ function cleanupFailure(path, cause) {
271
+ return Object.assign(new Error(`S3 cache cleanup failed. Retained ${path}.`, { cause }), {
272
+ code: "STIM_CLAIM_UNAVAILABLE",
273
+ reportCleanup: true,
274
+ remedy: "Check the retained directory permissions and remove it before retrying."
275
+ });
133
276
  }
134
- function createCacheProvider({ options }) {
277
+ async function scratch(run, parent) {
278
+ if (parent) await mkdir(parent, { recursive: true });
279
+ const dir = await mkdtemp(join(parent ?? tmpdir(), "stim-s3-cache-"));
280
+ return Promise.resolve().then(() => run(dir)).finally(async () => {
281
+ try {
282
+ await rm(dir, {
283
+ recursive: true,
284
+ force: true
285
+ });
286
+ } catch (cause) {
287
+ throw cleanupFailure(parent ?? dir, cause);
288
+ }
289
+ });
290
+ }
291
+ function createCacheProvider({ options, projectRoot, env }) {
292
+ const environment = env === void 0 ? void 0 : { ...env };
135
293
  const files = createS3FileCache({
294
+ env: environment,
295
+ projectRoot,
136
296
  bucket: typeof options.bucket === "string" ? options.bucket : "",
137
297
  prefix: typeof options.prefix === "string" ? options.prefix : void 0,
138
298
  endpoint: typeof options.endpoint === "string" ? options.endpoint : void 0,
@@ -153,25 +313,16 @@ function createCacheProvider({ options }) {
153
313
  if (!archive) return null;
154
314
  const staging = await mkdtemp(join(destinationDir, "s3-"));
155
315
  try {
156
- let invalidLink = false;
157
- await pipeline(createReadStream(archive), extract({
158
- cwd: staging,
159
- strict: true,
160
- filter(path, entry) {
161
- if (!("type" in entry)) return true;
162
- if (entry.type !== "SymbolicLink" && entry.type !== "Link") return true;
163
- const link = entry.linkpath ?? "";
164
- const target = resolve(staging, entry.type === "SymbolicLink" ? dirname(path) : "", link);
165
- const root = relative(staging, resolve(staging, path)).split(sep)[0];
166
- const outside = relative(resolve(staging, root ?? ""), target);
167
- if (isAbsolute(link) || outside === ".." || outside.startsWith(`..${sep}`)) {
168
- invalidLink = true;
169
- return false;
170
- }
171
- return true;
172
- }
173
- }), { signal });
174
- if (invalidLink) throw new Error("The S3 build entry contains a link outside the artifact.");
316
+ await scopedWorker(new URL(import.meta.url.endsWith(".ts") ? "./extract-worker.ts" : "./extract-worker.mjs", import.meta.url), {
317
+ archive,
318
+ staging
319
+ }, {
320
+ env: { ...environment ?? process.env },
321
+ cwd: projectRoot,
322
+ signal,
323
+ timeoutMs: buildResolveTimeoutMs(environment),
324
+ name: "S3 archive extraction"
325
+ });
175
326
  signal.throwIfAborted();
176
327
  const entries = await readdir(staging);
177
328
  const [entry] = entries;
@@ -181,13 +332,17 @@ function createCacheProvider({ options }) {
181
332
  if (platform === "ios" ? !info.isDirectory() || !path.endsWith(".app") : !info.isFile() || !path.endsWith(".apk")) throw new Error("The S3 build entry has the wrong artifact type.");
182
333
  return path;
183
334
  } catch (error) {
184
- await rm(staging, {
185
- recursive: true,
186
- force: true
187
- });
335
+ try {
336
+ await rm(staging, {
337
+ recursive: true,
338
+ force: true
339
+ });
340
+ } catch (cause) {
341
+ throw cleanupFailure(destinationDir, cause);
342
+ }
188
343
  throw error;
189
344
  }
190
- });
345
+ }, destinationDir);
191
346
  },
192
347
  async store({ platform, key, sourcePath, overwrite, signal }) {
193
348
  await scratch(async (dir) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stim-cli/cache-s3",
3
- "version": "1.21.0",
3
+ "version": "1.23.0",
4
4
  "description": "Optional S3-compatible file, native artifact and Metro cache provider for Stim.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@aws-sdk/client-s3": "^3.1149.0",
28
- "@stim-cli/cache": "^1.21.0",
28
+ "@stim-cli/cache": "^1.23.0",
29
29
  "tar": "^7.5.22"
30
30
  },
31
31
  "engines": {