@effected/github-actions 0.6.1 → 0.8.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.
@@ -286,6 +286,26 @@ var ActionEnvironment = class ActionEnvironment extends Context.Service()("@effe
286
286
  * `undefined` means *not served*, so a suite cannot arrange `undefined` as
287
287
  * the payload itself. A webhook payload is always a JSON object; the case
288
288
  * does not arise, and reserving the sentinel keeps the argument optional.
289
+ *
290
+ * When a suite wants the **real read path** rather than a served value —
291
+ * exercising the actual `GITHUB_EVENT_PATH` read-and-parse — this member is
292
+ * the seam: it leaves `FileSystem` in `R` precisely so the suite chooses
293
+ * the filesystem. Compose it with `@effected/memfs`'s seeded in-memory
294
+ * volume rather than a hand-rolled stub: the volume honors the whole
295
+ * `FileSystem` contract, and an unseeded path fails typed `NotFound`
296
+ * instead of whatever a hand stub happens to answer.
297
+ *
298
+ * @example
299
+ * ```ts
300
+ * import { ActionEnvironment } from "@effected/github-actions";
301
+ * import { MemoryFileSystem } from "@effected/memfs";
302
+ * import { Layer } from "effect";
303
+ *
304
+ * const layer = Layer.effect(
305
+ * ActionEnvironment,
306
+ * ActionEnvironment.makeTest({ GITHUB_EVENT_PATH: "/event.json" }),
307
+ * ).pipe(Layer.provide(MemoryFileSystem.layerWith({ "/event.json": JSON.stringify({ action: "opened" }) })));
308
+ * ```
289
309
  */
290
310
  static makeTest = (overrides = {}, payload) => Effect.map(make({
291
311
  ...TEST_DEFAULTS,
@@ -302,7 +322,10 @@ var ActionEnvironment = class ActionEnvironment extends Context.Service()("@effe
302
322
  * The stub is why the payload has to be served here rather than through the
303
323
  * filesystem: `make` captures the filesystem at construction, so seeding
304
324
  * `GITHUB_EVENT_PATH` through `overrides` would send the read to a noop
305
- * filesystem and fail. Pass the payload as the second argument instead.
325
+ * filesystem and fail. Pass the payload as the second argument instead —
326
+ * or, for a suite that wants the real read path over a full filesystem,
327
+ * compose {@link ActionEnvironment.makeTest} with `@effected/memfs`'s
328
+ * seeded volume; the recipe is on that member.
306
329
  *
307
330
  * @example
308
331
  * ```ts
@@ -1,4 +1,5 @@
1
1
  import { Effect, Schema } from "effect";
2
+ import { HttpClient } from "effect/unstable/http";
2
3
  import { ChildProcessSpawner } from "effect/unstable/process";
3
4
  import { spawn } from "node:child_process";
4
5
  import { closeSync, openSync } from "node:fs";
@@ -79,6 +80,10 @@ var DetachedProcessError = class extends Schema.TaggedError()("DetachedProcessEr
79
80
  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"));
80
81
  /** Whether a thrown value is a Node errno error with the given code. */
81
82
  const isErrno = (cause, code) => typeof cause === "object" && cause !== null && cause.code === code;
83
+ /** See {@link DetachedProcess.makeTestOps}: an unstubbed member dies naming itself. */
84
+ const unimplemented = (member) => {
85
+ throw new Error(`DetachedProcess.makeTestOps: ${member}() was called but not stubbed — pass a \`${member}\` override.`);
86
+ };
82
87
  /**
83
88
  * A long-lived child that outlives the phase that started it.
84
89
  *
@@ -111,7 +116,7 @@ const isErrno = (cause, code) => typeof cause === "object" && cause !== null &&
111
116
  *
112
117
  * @public
113
118
  */
114
- var DetachedProcess = class {
119
+ var DetachedProcess = class DetachedProcess {
115
120
  constructor() {}
116
121
  /**
117
122
  * Start a detached child with its output routed to a log file.
@@ -187,6 +192,34 @@ var DetachedProcess = class {
187
192
  return yield* poll(attempts);
188
193
  });
189
194
  /**
195
+ * A readiness probe over HTTP: `true` when a `GET` answers 2xx.
196
+ *
197
+ * @remarks
198
+ * The probe {@link DetachedProcess.awaitReady} almost always wants: an HTTP
199
+ * `GET` at a port the child is still binding, where connection-refused *is*
200
+ * "not up yet". The consumers that hand-rolled the polling wrote exactly
201
+ * this probe around it, and each had to discover the same subtlety — the
202
+ * probe's error channel must stay empty, because `awaitReady` deliberately
203
+ * propagates probe failures, and a refused connection is not a failure of
204
+ * the probe.
205
+ *
206
+ * So this probe never fails: a refused connection, any other transport
207
+ * error and a non-2xx answer all collapse to `false`, leaving
208
+ * `awaitReady`'s own exhaustion as the only failure left. That is a
209
+ * deliberate opt-out of the propagate-probe-failures posture, and it is
210
+ * only sound because every failure an HTTP readiness check can see means
211
+ * the same thing here. The cost is the documented one: a *misconfigured*
212
+ * probe — a typo'd port, a wrong path — is indistinguishable from a child
213
+ * that never came up, and reports as `notReady` only after the full budget.
214
+ * A probe whose failures are meaningfully distinct should stay a
215
+ * hand-written effect with those failures in `E`.
216
+ *
217
+ * The request goes through core's `HttpClient` — `Action.run` provides it
218
+ * ambiently — rather than a bare `fetch`, per the package rule that
219
+ * everything that *can* go through a core contract does.
220
+ */
221
+ static httpProbe = (url) => HttpClient.get(url).pipe(Effect.map((response) => response.status >= 200 && response.status < 300), Effect.catch(() => Effect.succeed(false)));
222
+ /**
190
223
  * Signal a pid that came back across the phase boundary.
191
224
  *
192
225
  * @remarks
@@ -225,6 +258,36 @@ var DetachedProcess = class {
225
258
  }
226
259
  });
227
260
  });
261
+ /**
262
+ * The real operations: the production default for a
263
+ * {@link DetachedProcessOps} parameter.
264
+ *
265
+ * @remarks
266
+ * The members ARE the statics — same references, not wrappers — so taking
267
+ * the seam costs a consumer nothing over calling the statics directly.
268
+ */
269
+ static ops = {
270
+ spawn: DetachedProcess.spawn,
271
+ awaitReady: DetachedProcess.awaitReady,
272
+ reap: DetachedProcess.reap
273
+ };
274
+ /**
275
+ * A test double. Unstubbed members die rather than silently succeeding.
276
+ *
277
+ * @remarks
278
+ * The package's `makeTest` convention, on the module's one non-service.
279
+ * Every member a test does not stub dies loudly **naming the member**,
280
+ * because silence is worse here than anywhere else in the package: a spawn
281
+ * that silently "succeeds" fakes a child that does not exist, and a reap
282
+ * that silently succeeds asserts nothing about the one call whose
283
+ * production form signals a real pid.
284
+ */
285
+ static makeTestOps = (overrides = {}) => ({
286
+ spawn: () => Effect.sync(() => unimplemented("spawn")),
287
+ awaitReady: () => Effect.sync(() => unimplemented("awaitReady")),
288
+ reap: () => Effect.sync(() => unimplemented("reap")),
289
+ ...overrides
290
+ });
228
291
  };
229
292
 
230
293
  //#endregion
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  The GitHub Actions runtime for [Effect](https://effect.website) v4: the services an action needs to talk to the runner it is executing inside. `Action.run` composes the runtime, runs your program, renders a failure as an `::error::` annotation and sets the exit code. `ActionInput` reads workflow inputs as typed `Config` values — string, boolean, integer, redacted secret, multiline list, `key=value` pairs, or a schema-decoded JSON blob — with one absence rule shared by every accessor: unset and `""` are both missing data. `ActionOutputs`, `ActionState`, `ActionLogger` and a fiber-local `ActionEnvironment` round out the runner surface, alongside `ActionCache`, `Artifact`, a metadata-carrying `BlobStore` (S3-compatible or the runner's own cache), `OidcTokenIssuer` and `ToolInstaller` for the heavier protocols. A reporting suite — `GitHubMarkdown`, `ManagedDocument`, `CheckState` and `CheckDocument` — covers the other direction: rendering what a run did onto a pull request comment, a check run or the job summary. No `@actions/*` dependency anywhere — the cache, artifact and tool-cache protocols are implemented directly against their HTTP APIs.
9
9
 
10
10
  > **Pre-release.** This package is part of the `@effected/*` kit, in pre-`1.0.0`
11
- > development against a single pinned Effect v4 beta. Packages graduate to
11
+ > development against a single pinned Effect v4 prerelease. Packages graduate to
12
12
  > `1.0.0` once Effect `4.0.0` ships. To hold your own `effect` versions at
13
13
  > exactly the ones the kit is built and tested against, install
14
14
  > [`@effected/pnpm-plugin-effect`](https://www.npmjs.com/package/@effected/pnpm-plugin-effect).
package/Secret.js CHANGED
@@ -14,10 +14,18 @@ import { Config, Effect, Redacted } from "effect";
14
14
  * it makes declassification **explicit, auditable, and impossible to do
15
15
  * quietly**.
16
16
  *
17
- * The invariant: **masking and declassification are the same call.** Every
18
- * member registers the value with the runner's log filter (`::add-mask::`)
19
- * *before* returning plaintext, so a secret cannot reach a child's environment
20
- * or a state file without the runner already knowing to redact it from logs.
17
+ * The invariant: **masking is the floor, and declassification implies it.**
18
+ * Every member that touches plaintext routes through
19
+ * `ActionOutputs.setSecret`, registering the value with the runner's log
20
+ * filter (`::add-mask::`) *before* any plaintext is returned — and the one
21
+ * member that returns nothing, {@link Secret.mask}, registers and stops there
22
+ * — so under the normal runner layer a secret cannot reach a child's
23
+ * environment or a state file without the runner already knowing to redact it
24
+ * from logs. Two scoped exceptions: {@link Secret.adopt} is a `Config` that
25
+ * re-wraps an inherited plaintext without involving `ActionOutputs` at all —
26
+ * the parent masked that value before the handoff — and under
27
+ * `ActionOutputs.layerDetached` the registration is a documented no-op,
28
+ * because a mask emitted inside a detached worker would itself be the leak.
21
29
  *
22
30
  * `Redacted.value` appears nowhere else in this package, and a test asserts
23
31
  * that structurally — a reintroduction elsewhere fails the suite rather than
@@ -97,12 +105,9 @@ var Secret = class Secret {
97
105
  * dividing line is where the value goes, not what it is for: a value
98
106
  * crossing into a **child's** environment is {@link Secret.forChildEnv},
99
107
  * one written to `GITHUB_STATE` or `GITHUB_OUTPUT` is
100
- * {@link Secret.forRunnerFile}, and one that stays in this process is this
101
- * member. A third-party SDK that reads only the ambient environment is the
102
- * hard case: this package never mutates `process.env` (reads are seeded
103
- * once through `ActionEnvironment`), so if a consumer chooses that bridge,
104
- * the mutation — and its restore discipline — lives in consumer code as
105
- * the consumer's own tradeoff, not as a pattern this member recommends.
108
+ * {@link Secret.forRunnerFile}, one bound for the ambient environment is
109
+ * {@link Secret.forProcessEnv}, and one that stays in this process — held
110
+ * by this code, passed as an argument — is this member.
106
111
  *
107
112
  * It masks even though the value is never *written* anywhere, and that is
108
113
  * deliberate rather than superstitious: a signing key that leaks does so
@@ -126,6 +131,70 @@ var Secret = class Secret {
126
131
  */
127
132
  static forSigning = (secret) => Secret.forRunnerFile(secret);
128
133
  /**
134
+ * Declassify one secret for the caller to bridge into `process.env`.
135
+ *
136
+ * @remarks
137
+ * The third-party-SDK case, under its own auditable name: an SDK that reads
138
+ * only the ambient environment cannot take the plaintext as an argument, so
139
+ * the value has to cross into `process.env` before the SDK looks. The
140
+ * mechanism is identical to {@link Secret.forRunnerFile} — mask first, then
141
+ * return plaintext — and the name is what earns the member: these names are
142
+ * the audit vocabulary, and a search for this one finds every place a
143
+ * secret enters the ambient environment without wading through signing
144
+ * keys and runner files.
145
+ *
146
+ * The ruling on who mutates is unchanged by the name existing: **this
147
+ * package never mutates `process.env`.** Reads are seeded once, at layer
148
+ * construction, through `ActionEnvironment`, and nothing here writes back
149
+ * — a write from inside the kit would be invisible to that seeding and to
150
+ * every consumer's assumptions about when the environment is stable. This
151
+ * member declassifies and masks; the assignment to `process.env`, and the
152
+ * restore discipline that unwinds it when the bridged scope ends, live in
153
+ * the **caller**, as the caller's own explicit tradeoff.
154
+ *
155
+ * The detached-worker inversion applies exactly as it does to
156
+ * {@link Secret.forSigning}: the mask only works where the runner parses
157
+ * stdout, so a worker that needs a bridged environment gets it from the
158
+ * parent — masked before the spawn via {@link Secret.forChildEnv} — rather
159
+ * than declassifying inside itself.
160
+ */
161
+ static forProcessEnv = (secret) => Secret.forRunnerFile(secret);
162
+ /**
163
+ * Register a secret with the runner's log filter — and return nothing.
164
+ *
165
+ * @remarks
166
+ * Masking without declassification: the value is registered with
167
+ * `::add-mask::` through the same route as every other member, and the
168
+ * success channel is `void`, so a caller cannot come away holding the raw
169
+ * value at all. This is the register-only shape consumers previously spelled
170
+ * as a {@link Secret.forSigning} whose result was discarded — a spelling
171
+ * that lied to the audit vocabulary (a grep for signing found masking),
172
+ * contradicted forSigning's own once-at-construction guidance, and needed a
173
+ * comment to apologize for itself. The names are the vocabulary: a grep for
174
+ * this member finds every register-only site, and a grep for `forSigning`
175
+ * again finds only signing.
176
+ *
177
+ * The canonical caller masks every supplied credential input
178
+ * unconditionally, before the logic that decides which of them will
179
+ * actually be used — a secret the workflow supplied deserves redaction from
180
+ * the log whether or not resolution reaches for it.
181
+ *
182
+ * One value per call is the blessed shape; there is deliberately no
183
+ * set-taking form. {@link Secret.forChildEnv} takes a set because its
184
+ * mask-everything-before-returning-anything ordering is load-bearing; a
185
+ * plain mask returns nothing to order against, and a loop is fine.
186
+ *
187
+ * **In a detached worker, the mask this member emits is a leak.** The
188
+ * `::add-mask::` command only masks when the runner parses this process's
189
+ * stdout; a detached worker's stdout is a log file no runner parses, so
190
+ * there the command masks nothing *and* spells the plaintext into the log
191
+ * — a signing key shipped exactly that way for one round. A worker must
192
+ * compose `ActionOutputs.layerDetached`, under which the mask is a
193
+ * documented no-op; the masking itself is the **parent's** job, done
194
+ * before the spawn via {@link Secret.forChildEnv} under the real layer.
195
+ */
196
+ static mask = (secret) => Effect.asVoid(Secret.forRunnerFile(secret));
197
+ /**
129
198
  * The far side of a handoff: re-wrap a plaintext environment variable.
130
199
  *
131
200
  * @remarks
package/index.d.ts CHANGED
@@ -194,6 +194,26 @@ declare class ActionEnvironment extends ActionEnvironment_base {
194
194
  * `undefined` means *not served*, so a suite cannot arrange `undefined` as
195
195
  * the payload itself. A webhook payload is always a JSON object; the case
196
196
  * does not arise, and reserving the sentinel keeps the argument optional.
197
+ *
198
+ * When a suite wants the **real read path** rather than a served value —
199
+ * exercising the actual `GITHUB_EVENT_PATH` read-and-parse — this member is
200
+ * the seam: it leaves `FileSystem` in `R` precisely so the suite chooses
201
+ * the filesystem. Compose it with `@effected/memfs`'s seeded in-memory
202
+ * volume rather than a hand-rolled stub: the volume honors the whole
203
+ * `FileSystem` contract, and an unseeded path fails typed `NotFound`
204
+ * instead of whatever a hand stub happens to answer.
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * import { ActionEnvironment } from "@effected/github-actions";
209
+ * import { MemoryFileSystem } from "@effected/memfs";
210
+ * import { Layer } from "effect";
211
+ *
212
+ * const layer = Layer.effect(
213
+ * ActionEnvironment,
214
+ * ActionEnvironment.makeTest({ GITHUB_EVENT_PATH: "/event.json" }),
215
+ * ).pipe(Layer.provide(MemoryFileSystem.layerWith({ "/event.json": JSON.stringify({ action: "opened" }) })));
216
+ * ```
197
217
  */
198
218
  static readonly makeTest: (overrides?: Readonly<Record<string, string>>, payload?: unknown) => Effect.Effect<ActionEnvironmentShape, never, FileSystem.FileSystem>;
199
219
  /**
@@ -204,7 +224,10 @@ declare class ActionEnvironment extends ActionEnvironment_base {
204
224
  * The stub is why the payload has to be served here rather than through the
205
225
  * filesystem: `make` captures the filesystem at construction, so seeding
206
226
  * `GITHUB_EVENT_PATH` through `overrides` would send the read to a noop
207
- * filesystem and fail. Pass the payload as the second argument instead.
227
+ * filesystem and fail. Pass the payload as the second argument instead —
228
+ * or, for a suite that wants the real read path over a full filesystem,
229
+ * compose {@link ActionEnvironment.makeTest} with `@effected/memfs`'s
230
+ * seeded volume; the recipe is on that member.
208
231
  *
209
232
  * @example
210
233
  * ```ts
@@ -2897,6 +2920,36 @@ interface ReadinessOptions {
2897
2920
  /** How many times to retry after the first probe. Defaults to 40. */
2898
2921
  readonly attempts?: number | undefined;
2899
2922
  }
2923
+ /**
2924
+ * The module's operations as a value: the seam a consumer's tests inject.
2925
+ *
2926
+ * @remarks
2927
+ * `DetachedProcess` is statics on a class, deliberately — there is no scope to
2928
+ * hang a service off, because the whole point is a child that outlives the
2929
+ * process holding any handle to it. The cost of that choice is that the
2930
+ * package's `makeTest`/`layerTest` convention has nothing to attach to:
2931
+ * statics cannot be swapped by a layer, and a test must never spawn a real
2932
+ * child, poll a real port, or — above all — signal a real pid. This interface
2933
+ * restores the seam as **parameter injection**: production code takes a
2934
+ * `DetachedProcessOps`, defaulting to {@link DetachedProcess.ops}, and a test
2935
+ * passes {@link DetachedProcess.makeTestOps} with only the members it means to
2936
+ * observe.
2937
+ *
2938
+ * {@link DetachedProcess.httpProbe} is deliberately not a member. It is a
2939
+ * probe *constructor* with no process side effect of its own; a test stubs
2940
+ * `awaitReady` — the operation that runs the probe — or provides a stub
2941
+ * `HttpClient`.
2942
+ *
2943
+ * @public
2944
+ */
2945
+ interface DetachedProcessOps {
2946
+ /** {@link DetachedProcess.spawn}. */
2947
+ readonly spawn: (options: DetachedSpawnOptions) => Effect.Effect<ChildProcessSpawner.ProcessId, DetachedProcessError>;
2948
+ /** {@link DetachedProcess.awaitReady}. */
2949
+ readonly awaitReady: <E, R>(probe: Effect.Effect<boolean, E, R>, options?: ReadinessOptions) => Effect.Effect<void, E | DetachedProcessError, R>;
2950
+ /** {@link DetachedProcess.reap}. */
2951
+ readonly reap: (pid: number, signal?: NodeJS.Signals) => Effect.Effect<boolean, DetachedProcessError>;
2952
+ }
2900
2953
  /**
2901
2954
  * A long-lived child that outlives the phase that started it.
2902
2955
  *
@@ -2955,6 +3008,34 @@ declare class DetachedProcess {
2955
3008
  * timeout six seconds later with nothing to show for it.
2956
3009
  */
2957
3010
  static readonly awaitReady: <E, R>(probe: Effect.Effect<boolean, E, R>, options?: ReadinessOptions | undefined) => Effect.Effect<void, E | DetachedProcessError, R>;
3011
+ /**
3012
+ * A readiness probe over HTTP: `true` when a `GET` answers 2xx.
3013
+ *
3014
+ * @remarks
3015
+ * The probe {@link DetachedProcess.awaitReady} almost always wants: an HTTP
3016
+ * `GET` at a port the child is still binding, where connection-refused *is*
3017
+ * "not up yet". The consumers that hand-rolled the polling wrote exactly
3018
+ * this probe around it, and each had to discover the same subtlety — the
3019
+ * probe's error channel must stay empty, because `awaitReady` deliberately
3020
+ * propagates probe failures, and a refused connection is not a failure of
3021
+ * the probe.
3022
+ *
3023
+ * So this probe never fails: a refused connection, any other transport
3024
+ * error and a non-2xx answer all collapse to `false`, leaving
3025
+ * `awaitReady`'s own exhaustion as the only failure left. That is a
3026
+ * deliberate opt-out of the propagate-probe-failures posture, and it is
3027
+ * only sound because every failure an HTTP readiness check can see means
3028
+ * the same thing here. The cost is the documented one: a *misconfigured*
3029
+ * probe — a typo'd port, a wrong path — is indistinguishable from a child
3030
+ * that never came up, and reports as `notReady` only after the full budget.
3031
+ * A probe whose failures are meaningfully distinct should stay a
3032
+ * hand-written effect with those failures in `E`.
3033
+ *
3034
+ * The request goes through core's `HttpClient` — `Action.run` provides it
3035
+ * ambiently — rather than a bare `fetch`, per the package rule that
3036
+ * everything that *can* go through a core contract does.
3037
+ */
3038
+ static readonly httpProbe: (url: string | URL) => Effect.Effect<boolean, never, HttpClient.HttpClient>;
2958
3039
  /**
2959
3040
  * Signal a pid that came back across the phase boundary.
2960
3041
  *
@@ -2976,6 +3057,27 @@ declare class DetachedProcess {
2976
3057
  * finding its child already dead is the normal ending, not a failure.
2977
3058
  */
2978
3059
  static readonly reap: (pid: number, signal?: NodeJS.Signals | undefined) => Effect.Effect<boolean, DetachedProcessError, never>;
3060
+ /**
3061
+ * The real operations: the production default for a
3062
+ * {@link DetachedProcessOps} parameter.
3063
+ *
3064
+ * @remarks
3065
+ * The members ARE the statics — same references, not wrappers — so taking
3066
+ * the seam costs a consumer nothing over calling the statics directly.
3067
+ */
3068
+ static readonly ops: DetachedProcessOps;
3069
+ /**
3070
+ * A test double. Unstubbed members die rather than silently succeeding.
3071
+ *
3072
+ * @remarks
3073
+ * The package's `makeTest` convention, on the module's one non-service.
3074
+ * Every member a test does not stub dies loudly **naming the member**,
3075
+ * because silence is worse here than anywhere else in the package: a spawn
3076
+ * that silently "succeeds" fakes a child that does not exist, and a reap
3077
+ * that silently succeeds asserts nothing about the one call whose
3078
+ * production form signals a real pid.
3079
+ */
3080
+ static readonly makeTestOps: (overrides?: Partial<DetachedProcessOps>) => DetachedProcessOps;
2979
3081
  }
2980
3082
  //#endregion
2981
3083
  //#region src/DryRun.d.ts
@@ -4134,10 +4236,18 @@ declare class PackageManagerInstaller extends PackageManagerInstaller_base {
4134
4236
  * it makes declassification **explicit, auditable, and impossible to do
4135
4237
  * quietly**.
4136
4238
  *
4137
- * The invariant: **masking and declassification are the same call.** Every
4138
- * member registers the value with the runner's log filter (`::add-mask::`)
4139
- * *before* returning plaintext, so a secret cannot reach a child's environment
4140
- * or a state file without the runner already knowing to redact it from logs.
4239
+ * The invariant: **masking is the floor, and declassification implies it.**
4240
+ * Every member that touches plaintext routes through
4241
+ * `ActionOutputs.setSecret`, registering the value with the runner's log
4242
+ * filter (`::add-mask::`) *before* any plaintext is returned — and the one
4243
+ * member that returns nothing, {@link Secret.mask}, registers and stops there
4244
+ * — so under the normal runner layer a secret cannot reach a child's
4245
+ * environment or a state file without the runner already knowing to redact it
4246
+ * from logs. Two scoped exceptions: {@link Secret.adopt} is a `Config` that
4247
+ * re-wraps an inherited plaintext without involving `ActionOutputs` at all —
4248
+ * the parent masked that value before the handoff — and under
4249
+ * `ActionOutputs.layerDetached` the registration is a documented no-op,
4250
+ * because a mask emitted inside a detached worker would itself be the leak.
4141
4251
  *
4142
4252
  * `Redacted.value` appears nowhere else in this package, and a test asserts
4143
4253
  * that structurally — a reintroduction elsewhere fails the suite rather than
@@ -4203,12 +4313,9 @@ declare class Secret {
4203
4313
  * dividing line is where the value goes, not what it is for: a value
4204
4314
  * crossing into a **child's** environment is {@link Secret.forChildEnv},
4205
4315
  * one written to `GITHUB_STATE` or `GITHUB_OUTPUT` is
4206
- * {@link Secret.forRunnerFile}, and one that stays in this process is this
4207
- * member. A third-party SDK that reads only the ambient environment is the
4208
- * hard case: this package never mutates `process.env` (reads are seeded
4209
- * once through `ActionEnvironment`), so if a consumer chooses that bridge,
4210
- * the mutation — and its restore discipline — lives in consumer code as
4211
- * the consumer's own tradeoff, not as a pattern this member recommends.
4316
+ * {@link Secret.forRunnerFile}, one bound for the ambient environment is
4317
+ * {@link Secret.forProcessEnv}, and one that stays in this process — held
4318
+ * by this code, passed as an argument — is this member.
4212
4319
  *
4213
4320
  * It masks even though the value is never *written* anywhere, and that is
4214
4321
  * deliberate rather than superstitious: a signing key that leaks does so
@@ -4231,6 +4338,70 @@ declare class Secret {
4231
4338
  * {@link Secret.forChildEnv} under the real layer.
4232
4339
  */
4233
4340
  static readonly forSigning: (secret: Redacted.Redacted<string>) => Effect.Effect<string, never, ActionOutputs>;
4341
+ /**
4342
+ * Declassify one secret for the caller to bridge into `process.env`.
4343
+ *
4344
+ * @remarks
4345
+ * The third-party-SDK case, under its own auditable name: an SDK that reads
4346
+ * only the ambient environment cannot take the plaintext as an argument, so
4347
+ * the value has to cross into `process.env` before the SDK looks. The
4348
+ * mechanism is identical to {@link Secret.forRunnerFile} — mask first, then
4349
+ * return plaintext — and the name is what earns the member: these names are
4350
+ * the audit vocabulary, and a search for this one finds every place a
4351
+ * secret enters the ambient environment without wading through signing
4352
+ * keys and runner files.
4353
+ *
4354
+ * The ruling on who mutates is unchanged by the name existing: **this
4355
+ * package never mutates `process.env`.** Reads are seeded once, at layer
4356
+ * construction, through `ActionEnvironment`, and nothing here writes back
4357
+ * — a write from inside the kit would be invisible to that seeding and to
4358
+ * every consumer's assumptions about when the environment is stable. This
4359
+ * member declassifies and masks; the assignment to `process.env`, and the
4360
+ * restore discipline that unwinds it when the bridged scope ends, live in
4361
+ * the **caller**, as the caller's own explicit tradeoff.
4362
+ *
4363
+ * The detached-worker inversion applies exactly as it does to
4364
+ * {@link Secret.forSigning}: the mask only works where the runner parses
4365
+ * stdout, so a worker that needs a bridged environment gets it from the
4366
+ * parent — masked before the spawn via {@link Secret.forChildEnv} — rather
4367
+ * than declassifying inside itself.
4368
+ */
4369
+ static readonly forProcessEnv: (secret: Redacted.Redacted<string>) => Effect.Effect<string, never, ActionOutputs>;
4370
+ /**
4371
+ * Register a secret with the runner's log filter — and return nothing.
4372
+ *
4373
+ * @remarks
4374
+ * Masking without declassification: the value is registered with
4375
+ * `::add-mask::` through the same route as every other member, and the
4376
+ * success channel is `void`, so a caller cannot come away holding the raw
4377
+ * value at all. This is the register-only shape consumers previously spelled
4378
+ * as a {@link Secret.forSigning} whose result was discarded — a spelling
4379
+ * that lied to the audit vocabulary (a grep for signing found masking),
4380
+ * contradicted forSigning's own once-at-construction guidance, and needed a
4381
+ * comment to apologize for itself. The names are the vocabulary: a grep for
4382
+ * this member finds every register-only site, and a grep for `forSigning`
4383
+ * again finds only signing.
4384
+ *
4385
+ * The canonical caller masks every supplied credential input
4386
+ * unconditionally, before the logic that decides which of them will
4387
+ * actually be used — a secret the workflow supplied deserves redaction from
4388
+ * the log whether or not resolution reaches for it.
4389
+ *
4390
+ * One value per call is the blessed shape; there is deliberately no
4391
+ * set-taking form. {@link Secret.forChildEnv} takes a set because its
4392
+ * mask-everything-before-returning-anything ordering is load-bearing; a
4393
+ * plain mask returns nothing to order against, and a loop is fine.
4394
+ *
4395
+ * **In a detached worker, the mask this member emits is a leak.** The
4396
+ * `::add-mask::` command only masks when the runner parses this process's
4397
+ * stdout; a detached worker's stdout is a log file no runner parses, so
4398
+ * there the command masks nothing *and* spells the plaintext into the log
4399
+ * — a signing key shipped exactly that way for one round. A worker must
4400
+ * compose `ActionOutputs.layerDetached`, under which the mask is a
4401
+ * documented no-op; the masking itself is the **parent's** job, done
4402
+ * before the spawn via {@link Secret.forChildEnv} under the real layer.
4403
+ */
4404
+ static readonly mask: (secret: Redacted.Redacted<string>) => Effect.Effect<void, never, ActionOutputs>;
4234
4405
  /**
4235
4406
  * The far side of a handoff: re-wrap a plaintext environment variable.
4236
4407
  *
@@ -4242,5 +4413,5 @@ declare class Secret {
4242
4413
  static readonly adopt: (name: string) => Config.Config<Redacted.Redacted<string>>;
4243
4414
  }
4244
4415
  //#endregion
4245
- 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, type WithStepOptions, WorkflowCommand, describeCause, projectCheckState };
4416
+ 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 DetachedProcessOps, 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, type WithStepOptions, WorkflowCommand, describeCause, projectCheckState };
4246
4417
  //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/github-actions",
3
- "version": "0.6.1",
3
+ "version": "0.8.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,16 +39,16 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@azure/storage-blob": "^12.33.0",
42
- "@effected/github": "^0.4.0",
43
- "@effected/glob": "^0.3.0",
44
- "@effected/markdown": "^0.5.1",
45
- "@effected/npm": "^0.9.0",
46
- "@effected/sbom": "^0.3.0",
47
- "@effected/templates": "^0.2.0"
42
+ "@effected/github": "^0.5.0",
43
+ "@effected/glob": "^0.4.0",
44
+ "@effected/markdown": "^0.6.0",
45
+ "@effected/npm": "^0.10.0",
46
+ "@effected/sbom": "^0.4.0",
47
+ "@effected/templates": "^0.3.0"
48
48
  },
49
49
  "peerDependencies": {
50
- "@effect/platform-node": "4.0.0-beta.107",
51
- "effect": "4.0.0-beta.107"
50
+ "@effect/platform-node": "4.0.0-rc.109",
51
+ "effect": "4.0.0-rc.109"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=24.11.0"