@effected/github-actions 0.4.1 → 0.5.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.
@@ -275,13 +275,46 @@ var ActionEnvironment = class ActionEnvironment extends Context.Service()("@effe
275
275
  * context variables have one obviously-correct shape, and requiring each
276
276
  * suite to restate them produced a byte-identical block duplicated six times
277
277
  * across the consumers. Overrides merge on top.
278
+ *
279
+ * The optional second argument serves {@link ActionEnvironmentShape.payload}
280
+ * **directly**, replacing the `GITHUB_EVENT_PATH` read rather than seeding a
281
+ * path and a file behind it. That is the only route to a payload through the
282
+ * standard double: `TEST_DEFAULTS` deliberately omits `GITHUB_EVENT_PATH`, so
283
+ * an unserved payload fails typed and names the variable — the die-on-
284
+ * unstubbed posture the rest of this package keeps.
285
+ *
286
+ * `undefined` means *not served*, so a suite cannot arrange `undefined` as
287
+ * the payload itself. A webhook payload is always a JSON object; the case
288
+ * does not arise, and reserving the sentinel keeps the argument optional.
278
289
  */
279
- static makeTest = (overrides = {}) => make({
290
+ static makeTest = (overrides = {}, payload) => Effect.map(make({
280
291
  ...TEST_DEFAULTS,
281
292
  ...overrides
293
+ }), (shape) => payload === void 0 ? shape : {
294
+ ...shape,
295
+ payload: Effect.succeed(payload)
282
296
  });
283
- /** {@link ActionEnvironment.makeTest} behind a layer, with `FileSystem` stubbed out. */
284
- static layerTest = (overrides = {}) => Layer.effect(ActionEnvironment, ActionEnvironment.makeTest(overrides)).pipe(Layer.provide(FileSystem.layerNoop({})));
297
+ /**
298
+ * {@link ActionEnvironment.makeTest} behind a layer, with `FileSystem` stubbed
299
+ * out.
300
+ *
301
+ * @remarks
302
+ * The stub is why the payload has to be served here rather than through the
303
+ * filesystem: `make` captures the filesystem at construction, so seeding
304
+ * `GITHUB_EVENT_PATH` through `overrides` would send the read to a noop
305
+ * filesystem and fail. Pass the payload as the second argument instead.
306
+ *
307
+ * @example
308
+ * ```ts
309
+ * import { ActionEnvironment } from "@effected/github-actions";
310
+ *
311
+ * const layer = ActionEnvironment.layerTest(
312
+ * { GITHUB_EVENT_NAME: "pull_request" },
313
+ * { pull_request: { number: 42 } },
314
+ * );
315
+ * ```
316
+ */
317
+ static layerTest = (overrides = {}, payload) => Layer.effect(ActionEnvironment, ActionEnvironment.makeTest(overrides, payload)).pipe(Layer.provide(FileSystem.layerNoop({})));
285
318
  };
286
319
 
287
320
  //#endregion
package/ActionInput.js CHANGED
@@ -115,6 +115,43 @@ var ActionInput = class ActionInput {
115
115
  * Every accessor on this class shares the rule, so an **optional** input
116
116
  * needs `Config.withDefault` (or `Config.option`) at the call site or the
117
117
  * read fails outright.
118
+ *
119
+ * **An input whose documented meaning is "set it empty to disable this"
120
+ * cannot be read with `Config.withDefault`.** Empty is classified as missing
121
+ * *before* the default is consulted, so the default substitutes and the
122
+ * documented behavior can never happen. A live action shipped exactly that
123
+ * for its whole lifetime: `action.yml` promised `release-prefix: ""` would
124
+ * disable retry, and it never could. `Config.option` is the shape that
125
+ * distinguishes the states:
126
+ *
127
+ * ```ts
128
+ * Config.option(ActionInput.string("release-prefix"))
129
+ * // WITH a non-empty default in action.yml:
130
+ * // unsupplied -> Some("release:") (the manifest default, via the runner)
131
+ * // explicit "" -> None
132
+ * // explicit value -> Some(value)
133
+ * // WITHOUT a default in action.yml:
134
+ * // unsupplied -> None (the runner still writes the variable, empty)
135
+ * ```
136
+ *
137
+ * Both rows matter: "the runner writes `""` for an unsupplied input" is the
138
+ * no-default case, and it is the manifest default — when there is one — that
139
+ * makes the unsupplied and explicitly-empty cases distinguishable at all.
140
+ *
141
+ * Two consequences follow from how the runner actually behaves, both
142
+ * confirmed against `actions/runner` and on a live runner:
143
+ *
144
+ * - **The runner publishes a variable for every declared input**, so *absent*
145
+ * is a state a runner never produces. The `Some` branch above depends on
146
+ * `action.yml` declaring a **non-empty default** — a fact in a different
147
+ * file, invisible at this call site. Deleting that default does not restore
148
+ * a fallback; an input declaring no default arrives set-and-empty when
149
+ * unsupplied, silently flipping every unsupplied run to `None`. Pin the
150
+ * default with a test if a read depends on it.
151
+ * - **A bare test environment inverts the unsupplied case.** Omitting the key
152
+ * in a test means genuinely absent, which no runner emits, so a test named
153
+ * "unsupplied" asserts the opposite of production and passes. Arrange the
154
+ * manifest default explicitly instead of omitting the key.
118
155
  */
119
156
  static string(name) {
120
157
  return Config.string(inputVariable(name));
@@ -299,6 +336,22 @@ var ActionInput = class ActionInput {
299
336
  *
300
337
  * Taking the environment as an argument rather than reading `process.env`
301
338
  * is what makes inputs testable without mutating the test process.
339
+ *
340
+ * **A bare `ConfigProvider.fromEnv` cannot serve input-name keys.** Composed
341
+ * beneath this provider or {@link ActionInput.providerOver}, it uppercases
342
+ * the config path, so `{ "target-branch": "trunk" }` is looked up as
343
+ * `TARGET-BRANCH`, never matches, and the read falls through to whatever
344
+ * default the call site supplies. It **fails green**: nothing errors, the
345
+ * suite passes against the default, and the test's name claims it proved the
346
+ * input was read. A consuming action's integration harness did exactly this
347
+ * and stayed green across several commits while a supplied `target-branch`
348
+ * silently did nothing.
349
+ *
350
+ * Runner-variable keys (`INPUT_TARGET-BRANCH`, `PLAIN_VAR`) are unaffected —
351
+ * they are already in the spelling `fromEnv` produces, and they resolve. The
352
+ * hazard is specific to keying a `fromEnv` record by **input name**. Passing
353
+ * the record to *this* function instead removes the distinction: it
354
+ * dual-accepts both spellings, so neither can miss.
302
355
  */
303
356
  static provider(env = process.env) {
304
357
  /** Unset, and the `""` the runner writes for an unsupplied input, are both absent. */
@@ -362,6 +415,22 @@ var ActionInput = class ActionInput {
362
415
  * `ActionInput` accessors themselves are unaffected — they read
363
416
  * `INPUT_<MANGLED>` names, whose re-mangled form (`INPUT_INPUT_…`) never
364
417
  * matches, so they fall through to the ambient lookup they always used.
418
+ *
419
+ * **A bare `ConfigProvider.fromEnv({ env })` here serves only runner-variable
420
+ * keys.** `INPUT_TARGET-BRANCH` and `PLAIN_VAR` resolve; an input-name key
421
+ * like `target-branch` is uppercased to `TARGET-BRANCH`, never matches, and
422
+ * fails green — see {@link ActionInput.provider} for the mechanism. Use
423
+ * `ActionInput.provider(env)`, which dual-accepts both spellings.
424
+ *
425
+ * **The single-segment retry above makes the obvious test
426
+ * non-discriminating**, which is worth knowing before writing one. Because a
427
+ * bare `Config.string("release-branch")` resolves through the same `INPUT_`
428
+ * derivation as `ActionInput.string("release-branch")`, a test meant to prove
429
+ * the accessor reads the mangled key passes just as well with the accessor
430
+ * swapped for a bare `Config`. Only a bare `fromEnv` underneath tells them
431
+ * apart — and that is the one shape you should never reach for. Test the
432
+ * derivation through `ActionInput.variable` instead of trying to catch it by
433
+ * substitution.
365
434
  */
366
435
  static providerOver(ambient) {
367
436
  return ConfigProvider.orElse(ConfigProvider.make((path) => {
@@ -380,9 +449,16 @@ var ActionInput = class ActionInput {
380
449
  * `ConfigProvider` layer still wins by normal layer precedence.
381
450
  *
382
451
  * The ambient half is whatever provider is **explicitly installed** when the
383
- * layer builds — which is how a test injects a deterministic environment by
384
- * providing `ConfigProvider.layer(ConfigProvider.fromEnv({ env }))` beneath
385
- * the runtime. When none is installed, a **fresh** `ConfigProvider.fromEnv()`
452
+ * layer builds — which is how a test injects a deterministic environment, by
453
+ * providing `ConfigProvider.layer(ActionInput.provider(env))` beneath the
454
+ * runtime. Prefer `ActionInput.provider` over a bare
455
+ * `ConfigProvider.fromEnv({ env })`: `fromEnv` serves runner-variable keys
456
+ * fine, but uppercases the config path, so a record keyed by **input name**
457
+ * never matches and every such read silently takes its default while the
458
+ * suite stays green ({@link ActionInput.provider} has the full account).
459
+ * `ActionInput.provider` dual-accepts both spellings, so neither can miss.
460
+ *
461
+ * When none is installed, a **fresh** `ConfigProvider.fromEnv()`
386
462
  * is built rather than reading the reference's default: v4 caches that
387
463
  * default once per process, and a snapshot taken before the runner's
388
464
  * variables are visible would resurrect exactly the missed-input class this
package/ActionLogger.js CHANGED
@@ -109,25 +109,27 @@ const flush = (state) => Effect.suspend(() => {
109
109
  const make = Effect.gen(function* () {
110
110
  const env = yield* ActionEnvironment;
111
111
  const flushActive = Effect.flatMap(ActiveBuffer, (state) => state === null ? Effect.void : flush(state));
112
+ const withBuffer = (label, effect, options) => Effect.gen(function* () {
113
+ const minimum = yield* References.MinimumLogLevel;
114
+ if ((yield* env.isDebug) || LogLevel.isLessThanOrEqualTo(minimum, "Debug")) return yield* effect;
115
+ const state = {
116
+ label,
117
+ entries: []
118
+ };
119
+ const buffering = Logger.make((options) => {
120
+ if (LogLevel.isGreaterThanOrEqualTo(options.logLevel, "Warn")) {
121
+ const annotations = options.fiber.getRef(References.CurrentLogAnnotations);
122
+ options.fiber.getRef(Console.Console).log(renderEntry(options.logLevel, options.message, annotations));
123
+ return;
124
+ }
125
+ state.entries.push(formatMessage(options.message));
126
+ });
127
+ return yield* effect.pipe(Effect.provideService(References.MinimumLogLevel, "All"), Effect.provideService(Logger.CurrentLoggers, /* @__PURE__ */ new Set([buffering])), Effect.provideService(ActiveBuffer, state), Effect.onExit((exit) => Exit.isSuccess(exit) && options?.onSuccess === "discard" ? discard(state) : flush(state)));
128
+ });
112
129
  return {
113
130
  group: (name, effect) => Effect.acquireUseRelease(Console.log(WorkflowCommand.group(name)), () => Effect.tapCause(effect, () => flushActive), () => Console.log(WorkflowCommand.endGroup())),
114
- withBuffer: (label, effect, options) => Effect.gen(function* () {
115
- const minimum = yield* References.MinimumLogLevel;
116
- if ((yield* env.isDebug) || LogLevel.isLessThanOrEqualTo(minimum, "Debug")) return yield* effect;
117
- const state = {
118
- label,
119
- entries: []
120
- };
121
- const buffering = Logger.make((options) => {
122
- if (LogLevel.isGreaterThanOrEqualTo(options.logLevel, "Warn")) {
123
- const annotations = options.fiber.getRef(References.CurrentLogAnnotations);
124
- options.fiber.getRef(Console.Console).log(renderEntry(options.logLevel, options.message, annotations));
125
- return;
126
- }
127
- state.entries.push(formatMessage(options.message));
128
- });
129
- return yield* effect.pipe(Effect.provideService(References.MinimumLogLevel, "All"), Effect.provideService(Logger.CurrentLoggers, /* @__PURE__ */ new Set([buffering])), Effect.provideService(ActiveBuffer, state), Effect.onExit((exit) => Exit.isSuccess(exit) && options?.onSuccess === "discard" ? discard(state) : flush(state)));
130
- }),
131
+ withBuffer,
132
+ withStep: (name, effect, options) => withBuffer(name, Effect.tapCause(effect, () => Console.log(`❌ ${name}`)), { onSuccess: "discard" }).pipe(Effect.tap(() => Effect.logInfo(options?.summary ?? `✅ ${name}`))),
131
133
  notice: (message, properties = {}) => Console.log(WorkflowCommand.notice(message, properties)),
132
134
  annotated: (properties, effect) => Effect.annotateLogs(effect, annotationRecord(properties))
133
135
  };
@@ -187,6 +189,7 @@ var ActionLogger = class ActionLogger extends Context.Service()("@effected/githu
187
189
  static makeTest = (overrides = {}) => ({
188
190
  group: (_name, effect) => effect,
189
191
  withBuffer: (_label, effect) => effect,
192
+ withStep: (_name, effect) => effect,
190
193
  notice: () => Effect.void,
191
194
  annotated: (_properties, effect) => effect,
192
195
  ...overrides
package/GitHubMarkdown.js CHANGED
@@ -13,11 +13,25 @@ const passthrough = (value) => Html.make({ value });
13
13
  *
14
14
  * @remarks
15
15
  * `Markdown.stringifyResult` is total over every tree this module can
16
- * construct — its only failure is the hardening depth cap, which a
17
- * fixed-shape two-level tree cannot reach — so the impossible arm is a
18
- * defect, **not** a silent fallback to unescaped string joining. The
19
- * predecessor's fallback arm is the live table-corruption defect this module
20
- * exists to delete.
16
+ * construct — its only failure is the hardening nesting-depth guard (cap
17
+ * 256) — so the impossible arm is a defect, **not** a silent fallback to
18
+ * unescaped string joining. The predecessor's fallback arm is the live
19
+ * table-corruption defect this module exists to delete.
20
+ *
21
+ * What makes the guard unreachable is not that these trees are small; it is
22
+ * that **their depth does not depend on input**. A fragment handed to a
23
+ * member always becomes a **leaf**, never a subtree: the composition members
24
+ * (`table`, `list`, `link`) wrap it in a single `passthrough` node, and the
25
+ * content members (`code`, `codeBlock`) carry it as a node's `value` string.
26
+ * Either way nothing re-parses a fragment into child nodes, so a composition
27
+ * nests strings rather than nodes and the deepest tree the writer can build
28
+ * is a table's five levels, whatever it is handed. Pinned by
29
+ * `__test__/GitHubMarkdown.test.ts`, which nests the writer's own output a
30
+ * thousand deep — four times the cap — and still renders.
31
+ *
32
+ * Keep that property when adding a member: a member that accepts a *node*, or
33
+ * that re-parses a fragment into one, makes depth input-dependent and this
34
+ * whole argument — and the no-wrapping guidance on the class — collapses.
21
35
  */
22
36
  const render = (root) => Result.getOrThrow(Markdown.stringifyResult(root)).trimEnd();
23
37
  const block = (node) => render(Root.make({ children: [node] }));
@@ -41,6 +55,22 @@ const block = (node) => render(Root.make({ children: [node] }));
41
55
  * structural test pins that, on the same terms as the Azure confinement — so
42
56
  * an action that never renders markdown never links the engine.
43
57
  *
58
+ * **Renders cannot fail, so do not `Effect.try`-wrap them.** The serializer's
59
+ * single failure mode is a nesting-depth guard, and no input can reach it: a
60
+ * fragment handed to a member becomes a leaf — a passthrough node or a node's
61
+ * `value` string — never a parsed subtree, so a composition nests strings
62
+ * rather than nodes and the tree's depth is fixed by the construct, not by
63
+ * what it is handed. A test nests the writer's own output a thousand deep and
64
+ * still renders. Wrapping is boilerplate that also widens the catch to
65
+ * anything else thrown inside the builder — a reporting surface degrading for
66
+ * reasons it did not intend.
67
+ *
68
+ * Two exceptions, both in {@link GitHubMarkdown.tableFor} and neither in the
69
+ * serializer. Cells encode through the row schema, so a value smuggled past
70
+ * the types throws its `SchemaError`; and a column's own `format` function is
71
+ * called directly, so its totality is the caller's to guarantee. Well-typed
72
+ * rows through the default codec are total.
73
+ *
44
74
  * @example
45
75
  * ```ts
46
76
  * import { GitHubMarkdown } from "@effected/github-actions";
@@ -95,6 +125,19 @@ var GitHubMarkdown = class GitHubMarkdown {
95
125
  * past the types throws the codec's `SchemaError` as a defect — same
96
126
  * posture as the serializer arm above, **not** a silent fallback.
97
127
  *
128
+ * This is the **only** member where a throw is reachable at runtime, and
129
+ * neither route is the serializer — see the no-wrapping remark on
130
+ * {@link GitHubMarkdown}. There are two:
131
+ *
132
+ * - the **codec**, for a value smuggled past the types. A caller holding
133
+ * only well-typed rows (the normal case, and what the types express)
134
+ * needs no wrapping; a caller feeding rows decoded from somewhere
135
+ * untyped should decode through the schema rather than guard the render.
136
+ * - a column's own **`format`** function, which is called directly and
137
+ * bypasses the codec entirely. Nothing here can make user code total, so
138
+ * a `format` that can throw makes its render able to throw — keep
139
+ * formatters total, or guard at the formatter rather than the render.
140
+ *
98
141
  * Only string-keyed fields become columns; symbol keys are not
99
142
  * enumerable table columns.
100
143
  *
package/README.md CHANGED
@@ -202,7 +202,16 @@ The debounce is trailing with a max-wait, so a burst of reports coalesces into o
202
202
 
203
203
  ## Testing
204
204
 
205
- Every service ships `makeTest(overrides?)` and `layerTest(overrides?)`, with unstubbed members dying loudly and naming themselves — three recorded exceptions state why they default instead: `ActionEnvironment.makeTest` seeds the twelve `GITHUB_*`/`RUNNER_*` variables, `ActionLogger.makeTest` defaults to silent, and `DryRun.makeTest` defaults to rehearsing, the safe direction:
205
+ Every service ships `makeTest(overrides?)` and `layerTest(overrides?)`, with unstubbed members dying loudly and naming themselves — three recorded exceptions state why they default instead: `ActionEnvironment.makeTest` seeds the twelve `GITHUB_*`/`RUNNER_*` variables, `ActionLogger.makeTest` defaults to silent, and `DryRun.makeTest` defaults to rehearsing, the safe direction. `ActionEnvironment.makeTest`/`layerTest` also take the webhook event payload as an optional second argument, serving it directly instead of routing it through a `GITHUB_EVENT_PATH` filesystem read the stubbed filesystem could never satisfy:
206
+
207
+ ```ts
208
+ import { ActionEnvironment } from "@effected/github-actions";
209
+
210
+ const layer = ActionEnvironment.layerTest(
211
+ { GITHUB_EVENT_NAME: "pull_request" },
212
+ { pull_request: { number: 42 } },
213
+ );
214
+ ```
206
215
 
207
216
  ```ts
208
217
  import { ActionOutputs } from "@effected/github-actions";
@@ -221,7 +230,7 @@ const TestOutputs = ActionOutputs.layerTest({
221
230
  - `ActionInput` — typed input accessors (`string`, `boolean`, `integer`, `redacted`, `lines`, `list`, `pairs`, `schema`) sharing one absence rule, plus the `INPUT_`-aware `ConfigProvider` `Action.run` installs by default.
222
231
  - `ActionOutputs` — step outputs, JSON outputs, the job summary, exported variables, `PATH` additions, failure annotations and log masking.
223
232
  - `ActionState` / `ActionEnvironment` — persisted `pre`/`main`/`post` state, and the fiber-local `GITHUB_*`/`RUNNER_*` context read once from `process.env`.
224
- - `ActionLogger` — collapsible groups, buffered step transcripts (`withBuffer({ onSuccess: "discard" })` keeps a green log to one line per step, while a failure, defect or interruption still flushes), `::notice::` annotations, and the `Logger` that renders every `Effect.log*` as a workflow command.
233
+ - `ActionLogger` — collapsible groups, buffered step transcripts (`withBuffer({ onSuccess: "discard" })` drops a green step's transcript entirely, while a failure, defect or interruption still flushes it), `withStep(name, effect, options?)` for the same buffering plus one summary line on success and a `❌ <name>` header above the transcript on failure, `::notice::` annotations, and the `Logger` that renders every `Effect.log*` as a workflow command.
225
234
  - `DryRun` — the rehearsal guard every mutation goes through, driven by the `dry-run` input by default.
226
235
  - `Secret` — the one declassification seam between a `Redacted` value and a plain string, masking before it returns plaintext.
227
236
  - `GitHubToken` — the App-token lifecycle shaped like the `pre`/`main`/`post` workflow: mint, verify scopes, persist, read, revoke.
package/index.d.ts CHANGED
@@ -183,10 +183,40 @@ declare class ActionEnvironment extends ActionEnvironment_base {
183
183
  * context variables have one obviously-correct shape, and requiring each
184
184
  * suite to restate them produced a byte-identical block duplicated six times
185
185
  * across the consumers. Overrides merge on top.
186
+ *
187
+ * The optional second argument serves {@link ActionEnvironmentShape.payload}
188
+ * **directly**, replacing the `GITHUB_EVENT_PATH` read rather than seeding a
189
+ * path and a file behind it. That is the only route to a payload through the
190
+ * standard double: `TEST_DEFAULTS` deliberately omits `GITHUB_EVENT_PATH`, so
191
+ * an unserved payload fails typed and names the variable — the die-on-
192
+ * unstubbed posture the rest of this package keeps.
193
+ *
194
+ * `undefined` means *not served*, so a suite cannot arrange `undefined` as
195
+ * the payload itself. A webhook payload is always a JSON object; the case
196
+ * does not arise, and reserving the sentinel keeps the argument optional.
186
197
  */
187
- static readonly makeTest: (overrides?: Readonly<Record<string, string>>) => Effect.Effect<ActionEnvironmentShape, never, FileSystem.FileSystem>;
188
- /** {@link ActionEnvironment.makeTest} behind a layer, with `FileSystem` stubbed out. */
189
- static readonly layerTest: (overrides?: Readonly<Record<string, string>>) => Layer.Layer<ActionEnvironment>;
198
+ static readonly makeTest: (overrides?: Readonly<Record<string, string>>, payload?: unknown) => Effect.Effect<ActionEnvironmentShape, never, FileSystem.FileSystem>;
199
+ /**
200
+ * {@link ActionEnvironment.makeTest} behind a layer, with `FileSystem` stubbed
201
+ * out.
202
+ *
203
+ * @remarks
204
+ * The stub is why the payload has to be served here rather than through the
205
+ * filesystem: `make` captures the filesystem at construction, so seeding
206
+ * `GITHUB_EVENT_PATH` through `overrides` would send the read to a noop
207
+ * filesystem and fail. Pass the payload as the second argument instead.
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * import { ActionEnvironment } from "@effected/github-actions";
212
+ *
213
+ * const layer = ActionEnvironment.layerTest(
214
+ * { GITHUB_EVENT_NAME: "pull_request" },
215
+ * { pull_request: { number: 42 } },
216
+ * );
217
+ * ```
218
+ */
219
+ static readonly layerTest: (overrides?: Readonly<Record<string, string>>, payload?: unknown) => Layer.Layer<ActionEnvironment>;
190
220
  }
191
221
  //#endregion
192
222
  //#region src/WorkflowCommand.d.ts
@@ -286,6 +316,21 @@ interface WithBufferOptions {
286
316
  */
287
317
  readonly onSuccess?: "flush" | "discard";
288
318
  }
319
+ /**
320
+ * Options for {@link ActionLoggerShape.withStep}.
321
+ *
322
+ * @public
323
+ */
324
+ interface WithStepOptions {
325
+ /**
326
+ * The info line emitted when the step **succeeds**.
327
+ *
328
+ * @remarks
329
+ * Defaults to `✅ <name>`. This is the line the buffered transcript is traded
330
+ * for: a green step reports that it happened and nothing else.
331
+ */
332
+ readonly summary?: string;
333
+ }
289
334
  /**
290
335
  * The {@link ActionLogger} service shape.
291
336
  *
@@ -313,6 +358,40 @@ interface ActionLoggerShape {
313
358
  * what a green step did.
314
359
  */
315
360
  readonly withBuffer: <A, E, R>(label: string, effect: Effect.Effect<A, E, R>, options?: WithBufferOptions) => Effect.Effect<A, E, R>;
361
+ /**
362
+ * Run a named step: quiet when it succeeds, its full transcript when it does
363
+ * not.
364
+ *
365
+ * @remarks
366
+ * The composition a release log actually wants, and the one
367
+ * {@link ActionLoggerShape.withBuffer} alone does not reach. Buffering with
368
+ * `{ onSuccess: "discard" }` gets a green step to *zero* lines; this trades
369
+ * that transcript for exactly one:
370
+ *
371
+ * 1. verbose output is captured rather than printed live,
372
+ * 2. on success the transcript is dropped and a single info line — `summary`,
373
+ * default `✅ <name>` ({@link WithStepOptions}) — goes out in its place,
374
+ * 3. on failure a `❌ <name>` header lands **first**, then the transcript
375
+ * spills beneath it.
376
+ *
377
+ * The ordering in (3) is a property of the **buffered** path. With step
378
+ * debugging on, nothing is buffered — verbose output has already gone out
379
+ * live by the time the failure fires — so the header lands *after* the
380
+ * transcript it would otherwise introduce. That is the honest trade of
381
+ * asking for live output, not a defect, and both orderings are pinned by
382
+ * test.
383
+ *
384
+ * The header and the summary are the whole difference from `withBuffer`, and
385
+ * they are why this is a member rather than a documented recipe: the recipe
386
+ * was independently derived wrong three times by careful readers during one
387
+ * port, because `group` + `withBuffer` looks like complete parity until you
388
+ * notice nothing emits the success line.
389
+ *
390
+ * The summary survives step debugging. Buffering is skipped when the runner
391
+ * asks for verbose output, but the line naming what succeeded is still the
392
+ * cheapest thing in the log.
393
+ */
394
+ readonly withStep: <A, E, R>(name: string, effect: Effect.Effect<A, E, R>, options?: WithStepOptions) => Effect.Effect<A, E, R>;
316
395
  /**
317
396
  * Emit a `::notice::` annotation.
318
397
  *
@@ -1273,6 +1352,43 @@ declare class ActionInput {
1273
1352
  * Every accessor on this class shares the rule, so an **optional** input
1274
1353
  * needs `Config.withDefault` (or `Config.option`) at the call site or the
1275
1354
  * read fails outright.
1355
+ *
1356
+ * **An input whose documented meaning is "set it empty to disable this"
1357
+ * cannot be read with `Config.withDefault`.** Empty is classified as missing
1358
+ * *before* the default is consulted, so the default substitutes and the
1359
+ * documented behavior can never happen. A live action shipped exactly that
1360
+ * for its whole lifetime: `action.yml` promised `release-prefix: ""` would
1361
+ * disable retry, and it never could. `Config.option` is the shape that
1362
+ * distinguishes the states:
1363
+ *
1364
+ * ```ts
1365
+ * Config.option(ActionInput.string("release-prefix"))
1366
+ * // WITH a non-empty default in action.yml:
1367
+ * // unsupplied -> Some("release:") (the manifest default, via the runner)
1368
+ * // explicit "" -> None
1369
+ * // explicit value -> Some(value)
1370
+ * // WITHOUT a default in action.yml:
1371
+ * // unsupplied -> None (the runner still writes the variable, empty)
1372
+ * ```
1373
+ *
1374
+ * Both rows matter: "the runner writes `""` for an unsupplied input" is the
1375
+ * no-default case, and it is the manifest default — when there is one — that
1376
+ * makes the unsupplied and explicitly-empty cases distinguishable at all.
1377
+ *
1378
+ * Two consequences follow from how the runner actually behaves, both
1379
+ * confirmed against `actions/runner` and on a live runner:
1380
+ *
1381
+ * - **The runner publishes a variable for every declared input**, so *absent*
1382
+ * is a state a runner never produces. The `Some` branch above depends on
1383
+ * `action.yml` declaring a **non-empty default** — a fact in a different
1384
+ * file, invisible at this call site. Deleting that default does not restore
1385
+ * a fallback; an input declaring no default arrives set-and-empty when
1386
+ * unsupplied, silently flipping every unsupplied run to `None`. Pin the
1387
+ * default with a test if a read depends on it.
1388
+ * - **A bare test environment inverts the unsupplied case.** Omitting the key
1389
+ * in a test means genuinely absent, which no runner emits, so a test named
1390
+ * "unsupplied" asserts the opposite of production and passes. Arrange the
1391
+ * manifest default explicitly instead of omitting the key.
1276
1392
  */
1277
1393
  static string(name: string): Config.Config<string>;
1278
1394
  /**
@@ -1406,6 +1522,22 @@ declare class ActionInput {
1406
1522
  *
1407
1523
  * Taking the environment as an argument rather than reading `process.env`
1408
1524
  * is what makes inputs testable without mutating the test process.
1525
+ *
1526
+ * **A bare `ConfigProvider.fromEnv` cannot serve input-name keys.** Composed
1527
+ * beneath this provider or {@link ActionInput.providerOver}, it uppercases
1528
+ * the config path, so `{ "target-branch": "trunk" }` is looked up as
1529
+ * `TARGET-BRANCH`, never matches, and the read falls through to whatever
1530
+ * default the call site supplies. It **fails green**: nothing errors, the
1531
+ * suite passes against the default, and the test's name claims it proved the
1532
+ * input was read. A consuming action's integration harness did exactly this
1533
+ * and stayed green across several commits while a supplied `target-branch`
1534
+ * silently did nothing.
1535
+ *
1536
+ * Runner-variable keys (`INPUT_TARGET-BRANCH`, `PLAIN_VAR`) are unaffected —
1537
+ * they are already in the spelling `fromEnv` produces, and they resolve. The
1538
+ * hazard is specific to keying a `fromEnv` record by **input name**. Passing
1539
+ * the record to *this* function instead removes the distinction: it
1540
+ * dual-accepts both spellings, so neither can miss.
1409
1541
  */
1410
1542
  static provider(env?: Readonly<Record<string, string | undefined>>): ConfigProvider.ConfigProvider;
1411
1543
  /**
@@ -1447,6 +1579,22 @@ declare class ActionInput {
1447
1579
  * `ActionInput` accessors themselves are unaffected — they read
1448
1580
  * `INPUT_<MANGLED>` names, whose re-mangled form (`INPUT_INPUT_…`) never
1449
1581
  * matches, so they fall through to the ambient lookup they always used.
1582
+ *
1583
+ * **A bare `ConfigProvider.fromEnv({ env })` here serves only runner-variable
1584
+ * keys.** `INPUT_TARGET-BRANCH` and `PLAIN_VAR` resolve; an input-name key
1585
+ * like `target-branch` is uppercased to `TARGET-BRANCH`, never matches, and
1586
+ * fails green — see {@link ActionInput.provider} for the mechanism. Use
1587
+ * `ActionInput.provider(env)`, which dual-accepts both spellings.
1588
+ *
1589
+ * **The single-segment retry above makes the obvious test
1590
+ * non-discriminating**, which is worth knowing before writing one. Because a
1591
+ * bare `Config.string("release-branch")` resolves through the same `INPUT_`
1592
+ * derivation as `ActionInput.string("release-branch")`, a test meant to prove
1593
+ * the accessor reads the mangled key passes just as well with the accessor
1594
+ * swapped for a bare `Config`. Only a bare `fromEnv` underneath tells them
1595
+ * apart — and that is the one shape you should never reach for. Test the
1596
+ * derivation through `ActionInput.variable` instead of trying to catch it by
1597
+ * substitution.
1450
1598
  */
1451
1599
  static providerOver(ambient: ConfigProvider.ConfigProvider): ConfigProvider.ConfigProvider;
1452
1600
  /**
@@ -1460,9 +1608,16 @@ declare class ActionInput {
1460
1608
  * `ConfigProvider` layer still wins by normal layer precedence.
1461
1609
  *
1462
1610
  * The ambient half is whatever provider is **explicitly installed** when the
1463
- * layer builds — which is how a test injects a deterministic environment by
1464
- * providing `ConfigProvider.layer(ConfigProvider.fromEnv({ env }))` beneath
1465
- * the runtime. When none is installed, a **fresh** `ConfigProvider.fromEnv()`
1611
+ * layer builds — which is how a test injects a deterministic environment, by
1612
+ * providing `ConfigProvider.layer(ActionInput.provider(env))` beneath the
1613
+ * runtime. Prefer `ActionInput.provider` over a bare
1614
+ * `ConfigProvider.fromEnv({ env })`: `fromEnv` serves runner-variable keys
1615
+ * fine, but uppercases the config path, so a record keyed by **input name**
1616
+ * never matches and every such read silently takes its default while the
1617
+ * suite stays green ({@link ActionInput.provider} has the full account).
1618
+ * `ActionInput.provider` dual-accepts both spellings, so neither can miss.
1619
+ *
1620
+ * When none is installed, a **fresh** `ConfigProvider.fromEnv()`
1466
1621
  * is built rather than reading the reference's default: v4 caches that
1467
1622
  * default once per process, and a snapshot taken before the runner's
1468
1623
  * variables are visible would resurrect exactly the missed-input class this
@@ -3023,6 +3178,22 @@ interface GitHubSchemaTable<Row> {
3023
3178
  * structural test pins that, on the same terms as the Azure confinement — so
3024
3179
  * an action that never renders markdown never links the engine.
3025
3180
  *
3181
+ * **Renders cannot fail, so do not `Effect.try`-wrap them.** The serializer's
3182
+ * single failure mode is a nesting-depth guard, and no input can reach it: a
3183
+ * fragment handed to a member becomes a leaf — a passthrough node or a node's
3184
+ * `value` string — never a parsed subtree, so a composition nests strings
3185
+ * rather than nodes and the tree's depth is fixed by the construct, not by
3186
+ * what it is handed. A test nests the writer's own output a thousand deep and
3187
+ * still renders. Wrapping is boilerplate that also widens the catch to
3188
+ * anything else thrown inside the builder — a reporting surface degrading for
3189
+ * reasons it did not intend.
3190
+ *
3191
+ * Two exceptions, both in {@link GitHubMarkdown.tableFor} and neither in the
3192
+ * serializer. Cells encode through the row schema, so a value smuggled past
3193
+ * the types throws its `SchemaError`; and a column's own `format` function is
3194
+ * called directly, so its totality is the caller's to guarantee. Well-typed
3195
+ * rows through the default codec are total.
3196
+ *
3026
3197
  * @example
3027
3198
  * ```ts
3028
3199
  * import { GitHubMarkdown } from "@effected/github-actions";
@@ -3073,6 +3244,19 @@ declare class GitHubMarkdown {
3073
3244
  * past the types throws the codec's `SchemaError` as a defect — same
3074
3245
  * posture as the serializer arm above, **not** a silent fallback.
3075
3246
  *
3247
+ * This is the **only** member where a throw is reachable at runtime, and
3248
+ * neither route is the serializer — see the no-wrapping remark on
3249
+ * {@link GitHubMarkdown}. There are two:
3250
+ *
3251
+ * - the **codec**, for a value smuggled past the types. A caller holding
3252
+ * only well-typed rows (the normal case, and what the types express)
3253
+ * needs no wrapping; a caller feeding rows decoded from somewhere
3254
+ * untyped should decode through the schema rather than guard the render.
3255
+ * - a column's own **`format`** function, which is called directly and
3256
+ * bypasses the codec entirely. Nothing here can make user code total, so
3257
+ * a `format` that can throw makes its render able to throw — keep
3258
+ * formatters total, or guard at the formatter rather than the render.
3259
+ *
3076
3260
  * Only string-keyed fields become columns; symbol keys are not
3077
3261
  * enumerable table columns.
3078
3262
  *
@@ -4058,5 +4242,5 @@ declare class Secret {
4058
4242
  static readonly adopt: (name: string) => Config.Config<Redacted.Redacted<string>>;
4059
4243
  }
4060
4244
  //#endregion
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 };
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 };
4062
4246
  //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/github-actions",
3
- "version": "0.4.1",
3
+ "version": "0.5.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": [