@effected/github-actions 0.13.1 → 0.13.3

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.
@@ -3,6 +3,7 @@ import { unstubbed } from "./internal/unstubbed.js";
3
3
  import { digestFileHex } from "./internal/digest.js";
4
4
  import { isWindowsRunner, toolCacheRoot } from "./internal/runner.js";
5
5
  import { typeAt } from "./internal/fsProbe.js";
6
+ import { PNPM_EXE_PREFIX, detectMusl, isNodeScript, pnpmExeTarget, strongestSri } from "./internal/pnpmExe.js";
6
7
  import { ToolInstaller } from "./ToolInstaller.js";
7
8
  import { Context, Effect, FileSystem, Layer, Option, Path, Schema } from "effect";
8
9
  import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
@@ -22,9 +23,11 @@ var PackageManagerInstallerError = class extends Schema.TaggedError()("PackageMa
22
23
  * entry). `integrityMismatch` — the downloaded artifact does not hash to
23
24
  * what the pin declares; nothing was cached. `integrityMissing` — the pin
24
25
  * carries no integrity and the caller asked for `requireIntegrity`.
25
- * `unsupportedPlatform` — bun publishes no build for this runner's
26
- * OS/architecture pair. `layoutUnexpected` — the artifact extracted, but
27
- * its contents are not shaped like the package manager it claims to be.
26
+ * `unsupportedPlatform` — no build exists for this runner's
27
+ * OS/architecture pair: bun publishes none, or pnpm 12 ships no
28
+ * `@pnpm/exe.*` native binary for it. `layoutUnexpected` — the artifact
29
+ * extracted, but its contents are not shaped like the package manager it
30
+ * claims to be.
28
31
  */
29
32
  reason: Schema.Literals([
30
33
  "downloadFailed",
@@ -93,10 +96,14 @@ var AmbientPackageManager = class extends Schema.Class("AmbientPackageManager")(
93
96
  * on Windows); for bun it is the entry directory itself, because the cached
94
97
  * `bun` binary is directly executable and a shim would add nothing but a
95
98
  * fork. `bins` still maps each published bin name to the underlying entry —
96
- * a Node script to run with `node` for npm/pnpm/yarn, the native binary for
97
- * bun. Calling `addPath` remains deliberately the consumer's move, and note
98
- * that `addPath` targets *subsequent* steps: a same-process probe must use
99
- * the absolute paths in `bins` or `binDir`.
99
+ * a Node script to run with `node` for npm, yarn and pnpm 11 and earlier; an
100
+ * executable to run directly for bun and for pnpm 12+, whose `pnpm` is the
101
+ * host's native binary (overlaid onto the cached wrapper the way pnpm's own
102
+ * install script does) and whose `pn`/`pnpx`/`pnx` are shell aliases of it.
103
+ * Each shim already applies that distinction. Calling `addPath` remains
104
+ * deliberately the consumer's move, and note that `addPath` targets
105
+ * *subsequent* steps: a same-process probe must use the absolute paths in
106
+ * `bins` or `binDir`.
100
107
  *
101
108
  * @public
102
109
  */
@@ -136,12 +143,23 @@ const InstalledPackageManager = Schema.Union([AmbientPackageManager, CachedPacka
136
143
  /** The shim directory name inside a cached npm-registry-manager entry. */
137
144
  const SHIM_DIR = ".bin";
138
145
  /**
139
- * The POSIX shim body: an `exec` wrapper so the shim's process *becomes*
140
- * node, and `"$@"` so arguments survive quoting intact.
146
+ * The POSIX shim body: an `exec` wrapper so the shim's process *becomes* the
147
+ * target, and `"$@"` so arguments survive quoting intact. A Node script
148
+ * (`.js`/`.mjs`/`.cjs`) runs under `node`; anything else — pnpm 12's native
149
+ * binary, its `#!/bin/sh` alias scripts — is exec'd directly, because handing
150
+ * a shell script or a Mach-O to node is exactly the "Invalid or unexpected
151
+ * token" failure this rule exists to prevent.
141
152
  */
142
- const posixShim = (target) => `#!/bin/sh\nexec node "${target}" "$@"\n`;
143
- /** The Windows shim body, CRLF-terminated as cmd expects. */
144
- const cmdShim = (target) => `@echo off\r\nnode "${target}" %*\r\n`;
153
+ const posixShim = (target) => isNodeScript(target) ? `#!/bin/sh\nexec node "${target}" "$@"\n` : `#!/bin/sh\nexec "${target}" "$@"\n`;
154
+ /** The Windows shim body, CRLF-terminated as cmd expects; same node-vs-direct rule. */
155
+ const cmdShim = (target) => isNodeScript(target) ? `@echo off\r\nnode "${target}" %*\r\n` : `@echo off\r\n"${target}" %*\r\n`;
156
+ /** The bin names pnpm's wrapper publishes, every one of which becomes the native binary on Windows. */
157
+ const PNPM_NATIVE_BIN_NAMES = [
158
+ "pnpm",
159
+ "pn",
160
+ "pnpx",
161
+ "pnx"
162
+ ];
145
163
  /**
146
164
  * The bun release asset name for a runner platform, or `None` when bun
147
165
  * publishes no build for it.
@@ -201,6 +219,7 @@ const make = Effect.gen(function* () {
201
219
  onSome: archFromRunner
202
220
  }));
203
221
  const windows = yield* isWindowsRunner(env);
222
+ const musl = detectMusl();
204
223
  const bunBinaryName = windows ? "bun.exe" : "bun";
205
224
  const shimFileName = (name) => windows ? `${name}.cmd` : name;
206
225
  const shimBody = windows ? cmdShim : posixShim;
@@ -265,13 +284,18 @@ const make = Effect.gen(function* () {
265
284
  }));
266
285
  });
267
286
  /**
268
- * The bin entries a package directory publishes, from its own manifest.
287
+ * What a package directory's own manifest says about its entry points: the
288
+ * `bin` map, plus the `@pnpm/exe.*` optional dependencies that mark the
289
+ * pnpm 12 native-binary layout (empty for every other manager and version).
269
290
  *
270
291
  * Read from the artifact rather than hardcoded per manager, because the
271
292
  * layouts genuinely move — corepack's own table has pnpm's entry at
272
- * `bin/pnpm.cjs` through v10 and `bin/pnpm.mjs` from v11.
293
+ * `bin/pnpm.cjs` through v10 and `bin/pnpm.mjs` from v11, and from v12 the
294
+ * `pnpm` bin is a placeholder for a native binary shipped as a separate
295
+ * package. Both maps are attacker-supplied bytes and are normalized to
296
+ * string → string before anything downstream reads them.
273
297
  */
274
- const readPackageBins = (pin, packageDir) => Effect.gen(function* () {
298
+ const readPackageManifest = (pin, packageDir) => Effect.gen(function* () {
275
299
  const manifestPath = path.join(packageDir, "package.json");
276
300
  const raw = yield* fs.readFileString(manifestPath).pipe(Effect.mapError((cause) => errorFor(pin)({
277
301
  reason: "layoutUnexpected",
@@ -300,8 +324,41 @@ const make = Effect.gen(function* () {
300
324
  }));
301
325
  yield* assertFile(pin, target, `bin ${name} (${relative}) is missing`);
302
326
  }
303
- return bins;
327
+ return {
328
+ bins,
329
+ nativePackages: nativePackagesOf(manifest.optionalDependencies)
330
+ };
304
331
  });
332
+ /**
333
+ * The `@pnpm/exe.<target>` → version entries of an `optionalDependencies`
334
+ * value, and nothing else: only well-formed string pairs under that prefix
335
+ * survive, so a hostile or malformed map cannot smuggle a package name
336
+ * into a registry url.
337
+ */
338
+ const nativePackagesOf = (optionalDependencies) => {
339
+ if (typeof optionalDependencies !== "object" || optionalDependencies === null) return {};
340
+ const entries = {};
341
+ for (const [name, version] of Object.entries(optionalDependencies)) if (name.startsWith("@pnpm/exe.") && typeof version === "string") entries[name] = version;
342
+ return entries;
343
+ };
344
+ /**
345
+ * pnpm's Windows overlay places the native binary as `<name>.exe` for every
346
+ * wrapper bin and repoints `bin` at those; this is that repoint, applied to
347
+ * the in-memory map (the shims and the record) rather than the manifest.
348
+ */
349
+ const nativeWindowsBins = (bins) => Object.fromEntries(Object.entries(bins).map(([name, relative]) => PNPM_NATIVE_BIN_NAMES.includes(name) ? [name, `${relative}.exe`] : [name, relative]));
350
+ /**
351
+ * Whether a file is still pnpm's shebang-less placeholder (a `#`-led text
352
+ * file that is not a `#!` script) rather than the native binary meant to
353
+ * overlay it — the mark of an entry written by a version of this module
354
+ * that predates the overlay, or by a foreign writer that ran no lifecycle
355
+ * scripts. Anything else (a shebang, an executable's magic, an unreadable
356
+ * or empty file) is left to the ordinary layout checks.
357
+ */
358
+ const isPlaceholder = (file) => Effect.scoped(Effect.flatMap(fs.open(file), (handle) => handle.readAlloc(2))).pipe(Effect.map((head) => Option.match(head, {
359
+ onNone: () => false,
360
+ onSome: (bytes) => bytes.length >= 1 && bytes[0] === 35 && bytes[1] !== 33
361
+ })), Effect.orElseSucceed(() => false));
305
362
  const rootBins = (bins, directory) => Object.fromEntries(Object.entries(bins).map(([name, relative]) => [name, path.join(directory, relative)]));
306
363
  /**
307
364
  * Write one executable shim per bin into `<into>/.bin`, each invoking
@@ -333,28 +390,118 @@ const make = Effect.gen(function* () {
333
390
  if (!windows) yield* fs.chmod(shim, 493).pipe(Effect.mapError(cacheError(shim)));
334
391
  }
335
392
  });
336
- /** The record for a directory already in the cache, regenerating absent shims. */
393
+ /**
394
+ * The record for a directory already in the cache, regenerating absent
395
+ * shims — or `None` when the entry is a pnpm 12 wrapper whose `pnpm` bin is
396
+ * still the placeholder, which no shim can make runnable: the caller then
397
+ * reinstalls over it through the ordinary install path (ToolInstaller's
398
+ * swap removes the old entry and renames the complete new one into place).
399
+ */
337
400
  const cachedRecord = (pin, directory) => Effect.gen(function* () {
338
401
  if (pin.name === "bun") {
339
402
  const binary = path.join(directory, bunBinaryName);
340
403
  yield* assertFile(pin, binary, `cached bun binary (${bunBinaryName}) is missing`);
341
- return CachedPackageManager.make({
404
+ return Option.some(CachedPackageManager.make({
342
405
  name: pin.name,
343
406
  version: pin.version.toString(),
344
407
  directory,
345
408
  binDir: directory,
346
409
  bins: { bun: binary }
347
- });
410
+ }));
411
+ }
412
+ const manifest = yield* readPackageManifest(pin, directory);
413
+ let bins = manifest.bins;
414
+ if (Object.keys(manifest.nativePackages).length > 0) {
415
+ const entry = bins[pin.name];
416
+ if (entry !== void 0 && (yield* isPlaceholder(path.join(directory, entry)))) {
417
+ yield* Effect.logInfo(`The cached ${pin.name}@${pin.version.toString()} still holds pnpm's placeholder bin; reinstalling it over.`);
418
+ return Option.none();
419
+ }
420
+ if (windows) bins = nativeWindowsBins(bins);
348
421
  }
349
- const bins = yield* readPackageBins(pin, directory);
350
422
  yield* writeShims(pin, directory, directory, bins, { skipExisting: true });
351
- return CachedPackageManager.make({
423
+ return Option.some(CachedPackageManager.make({
352
424
  name: pin.name,
353
425
  version: pin.version.toString(),
354
426
  directory,
355
427
  binDir: path.join(directory, SHIM_DIR),
356
428
  bins: rootBins(bins, directory)
429
+ }));
430
+ });
431
+ /**
432
+ * pnpm 12's native overlay, done here because the installer never runs the
433
+ * lifecycle script that does it upstream: pick the host's `@pnpm/exe.*`
434
+ * package from the wrapper's optional dependencies, fetch its tarball from
435
+ * the SAME registry the wrapper came from, verify it against the registry's
436
+ * own `dist.integrity` (fail-closed — this is a second artifact the pin
437
+ * never named, so there is no integrity-less posture to honor), and copy
438
+ * the executable over the placeholder in the STAGED wrapper, where `dist/`
439
+ * sits beside it as the binary expects. Answers the bins as they should be
440
+ * shimmed and recorded — retargeted to `.exe` on Windows, as upstream does.
441
+ */
442
+ const overlayNativeBinary = (pin, packageDir, bins, nativePackages, registry) => Effect.gen(function* () {
443
+ const target = Option.getOrUndefined(pnpmExeTarget(runnerOs, arch, musl));
444
+ const packageName = target === void 0 ? void 0 : `${PNPM_EXE_PREFIX}${target}`;
445
+ const nativeVersion = packageName === void 0 ? void 0 : nativePackages[packageName];
446
+ if (target === void 0 || packageName === void 0 || nativeVersion === void 0) return yield* Effect.fail(errorFor(pin)({
447
+ reason: "unsupportedPlatform",
448
+ subject: `${runnerOs || "unknown"}/${arch}`
449
+ }));
450
+ if (nativeVersion !== pin.version.toString()) return yield* Effect.fail(errorFor(pin)({
451
+ reason: "layoutUnexpected",
452
+ subject: `${packageName} is pinned at ${nativeVersion}, not the wrapper's ${pin.version.toString()}`
453
+ }));
454
+ const placeholder = bins[pin.name];
455
+ if (placeholder === void 0) return yield* Effect.fail(errorFor(pin)({
456
+ reason: "layoutUnexpected",
457
+ subject: `package.json names no ${pin.name} bin to overlay`
458
+ }));
459
+ const packumentUrl = `${registry}/${packageName}/${nativeVersion}`;
460
+ const packumentFile = yield* installer.download(packumentUrl).pipe(Effect.mapError(fromInstaller(pin)));
461
+ const unverifiable = (cause) => errorFor(pin)({
462
+ reason: "integrityMismatch",
463
+ subject: packumentUrl,
464
+ ...cause === void 0 ? {} : { cause }
465
+ });
466
+ const declared = (yield* fs.readFileString(packumentFile).pipe(Effect.mapError((cause) => unverifiable(cause)), Effect.flatMap((raw) => Effect.try({
467
+ try: () => JSON.parse(raw),
468
+ catch: (cause) => unverifiable(cause)
469
+ })))).dist?.integrity;
470
+ const expected = typeof declared === "string" ? Option.getOrUndefined(strongestSri(declared)) : void 0;
471
+ if (expected === void 0) return yield* Effect.fail(unverifiable());
472
+ const tarballUrl = `${registry}/${packageName}/-/exe.${target}-${nativeVersion}.tgz`;
473
+ const archive = yield* installer.download(tarballUrl).pipe(Effect.mapError(fromInstaller(pin)));
474
+ const actualHex = yield* hashFile(pin, archive, expected.algorithm);
475
+ if (actualHex !== expected.hex) return yield* Effect.fail(errorFor(pin)({
476
+ reason: "integrityMismatch",
477
+ subject: tarballUrl,
478
+ expected: `${expected.algorithm}.${expected.hex}`,
479
+ actual: `${expected.algorithm}.${actualHex}`
480
+ }));
481
+ const extracted = yield* installer.extractTar(archive).pipe(Effect.mapError(fromInstaller(pin)));
482
+ const nativeName = windows ? "pnpm.exe" : "pnpm";
483
+ const native = path.join(extracted, "package", nativeName);
484
+ yield* assertFile(pin, native, `package/${nativeName} is missing from the ${packageName} tarball`);
485
+ const cacheError = (subject) => (cause) => errorFor(pin)({
486
+ reason: "cacheFailed",
487
+ subject,
488
+ cause
357
489
  });
490
+ if (windows) {
491
+ for (const name of PNPM_NATIVE_BIN_NAMES) {
492
+ const relative = bins[name];
493
+ if (relative === void 0) continue;
494
+ for (const destination of [path.join(packageDir, relative), path.join(packageDir, `${relative}.exe`)]) yield* fs.copyFile(native, destination).pipe(Effect.mapError(cacheError(destination)));
495
+ }
496
+ return nativeWindowsBins(bins);
497
+ }
498
+ const destination = path.join(packageDir, placeholder);
499
+ yield* fs.copyFile(native, destination).pipe(Effect.mapError(cacheError(destination)));
500
+ for (const relative of Object.values(bins)) if (!isNodeScript(relative)) {
501
+ const file = path.join(packageDir, relative);
502
+ yield* fs.chmod(file, 493).pipe(Effect.mapError(cacheError(file)));
503
+ }
504
+ return bins;
358
505
  });
359
506
  /** Download bun's per-platform zip from GitHub releases and cache the binary. */
360
507
  const installBun = (pin) => Effect.gen(function* () {
@@ -397,7 +544,8 @@ const make = Effect.gen(function* () {
397
544
  reason: "layoutUnexpected",
398
545
  subject: "tarball has no package/ root"
399
546
  }));
400
- const bins = yield* readPackageBins(pin, packageDir);
547
+ const manifest = yield* readPackageManifest(pin, packageDir);
548
+ const bins = Object.keys(manifest.nativePackages).length > 0 ? yield* overlayNativeBinary(pin, packageDir, manifest.bins, manifest.nativePackages, registry) : manifest.bins;
401
549
  if (berry) {
402
550
  const cli = path.join(packageDir, "bin/yarn.js");
403
551
  yield* assertFile(pin, cli, "bin/yarn.js is missing from the cli-dist tarball");
@@ -426,7 +574,10 @@ const make = Effect.gen(function* () {
426
574
  });
427
575
  if (options?.requireIntegrity === true && pin.integrity === void 0) return yield* Effect.fail(errorFor(pin)({ reason: "integrityMissing" }));
428
576
  const cached = yield* installer.find(pin.name, version);
429
- if (Option.isSome(cached)) return yield* cachedRecord(pin, cached.value);
577
+ if (Option.isSome(cached)) {
578
+ const record = yield* cachedRecord(pin, cached.value);
579
+ if (Option.isSome(record)) return record.value;
580
+ }
430
581
  if (pin.name === "npm" && options?.allowAmbient !== false) {
431
582
  const probed = yield* ambientNpmVersion;
432
583
  if (Option.isSome(probed) && probed.value === version) return AmbientPackageManager.make({
package/index.d.ts CHANGED
@@ -4490,9 +4490,11 @@ declare const PackageManagerInstallerError_base: Schema.Class<PackageManagerInst
4490
4490
  * entry). `integrityMismatch` — the downloaded artifact does not hash to
4491
4491
  * what the pin declares; nothing was cached. `integrityMissing` — the pin
4492
4492
  * carries no integrity and the caller asked for `requireIntegrity`.
4493
- * `unsupportedPlatform` — bun publishes no build for this runner's
4494
- * OS/architecture pair. `layoutUnexpected` — the artifact extracted, but
4495
- * its contents are not shaped like the package manager it claims to be.
4493
+ * `unsupportedPlatform` — no build exists for this runner's
4494
+ * OS/architecture pair: bun publishes none, or pnpm 12 ships no
4495
+ * `@pnpm/exe.*` native binary for it. `layoutUnexpected` — the artifact
4496
+ * extracted, but its contents are not shaped like the package manager it
4497
+ * claims to be.
4496
4498
  */
4497
4499
  readonly reason: Schema.Literals<readonly ["downloadFailed", "extractFailed", "integrityMismatch", "integrityMissing", "unsupportedPlatform", "layoutUnexpected", "cacheFailed"]>;
4498
4500
  /** The package manager being installed. */
@@ -4530,10 +4532,14 @@ interface PackageManagerInstallOptions {
4530
4532
  readonly requireIntegrity?: boolean | undefined;
4531
4533
  /**
4532
4534
  * The npm registry host the npm, pnpm and yarn tarballs download from.
4533
- * Defaults to `https://registry.npmjs.org`; a corporate mirror that serves
4534
- * the standard `/<name>/-/<basename>-<version>.tgz` tarball routes works
4535
- * unchanged. bun does not ship through a registry — its per-platform zip
4536
- * always comes from GitHub releases.
4535
+ * Defaults to `https://registry.npmjs.org`. A corporate mirror qualifies
4536
+ * when it serves the standard `/<name>/-/<basename>-<version>.tgz` tarball
4537
+ * route and — for pnpm 12 and later, whose native binary is a second
4538
+ * artifact verified against the registry's own integrity — the
4539
+ * `/<name>/<version>` packument route as JSON carrying `dist.integrity`.
4540
+ * A tarball-only proxy fails a pnpm 12 pin with `downloadFailed` naming
4541
+ * that packument url. bun does not ship through a registry — its
4542
+ * per-platform zip always comes from GitHub releases.
4537
4543
  */
4538
4544
  readonly registry?: string | undefined;
4539
4545
  /**
@@ -4600,10 +4606,14 @@ declare const CachedPackageManager_base: Schema.Class<CachedPackageManager, Sche
4600
4606
  * on Windows); for bun it is the entry directory itself, because the cached
4601
4607
  * `bun` binary is directly executable and a shim would add nothing but a
4602
4608
  * fork. `bins` still maps each published bin name to the underlying entry —
4603
- * a Node script to run with `node` for npm/pnpm/yarn, the native binary for
4604
- * bun. Calling `addPath` remains deliberately the consumer's move, and note
4605
- * that `addPath` targets *subsequent* steps: a same-process probe must use
4606
- * the absolute paths in `bins` or `binDir`.
4609
+ * a Node script to run with `node` for npm, yarn and pnpm 11 and earlier; an
4610
+ * executable to run directly for bun and for pnpm 12+, whose `pnpm` is the
4611
+ * host's native binary (overlaid onto the cached wrapper the way pnpm's own
4612
+ * install script does) and whose `pn`/`pnpx`/`pnx` are shell aliases of it.
4613
+ * Each shim already applies that distinction. Calling `addPath` remains
4614
+ * deliberately the consumer's move, and note that `addPath` targets
4615
+ * *subsequent* steps: a same-process probe must use the absolute paths in
4616
+ * `bins` or `binDir`.
4607
4617
  *
4608
4618
  * @public
4609
4619
  */
@@ -0,0 +1,124 @@
1
+ import { Encoding, Option, Result } from "effect";
2
+
3
+ //#region src/internal/pnpmExe.ts
4
+ /**
5
+ * The pnpm native-binary layout: from pnpm 12 the `pnpm` registry package is a
6
+ * thin wrapper whose `pnpm` bin is a shebang-less placeholder that the
7
+ * package's own install script overwrites with the host's native executable,
8
+ * fetched as an optional dependency named `@pnpm/exe.<os>-<arch>[-musl]`.
9
+ * The installer never runs lifecycle scripts, so it performs that overlay
10
+ * itself; the pure pieces of that live here, where they are testable without
11
+ * a tarball.
12
+ *
13
+ * Detection is by LAYOUT (the manifest's `optionalDependencies` naming an
14
+ * `@pnpm/exe.*` package), never by major version — pnpm 11 and earlier ship
15
+ * no such dependency and keep their Node entry points.
16
+ *
17
+ * @internal
18
+ */
19
+ /** The prefix every native-binary package name carries. @internal */
20
+ const PNPM_EXE_PREFIX = "@pnpm/exe.";
21
+ /**
22
+ * The targets pnpm publishes a native binary for — the `PLATFORMS` table of
23
+ * the wrapper's own `native-binary.mjs`, flattened to the spellings that
24
+ * follow the prefix. Only linux x64/arm64 have a musl twin.
25
+ */
26
+ const PNPM_EXE_TARGETS = /* @__PURE__ */ new Set([
27
+ "linux-x64",
28
+ "linux-arm64",
29
+ "linux-x64-musl",
30
+ "linux-arm64-musl",
31
+ "linux-riscv64",
32
+ "linux-ppc64",
33
+ "linux-s390x",
34
+ "darwin-x64",
35
+ "darwin-arm64",
36
+ "win32-x64",
37
+ "win32-arm64",
38
+ "freebsd-x64",
39
+ "android-x64",
40
+ "android-arm64"
41
+ ]);
42
+ /** `RUNNER_OS` spellings (lower-cased) onto Node's `process.platform` spellings, which the targets use. */
43
+ const RUNNER_OS_TO_PLATFORM = {
44
+ linux: "linux",
45
+ macos: "darwin",
46
+ windows: "win32"
47
+ };
48
+ /**
49
+ * The `@pnpm/exe.*` target for a runner platform, or `None` when pnpm
50
+ * publishes no native binary for it.
51
+ *
52
+ * `musl` only distinguishes on linux; an arch released for glibc alone
53
+ * (riscv64, ppc64, s390x) answers `None` on a musl host, as upstream does —
54
+ * the glibc binary cannot run there.
55
+ *
56
+ * @internal
57
+ */
58
+ const pnpmExeTarget = (runnerOs, arch, musl) => {
59
+ const platform = RUNNER_OS_TO_PLATFORM[runnerOs.toLowerCase()];
60
+ if (platform === void 0) return Option.none();
61
+ const target = `${platform}-${arch}${platform === "linux" && musl ? "-musl" : ""}`;
62
+ return PNPM_EXE_TARGETS.has(target) ? Option.some(target) : Option.none();
63
+ };
64
+ /**
65
+ * Whether the HOST libc is musl, the way the wrapper's `native-binary.mjs`
66
+ * decides it: a glibc build of node reports `glibcVersionRuntime` in its
67
+ * diagnostic report header, a musl build does not. Only meaningful on a
68
+ * linux host; anything unprobeable counts as glibc, which is what every
69
+ * hosted runner is.
70
+ *
71
+ * `process.report` is a global, not a `node:` import, so this stays inside
72
+ * the package's closed `node:` list.
73
+ *
74
+ * @internal
75
+ */
76
+ const detectMusl = () => {
77
+ if (process.platform !== "linux") return false;
78
+ try {
79
+ const report = process.report?.getReport();
80
+ if (report === void 0 || report.header === void 0) return false;
81
+ return report.header.glibcVersionRuntime === void 0 || report.header.glibcVersionRuntime === "";
82
+ } catch {
83
+ return false;
84
+ }
85
+ };
86
+ /** The SRI algorithms a registry may list, strongest first. */
87
+ const SRI_ALGORITHMS = [
88
+ "sha512",
89
+ "sha384",
90
+ "sha256",
91
+ "sha1"
92
+ ];
93
+ /**
94
+ * The strongest entry of a Subresource Integrity string (`<algo>-<base64>`,
95
+ * several separated by whitespace), as `{ algorithm, hex }` — the spelling a
96
+ * corepack pin uses, so a mismatch reports both sides the same way. `None`
97
+ * when nothing parseable is there.
98
+ *
99
+ * @internal
100
+ */
101
+ const strongestSri = (integrity) => {
102
+ const entries = integrity.split(/\s+/).filter((entry) => entry.length > 0).flatMap((entry) => {
103
+ const dash = entry.indexOf("-");
104
+ if (dash <= 0) return [];
105
+ const algorithm = entry.slice(0, dash).toLowerCase();
106
+ if (!SRI_ALGORITHMS.includes(algorithm)) return [];
107
+ const digest = entry.slice(dash + 1).split("?")[0] ?? "";
108
+ const decoded = Encoding.decodeBase64(digest);
109
+ return Result.isSuccess(decoded) ? [{
110
+ algorithm,
111
+ hex: Encoding.encodeHex(decoded.success)
112
+ }] : [];
113
+ });
114
+ for (const algorithm of SRI_ALGORITHMS) {
115
+ const found = entries.find((entry) => entry.algorithm === algorithm);
116
+ if (found !== void 0) return Option.some(found);
117
+ }
118
+ return Option.none();
119
+ };
120
+ /** Whether a bin target is a Node script (run with `node`) rather than an executable run directly. @internal */
121
+ const isNodeScript = (target) => /\.[cm]?js$/i.test(target);
122
+
123
+ //#endregion
124
+ export { PNPM_EXE_PREFIX, detectMusl, isNodeScript, pnpmExeTarget, strongestSri };
@@ -25,11 +25,30 @@ const delimiterFor = (value) => {
25
25
  };
26
26
  /**
27
27
  * Whether `name` can head a block without breaking the structure it lives
28
- * in: non-empty, and free of the line breaks that would end it early.
28
+ * in: non-empty, free of the line breaks that would end it early, and free
29
+ * of the separators the runner's file-command parser would re-split the
30
+ * line on.
31
+ *
32
+ * @remarks
33
+ * The runner (`FileCommandManager`) locates the first `=` and the first
34
+ * `<<` on each line and whichever comes first decides the shape: a
35
+ * `key=value` property assignment, or a `name<<delimiter` heredoc block.
36
+ * Because a block line is always `name<<DELIMITER`, an `=` anywhere in the
37
+ * name precedes the marker and the line parses as a property assignment cut
38
+ * at that `=`, leaving every line of the block to be read as stray entries;
39
+ * a `<<` inside the name splits there instead and yields a delimiter the
40
+ * terminating line can never match. Either way the entries after the
41
+ * malformed block are corrupted, so both separators are refused. A name
42
+ * ending in a single `<` corrupts the block the same way: the composed
43
+ * header `name<<<DELIMITER` matches the runner's first `<<` one character
44
+ * early, so the delimiter it waits for is `<DELIMITER` and the terminating
45
+ * line never matches — the whole file is lost. An interior `<` is safe
46
+ * (`a<b` still composes a header that splits exactly at the marker), so
47
+ * only the trailing position is refused.
29
48
  *
30
49
  * @internal
31
50
  */
32
- const isUsableName = (name) => name !== "" && !/[\r\n]/.test(name);
51
+ const isUsableName = (name) => name !== "" && !/[\r\n]/.test(name) && !name.includes("=") && !name.includes("<<") && !name.endsWith("<");
33
52
  /**
34
53
  * One heredoc block, ready to append. The caller has checked the name with
35
54
  * {@link isUsableName}.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/github-actions",
3
- "version": "0.13.1",
3
+ "version": "0.13.3",
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,13 +39,14 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@azure/storage-blob": "^12.33.0",
42
- "@effected/github": "^0.10.1",
43
- "@effected/glob": "^0.6.0",
44
- "@effected/markdown": "^0.10.0",
45
- "@effected/npm": "^0.14.0",
46
- "@effected/sbom": "^0.6.1",
47
- "@effected/templates": "^0.6.0",
48
- "@effected/walker": "^0.9.0"
42
+ "@effected/github": "^0.10.2",
43
+ "@effected/glob": "^0.6.1",
44
+ "@effected/markdown": "^0.10.1",
45
+ "@effected/npm": "^0.14.1",
46
+ "@effected/sbom": "^0.6.2",
47
+ "@effected/semver": "^0.7.1",
48
+ "@effected/templates": "^0.6.1",
49
+ "@effected/walker": "^0.9.1"
49
50
  },
50
51
  "peerDependencies": {
51
52
  "@effect/platform-node": "4.0.0-rc.115",