@effected/github-actions 0.12.0 → 0.13.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/ActionCache.js +62 -106
- package/ActionEnvironment.js +25 -56
- package/ActionInput.js +8 -7
- package/ActionLogger.js +13 -15
- package/ActionOutputs.js +144 -52
- package/ActionState.js +13 -25
- package/Artifact.js +27 -47
- package/BlobEnvelope.js +3 -9
- package/BlobStore.githubCache.js +33 -54
- package/BlobStore.js +14 -21
- package/CacheKey.js +57 -32
- package/CheckDocument.js +18 -11
- package/DetachedProcess.js +11 -15
- package/DryRun.js +9 -9
- package/GitHubMarkdown.js +3 -3
- package/GitHubToken.js +1 -1
- package/ManagedDocument.js +26 -29
- package/OidcTokenIssuer.js +18 -42
- package/PackageManagerInstaller.js +33 -29
- package/ToolInstaller.js +24 -34
- package/index.d.ts +118 -25
- package/index.js +2 -2
- package/internal/actionsResults.js +10 -20
- package/internal/cacheService.js +70 -0
- package/internal/digest.js +42 -0
- package/internal/fsProbe.js +14 -0
- package/internal/jwt.js +63 -0
- package/internal/runner.js +31 -0
- package/internal/runnerFile.js +45 -0
- package/internal/sigv4.js +3 -3
- package/internal/spawn.js +18 -0
- package/internal/twirp.js +18 -1
- package/internal/unstubbed.js +28 -0
- package/package.json +5 -4
package/BlobStore.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unstubbed } from "./internal/unstubbed.js";
|
|
1
2
|
import { BlobEnvelope } from "./BlobEnvelope.js";
|
|
2
3
|
import { sign } from "./internal/sigv4.js";
|
|
3
4
|
import { Secret } from "./Secret.js";
|
|
@@ -85,9 +86,9 @@ var BlobStore = class BlobStore extends Context.Service()("@effected/github-acti
|
|
|
85
86
|
static layerS3 = (config) => Layer.effect(BlobStore, makeS3(config));
|
|
86
87
|
/** A test double. Unstubbed members die rather than reporting a miss. */
|
|
87
88
|
static makeTest = (overrides = {}) => ({
|
|
88
|
-
get: () =>
|
|
89
|
-
put: () =>
|
|
90
|
-
has: () =>
|
|
89
|
+
get: () => dies("get"),
|
|
90
|
+
put: () => dies("put"),
|
|
91
|
+
has: () => dies("has"),
|
|
91
92
|
...overrides
|
|
92
93
|
});
|
|
93
94
|
/** {@link BlobStore.makeTest} behind `Layer.succeed`. */
|
|
@@ -115,9 +116,7 @@ var BlobStore = class BlobStore extends Context.Service()("@effected/github-acti
|
|
|
115
116
|
};
|
|
116
117
|
});
|
|
117
118
|
};
|
|
118
|
-
const
|
|
119
|
-
throw new Error(`BlobStore.makeTest: ${member}() was called but not stubbed — pass a \`${member}\` override.`);
|
|
120
|
-
};
|
|
119
|
+
const dies = unstubbed("BlobStore.makeTest");
|
|
121
120
|
const makeS3 = (config) => Effect.gen(function* () {
|
|
122
121
|
const http = yield* HttpClient.HttpClient;
|
|
123
122
|
let endpoint = config.endpoint ?? `https://s3.${config.region}.amazonaws.com`;
|
|
@@ -161,15 +160,17 @@ const makeS3 = (config) => Effect.gen(function* () {
|
|
|
161
160
|
cause
|
|
162
161
|
})));
|
|
163
162
|
});
|
|
163
|
+
/** Anything outside 2xx is the store refusing; a caller reads `404` first where a miss is an answer. */
|
|
164
|
+
const accepted = (key, status) => status < 200 || status >= 300 ? Effect.fail(new BlobStoreError({
|
|
165
|
+
reason: "refused",
|
|
166
|
+
key,
|
|
167
|
+
status
|
|
168
|
+
})) : Effect.void;
|
|
164
169
|
return {
|
|
165
170
|
get: (key, schema) => Effect.gen(function* () {
|
|
166
171
|
const response = yield* send("GET", key, /* @__PURE__ */ new Uint8Array(0));
|
|
167
172
|
if (response.status === 404) return Option.none();
|
|
168
|
-
|
|
169
|
-
reason: "refused",
|
|
170
|
-
key,
|
|
171
|
-
status: response.status
|
|
172
|
-
}));
|
|
173
|
+
yield* accepted(key, response.status);
|
|
173
174
|
const buffer = yield* response.arrayBuffer.pipe(Effect.mapError((cause) => new BlobStoreError({
|
|
174
175
|
reason: "unreachable",
|
|
175
176
|
key,
|
|
@@ -180,20 +181,12 @@ const makeS3 = (config) => Effect.gen(function* () {
|
|
|
180
181
|
put: (key, blob, schema) => Effect.gen(function* () {
|
|
181
182
|
const framed = yield* Effect.fromResult(BlobEnvelope.encodeResult(blob.metadata, blob.body, schema));
|
|
182
183
|
const response = yield* send("PUT", key, framed);
|
|
183
|
-
|
|
184
|
-
reason: "refused",
|
|
185
|
-
key,
|
|
186
|
-
status: response.status
|
|
187
|
-
}));
|
|
184
|
+
yield* accepted(key, response.status);
|
|
188
185
|
}),
|
|
189
186
|
has: (key) => Effect.gen(function* () {
|
|
190
187
|
const response = yield* send("HEAD", key, /* @__PURE__ */ new Uint8Array(0));
|
|
191
188
|
if (response.status === 404) return false;
|
|
192
|
-
|
|
193
|
-
reason: "refused",
|
|
194
|
-
key,
|
|
195
|
-
status: response.status
|
|
196
|
-
}));
|
|
189
|
+
yield* accepted(key, response.status);
|
|
197
190
|
return true;
|
|
198
191
|
})
|
|
199
192
|
};
|
package/CacheKey.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { sha256, sha256Hex } from "./internal/digest.js";
|
|
1
2
|
import { Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
3
|
import { GlobSet } from "@effected/glob";
|
|
4
|
+
import { descend } from "@effected/walker";
|
|
4
5
|
|
|
5
6
|
//#region src/CacheKey.ts
|
|
6
7
|
/**
|
|
@@ -257,9 +258,11 @@ var CacheKey = class CacheKey extends Schema.Class("CacheKey")(Schema.Struct({
|
|
|
257
258
|
* state.
|
|
258
259
|
*/
|
|
259
260
|
static forBranch(options) {
|
|
260
|
-
const segments = []
|
|
261
|
-
|
|
262
|
-
|
|
261
|
+
const segments = options.os === void 0 ? [options.scope, options.branch] : [
|
|
262
|
+
options.os,
|
|
263
|
+
options.scope,
|
|
264
|
+
options.branch
|
|
265
|
+
];
|
|
263
266
|
if (options.hash !== void 0) segments.push(options.hash);
|
|
264
267
|
return CacheKey.make({ segments });
|
|
265
268
|
}
|
|
@@ -302,7 +305,7 @@ var CacheKey = class CacheKey extends Schema.Class("CacheKey")(Schema.Struct({
|
|
|
302
305
|
*/
|
|
303
306
|
static digest(input, length = 8) {
|
|
304
307
|
if (!Number.isInteger(length) || length < 1 || length > 64) throw new RangeError(`A digest length must be an integer between 1 and 64, got ${length}`);
|
|
305
|
-
return
|
|
308
|
+
return sha256Hex(input).slice(0, length);
|
|
306
309
|
}
|
|
307
310
|
/**
|
|
308
311
|
* Hash a set of files into a single digest.
|
|
@@ -330,30 +333,38 @@ var CacheKey = class CacheKey extends Schema.Class("CacheKey")(Schema.Struct({
|
|
|
330
333
|
const digests = yield* Effect.all(ordered.map((path) => fs.readFile(path).pipe(Effect.mapError((cause) => new CacheKeyReadError({
|
|
331
334
|
path,
|
|
332
335
|
cause
|
|
333
|
-
})), Effect.map(
|
|
334
|
-
|
|
335
|
-
for (const digest of digests) accumulator.update(digest);
|
|
336
|
-
return Option.some(accumulator.digest("hex"));
|
|
336
|
+
})), Effect.map(sha256))), { concurrency: 8 });
|
|
337
|
+
return Option.some(sha256Hex(Uint8Array.from(digests.flatMap((digest) => [...digest]))));
|
|
337
338
|
});
|
|
338
339
|
/**
|
|
339
340
|
* Every file under `workspace` that the pattern set matches.
|
|
340
341
|
*
|
|
341
342
|
* @remarks
|
|
342
343
|
* Discovery and matching are **two different jobs**, and separating them is
|
|
343
|
-
* what makes this testable and correct. The walk is
|
|
344
|
-
* `
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
344
|
+
* what makes this testable and correct. The walk is `@effected/walker`'s
|
|
345
|
+
* `descend`, run once per include from that include's own literal prefix
|
|
346
|
+
* (a literal include is one `stat`, never a walk) with nothing pruned
|
|
347
|
+
* implicitly; the matching is `@effected/glob`'s full minimatch dialect —
|
|
348
|
+
* the same dialect `@actions/glob` uses, so a workflow author's
|
|
349
|
+
* `!**\/node_modules/**` behaves here exactly as it does in every other
|
|
350
|
+
* cache step in the same workflow. `node:fs.globSync` would have welded the
|
|
351
|
+
* two together behind one non-stubbable call *and* changed the dialect, and
|
|
352
|
+
* a dialect divergence surfaces as a silent cache-key difference — the worst
|
|
353
|
+
* failure mode a cache key has.
|
|
354
|
+
*
|
|
355
|
+
* Candidates are matched by their path **relative to the workspace**, and a
|
|
356
|
+
* literal that climbs above it (`../lockfile`) is dropped, which is what
|
|
357
|
+
* makes "never hash a file outside the workspace" structural rather than a
|
|
358
|
+
* rule someone has to remember. Directories are excluded: a directory
|
|
355
359
|
* called `notes.txt` matches `**\/*.txt` and is not a file, and hashing it
|
|
356
|
-
* would fail rather than being ignored.
|
|
360
|
+
* would fail rather than being ignored. An absent literal is a miss; any
|
|
361
|
+
* other failure to read one is a typed `CacheKeyReadError`, because a key
|
|
362
|
+
* derived from an incomplete file set is wrong in a way nothing reports.
|
|
363
|
+
*
|
|
364
|
+
* **One knowing divergence from the runner's `hashFiles()`:** `descend`
|
|
365
|
+
* never enters a symlinked directory (cycle safety), where `@actions/glob`
|
|
366
|
+
* follows links by default. A file reachable only through a symlinked
|
|
367
|
+
* directory does not contribute to the key.
|
|
357
368
|
*
|
|
358
369
|
* The answer is sorted, so a caller cannot make its key depend on the order
|
|
359
370
|
* the filesystem happened to report.
|
|
@@ -361,24 +372,38 @@ var CacheKey = class CacheKey extends Schema.Class("CacheKey")(Schema.Struct({
|
|
|
361
372
|
static matchingFiles = Effect.fn("CacheKey.matchingFiles")(function* (options) {
|
|
362
373
|
const fs = yield* FileSystem.FileSystem;
|
|
363
374
|
const path = yield* Path.Path;
|
|
375
|
+
const { workspace } = options;
|
|
364
376
|
const set = yield* GlobSet.compile(options.patterns).pipe(Effect.mapError((cause) => new CacheKeyBadPatternError({
|
|
365
377
|
pattern: cause.pattern,
|
|
366
378
|
cause
|
|
367
379
|
})));
|
|
368
|
-
|
|
369
|
-
path:
|
|
380
|
+
yield* fs.stat(workspace).pipe(Effect.mapError((cause) => new CacheKeyReadError({
|
|
381
|
+
path: workspace,
|
|
370
382
|
cause
|
|
371
383
|
})));
|
|
372
|
-
const
|
|
373
|
-
for (const
|
|
374
|
-
|
|
375
|
-
const
|
|
376
|
-
if (
|
|
377
|
-
|
|
384
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
385
|
+
for (const literal of set.literals) {
|
|
386
|
+
const target = path.join(workspace, literal);
|
|
387
|
+
const relative = path.relative(workspace, target);
|
|
388
|
+
if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) continue;
|
|
389
|
+
const info = yield* fs.stat(target).pipe(Effect.map(Option.some), Effect.catchIf((error) => error.reason._tag === "NotFound", () => Effect.succeedNone), Effect.mapError((cause) => new CacheKeyReadError({
|
|
390
|
+
path: target,
|
|
391
|
+
cause
|
|
392
|
+
})));
|
|
393
|
+
if (Option.isSome(info) && info.value.type === "File") candidates.add(literal);
|
|
394
|
+
}
|
|
395
|
+
for (const wildcard of set.wildcards) {
|
|
396
|
+
if (wildcard.negated) continue;
|
|
397
|
+
const found = yield* descend(wildcard, {
|
|
398
|
+
cwd: workspace,
|
|
399
|
+
prune: []
|
|
400
|
+
}).pipe(Effect.mapError((cause) => new CacheKeyReadError({
|
|
401
|
+
path: path.join(workspace, cause.path),
|
|
378
402
|
cause
|
|
379
|
-
})))
|
|
403
|
+
})));
|
|
404
|
+
for (const match of found) candidates.add(match);
|
|
380
405
|
}
|
|
381
|
-
return
|
|
406
|
+
return [...candidates].filter((candidate) => set.matches(candidate)).sort().map((candidate) => path.join(workspace, candidate));
|
|
382
407
|
});
|
|
383
408
|
/**
|
|
384
409
|
* Hash every file under `workspace` that the pattern set matches.
|
package/CheckDocument.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unstubbed } from "./internal/unstubbed.js";
|
|
1
2
|
import { CheckState } from "./CheckState.js";
|
|
2
3
|
import { ManagedDocument } from "./ManagedDocument.js";
|
|
3
4
|
import { Clock, Context, Duration, Effect, Latch, Layer, Ref, Schema, Semaphore } from "effect";
|
|
@@ -28,6 +29,12 @@ var CheckReport = class extends Schema.Class("CheckReport")({
|
|
|
28
29
|
/** Where the check's name should link. */
|
|
29
30
|
url: Schema.optionalKey(Schema.String)
|
|
30
31
|
}) {};
|
|
32
|
+
/** What each {@link CheckDocumentError} kind reads as. */
|
|
33
|
+
const KIND_PROSE = {
|
|
34
|
+
render: "The check document could not be regenerated from the reported state",
|
|
35
|
+
read: "Reading the check document's current text failed",
|
|
36
|
+
sink: "Writing the check document failed"
|
|
37
|
+
};
|
|
31
38
|
/**
|
|
32
39
|
* Raised when the check document cannot be regenerated, read back or written.
|
|
33
40
|
*
|
|
@@ -53,7 +60,7 @@ var CheckDocumentError = class extends Schema.TaggedError()("CheckDocumentError"
|
|
|
53
60
|
cause: Schema.optionalKey(Schema.Defect())
|
|
54
61
|
}) {
|
|
55
62
|
get message() {
|
|
56
|
-
return this.kind
|
|
63
|
+
return KIND_PROSE[this.kind];
|
|
57
64
|
}
|
|
58
65
|
};
|
|
59
66
|
/**
|
|
@@ -90,12 +97,13 @@ var CheckDocumentStamp = class extends Schema.Class("CheckDocumentStamp")({
|
|
|
90
97
|
return compareRunId(incoming.runId, existing.runId) >= 0;
|
|
91
98
|
}
|
|
92
99
|
};
|
|
100
|
+
/** A total order over two comparable values. */
|
|
101
|
+
const order = (left, right) => left === right ? 0 : left < right ? -1 : 1;
|
|
93
102
|
/** Epoch order when both sides parse as dates, lexical otherwise. */
|
|
94
103
|
const compareAt = (left, right) => {
|
|
95
104
|
const leftEpoch = Date.parse(left);
|
|
96
105
|
const rightEpoch = Date.parse(right);
|
|
97
|
-
|
|
98
|
-
return left === right ? 0 : left < right ? -1 : 1;
|
|
106
|
+
return Number.isNaN(leftEpoch) || Number.isNaN(rightEpoch) ? order(left, right) : order(leftEpoch, rightEpoch);
|
|
99
107
|
};
|
|
100
108
|
/**
|
|
101
109
|
* Numeric order when both sides are non-blank finite numbers, lexical
|
|
@@ -112,9 +120,9 @@ const compareRunId = (left, right) => {
|
|
|
112
120
|
if (left.trim() !== "" && right.trim() !== "") {
|
|
113
121
|
const leftNumber = Number(left);
|
|
114
122
|
const rightNumber = Number(right);
|
|
115
|
-
if (Number.isFinite(leftNumber) && Number.isFinite(rightNumber)) return leftNumber
|
|
123
|
+
if (Number.isFinite(leftNumber) && Number.isFinite(rightNumber)) return order(leftNumber, rightNumber);
|
|
116
124
|
}
|
|
117
|
-
return left
|
|
125
|
+
return order(left, right);
|
|
118
126
|
};
|
|
119
127
|
/** The stamp a document's own regions carry, when they carry one. */
|
|
120
128
|
const stampOf = (meta) => {
|
|
@@ -293,16 +301,15 @@ var CheckDocument = class CheckDocument extends Context.Service()("@effected/git
|
|
|
293
301
|
}
|
|
294
302
|
/** An inert double; unstubbed members die naming themselves. */
|
|
295
303
|
static makeTest = (overrides = {}) => ({
|
|
296
|
-
report:
|
|
297
|
-
checks:
|
|
298
|
-
flush:
|
|
304
|
+
report: () => dies("report"),
|
|
305
|
+
checks: dies("checks"),
|
|
306
|
+
flush: dies("flush"),
|
|
307
|
+
...overrides
|
|
299
308
|
});
|
|
300
309
|
/** {@link CheckDocument.makeTest} behind a `Layer`. */
|
|
301
310
|
static layerTest = (overrides = {}) => Layer.succeed(CheckDocument, CheckDocument.makeTest(overrides));
|
|
302
311
|
};
|
|
303
|
-
const
|
|
304
|
-
throw new Error(`CheckDocument.makeTest: ${member}() was called but not stubbed — pass an override.`);
|
|
305
|
-
};
|
|
312
|
+
const dies = unstubbed("CheckDocument.makeTest");
|
|
306
313
|
|
|
307
314
|
//#endregion
|
|
308
315
|
export { CheckDocument, CheckDocumentError, CheckDocumentStamp, CheckReport };
|
package/DetachedProcess.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { unstubbed } from "./internal/unstubbed.js";
|
|
2
|
+
import { Effect, Schedule, Schema } from "effect";
|
|
2
3
|
import { HttpClient } from "effect/unstable/http";
|
|
3
4
|
import { ChildProcessSpawner } from "effect/unstable/process";
|
|
4
5
|
import { spawn } from "node:child_process";
|
|
@@ -116,9 +117,7 @@ const ProcessId = Schema.Number.pipe(Schema.check(Schema.makeFilter((value) => N
|
|
|
116
117
|
/** Whether a thrown value is a Node errno error with the given code. */
|
|
117
118
|
const isErrno = (cause, code) => typeof cause === "object" && cause !== null && cause.code === code;
|
|
118
119
|
/** See {@link DetachedProcess.makeTestOps}: an unstubbed member dies naming itself. */
|
|
119
|
-
const
|
|
120
|
-
throw new Error(`DetachedProcess.makeTestOps: ${member}() was called but not stubbed — pass a \`${member}\` override.`);
|
|
121
|
-
};
|
|
120
|
+
const dies = unstubbed("DetachedProcess.makeTestOps");
|
|
122
121
|
/**
|
|
123
122
|
* A long-lived child that outlives the phase that started it.
|
|
124
123
|
*
|
|
@@ -213,14 +212,11 @@ var DetachedProcess = class DetachedProcess {
|
|
|
213
212
|
* timeout six seconds later with nothing to show for it.
|
|
214
213
|
*/
|
|
215
214
|
static awaitReady = Effect.fn("DetachedProcess.awaitReady")(function* (probe, options) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
return Effect.flatMap(Effect.sleep(interval), () => poll(remaining - 1));
|
|
222
|
-
});
|
|
223
|
-
return yield* poll(attempts);
|
|
215
|
+
if (!(yield* Effect.repeat(probe, {
|
|
216
|
+
schedule: Schedule.spaced(options?.interval ?? "150 millis"),
|
|
217
|
+
times: options?.attempts ?? 40,
|
|
218
|
+
until: (answer) => answer
|
|
219
|
+
}))) return yield* Effect.fail(new DetachedNotReadyError({}));
|
|
224
220
|
});
|
|
225
221
|
/**
|
|
226
222
|
* A readiness probe over HTTP: `true` when a `GET` answers 2xx.
|
|
@@ -310,9 +306,9 @@ var DetachedProcess = class DetachedProcess {
|
|
|
310
306
|
* production form signals a real pid.
|
|
311
307
|
*/
|
|
312
308
|
static makeTestOps = (overrides = {}) => ({
|
|
313
|
-
spawn: () =>
|
|
314
|
-
awaitReady: () =>
|
|
315
|
-
reap: () =>
|
|
309
|
+
spawn: () => dies("spawn"),
|
|
310
|
+
awaitReady: () => dies("awaitReady"),
|
|
311
|
+
reap: () => dies("reap"),
|
|
316
312
|
...overrides
|
|
317
313
|
});
|
|
318
314
|
};
|
package/DryRun.js
CHANGED
|
@@ -32,15 +32,6 @@ const make = (enabled) => ({
|
|
|
32
32
|
* @public
|
|
33
33
|
*/
|
|
34
34
|
var DryRun = class DryRun extends Context.Service()("@effected/github-actions/DryRun") {
|
|
35
|
-
/**
|
|
36
|
-
* Driven by the `dry-run` action input, defaulting to a real run.
|
|
37
|
-
*
|
|
38
|
-
* @remarks
|
|
39
|
-
* Fails with a `ConfigError` when the input is present but is not a YAML 1.2
|
|
40
|
-
* core-schema boolean — a workflow that writes `dry-run: yes` should stop,
|
|
41
|
-
* not quietly perform the mutations it meant to rehearse.
|
|
42
|
-
*/
|
|
43
|
-
static layer = Layer.effect(this, Effect.map(ActionInput.boolean(DEFAULT_INPUT).pipe(Config.withDefault(false)), make));
|
|
44
35
|
/**
|
|
45
36
|
* Driven by a named input.
|
|
46
37
|
*
|
|
@@ -50,6 +41,15 @@ var DryRun = class DryRun extends Context.Service()("@effected/github-actions/Dr
|
|
|
50
41
|
* composition site.
|
|
51
42
|
*/
|
|
52
43
|
static layerFromInput = (name) => Layer.effect(DryRun, Effect.map(ActionInput.boolean(name).pipe(Config.withDefault(false)), make));
|
|
44
|
+
/**
|
|
45
|
+
* Driven by the `dry-run` action input, defaulting to a real run.
|
|
46
|
+
*
|
|
47
|
+
* @remarks
|
|
48
|
+
* Fails with a `ConfigError` when the input is present but is not a YAML 1.2
|
|
49
|
+
* core-schema boolean — a workflow that writes `dry-run: yes` should stop,
|
|
50
|
+
* not quietly perform the mutations it meant to rehearse.
|
|
51
|
+
*/
|
|
52
|
+
static layer = DryRun.layerFromInput(DEFAULT_INPUT);
|
|
53
53
|
/** Driven by an explicit decision the caller has already made. */
|
|
54
54
|
static layerFrom = (enabled) => Layer.succeed(DryRun, make(enabled));
|
|
55
55
|
/**
|
package/GitHubMarkdown.js
CHANGED
|
@@ -159,12 +159,12 @@ var GitHubMarkdown = class GitHubMarkdown {
|
|
|
159
159
|
const columns = Object.keys(schema.fields).map((key) => {
|
|
160
160
|
const field = schema.fields[key];
|
|
161
161
|
const column = overrides[key];
|
|
162
|
-
const
|
|
163
|
-
const
|
|
162
|
+
const encode = Schema.encodeSync(field);
|
|
163
|
+
const project = column?.format ?? ((value) => encode(value) ?? "");
|
|
164
164
|
return {
|
|
165
165
|
key,
|
|
166
166
|
header: column?.header ?? SchemaAST.resolveTitle(field.ast) ?? key,
|
|
167
|
-
cell: (value) => value === void 0 ? "" :
|
|
167
|
+
cell: (value) => value === void 0 ? "" : project(value)
|
|
168
168
|
};
|
|
169
169
|
});
|
|
170
170
|
const headers = columns.map((column) => column.header);
|
package/GitHubToken.js
CHANGED
|
@@ -160,7 +160,7 @@ var GitHubToken = class GitHubToken {
|
|
|
160
160
|
static read = Effect.fn("GitHubToken.read")(function* (options = {}) {
|
|
161
161
|
const token = yield* (yield* ActionState).get(options.stateKey ?? DEFAULT_KEY, InstallationToken);
|
|
162
162
|
const now = yield* DateTime.now;
|
|
163
|
-
if (
|
|
163
|
+
if (token.isExpired(DateTime.toEpochMillis(now), options.skew)) return yield* Effect.fail(new GitHubTokenError({
|
|
164
164
|
reason: "expired",
|
|
165
165
|
expiresAt: DateTime.formatIso(token.expiresAt)
|
|
166
166
|
}));
|
package/ManagedDocument.js
CHANGED
|
@@ -83,6 +83,12 @@ const STRUCTURAL_KIND = {
|
|
|
83
83
|
overlappingSections: "overlappingRegions",
|
|
84
84
|
duplicateSection: "duplicateRegion"
|
|
85
85
|
};
|
|
86
|
+
/** A structurally corrupt region layout, in this surface's vocabulary. */
|
|
87
|
+
const structural = (failure) => new ManagedDocumentError({
|
|
88
|
+
kind: STRUCTURAL_KIND[failure.reason],
|
|
89
|
+
line: failure.line,
|
|
90
|
+
...failure.key === void 0 ? {} : { key: failure.key }
|
|
91
|
+
});
|
|
86
92
|
/**
|
|
87
93
|
* A marker-delimited document: named regions a tool owns, inside text a human
|
|
88
94
|
* may also edit.
|
|
@@ -145,21 +151,12 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
|
|
|
145
151
|
* a silent choice would destroy content a human wrote.
|
|
146
152
|
*/
|
|
147
153
|
static parseResult(source) {
|
|
148
|
-
const
|
|
149
|
-
const scanned = SectionDocument.parseResult(text, REGION_DIALECT);
|
|
150
|
-
if (Result.isFailure(scanned)) {
|
|
151
|
-
const failure = scanned.failure;
|
|
152
|
-
return Result.fail(new ManagedDocumentError({
|
|
153
|
-
kind: STRUCTURAL_KIND[failure.reason],
|
|
154
|
-
line: failure.line,
|
|
155
|
-
...failure.key === void 0 ? {} : { key: failure.key }
|
|
156
|
-
}));
|
|
157
|
-
}
|
|
158
|
-
return Result.succeed(ManagedDocument.make({
|
|
154
|
+
const document = ManagedDocument.make({
|
|
159
155
|
namespace: source.namespace,
|
|
160
156
|
key: source.key,
|
|
161
|
-
text
|
|
162
|
-
})
|
|
157
|
+
text: source.text ?? ""
|
|
158
|
+
});
|
|
159
|
+
return Result.map(Result.mapError(document.scan(), structural), () => document);
|
|
163
160
|
}
|
|
164
161
|
/**
|
|
165
162
|
* Read a document out of existing text, in `Effect`.
|
|
@@ -179,8 +176,7 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
|
|
|
179
176
|
}
|
|
180
177
|
/** The content of one region, if the document carries it. */
|
|
181
178
|
region(key) {
|
|
182
|
-
|
|
183
|
-
return found === void 0 ? Option.none() : Option.some(found.content);
|
|
179
|
+
return Option.map(this.entry(key), (found) => found.content);
|
|
184
180
|
}
|
|
185
181
|
/**
|
|
186
182
|
* One region's content and metadata together, if the document carries it.
|
|
@@ -235,15 +231,8 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
|
|
|
235
231
|
* surfacing in the typed channel.
|
|
236
232
|
*/
|
|
237
233
|
withRegionsResult(entries) {
|
|
238
|
-
const parsed =
|
|
239
|
-
if (Result.isFailure(parsed))
|
|
240
|
-
const failure = parsed.failure;
|
|
241
|
-
return Result.fail(new ManagedDocumentError({
|
|
242
|
-
kind: STRUCTURAL_KIND[failure.reason],
|
|
243
|
-
line: failure.line,
|
|
244
|
-
...failure.key === void 0 ? {} : { key: failure.key }
|
|
245
|
-
}));
|
|
246
|
-
}
|
|
234
|
+
const parsed = this.scan();
|
|
235
|
+
if (Result.isFailure(parsed)) return Result.fail(structural(parsed.failure));
|
|
247
236
|
const declared = entries.map(([key, content, meta]) => SectionId.make({
|
|
248
237
|
key: this.wireKey(key),
|
|
249
238
|
commentStyle: CommentStyle.html
|
|
@@ -275,14 +264,22 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
|
|
|
275
264
|
withRegions(entries) {
|
|
276
265
|
return Effect.fromResult(this.withRegionsResult(entries));
|
|
277
266
|
}
|
|
267
|
+
#scanned;
|
|
268
|
+
scan() {
|
|
269
|
+
this.#scanned ??= SectionDocument.parseResult(this.text, REGION_DIALECT);
|
|
270
|
+
return this.#scanned;
|
|
271
|
+
}
|
|
272
|
+
/** What every wire key of this document's regions begins with. */
|
|
273
|
+
get wirePrefix() {
|
|
274
|
+
return `${this.namespace}.${this.key}.`;
|
|
275
|
+
}
|
|
278
276
|
/** The marker key a region is written under: `namespace.key.region`. */
|
|
279
277
|
wireKey(region) {
|
|
280
|
-
return `${this.
|
|
278
|
+
return `${this.wirePrefix}${region}`;
|
|
281
279
|
}
|
|
282
280
|
/** The region key back out of a wire key, for error reporting. */
|
|
283
281
|
regionKeyOf(wireKey) {
|
|
284
|
-
|
|
285
|
-
return wireKey.startsWith(prefix) ? wireKey.slice(prefix.length) : wireKey;
|
|
282
|
+
return wireKey.startsWith(this.wirePrefix) ? wireKey.slice(this.wirePrefix.length) : wireKey;
|
|
286
283
|
}
|
|
287
284
|
/**
|
|
288
285
|
* This document's own regions, in document order.
|
|
@@ -296,9 +293,9 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
|
|
|
296
293
|
* never reported.
|
|
297
294
|
*/
|
|
298
295
|
ownRegions() {
|
|
299
|
-
const parsed =
|
|
296
|
+
const parsed = this.scan();
|
|
300
297
|
if (Result.isFailure(parsed)) return [];
|
|
301
|
-
const prefix =
|
|
298
|
+
const prefix = this.wirePrefix;
|
|
302
299
|
return parsed.success.sections.filter((placed) => placed.section.key.startsWith(prefix)).map((placed) => ({
|
|
303
300
|
key: placed.section.key.slice(prefix.length),
|
|
304
301
|
content: placed.section.content,
|
package/OidcTokenIssuer.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ActionEnvironment } from "./ActionEnvironment.js";
|
|
2
|
+
import { unstubbed } from "./internal/unstubbed.js";
|
|
3
|
+
import { payloadOf, unsignedJwt } from "./internal/jwt.js";
|
|
2
4
|
import { Context, Effect, Layer, Redacted, Schema } from "effect";
|
|
3
|
-
import { HttpClient } from "effect/unstable/http";
|
|
5
|
+
import { HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
4
6
|
import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
|
|
5
7
|
|
|
6
8
|
//#region src/OidcTokenIssuer.ts
|
|
@@ -89,18 +91,6 @@ const TokenEnvelope = Schema.Struct({
|
|
|
89
91
|
const REQUEST_TOKEN = "ACTIONS_ID_TOKEN_REQUEST_TOKEN";
|
|
90
92
|
const REQUEST_URL = "ACTIONS_ID_TOKEN_REQUEST_URL";
|
|
91
93
|
/**
|
|
92
|
-
* Decode a JWT payload segment.
|
|
93
|
-
*
|
|
94
|
-
* @remarks
|
|
95
|
-
* `base64url` states the intent, though it is not what makes this work: Node's
|
|
96
|
-
* plain `base64` decoder happens to accept the url alphabet and unpadded input
|
|
97
|
-
* too (probed at beta.101), so the two are interchangeable *here*. They are not
|
|
98
|
-
* interchangeable everywhere — `atob` is not forgiving — which is why the
|
|
99
|
-
* declaration is the strict one and there is a test whose payload actually
|
|
100
|
-
* contains `-` and `_`.
|
|
101
|
-
*/
|
|
102
|
-
const decodeSegment = (segment) => JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
|
|
103
|
-
/**
|
|
104
94
|
* Read the claims out of a JWT **without verifying its signature**.
|
|
105
95
|
*
|
|
106
96
|
* @remarks
|
|
@@ -108,20 +98,14 @@ const decodeSegment = (segment) => JSON.parse(Buffer.from(segment, "base64url").
|
|
|
108
98
|
* {@link OidcTokenIssuerShape.claims}; do not "fix" it here.
|
|
109
99
|
*/
|
|
110
100
|
const readClaims = (token) => Effect.gen(function* () {
|
|
111
|
-
const
|
|
112
|
-
const payload = parts[1];
|
|
113
|
-
if (parts.length !== 3 || payload === void 0 || payload === "") return yield* Effect.fail(new OidcTokenError({
|
|
101
|
+
const decoded = yield* Effect.fromResult(payloadOf(token)).pipe(Effect.mapError((failure) => failure.kind === "segments" ? new OidcTokenError({
|
|
114
102
|
reason: "malformedToken",
|
|
115
|
-
detail:
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
detail: "the payload is not base64url JSON",
|
|
122
|
-
cause
|
|
123
|
-
})
|
|
124
|
-
});
|
|
103
|
+
detail: failure.detail
|
|
104
|
+
}) : new OidcTokenError({
|
|
105
|
+
reason: "malformedToken",
|
|
106
|
+
detail: failure.detail,
|
|
107
|
+
cause: failure.cause
|
|
108
|
+
})));
|
|
125
109
|
return yield* Schema.decodeUnknownEffect(OidcClaims)(decoded).pipe(Effect.mapError((cause) => new OidcTokenError({
|
|
126
110
|
reason: "missingClaims",
|
|
127
111
|
cause
|
|
@@ -150,10 +134,7 @@ const make = Effect.gen(function* () {
|
|
|
150
134
|
const bearer = yield* required(REQUEST_TOKEN);
|
|
151
135
|
const base = yield* required(REQUEST_URL);
|
|
152
136
|
const url = audience === void 0 ? base : `${base}&audience=${encodeURIComponent(audience)}`;
|
|
153
|
-
const response = yield* http.get(url,
|
|
154
|
-
authorization: `Bearer ${bearer}`,
|
|
155
|
-
accept: "application/json"
|
|
156
|
-
} }).pipe(Effect.mapError((cause) => new OidcTokenError({
|
|
137
|
+
const response = yield* http.execute(HttpClientRequest.get(url).pipe(HttpClientRequest.bearerToken(bearer), HttpClientRequest.acceptJson)).pipe(Effect.mapError((cause) => new OidcTokenError({
|
|
157
138
|
reason: "requestFailed",
|
|
158
139
|
cause
|
|
159
140
|
})));
|
|
@@ -173,9 +154,7 @@ const make = Effect.gen(function* () {
|
|
|
173
154
|
})
|
|
174
155
|
};
|
|
175
156
|
});
|
|
176
|
-
const
|
|
177
|
-
throw new Error(`OidcTokenIssuer.makeTest: ${member}() was called but not stubbed — pass a \`${member}\` override.`);
|
|
178
|
-
};
|
|
157
|
+
const dies = unstubbed("OidcTokenIssuer.makeTest");
|
|
179
158
|
/**
|
|
180
159
|
* The runner's OIDC token service.
|
|
181
160
|
*
|
|
@@ -211,17 +190,14 @@ var OidcTokenIssuer = class OidcTokenIssuer extends Context.Service()("@effected
|
|
|
211
190
|
* provenance path structurally unreachable in the source package, drawing
|
|
212
191
|
* four separate apologetic comments from one consumer.
|
|
213
192
|
*/
|
|
214
|
-
static unsignedTokenFor = (claims) => {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
typ: "JWT"
|
|
219
|
-
})}.${segment(Schema.encodeUnknownSync(OidcClaims)(claims))}.unsigned`);
|
|
220
|
-
};
|
|
193
|
+
static unsignedTokenFor = (claims) => Redacted.make(unsignedJwt({
|
|
194
|
+
alg: "RS256",
|
|
195
|
+
typ: "JWT"
|
|
196
|
+
}, Schema.encodeUnknownSync(OidcClaims)(claims)));
|
|
221
197
|
/** A test double. Unstubbed members die rather than answering with a non-token. */
|
|
222
198
|
static makeTest = (overrides = {}) => ({
|
|
223
|
-
token: () =>
|
|
224
|
-
claims: () =>
|
|
199
|
+
token: () => dies("token"),
|
|
200
|
+
claims: () => dies("claims"),
|
|
225
201
|
...overrides
|
|
226
202
|
});
|
|
227
203
|
/** {@link OidcTokenIssuer.makeTest} behind `Layer.succeed`. */
|