@effected/github-actions 0.9.2 → 0.10.1

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/ActionInput.js CHANGED
@@ -264,10 +264,22 @@ var ActionInput = class ActionInput {
264
264
  * @remarks
265
265
  * Only the **first** `=` splits, so a value may contain one.
266
266
  *
267
+ * An **empty key** (`=value`, or a bare `=`) is always rejected: `{ "": v }`
268
+ * cannot be what a workflow meant, and the damage lands far from the typo —
269
+ * an empty key became a repository filter that matched nothing, and the run
270
+ * reported zero results with no indication why. An empty **value** (`key=`)
271
+ * is accepted by default, because setting a property to the empty string is
272
+ * legitimate; pass `requireValue` to reject it. Every rejection names the
273
+ * offending line.
274
+ *
267
275
  * Absent or `""` fails as missing data (see {@link ActionInput.string}) —
268
276
  * `Config.withDefault({})` is the idiom for an optional pairs input.
277
+ *
278
+ * @param name - The input name, unmangled.
279
+ * @param options - `requireValue` rejects a pair whose value is empty.
269
280
  */
270
- static pairs(name) {
281
+ static pairs(name, options) {
282
+ const requireValue = options?.requireValue ?? false;
271
283
  return Config.string(inputVariable(name)).pipe(Config.mapOrFail((raw) => {
272
284
  const result = {};
273
285
  for (const line of raw.split("\n")) {
@@ -275,7 +287,11 @@ var ActionInput = class ActionInput {
275
287
  if (stripped === "") continue;
276
288
  const split = stripped.indexOf("=");
277
289
  if (split === -1) return Effect.fail(configError(`Input "${name}" has a line that is not \`key=value\`: "${stripped}"`, raw));
278
- result[stripped.slice(0, split).trim()] = stripped.slice(split + 1).trim();
290
+ const key = stripped.slice(0, split).trim();
291
+ const value = stripped.slice(split + 1).trim();
292
+ if (key === "") return Effect.fail(configError(`Input "${name}" has a line with an empty key: "${stripped}"`, raw));
293
+ if (requireValue && value === "") return Effect.fail(configError(`Input "${name}" has a line with an empty value: "${stripped}"`, raw));
294
+ result[key] = value;
279
295
  }
280
296
  return Effect.succeed(result);
281
297
  }));
package/ActionOutputs.js CHANGED
@@ -8,38 +8,79 @@ import { Console, Context, Effect, FileSystem, Layer, Schema } from "effect";
8
8
  *
9
9
  * @public
10
10
  */
11
- var ActionOutputError = class extends Schema.TaggedError()("ActionOutputError", {
12
- /**
13
- * `unavailable` — the runner file variable is not set, which usually means
14
- * the code is running outside a GitHub runner. `writeFailed` — the file
15
- * exists but could not be appended to. `invalidName` — the name would
16
- * corrupt the file's block structure. `encodeFailed` — a value did not
17
- * satisfy its schema. `detached` — the member was called under
18
- * {@link ActionOutputs.layerDetached}, where the channel it writes cannot
19
- * reach the job that would read it.
20
- */
21
- reason: Schema.Literals([
22
- "unavailable",
23
- "writeFailed",
24
- "invalidName",
25
- "encodeFailed",
26
- "detached"
27
- ]),
28
- /** Which runner file was involved, by environment variable name. */
11
+ var RunnerFileUnavailableError = class extends Schema.TaggedError()("RunnerFileUnavailableError", {
12
+ /** The runner file involved, by environment variable name. */
13
+ file: Schema.String,
14
+ /** The underlying failure, preserved structurally. */
15
+ cause: Schema.optionalKey(Schema.Defect())
16
+ }) {
17
+ get message() {
18
+ return `Runner file "${this.file}" is not available; is this running on a GitHub runner?`;
19
+ }
20
+ };
21
+ /**
22
+ * Raised when a runner file exists but could not be appended to.
23
+ *
24
+ * @public
25
+ */
26
+ var RunnerFileWriteError = class extends Schema.TaggedError()("RunnerFileWriteError", {
27
+ /** The runner file involved, by environment variable name. */
28
+ file: Schema.String,
29
+ /** The underlying failure, preserved structurally. */
30
+ cause: Schema.optionalKey(Schema.Defect())
31
+ }) {
32
+ get message() {
33
+ return `Failed to write to runner file "${this.file}"`;
34
+ }
35
+ };
36
+ /**
37
+ * Raised when a name would corrupt the runner file's block structure.
38
+ *
39
+ * @public
40
+ */
41
+ var InvalidOutputNameError = class extends Schema.TaggedError()("InvalidOutputNameError", {
42
+ /** The offending name. */
43
+ name: Schema.String,
44
+ /** The runner file involved, by environment variable name. */
29
45
  file: Schema.optionalKey(Schema.String),
46
+ /** The underlying failure, preserved structurally. */
47
+ cause: Schema.optionalKey(Schema.Defect())
48
+ }) {
49
+ get message() {
50
+ return `"${this.name}" is not a usable output name`;
51
+ }
52
+ };
53
+ /**
54
+ * Raised when a value did not satisfy its schema.
55
+ *
56
+ * @public
57
+ */
58
+ var OutputEncodeError = class extends Schema.TaggedError()("OutputEncodeError", {
59
+ /** The output or variable name whose value would not encode. */
60
+ name: Schema.String,
61
+ /** The underlying failure, preserved structurally. */
62
+ cause: Schema.optionalKey(Schema.Defect())
63
+ }) {
64
+ get message() {
65
+ return `Failed to encode the value for "${this.name}"`;
66
+ }
67
+ };
68
+ /**
69
+ * Raised when an output member was called under
70
+ * {@link ActionOutputs.layerDetached}.
71
+ *
72
+ * @public
73
+ */
74
+ var DetachedOutputError = class extends Schema.TaggedError()("DetachedOutputError", {
75
+ /** The runner file involved, by environment variable name. */
76
+ file: Schema.String,
30
77
  /** The output or variable name, when one is involved. */
31
78
  name: Schema.optionalKey(Schema.String),
32
79
  /** The underlying failure, preserved structurally. */
33
80
  cause: Schema.optionalKey(Schema.Defect())
34
81
  }) {
35
82
  get message() {
36
- switch (this.reason) {
37
- case "unavailable": return `Runner file "${this.file}" is not available; is this running on a GitHub runner?`;
38
- case "writeFailed": return `Failed to write to runner file "${this.file}"`;
39
- case "invalidName": return `"${this.name}" is not a usable output name`;
40
- case "detached": return `Runner file "${this.file}" cannot be reached from a detached worker — it configures the parent job's later steps, and a worker has none and may outlive the job. Publish this from the parent process instead.`;
41
- default: return `Failed to encode the value for "${this.name}"`;
42
- }
83
+ return `Runner file "${this.file}" cannot be reached from a detached worker — it configures the parent job's later steps, and a worker has none and may outlive the job. Publish this from the parent process instead.`;
43
84
  }
44
85
  };
45
86
  /** The base delimiter for a heredoc block. */
@@ -66,19 +107,14 @@ const make = Effect.gen(function* () {
66
107
  const env = yield* ActionEnvironment;
67
108
  const fs = yield* FileSystem.FileSystem;
68
109
  const append = (file, content) => Effect.gen(function* () {
69
- const path = yield* env.get(file).pipe(Effect.mapError(() => new ActionOutputError({
70
- reason: "unavailable",
71
- file
72
- })));
73
- yield* fs.writeFileString(path, content, { flag: "a" }).pipe(Effect.mapError((cause) => new ActionOutputError({
74
- reason: "writeFailed",
110
+ const path = yield* env.get(file).pipe(Effect.mapError(() => new RunnerFileUnavailableError({ file })));
111
+ yield* fs.writeFileString(path, content, { flag: "a" }).pipe(Effect.mapError((cause) => new RunnerFileWriteError({
75
112
  file,
76
113
  cause
77
114
  })));
78
115
  });
79
116
  const appendBlock = (file, name, value) => {
80
- if (!isUsableName(name)) return Effect.fail(new ActionOutputError({
81
- reason: "invalidName",
117
+ if (!isUsableName(name)) return Effect.fail(new InvalidOutputNameError({
82
118
  file,
83
119
  name
84
120
  }));
@@ -88,8 +124,7 @@ const make = Effect.gen(function* () {
88
124
  const set = (name, value) => appendBlock("GITHUB_OUTPUT", name, value);
89
125
  return {
90
126
  set,
91
- setJson: (name, value, schema) => Schema.encodeUnknownEffect(schema)(value).pipe(Effect.mapError((cause) => new ActionOutputError({
92
- reason: "encodeFailed",
127
+ setJson: (name, value, schema) => Schema.encodeUnknownEffect(schema)(value).pipe(Effect.mapError((cause) => new OutputEncodeError({
93
128
  name,
94
129
  cause
95
130
  })), Effect.flatMap((encoded) => set(name, JSON.stringify(encoded)))),
@@ -159,29 +194,20 @@ var ActionOutputs = class ActionOutputs extends Context.Service()("@effected/git
159
194
  * channel to fail through.
160
195
  */
161
196
  static layerDetached = Layer.succeed(this, {
162
- set: (name) => Effect.fail(new ActionOutputError({
163
- reason: "detached",
197
+ set: (name) => Effect.fail(new DetachedOutputError({
164
198
  file: "GITHUB_OUTPUT",
165
199
  name
166
200
  })),
167
- setJson: (name) => Effect.fail(new ActionOutputError({
168
- reason: "detached",
201
+ setJson: (name) => Effect.fail(new DetachedOutputError({
169
202
  file: "GITHUB_OUTPUT",
170
203
  name
171
204
  })),
172
- summary: () => Effect.fail(new ActionOutputError({
173
- reason: "detached",
174
- file: "GITHUB_STEP_SUMMARY"
175
- })),
176
- exportVariable: (name) => Effect.fail(new ActionOutputError({
177
- reason: "detached",
205
+ summary: () => Effect.fail(new DetachedOutputError({ file: "GITHUB_STEP_SUMMARY" })),
206
+ exportVariable: (name) => Effect.fail(new DetachedOutputError({
178
207
  file: "GITHUB_ENV",
179
208
  name
180
209
  })),
181
- addPath: () => Effect.fail(new ActionOutputError({
182
- reason: "detached",
183
- file: "GITHUB_PATH"
184
- })),
210
+ addPath: () => Effect.fail(new DetachedOutputError({ file: "GITHUB_PATH" })),
185
211
  setFailed: (message) => Console.error(message),
186
212
  setSecret: () => Effect.void
187
213
  });
@@ -201,4 +227,4 @@ var ActionOutputs = class ActionOutputs extends Context.Service()("@effected/git
201
227
  };
202
228
 
203
229
  //#endregion
204
- export { ActionOutputError, ActionOutputs };
230
+ export { ActionOutputs, DetachedOutputError, InvalidOutputNameError, OutputEncodeError, RunnerFileUnavailableError, RunnerFileWriteError };
package/BlobEnvelope.js CHANGED
@@ -6,35 +6,62 @@ import { Result, Schema } from "effect";
6
6
  *
7
7
  * @public
8
8
  */
9
- var BlobEnvelopeError = class extends Schema.TaggedError()("BlobEnvelopeError", {
10
- /**
11
- * `notAnEnvelope` — the magic prefix is absent, so these are raw bytes
12
- * written before this format existed (or by something else entirely).
13
- * `unsupportedVersion` — an envelope from a newer revision.
14
- * `truncated` — the frame ends mid-header or mid-metadata.
15
- * `metadataDecodeFailed` — the metadata is well-framed but does not satisfy
16
- * the caller's schema. `metadataEncodeFailed` — the value being stored does
17
- * not satisfy it.
18
- */
19
- reason: Schema.Literals([
20
- "notAnEnvelope",
21
- "unsupportedVersion",
22
- "truncated",
23
- "metadataDecodeFailed",
24
- "metadataEncodeFailed"
25
- ]),
26
- /** The envelope version found, when one was readable. */
27
- version: Schema.optionalKey(Schema.Number),
9
+ var NotABlobEnvelopeError = class extends Schema.TaggedError()("NotABlobEnvelopeError", {
10
+ /** The underlying failure, preserved structurally. */
11
+ cause: Schema.optionalKey(Schema.Defect()) }) {
12
+ get message() {
13
+ return "Bytes are not an @effected/github-actions blob envelope";
14
+ }
15
+ };
16
+ /**
17
+ * Raised when the frame ends mid-header or mid-metadata.
18
+ *
19
+ * @public
20
+ */
21
+ var TruncatedBlobEnvelopeError = class extends Schema.TaggedError()("TruncatedBlobEnvelopeError", {
22
+ /** The underlying failure, preserved structurally. */
23
+ cause: Schema.optionalKey(Schema.Defect()) }) {
24
+ get message() {
25
+ return "Blob envelope is truncated";
26
+ }
27
+ };
28
+ /**
29
+ * Raised when the envelope came from a newer revision of the format.
30
+ *
31
+ * @public
32
+ */
33
+ var UnsupportedBlobEnvelopeVersionError = class extends Schema.TaggedError()("UnsupportedBlobEnvelopeVersionError", {
34
+ /** The envelope version found. */
35
+ version: Schema.Number,
36
+ /** The underlying failure, preserved structurally. */
28
37
  cause: Schema.optionalKey(Schema.Defect())
29
38
  }) {
30
39
  get message() {
31
- switch (this.reason) {
32
- case "notAnEnvelope": return "Bytes are not an @effected/github-actions blob envelope";
33
- case "unsupportedVersion": return `Blob envelope version ${this.version} is not supported`;
34
- case "truncated": return "Blob envelope is truncated";
35
- case "metadataDecodeFailed": return "Blob envelope metadata did not satisfy the schema";
36
- default: return "Blob metadata could not be encoded";
37
- }
40
+ return `Blob envelope version ${this.version} is not supported`;
41
+ }
42
+ };
43
+ /**
44
+ * Raised when well-framed metadata does not satisfy the caller's schema.
45
+ *
46
+ * @public
47
+ */
48
+ var BlobMetadataDecodeError = class extends Schema.TaggedError()("BlobMetadataDecodeError", {
49
+ /** The underlying failure, preserved structurally. */
50
+ cause: Schema.optionalKey(Schema.Defect()) }) {
51
+ get message() {
52
+ return "Blob envelope metadata did not satisfy the schema";
53
+ }
54
+ };
55
+ /**
56
+ * Raised when the value being stored does not satisfy its schema.
57
+ *
58
+ * @public
59
+ */
60
+ var BlobMetadataEncodeError = class extends Schema.TaggedError()("BlobMetadataEncodeError", {
61
+ /** The underlying failure, preserved structurally. */
62
+ cause: Schema.optionalKey(Schema.Defect()) }) {
63
+ get message() {
64
+ return "Blob metadata could not be encoded";
38
65
  }
39
66
  };
40
67
  /** Identifies the frame family. Four bytes: `E F B S`. */
@@ -85,10 +112,7 @@ var BlobEnvelope = class {
85
112
  /** Frame metadata and a body into a single blob. */
86
113
  static encodeResult(metadata, body, schema) {
87
114
  const encoded = Schema.encodeUnknownResult(schema)(metadata);
88
- if (Result.isFailure(encoded)) return Result.fail(new BlobEnvelopeError({
89
- reason: "metadataEncodeFailed",
90
- cause: encoded.failure
91
- }));
115
+ if (Result.isFailure(encoded)) return Result.fail(new BlobMetadataEncodeError({ cause: encoded.failure }));
92
116
  const metaBytes = new TextEncoder().encode(JSON.stringify(encoded.success));
93
117
  const out = new Uint8Array(HEADER_BYTES + metaBytes.length + body.length);
94
118
  out.set(MAGIC, 0);
@@ -100,30 +124,21 @@ var BlobEnvelope = class {
100
124
  }
101
125
  /** Read a framed blob back into its metadata and body. */
102
126
  static decodeResult(bytes, schema) {
103
- if (bytes.length < MAGIC.length || !MAGIC.every((byte, index) => bytes[index] === byte)) return Result.fail(new BlobEnvelopeError({ reason: "notAnEnvelope" }));
104
- if (bytes.length < HEADER_BYTES) return Result.fail(new BlobEnvelopeError({ reason: "truncated" }));
127
+ if (bytes.length < MAGIC.length || !MAGIC.every((byte, index) => bytes[index] === byte)) return Result.fail(new NotABlobEnvelopeError({}));
128
+ if (bytes.length < HEADER_BYTES) return Result.fail(new TruncatedBlobEnvelopeError({}));
105
129
  const version = bytes[MAGIC.length] ?? 0;
106
- if (version !== VERSION) return Result.fail(new BlobEnvelopeError({
107
- reason: "unsupportedVersion",
108
- version
109
- }));
130
+ if (version !== VERSION) return Result.fail(new UnsupportedBlobEnvelopeVersionError({ version }));
110
131
  const metaLength = new DataView(bytes.buffer, bytes.byteOffset).getUint32(MAGIC.length + 1, false);
111
- if (bytes.length < HEADER_BYTES + metaLength) return Result.fail(new BlobEnvelopeError({ reason: "truncated" }));
132
+ if (bytes.length < HEADER_BYTES + metaLength) return Result.fail(new TruncatedBlobEnvelopeError({}));
112
133
  const metaText = new TextDecoder().decode(bytes.subarray(HEADER_BYTES, HEADER_BYTES + metaLength));
113
134
  let parsed;
114
135
  try {
115
136
  parsed = JSON.parse(metaText);
116
137
  } catch (cause) {
117
- return Result.fail(new BlobEnvelopeError({
118
- reason: "metadataDecodeFailed",
119
- cause
120
- }));
138
+ return Result.fail(new BlobMetadataDecodeError({ cause }));
121
139
  }
122
140
  const decoded = Schema.decodeUnknownResult(schema)(parsed);
123
- if (Result.isFailure(decoded)) return Result.fail(new BlobEnvelopeError({
124
- reason: "metadataDecodeFailed",
125
- cause: decoded.failure
126
- }));
141
+ if (Result.isFailure(decoded)) return Result.fail(new BlobMetadataDecodeError({ cause: decoded.failure }));
127
142
  return Result.succeed({
128
143
  metadata: decoded.success,
129
144
  body: bytes.slice(HEADER_BYTES + metaLength)
@@ -132,4 +147,4 @@ var BlobEnvelope = class {
132
147
  };
133
148
 
134
149
  //#endregion
135
- export { BlobEnvelope, BlobEnvelopeError };
150
+ export { BlobEnvelope, BlobMetadataDecodeError, BlobMetadataEncodeError, NotABlobEnvelopeError, TruncatedBlobEnvelopeError, UnsupportedBlobEnvelopeVersionError };
package/CacheKey.js CHANGED
@@ -4,27 +4,34 @@ import { GlobSet } from "@effected/glob";
4
4
 
5
5
  //#region src/CacheKey.ts
6
6
  /**
7
- * Raised when a cache key cannot be derived from the filesystem.
7
+ * Raised when a file or directory that was going to be hashed could not be
8
+ * read.
8
9
  *
9
10
  * @public
10
11
  */
11
- var CacheKeyError = class extends Schema.TaggedError()("CacheKeyError", {
12
- /**
13
- * `readFailed` — a file or directory that was going to be hashed could not be
14
- * read. `badPattern` — a glob pattern would not compile. Key *derivation*
15
- * itself is pure and total once the bytes are in hand, so these are the only
16
- * two things that can go wrong.
17
- */
18
- reason: Schema.Literals(["readFailed", "badPattern"]),
12
+ var CacheKeyReadError = class extends Schema.TaggedError()("CacheKeyReadError", {
19
13
  /** The path being read when it went wrong. */
20
- path: Schema.optionalKey(Schema.String),
14
+ path: Schema.String,
15
+ /** The underlying failure, preserved structurally. */
16
+ cause: Schema.optionalKey(Schema.Defect())
17
+ }) {
18
+ get message() {
19
+ return `Could not read "${this.path}" while deriving a cache key`;
20
+ }
21
+ };
22
+ /**
23
+ * Raised when a glob pattern would not compile.
24
+ *
25
+ * @public
26
+ */
27
+ var CacheKeyBadPatternError = class extends Schema.TaggedError()("CacheKeyBadPatternError", {
21
28
  /** The pattern that would not compile. */
22
- pattern: Schema.optionalKey(Schema.String),
29
+ pattern: Schema.String,
23
30
  /** The underlying failure, preserved structurally. */
24
31
  cause: Schema.optionalKey(Schema.Defect())
25
32
  }) {
26
33
  get message() {
27
- return this.reason === "readFailed" ? `Could not read "${this.path}" while deriving a cache key` : `"${this.pattern}" is not a usable glob pattern`;
34
+ return `"${this.pattern}" is not a usable glob pattern`;
28
35
  }
29
36
  };
30
37
  /**
@@ -188,6 +195,48 @@ var CacheKey = class CacheKey extends Schema.Class("CacheKey")(Schema.Struct({
188
195
  restoreDepths: []
189
196
  });
190
197
  }
198
+ /**
199
+ * A copy of this key namespaced by `segment` — the whole key, ladder
200
+ * included.
201
+ *
202
+ * @remarks
203
+ * The cache-bust case: a run that must match nothing an unbusted run wrote,
204
+ * and whose own entries must be invisible to unbusted runs. Both halves of
205
+ * that are one intent, and spelling them separately is what makes the wrong
206
+ * version undetectable — a restore key is a **prefix match**, so folding the
207
+ * bust in *after* the retained prefix leaves an ordinary run's rung
208
+ * prefix-matching busted entries. The cache still appears to work, and
209
+ * quietly serves poisoned entries into unrelated runs.
210
+ *
211
+ * Two decisions make this safe for **any** segment value, with no prefix
212
+ * reasoning at the call site. The segment goes **first**, so the namespaced
213
+ * key shares no prefix with an unnamespaced one. And the ladder is
214
+ * **dropped**, so no rung of this key can reach outside the namespace even
215
+ * when the segment happens to equal an ordinary leading segment — which a
216
+ * prepend alone would not survive.
217
+ *
218
+ * Dropping the ladder is the safe default, not a prohibition: the segments
219
+ * are all still there, so a caller who wants one busted run to warm from
220
+ * another can follow with {@link CacheKey.withRestoreDepths} and get an
221
+ * in-namespace ladder deliberately.
222
+ *
223
+ * @example
224
+ * ```ts
225
+ * import { CacheKey } from "@effected/github-actions";
226
+ *
227
+ * const key = CacheKey.of("Linux", "pnpm-store", "abc123").withNamespace("bust7");
228
+ * key.key; // "bust7-Linux-pnpm-store-abc123"
229
+ * key.restoreKeys; // []
230
+ * ```
231
+ *
232
+ * @param segment - The namespace, e.g. a cache-bust token.
233
+ */
234
+ withNamespace(segment) {
235
+ return CacheKey.make({
236
+ segments: [segment, ...this.segments],
237
+ restoreDepths: []
238
+ });
239
+ }
191
240
  /** Build a key from its segments. */
192
241
  static of(...segments) {
193
242
  return CacheKey.make({ segments });
@@ -278,8 +327,7 @@ var CacheKey = class CacheKey extends Schema.Class("CacheKey")(Schema.Struct({
278
327
  const fs = yield* FileSystem.FileSystem;
279
328
  const ordered = [...new Set(files)].sort();
280
329
  if (ordered.length === 0) return Option.none();
281
- const digests = yield* Effect.all(ordered.map((path) => fs.readFile(path).pipe(Effect.mapError((cause) => new CacheKeyError({
282
- reason: "readFailed",
330
+ const digests = yield* Effect.all(ordered.map((path) => fs.readFile(path).pipe(Effect.mapError((cause) => new CacheKeyReadError({
283
331
  path,
284
332
  cause
285
333
  })), Effect.map((bytes) => createHash("sha256").update(bytes).digest()))), { concurrency: 8 });
@@ -313,13 +361,11 @@ var CacheKey = class CacheKey extends Schema.Class("CacheKey")(Schema.Struct({
313
361
  static matchingFiles = Effect.fn("CacheKey.matchingFiles")(function* (options) {
314
362
  const fs = yield* FileSystem.FileSystem;
315
363
  const path = yield* Path.Path;
316
- const set = yield* GlobSet.compile(options.patterns).pipe(Effect.mapError((cause) => new CacheKeyError({
317
- reason: "badPattern",
364
+ const set = yield* GlobSet.compile(options.patterns).pipe(Effect.mapError((cause) => new CacheKeyBadPatternError({
318
365
  pattern: cause.pattern,
319
366
  cause
320
367
  })));
321
- const entries = yield* fs.readDirectory(options.workspace, { recursive: true }).pipe(Effect.mapError((cause) => new CacheKeyError({
322
- reason: "readFailed",
368
+ const entries = yield* fs.readDirectory(options.workspace, { recursive: true }).pipe(Effect.mapError((cause) => new CacheKeyReadError({
323
369
  path: options.workspace,
324
370
  cause
325
371
  })));
@@ -327,8 +373,7 @@ var CacheKey = class CacheKey extends Schema.Class("CacheKey")(Schema.Struct({
327
373
  for (const entry of entries) {
328
374
  if (!set.matches(entry.replaceAll("\\", "/"))) continue;
329
375
  const absolute = path.join(options.workspace, entry);
330
- if ((yield* fs.stat(absolute).pipe(Effect.mapError((cause) => new CacheKeyError({
331
- reason: "readFailed",
376
+ if ((yield* fs.stat(absolute).pipe(Effect.mapError((cause) => new CacheKeyReadError({
332
377
  path: absolute,
333
378
  cause
334
379
  })))).type === "File") matched.push(absolute);
@@ -368,4 +413,4 @@ var CacheKey = class CacheKey extends Schema.Class("CacheKey")(Schema.Struct({
368
413
  };
369
414
 
370
415
  //#endregion
371
- export { CacheKey, CacheKeyError };
416
+ export { CacheKey, CacheKeyBadPatternError, CacheKeyReadError };
@@ -10,37 +10,72 @@ import { closeSync, openSync } from "node:fs";
10
10
  *
11
11
  * @public
12
12
  */
13
- var DetachedProcessError = class extends Schema.TaggedError()("DetachedProcessError", {
14
- /**
15
- * `logUnavailable` — the log file could not be opened, so there is nowhere
16
- * for the child's output to go. `spawnFailed` — the child did not start.
17
- * `invalidPid` — a non-positive pid was handed to {@link DetachedProcess.reap};
18
- * see there for why this is the most important value in the module.
19
- * `signalFailed` — the signal was refused, e.g. the process belongs to
20
- * another user. `notReady` — the readiness probe never held.
21
- */
22
- reason: Schema.Literals([
23
- "logUnavailable",
24
- "spawnFailed",
25
- "invalidPid",
26
- "signalFailed",
27
- "notReady"
28
- ]),
29
- /** The pid involved, when one is. */
30
- pid: Schema.optionalKey(Schema.Number),
31
- /** The path involved, when one is. */
32
- path: Schema.optionalKey(Schema.String),
13
+ var DetachedLogUnavailableError = class extends Schema.TaggedError()("DetachedLogUnavailableError", {
14
+ /** The log file that could not be opened. */
15
+ path: Schema.String,
16
+ /** The underlying failure, preserved structurally. */
17
+ cause: Schema.optionalKey(Schema.Defect())
18
+ }) {
19
+ get message() {
20
+ return `Could not open the detached log file "${this.path}"`;
21
+ }
22
+ };
23
+ /**
24
+ * Raised when the detached child did not start.
25
+ *
26
+ * @public
27
+ */
28
+ var DetachedSpawnFailedError = class extends Schema.TaggedError()("DetachedSpawnFailedError", {
29
+ /** The underlying failure, preserved structurally. */
30
+ cause: Schema.optionalKey(Schema.Defect()) }) {
31
+ get message() {
32
+ return "The detached child did not start";
33
+ }
34
+ };
35
+ /**
36
+ * Raised when a non-positive pid was handed to {@link DetachedProcess.reap}.
37
+ *
38
+ * @remarks
39
+ * See {@link DetachedProcess.reap} for why this is the most important value in
40
+ * the module: a non-positive pid targets a process *group*, not a process.
41
+ *
42
+ * @public
43
+ */
44
+ var InvalidPidError = class extends Schema.TaggedError()("InvalidPidError", {
45
+ /** The pid that was refused. */
46
+ pid: Schema.Number,
47
+ /** The underlying failure, preserved structurally. */
48
+ cause: Schema.optionalKey(Schema.Defect())
49
+ }) {
50
+ get message() {
51
+ return `Refusing to signal pid ${this.pid}: a non-positive pid targets a process group, not a process`;
52
+ }
53
+ };
54
+ /**
55
+ * Raised when a signal was refused, e.g. the process belongs to another user.
56
+ *
57
+ * @public
58
+ */
59
+ var DetachedSignalFailedError = class extends Schema.TaggedError()("DetachedSignalFailedError", {
60
+ /** The pid that could not be signalled. */
61
+ pid: Schema.Number,
33
62
  /** The underlying failure, preserved structurally. */
34
63
  cause: Schema.optionalKey(Schema.Defect())
35
64
  }) {
36
65
  get message() {
37
- switch (this.reason) {
38
- case "logUnavailable": return `Could not open the detached log file "${this.path}"`;
39
- case "spawnFailed": return "The detached child did not start";
40
- case "invalidPid": return `Refusing to signal pid ${this.pid}: a non-positive pid targets a process group, not a process`;
41
- case "signalFailed": return `Could not signal pid ${this.pid}`;
42
- default: return "The detached child never became ready";
43
- }
66
+ return `Could not signal pid ${this.pid}`;
67
+ }
68
+ };
69
+ /**
70
+ * Raised when the readiness probe never held.
71
+ *
72
+ * @public
73
+ */
74
+ var DetachedNotReadyError = class extends Schema.TaggedError()("DetachedNotReadyError", {
75
+ /** The underlying failure, preserved structurally. */
76
+ cause: Schema.optionalKey(Schema.Defect()) }) {
77
+ get message() {
78
+ return "The detached child never became ready";
44
79
  }
45
80
  };
46
81
  /**
@@ -129,8 +164,7 @@ var DetachedProcess = class DetachedProcess {
129
164
  static spawn = Effect.fn("DetachedProcess.spawn")(function* (options) {
130
165
  const descriptor = yield* Effect.try({
131
166
  try: () => openSync(options.logFile, "a"),
132
- catch: (cause) => new DetachedProcessError({
133
- reason: "logUnavailable",
167
+ catch: (cause) => new DetachedLogUnavailableError({
134
168
  path: options.logFile,
135
169
  cause
136
170
  })
@@ -159,10 +193,7 @@ var DetachedProcess = class DetachedProcess {
159
193
  closeSync(descriptor);
160
194
  }
161
195
  },
162
- catch: (cause) => new DetachedProcessError({
163
- reason: "spawnFailed",
164
- cause
165
- })
196
+ catch: (cause) => new DetachedSpawnFailedError({ cause })
166
197
  });
167
198
  yield* Effect.annotateCurrentSpan({ pid });
168
199
  return ChildProcessSpawner.ProcessId(pid);
@@ -186,7 +217,7 @@ var DetachedProcess = class DetachedProcess {
186
217
  const attempts = options?.attempts ?? 40;
187
218
  const poll = (remaining) => Effect.flatMap(probe, (ready) => {
188
219
  if (ready) return Effect.void;
189
- if (remaining <= 0) return Effect.fail(new DetachedProcessError({ reason: "notReady" }));
220
+ if (remaining <= 0) return Effect.fail(new DetachedNotReadyError({}));
190
221
  return Effect.flatMap(Effect.sleep(interval), () => poll(remaining - 1));
191
222
  });
192
223
  return yield* poll(attempts);
@@ -241,17 +272,13 @@ var DetachedProcess = class DetachedProcess {
241
272
  */
242
273
  static reap = Effect.fn("DetachedProcess.reap")(function* (pid, signal = "SIGTERM") {
243
274
  yield* Effect.annotateCurrentSpan({ pid });
244
- if (!Number.isInteger(pid) || pid <= 0) return yield* Effect.fail(new DetachedProcessError({
245
- reason: "invalidPid",
246
- pid
247
- }));
275
+ if (!Number.isInteger(pid) || pid <= 0) return yield* Effect.fail(new InvalidPidError({ pid }));
248
276
  return yield* Effect.suspend(() => {
249
277
  try {
250
278
  process.kill(pid, signal);
251
279
  return Effect.succeed(true);
252
280
  } catch (cause) {
253
- return isErrno(cause, "ESRCH") ? Effect.succeed(false) : Effect.fail(new DetachedProcessError({
254
- reason: "signalFailed",
281
+ return isErrno(cause, "ESRCH") ? Effect.succeed(false) : Effect.fail(new DetachedSignalFailedError({
255
282
  pid,
256
283
  cause
257
284
  }));
@@ -291,4 +318,4 @@ var DetachedProcess = class DetachedProcess {
291
318
  };
292
319
 
293
320
  //#endregion
294
- export { DetachedProcess, DetachedProcessError, ProcessId };
321
+ export { DetachedLogUnavailableError, DetachedNotReadyError, DetachedProcess, DetachedSignalFailedError, DetachedSpawnFailedError, InvalidPidError, ProcessId };
@@ -50,7 +50,7 @@ var PackageManagerInstallerError = class extends Schema.TaggedError()("PackageMa
50
50
  switch (this.reason) {
51
51
  case "downloadFailed": return `Could not download ${pin}${this.subject === void 0 ? "" : ` from ${this.subject}`}`;
52
52
  case "extractFailed": return `Could not extract ${pin}${this.subject === void 0 ? "" : ` (${this.subject})`}`;
53
- case "integrityMismatch": return `Integrity mismatch for ${pin}: expected ${this.expected}, got ${this.actual}`;
53
+ case "integrityMismatch": return this.expected === void 0 || this.actual === void 0 ? `Could not verify the integrity of ${pin}${this.subject === void 0 ? "" : ` (${this.subject})`}` : `Integrity mismatch for ${pin}: expected ${this.expected}, got ${this.actual}`;
54
54
  case "integrityMissing": return `The pin for ${pin} carries no integrity hash and requireIntegrity is set`;
55
55
  case "unsupportedPlatform": return `No ${pin} build is published for ${this.subject}`;
56
56
  case "layoutUnexpected": return `The ${pin} artifact does not have the expected layout${this.subject === void 0 ? "" : `: ${this.subject}`}`;