@effected/github-actions 0.3.0 → 0.4.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/ActionEnvironment.js +34 -1
- package/CacheKey.js +41 -0
- package/ChildEnv.js +124 -0
- package/index.d.ts +215 -2
- package/index.js +2 -1
- package/package.json +7 -7
package/ActionEnvironment.js
CHANGED
|
@@ -37,6 +37,22 @@ var GitHubContext = class extends Schema.Class("GitHubContext")({
|
|
|
37
37
|
ref: Schema.String,
|
|
38
38
|
/** The short ref, e.g. `main`. */
|
|
39
39
|
refName: Schema.String,
|
|
40
|
+
/**
|
|
41
|
+
* The source branch of the pull request, when the event has one.
|
|
42
|
+
*
|
|
43
|
+
* @remarks
|
|
44
|
+
* `GITHUB_HEAD_REF` is only set for `pull_request` events — and on every
|
|
45
|
+
* other event the runner does not merely omit it, it may write the **empty
|
|
46
|
+
* string**, which a raw `process.env` read happily reports as present. That
|
|
47
|
+
* trap is encoded in the type: absent and `""` both decode to
|
|
48
|
+
* `Option.none()`, so a consumer cannot build a cache key segment out of an
|
|
49
|
+
* empty branch name by accident. For "the branch a human means", use
|
|
50
|
+
* {@link GitHubContext.branch}, which owns the fallback to `refName`.
|
|
51
|
+
*
|
|
52
|
+
* The encoded form is `string | null` rather than a serialized `Option`, so
|
|
53
|
+
* an encoded context stays plain JSON.
|
|
54
|
+
*/
|
|
55
|
+
headRef: Schema.OptionFromNullOr(Schema.String),
|
|
40
56
|
sha: Schema.String,
|
|
41
57
|
workflow: Schema.String,
|
|
42
58
|
job: Schema.String,
|
|
@@ -48,7 +64,22 @@ var GitHubContext = class extends Schema.Class("GitHubContext")({
|
|
|
48
64
|
apiUrl: Schema.String,
|
|
49
65
|
graphqlUrl: Schema.String,
|
|
50
66
|
workspace: Schema.String
|
|
51
|
-
}) {
|
|
67
|
+
}) {
|
|
68
|
+
/**
|
|
69
|
+
* The branch a human means: `headRef` when present (a pull request, where
|
|
70
|
+
* `refName` is the useless `123/merge`), otherwise `refName`.
|
|
71
|
+
*
|
|
72
|
+
* @remarks
|
|
73
|
+
* This is the universal branch-scoped-cache-key fallback chain, written
|
|
74
|
+
* once. On a `push` or any other non-PR event `headRef` is `Option.none()`
|
|
75
|
+
* — including when the runner wrote it as the empty string — so the answer
|
|
76
|
+
* is the short ref. Note that on a tag push `refName` names the tag, not a
|
|
77
|
+
* branch; a consumer that must distinguish still has the full `ref`.
|
|
78
|
+
*/
|
|
79
|
+
get branch() {
|
|
80
|
+
return Option.getOrElse(this.headRef, () => this.refName);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
52
83
|
/**
|
|
53
84
|
* The runner's own context, projected from the `RUNNER_*` variables.
|
|
54
85
|
*
|
|
@@ -110,6 +141,7 @@ const make = (base) => Effect.gen(function* () {
|
|
|
110
141
|
]);
|
|
111
142
|
const runId = yield* Effect.flatMap(get("GITHUB_RUN_ID"), (raw) => numeric("GITHUB_RUN_ID", raw));
|
|
112
143
|
const runAttempt = yield* Effect.flatMap(get("GITHUB_RUN_ATTEMPT"), (raw) => numeric("GITHUB_RUN_ATTEMPT", raw));
|
|
144
|
+
const headRef = yield* lookup("GITHUB_HEAD_REF");
|
|
113
145
|
const [eventName, actor, serverUrl, apiUrl, graphqlUrl, workspace] = yield* Effect.all([
|
|
114
146
|
get("GITHUB_EVENT_NAME"),
|
|
115
147
|
get("GITHUB_ACTOR"),
|
|
@@ -123,6 +155,7 @@ const make = (base) => Effect.gen(function* () {
|
|
|
123
155
|
repositoryOwner,
|
|
124
156
|
ref,
|
|
125
157
|
refName,
|
|
158
|
+
headRef,
|
|
126
159
|
sha,
|
|
127
160
|
workflow,
|
|
128
161
|
job,
|
package/CacheKey.js
CHANGED
|
@@ -215,6 +215,47 @@ var CacheKey = class CacheKey extends Schema.Class("CacheKey")(Schema.Struct({
|
|
|
215
215
|
return CacheKey.make({ segments });
|
|
216
216
|
}
|
|
217
217
|
/**
|
|
218
|
+
* A segment-safe short digest of a string.
|
|
219
|
+
*
|
|
220
|
+
* @remarks
|
|
221
|
+
* {@link CacheKey.hashFiles} covers file *content*; this covers the key
|
|
222
|
+
* segments built from **non-file** inputs — a sorted version list, a branch
|
|
223
|
+
* name — that every compound key hashes and truncates by hand, each caller
|
|
224
|
+
* re-deciding the length and the placement. One home for the derivation:
|
|
225
|
+
* SHA-256, lowercase hex, the first `length` characters.
|
|
226
|
+
*
|
|
227
|
+
* The answer is **guaranteed to satisfy the segment grammar**: lowercase hex
|
|
228
|
+
* is nonempty (for any permitted `length`) and can never contain the comma
|
|
229
|
+
* or newline the restore-key protocol reserves, so the result drops straight
|
|
230
|
+
* into {@link CacheKey.of} with nothing to check at the call site.
|
|
231
|
+
*
|
|
232
|
+
* Deterministic and pure — the same input always answers the same digest,
|
|
233
|
+
* which is the entire point of putting one in a cache key. Distinct inputs
|
|
234
|
+
* are only *probably* distinct, as with any truncated hash; eight hex
|
|
235
|
+
* characters (32 bits) is the conventional balance between key legibility
|
|
236
|
+
* and collision risk for cache segments.
|
|
237
|
+
*
|
|
238
|
+
* A `length` outside `1..64` (or a fractional one) is a wiring mistake, not
|
|
239
|
+
* data, and **throws a `RangeError`** rather than failing typed: 64 is all
|
|
240
|
+
* SHA-256 has, and asking for more would silently answer fewer characters
|
|
241
|
+
* than the caller believes it got.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```ts
|
|
245
|
+
* import { CacheKey } from "@effected/github-actions";
|
|
246
|
+
*
|
|
247
|
+
* const key = CacheKey.of(
|
|
248
|
+
* "Linux",
|
|
249
|
+
* CacheKey.digest("node:24.4.0,pnpm:10.13.1"),
|
|
250
|
+
* CacheKey.digest("feat/my-branch"),
|
|
251
|
+
* );
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
static digest(input, length = 8) {
|
|
255
|
+
if (!Number.isInteger(length) || length < 1 || length > 64) throw new RangeError(`A digest length must be an integer between 1 and 64, got ${length}`);
|
|
256
|
+
return createHash("sha256").update(input).digest("hex").slice(0, length);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
218
259
|
* Hash a set of files into a single digest.
|
|
219
260
|
*
|
|
220
261
|
* @remarks
|
package/ChildEnv.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
//#region src/ChildEnv.ts
|
|
2
|
+
/**
|
|
3
|
+
* Child-process environment construction: prepending directories to `PATH`
|
|
4
|
+
* without falling into the three traps that each cost a real consumer a
|
|
5
|
+
* cross-OS matrix round.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* Spawning a child with directories prepended to `PATH` is one tiny concern
|
|
9
|
+
* with three sharp edges, and every one of them fails silently or only on
|
|
10
|
+
* Windows:
|
|
11
|
+
*
|
|
12
|
+
* 1. **`env` without `extendEnv: true` strips the child's environment.**
|
|
13
|
+
* Core's `ChildProcess.CommandOptions.env` *replaces* the block unless
|
|
14
|
+
* `extendEnv` says to merge, so a bare `{ env }` costs the child everything
|
|
15
|
+
* — including the very `PATH` being extended.
|
|
16
|
+
* 2. **Windows spells it `Path`, and casing decides who wins.** The
|
|
17
|
+
* environment block is case-insensitive on win32, and Node de-duplicates a
|
|
18
|
+
* merged block by keeping the **lexicographically first** spelling — an
|
|
19
|
+
* undocumented internal that happens to favour `PATH` over an inherited
|
|
20
|
+
* `Path`, and that nothing obliges Node to keep. Writing through the
|
|
21
|
+
* inherited key's own casing means never depending on it.
|
|
22
|
+
* 3. **`.cmd` shims need a shell since CVE-2024-27980.** See
|
|
23
|
+
* {@link ChildEnv.needsShell}.
|
|
24
|
+
*
|
|
25
|
+
* The first two live in {@link ChildEnv.prependPath}; the third is a separate
|
|
26
|
+
* predicate because it belongs on the spawn call, not in the environment.
|
|
27
|
+
*
|
|
28
|
+
* This class is total and pure — no service, no `R`, nothing read from the
|
|
29
|
+
* ambient process — the same species as `Secret`'s statics, one concern per
|
|
30
|
+
* member.
|
|
31
|
+
*
|
|
32
|
+
* @public
|
|
33
|
+
*/
|
|
34
|
+
var ChildEnv = class ChildEnv {
|
|
35
|
+
constructor() {}
|
|
36
|
+
/**
|
|
37
|
+
* The key `base` spells `PATH` with — `Path` on a typical Windows block,
|
|
38
|
+
* `PATH` elsewhere, and `PATH` when the base has no entry at all.
|
|
39
|
+
*
|
|
40
|
+
* @remarks
|
|
41
|
+
* The lookup is case-insensitive because the Windows environment block is;
|
|
42
|
+
* the answer preserves the base's own casing because that is the spelling a
|
|
43
|
+
* merged block must write through (trap 2 above).
|
|
44
|
+
*/
|
|
45
|
+
static pathKeyOf(base) {
|
|
46
|
+
return Object.keys(base).find((key) => key.toUpperCase() === "PATH") ?? "PATH";
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The environment additions that prepend `dirs` to the child's `PATH`,
|
|
50
|
+
* paired with the `extendEnv: true` that keeps them a *prepend*.
|
|
51
|
+
*
|
|
52
|
+
* @remarks
|
|
53
|
+
* `dirs` are joined ahead of the inherited value **in the order given** —
|
|
54
|
+
* order is the caller's policy, because the first directory wins any name
|
|
55
|
+
* collision (a package manager's shim directory leads so it beats a runtime
|
|
56
|
+
* of the same name). The entry is written under the base's own `PATH`
|
|
57
|
+
* spelling ({@link ChildEnv.pathKeyOf}), so a Windows child never receives
|
|
58
|
+
* `PATH` beside its inherited `Path`. An absent or empty inherited value
|
|
59
|
+
* appends nothing — deliberately not an empty trailing entry, which both
|
|
60
|
+
* platforms historically read as "the current directory".
|
|
61
|
+
*
|
|
62
|
+
* Two consumers, two compositions, and the difference is exactly trap 1:
|
|
63
|
+
*
|
|
64
|
+
* - A spawner call takes the **whole pair** — spread it into the command
|
|
65
|
+
* options, as in the example below, so the `extendEnv: true` that makes
|
|
66
|
+
* `env` a merge rather than a replacement cannot be forgotten.
|
|
67
|
+
* - An API that already merges additions over the parent itself —
|
|
68
|
+
* `DetachedProcess.spawn`, whose `env` is documented as merged — takes
|
|
69
|
+
* `.env` alone; `extendEnv` would be meaningless there.
|
|
70
|
+
*
|
|
71
|
+
* On a runner, pass `{ base: process.env, platform: process.platform }`.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* import { ChildEnv } from "@effected/github-actions";
|
|
76
|
+
* import { ChildProcess } from "effect/unstable/process";
|
|
77
|
+
*
|
|
78
|
+
* const command = ChildProcess.make("pnpm", ["install"], {
|
|
79
|
+
* ...ChildEnv.prependPath(["/opt/hostedtoolcache/pnpm/10.13.1/x64/bin"], {
|
|
80
|
+
* base: process.env,
|
|
81
|
+
* platform: process.platform,
|
|
82
|
+
* }),
|
|
83
|
+
* ...(ChildEnv.needsShell(process.platform) ? { shell: true } : {}),
|
|
84
|
+
* });
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
static prependPath(dirs, options) {
|
|
88
|
+
const delimiter = options.platform === "win32" ? ";" : ":";
|
|
89
|
+
const key = ChildEnv.pathKeyOf(options.base);
|
|
90
|
+
const inherited = options.base[key];
|
|
91
|
+
const entries = inherited === void 0 || inherited === "" ? dirs : [...dirs, inherited];
|
|
92
|
+
return {
|
|
93
|
+
env: { [key]: entries.join(delimiter) },
|
|
94
|
+
extendEnv: true
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Whether a bare-name command must be launched through a shell on this
|
|
99
|
+
* platform.
|
|
100
|
+
*
|
|
101
|
+
* @remarks
|
|
102
|
+
* On Windows every npm-installed tool on `PATH` is a `.cmd` batch shim, not
|
|
103
|
+
* an executable: `CreateProcess` cannot run one, and since CVE-2024-27980
|
|
104
|
+
* Node refuses to hand `.cmd`/`.bat` files to it at all unless a shell is
|
|
105
|
+
* asked for — so the spawn fails at launch, before the tool ever runs.
|
|
106
|
+
* Under `shell: true`, `cmd.exe` resolves the bare name off the **child's**
|
|
107
|
+
* `PATH` — the one {@link ChildEnv.prependPath} builds — through `PATHEXT`,
|
|
108
|
+
* where `.EXE` precedes `.CMD`, so a real binary of the same name is still
|
|
109
|
+
* found first and the prepends keep working exactly as they do without a
|
|
110
|
+
* shell.
|
|
111
|
+
*
|
|
112
|
+
* POSIX answers `false`: the direct spawn already works, and a shell would
|
|
113
|
+
* only add a quoting hazard between the caller and the exit code. Note the
|
|
114
|
+
* hazard `true` carries: under a shell Node concatenates the command line
|
|
115
|
+
* rather than passing an argv, so arguments must be static literals, never
|
|
116
|
+
* derived from input.
|
|
117
|
+
*/
|
|
118
|
+
static needsShell(platform) {
|
|
119
|
+
return platform === "win32";
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
//#endregion
|
|
124
|
+
export { ChildEnv };
|
package/index.d.ts
CHANGED
|
@@ -36,6 +36,22 @@ declare const GitHubContext_base: Schema.Class<GitHubContext, Schema.Struct<{
|
|
|
36
36
|
readonly ref: Schema.String;
|
|
37
37
|
/** The short ref, e.g. `main`. */
|
|
38
38
|
readonly refName: Schema.String;
|
|
39
|
+
/**
|
|
40
|
+
* The source branch of the pull request, when the event has one.
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* `GITHUB_HEAD_REF` is only set for `pull_request` events — and on every
|
|
44
|
+
* other event the runner does not merely omit it, it may write the **empty
|
|
45
|
+
* string**, which a raw `process.env` read happily reports as present. That
|
|
46
|
+
* trap is encoded in the type: absent and `""` both decode to
|
|
47
|
+
* `Option.none()`, so a consumer cannot build a cache key segment out of an
|
|
48
|
+
* empty branch name by accident. For "the branch a human means", use
|
|
49
|
+
* {@link GitHubContext.branch}, which owns the fallback to `refName`.
|
|
50
|
+
*
|
|
51
|
+
* The encoded form is `string | null` rather than a serialized `Option`, so
|
|
52
|
+
* an encoded context stays plain JSON.
|
|
53
|
+
*/
|
|
54
|
+
readonly headRef: Schema.OptionFromNullOr<Schema.String>;
|
|
39
55
|
readonly sha: Schema.String;
|
|
40
56
|
readonly workflow: Schema.String;
|
|
41
57
|
readonly job: Schema.String;
|
|
@@ -53,7 +69,20 @@ declare const GitHubContext_base: Schema.Class<GitHubContext, Schema.Struct<{
|
|
|
53
69
|
*
|
|
54
70
|
* @public
|
|
55
71
|
*/
|
|
56
|
-
declare class GitHubContext extends GitHubContext_base {
|
|
72
|
+
declare class GitHubContext extends GitHubContext_base {
|
|
73
|
+
/**
|
|
74
|
+
* The branch a human means: `headRef` when present (a pull request, where
|
|
75
|
+
* `refName` is the useless `123/merge`), otherwise `refName`.
|
|
76
|
+
*
|
|
77
|
+
* @remarks
|
|
78
|
+
* This is the universal branch-scoped-cache-key fallback chain, written
|
|
79
|
+
* once. On a `push` or any other non-PR event `headRef` is `Option.none()`
|
|
80
|
+
* — including when the runner wrote it as the empty string — so the answer
|
|
81
|
+
* is the short ref. Note that on a tag push `refName` names the tag, not a
|
|
82
|
+
* branch; a consumer that must distinguish still has the full `ref`.
|
|
83
|
+
*/
|
|
84
|
+
get branch(): string;
|
|
85
|
+
}
|
|
57
86
|
declare const RunnerContext_base: Schema.Class<RunnerContext, Schema.Struct<{
|
|
58
87
|
readonly os: Schema.String;
|
|
59
88
|
readonly arch: Schema.String;
|
|
@@ -915,6 +944,44 @@ declare class CacheKey extends CacheKey_base {
|
|
|
915
944
|
/** The content hash, e.g. from {@link CacheKey.hashFiles}. Included last when given. */
|
|
916
945
|
readonly hash?: string;
|
|
917
946
|
}): CacheKey;
|
|
947
|
+
/**
|
|
948
|
+
* A segment-safe short digest of a string.
|
|
949
|
+
*
|
|
950
|
+
* @remarks
|
|
951
|
+
* {@link CacheKey.hashFiles} covers file *content*; this covers the key
|
|
952
|
+
* segments built from **non-file** inputs — a sorted version list, a branch
|
|
953
|
+
* name — that every compound key hashes and truncates by hand, each caller
|
|
954
|
+
* re-deciding the length and the placement. One home for the derivation:
|
|
955
|
+
* SHA-256, lowercase hex, the first `length` characters.
|
|
956
|
+
*
|
|
957
|
+
* The answer is **guaranteed to satisfy the segment grammar**: lowercase hex
|
|
958
|
+
* is nonempty (for any permitted `length`) and can never contain the comma
|
|
959
|
+
* or newline the restore-key protocol reserves, so the result drops straight
|
|
960
|
+
* into {@link CacheKey.of} with nothing to check at the call site.
|
|
961
|
+
*
|
|
962
|
+
* Deterministic and pure — the same input always answers the same digest,
|
|
963
|
+
* which is the entire point of putting one in a cache key. Distinct inputs
|
|
964
|
+
* are only *probably* distinct, as with any truncated hash; eight hex
|
|
965
|
+
* characters (32 bits) is the conventional balance between key legibility
|
|
966
|
+
* and collision risk for cache segments.
|
|
967
|
+
*
|
|
968
|
+
* A `length` outside `1..64` (or a fractional one) is a wiring mistake, not
|
|
969
|
+
* data, and **throws a `RangeError`** rather than failing typed: 64 is all
|
|
970
|
+
* SHA-256 has, and asking for more would silently answer fewer characters
|
|
971
|
+
* than the caller believes it got.
|
|
972
|
+
*
|
|
973
|
+
* @example
|
|
974
|
+
* ```ts
|
|
975
|
+
* import { CacheKey } from "@effected/github-actions";
|
|
976
|
+
*
|
|
977
|
+
* const key = CacheKey.of(
|
|
978
|
+
* "Linux",
|
|
979
|
+
* CacheKey.digest("node:24.4.0,pnpm:10.13.1"),
|
|
980
|
+
* CacheKey.digest("feat/my-branch"),
|
|
981
|
+
* );
|
|
982
|
+
* ```
|
|
983
|
+
*/
|
|
984
|
+
static digest(input: string, length?: number): string;
|
|
918
985
|
/**
|
|
919
986
|
* Hash a set of files into a single digest.
|
|
920
987
|
*
|
|
@@ -2422,6 +2489,152 @@ type CheckRunProjection = {
|
|
|
2422
2489
|
*/
|
|
2423
2490
|
declare const projectCheckState: (state: CheckState) => CheckRunProjection;
|
|
2424
2491
|
//#endregion
|
|
2492
|
+
//#region src/ChildEnv.d.ts
|
|
2493
|
+
/**
|
|
2494
|
+
* The environment additions {@link ChildEnv.prependPath} builds: exactly one
|
|
2495
|
+
* entry, keyed by the spelling of `PATH` the base environment already uses.
|
|
2496
|
+
*
|
|
2497
|
+
* @public
|
|
2498
|
+
*/
|
|
2499
|
+
interface PathPrependEnv {
|
|
2500
|
+
/** The additions to hand a child, ready to merge over the parent's block. */
|
|
2501
|
+
readonly env: Readonly<Record<string, string>>;
|
|
2502
|
+
/**
|
|
2503
|
+
* Always `true`, and part of the answer on purpose: `env` **without**
|
|
2504
|
+
* `extendEnv: true` silently replaces the child's entire environment with
|
|
2505
|
+
* this one entry. See {@link ChildEnv.prependPath}.
|
|
2506
|
+
*/
|
|
2507
|
+
readonly extendEnv: true;
|
|
2508
|
+
}
|
|
2509
|
+
/**
|
|
2510
|
+
* What {@link ChildEnv.prependPath} derives the additions from.
|
|
2511
|
+
*
|
|
2512
|
+
* @public
|
|
2513
|
+
*/
|
|
2514
|
+
interface PathPrependOptions {
|
|
2515
|
+
/**
|
|
2516
|
+
* The environment the child will inherit — on a runner, `process.env`.
|
|
2517
|
+
*
|
|
2518
|
+
* @remarks
|
|
2519
|
+
* Explicit rather than defaulted, matching this package's rule that ambient
|
|
2520
|
+
* process state is never read behind a caller's back — which is also what
|
|
2521
|
+
* makes the Windows casing branch exercisable from a test on any host.
|
|
2522
|
+
*/
|
|
2523
|
+
readonly base: Readonly<Record<string, string | undefined>>;
|
|
2524
|
+
/**
|
|
2525
|
+
* The platform the child runs on, `process.platform`-shaped. Decides the
|
|
2526
|
+
* `PATH` entry delimiter: `;` on `win32`, `:` everywhere else.
|
|
2527
|
+
*/
|
|
2528
|
+
readonly platform: string;
|
|
2529
|
+
}
|
|
2530
|
+
/**
|
|
2531
|
+
* Child-process environment construction: prepending directories to `PATH`
|
|
2532
|
+
* without falling into the three traps that each cost a real consumer a
|
|
2533
|
+
* cross-OS matrix round.
|
|
2534
|
+
*
|
|
2535
|
+
* @remarks
|
|
2536
|
+
* Spawning a child with directories prepended to `PATH` is one tiny concern
|
|
2537
|
+
* with three sharp edges, and every one of them fails silently or only on
|
|
2538
|
+
* Windows:
|
|
2539
|
+
*
|
|
2540
|
+
* 1. **`env` without `extendEnv: true` strips the child's environment.**
|
|
2541
|
+
* Core's `ChildProcess.CommandOptions.env` *replaces* the block unless
|
|
2542
|
+
* `extendEnv` says to merge, so a bare `{ env }` costs the child everything
|
|
2543
|
+
* — including the very `PATH` being extended.
|
|
2544
|
+
* 2. **Windows spells it `Path`, and casing decides who wins.** The
|
|
2545
|
+
* environment block is case-insensitive on win32, and Node de-duplicates a
|
|
2546
|
+
* merged block by keeping the **lexicographically first** spelling — an
|
|
2547
|
+
* undocumented internal that happens to favour `PATH` over an inherited
|
|
2548
|
+
* `Path`, and that nothing obliges Node to keep. Writing through the
|
|
2549
|
+
* inherited key's own casing means never depending on it.
|
|
2550
|
+
* 3. **`.cmd` shims need a shell since CVE-2024-27980.** See
|
|
2551
|
+
* {@link ChildEnv.needsShell}.
|
|
2552
|
+
*
|
|
2553
|
+
* The first two live in {@link ChildEnv.prependPath}; the third is a separate
|
|
2554
|
+
* predicate because it belongs on the spawn call, not in the environment.
|
|
2555
|
+
*
|
|
2556
|
+
* This class is total and pure — no service, no `R`, nothing read from the
|
|
2557
|
+
* ambient process — the same species as `Secret`'s statics, one concern per
|
|
2558
|
+
* member.
|
|
2559
|
+
*
|
|
2560
|
+
* @public
|
|
2561
|
+
*/
|
|
2562
|
+
declare class ChildEnv {
|
|
2563
|
+
private constructor();
|
|
2564
|
+
/**
|
|
2565
|
+
* The key `base` spells `PATH` with — `Path` on a typical Windows block,
|
|
2566
|
+
* `PATH` elsewhere, and `PATH` when the base has no entry at all.
|
|
2567
|
+
*
|
|
2568
|
+
* @remarks
|
|
2569
|
+
* The lookup is case-insensitive because the Windows environment block is;
|
|
2570
|
+
* the answer preserves the base's own casing because that is the spelling a
|
|
2571
|
+
* merged block must write through (trap 2 above).
|
|
2572
|
+
*/
|
|
2573
|
+
static pathKeyOf(base: Readonly<Record<string, string | undefined>>): string;
|
|
2574
|
+
/**
|
|
2575
|
+
* The environment additions that prepend `dirs` to the child's `PATH`,
|
|
2576
|
+
* paired with the `extendEnv: true` that keeps them a *prepend*.
|
|
2577
|
+
*
|
|
2578
|
+
* @remarks
|
|
2579
|
+
* `dirs` are joined ahead of the inherited value **in the order given** —
|
|
2580
|
+
* order is the caller's policy, because the first directory wins any name
|
|
2581
|
+
* collision (a package manager's shim directory leads so it beats a runtime
|
|
2582
|
+
* of the same name). The entry is written under the base's own `PATH`
|
|
2583
|
+
* spelling ({@link ChildEnv.pathKeyOf}), so a Windows child never receives
|
|
2584
|
+
* `PATH` beside its inherited `Path`. An absent or empty inherited value
|
|
2585
|
+
* appends nothing — deliberately not an empty trailing entry, which both
|
|
2586
|
+
* platforms historically read as "the current directory".
|
|
2587
|
+
*
|
|
2588
|
+
* Two consumers, two compositions, and the difference is exactly trap 1:
|
|
2589
|
+
*
|
|
2590
|
+
* - A spawner call takes the **whole pair** — spread it into the command
|
|
2591
|
+
* options, as in the example below, so the `extendEnv: true` that makes
|
|
2592
|
+
* `env` a merge rather than a replacement cannot be forgotten.
|
|
2593
|
+
* - An API that already merges additions over the parent itself —
|
|
2594
|
+
* `DetachedProcess.spawn`, whose `env` is documented as merged — takes
|
|
2595
|
+
* `.env` alone; `extendEnv` would be meaningless there.
|
|
2596
|
+
*
|
|
2597
|
+
* On a runner, pass `{ base: process.env, platform: process.platform }`.
|
|
2598
|
+
*
|
|
2599
|
+
* @example
|
|
2600
|
+
* ```ts
|
|
2601
|
+
* import { ChildEnv } from "@effected/github-actions";
|
|
2602
|
+
* import { ChildProcess } from "effect/unstable/process";
|
|
2603
|
+
*
|
|
2604
|
+
* const command = ChildProcess.make("pnpm", ["install"], {
|
|
2605
|
+
* ...ChildEnv.prependPath(["/opt/hostedtoolcache/pnpm/10.13.1/x64/bin"], {
|
|
2606
|
+
* base: process.env,
|
|
2607
|
+
* platform: process.platform,
|
|
2608
|
+
* }),
|
|
2609
|
+
* ...(ChildEnv.needsShell(process.platform) ? { shell: true } : {}),
|
|
2610
|
+
* });
|
|
2611
|
+
* ```
|
|
2612
|
+
*/
|
|
2613
|
+
static prependPath(dirs: readonly [string, ...ReadonlyArray<string>], options: PathPrependOptions): PathPrependEnv;
|
|
2614
|
+
/**
|
|
2615
|
+
* Whether a bare-name command must be launched through a shell on this
|
|
2616
|
+
* platform.
|
|
2617
|
+
*
|
|
2618
|
+
* @remarks
|
|
2619
|
+
* On Windows every npm-installed tool on `PATH` is a `.cmd` batch shim, not
|
|
2620
|
+
* an executable: `CreateProcess` cannot run one, and since CVE-2024-27980
|
|
2621
|
+
* Node refuses to hand `.cmd`/`.bat` files to it at all unless a shell is
|
|
2622
|
+
* asked for — so the spawn fails at launch, before the tool ever runs.
|
|
2623
|
+
* Under `shell: true`, `cmd.exe` resolves the bare name off the **child's**
|
|
2624
|
+
* `PATH` — the one {@link ChildEnv.prependPath} builds — through `PATHEXT`,
|
|
2625
|
+
* where `.EXE` precedes `.CMD`, so a real binary of the same name is still
|
|
2626
|
+
* found first and the prepends keep working exactly as they do without a
|
|
2627
|
+
* shell.
|
|
2628
|
+
*
|
|
2629
|
+
* POSIX answers `false`: the direct spawn already works, and a shell would
|
|
2630
|
+
* only add a quoting hazard between the caller and the exit code. Note the
|
|
2631
|
+
* hazard `true` carries: under a shell Node concatenates the command line
|
|
2632
|
+
* rather than passing an argv, so arguments must be static literals, never
|
|
2633
|
+
* derived from input.
|
|
2634
|
+
*/
|
|
2635
|
+
static needsShell(platform: string): boolean;
|
|
2636
|
+
}
|
|
2637
|
+
//#endregion
|
|
2425
2638
|
//#region src/DetachedProcess.d.ts
|
|
2426
2639
|
declare const DetachedProcessError_base: Schema.Class<DetachedProcessError, Schema.TaggedStruct<"DetachedProcessError", {
|
|
2427
2640
|
/**
|
|
@@ -3845,5 +4058,5 @@ declare class Secret {
|
|
|
3845
4058
|
static readonly adopt: (name: string) => Config.Config<Redacted.Redacted<string>>;
|
|
3846
4059
|
}
|
|
3847
4060
|
//#endregion
|
|
3848
|
-
export { Action, ActionCache, ActionCacheError, type ActionCacheShape, ActionEnvironment, ActionEnvironmentError, type ActionEnvironmentShape, ActionInput, ActionLogger, type ActionLoggerShape, ActionOutputError, ActionOutputs, type ActionOutputsShape, type ActionRunOptions, ActionRuntime, type ActionServices, ActionState, ActionStateError, type ActionStateShape, ActionsIdentityToken, ActionsProvenance, AmbientPackageManager, type AnnotationProperties, Artifact, ArtifactError, type ArtifactItem, type ArtifactRef, type ArtifactShape, type Blob, BlobEnvelope, BlobEnvelopeError, BlobStore, BlobStoreError, type BlobStoreShape, BlobTransferError, CacheKey, CacheKeyError, CachedPackageManager, CheckDocument, CheckDocumentError, type CheckDocumentOptions, type CheckDocumentShape, type CheckDocumentSink, CheckReport, type CheckRunConclusion, type CheckRunProjection, CheckState, type ClientLayerOptions, type DataBlobTransfer, DetachedProcess, DetachedProcessError, type DetachedSpawnOptions, type DownloadOptions, type DownloadResult, DryRun, type DryRunShape, type ExtractOptions, type FileBlobTransfer, GitHubCacheBlobStore, GitHubContext, type GitHubHeadingDepth, type GitHubListOptions, GitHubMarkdown, type GitHubRowSchema, type GitHubSchemaTable, type GitHubSchemaTableColumn, type GitHubSchemaTableColumns, type GitHubSchemaTableFormatRequiredKeys, type GitHubSchemaTableFormattedColumn, type GitHubSchemaTableOptions, GitHubToken, GitHubTokenError, InstalledPackageManager, ManagedDocument, ManagedDocumentError, type ManagedDocumentSource, OidcClaims, OidcTokenError, OidcTokenIssuer, type OidcTokenIssuerShape, type PackageManagerInstallOptions, PackageManagerInstaller, PackageManagerInstallerError, type PackageManagerInstallerShape, ProcessId, type ProvisionFileOptions, type ProvisionOptions, type ProvisionedFile, type ReadOptions, type ReadinessOptions, RunnerContext, type S3Config, Secret, type ToolDownloadOptions, ToolInstaller, ToolInstallerError, type ToolInstallerShape, type UploadOptions, type UploadResult, type WithBufferOptions, WorkflowCommand, describeCause, projectCheckState };
|
|
4061
|
+
export { Action, ActionCache, ActionCacheError, type ActionCacheShape, ActionEnvironment, ActionEnvironmentError, type ActionEnvironmentShape, ActionInput, ActionLogger, type ActionLoggerShape, ActionOutputError, ActionOutputs, type ActionOutputsShape, type ActionRunOptions, ActionRuntime, type ActionServices, ActionState, ActionStateError, type ActionStateShape, ActionsIdentityToken, ActionsProvenance, AmbientPackageManager, type AnnotationProperties, Artifact, ArtifactError, type ArtifactItem, type ArtifactRef, type ArtifactShape, type Blob, BlobEnvelope, BlobEnvelopeError, BlobStore, BlobStoreError, type BlobStoreShape, BlobTransferError, CacheKey, CacheKeyError, CachedPackageManager, CheckDocument, CheckDocumentError, type CheckDocumentOptions, type CheckDocumentShape, type CheckDocumentSink, CheckReport, type CheckRunConclusion, type CheckRunProjection, CheckState, ChildEnv, type ClientLayerOptions, type DataBlobTransfer, DetachedProcess, DetachedProcessError, type DetachedSpawnOptions, type DownloadOptions, type DownloadResult, DryRun, type DryRunShape, type ExtractOptions, type FileBlobTransfer, GitHubCacheBlobStore, GitHubContext, type GitHubHeadingDepth, type GitHubListOptions, GitHubMarkdown, type GitHubRowSchema, type GitHubSchemaTable, type GitHubSchemaTableColumn, type GitHubSchemaTableColumns, type GitHubSchemaTableFormatRequiredKeys, type GitHubSchemaTableFormattedColumn, type GitHubSchemaTableOptions, GitHubToken, GitHubTokenError, InstalledPackageManager, ManagedDocument, ManagedDocumentError, type ManagedDocumentSource, OidcClaims, OidcTokenError, OidcTokenIssuer, type OidcTokenIssuerShape, type PackageManagerInstallOptions, PackageManagerInstaller, PackageManagerInstallerError, type PackageManagerInstallerShape, type PathPrependEnv, type PathPrependOptions, ProcessId, type ProvisionFileOptions, type ProvisionOptions, type ProvisionedFile, type ReadOptions, type ReadinessOptions, RunnerContext, type S3Config, Secret, type ToolDownloadOptions, ToolInstaller, ToolInstallerError, type ToolInstallerShape, type UploadOptions, type UploadResult, type WithBufferOptions, WorkflowCommand, describeCause, projectCheckState };
|
|
3849
4062
|
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { CacheKey, CacheKeyError } from "./CacheKey.js";
|
|
|
19
19
|
import { CheckState, projectCheckState } from "./CheckState.js";
|
|
20
20
|
import { ManagedDocument, ManagedDocumentError } from "./ManagedDocument.js";
|
|
21
21
|
import { CheckDocument, CheckDocumentError, CheckReport } from "./CheckDocument.js";
|
|
22
|
+
import { ChildEnv } from "./ChildEnv.js";
|
|
22
23
|
import { DetachedProcess, DetachedProcessError, ProcessId } from "./DetachedProcess.js";
|
|
23
24
|
import { DryRun } from "./DryRun.js";
|
|
24
25
|
import { GitHubMarkdown } from "./GitHubMarkdown.js";
|
|
@@ -26,4 +27,4 @@ import { GitHubToken, GitHubTokenError } from "./GitHubToken.js";
|
|
|
26
27
|
import { ToolInstaller, ToolInstallerError } from "./ToolInstaller.js";
|
|
27
28
|
import { AmbientPackageManager, CachedPackageManager, InstalledPackageManager, PackageManagerInstaller, PackageManagerInstallerError } from "./PackageManagerInstaller.js";
|
|
28
29
|
|
|
29
|
-
export { Action, ActionCache, ActionCacheError, ActionEnvironment, ActionEnvironmentError, ActionInput, ActionLogger, ActionOutputError, ActionOutputs, ActionRuntime, ActionState, ActionStateError, ActionsIdentityToken, ActionsProvenance, AmbientPackageManager, Artifact, ArtifactError, BlobEnvelope, BlobEnvelopeError, BlobStore, BlobStoreError, BlobTransferError, CacheKey, CacheKeyError, CachedPackageManager, CheckDocument, CheckDocumentError, CheckReport, CheckState, DetachedProcess, DetachedProcessError, DryRun, GitHubCacheBlobStore, GitHubContext, GitHubMarkdown, GitHubToken, GitHubTokenError, InstalledPackageManager, ManagedDocument, ManagedDocumentError, OidcClaims, OidcTokenError, OidcTokenIssuer, PackageManagerInstaller, PackageManagerInstallerError, ProcessId, RunnerContext, Secret, ToolInstaller, ToolInstallerError, WorkflowCommand, describeCause, projectCheckState };
|
|
30
|
+
export { Action, ActionCache, ActionCacheError, ActionEnvironment, ActionEnvironmentError, ActionInput, ActionLogger, ActionOutputError, ActionOutputs, ActionRuntime, ActionState, ActionStateError, ActionsIdentityToken, ActionsProvenance, AmbientPackageManager, Artifact, ArtifactError, BlobEnvelope, BlobEnvelopeError, BlobStore, BlobStoreError, BlobTransferError, CacheKey, CacheKeyError, CachedPackageManager, CheckDocument, CheckDocumentError, CheckReport, CheckState, ChildEnv, DetachedProcess, DetachedProcessError, DryRun, GitHubCacheBlobStore, GitHubContext, GitHubMarkdown, GitHubToken, GitHubTokenError, InstalledPackageManager, ManagedDocument, ManagedDocumentError, OidcClaims, OidcTokenError, OidcTokenIssuer, PackageManagerInstaller, PackageManagerInstallerError, ProcessId, RunnerContext, Secret, ToolInstaller, ToolInstallerError, WorkflowCommand, describeCause, projectCheckState };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/github-actions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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,12 +39,12 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@azure/storage-blob": "^12.33.0",
|
|
42
|
-
"@effected/github": "
|
|
43
|
-
"@effected/glob": "
|
|
44
|
-
"@effected/markdown": "
|
|
45
|
-
"@effected/npm": "
|
|
46
|
-
"@effected/sbom": "
|
|
47
|
-
"@effected/templates": "
|
|
42
|
+
"@effected/github": "^0.2.2",
|
|
43
|
+
"@effected/glob": "^0.2.2",
|
|
44
|
+
"@effected/markdown": "^0.4.2",
|
|
45
|
+
"@effected/npm": "^0.8.1",
|
|
46
|
+
"@effected/sbom": "^0.2.2",
|
|
47
|
+
"@effected/templates": "^0.1.1"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@effect/platform-node": "4.0.0-beta.101",
|