@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 +18 -2
- package/ActionOutputs.js +76 -50
- package/BlobEnvelope.js +61 -46
- package/CacheKey.js +66 -21
- package/DetachedProcess.js +68 -41
- package/PackageManagerInstaller.js +1 -1
- package/README.md +40 -4
- package/ToolInstaller.js +2 -1
- package/index.d.ts +317 -66
- package/index.js +5 -5
- package/package.json +5 -5
- package/tsdoc-metadata.json +1 -1
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
|
-
|
|
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
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
|
70
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
173
|
-
|
|
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
|
|
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 {
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
|
104
|
-
if (bytes.length < HEADER_BYTES) return Result.fail(new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
416
|
+
export { CacheKey, CacheKeyBadPatternError, CacheKeyReadError };
|
package/DetachedProcess.js
CHANGED
|
@@ -10,37 +10,72 @@ import { closeSync, openSync } from "node:fs";
|
|
|
10
10
|
*
|
|
11
11
|
* @public
|
|
12
12
|
*/
|
|
13
|
-
var
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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}`}`;
|