@effected/github-actions 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ActionInput.js +42 -1
- package/Artifact.js +26 -22
- package/CacheKey.js +13 -8
- package/ChildEnv.js +10 -0
- package/DetachedProcess.js +2 -3
- package/PackageManagerInstaller.js +2 -13
- package/ToolInstaller.js +65 -21
- package/index.d.ts +129 -20
- package/internal/archiveCommands.js +153 -0
- package/internal/digest.js +6 -5
- package/internal/fsProbe.js +9 -1
- package/internal/runner.js +3 -3
- package/package.json +11 -11
package/ActionInput.js
CHANGED
|
@@ -176,6 +176,37 @@ var ActionInput = class ActionInput {
|
|
|
176
176
|
}));
|
|
177
177
|
}
|
|
178
178
|
/**
|
|
179
|
+
* An input that must be one of a closed set of strings, typed as their
|
|
180
|
+
* union.
|
|
181
|
+
*
|
|
182
|
+
* @remarks
|
|
183
|
+
* The match is **exact**: no trimming and no case folding, so `" pr"` and
|
|
184
|
+
* `"PR"` are both rejected for `["commit", "pr"]`. An action manifest
|
|
185
|
+
* declares the set in `options:`-style prose and the workflow author copies
|
|
186
|
+
* a value verbatim; the one thing worth tolerating there is nothing, because
|
|
187
|
+
* a tolerated near-miss is a value the manifest never named. Absent or `""`
|
|
188
|
+
* is **missing data** (see {@link ActionInput.string}) — `Config.withDefault`
|
|
189
|
+
* for an optional input. A present value outside the set fails carrying its
|
|
190
|
+
* `actual`, naming the input, the value and the allowed set, and is never
|
|
191
|
+
* swallowed by a default.
|
|
192
|
+
*
|
|
193
|
+
* Core's `Config.Literals` is the same idea over `Config.schema`; this one
|
|
194
|
+
* exists so the failure reads like every other `ActionInput` failure —
|
|
195
|
+
* naming the input by its action name, not its `INPUT_` variable.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```ts
|
|
199
|
+
* import { ActionInput } from "@effected/github-actions";
|
|
200
|
+
* import { Config } from "effect";
|
|
201
|
+
*
|
|
202
|
+
* // Config<"commit" | "pr">
|
|
203
|
+
* const mode = ActionInput.literals("mode", ["commit", "pr"]).pipe(Config.withDefault("commit"));
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
static literals(name, allowed) {
|
|
207
|
+
return Config.String(inputVariable(name)).pipe(Config.mapEffect((raw) => allowed.includes(raw) ? Effect.succeed(raw) : Effect.fail(configError(`Input "${name}" must be one of: ${allowed.join(" | ")} — received "${raw}"`, raw))));
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
179
210
|
* An integer input.
|
|
180
211
|
*
|
|
181
212
|
* @remarks
|
|
@@ -306,6 +337,12 @@ var ActionInput = class ActionInput {
|
|
|
306
337
|
* `Config.option` for an optional input. A present value that is not valid
|
|
307
338
|
* JSON, or does not satisfy the schema, fails carrying its `actual` and is
|
|
308
339
|
* never swallowed by a default.
|
|
340
|
+
*
|
|
341
|
+
* There is no `schemaOption`: `ActionInput.schema(name, S).pipe(Config.option)`
|
|
342
|
+
* yields `Config<Option<A>>` with missing → `None` and a malformed value
|
|
343
|
+
* still failing — core's documented `Config.option` behaviour ("validation
|
|
344
|
+
* errors ... still propagate", `Config.ts`), which is exactly the
|
|
345
|
+
* missing-versus-malformed split above.
|
|
309
346
|
*/
|
|
310
347
|
static schema(name, schema) {
|
|
311
348
|
return Config.String(inputVariable(name)).pipe(Config.mapEffect((raw) => {
|
|
@@ -352,7 +389,11 @@ var ActionInput = class ActionInput {
|
|
|
352
389
|
* to input-name entries too: `{ "flag": "" }` is an unsupplied input.
|
|
353
390
|
*
|
|
354
391
|
* Taking the environment as an argument rather than reading `process.env`
|
|
355
|
-
* is what makes inputs testable without mutating the test process.
|
|
392
|
+
* is what makes inputs testable without mutating the test process. The
|
|
393
|
+
* `process.env` default is a sanctioned exception to the package rule
|
|
394
|
+
* stated on `ChildEnv` (ambient process state is never read behind a
|
|
395
|
+
* caller's back) — a default the caller overrides by passing a value, the
|
|
396
|
+
* same class as `DetachedProcess.spawn`'s `base`.
|
|
356
397
|
*
|
|
357
398
|
* **A bare `ConfigProvider.fromEnv` cannot serve input-name keys.** Composed
|
|
358
399
|
* beneath this provider or {@link ActionInput.providerOver}, it uppercases
|
package/Artifact.js
CHANGED
|
@@ -6,10 +6,11 @@ import { CONFLICT, field, isOk, stringField, twirpCall, twirpFailureFields } fro
|
|
|
6
6
|
import { digestFileHex } from "./internal/digest.js";
|
|
7
7
|
import { isWindowsRunner } from "./internal/runner.js";
|
|
8
8
|
import { spawnOnce } from "./internal/spawn.js";
|
|
9
|
+
import { unzipCommand, zipCommand, zipManifest } from "./internal/archiveCommands.js";
|
|
9
10
|
import { Context, Effect, FileSystem, Layer, Option, Path, Result, Schema } from "effect";
|
|
10
11
|
import { HttpClient } from "effect/unstable/http";
|
|
11
12
|
import { BlobClient, BlockBlobClient } from "@azure/storage-blob";
|
|
12
|
-
import {
|
|
13
|
+
import { ChildProcessSpawner } from "effect/unstable/process";
|
|
13
14
|
|
|
14
15
|
//#region src/Artifact.ts
|
|
15
16
|
/**
|
|
@@ -195,31 +196,34 @@ const make = (transfer) => Effect.gen(function* () {
|
|
|
195
196
|
artifact,
|
|
196
197
|
cause
|
|
197
198
|
}));
|
|
198
|
-
const zip = (files, root, destination, level, artifact) => {
|
|
199
|
+
const zip = (files, root, destination, level, artifact) => Effect.gen(function* () {
|
|
199
200
|
const relative = files.map((file) => path.relative(root, file));
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
"
|
|
203
|
-
|
|
204
|
-
`
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
201
|
+
const unrepresentable = relative.find((file) => file.includes("\n") || file.includes("\r"));
|
|
202
|
+
if (unrepresentable !== void 0) return yield* Effect.fail(new ArtifactError({
|
|
203
|
+
reason: "invalidOptions",
|
|
204
|
+
artifact,
|
|
205
|
+
detail: `a file path may not contain a line break: ${JSON.stringify(unrepresentable)}`
|
|
206
|
+
}));
|
|
207
|
+
const manifest = path.join(path.dirname(destination), "artifact.manifest");
|
|
208
|
+
if (windows) yield* fs.writeFileString(manifest, zipManifest(relative)).pipe(Effect.mapError((cause) => new ArtifactError({
|
|
209
|
+
reason: "archiveFailed",
|
|
210
|
+
artifact,
|
|
211
|
+
cause
|
|
212
|
+
})));
|
|
213
|
+
yield* archive(zipCommand({
|
|
214
|
+
windows,
|
|
215
|
+
root,
|
|
216
|
+
files: relative,
|
|
217
|
+
manifest,
|
|
208
218
|
destination,
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
};
|
|
212
|
-
const unzip = (source, destination, artifact) =>
|
|
213
|
-
|
|
214
|
-
"-NonInteractive",
|
|
215
|
-
"-Command",
|
|
216
|
-
`Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('${source.replaceAll("'", "''")}', '${destination.replaceAll("'", "''")}')`
|
|
217
|
-
]), artifact) : archive(ChildProcess.make("unzip", [
|
|
218
|
-
"-oq",
|
|
219
|
+
level
|
|
220
|
+
}), artifact);
|
|
221
|
+
});
|
|
222
|
+
const unzip = (source, destination, artifact) => archive(unzipCommand({
|
|
223
|
+
windows,
|
|
219
224
|
source,
|
|
220
|
-
"-d",
|
|
221
225
|
destination
|
|
222
|
-
|
|
226
|
+
}), artifact);
|
|
223
227
|
return {
|
|
224
228
|
upload: Effect.fn("Artifact.upload")(function* (name, files, rootDirectory, options) {
|
|
225
229
|
yield* Effect.annotateCurrentSpan({ name });
|
package/CacheKey.js
CHANGED
|
@@ -353,18 +353,22 @@ var CacheKey = class CacheKey extends Schema.Class("CacheKey")(Schema.Struct({
|
|
|
353
353
|
* failure mode a cache key has.
|
|
354
354
|
*
|
|
355
355
|
* Candidates are matched by their path **relative to the workspace**, and a
|
|
356
|
-
* literal that climbs above it (`../lockfile`) is dropped
|
|
357
|
-
*
|
|
358
|
-
*
|
|
356
|
+
* literal that climbs above it (`../lockfile`) is dropped lexically — no
|
|
357
|
+
* literal reaches outside the workspace through path text. The walk is
|
|
358
|
+
* another matter: under `followSymlinks` a symlinked directory whose target
|
|
359
|
+
* lives outside the workspace IS descended, and its files DO contribute to
|
|
360
|
+
* the key — parity with `@actions/glob`, which follows links out of the
|
|
361
|
+
* tree too. Directories are excluded: a directory
|
|
359
362
|
* called `notes.txt` matches `**\/*.txt` and is not a file, and hashing it
|
|
360
363
|
* would fail rather than being ignored. An absent literal is a miss; any
|
|
361
364
|
* other failure to read one is a typed `CacheKeyReadError`, because a key
|
|
362
365
|
* derived from an incomplete file set is wrong in a way nothing reports.
|
|
363
366
|
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
* directory
|
|
367
|
+
* Symlinked directories are followed (`descend` under
|
|
368
|
+
* `followSymlinks: true`), matching `@actions/glob`'s default
|
|
369
|
+
* `followSymbolicLinks: true` — a file reachable only through a symlinked
|
|
370
|
+
* directory contributes to the key, and `descend`'s real-path cycle guard
|
|
371
|
+
* keeps link loops finite.
|
|
368
372
|
*
|
|
369
373
|
* The answer is sorted, so a caller cannot make its key depend on the order
|
|
370
374
|
* the filesystem happened to report.
|
|
@@ -396,7 +400,8 @@ var CacheKey = class CacheKey extends Schema.Class("CacheKey")(Schema.Struct({
|
|
|
396
400
|
if (wildcard.negated) continue;
|
|
397
401
|
const found = yield* descend(wildcard, {
|
|
398
402
|
cwd: workspace,
|
|
399
|
-
prune: []
|
|
403
|
+
prune: [],
|
|
404
|
+
followSymlinks: true
|
|
400
405
|
}).pipe(Effect.mapError((cause) => new CacheKeyReadError({
|
|
401
406
|
path: path.join(workspace, cause.path),
|
|
402
407
|
cause
|
package/ChildEnv.js
CHANGED
|
@@ -29,6 +29,16 @@
|
|
|
29
29
|
* ambient process — the same species as `Secret`'s statics, one concern per
|
|
30
30
|
* member.
|
|
31
31
|
*
|
|
32
|
+
* **The package rule, stated once: ambient process state is never read behind
|
|
33
|
+
* a caller's back.** `process.env`, `process.arch` and `process.platform` are
|
|
34
|
+
* read by `ActionEnvironment` (once, at layer construction) and otherwise
|
|
35
|
+
* taken as an argument — which is what makes every platform branch
|
|
36
|
+
* exercisable from a test on any host. The sanctioned exceptions are
|
|
37
|
+
* *defaults a caller can override by passing the value*: `ActionInput.provider`'s
|
|
38
|
+
* `env = process.env` and `DetachedProcess.spawn`'s `base`. A structural test
|
|
39
|
+
* (`__test__/ambientReads.test.ts`) holds the list closed; a new default is
|
|
40
|
+
* added there with its reason, never silently.
|
|
41
|
+
*
|
|
32
42
|
* @public
|
|
33
43
|
*/
|
|
34
44
|
var ChildEnv = class ChildEnv {
|
package/DetachedProcess.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { unstubbed } from "./internal/unstubbed.js";
|
|
2
|
+
import { isErrno } from "./internal/fsProbe.js";
|
|
2
3
|
import { Effect, Schedule, Schema } from "effect";
|
|
3
4
|
import { HttpClient } from "effect/unstable/http";
|
|
4
5
|
import { ChildProcessSpawner } from "effect/unstable/process";
|
|
@@ -114,8 +115,6 @@ cause: Schema.optionalKey(Schema.Defect()) }) {
|
|
|
114
115
|
* @public
|
|
115
116
|
*/
|
|
116
117
|
const ProcessId = Schema.Number.pipe(Schema.check(Schema.makeFilter((value) => Number.isInteger(value) && value > 0 ? void 0 : "Expected a positive integer process id")), Schema.brand("ProcessId"));
|
|
117
|
-
/** Whether a thrown value is a Node errno error with the given code. */
|
|
118
|
-
const isErrno = (cause, code) => typeof cause === "object" && cause !== null && cause.code === code;
|
|
119
118
|
/** See {@link DetachedProcess.makeTestOps}: an unstubbed member dies naming itself. */
|
|
120
119
|
const dies = unstubbed("DetachedProcess.makeTestOps");
|
|
121
120
|
/**
|
|
@@ -174,7 +173,7 @@ var DetachedProcess = class DetachedProcess {
|
|
|
174
173
|
const child = spawn(options.command, [...options.args ?? []], {
|
|
175
174
|
...options.cwd === void 0 ? {} : { cwd: options.cwd },
|
|
176
175
|
env: {
|
|
177
|
-
...process.env,
|
|
176
|
+
...options.base ?? process.env,
|
|
178
177
|
...options.env
|
|
179
178
|
},
|
|
180
179
|
detached: true,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ActionEnvironment } from "./ActionEnvironment.js";
|
|
2
2
|
import { unstubbed } from "./internal/unstubbed.js";
|
|
3
3
|
import { digestFileHex } from "./internal/digest.js";
|
|
4
|
-
import { isWindowsRunner
|
|
4
|
+
import { isWindowsRunner } from "./internal/runner.js";
|
|
5
5
|
import { typeAt } from "./internal/fsProbe.js";
|
|
6
6
|
import { PNPM_EXE_PREFIX, detectMusl, isNodeScript, pnpmExeTarget, strongestSri } from "./internal/pnpmExe.js";
|
|
7
7
|
import { ToolInstaller } from "./ToolInstaller.js";
|
|
@@ -223,13 +223,6 @@ const make = Effect.gen(function* () {
|
|
|
223
223
|
const bunBinaryName = windows ? "bun.exe" : "bun";
|
|
224
224
|
const shimFileName = (name) => windows ? `${name}.cmd` : name;
|
|
225
225
|
const shimBody = windows ? cmdShim : posixShim;
|
|
226
|
-
const cacheRoot = yield* toolCacheRoot(env, path);
|
|
227
|
-
const finalCachePath = (tool, version) => ToolInstaller.cachePath({
|
|
228
|
-
root: cacheRoot,
|
|
229
|
-
tool,
|
|
230
|
-
version,
|
|
231
|
-
arch: process.arch
|
|
232
|
-
});
|
|
233
226
|
const errorFor = (pin) => (fields) => new PackageManagerInstallerError({
|
|
234
227
|
name: pin.name,
|
|
235
228
|
version: pin.version.toString(),
|
|
@@ -551,13 +544,9 @@ const make = Effect.gen(function* () {
|
|
|
551
544
|
yield* assertFile(pin, cli, "bin/yarn.js is missing from the cli-dist tarball");
|
|
552
545
|
yield* verifyIntegrity(pin, cli);
|
|
553
546
|
}
|
|
554
|
-
const destination =
|
|
547
|
+
const destination = installer.cachePath(name, version);
|
|
555
548
|
yield* writeShims(pin, packageDir, destination, bins, { skipExisting: false });
|
|
556
549
|
const directory = yield* installer.cacheDir(packageDir, name, version).pipe(Effect.mapError(fromInstaller(pin)));
|
|
557
|
-
if (directory !== destination) return yield* Effect.fail(errorFor(pin)({
|
|
558
|
-
reason: "cacheFailed",
|
|
559
|
-
subject: `cache destination diverged: shims name ${destination}, entry landed at ${directory}`
|
|
560
|
-
}));
|
|
561
550
|
return CachedPackageManager.make({
|
|
562
551
|
name,
|
|
563
552
|
version,
|
package/ToolInstaller.js
CHANGED
|
@@ -2,10 +2,11 @@ import { ActionEnvironment } from "./ActionEnvironment.js";
|
|
|
2
2
|
import { unstubbed } from "./internal/unstubbed.js";
|
|
3
3
|
import { isWindowsRunner, toolCacheRoot } from "./internal/runner.js";
|
|
4
4
|
import { spawnOnce } from "./internal/spawn.js";
|
|
5
|
-
import {
|
|
5
|
+
import { tarExtractCommand, unzipCommand } from "./internal/archiveCommands.js";
|
|
6
|
+
import { isErrno, typeAt } from "./internal/fsProbe.js";
|
|
6
7
|
import { Context, Effect, FileSystem, Layer, Option, Path, Schedule, Schema, Stream } from "effect";
|
|
7
8
|
import { HttpClient } from "effect/unstable/http";
|
|
8
|
-
import {
|
|
9
|
+
import { ChildProcessSpawner } from "effect/unstable/process";
|
|
9
10
|
|
|
10
11
|
//#region src/ToolInstaller.ts
|
|
11
12
|
/**
|
|
@@ -135,6 +136,29 @@ const make = Effect.gen(function* () {
|
|
|
135
136
|
subject: tool,
|
|
136
137
|
cause
|
|
137
138
|
})));
|
|
139
|
+
/**
|
|
140
|
+
* Move a source tree into the staging directory, consuming the source.
|
|
141
|
+
*
|
|
142
|
+
* @remarks
|
|
143
|
+
* A rename first: on the same filesystem it is O(1) however large the
|
|
144
|
+
* toolchain is, where a recursive copy of a hundreds-of-MB tree is the
|
|
145
|
+
* dominant cost of an install. The staging directory is removed before the
|
|
146
|
+
* rename because Windows refuses to rename onto an existing directory; its
|
|
147
|
+
* mktemp name stays reserved in practice. The copy fallback is taken ONLY
|
|
148
|
+
* for `EXDEV` — a cross-filesystem move, the one case a rename cannot do —
|
|
149
|
+
* and every other rename failure is reported as-is: a copy after a
|
|
150
|
+
* permission error would turn a real failure into a slow success. The
|
|
151
|
+
* fallback removes the source afterwards so the contract is the same on
|
|
152
|
+
* both paths; that removal is best-effort, since the entry is already
|
|
153
|
+
* complete and a leftover temp directory is not worth failing the install.
|
|
154
|
+
*/
|
|
155
|
+
const moveIntoStaging = (source, staging) => fs.remove(staging, {
|
|
156
|
+
recursive: true,
|
|
157
|
+
force: true
|
|
158
|
+
}).pipe(Effect.flatMap(() => fs.rename(source, staging)), Effect.catchIf((error) => isErrno(error.cause, "EXDEV"), () => fs.copy(source, staging, { overwrite: true }).pipe(Effect.andThen(Effect.ignore(fs.remove(source, {
|
|
159
|
+
recursive: true,
|
|
160
|
+
force: true
|
|
161
|
+
}))))));
|
|
138
162
|
/** A staging directory beside the cache, so the swap is a rename and not a copy. */
|
|
139
163
|
const staged = (tool, use) => Effect.gen(function* () {
|
|
140
164
|
yield* fs.makeDirectory(root, { recursive: true });
|
|
@@ -198,32 +222,24 @@ const make = Effect.gen(function* () {
|
|
|
198
222
|
});
|
|
199
223
|
return {
|
|
200
224
|
find,
|
|
225
|
+
cachePath,
|
|
201
226
|
download,
|
|
202
227
|
extractTar: Effect.fn("ToolInstaller.extractTar")(function* (archive, options) {
|
|
203
228
|
const destination = yield* destinationFor(options);
|
|
204
|
-
|
|
205
|
-
yield* extractWith(ChildProcess.make("tar", [
|
|
206
|
-
...flags,
|
|
229
|
+
yield* extractWith(tarExtractCommand({
|
|
207
230
|
archive,
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
231
|
+
destination,
|
|
232
|
+
flags: options?.flags
|
|
233
|
+
}), archive);
|
|
211
234
|
return destination;
|
|
212
235
|
}),
|
|
213
236
|
extractZip: Effect.fn("ToolInstaller.extractZip")(function* (archive, options) {
|
|
214
237
|
const destination = yield* destinationFor(options);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
"-Command",
|
|
219
|
-
`$ErrorActionPreference = 'Stop'; try { Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('${archive.replaceAll("'", "''")}', '${destination.replaceAll("'", "''")}', $true) } catch { [Console]::Error.WriteLine($_.Exception.ToString()); exit 1 }`
|
|
220
|
-
]) : ChildProcess.make("unzip", [
|
|
221
|
-
"-oq",
|
|
222
|
-
archive,
|
|
223
|
-
"-d",
|
|
238
|
+
yield* extractWith(unzipCommand({
|
|
239
|
+
windows,
|
|
240
|
+
source: archive,
|
|
224
241
|
destination
|
|
225
|
-
|
|
226
|
-
yield* extractWith(command, archive);
|
|
242
|
+
}), archive);
|
|
227
243
|
return destination;
|
|
228
244
|
}),
|
|
229
245
|
cacheDir: Effect.fn("ToolInstaller.cacheDir")(function* (source, tool, version) {
|
|
@@ -231,7 +247,7 @@ const make = Effect.gen(function* () {
|
|
|
231
247
|
tool,
|
|
232
248
|
version
|
|
233
249
|
});
|
|
234
|
-
return yield* staged(tool, (staging) =>
|
|
250
|
+
return yield* staged(tool, (staging) => moveIntoStaging(source, staging).pipe(Effect.mapError((cause) => new ToolInstallerError({
|
|
235
251
|
reason: "cacheFailed",
|
|
236
252
|
subject: tool,
|
|
237
253
|
cause
|
|
@@ -266,6 +282,15 @@ const make = Effect.gen(function* () {
|
|
|
266
282
|
});
|
|
267
283
|
const dies = unstubbed("ToolInstaller.makeTest");
|
|
268
284
|
/**
|
|
285
|
+
* The root {@link ToolInstaller.makeTest}'s `cachePath` answers under: the
|
|
286
|
+
* same resolution as `make`'s (`internal/runner.ts`), read from the ambient
|
|
287
|
+
* environment because a double has no `ActionEnvironment` to ask. The literal
|
|
288
|
+
* `/tmp/runner-tool-cache` is what `make`'s `path.join("/tmp", "runner-tool-cache")`
|
|
289
|
+
* spells on POSIX, which is the only place a test double runs. Test-double
|
|
290
|
+
* only; allowlisted as such in `__test__/ambientReads.test.ts`.
|
|
291
|
+
*/
|
|
292
|
+
const testRoot = () => process.env.RUNNER_TOOL_CACHE ?? "/tmp/runner-tool-cache";
|
|
293
|
+
/**
|
|
269
294
|
* Download, extract and cache a toolchain in the runner's tool cache.
|
|
270
295
|
*
|
|
271
296
|
* @remarks
|
|
@@ -311,9 +336,28 @@ var ToolInstaller = class ToolInstaller extends Context.Service()("@effected/git
|
|
|
311
336
|
* runner's `RUNNER_ARCH` (`X64`) for the same reason.
|
|
312
337
|
*/
|
|
313
338
|
static cachePath = (options) => `${options.root}/${options.tool}/${options.version}/${options.arch}`;
|
|
314
|
-
/**
|
|
339
|
+
/**
|
|
340
|
+
* A test double. Unstubbed members die rather than reporting a tool that is
|
|
341
|
+
* not there.
|
|
342
|
+
*
|
|
343
|
+
* @remarks
|
|
344
|
+
* `cachePath` is the one member with a default rather than a death: it is
|
|
345
|
+
* pure and total, and a caller composing it into shim contents would
|
|
346
|
+
* otherwise have to stub it in every test. The default is the static layout
|
|
347
|
+
* over `RUNNER_TOOL_CACHE`, or the same off-runner root `make` resolves
|
|
348
|
+
* (`internal/runner.ts`). **That is a read of the ambient environment**,
|
|
349
|
+
* sanctioned here only because a double has no `ActionEnvironment` to ask —
|
|
350
|
+
* test-double-only, and listed as such in the package's ambient-read
|
|
351
|
+
* allowlist (`__test__/ambientReads.test.ts`).
|
|
352
|
+
*/
|
|
315
353
|
static makeTest = (overrides = {}) => ({
|
|
316
354
|
find: () => dies("find"),
|
|
355
|
+
cachePath: (tool, version) => ToolInstaller.cachePath({
|
|
356
|
+
root: testRoot(),
|
|
357
|
+
tool,
|
|
358
|
+
version,
|
|
359
|
+
arch: process.arch
|
|
360
|
+
}),
|
|
317
361
|
download: () => dies("download"),
|
|
318
362
|
extractTar: () => dies("extractTar"),
|
|
319
363
|
extractZip: () => dies("extractZip"),
|
package/index.d.ts
CHANGED
|
@@ -1319,18 +1319,22 @@ export declare class CacheKey extends CacheKey_base {
|
|
|
1319
1319
|
* failure mode a cache key has.
|
|
1320
1320
|
*
|
|
1321
1321
|
* Candidates are matched by their path **relative to the workspace**, and a
|
|
1322
|
-
* literal that climbs above it (`../lockfile`) is dropped
|
|
1323
|
-
*
|
|
1324
|
-
*
|
|
1322
|
+
* literal that climbs above it (`../lockfile`) is dropped lexically — no
|
|
1323
|
+
* literal reaches outside the workspace through path text. The walk is
|
|
1324
|
+
* another matter: under `followSymlinks` a symlinked directory whose target
|
|
1325
|
+
* lives outside the workspace IS descended, and its files DO contribute to
|
|
1326
|
+
* the key — parity with `@actions/glob`, which follows links out of the
|
|
1327
|
+
* tree too. Directories are excluded: a directory
|
|
1325
1328
|
* called `notes.txt` matches `**\/*.txt` and is not a file, and hashing it
|
|
1326
1329
|
* would fail rather than being ignored. An absent literal is a miss; any
|
|
1327
1330
|
* other failure to read one is a typed `CacheKeyReadError`, because a key
|
|
1328
1331
|
* derived from an incomplete file set is wrong in a way nothing reports.
|
|
1329
1332
|
*
|
|
1330
|
-
*
|
|
1331
|
-
*
|
|
1332
|
-
*
|
|
1333
|
-
* directory
|
|
1333
|
+
* Symlinked directories are followed (`descend` under
|
|
1334
|
+
* `followSymlinks: true`), matching `@actions/glob`'s default
|
|
1335
|
+
* `followSymbolicLinks: true` — a file reachable only through a symlinked
|
|
1336
|
+
* directory contributes to the key, and `descend`'s real-path cycle guard
|
|
1337
|
+
* keeps link loops finite.
|
|
1334
1338
|
*
|
|
1335
1339
|
* The answer is sorted, so a caller cannot make its key depend on the order
|
|
1336
1340
|
* the filesystem happened to report.
|
|
@@ -1646,6 +1650,35 @@ export declare class ActionInput {
|
|
|
1646
1650
|
* a malformed `dry-run` quietly reading as `false`.
|
|
1647
1651
|
*/
|
|
1648
1652
|
static boolean(name: string): Config.Config<boolean>;
|
|
1653
|
+
/**
|
|
1654
|
+
* An input that must be one of a closed set of strings, typed as their
|
|
1655
|
+
* union.
|
|
1656
|
+
*
|
|
1657
|
+
* @remarks
|
|
1658
|
+
* The match is **exact**: no trimming and no case folding, so `" pr"` and
|
|
1659
|
+
* `"PR"` are both rejected for `["commit", "pr"]`. An action manifest
|
|
1660
|
+
* declares the set in `options:`-style prose and the workflow author copies
|
|
1661
|
+
* a value verbatim; the one thing worth tolerating there is nothing, because
|
|
1662
|
+
* a tolerated near-miss is a value the manifest never named. Absent or `""`
|
|
1663
|
+
* is **missing data** (see {@link ActionInput.string}) — `Config.withDefault`
|
|
1664
|
+
* for an optional input. A present value outside the set fails carrying its
|
|
1665
|
+
* `actual`, naming the input, the value and the allowed set, and is never
|
|
1666
|
+
* swallowed by a default.
|
|
1667
|
+
*
|
|
1668
|
+
* Core's `Config.Literals` is the same idea over `Config.schema`; this one
|
|
1669
|
+
* exists so the failure reads like every other `ActionInput` failure —
|
|
1670
|
+
* naming the input by its action name, not its `INPUT_` variable.
|
|
1671
|
+
*
|
|
1672
|
+
* @example
|
|
1673
|
+
* ```ts
|
|
1674
|
+
* import { ActionInput } from "@effected/github-actions";
|
|
1675
|
+
* import { Config } from "effect";
|
|
1676
|
+
*
|
|
1677
|
+
* // Config<"commit" | "pr">
|
|
1678
|
+
* const mode = ActionInput.literals("mode", ["commit", "pr"]).pipe(Config.withDefault("commit"));
|
|
1679
|
+
* ```
|
|
1680
|
+
*/
|
|
1681
|
+
static literals<const L extends readonly [string, ...Array<string>]>(name: string, allowed: L): Config.Config<L[number]>;
|
|
1649
1682
|
/**
|
|
1650
1683
|
* An integer input.
|
|
1651
1684
|
*
|
|
@@ -1740,6 +1773,12 @@ export declare class ActionInput {
|
|
|
1740
1773
|
* `Config.option` for an optional input. A present value that is not valid
|
|
1741
1774
|
* JSON, or does not satisfy the schema, fails carrying its `actual` and is
|
|
1742
1775
|
* never swallowed by a default.
|
|
1776
|
+
*
|
|
1777
|
+
* There is no `schemaOption`: `ActionInput.schema(name, S).pipe(Config.option)`
|
|
1778
|
+
* yields `Config<Option<A>>` with missing → `None` and a malformed value
|
|
1779
|
+
* still failing — core's documented `Config.option` behaviour ("validation
|
|
1780
|
+
* errors ... still propagate", `Config.ts`), which is exactly the
|
|
1781
|
+
* missing-versus-malformed split above.
|
|
1743
1782
|
*/
|
|
1744
1783
|
static schema<A, I>(name: string, schema: Schema.Codec<A, I>): Config.Config<A>;
|
|
1745
1784
|
/**
|
|
@@ -1776,7 +1815,11 @@ export declare class ActionInput {
|
|
|
1776
1815
|
* to input-name entries too: `{ "flag": "" }` is an unsupplied input.
|
|
1777
1816
|
*
|
|
1778
1817
|
* Taking the environment as an argument rather than reading `process.env`
|
|
1779
|
-
* is what makes inputs testable without mutating the test process.
|
|
1818
|
+
* is what makes inputs testable without mutating the test process. The
|
|
1819
|
+
* `process.env` default is a sanctioned exception to the package rule
|
|
1820
|
+
* stated on `ChildEnv` (ambient process state is never read behind a
|
|
1821
|
+
* caller's back) — a default the caller overrides by passing a value, the
|
|
1822
|
+
* same class as `DetachedProcess.spawn`'s `base`.
|
|
1780
1823
|
*
|
|
1781
1824
|
* **A bare `ConfigProvider.fromEnv` cannot serve input-name keys.** Composed
|
|
1782
1825
|
* beneath this provider or {@link ActionInput.providerOver}, it uppercases
|
|
@@ -2233,8 +2276,9 @@ interface UploadOptions {
|
|
|
2233
2276
|
* The zlib level, 0–9, defaulting to 6 as `@actions/artifact` does.
|
|
2234
2277
|
*
|
|
2235
2278
|
* @remarks
|
|
2236
|
-
* Out-of-range values are clamped.
|
|
2237
|
-
* `
|
|
2279
|
+
* Out-of-range values are clamped. On Windows the level maps onto .NET's
|
|
2280
|
+
* `CompressionLevel`: `0` is `NoCompression`, `1..3` `Fastest`, `4..8`
|
|
2281
|
+
* `Optimal`, `9` `SmallestSize`.
|
|
2238
2282
|
*/
|
|
2239
2283
|
readonly compressionLevel?: number | undefined;
|
|
2240
2284
|
}
|
|
@@ -3068,9 +3112,9 @@ interface PathPrependOptions {
|
|
|
3068
3112
|
* The environment the child will inherit — on a runner, `process.env`.
|
|
3069
3113
|
*
|
|
3070
3114
|
* @remarks
|
|
3071
|
-
* Explicit rather than defaulted,
|
|
3072
|
-
*
|
|
3073
|
-
*
|
|
3115
|
+
* Explicit rather than defaulted, per the package rule stated on
|
|
3116
|
+
* {@link ChildEnv} — which is also what makes the Windows casing branch
|
|
3117
|
+
* exercisable from a test on any host.
|
|
3074
3118
|
*/
|
|
3075
3119
|
readonly base: Readonly<Record<string, string | undefined>>;
|
|
3076
3120
|
/**
|
|
@@ -3109,6 +3153,16 @@ interface PathPrependOptions {
|
|
|
3109
3153
|
* ambient process — the same species as `Secret`'s statics, one concern per
|
|
3110
3154
|
* member.
|
|
3111
3155
|
*
|
|
3156
|
+
* **The package rule, stated once: ambient process state is never read behind
|
|
3157
|
+
* a caller's back.** `process.env`, `process.arch` and `process.platform` are
|
|
3158
|
+
* read by `ActionEnvironment` (once, at layer construction) and otherwise
|
|
3159
|
+
* taken as an argument — which is what makes every platform branch
|
|
3160
|
+
* exercisable from a test on any host. The sanctioned exceptions are
|
|
3161
|
+
* *defaults a caller can override by passing the value*: `ActionInput.provider`'s
|
|
3162
|
+
* `env = process.env` and `DetachedProcess.spawn`'s `base`. A structural test
|
|
3163
|
+
* (`__test__/ambientReads.test.ts`) holds the list closed; a new default is
|
|
3164
|
+
* added there with its reason, never silently.
|
|
3165
|
+
*
|
|
3112
3166
|
* @public
|
|
3113
3167
|
*/
|
|
3114
3168
|
export declare class ChildEnv {
|
|
@@ -3328,15 +3382,32 @@ interface DetachedSpawnOptions {
|
|
|
3328
3382
|
readonly logFile: string;
|
|
3329
3383
|
/** The working directory. */
|
|
3330
3384
|
readonly cwd?: string | undefined;
|
|
3385
|
+
/**
|
|
3386
|
+
* The environment the child inherits, before `env` is merged over it.
|
|
3387
|
+
*
|
|
3388
|
+
* @remarks
|
|
3389
|
+
* Defaults to the parent's `process.env` — **the one sanctioned ambient
|
|
3390
|
+
* fallback in this module**, the same class of default as
|
|
3391
|
+
* `ActionInput.provider`'s `env = process.env`, and an exception to the
|
|
3392
|
+
* package rule stated on `ChildEnv` (ambient process state is never read
|
|
3393
|
+
* behind a caller's back) precisely because passing a value here is how a
|
|
3394
|
+
* caller opts out of it. Supply it to give the child a controlled block —
|
|
3395
|
+
* a test proving exactly what a worker sees, or a worker that must not
|
|
3396
|
+
* inherit the action's secrets — remembering that a base without `PATH`
|
|
3397
|
+
* costs the child its runtime. An `undefined` value is dropped by Node's
|
|
3398
|
+
* spawn, never stringified.
|
|
3399
|
+
*/
|
|
3400
|
+
readonly base?: Readonly<Record<string, string | undefined>> | undefined;
|
|
3331
3401
|
/**
|
|
3332
3402
|
* Environment additions.
|
|
3333
3403
|
*
|
|
3334
3404
|
* @remarks
|
|
3335
|
-
* **Merged over the parent's environment, not
|
|
3336
|
-
* environment costs the child `PATH`, which
|
|
3337
|
-
* own runtime and dies before writing a
|
|
3338
|
-
* looks like a spawn bug and is not.
|
|
3339
|
-
* `Secret.forChildEnv`, which masks them on
|
|
3405
|
+
* **Merged over `base` (the parent's environment by default), not
|
|
3406
|
+
* substituted for it.** A bare environment costs the child `PATH`, which
|
|
3407
|
+
* usually means it cannot find its own runtime and dies before writing a
|
|
3408
|
+
* word to the log — a failure that looks like a spawn bug and is not.
|
|
3409
|
+
* Secrets belong here only via `Secret.forChildEnv`, which masks them on
|
|
3410
|
+
* the way out.
|
|
3340
3411
|
*/
|
|
3341
3412
|
readonly env?: Readonly<Record<string, string>> | undefined;
|
|
3342
3413
|
}
|
|
@@ -4393,13 +4464,38 @@ interface ToolInstallerShape {
|
|
|
4393
4464
|
* say) to a foreign entry is a real shipped bug.
|
|
4394
4465
|
*/
|
|
4395
4466
|
readonly find: (tool: string, version: string) => Effect.Effect<Option.Option<string>>;
|
|
4467
|
+
/**
|
|
4468
|
+
* Where this installer's `cacheDir` / `cacheFile` will land `tool@version`:
|
|
4469
|
+
* the final `<root>/<tool>/<version>/<arch>`, over the root this layer
|
|
4470
|
+
* resolved at construction.
|
|
4471
|
+
*
|
|
4472
|
+
* @remarks
|
|
4473
|
+
* Pure — no IO, nothing is created — and exposed so a caller that must
|
|
4474
|
+
* write the final path INTO the staged tree before the swap (a shim that
|
|
4475
|
+
* names its own cached entry) reads the one answer `cacheDir` is about to
|
|
4476
|
+
* use, instead of deriving root and arch a second time and guarding the
|
|
4477
|
+
* two against drifting apart. {@link ToolInstaller.cachePath} is the same
|
|
4478
|
+
* layout as a static function of an explicit root.
|
|
4479
|
+
*/
|
|
4480
|
+
readonly cachePath: (tool: string, version: string) => string;
|
|
4396
4481
|
/** Download a url to a temporary file, retrying what is worth retrying. */
|
|
4397
4482
|
readonly download: (url: string, options?: ToolDownloadOptions) => Effect.Effect<string, ToolInstallerError>;
|
|
4398
4483
|
/** Extract a tarball. Returns the directory its contents landed in. */
|
|
4399
4484
|
readonly extractTar: (archive: string, options?: ExtractOptions) => Effect.Effect<string, ToolInstallerError>;
|
|
4400
4485
|
/** Extract a zip. Returns the directory its contents landed in. */
|
|
4401
4486
|
readonly extractZip: (archive: string, options?: ExtractOptions) => Effect.Effect<string, ToolInstallerError>;
|
|
4402
|
-
/**
|
|
4487
|
+
/**
|
|
4488
|
+
* Install a directory into the tool cache. Returns its cached path.
|
|
4489
|
+
*
|
|
4490
|
+
* @remarks
|
|
4491
|
+
* **`source` is consumed.** On success it no longer exists at its original
|
|
4492
|
+
* path: the tree is renamed into the cache when the two share a filesystem
|
|
4493
|
+
* (`RUNNER_TEMP` and `RUNNER_TOOL_CACHE` do on hosted runners, making the
|
|
4494
|
+
* install O(1) rather than a recursive copy of the whole toolchain), and
|
|
4495
|
+
* otherwise copied and then removed. Write everything the cached entry must
|
|
4496
|
+
* contain — shims, overlaid binaries — into `source` *before* this call,
|
|
4497
|
+
* and read nothing from it afterwards.
|
|
4498
|
+
*/
|
|
4403
4499
|
readonly cacheDir: (source: string, tool: string, version: string) => Effect.Effect<string, ToolInstallerError>;
|
|
4404
4500
|
/** Install a single file into the tool cache under `name`. Returns the cached directory. */
|
|
4405
4501
|
readonly cacheFile: (source: string, name: string, tool: string, version: string) => Effect.Effect<string, ToolInstallerError>;
|
|
@@ -4475,7 +4571,20 @@ export declare class ToolInstaller extends ToolInstaller_base {
|
|
|
4475
4571
|
readonly version: string;
|
|
4476
4572
|
readonly arch: string;
|
|
4477
4573
|
}) => string;
|
|
4478
|
-
/**
|
|
4574
|
+
/**
|
|
4575
|
+
* A test double. Unstubbed members die rather than reporting a tool that is
|
|
4576
|
+
* not there.
|
|
4577
|
+
*
|
|
4578
|
+
* @remarks
|
|
4579
|
+
* `cachePath` is the one member with a default rather than a death: it is
|
|
4580
|
+
* pure and total, and a caller composing it into shim contents would
|
|
4581
|
+
* otherwise have to stub it in every test. The default is the static layout
|
|
4582
|
+
* over `RUNNER_TOOL_CACHE`, or the same off-runner root `make` resolves
|
|
4583
|
+
* (`internal/runner.ts`). **That is a read of the ambient environment**,
|
|
4584
|
+
* sanctioned here only because a double has no `ActionEnvironment` to ask —
|
|
4585
|
+
* test-double-only, and listed as such in the package's ambient-read
|
|
4586
|
+
* allowlist (`__test__/ambientReads.test.ts`).
|
|
4587
|
+
*/
|
|
4479
4588
|
static readonly makeTest: (overrides?: Partial<ToolInstallerShape>) => ToolInstallerShape;
|
|
4480
4589
|
/** {@link ToolInstaller.makeTest} behind `Layer.succeed`. */
|
|
4481
4590
|
static readonly layerTest: (overrides?: Partial<ToolInstallerShape>) => Layer.Layer<ToolInstaller>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { ChildProcess } from "effect/unstable/process";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/archiveCommands.ts
|
|
4
|
+
/**
|
|
5
|
+
* The archiver command lines this package spawns, spelled once.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* `Artifact` (pack and unpack) and `ToolInstaller` (unpack only) each spawn
|
|
9
|
+
* the same three shapes — a zip, an unzip, a tar extraction — and each has a
|
|
10
|
+
* Windows half that no test on a POSIX host can execute. Two hand-written
|
|
11
|
+
* copies drifted: `ToolInstaller.extractZip` learned the overwrite overload
|
|
12
|
+
* and the stderr capture below, `Artifact.download` did not, and a download
|
|
13
|
+
* into a non-empty directory kept failing on Windows with an EMPTY stderr.
|
|
14
|
+
* This module is the one spelling; the tests pin its text verbatim, which
|
|
15
|
+
* is the only proof the Windows halves get.
|
|
16
|
+
*
|
|
17
|
+
* **The pwsh unzip is deliberately belt-and-braces.** The THREE-argument
|
|
18
|
+
* `ExtractToDirectory(src, dest, $true)` overload overwrites existing files —
|
|
19
|
+
* the two-argument one refuses, which is what turned a repeated extraction
|
|
20
|
+
* into a hard failure — and the `$ErrorActionPreference = 'Stop'` plus
|
|
21
|
+
* try/catch writes the actual .NET exception text plainly to stderr and
|
|
22
|
+
* exits 1. pwsh's own error rendering does not reliably reach a captured
|
|
23
|
+
* stream, and an empty complaint costs a source dive to even hypothesize
|
|
24
|
+
* about.
|
|
25
|
+
*
|
|
26
|
+
* **The pwsh zip drives .NET's `ZipFile` directly, not `Compress-Archive`.**
|
|
27
|
+
* Microsoft's own reference for `Compress-Archive -Path` states both faults:
|
|
28
|
+
* handed individual file paths it stores every entry under its bare file
|
|
29
|
+
* name — directory structure is kept only when `-Path` names a directory —
|
|
30
|
+
* so `dir\b.txt` landed as `b.txt`, same-named files in different
|
|
31
|
+
* directories collided, and a Windows artifact was structurally different
|
|
32
|
+
* from the POSIX `zip -qr` one; and `-Path` expands wildcards, so a literal
|
|
33
|
+
* `report[1].txt` matched nothing and failed the upload. Stating every entry
|
|
34
|
+
* name explicitly through `CreateEntryFromFile(zip, source, entryName)`
|
|
35
|
+
* removes both: the source is `Path.Combine(root, rel)`, taken literally,
|
|
36
|
+
* and the entry name is `rel` with `\` turned to `/`, which is what `zip`
|
|
37
|
+
* records on POSIX.
|
|
38
|
+
*
|
|
39
|
+
* **The Windows script is constant-size: the file list travels in a manifest
|
|
40
|
+
* file, never in `-Command`.** `CreateProcessW` caps the whole command line
|
|
41
|
+
* at 32,767 characters, and inlining one `CreateEntryFromFile` statement per
|
|
42
|
+
* file (~214 characters each) hit that ceiling at roughly 150 files with
|
|
43
|
+
* `ENAMETOOLONG` — fewer than the `Compress-Archive` form it replaced carried.
|
|
44
|
+
* The script now reads {@link zipManifest}'s output (one relative path per
|
|
45
|
+
* line) with `File.ReadAllLines` and loops. **The two branches deliberately
|
|
46
|
+
* differ in mechanism**: POSIX `zip` takes the files as argv, whose ceiling
|
|
47
|
+
* (`ARG_MAX`, megabytes on the hosted runners) is far larger, so the only
|
|
48
|
+
* file-count ceiling left is that POSIX argv limit. The manifest is written by
|
|
49
|
+
* the caller (`Artifact.zip`) as UTF-8 WITHOUT a BOM, which is what
|
|
50
|
+
* `ReadAllLines` reads by default. Its BOM detection would consume a BOM
|
|
51
|
+
* rather than leak it into the first path (measured), so this is a
|
|
52
|
+
* plain-bytes preference, not a correctness guard.
|
|
53
|
+
*
|
|
54
|
+
* **Every `CreateEntryFromFile` is assigned to `$null`.** Unassigned, pwsh
|
|
55
|
+
* writes the returned `ZipArchiveEntry` to stdout — roughly 430 bytes of
|
|
56
|
+
* formatted object per file — and `internal/spawn.ts` interleaves stdout with
|
|
57
|
+
* stderr into the captured output, which is what `ArtifactError.stderr`
|
|
58
|
+
* carries. Left in, that chatter buried the one line that matters, the .NET
|
|
59
|
+
* exception, under kilobytes of entry listings.
|
|
60
|
+
*
|
|
61
|
+
* Every path handed to PowerShell is single-quoted, and a single quote inside
|
|
62
|
+
* one is doubled — the only escape a single-quoted PowerShell literal has.
|
|
63
|
+
*
|
|
64
|
+
* Pure: no filesystem, no environment, no spawn. `internal/spawn.ts` is the
|
|
65
|
+
* execution half. Reaches `effect/unstable/process` alone, so it is safe for
|
|
66
|
+
* any module to import (`__test__/reachability.test.ts`).
|
|
67
|
+
*
|
|
68
|
+
* @internal
|
|
69
|
+
*/
|
|
70
|
+
/** A PowerShell single-quoted literal: `'` becomes `''`, nothing else is special. */
|
|
71
|
+
const pwshLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
72
|
+
/** A non-interactive pwsh invocation of one command line. */
|
|
73
|
+
const pwsh = (script, options) => ChildProcess.make("pwsh", [
|
|
74
|
+
"-NoProfile",
|
|
75
|
+
"-NonInteractive",
|
|
76
|
+
"-Command",
|
|
77
|
+
script
|
|
78
|
+
], options);
|
|
79
|
+
/** {@link ZipCommandOptions.level} clamped to `zip`'s `0..9` and truncated. */
|
|
80
|
+
const clampLevel = (level) => Math.min(9, Math.max(0, Math.trunc(level)));
|
|
81
|
+
/** The .NET `CompressionLevel` member a clamped zlib level maps onto. */
|
|
82
|
+
const dotnetCompressionLevel = (level) => level === 0 ? "NoCompression" : level <= 3 ? "Fastest" : level <= 8 ? "Optimal" : "SmallestSize";
|
|
83
|
+
/**
|
|
84
|
+
* The manifest the Windows zip script reads: `files` one per line, `\n`-joined,
|
|
85
|
+
* with a trailing newline. Pure; the caller writes it (UTF-8, no BOM).
|
|
86
|
+
*
|
|
87
|
+
* @remarks
|
|
88
|
+
* A path containing `\n` or `\r` is unrepresentable — it would split into two
|
|
89
|
+
* entries — and this function does not check; `Artifact.zip` rejects such a
|
|
90
|
+
* file as `invalidOptions`, naming it, before anything is written.
|
|
91
|
+
*
|
|
92
|
+
* @internal
|
|
93
|
+
*/
|
|
94
|
+
const zipManifest = (files) => files.map((file) => `${file}\n`).join("");
|
|
95
|
+
/**
|
|
96
|
+
* Pack `files` into a zip: `zip -<level> -qr <destination> <files...>` with
|
|
97
|
+
* `root` as the working directory, or on Windows a pwsh script that opens
|
|
98
|
+
* `destination` with `ZipFile.Open(..., Create)` after a `File.Delete` (the
|
|
99
|
+
* overwrite) and adds one `CreateEntryFromFile` per line of `manifest`,
|
|
100
|
+
* naming each entry explicitly — under `$ErrorActionPreference = 'Stop'` and
|
|
101
|
+
* a stderr-writing try/catch, and with the archive disposed in a `finally` so
|
|
102
|
+
* a failed entry cannot leave the handle open (the module doc says why not
|
|
103
|
+
* `Compress-Archive`, and why the list travels in a file).
|
|
104
|
+
*
|
|
105
|
+
* @internal
|
|
106
|
+
*/
|
|
107
|
+
const zipCommand = (options) => {
|
|
108
|
+
const level = clampLevel(options.level);
|
|
109
|
+
if (!options.windows) return ChildProcess.make("zip", [
|
|
110
|
+
`-${level}`,
|
|
111
|
+
"-qr",
|
|
112
|
+
options.destination,
|
|
113
|
+
...options.files
|
|
114
|
+
], { cwd: options.root });
|
|
115
|
+
const destination = pwshLiteral(options.destination);
|
|
116
|
+
const root = pwshLiteral(options.root);
|
|
117
|
+
const manifest = pwshLiteral(options.manifest);
|
|
118
|
+
const compression = `[System.IO.Compression.CompressionLevel]::${dotnetCompressionLevel(level)}`;
|
|
119
|
+
return pwsh(`\$ErrorActionPreference = 'Stop'; try { Add-Type -AssemblyName System.IO.Compression; Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.File]::Delete(${destination}); $zip = [System.IO.Compression.ZipFile]::Open(${destination}, [System.IO.Compression.ZipArchiveMode]::Create); try { foreach ($rel in [System.IO.File]::ReadAllLines(${manifest})) { if ($rel -ne '') { $null = [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, [System.IO.Path]::Combine(${root}, $rel), $rel.Replace('\\', '/'), ${compression}) } } } finally { \$zip.Dispose() } } catch { [Console]::Error.WriteLine(\$_.Exception.ToString()); exit 1 }`, { cwd: options.root });
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Unpack a zip into `destination`, overwriting: `unzip -oq <source> -d
|
|
123
|
+
* <destination>`, or the three-argument `ExtractToDirectory` overload under
|
|
124
|
+
* `$ErrorActionPreference = 'Stop'` and a stderr-writing try/catch on
|
|
125
|
+
* Windows (the module doc says why both halves are load-bearing).
|
|
126
|
+
*
|
|
127
|
+
* @internal
|
|
128
|
+
*/
|
|
129
|
+
const unzipCommand = (options) => options.windows ? pwsh(`\$ErrorActionPreference = 'Stop'; try { Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory(${pwshLiteral(options.source)}, ${pwshLiteral(options.destination)}, $true) } catch { [Console]::Error.WriteLine(\$_.Exception.ToString()); exit 1 }`) : ChildProcess.make("unzip", [
|
|
130
|
+
"-oq",
|
|
131
|
+
options.source,
|
|
132
|
+
"-d",
|
|
133
|
+
options.destination
|
|
134
|
+
]);
|
|
135
|
+
/**
|
|
136
|
+
* Extract a tarball: `tar <flags> <archive> -C <destination>`. The same on
|
|
137
|
+
* every platform — bsdtar ships on the Windows runner image — so unlike the
|
|
138
|
+
* zip pair there is no `windows` switch.
|
|
139
|
+
*
|
|
140
|
+
* @internal
|
|
141
|
+
*/
|
|
142
|
+
const tarExtractCommand = (options) => {
|
|
143
|
+
const flags = options.flags === void 0 || options.flags.length === 0 ? ["xzf"] : [...options.flags];
|
|
144
|
+
return ChildProcess.make("tar", [
|
|
145
|
+
...flags,
|
|
146
|
+
options.archive,
|
|
147
|
+
"-C",
|
|
148
|
+
options.destination
|
|
149
|
+
]);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
//#endregion
|
|
153
|
+
export { tarExtractCommand, unzipCommand, zipCommand, zipManifest };
|
package/internal/digest.js
CHANGED
|
@@ -6,11 +6,12 @@ import { createHash } from "node:crypto";
|
|
|
6
6
|
* The sanctioned `node:crypto` digests, spelled once.
|
|
7
7
|
*
|
|
8
8
|
* @remarks
|
|
9
|
-
* Core `Crypto`
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* Core `Crypto` offers one-shot SHA digests over bytes already in memory, but
|
|
10
|
+
* no HMAC and no incremental hasher, so this package's licence for a `node:`
|
|
11
|
+
* import covers hashing (see the Module concept in `okf/`). Keeping every
|
|
12
|
+
* `createHash` behind this module is what makes that licence auditable in one
|
|
13
|
+
* place rather than six, and keeps the streamed file digest — the shape a
|
|
14
|
+
* multi-gigabyte toolchain archive needs — from being re-derived per caller.
|
|
14
15
|
*
|
|
15
16
|
* @internal
|
|
16
17
|
*/
|
package/internal/fsProbe.js
CHANGED
|
@@ -9,6 +9,14 @@ import { Effect } from "effect";
|
|
|
9
9
|
* @internal
|
|
10
10
|
*/
|
|
11
11
|
const typeAt = (fs, path) => Effect.map(Effect.option(fs.stat(path)), (info) => info._tag === "Some" ? info.value.type : void 0);
|
|
12
|
+
/**
|
|
13
|
+
* Whether a thrown value is a Node errno error with the given code — the
|
|
14
|
+
* platform's `SystemError` keeps the raw exception as its `cause`, and codes
|
|
15
|
+
* it maps to no named tag (`EXDEV`, `ESRCH`) are only recoverable from there.
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
const isErrno = (cause, code) => typeof cause === "object" && cause !== null && cause.code === code;
|
|
12
20
|
|
|
13
21
|
//#endregion
|
|
14
|
-
export { typeAt };
|
|
22
|
+
export { isErrno, typeAt };
|
package/internal/runner.js
CHANGED
|
@@ -19,9 +19,9 @@ const isWindowsRunner = (env) => Effect.map(env.getOptional("RUNNER_OS"), (found
|
|
|
19
19
|
* Resolved at layer construction, never at import: the source package read
|
|
20
20
|
* the variable into a module-level constant, which fixed the root before any
|
|
21
21
|
* layer could say otherwise and made it impossible to point a test elsewhere.
|
|
22
|
-
* `
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* `ToolInstaller` is its only reader: a consumer that needs the final cache
|
|
23
|
+
* path (`PackageManagerInstaller`, for its shims) asks the installer's own
|
|
24
|
+
* `cachePath` member rather than resolving the root a second time.
|
|
25
25
|
*
|
|
26
26
|
* @internal
|
|
27
27
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/github-actions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The GitHub Actions runtime: env, inputs, outputs, state, logging, cache, artifacts, blob storage, OIDC and the token bridge.",
|
|
6
6
|
"keywords": [
|
|
@@ -39,18 +39,18 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@azure/storage-blob": "^12.33.0",
|
|
42
|
-
"@effected/github": "^0.
|
|
43
|
-
"@effected/glob": "^0.
|
|
44
|
-
"@effected/markdown": "^0.
|
|
45
|
-
"@effected/npm": "^0.
|
|
46
|
-
"@effected/sbom": "^0.
|
|
47
|
-
"@effected/semver": "^0.
|
|
48
|
-
"@effected/templates": "^0.
|
|
49
|
-
"@effected/walker": "^0.
|
|
42
|
+
"@effected/github": "^0.12.0",
|
|
43
|
+
"@effected/glob": "^0.8.0",
|
|
44
|
+
"@effected/markdown": "^0.12.0",
|
|
45
|
+
"@effected/npm": "^0.16.0",
|
|
46
|
+
"@effected/sbom": "^0.8.0",
|
|
47
|
+
"@effected/semver": "^0.9.0",
|
|
48
|
+
"@effected/templates": "^0.8.0",
|
|
49
|
+
"@effected/walker": "^0.12.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@effect/platform-node": "4.0.0-rc.
|
|
53
|
-
"effect": "4.0.0-rc.
|
|
52
|
+
"@effect/platform-node": "4.0.0-rc.117",
|
|
53
|
+
"effect": "4.0.0-rc.117"
|
|
54
54
|
},
|
|
55
55
|
"engines": {
|
|
56
56
|
"node": ">=24.11.0"
|