@effected/github-actions 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Action.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { ActionEnvironment } from "./ActionEnvironment.js";
2
2
  import { ActionInput } from "./ActionInput.js";
3
- import { WorkflowCommand } from "./WorkflowCommand.js";
4
3
  import { ActionLogger } from "./ActionLogger.js";
5
4
  import { ActionOutputs } from "./ActionOutputs.js";
6
5
  import { ActionState } from "./ActionState.js";
6
+ import { WorkflowCommand } from "@effected/github-commands";
7
7
  import { NodeServices } from "@effect/platform-node";
8
8
  import { Cause, Effect, Exit, Layer, LogLevel, Option, References, Result } from "effect";
9
9
  import { FetchHttpClient } from "effect/http";
@@ -52,7 +52,7 @@ const withStepDebugLogLevel = (program) => Effect.gen(function* () {
52
52
  * A readable one-line summary of why an action failed.
53
53
  *
54
54
  * @remarks
55
- * `[Tag] message` because a workflow log is read by a human scanning for the
55
+ * `[Tag]: message` because a workflow log is read by a human scanning for the
56
56
  * first red line, and every error in this kit carries both. The `_tag` is what
57
57
  * makes two failures with the same wording distinguishable, and the `message`
58
58
  * getter is what makes the line worth reading.
@@ -99,11 +99,11 @@ const describeError = (error) => {
99
99
  *
100
100
  * Two things it deliberately does **not** do:
101
101
  *
102
- * - **It does not wrap the program in a log buffer.** The package this replaces
103
- * did, and an unhandled defect inside the buffer swallowed the entire
104
- * transcript — the run failed and printed nothing. `ActionLogger.withBuffer`
105
- * is opt-in and flushes on every exit path including a defect, so the
106
- * buffering is where the caller can see it.
102
+ * - **It does not wrap the program in a log buffer.** A buffer that swallowed
103
+ * the transcript on an unhandled defect would leave a run that failed and
104
+ * printed nothing. `ActionLogger.withBuffer` is opt-in and flushes on every
105
+ * exit path including a defect, so the buffering is where the caller can
106
+ * see it.
107
107
  * - **It does not throw.** The returned promise always resolves; the failure is
108
108
  * in `process.exitCode`, which is what the runner reads. An action entry
109
109
  * point that rejected would produce an unhandled rejection *and* a failed
@@ -112,9 +112,9 @@ const describeError = (error) => {
112
112
  * The rendering depth is deliberate too: one `::error::` line carrying
113
113
  * `[Tag]: message`, and the fiddly diagnostics — the full `Cause.pretty` render
114
114
  * with its span trace and stack — behind `::debug::`, which the runner shows
115
- * only when someone turns step debugging on. The package this replaces spliced
116
- * a JS stack into the visible error, which in a bundled action points at one
117
- * line of `dist/main.js`.
115
+ * only when someone turns step debugging on. No JS stack is spliced into the
116
+ * visible error, because in a bundled action it points at one line of
117
+ * `dist/main.js`.
118
118
  *
119
119
  * @example
120
120
  * ```ts
@@ -133,8 +133,9 @@ var Action = class {
133
133
  * Run an action program to completion.
134
134
  *
135
135
  * @remarks
136
- * Resolves whether the program succeeded or not; read `process.exitCode` for
137
- * the verdict.
136
+ * Never rejects: the promise resolves whether the program succeeded or not,
137
+ * so read `process.exitCode` for the verdict. Requirements beyond
138
+ * {@link ActionServices} come from `options.layer`.
138
139
  */
139
140
  static run = (program, options = {}) => {
140
141
  const extra = options.layer;
package/ActionCache.js CHANGED
@@ -169,8 +169,7 @@ const make = (transfer) => Effect.gen(function* () {
169
169
  *
170
170
  * Output and exit code come from the SAME `spawn` handle
171
171
  * (`internal/spawn.ts` — the spawner's convenience members each spawn
172
- * independently, and the double run failed loudly in
173
- * `ToolInstaller.extractZip` on Windows).
172
+ * independently, so reading both from them would run `tar` twice).
174
173
  */
175
174
  const tar = (args, key, tolerateWarnings) => Effect.gen(function* () {
176
175
  const { output, code } = yield* spawnOnce(spawner, ChildProcess.make("tar", [...args])).pipe(Effect.mapError(archiveFailed(key)));
@@ -14,6 +14,7 @@ import { Context, Effect, FileSystem, Layer, Option, Schema } from "effect";
14
14
  * @public
15
15
  */
16
16
  var ActionEnvironmentError = class extends Schema.TaggedError()("ActionEnvironmentError", {
17
+ /** `missing` — absent or empty; `malformed` — present but unusable. */
17
18
  reason: Schema.Literals(["missing", "malformed"]),
18
19
  /** The environment variable involved. */
19
20
  name: Schema.String,
@@ -32,6 +33,7 @@ var ActionEnvironmentError = class extends Schema.TaggedError()("ActionEnvironme
32
33
  var GitHubContext = class extends Schema.Class("GitHubContext")({
33
34
  /** `owner/repo`. */
34
35
  repository: Schema.String,
36
+ /** The repository owner's login. */
35
37
  repositoryOwner: Schema.String,
36
38
  /** The full ref, e.g. `refs/heads/main`. */
37
39
  ref: Schema.String,
@@ -53,16 +55,27 @@ var GitHubContext = class extends Schema.Class("GitHubContext")({
53
55
  * an encoded context stays plain JSON.
54
56
  */
55
57
  headRef: Schema.OptionFromNullOr(Schema.String),
58
+ /** The commit SHA that triggered the workflow. */
56
59
  sha: Schema.String,
60
+ /** The workflow's name. */
57
61
  workflow: Schema.String,
62
+ /** The current job's id. */
58
63
  job: Schema.String,
64
+ /** The run's unique number, decoded from `GITHUB_RUN_ID`. */
59
65
  runId: Schema.Number,
66
+ /** The attempt number of this run, starting at 1. */
60
67
  runAttempt: Schema.Number,
68
+ /** The name of the event that triggered the workflow, e.g. `push`. */
61
69
  eventName: Schema.String,
70
+ /** The login of the user that triggered the run. */
62
71
  actor: Schema.String,
72
+ /** The GitHub server URL, e.g. `https://github.com`. */
63
73
  serverUrl: Schema.String,
74
+ /** The REST API URL, e.g. `https://api.github.com`. */
64
75
  apiUrl: Schema.String,
76
+ /** The GraphQL API URL. */
65
77
  graphqlUrl: Schema.String,
78
+ /** The default working directory on the runner for steps. */
66
79
  workspace: Schema.String
67
80
  }) {
68
81
  /**
@@ -86,8 +99,11 @@ var GitHubContext = class extends Schema.Class("GitHubContext")({
86
99
  * @public
87
100
  */
88
101
  var RunnerContext = class extends Schema.Class("RunnerContext")({
102
+ /** The runner's operating system: `Linux`, `Windows` or `macOS`. */
89
103
  os: Schema.String,
104
+ /** The runner's architecture, e.g. `X64` or `ARM64`. */
90
105
  arch: Schema.String,
106
+ /** The runner's name. */
91
107
  name: Schema.String,
92
108
  /** A scratch directory emptied between jobs. */
93
109
  temp: Schema.String,
@@ -101,8 +117,7 @@ var RunnerContext = class extends Schema.Class("RunnerContext")({
101
117
  * A `Context.Reference` rather than a mutable global: overrides are
102
118
  * **fiber-local**, so two concurrent fibers can override the same variable
103
119
  * without seeing each other, and nothing needs restoring afterwards. This is
104
- * what makes {@link ActionEnvironmentShape.withEnv} parallel-safe — the
105
- * property the hand-rolled set/restore version it replaces could not have.
120
+ * what makes {@link ActionEnvironmentShape.withEnv} parallel-safe.
106
121
  *
107
122
  * @internal
108
123
  */
@@ -240,10 +255,10 @@ var ActionEnvironment = class ActionEnvironment extends Context.Service()("@effe
240
255
  * @remarks
241
256
  * **A recorded exception to the die-on-unstubbed rule.** Every other service
242
257
  * in this package dies when a suite calls something it did not stub, because
243
- * a plausible default hides a real gap. Here the opposite is true: the twelve
244
- * context variables have one obviously-correct shape, and requiring each
245
- * suite to restate them produced a byte-identical block duplicated six times
246
- * across the consumers. Overrides merge on top.
258
+ * a plausible default hides a real gap. Here the opposite is true: the context
259
+ * variables have one obviously-correct shape, and requiring each suite to
260
+ * restate them would duplicate a byte-identical block in every suite.
261
+ * Overrides merge on top.
247
262
  *
248
263
  * The optional second argument serves {@link ActionEnvironmentShape.payload}
249
264
  * **directly**, replacing the `GITHUB_EVENT_PATH` read rather than seeding a
package/ActionInput.js CHANGED
@@ -9,10 +9,10 @@ import { Config, ConfigProvider, Context, Effect, Schema, SchemaIssue } from "ef
9
9
  * and leaves every other character, **dashes included**, alone. So the input
10
10
  * `sbom-config` arrives as `INPUT_SBOM-CONFIG`, not `INPUT_SBOM_CONFIG`.
11
11
  *
12
- * That distinction is not academic: a consumer read `process.env["INPUT_SBOM_CONFIG"]`
13
- * directly, silently got nothing, and shipped it. Every accessor in this module
14
- * goes through this function so no caller ever spells the variable name, which
15
- * makes that class of bug unrepresentable rather than merely documented.
12
+ * That distinction is not academic: reading `process.env["INPUT_SBOM_CONFIG"]`
13
+ * directly silently yields nothing. Every accessor in this module goes through
14
+ * this function so no caller ever spells the variable name, which makes that
15
+ * class of bug unrepresentable rather than merely documented.
16
16
  *
17
17
  * @internal
18
18
  */
@@ -24,11 +24,10 @@ const inputVariable = (name) => `INPUT_${name.replaceAll(" ", "_").toUpperCase()
24
24
  * `Config.withDefault` and `Config.option` fall back only when the config
25
25
  * resolves *absent* — no provider input at all. A failure raised from inside
26
26
  * `Config.mapEffect` is a hard evaluation failure carrying the evidence that
27
- * input WAS present, so a default never swallows it (`Config.ts`: the
28
- * `Resolved`/`Absent` resolution model and `mapEffect`). That is the guard
29
- * against the shipped defect where `dry-run: yes` quietly became `false` and
30
- * the action performed the mutations the author meant to rehearse. `actual`
31
- * carries the offending value so the rendered error names it.
27
+ * input WAS present, so a default never swallows it. That is the guard against
28
+ * `dry-run: yes` quietly becoming `false` and the action performing the
29
+ * mutations the author meant to rehearse. `actual` carries the offending value
30
+ * so the rendered error names it.
32
31
  */
33
32
  const configError = (message, actual) => new Config.ConfigError(new Schema.SchemaError(new SchemaIssue.InvalidValue({ message }, actual)));
34
33
  /** YAML 1.2 core-schema booleans, which is what the runner documents. */
@@ -53,19 +52,12 @@ const stripComment = (line) => {
53
52
  * @remarks
54
53
  * Inputs are **never** read from `process.env` directly. Every accessor names
55
54
  * the input the way the workflow author wrote it and lets this module derive
56
- * the runner's variable name — see `inputVariable` for why that matters. (A
57
- * `{@link}` cannot reach it: it is internal, so it is not exported from the
58
- * entrypoint and the reference would not resolve.)
55
+ * the runner's variable name — see `ActionInput.variable` for why that
56
+ * matters.
59
57
  *
60
- * `lines`, `list` and `pairs` exist because the `@actions/core`-faithful
61
- * newline split was not enough for real consumers: two of them reinvented
62
- * richer parsing independently, one parsing JSON arrays and bullet lists, the
63
- * other stripping comments and reading `key=value` pairs. Those are now one
64
- * implementation with one set of tests instead of two divergent ones with
65
- * none.
66
- *
67
- * These are grouped statics over one concept reaching nothing but `Config` —
68
- * not a namespace object over engines.
58
+ * `lines` is the `@actions/core`-faithful newline split; `list` and `pairs`
59
+ * read the richer shapes workflow authors write — JSON arrays, bullet lists,
60
+ * comments and `key=value` pairs.
69
61
  *
70
62
  * **The absence contract, shared by every accessor:** the runner writes `""`
71
63
  * for an input the workflow left out, and the provider reads an empty string
@@ -120,10 +112,9 @@ var ActionInput = class ActionInput {
120
112
  * **An input whose documented meaning is "set it empty to disable this"
121
113
  * cannot be read with `Config.withDefault`.** Empty is classified as missing
122
114
  * *before* the default is consulted, so the default substitutes and the
123
- * documented behavior can never happen. A live action shipped exactly that
124
- * for its whole lifetime: `action.yml` promised `release-prefix: ""` would
125
- * disable retry, and it never could. `Config.option` is the shape that
126
- * distinguishes the states:
115
+ * documented behavior can never happen: if `action.yml` promises
116
+ * `release-prefix: ""` disables retry, a `withDefault` read makes that
117
+ * impossible. `Config.option` is the shape that distinguishes the states:
127
118
  *
128
119
  * ```ts
129
120
  * Config.option(ActionInput.string("release-prefix"))
@@ -139,8 +130,7 @@ var ActionInput = class ActionInput {
139
130
  * no-default case, and it is the manifest default — when there is one — that
140
131
  * makes the unsupplied and explicitly-empty cases distinguishable at all.
141
132
  *
142
- * Two consequences follow from how the runner actually behaves, both
143
- * confirmed against `actions/runner` and on a live runner:
133
+ * Two consequences follow from how the runner actually behaves:
144
134
  *
145
135
  * - **The runner publishes a variable for every declared input**, so *absent*
146
136
  * is a state a runner never produces. The `Some` branch above depends on
@@ -164,8 +154,8 @@ var ActionInput = class ActionInput {
164
154
  * Absent or `""` is **missing data** — wrap with `Config.withDefault` for
165
155
  * an optional flag. A **present but malformed** value (`yes`, `on`, `1`) is
166
156
  * a different class: it fails carrying its `actual`, deliberately, so a
167
- * default does NOT swallow it — the shipped defect this guards against was
168
- * a malformed `dry-run` quietly reading as `false`.
157
+ * default does NOT swallow it, so a malformed `dry-run` never quietly reads
158
+ * as `false`.
169
159
  */
170
160
  static boolean(name) {
171
161
  return Config.String(inputVariable(name)).pipe(Config.mapEffect((raw) => {
@@ -247,10 +237,8 @@ var ActionInput = class ActionInput {
247
237
  * @remarks
248
238
  * A JSON array (`["a","b"]`), a bullet list (`- a` or `* a`), or comma- and
249
239
  * newline-separated values, with full-line `#` comments dropped — any
250
- * combination of these in the same input. Each shape was reinvented
251
- * independently by a consumer (one a bullet-list-and-JSON parser, the other
252
- * comment-stripping); accepting the union means a workflow author's first
253
- * guess works and nothing either prior consumer relied on regresses.
240
+ * combination of these in the same input — so a workflow author's first
241
+ * guess works.
254
242
  *
255
243
  * Parsing order, applied per newline/comma-separated item after trimming:
256
244
  *
@@ -297,9 +285,9 @@ var ActionInput = class ActionInput {
297
285
  * Only the **first** `=` splits, so a value may contain one.
298
286
  *
299
287
  * An **empty key** (`=value`, or a bare `=`) is always rejected: `{ "": v }`
300
- * cannot be what a workflow meant, and the damage lands far from the typo —
301
- * an empty key became a repository filter that matched nothing, and the run
302
- * reported zero results with no indication why. An empty **value** (`key=`)
288
+ * cannot be what a workflow meant, and the damage would land far from the
289
+ * typo — an empty key becomes a filter that matches nothing, and the run
290
+ * reports zero results with no indication why. An empty **value** (`key=`)
303
291
  * is accepted by default, because setting a property to the empty string is
304
292
  * legitimate; pass `requireValue` to reject it. Every rejection names the
305
293
  * offending line.
@@ -340,9 +328,8 @@ var ActionInput = class ActionInput {
340
328
  *
341
329
  * There is no `schemaOption`: `ActionInput.schema(name, S).pipe(Config.option)`
342
330
  * yields `Config<Option<A>>` with missing → `None` and a malformed value
343
- * still failing — core's documented `Config.option` behaviour ("validation
344
- * errors ... still propagate", `Config.ts`), which is exactly the
345
- * missing-versus-malformed split above.
331
+ * still failing — core's `Config.option` lets validation errors propagate,
332
+ * which is exactly the missing-versus-malformed split above.
346
333
  */
347
334
  static schema(name, schema) {
348
335
  return Config.String(inputVariable(name)).pipe(Config.mapEffect((raw) => {
@@ -362,7 +349,7 @@ var ActionInput = class ActionInput {
362
349
  * The record dual-accepts, and the split is the `INPUT_` prefix:
363
350
  *
364
351
  * - A key already spelled as a runner variable (`INPUT_BIOME-VERSION`,
365
- * `PLAIN_VAR`) is read verbatim, exactly as before.
352
+ * `PLAIN_VAR`) is read verbatim.
366
353
  * - Any other key is treated as an **input name**, `with:`-block style —
367
354
  * `{ "biome-version": "2.3.14" }` — and serves the variable this module
368
355
  * derives for it (`ActionInput.variable`). This is the spelling to
@@ -379,11 +366,10 @@ var ActionInput = class ActionInput {
379
366
  * installs ({@link ActionInput.providerOver} via `layerDefault`) — so a
380
367
  * bare `Config.String("biome-version")` resolves under this provider
381
368
  * exactly as it does inside `Action.run`. Nested and numeric paths pass
382
- * through as the joined spelling only, as ever.
369
+ * through as the joined spelling only.
383
370
  *
384
- * The source package's `ActionsConfigProvider` behaviors are otherwise
385
- * preserved exactly: the path is joined with `_`, spaces become underscores
386
- * and the whole is uppercased; and an **empty string reads as absent**,
371
+ * Otherwise the path is joined with `_`, spaces become underscores and the
372
+ * whole is uppercased; and an **empty string reads as absent**,
387
373
  * because the runner sets unsupplied inputs to `""` and treating that as
388
374
  * present would make every optional input look supplied. That rule applies
389
375
  * to input-name entries too: `{ "flag": "" }` is an unsupplied input.
@@ -401,9 +387,7 @@ var ActionInput = class ActionInput {
401
387
  * `TARGET-BRANCH`, never matches, and the read falls through to whatever
402
388
  * default the call site supplies. It **fails green**: nothing errors, the
403
389
  * suite passes against the default, and the test's name claims it proved the
404
- * input was read. A consuming action's integration harness did exactly this
405
- * and stayed green across several commits while a supplied `target-branch`
406
- * silently did nothing.
390
+ * input was read.
407
391
  *
408
392
  * Runner-variable keys (`INPUT_TARGET-BRANCH`, `PLAIN_VAR`) are unaffected —
409
393
  * they are already in the spelling `fromEnv` produces, and they resolve. The
@@ -437,9 +421,8 @@ var ActionInput = class ActionInput {
437
421
  * A layer installing {@link ActionInput.provider}.
438
422
  *
439
423
  * @remarks
440
- * `ConfigProvider.ConfigProvider` is a `Context.Reference` in v4, so this is
441
- * a reference override rather than a service — there is no
442
- * `Effect.withConfigProvider`.
424
+ * `ConfigProvider.ConfigProvider` is a `Context.Reference`, so this is a
425
+ * reference override rather than a service.
443
426
  *
444
427
  * A parameterized layer factory mints a fresh reference per call; bind it to
445
428
  * a `const` rather than calling it at each composition site.
@@ -459,10 +442,9 @@ var ActionInput = class ActionInput {
459
442
  * passed to `ambient` untouched, so no path semantics are invented that the
460
443
  * runner does not have.
461
444
  *
462
- * This exists because a live action shipped a **false green**: every bare
463
- * `Config` read fell back to its `withDefault` because the runner exposes
464
- * inputs as `INPUT_<MANGLED>` variables and a plain-named lookup finds
465
- * nothing. Under this provider a bare read resolves through the same
445
+ * This exists because the runner exposes inputs as `INPUT_<MANGLED>`
446
+ * variables, so a plain-named lookup finds nothing and a bare `Config` read
447
+ * would silently fall back to its `withDefault`. Under this provider a bare read resolves through the same
466
448
  * derivation `ActionInput.string` uses, so side-stepping the typed accessors
467
449
  * degrades to the right answer instead of to the default.
468
450
  *
@@ -518,7 +500,7 @@ var ActionInput = class ActionInput {
518
500
  * `ActionInput.provider` dual-accepts both spellings, so neither can miss.
519
501
  *
520
502
  * When none is installed, a **fresh** `ConfigProvider.fromEnv()`
521
- * is built rather than reading the reference's default: v4 caches that
503
+ * is built rather than reading the reference's default: core caches that
522
504
  * default once per process, and a snapshot taken before the runner's
523
505
  * variables are visible would resurrect exactly the missed-input class this
524
506
  * layer exists to kill. An action's environment is fixed before the process
package/ActionLogger.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ActionEnvironment } from "./ActionEnvironment.js";
2
- import { WorkflowCommand } from "./WorkflowCommand.js";
2
+ import { CommandNeutralizer, WorkflowCommand } from "@effected/github-commands";
3
3
  import { Console, Context, Effect, Exit, Inspectable, Layer, LogLevel, Logger, References } from "effect";
4
4
 
5
5
  //#region src/ActionLogger.ts
@@ -67,7 +67,11 @@ const annotationRecord = (properties) => ({ ...properties });
67
67
  * @remarks
68
68
  * `Info` is deliberately plain text with no command prefix — it is ordinary
69
69
  * step output, and prefixing it would make every informational line an
70
- * annotation in the workflow summary.
70
+ * annotation in the workflow summary. It is neutralized, though: the text is
71
+ * whatever the program logged, and the runner reads every line of stdout, so a
72
+ * message carrying `::add-mask::` or a `##[` would be a command. The levels that
73
+ * render AS a command escape their data instead, and a line that starts `::` is
74
+ * read by the runner as that command alone.
71
75
  */
72
76
  const renderEntry = (level, message, annotations) => {
73
77
  const text = formatMessage(message);
@@ -75,7 +79,7 @@ const renderEntry = (level, message, annotations) => {
75
79
  const properties = readAnnotations(annotations);
76
80
  return LogLevel.isGreaterThanOrEqualTo(level, "Error") ? WorkflowCommand.error(text, properties) : WorkflowCommand.warning(text, properties);
77
81
  }
78
- return LogLevel.isGreaterThanOrEqualTo(level, "Info") ? text : WorkflowCommand.debug(text);
82
+ return LogLevel.isGreaterThanOrEqualTo(level, "Info") ? CommandNeutralizer.text(text) : WorkflowCommand.debug(text);
79
83
  };
80
84
  /** The line the runner should see for one log event, with the fiber's annotations. */
81
85
  const renderLogger = Logger.make((options) => renderEntry(options.logLevel, options.message, options.fiber.getRef(References.CurrentLogAnnotations)));
@@ -106,7 +110,7 @@ const flush = (state) => Effect.suspend(() => {
106
110
  `--- End buffered output for "${state.label}" ---`
107
111
  ];
108
112
  state.entries.length = 0;
109
- return Effect.forEach(body, (line) => Console.log(line), { discard: true });
113
+ return Effect.forEach(body, (line) => Console.log(CommandNeutralizer.text(line)), { discard: true });
110
114
  });
111
115
  const make = Effect.gen(function* () {
112
116
  const env = yield* ActionEnvironment;
@@ -130,7 +134,7 @@ const make = Effect.gen(function* () {
130
134
  return {
131
135
  group: (name, effect) => Effect.acquireUseRelease(Console.log(WorkflowCommand.group(name)), () => Effect.tapCause(effect, () => flushActive), () => Console.log(WorkflowCommand.endGroup())),
132
136
  withBuffer,
133
- withStep: (name, effect, options) => withBuffer(name, Effect.tapCause(effect, () => Console.log(`❌ ${name}`)), { onSuccess: "discard" }).pipe(Effect.tap(() => Effect.logInfo(options?.summary ?? `✅ ${name}`))),
137
+ withStep: (name, effect, options) => withBuffer(name, Effect.tapCause(effect, () => Console.log(CommandNeutralizer.text(`❌ ${name}`))), { onSuccess: "discard" }).pipe(Effect.tap(() => Effect.logInfo(options?.summary ?? `✅ ${name}`))),
134
138
  notice: (message, properties = {}) => Console.log(WorkflowCommand.notice(message, properties)),
135
139
  annotated: (properties, effect) => Effect.annotateLogs(effect, annotationRecord(properties))
136
140
  };
@@ -159,6 +163,14 @@ const make = Effect.gen(function* () {
159
163
  * @public
160
164
  */
161
165
  var ActionLogger = class ActionLogger extends Context.Service()("@effected/github-actions/ActionLogger") {
166
+ /**
167
+ * The service: groups, the buffered step renderer, notices and annotations.
168
+ *
169
+ * @remarks
170
+ * This installs no `Logger`. Without {@link ActionLogger.layerLogger} (or `Action.run`, which installs it) Effect's
171
+ * default logger writes `Effect.log*` text raw, so a message carrying `::add-mask::` or `##[` is a workflow command
172
+ * to the runner. Install both: the service for its structure, the logger for its neutralizing of every log line.
173
+ */
162
174
  static layer = Layer.effect(this, make);
163
175
  /**
164
176
  * The `Logger` that maps Effect log levels onto workflow commands.
@@ -172,7 +184,13 @@ var ActionLogger = class ActionLogger extends Context.Service()("@effected/githu
172
184
  * observable in a test without a runner.
173
185
  */
174
186
  static logger = commandLogger;
175
- /** {@link ActionLogger.logger} installed as the only logger. */
187
+ /**
188
+ * {@link ActionLogger.logger} installed as the only logger.
189
+ *
190
+ * @remarks
191
+ * Bound to a constant so every composition site shares one layer;
192
+ * `ActionRuntime.layer` already includes it.
193
+ */
176
194
  static layerLogger = Logger.layer([ActionLogger.logger]);
177
195
  /**
178
196
  * A silent double.
@@ -198,10 +216,9 @@ var ActionLogger = class ActionLogger extends Context.Service()("@effected/githu
198
216
  * A silent service **and** a silent `Effect.log*`.
199
217
  *
200
218
  * @remarks
201
- * This one layer replaces the bare `Effect.provide(Logger.layer([]))` that
202
- * appeared thirteen times in one consuming action and twenty-three in
203
- * another, purely to keep test output quiet. Bound to a constant rather than
204
- * exposed as a factory, so composing it twice is free.
219
+ * This one layer stands in for a bare `Effect.provide(Logger.layer([]))`
220
+ * repeated in every suite just to keep test output quiet. Bound to a
221
+ * constant rather than exposed as a factory, so composing it twice is free.
205
222
  */
206
223
  static layerSilent = Layer.mergeAll(ActionLogger.layerTest(), Logger.layer([]));
207
224
  };
package/ActionOutputs.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ActionEnvironment } from "./ActionEnvironment.js";
2
- import { WorkflowCommand } from "./WorkflowCommand.js";
3
2
  import { heredocBlock, isUsableName } from "./internal/runnerFile.js";
4
3
  import { unstubbed } from "./internal/unstubbed.js";
4
+ import { CommandNeutralizer, WorkflowCommand } from "@effected/github-commands";
5
5
  import { Console, Context, Effect, FileSystem, Layer, Schema } from "effect";
6
6
 
7
7
  //#region src/ActionOutputs.ts
@@ -180,9 +180,30 @@ const dies = unstubbed("ActionOutputs.makeTest");
180
180
  * — that is `Action.run`'s job, so that an action which reports a failure and
181
181
  * then recovers is not silently doomed by a side effect it cannot undo.
182
182
  *
183
+ * Members that write a runner file fail with {@link ActionOutputError}.
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * import { ActionOutputs } from "@effected/github-actions";
188
+ * import { Effect, Schema } from "effect";
189
+ *
190
+ * const program = Effect.gen(function* () {
191
+ * const outputs = yield* ActionOutputs;
192
+ * yield* outputs.set("version", "1.2.3");
193
+ * yield* outputs.setJson("packages", ["a", "b"], Schema.Array(Schema.String));
194
+ * });
195
+ * ```
196
+ *
183
197
  * @public
184
198
  */
185
199
  var ActionOutputs = class ActionOutputs extends Context.Service()("@effected/github-actions/ActionOutputs") {
200
+ /**
201
+ * The live service, appending to the runner's `GITHUB_OUTPUT`, `GITHUB_ENV`,
202
+ * `GITHUB_PATH` and `GITHUB_STEP_SUMMARY` files.
203
+ *
204
+ * @remarks
205
+ * `ActionRuntime.layer` already provides every requirement.
206
+ */
186
207
  static layer = Layer.effect(this, make);
187
208
  /**
188
209
  * The outputs surface that is correct inside a **detached worker**.
@@ -192,10 +213,8 @@ var ActionOutputs = class ActionOutputs extends Context.Service()("@effected/git
192
213
  * stdout is a **log file no runner parses**, so under the real layer
193
214
  * `setSecret` emits `::add-mask::<plaintext>` into that file — a command
194
215
  * that is simultaneously **inert** (nothing reads it, so nothing is masked)
195
- * and a **secret leak** (the plaintext is now sitting verbatim in a log). A
196
- * production action shipped exactly that for one round: an S3 secret and
197
- * session token written into a worker's log by the masking call itself.
198
- * This layer is the worker-side fix; the parent-side rule is that every
216
+ * and a **secret leak** (the plaintext is now sitting verbatim in a log).
217
+ * This layer is the worker-side guard; the parent-side rule is that every
199
218
  * secret a worker will hold is masked **before** the spawn, by
200
219
  * `Secret.forChildEnv` under the real layer.
201
220
  *
@@ -208,7 +227,7 @@ var ActionOutputs = class ActionOutputs extends Context.Service()("@effected/git
208
227
  * spelling of it in the log IS the leak. Masking is the parent's job,
209
228
  * before the worker exists.
210
229
  * - **`set`, `setJson`, `exportVariable`, `addPath`, `summary` — fail
211
- * typed** (`reason: "detached"`, naming the file). Each writes a runner
230
+ * typed** with {@link DetachedOutputError}, naming the file. Each writes a runner
212
231
  * file that configures the parent job's *later steps* — `GITHUB_OUTPUT`,
213
232
  * `GITHUB_ENV`, `GITHUB_PATH` — or is collected when the step completes
214
233
  * (`GITHUB_STEP_SUMMARY`). A detached worker has no later steps, may
@@ -237,7 +256,7 @@ var ActionOutputs = class ActionOutputs extends Context.Service()("@effected/git
237
256
  name
238
257
  })),
239
258
  addPath: () => Effect.fail(new DetachedOutputError({ file: RUNNER_FILE.addPath })),
240
- setFailed: (message) => Console.error(message),
259
+ setFailed: (message) => Console.error(CommandNeutralizer.text(message)),
241
260
  setSecret: () => Effect.void
242
261
  });
243
262
  /**
@@ -277,7 +296,7 @@ var ActionOutputs = class ActionOutputs extends Context.Service()("@effected/git
277
296
  * @remarks
278
297
  * This is the double most tests of an action want — "what did it publish?"
279
298
  * — shipped so that nobody hand-writes a `setJson` override that drops the
280
- * schema encode (the trap `makeTest` now closes too). `setJson` encodes
299
+ * schema encode (a trap `makeTest` closes too). `setJson` encodes
281
300
  * through its schema exactly as the real layer does, failing typed with
282
301
  * {@link OutputEncodeError} and recording nothing on a drift, and records
283
302
  * the **encoded JSON text** — what the runner would have read — not the
package/ActionState.js CHANGED
@@ -6,7 +6,8 @@ import { Context, Effect, FileSystem, Layer, Option, Schema } from "effect";
6
6
 
7
7
  //#region src/ActionState.ts
8
8
  /**
9
- * Raised when state cannot cross the phase boundary.
9
+ * Raised when action state cannot be saved, read or decoded across the phase
10
+ * boundary.
10
11
  *
11
12
  * @public
12
13
  */
@@ -26,7 +27,9 @@ var ActionStateError = class extends Schema.TaggedError()("ActionStateError", {
26
27
  "notPlainJson",
27
28
  "writeFailed"
28
29
  ]),
30
+ /** The state key that was being saved or read. */
29
31
  key: Schema.String,
32
+ /** The underlying failure, preserved structurally. */
30
33
  cause: Schema.optionalKey(Schema.Defect())
31
34
  }) {
32
35
  get message() {
@@ -110,11 +113,37 @@ const dies = unstubbed("ActionState.makeTest");
110
113
  * write-only file (`GITHUB_STATE`) whose entries the runner republishes to the
111
114
  * next phase as `STATE_<key>` environment variables — so saving and reading go
112
115
  * through different mechanisms, which is why this is a service rather than a
113
- * pair of helpers.
116
+ * pair of helpers. Every member fails with {@link ActionStateError}.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * import { ActionState } from "@effected/github-actions";
121
+ * import { Effect, Schema } from "effect";
122
+ *
123
+ * // in `pre`
124
+ * const pre = Effect.gen(function* () {
125
+ * const state = yield* ActionState;
126
+ * yield* state.save("server-pid", 4242, Schema.Number);
127
+ * });
128
+ *
129
+ * // in `post`
130
+ * const post = Effect.gen(function* () {
131
+ * const state = yield* ActionState;
132
+ * const pid = yield* state.get("server-pid", Schema.Number);
133
+ * return pid;
134
+ * });
135
+ * ```
114
136
  *
115
137
  * @public
116
138
  */
117
139
  var ActionState = class ActionState extends Context.Service()("@effected/github-actions/ActionState") {
140
+ /**
141
+ * The live service, writing to the runner's `GITHUB_STATE` file and reading
142
+ * the `STATE_<key>` variables it republishes.
143
+ *
144
+ * @remarks
145
+ * `ActionRuntime.layer` already provides every requirement.
146
+ */
118
147
  static layer = Layer.effect(this, make);
119
148
  /** A test double. Unstubbed members die rather than answering wrongly. */
120
149
  static makeTest = (overrides = {}) => ({
@@ -4,7 +4,8 @@ import { IdentityToken, IdentityTokenError } from "@effected/sbom";
4
4
 
5
5
  //#region src/ActionsIdentityToken.ts
6
6
  /**
7
- * The Actions side of `@effected/sbom`'s inverted OIDC contract.
7
+ * Serves `@effected/sbom`'s `IdentityToken` contract from the runner's own OIDC
8
+ * token service, so signing an SBOM inside a workflow needs no further wiring.
8
9
  *
9
10
  * @remarks
10
11
  * `@effected/sbom` declares `IdentityToken` — one method, one audience,
@@ -15,7 +15,8 @@ import { SlsaProvenance } from "@effected/sbom";
15
15
  */
16
16
  const DEFAULT_SERVER_URL = "https://github.com";
17
17
  /**
18
- * The Actions side of `@effected/sbom`'s SLSA provenance constructor.
18
+ * Builds the current run's SLSA provenance from the runner's OIDC claims: the
19
+ * Actions side of `@effected/sbom`'s `SlsaProvenance.forGitHubWorkflow`.
19
20
  *
20
21
  * @remarks
21
22
  * `SlsaProvenance.forGitHubWorkflow` is total and does the real work, but it
package/Artifact.js CHANGED
@@ -352,14 +352,10 @@ const dies = unstubbed("Artifact.makeTest");
352
352
  * third thing that can be absent, and it is reported as `misconfigured` for the
353
353
  * same reason.
354
354
  *
355
- * **This surface is provisional.** Unlike every other service here it was
356
- * ported without a call site to shape it against, so it keeps the source's
357
- * shape rather than a narrower one invented from nothing. The first consumer to
358
- * adopt it is the one whose feedback reshapes it — the members lost their
359
- * `Artifact` prefix (`artifact.uploadArtifact(...)` stutters) and the
360
- * cross-run `findBy` lookup is **not** ported, because every path through it in
361
- * the source is a typed "not implemented" failure. Adding it later is additive;
362
- * shipping a parameter that has never had behavior is not.
355
+ * **This surface is provisional.** It has not yet been shaped by a real call
356
+ * site, so it may be reworked once a consumer adopts it. It covers the current
357
+ * run only: there is no cross-run lookup, and adding one later would be
358
+ * additive.
363
359
  *
364
360
  * @example
365
361
  * ```ts