@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
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { ActionEnvironment } from "./ActionEnvironment.js";
|
|
2
|
+
import { unstubbed } from "./internal/unstubbed.js";
|
|
3
|
+
import { digestFileHex } from "./internal/digest.js";
|
|
4
|
+
import { isWindowsRunner, toolCacheRoot } from "./internal/runner.js";
|
|
5
|
+
import { typeAt } from "./internal/fsProbe.js";
|
|
2
6
|
import { ToolInstaller } from "./ToolInstaller.js";
|
|
3
|
-
import { Context, Effect, FileSystem, Layer, Option, Path, Schema
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
7
|
+
import { Context, Effect, FileSystem, Layer, Option, Path, Schema } from "effect";
|
|
5
8
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
6
|
-
import { PackageManagerPinName } from "@effected/npm";
|
|
9
|
+
import { DEFAULT_REGISTRY, PackageManagerPinName } from "@effected/npm";
|
|
7
10
|
|
|
8
11
|
//#region src/PackageManagerInstaller.ts
|
|
9
12
|
/**
|
|
@@ -130,7 +133,6 @@ var CachedPackageManager = class extends Schema.Class("CachedPackageManager")({
|
|
|
130
133
|
* @public
|
|
131
134
|
*/
|
|
132
135
|
const InstalledPackageManager = Schema.Union([AmbientPackageManager, CachedPackageManager]);
|
|
133
|
-
const DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
134
136
|
/** The shim directory name inside a cached npm-registry-manager entry. */
|
|
135
137
|
const SHIM_DIR = ".bin";
|
|
136
138
|
/**
|
|
@@ -149,9 +151,18 @@ const cmdShim = (target) => `@echo off\r\nnode "${target}" %*\r\n`;
|
|
|
149
151
|
* bun's spelling (`macOS` → `darwin`) and the Node arch spelling to bun's
|
|
150
152
|
* (`arm64` → `aarch64`).
|
|
151
153
|
*/
|
|
154
|
+
const BUN_OS = {
|
|
155
|
+
linux: "linux",
|
|
156
|
+
macos: "darwin",
|
|
157
|
+
windows: "windows"
|
|
158
|
+
};
|
|
159
|
+
const BUN_CPU = {
|
|
160
|
+
x64: "x64",
|
|
161
|
+
arm64: "aarch64"
|
|
162
|
+
};
|
|
152
163
|
const bunTarget = (runnerOs, arch) => {
|
|
153
|
-
const os = runnerOs.toLowerCase()
|
|
154
|
-
const cpu = arch
|
|
164
|
+
const os = BUN_OS[runnerOs.toLowerCase()];
|
|
165
|
+
const cpu = BUN_CPU[arch];
|
|
155
166
|
return os === void 0 || cpu === void 0 ? Option.none() : Option.some(`bun-${os}-${cpu}`);
|
|
156
167
|
};
|
|
157
168
|
/**
|
|
@@ -174,7 +185,10 @@ const normalizeBins = (bin, fallbackName) => {
|
|
|
174
185
|
return Object.keys(entries).length === 0 ? Option.none() : Option.some(entries);
|
|
175
186
|
};
|
|
176
187
|
/** Map a `RUNNER_ARCH` value (`X64`, `ARM64`) onto the Node arch spelling. */
|
|
177
|
-
const archFromRunner = (runnerArch) =>
|
|
188
|
+
const archFromRunner = (runnerArch) => {
|
|
189
|
+
const lowered = runnerArch.toLowerCase();
|
|
190
|
+
return lowered === "x64" || lowered === "arm64" ? lowered : runnerArch;
|
|
191
|
+
};
|
|
178
192
|
const make = Effect.gen(function* () {
|
|
179
193
|
const env = yield* ActionEnvironment;
|
|
180
194
|
const fs = yield* FileSystem.FileSystem;
|
|
@@ -186,11 +200,11 @@ const make = Effect.gen(function* () {
|
|
|
186
200
|
onNone: () => process.arch,
|
|
187
201
|
onSome: archFromRunner
|
|
188
202
|
}));
|
|
189
|
-
const windows =
|
|
203
|
+
const windows = yield* isWindowsRunner(env);
|
|
190
204
|
const bunBinaryName = windows ? "bun.exe" : "bun";
|
|
191
205
|
const shimFileName = (name) => windows ? `${name}.cmd` : name;
|
|
192
206
|
const shimBody = windows ? cmdShim : posixShim;
|
|
193
|
-
const cacheRoot = yield*
|
|
207
|
+
const cacheRoot = yield* toolCacheRoot(env, path);
|
|
194
208
|
const finalCachePath = (tool, version) => ToolInstaller.cachePath({
|
|
195
209
|
root: cacheRoot,
|
|
196
210
|
tool,
|
|
@@ -209,16 +223,11 @@ const make = Effect.gen(function* () {
|
|
|
209
223
|
cause
|
|
210
224
|
});
|
|
211
225
|
/** The hex digest of a file's bytes, streamed rather than buffered. */
|
|
212
|
-
const hashFile = (pin, file, algorithm) => Effect.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
reason: "integrityMismatch",
|
|
218
|
-
subject: file,
|
|
219
|
-
cause
|
|
220
|
-
})));
|
|
221
|
-
});
|
|
226
|
+
const hashFile = (pin, file, algorithm) => digestFileHex(fs, file, algorithm).pipe(Effect.mapError((cause) => errorFor(pin)({
|
|
227
|
+
reason: "integrityMismatch",
|
|
228
|
+
subject: file,
|
|
229
|
+
cause
|
|
230
|
+
})));
|
|
222
231
|
/**
|
|
223
232
|
* Verify a downloaded artifact against the pin's integrity, fail-closed.
|
|
224
233
|
*
|
|
@@ -250,8 +259,7 @@ const make = Effect.gen(function* () {
|
|
|
250
259
|
*/
|
|
251
260
|
const ambientNpmVersion = Effect.map(Effect.result(spawner.string(ChildProcess.make("npm", ["--version"]))), (result) => result._tag === "Success" ? Option.some(result.success.trim()) : Option.none());
|
|
252
261
|
const assertFile = (pin, file, subject) => Effect.gen(function* () {
|
|
253
|
-
|
|
254
|
-
if (stat._tag !== "Success" || stat.success.type !== "File") return yield* Effect.fail(errorFor(pin)({
|
|
262
|
+
if ((yield* typeAt(fs, file)) !== "File") return yield* Effect.fail(errorFor(pin)({
|
|
255
263
|
reason: "layoutUnexpected",
|
|
256
264
|
subject
|
|
257
265
|
}));
|
|
@@ -319,8 +327,7 @@ const make = Effect.gen(function* () {
|
|
|
319
327
|
for (const [name, relative] of Object.entries(bins)) {
|
|
320
328
|
const shim = path.join(shimDir, shimFileName(name));
|
|
321
329
|
if (options.skipExisting) {
|
|
322
|
-
|
|
323
|
-
if (present._tag === "Success" && present.success.type === "File") continue;
|
|
330
|
+
if ((yield* typeAt(fs, shim)) === "File") continue;
|
|
324
331
|
}
|
|
325
332
|
yield* fs.writeFileString(shim, shimBody(path.join(finalDirectory, relative))).pipe(Effect.mapError(cacheError(shim)));
|
|
326
333
|
if (!windows) yield* fs.chmod(shim, 493).pipe(Effect.mapError(cacheError(shim)));
|
|
@@ -386,8 +393,7 @@ const make = Effect.gen(function* () {
|
|
|
386
393
|
if (!berry) yield* verifyIntegrity(pin, archive);
|
|
387
394
|
const extracted = yield* installer.extractTar(archive).pipe(Effect.mapError(fromInstaller(pin)));
|
|
388
395
|
const packageDir = path.join(extracted, "package");
|
|
389
|
-
|
|
390
|
-
if (stat._tag !== "Success" || stat.success.type !== "Directory") return yield* Effect.fail(errorFor(pin)({
|
|
396
|
+
if ((yield* typeAt(fs, packageDir)) !== "Directory") return yield* Effect.fail(errorFor(pin)({
|
|
391
397
|
reason: "layoutUnexpected",
|
|
392
398
|
subject: "tarball has no package/ root"
|
|
393
399
|
}));
|
|
@@ -437,9 +443,7 @@ const make = Effect.gen(function* () {
|
|
|
437
443
|
return yield* installFromRegistry(pin, pin.name, registry);
|
|
438
444
|
}) };
|
|
439
445
|
});
|
|
440
|
-
const
|
|
441
|
-
throw new Error(`PackageManagerInstaller.makeTest: ${member}() was called but not stubbed — pass a \`${member}\` override.`);
|
|
442
|
-
};
|
|
446
|
+
const dies = unstubbed("PackageManagerInstaller.makeTest");
|
|
443
447
|
/**
|
|
444
448
|
* First-class exact-version package-manager provisioning on a GitHub runner.
|
|
445
449
|
*
|
|
@@ -483,7 +487,7 @@ var PackageManagerInstaller = class PackageManagerInstaller extends Context.Serv
|
|
|
483
487
|
static layer = Layer.effect(this, make);
|
|
484
488
|
/** A test double. The unstubbed member dies rather than reporting an install that did not happen. */
|
|
485
489
|
static makeTest = (overrides = {}) => ({
|
|
486
|
-
install: () =>
|
|
490
|
+
install: () => dies("install"),
|
|
487
491
|
...overrides
|
|
488
492
|
});
|
|
489
493
|
/** {@link PackageManagerInstaller.makeTest} behind `Layer.succeed`. */
|
package/ToolInstaller.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { ActionEnvironment } from "./ActionEnvironment.js";
|
|
2
|
+
import { unstubbed } from "./internal/unstubbed.js";
|
|
3
|
+
import { isWindowsRunner, toolCacheRoot } from "./internal/runner.js";
|
|
4
|
+
import { spawnOnce } from "./internal/spawn.js";
|
|
5
|
+
import { typeAt } from "./internal/fsProbe.js";
|
|
2
6
|
import { Context, Effect, FileSystem, Layer, Option, Path, Schedule, Schema, Stream } from "effect";
|
|
3
7
|
import { HttpClient } from "effect/unstable/http";
|
|
4
8
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
@@ -61,11 +65,8 @@ const make = Effect.gen(function* () {
|
|
|
61
65
|
const path = yield* Path.Path;
|
|
62
66
|
const http = yield* HttpClient.HttpClient;
|
|
63
67
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
64
|
-
const root = yield*
|
|
65
|
-
const windows = yield*
|
|
66
|
-
onNone: () => false,
|
|
67
|
-
onSome: (os) => os.toLowerCase() === "windows"
|
|
68
|
-
}));
|
|
68
|
+
const root = yield* toolCacheRoot(env, path);
|
|
69
|
+
const windows = yield* isWindowsRunner(env);
|
|
69
70
|
const cachePath = (tool, version) => ToolInstaller.cachePath({
|
|
70
71
|
root,
|
|
71
72
|
tool,
|
|
@@ -93,30 +94,22 @@ const make = Effect.gen(function* () {
|
|
|
93
94
|
* the difference between "this is not a gzip archive" and "tar is not
|
|
94
95
|
* installed". Discarding it is what makes an extraction failure unactionable.
|
|
95
96
|
*
|
|
96
|
-
* **One spawn, not two — load-bearing
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* `ChildProcessSpawner.make` derives both from `spawn`). Calling them
|
|
100
|
-
* back-to-back double-executed every extraction — harmless for idempotent
|
|
101
|
-
* `tar`/`unzip -o`, but .NET's `ZipFile.ExtractToDirectory` refuses to
|
|
102
|
-
* overwrite, so the second run failed 5/5 on real Windows runners while the
|
|
103
|
-
* captured "complaint" was the FIRST run's silent success. Output and exit
|
|
104
|
-
* code must come from the same `spawn` handle.
|
|
97
|
+
* **One spawn, not two — load-bearing**, and why is spelled once in
|
|
98
|
+
* `internal/spawn.ts`: the double run failed 5/5 on real Windows runners
|
|
99
|
+
* because .NET's `ZipFile.ExtractToDirectory` refuses to overwrite.
|
|
105
100
|
*/
|
|
106
|
-
const extractWith = (command, archive) => Effect.
|
|
107
|
-
const
|
|
101
|
+
const extractWith = (command, archive) => Effect.gen(function* () {
|
|
102
|
+
const { output, code } = yield* spawnOnce(spawner, command).pipe(Effect.mapError((cause) => new ToolInstallerError({
|
|
108
103
|
reason: "extractFailed",
|
|
109
104
|
subject: archive,
|
|
110
105
|
cause
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
const output = yield* Stream.mkString(Stream.decodeText(handle.all)).pipe(Effect.mapError(extractError));
|
|
114
|
-
if ((yield* handle.exitCode.pipe(Effect.mapError(extractError))) !== 0) return yield* Effect.fail(new ToolInstallerError({
|
|
106
|
+
})));
|
|
107
|
+
if (code !== 0) return yield* Effect.fail(new ToolInstallerError({
|
|
115
108
|
reason: "extractFailed",
|
|
116
109
|
subject: archive,
|
|
117
110
|
stderr: output.trim()
|
|
118
111
|
}));
|
|
119
|
-
})
|
|
112
|
+
});
|
|
120
113
|
/**
|
|
121
114
|
* Install a staged directory into the tool cache by renaming it into place.
|
|
122
115
|
*
|
|
@@ -191,7 +184,7 @@ const make = Effect.gen(function* () {
|
|
|
191
184
|
while: (error) => error.retryable
|
|
192
185
|
}));
|
|
193
186
|
});
|
|
194
|
-
const find = (tool, version) => Effect.map(
|
|
187
|
+
const find = (tool, version) => Effect.map(typeAt(fs, cachePath(tool, version)), (type) => type === "Directory" ? Option.some(cachePath(tool, version)) : Option.none());
|
|
195
188
|
const cacheFile = Effect.fn("ToolInstaller.cacheFile")(function* (source, name, tool, version) {
|
|
196
189
|
yield* Effect.annotateCurrentSpan({
|
|
197
190
|
tool,
|
|
@@ -252,8 +245,7 @@ const make = Effect.gen(function* () {
|
|
|
252
245
|
});
|
|
253
246
|
const cached = yield* find(options.tool, options.version);
|
|
254
247
|
if (Option.isSome(cached)) {
|
|
255
|
-
|
|
256
|
-
if (present._tag === "Success" && present.success.type === "File") return {
|
|
248
|
+
if ((yield* typeAt(fs, path.join(cached.value, options.binary))) === "File") return {
|
|
257
249
|
directory: cached.value,
|
|
258
250
|
binDir: cached.value
|
|
259
251
|
};
|
|
@@ -272,9 +264,7 @@ const make = Effect.gen(function* () {
|
|
|
272
264
|
})
|
|
273
265
|
};
|
|
274
266
|
});
|
|
275
|
-
const
|
|
276
|
-
throw new Error(`ToolInstaller.makeTest: ${member}() was called but not stubbed — pass a \`${member}\` override.`);
|
|
277
|
-
};
|
|
267
|
+
const dies = unstubbed("ToolInstaller.makeTest");
|
|
278
268
|
/**
|
|
279
269
|
* Download, extract and cache a toolchain in the runner's tool cache.
|
|
280
270
|
*
|
|
@@ -323,13 +313,13 @@ var ToolInstaller = class ToolInstaller extends Context.Service()("@effected/git
|
|
|
323
313
|
static cachePath = (options) => `${options.root}/${options.tool}/${options.version}/${options.arch}`;
|
|
324
314
|
/** A test double. Unstubbed members die rather than reporting a tool that is not there. */
|
|
325
315
|
static makeTest = (overrides = {}) => ({
|
|
326
|
-
find: () =>
|
|
327
|
-
download: () =>
|
|
328
|
-
extractTar: () =>
|
|
329
|
-
extractZip: () =>
|
|
330
|
-
cacheDir: () =>
|
|
331
|
-
cacheFile: () =>
|
|
332
|
-
provisionFile: () =>
|
|
316
|
+
find: () => dies("find"),
|
|
317
|
+
download: () => dies("download"),
|
|
318
|
+
extractTar: () => dies("extractTar"),
|
|
319
|
+
extractZip: () => dies("extractZip"),
|
|
320
|
+
cacheDir: () => dies("cacheDir"),
|
|
321
|
+
cacheFile: () => dies("cacheFile"),
|
|
322
|
+
provisionFile: () => dies("provisionFile"),
|
|
333
323
|
...overrides
|
|
334
324
|
});
|
|
335
325
|
/** {@link ToolInstaller.makeTest} behind `Layer.succeed`. */
|
package/index.d.ts
CHANGED
|
@@ -586,6 +586,46 @@ export declare class DetachedOutputError extends DetachedOutputError_base {
|
|
|
586
586
|
* @public
|
|
587
587
|
*/
|
|
588
588
|
type ActionOutputError = RunnerFileUnavailableError | RunnerFileWriteError | InvalidOutputNameError | OutputEncodeError | DetachedOutputError;
|
|
589
|
+
declare const RecordedOutput_base: Schema.Class<RecordedOutput, Schema.Struct<{
|
|
590
|
+
/** Which {@link ActionOutputsShape} member was called. */
|
|
591
|
+
readonly member: Schema.Literals<readonly ["set", "setJson", "summary", "exportVariable", "addPath", "setFailed", "setSecret"]>;
|
|
592
|
+
/** The output or variable name, for the members that take one. */
|
|
593
|
+
readonly name: Schema.optionalKey<Schema.String>;
|
|
594
|
+
/** What was published — value, message, path, content or secret — as the runner would have read it. */
|
|
595
|
+
readonly value: Schema.String;
|
|
596
|
+
}>, {}>;
|
|
597
|
+
/**
|
|
598
|
+
* One call an {@link ActionOutputs.recording} double observed.
|
|
599
|
+
*
|
|
600
|
+
* @remarks
|
|
601
|
+
* `value` is always the string the runner would have seen: for `setJson` that
|
|
602
|
+
* is the **encoded** JSON text, after the same schema encode the real layer
|
|
603
|
+
* performs, so a test asserting on it reads exactly what a later step's
|
|
604
|
+
* `steps.<id>.outputs.<name>` expression would. `name` is present for the
|
|
605
|
+
* members that take one (`set`, `setJson`, `exportVariable`) and absent
|
|
606
|
+
* otherwise.
|
|
607
|
+
*
|
|
608
|
+
* @public
|
|
609
|
+
*/
|
|
610
|
+
export declare class RecordedOutput extends RecordedOutput_base {}
|
|
611
|
+
/**
|
|
612
|
+
* The member names a {@link RecordedOutput} can carry.
|
|
613
|
+
*
|
|
614
|
+
* @public
|
|
615
|
+
*/
|
|
616
|
+
type RecordedOutputMember = RecordedOutput["member"];
|
|
617
|
+
/**
|
|
618
|
+
* What {@link ActionOutputs.recording} returns: the layer to provide and the
|
|
619
|
+
* journal it fills.
|
|
620
|
+
*
|
|
621
|
+
* @public
|
|
622
|
+
*/
|
|
623
|
+
interface RecordingOutputs {
|
|
624
|
+
/** An {@link ActionOutputs} whose every member records into this journal. */
|
|
625
|
+
readonly layer: Layer.Layer<ActionOutputs>;
|
|
626
|
+
/** A snapshot of every recorded call so far, in call order. */
|
|
627
|
+
readonly entries: () => ReadonlyArray<RecordedOutput>;
|
|
628
|
+
}
|
|
589
629
|
/**
|
|
590
630
|
* The {@link ActionOutputs} service shape.
|
|
591
631
|
*
|
|
@@ -674,10 +714,49 @@ export declare class ActionOutputs extends ActionOutputs_base {
|
|
|
674
714
|
* channel to fail through.
|
|
675
715
|
*/
|
|
676
716
|
static readonly layerDetached: Layer.Layer<ActionOutputs>;
|
|
677
|
-
/**
|
|
717
|
+
/**
|
|
718
|
+
* A test double. Unstubbed members die rather than silently succeeding.
|
|
719
|
+
*
|
|
720
|
+
* @remarks
|
|
721
|
+
* **`setJson` always encodes first.** The value is encoded through its
|
|
722
|
+
* schema exactly as the real layer does — failing typed with
|
|
723
|
+
* {@link OutputEncodeError} — and only then is a supplied `setJson`
|
|
724
|
+
* override called, with the original `(name, value, schema)`. An override
|
|
725
|
+
* that accepts `schema` and ignores it therefore **cannot disable
|
|
726
|
+
* output-schema checking**: a value/schema drift is a typed failure under
|
|
727
|
+
* this double whether or not the override looks at the schema. Without an
|
|
728
|
+
* override, a valid value still dies unimplemented as every other unstubbed
|
|
729
|
+
* member does. Most tests want neither: reach for
|
|
730
|
+
* {@link ActionOutputs.recording} instead.
|
|
731
|
+
*/
|
|
678
732
|
static readonly makeTest: (overrides?: Partial<ActionOutputsShape>) => ActionOutputsShape;
|
|
679
733
|
/** {@link ActionOutputs.makeTest} behind `Layer.succeed`. */
|
|
680
734
|
static readonly layerTest: (overrides?: Partial<ActionOutputsShape>) => Layer.Layer<ActionOutputs>;
|
|
735
|
+
/**
|
|
736
|
+
* A recording double: every member appends a {@link RecordedOutput} to a
|
|
737
|
+
* journal the test reads back, in call order.
|
|
738
|
+
*
|
|
739
|
+
* @remarks
|
|
740
|
+
* This is the double most tests of an action want — "what did it publish?"
|
|
741
|
+
* — shipped so that nobody hand-writes a `setJson` override that drops the
|
|
742
|
+
* schema encode (the trap `makeTest` now closes too). `setJson` encodes
|
|
743
|
+
* through its schema exactly as the real layer does, failing typed with
|
|
744
|
+
* {@link OutputEncodeError} and recording nothing on a drift, and records
|
|
745
|
+
* the **encoded JSON text** — what the runner would have read — not the
|
|
746
|
+
* decoded value. `set`, `exportVariable` and `setJson` refuse an unusable
|
|
747
|
+
* name with {@link InvalidOutputNameError}, as the real layer does, rather
|
|
748
|
+
* than recording it.
|
|
749
|
+
*
|
|
750
|
+
* `setSecret` records the secret's **plaintext** under
|
|
751
|
+
* `member: "setSecret"`. That is deliberate for a recording double: the
|
|
752
|
+
* journal never leaves the test, and a test asserting that a value *was*
|
|
753
|
+
* masked needs to see which one. Do not hand this journal to anything that
|
|
754
|
+
* logs.
|
|
755
|
+
*
|
|
756
|
+
* Each call returns a fresh, independent journal — there is no state shared
|
|
757
|
+
* between two `recording()` calls.
|
|
758
|
+
*/
|
|
759
|
+
static readonly recording: () => RecordingOutputs;
|
|
681
760
|
}
|
|
682
761
|
//#endregion
|
|
683
762
|
//#region src/ActionState.d.ts
|
|
@@ -1228,20 +1307,30 @@ export declare class CacheKey extends CacheKey_base {
|
|
|
1228
1307
|
*
|
|
1229
1308
|
* @remarks
|
|
1230
1309
|
* Discovery and matching are **two different jobs**, and separating them is
|
|
1231
|
-
* what makes this testable and correct. The walk is
|
|
1232
|
-
* `
|
|
1233
|
-
*
|
|
1234
|
-
*
|
|
1235
|
-
*
|
|
1236
|
-
*
|
|
1237
|
-
*
|
|
1238
|
-
*
|
|
1239
|
-
*
|
|
1240
|
-
*
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1310
|
+
* what makes this testable and correct. The walk is `@effected/walker`'s
|
|
1311
|
+
* `descend`, run once per include from that include's own literal prefix
|
|
1312
|
+
* (a literal include is one `stat`, never a walk) with nothing pruned
|
|
1313
|
+
* implicitly; the matching is `@effected/glob`'s full minimatch dialect —
|
|
1314
|
+
* the same dialect `@actions/glob` uses, so a workflow author's
|
|
1315
|
+
* `!**\/node_modules/**` behaves here exactly as it does in every other
|
|
1316
|
+
* cache step in the same workflow. `node:fs.globSync` would have welded the
|
|
1317
|
+
* two together behind one non-stubbable call *and* changed the dialect, and
|
|
1318
|
+
* a dialect divergence surfaces as a silent cache-key difference — the worst
|
|
1319
|
+
* failure mode a cache key has.
|
|
1320
|
+
*
|
|
1321
|
+
* Candidates are matched by their path **relative to the workspace**, and a
|
|
1322
|
+
* literal that climbs above it (`../lockfile`) is dropped, which is what
|
|
1323
|
+
* makes "never hash a file outside the workspace" structural rather than a
|
|
1324
|
+
* rule someone has to remember. Directories are excluded: a directory
|
|
1243
1325
|
* called `notes.txt` matches `**\/*.txt` and is not a file, and hashing it
|
|
1244
|
-
* would fail rather than being ignored.
|
|
1326
|
+
* would fail rather than being ignored. An absent literal is a miss; any
|
|
1327
|
+
* other failure to read one is a typed `CacheKeyReadError`, because a key
|
|
1328
|
+
* derived from an incomplete file set is wrong in a way nothing reports.
|
|
1329
|
+
*
|
|
1330
|
+
* **One knowing divergence from the runner's `hashFiles()`:** `descend`
|
|
1331
|
+
* never enters a symlinked directory (cycle safety), where `@actions/glob`
|
|
1332
|
+
* follows links by default. A file reachable only through a symlinked
|
|
1333
|
+
* directory does not contribute to the key.
|
|
1245
1334
|
*
|
|
1246
1335
|
* The answer is sorted, so a caller cannot make its key depend on the order
|
|
1247
1336
|
* the filesystem happened to report.
|
|
@@ -3349,7 +3438,7 @@ export declare class DetachedProcess {
|
|
|
3349
3438
|
* yet, and silently retrying the first is how a misconfigured probe becomes a
|
|
3350
3439
|
* timeout six seconds later with nothing to show for it.
|
|
3351
3440
|
*/
|
|
3352
|
-
static readonly awaitReady: <E, R>(probe: Effect.Effect<boolean, E, R>, options?: ReadinessOptions | undefined) => Effect.Effect<
|
|
3441
|
+
static readonly awaitReady: <E, R>(probe: Effect.Effect<boolean, E, R>, options?: ReadinessOptions | undefined) => Effect.Effect<undefined, E | DetachedNotReadyError, R>;
|
|
3353
3442
|
/**
|
|
3354
3443
|
* A readiness probe over HTTP: `true` when a `GET` answers 2xx.
|
|
3355
3444
|
*
|
|
@@ -3471,15 +3560,6 @@ declare const DryRun_base: Context.ServiceClass<DryRun, "@effected/github-action
|
|
|
3471
3560
|
* @public
|
|
3472
3561
|
*/
|
|
3473
3562
|
export declare class DryRun extends DryRun_base {
|
|
3474
|
-
/**
|
|
3475
|
-
* Driven by the `dry-run` action input, defaulting to a real run.
|
|
3476
|
-
*
|
|
3477
|
-
* @remarks
|
|
3478
|
-
* Fails with a `ConfigError` when the input is present but is not a YAML 1.2
|
|
3479
|
-
* core-schema boolean — a workflow that writes `dry-run: yes` should stop,
|
|
3480
|
-
* not quietly perform the mutations it meant to rehearse.
|
|
3481
|
-
*/
|
|
3482
|
-
static readonly layer: Layer.Layer<DryRun, Config.ConfigError>;
|
|
3483
3563
|
/**
|
|
3484
3564
|
* Driven by a named input.
|
|
3485
3565
|
*
|
|
@@ -3489,6 +3569,15 @@ export declare class DryRun extends DryRun_base {
|
|
|
3489
3569
|
* composition site.
|
|
3490
3570
|
*/
|
|
3491
3571
|
static readonly layerFromInput: (name: string) => Layer.Layer<DryRun, Config.ConfigError>;
|
|
3572
|
+
/**
|
|
3573
|
+
* Driven by the `dry-run` action input, defaulting to a real run.
|
|
3574
|
+
*
|
|
3575
|
+
* @remarks
|
|
3576
|
+
* Fails with a `ConfigError` when the input is present but is not a YAML 1.2
|
|
3577
|
+
* core-schema boolean — a workflow that writes `dry-run: yes` should stop,
|
|
3578
|
+
* not quietly perform the mutations it meant to rehearse.
|
|
3579
|
+
*/
|
|
3580
|
+
static readonly layer: Layer.Layer<DryRun, Config.ConfigError>;
|
|
3492
3581
|
/** Driven by an explicit decision the caller has already made. */
|
|
3493
3582
|
static readonly layerFrom: (enabled: boolean) => Layer.Layer<DryRun>;
|
|
3494
3583
|
/**
|
|
@@ -4051,6 +4140,7 @@ declare const ManagedDocument_base: Schema.Class<ManagedDocument, Schema.Struct<
|
|
|
4051
4140
|
* @public
|
|
4052
4141
|
*/
|
|
4053
4142
|
export declare class ManagedDocument extends ManagedDocument_base {
|
|
4143
|
+
#private;
|
|
4054
4144
|
/**
|
|
4055
4145
|
* Read a document out of existing text, or begin a fresh one.
|
|
4056
4146
|
*
|
|
@@ -4137,6 +4227,9 @@ export declare class ManagedDocument extends ManagedDocument_base {
|
|
|
4137
4227
|
* synchronous callers can use that variant directly.
|
|
4138
4228
|
*/
|
|
4139
4229
|
withRegions(entries: ReadonlyArray<readonly [key: string, content: string, meta?: Readonly<Record<string, string>>]>): Effect.Effect<ManagedDocument, ManagedDocumentError>;
|
|
4230
|
+
private scan;
|
|
4231
|
+
/** What every wire key of this document's regions begins with. */
|
|
4232
|
+
private get wirePrefix();
|
|
4140
4233
|
/** The marker key a region is written under: `namespace.key.region`. */
|
|
4141
4234
|
private wireKey;
|
|
4142
4235
|
/** The region key back out of a wire key, for error reporting. */
|
|
@@ -4791,5 +4884,5 @@ export declare class Secret {
|
|
|
4791
4884
|
static readonly adopt: (name: string) => Config.Config<Redacted.Redacted<string>>;
|
|
4792
4885
|
}
|
|
4793
4886
|
//#endregion
|
|
4794
|
-
export type { ActionCacheShape, ActionEnvironmentShape, ActionLoggerShape, ActionOutputError, ActionOutputsShape, ActionRunOptions, ActionServices, ActionStateShape, AnnotationProperties, ArtifactItem, ArtifactRef, ArtifactShape, BlobEnvelopeError, BlobStoreShape, CacheKeyError, CheckDocumentOptions, CheckDocumentShape, CheckDocumentSink, CheckFlushOutcome, CheckRunConclusion, CheckRunProjection, ClientLayerOptions, DataBlobTransfer, DetachedProcessError, DetachedProcessOps, DetachedSpawnOptions, DownloadOptions, DownloadResult, DryRunShape, ExtractOptions, FileBlobTransfer, GitHubHeadingDepth, GitHubListOptions, GitHubRowSchema, GitHubSchemaTable, GitHubSchemaTableColumn, GitHubSchemaTableColumns, GitHubSchemaTableFormatRequiredKeys, GitHubSchemaTableFormattedColumn, GitHubSchemaTableOptions, ManagedDocumentSource, OidcTokenIssuerShape, PackageManagerInstallOptions, PackageManagerInstallerShape, PairsOptions, PathPrependEnv, PathPrependOptions, ProvisionFileOptions, ProvisionOptions, ProvisionedFile, ReadOptions, ReadinessOptions, S3Config, StoredBlob, ToolDownloadOptions, ToolInstallerShape, UploadOptions, UploadResult, WithBufferOptions, WithStepOptions };
|
|
4887
|
+
export type { ActionCacheShape, ActionEnvironmentShape, ActionLoggerShape, ActionOutputError, ActionOutputsShape, ActionRunOptions, ActionServices, ActionStateShape, AnnotationProperties, ArtifactItem, ArtifactRef, ArtifactShape, BlobEnvelopeError, BlobStoreShape, CacheKeyError, CheckDocumentOptions, CheckDocumentShape, CheckDocumentSink, CheckFlushOutcome, CheckRunConclusion, CheckRunProjection, ClientLayerOptions, DataBlobTransfer, DetachedProcessError, DetachedProcessOps, DetachedSpawnOptions, DownloadOptions, DownloadResult, DryRunShape, ExtractOptions, FileBlobTransfer, GitHubHeadingDepth, GitHubListOptions, GitHubRowSchema, GitHubSchemaTable, GitHubSchemaTableColumn, GitHubSchemaTableColumns, GitHubSchemaTableFormatRequiredKeys, GitHubSchemaTableFormattedColumn, GitHubSchemaTableOptions, ManagedDocumentSource, OidcTokenIssuerShape, PackageManagerInstallOptions, PackageManagerInstallerShape, PairsOptions, PathPrependEnv, PathPrependOptions, ProvisionFileOptions, ProvisionOptions, ProvisionedFile, ReadOptions, ReadinessOptions, RecordedOutputMember, RecordingOutputs, S3Config, StoredBlob, ToolDownloadOptions, ToolInstallerShape, UploadOptions, UploadResult, WithBufferOptions, WithStepOptions };
|
|
4795
4888
|
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { ActionEnvironment, ActionEnvironmentError, GitHubContext, RunnerContext
|
|
|
2
2
|
import { ActionInput } from "./ActionInput.js";
|
|
3
3
|
import { WorkflowCommand } from "./WorkflowCommand.js";
|
|
4
4
|
import { ActionLogger } from "./ActionLogger.js";
|
|
5
|
-
import { ActionOutputs, DetachedOutputError, InvalidOutputNameError, OutputEncodeError, RunnerFileUnavailableError, RunnerFileWriteError } from "./ActionOutputs.js";
|
|
5
|
+
import { ActionOutputs, DetachedOutputError, InvalidOutputNameError, OutputEncodeError, RecordedOutput, RunnerFileUnavailableError, RunnerFileWriteError } from "./ActionOutputs.js";
|
|
6
6
|
import { ActionState, ActionStateError } from "./ActionState.js";
|
|
7
7
|
import { Action, ActionRuntime, describeCause } from "./Action.js";
|
|
8
8
|
import { BlobTransferError } from "./BlobTransfer.js";
|
|
@@ -27,4 +27,4 @@ import { GitHubToken, GitHubTokenError } from "./GitHubToken.js";
|
|
|
27
27
|
import { ToolInstaller, ToolInstallerError } from "./ToolInstaller.js";
|
|
28
28
|
import { AmbientPackageManager, CachedPackageManager, InstalledPackageManager, PackageManagerInstaller, PackageManagerInstallerError } from "./PackageManagerInstaller.js";
|
|
29
29
|
|
|
30
|
-
export { Action, ActionCache, ActionCacheError, ActionEnvironment, ActionEnvironmentError, ActionInput, ActionLogger, ActionOutputs, ActionRuntime, ActionState, ActionStateError, ActionsIdentityToken, ActionsProvenance, AmbientPackageManager, Artifact, ArtifactError, BlobEnvelope, BlobMetadataDecodeError, BlobMetadataEncodeError, BlobStore, BlobStoreError, BlobTransferError, CacheKey, CacheKeyBadPatternError, CacheKeyReadError, CachedPackageManager, CheckDocument, CheckDocumentError, CheckDocumentStamp, CheckReport, CheckState, ChildEnv, DetachedLogUnavailableError, DetachedNotReadyError, DetachedOutputError, DetachedProcess, DetachedSignalFailedError, DetachedSpawnFailedError, DryRun, GitHubCacheBlobStore, GitHubContext, GitHubMarkdown, GitHubToken, GitHubTokenError, InstalledPackageManager, InvalidOutputNameError, InvalidPidError, ManagedDocument, ManagedDocumentError, NotABlobEnvelopeError, OidcClaims, OidcTokenError, OidcTokenIssuer, OutputEncodeError, PackageManagerInstaller, PackageManagerInstallerError, ProcessId, RunnerContext, RunnerFileUnavailableError, RunnerFileWriteError, Secret, ToolInstaller, ToolInstallerError, TruncatedBlobEnvelopeError, UnsupportedBlobEnvelopeVersionError, WorkflowCommand, describeCause, projectCheckState };
|
|
30
|
+
export { Action, ActionCache, ActionCacheError, ActionEnvironment, ActionEnvironmentError, ActionInput, ActionLogger, ActionOutputs, ActionRuntime, ActionState, ActionStateError, ActionsIdentityToken, ActionsProvenance, AmbientPackageManager, Artifact, ArtifactError, BlobEnvelope, BlobMetadataDecodeError, BlobMetadataEncodeError, BlobStore, BlobStoreError, BlobTransferError, CacheKey, CacheKeyBadPatternError, CacheKeyReadError, CachedPackageManager, CheckDocument, CheckDocumentError, CheckDocumentStamp, CheckReport, CheckState, ChildEnv, DetachedLogUnavailableError, DetachedNotReadyError, DetachedOutputError, DetachedProcess, DetachedSignalFailedError, DetachedSpawnFailedError, DryRun, GitHubCacheBlobStore, GitHubContext, GitHubMarkdown, GitHubToken, GitHubTokenError, InstalledPackageManager, InvalidOutputNameError, InvalidPidError, ManagedDocument, ManagedDocumentError, NotABlobEnvelopeError, OidcClaims, OidcTokenError, OidcTokenIssuer, OutputEncodeError, PackageManagerInstaller, PackageManagerInstallerError, ProcessId, RecordedOutput, RunnerContext, RunnerFileUnavailableError, RunnerFileWriteError, Secret, ToolInstaller, ToolInstallerError, TruncatedBlobEnvelopeError, UnsupportedBlobEnvelopeVersionError, WorkflowCommand, describeCause, projectCheckState };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { payloadOf } from "./jwt.js";
|
|
1
2
|
import { Effect, Option, Redacted, Result } from "effect";
|
|
2
3
|
|
|
3
4
|
//#region src/internal/actionsResults.ts
|
|
@@ -18,20 +19,13 @@ const RESULTS_URL = "ACTIONS_RESULTS_URL";
|
|
|
18
19
|
/** The variable holding the run-scoped credential. Absent outside a `uses:` step. */
|
|
19
20
|
const RUNTIME_TOKEN = "ACTIONS_RUNTIME_TOKEN";
|
|
20
21
|
/**
|
|
21
|
-
*
|
|
22
|
+
* The `misconfigured` detail every results-backend consumer reports when
|
|
23
|
+
* {@link resultsBackend} fails: names the missing variable, and says why it is
|
|
24
|
+
* missing — the single most common misuse of these services.
|
|
22
25
|
*
|
|
23
|
-
* @
|
|
24
|
-
* No signature verification, deliberately and for the same reason
|
|
25
|
-
* `OidcTokenIssuer` gives: the token was handed to this process by the runner
|
|
26
|
-
* that started it, and the claim read here scopes a request rather than
|
|
27
|
-
* authorizing one.
|
|
26
|
+
* @internal
|
|
28
27
|
*/
|
|
29
|
-
const
|
|
30
|
-
const segments = token.split(".");
|
|
31
|
-
if (segments.length !== 3) throw new Error(`expected a three-segment JWT, got ${segments.length}`);
|
|
32
|
-
const json = Buffer.from(segments[1] ?? "", "base64url").toString("utf8");
|
|
33
|
-
return JSON.parse(json);
|
|
34
|
-
};
|
|
28
|
+
const misconfiguredDetail = (variable, service) => `${variable} is not set — the ${service} is only reachable from a \`uses:\` step, never from \`run:\``;
|
|
35
29
|
/**
|
|
36
30
|
* The run and job ids the artifact protocol needs, from the runtime token's
|
|
37
31
|
* `scp` claim.
|
|
@@ -44,13 +38,9 @@ const payloadOf = (token) => {
|
|
|
44
38
|
* @internal
|
|
45
39
|
*/
|
|
46
40
|
const backendIdsFrom = (token) => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
} catch (cause) {
|
|
51
|
-
return Result.fail(`the runtime token is not a readable JWT: ${cause instanceof Error ? cause.message : cause}`);
|
|
52
|
-
}
|
|
53
|
-
const scope = payload.scp;
|
|
41
|
+
const payload = payloadOf(token);
|
|
42
|
+
if (Result.isFailure(payload)) return Result.fail(`the runtime token is not a readable JWT: ${payload.failure.detail}`);
|
|
43
|
+
const scope = payload.success?.scp;
|
|
54
44
|
if (typeof scope !== "string") return Result.fail("the runtime token carries no `scp` claim");
|
|
55
45
|
for (const entry of scope.split(" ")) {
|
|
56
46
|
const parts = entry.split(":");
|
|
@@ -95,4 +85,4 @@ const resultsBackend = (env) => Effect.gen(function* () {
|
|
|
95
85
|
});
|
|
96
86
|
|
|
97
87
|
//#endregion
|
|
98
|
-
export { RESULTS_URL, RUNTIME_TOKEN, backendIdsFrom, resultsBackend };
|
|
88
|
+
export { RESULTS_URL, RUNTIME_TOKEN, backendIdsFrom, misconfiguredDetail, resultsBackend };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { CONFLICT, isOk, stringField } from "./twirp.js";
|
|
2
|
+
import { Effect, Option } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/cacheService.ts
|
|
5
|
+
/**
|
|
6
|
+
* The Actions `CacheService` choreography — reserve, upload, finalize; look up
|
|
7
|
+
* — spelled once for the two services that speak it.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* `ActionCache` files a tar archive under a path-derived version and
|
|
11
|
+
* `BlobStore.githubCache` files an envelope under a constant one, but the
|
|
12
|
+
* three RPCs between them and their conflict semantics are identical. Each
|
|
13
|
+
* caller supplies its own per-key `call` and its own `refused` constructor, so
|
|
14
|
+
* the errors stay the caller's; this module owns only the protocol. It reaches
|
|
15
|
+
* `effect` and the Twirp client and nothing heavier — the Azure transfer of
|
|
16
|
+
* the bytes between the two RPCs stays in the three modules licensed to import
|
|
17
|
+
* it (`__test__/reachability.test.ts`).
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
/** The Twirp service the Actions cache protocol lives under. @internal */
|
|
22
|
+
const CACHE_SERVICE = "github.actions.results.api.v1.CacheService";
|
|
23
|
+
/**
|
|
24
|
+
* Reserve `(key, version)` for an upload. `Option.none()` means another job
|
|
25
|
+
* saved this key first — entries are immutable, so the cache already holds
|
|
26
|
+
* what the caller wanted and there is nothing left to do.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
const reserveUpload = (client, key, version) => Effect.flatMap(client.call("CreateCacheEntry", {
|
|
31
|
+
key,
|
|
32
|
+
version
|
|
33
|
+
}), (created) => {
|
|
34
|
+
if (created === CONFLICT) return Effect.succeedNone;
|
|
35
|
+
const url = stringField(created, "signedUploadUrl");
|
|
36
|
+
return !isOk(created) || url === void 0 ? Effect.fail(client.refused("CreateCacheEntry returned no upload url")) : Effect.succeedSome(url);
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* Confirm an upload. Not folded in with the reservation: an unfinalized upload
|
|
40
|
+
* leaves bytes in Azure that no lookup can reach — a silent no-op that reads
|
|
41
|
+
* as a successful save — so it is a failure even though every byte arrived.
|
|
42
|
+
*
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
const finalizeUpload = (client, key, version, sizeBytes) => Effect.flatMap(client.call("FinalizeCacheEntryUpload", {
|
|
46
|
+
key,
|
|
47
|
+
version,
|
|
48
|
+
size_bytes: String(sizeBytes)
|
|
49
|
+
}), (finalized) => finalized === CONFLICT || !isOk(finalized) ? Effect.fail(client.refused("FinalizeCacheEntryUpload did not confirm the upload")) : Effect.void);
|
|
50
|
+
/**
|
|
51
|
+
* The signed download url for a key, walking the restore-key ladder.
|
|
52
|
+
* `Option.none()` is a miss, not a failure — a cold cache is the normal state.
|
|
53
|
+
*
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
const lookupDownload = (client, key, restoreKeys, version) => Effect.map(client.call("GetCacheEntryDownloadURL", {
|
|
57
|
+
key,
|
|
58
|
+
restore_keys: [...restoreKeys],
|
|
59
|
+
version
|
|
60
|
+
}), (found) => {
|
|
61
|
+
if (found === CONFLICT || !isOk(found)) return Option.none();
|
|
62
|
+
const url = stringField(found, "signedDownloadUrl");
|
|
63
|
+
return url === void 0 ? Option.none() : Option.some({
|
|
64
|
+
url,
|
|
65
|
+
matchedKey: stringField(found, "matchedKey")
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
export { CACHE_SERVICE, finalizeUpload, lookupDownload, reserveUpload };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Effect, Encoding, Stream } from "effect";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/digest.ts
|
|
5
|
+
/**
|
|
6
|
+
* The sanctioned `node:crypto` digests, spelled once.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* Core `Crypto` is RNG-only — no digest, no HMAC — so this package's licence
|
|
10
|
+
* for a `node:` import covers hashing (`CLAUDE.md`). Keeping every `createHash`
|
|
11
|
+
* behind this module is what makes that licence auditable in one place rather
|
|
12
|
+
* than six, and keeps the streamed file digest — the shape a multi-gigabyte
|
|
13
|
+
* toolchain archive needs — from being re-derived per caller.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
/** The raw SHA-256 of a string or byte array held in memory. @internal */
|
|
18
|
+
const sha256 = (value) => new Uint8Array(createHash("sha256").update(value).digest());
|
|
19
|
+
/** The hex SHA-256 of a string or byte array held in memory. @internal */
|
|
20
|
+
const sha256Hex = (value) => createHash("sha256").update(value).digest("hex");
|
|
21
|
+
/**
|
|
22
|
+
* The raw digest of a file's bytes under `algorithm`, streamed rather than
|
|
23
|
+
* buffered. Raw, because a caller that folds several file digests into one
|
|
24
|
+
* (`CacheKey.hashFiles`, byte-compatible with the runner's `hashFiles()`)
|
|
25
|
+
* hashes the digest BYTES, not their hex spelling.
|
|
26
|
+
*
|
|
27
|
+
* `algorithm` must be one `node:crypto` supports — every caller's grammar
|
|
28
|
+
* admits only `sha1`/`sha256`/`sha384`/`sha512`, so `createHash` cannot throw.
|
|
29
|
+
*
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
const digestFile = (fs, file, algorithm) => Effect.suspend(() => {
|
|
33
|
+
const accumulator = createHash(algorithm);
|
|
34
|
+
return Stream.runForEach(fs.stream(file), (chunk) => Effect.sync(() => {
|
|
35
|
+
accumulator.update(chunk);
|
|
36
|
+
})).pipe(Effect.map(() => new Uint8Array(accumulator.digest())));
|
|
37
|
+
});
|
|
38
|
+
/** {@link digestFile}, as hex. @internal */
|
|
39
|
+
const digestFileHex = (fs, file, algorithm) => Effect.map(digestFile(fs, file, algorithm), Encoding.encodeHex);
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
export { digestFile, digestFileHex, sha256, sha256Hex };
|