@effected/github-actions 0.19.1 → 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/BlobEnvelope.js CHANGED
@@ -85,31 +85,49 @@ const HEADER_BYTES = MAGIC.length + 1 + 4;
85
85
  * [4B magic "EFBS"][1B version][4B metadata length, big-endian][metadata JSON][body]
86
86
  * ```
87
87
  *
88
- * Three properties earn the format, and each replaces something a consumer was
89
- * hand-rolling:
88
+ * Three properties earn the format:
90
89
  *
91
- * - **The magic prefix makes a legacy blob legible.** Raw, unframed bytes
92
- * decode as `notAnEnvelope` rather than as garbage metadata, so a store
93
- * holding pre-envelope entries produces a clean miss instead of a corrupt
94
- * read.
95
- * - **The version lives in the blob, not in the key.** A consumer previously
96
- * namespaced keys (`v2/...`) to represent a format change, because there was
97
- * no in-band version. Here a revision is detected on read and reported
98
- * typed, so **keys stay stable** and old entries age out naturally.
90
+ * - **The magic prefix makes an unframed blob legible.** Raw, unframed bytes
91
+ * decode as {@link NotABlobEnvelopeError} rather than as garbage metadata, so
92
+ * a store holding entries written without an envelope produces a clean miss
93
+ * instead of a corrupt read.
94
+ * - **The version lives in the blob, not in the key.** A format revision is
95
+ * detected on read and reported typed
96
+ * ({@link UnsupportedBlobEnvelopeVersionError}), so **keys stay stable** and
97
+ * old entries age out naturally rather than needing a `v2/...` key prefix.
99
98
  * - **The metadata is the caller's own schema.** This module owns *framing*;
100
- * the caller owns *meaning*. Fields like a cache tag or a duration stop
101
- * being bytes at fixed offsets in a consumer's private codec.
99
+ * the caller owns *meaning*. Fields like a cache tag or a duration are
100
+ * schema fields, not bytes at fixed offsets in a private codec.
102
101
  *
103
102
  * **Pure**: `Result`-returning, no IO, no service — the framing is testable
104
103
  * from a byte array, which is the point.
105
104
  *
105
+ * @example
106
+ * ```ts
107
+ * import { BlobEnvelope } from "@effected/github-actions";
108
+ * import { Result, Schema } from "effect";
109
+ *
110
+ * const Meta = Schema.Struct({ tag: Schema.String });
111
+ *
112
+ * const framed = BlobEnvelope.encodeResult({ tag: "v1" }, new Uint8Array([1, 2, 3]), Meta);
113
+ * if (Result.isSuccess(framed)) {
114
+ * const read = BlobEnvelope.decodeResult(framed.success, Meta);
115
+ * // => Result.succeed({ metadata: { tag: "v1" }, body: Uint8Array [1, 2, 3] })
116
+ * }
117
+ * ```
118
+ *
106
119
  * @public
107
120
  */
108
121
  var BlobEnvelope = class {
109
122
  constructor() {}
110
- /** The version this build writes. */
123
+ /** The envelope version this build writes and accepts. */
111
124
  static version = VERSION;
112
- /** Frame metadata and a body into a single blob. */
125
+ /**
126
+ * Frame metadata and a body into a single blob.
127
+ *
128
+ * @returns a `Result` holding the framed bytes, or a
129
+ * {@link BlobMetadataEncodeError} when `metadata` does not satisfy `schema`
130
+ */
113
131
  static encodeResult(metadata, body, schema) {
114
132
  const encoded = Schema.encodeUnknownResult(Schema.fromJsonString(schema))(metadata);
115
133
  if (Result.isFailure(encoded)) return Result.fail(new BlobMetadataEncodeError({ cause: encoded.failure }));
@@ -122,7 +140,15 @@ var BlobEnvelope = class {
122
140
  out.set(body, HEADER_BYTES + metaBytes.length);
123
141
  return Result.succeed(out);
124
142
  }
125
- /** Read a framed blob back into its metadata and body. */
143
+ /**
144
+ * Read a framed blob back into its metadata and body.
145
+ *
146
+ * @returns a `Result` holding the decoded metadata and a copy of the body, or
147
+ * a {@link BlobEnvelopeError}: {@link NotABlobEnvelopeError} for unframed
148
+ * bytes, {@link TruncatedBlobEnvelopeError},
149
+ * {@link UnsupportedBlobEnvelopeVersionError}, or
150
+ * {@link BlobMetadataDecodeError} when the metadata fails `schema`
151
+ */
126
152
  static decodeResult(bytes, schema) {
127
153
  if (bytes.length < MAGIC.length || !MAGIC.every((byte, index) => bytes[index] === byte)) return Result.fail(new NotABlobEnvelopeError({}));
128
154
  if (bytes.length < HEADER_BYTES) return Result.fail(new TruncatedBlobEnvelopeError({}));
@@ -113,9 +113,9 @@ const make = (transfer) => Effect.gen(function* () {
113
113
  * The Actions cache is reachable over a Twirp v2 protocol at
114
114
  * `ACTIONS_RESULTS_URL`, which answers a `CreateCacheEntry` /
115
115
  * `GetCacheEntryDownloadURL` RPC with a **pre-signed Azure blob url** — which is
116
- * why this backend lives in its own module rather than beside the S3 one, and
117
- * why the spec's "Azure is confined to the cache and the artifact service" is
118
- * three modules rather than two.
116
+ * why this backend lives in its own module rather than beside the S3 one: it,
117
+ * `ActionCache` and `Artifact` are the only three modules that import the
118
+ * Azure client.
119
119
  *
120
120
  * **Only reachable from a `uses:` step.** The runner injects
121
121
  * `ACTIONS_RESULTS_URL` and `ACTIONS_RUNTIME_TOKEN` into action execution
@@ -140,7 +140,14 @@ const make = (transfer) => Effect.gen(function* () {
140
140
  */
141
141
  var GitHubCacheBlobStore = class {
142
142
  constructor() {}
143
- /** The backend, over the real Azure client. */
143
+ /**
144
+ * The backend, over the real Azure client.
145
+ *
146
+ * @remarks
147
+ * Requires `ActionEnvironment` for `ACTIONS_RESULTS_URL` and
148
+ * `ACTIONS_RUNTIME_TOKEN`; fails with `BlobStoreError` (`misconfigured`) when
149
+ * either is absent.
150
+ */
144
151
  static layer = Layer.effect(BlobStore, make(azure));
145
152
  /**
146
153
  * The backend, over a supplied transport.
package/BlobStore.js CHANGED
@@ -44,14 +44,15 @@ var BlobStoreError = class extends Schema.TaggedError()("BlobStoreError", {
44
44
  *
45
45
  * @remarks
46
46
  * `get`/`put`/`has` over `Uint8Array` **plus the caller's own metadata**, framed
47
- * by {@link BlobEnvelope}. The metadata channel is the reason this exists: the
48
- * consumer it was designed against hand-rolled a binary frame
49
- * (`[4B tagLen][4B durationMs][tag][body]`) and namespaced its keys with a `v2`
50
- * prefix to represent a format change. Both are framing concerns that had
51
- * leaked into a consumer, and both are absorbed here.
47
+ * by {@link BlobEnvelope}. The metadata channel is the reason this exists:
48
+ * fields like a cache tag or a duration travel as schema-typed metadata, and a
49
+ * format revision is detected in the blob rather than by prefixing keys.
52
50
  *
53
- * Deliberately no `list` and no `delete`: eviction belongs to the backend, both
54
- * backends already have one, and no consumer asked for either.
51
+ * Deliberately no `list` and no `delete`: eviction belongs to the backend, and
52
+ * both backends already have one.
53
+ *
54
+ * Backends: {@link BlobStore.layerS3}, `GitHubCacheBlobStore.layer` and the
55
+ * in-memory {@link BlobStore.layerMemory}.
55
56
  *
56
57
  * @example
57
58
  * ```ts
package/CacheKey.js CHANGED
@@ -173,13 +173,12 @@ var CacheKey = class CacheKey extends Schema.Class("CacheKey")(Schema.Struct({
173
173
  * A copy of this key that restores from its **exact key only** — no ladder.
174
174
  *
175
175
  * @remarks
176
- * The third point in the policy space, and the one the typed key had no
177
- * spelling for: absence of a policy means the default every-prefix ladder,
178
- * so exact-match-only restores (a cache-bust mode, where a stale partial
179
- * hit is worse than a cold start) previously forced the caller back to the
180
- * string form. This is the same `restoreDepths` field carrying **zero
181
- * rungs** — an honest value, not a sentinel: `ActionCache.restore` sends no
182
- * `restore_keys` at all, so the backend answers this key or a miss.
176
+ * The third point in the policy space: absence of a policy means the default
177
+ * every-prefix ladder, so exact-match-only restores (a cache-bust mode, where
178
+ * a stale partial hit is worse than a cold start) need their own spelling.
179
+ * This is the same `restoreDepths` field carrying **zero rungs** — an honest
180
+ * value, not a sentinel: `ActionCache.restore` sends no `restore_keys` at
181
+ * all, so the backend answers this key or a miss.
183
182
  *
184
183
  * @example
185
184
  * ```ts
package/ChildEnv.js CHANGED
@@ -1,8 +1,7 @@
1
1
  //#region src/ChildEnv.ts
2
2
  /**
3
3
  * Child-process environment construction: prepending directories to `PATH`
4
- * without falling into the three traps that each cost a real consumer a
5
- * cross-OS matrix round.
4
+ * without falling into three cross-platform traps.
6
5
  *
7
6
  * @remarks
8
7
  * Spawning a child with directories prepended to `PATH` is one tiny concern
@@ -35,9 +34,7 @@
35
34
  * taken as an argument — which is what makes every platform branch
36
35
  * exercisable from a test on any host. The sanctioned exceptions are
37
36
  * *defaults a caller can override by passing the value*: `ActionInput.provider`'s
38
- * `env = process.env` and `DetachedProcess.spawn`'s `base`. A structural test
39
- * (`__test__/ambientReads.test.ts`) holds the list closed; a new default is
40
- * added there with its reason, never silently.
37
+ * `env = process.env` and `DetachedProcess.spawn`'s `base`.
41
38
  *
42
39
  * @public
43
40
  */
@@ -8,7 +8,7 @@ import { closeSync, openSync } from "node:fs";
8
8
 
9
9
  //#region src/DetachedProcess.ts
10
10
  /**
11
- * Raised when a detached child cannot be started, waited for, or reaped.
11
+ * Raised when the log file for a detached child's output cannot be opened.
12
12
  *
13
13
  * @public
14
14
  */
@@ -202,8 +202,7 @@ var DetachedProcess = class DetachedProcess {
202
202
  * @remarks
203
203
  * A domain predicate rather than a sleep: "the server answers" is the thing
204
204
  * being waited for, and any fixed delay is simultaneously too long on a fast
205
- * runner and too short on a slow one. Two consumers hand-rolled this, one of
206
- * them around an `Effect.suspend` footgun it had to explain in a comment.
205
+ * runner and too short on a slow one.
207
206
  *
208
207
  * A probe *failure* is propagated rather than treated as "not ready" — a
209
208
  * probe that cannot run is a different situation from a child that is not up
@@ -223,11 +222,9 @@ var DetachedProcess = class DetachedProcess {
223
222
  * @remarks
224
223
  * The probe {@link DetachedProcess.awaitReady} almost always wants: an HTTP
225
224
  * `GET` at a port the child is still binding, where connection-refused *is*
226
- * "not up yet". The consumers that hand-rolled the polling wrote exactly
227
- * this probe around it, and each had to discover the same subtlety — the
228
- * probe's error channel must stay empty, because `awaitReady` deliberately
229
- * propagates probe failures, and a refused connection is not a failure of
230
- * the probe.
225
+ * "not up yet". The probe's error channel must stay empty, because
226
+ * `awaitReady` deliberately propagates probe failures, and a refused
227
+ * connection is not a failure of the probe.
231
228
  *
232
229
  * So this probe never fails: a refused connection, any other transport
233
230
  * error and a non-2xx answer all collapse to `false`, leaving
@@ -236,9 +233,9 @@ var DetachedProcess = class DetachedProcess {
236
233
  * only sound because every failure an HTTP readiness check can see means
237
234
  * the same thing here. The cost is the documented one: a *misconfigured*
238
235
  * probe — a typo'd port, a wrong path — is indistinguishable from a child
239
- * that never came up, and reports as `notReady` only after the full budget.
240
- * A probe whose failures are meaningfully distinct should stay a
241
- * hand-written effect with those failures in `E`.
236
+ * that never came up, and reports as {@link DetachedNotReadyError} only after
237
+ * the full budget. A probe whose failures are meaningfully distinct should
238
+ * stay a hand-written effect with those failures in `E`.
242
239
  *
243
240
  * The request goes through core's `HttpClient` — `Action.run` provides it
244
241
  * ambiently — rather than a bare `fetch`, per the package rule that
package/GitHubMarkdown.js CHANGED
@@ -15,8 +15,7 @@ const passthrough = (value) => Html.make({ value });
15
15
  * `Markdown.stringifyResult` is total over every tree this module can
16
16
  * construct — its only failure is the hardening nesting-depth guard (cap
17
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.
18
+ * unescaped string joining.
20
19
  *
21
20
  * What makes the guard unreachable is not that these trees are small; it is
22
21
  * that **their depth does not depend on input**. A fragment handed to a
@@ -51,8 +50,7 @@ const block = (node) => render(Root.make({ children: [node] }));
51
50
  * a backtick gets a wider delimiter; a URL containing spaces or parentheses
52
51
  * is angle-bracketed.
53
52
  *
54
- * This module is the package's **only** importer of `@effected/markdown` — a
55
- * structural test pins that, on the same terms as the Azure confinement — so
53
+ * This module is the package's **only** importer of `@effected/markdown`, so
56
54
  * an action that never renders markdown never links the engine.
57
55
  *
58
56
  * **Renders cannot fail, so do not `Effect.try`-wrap them.** The serializer's
@@ -109,8 +107,8 @@ var GitHubMarkdown = class GitHubMarkdown {
109
107
  * each cell is the field value's **encoded** string form — a branded or
110
108
  * typed field projects through its own codec, so the vocabulary lives
111
109
  * with the schema instead of being respelled at every call site, and a
112
- * row can no longer transpose columns because it is a typed object, not
113
- * a positional array.
110
+ * row cannot transpose columns because it is a typed object, not a
111
+ * positional array.
114
112
  *
115
113
  * A field whose encoded side is not a string has no string projection to
116
114
  * borrow; the type of `options` makes its column's `format` — and
@@ -122,8 +120,8 @@ var GitHubMarkdown = class GitHubMarkdown {
122
120
  * {@link GitHubMarkdown.table}, so escaping is inherited and identical
123
121
  * rows produce identical output. Rows are already-decoded values of the
124
122
  * schema's type, so encoding them is total in practice; a value smuggled
125
- * past the types throws the codec's `SchemaError` as a defect — same
126
- * posture as the serializer arm above, **not** a silent fallback.
123
+ * past the types throws the codec's `SchemaError` as a defect, **not** a
124
+ * silent fallback.
127
125
  *
128
126
  * This is the **only** member where a throw is reachable at runtime, and
129
127
  * neither route is the serializer — see the no-wrapping remark on
@@ -228,8 +226,8 @@ var GitHubMarkdown = class GitHubMarkdown {
228
226
  * its fragments as markdown, so `raw` adds no behavior — it adds the
229
227
  * *statement* that a value is already rendered. Use it where a fragment
230
228
  * from elsewhere enters a composition and a reader would otherwise wonder
231
- * whether it still needs escaping. It replaces the `Html.make` idiom,
232
- * which passes markdown through correctly but announces the wrong intent.
229
+ * whether it still needs escaping. Prefer it to `Html.make`, which passes
230
+ * markdown through correctly but announces the wrong intent.
233
231
  */
234
232
  static raw(markdown) {
235
233
  return markdown;
package/GitHubToken.js CHANGED
@@ -64,9 +64,8 @@ const identified = (app, options, minted) => Effect.gen(function* () {
64
64
  * The GitHub App token lifecycle, shaped like a workflow.
65
65
  *
66
66
  * @remarks
67
- * One of the two seams between this package and
68
- * [`@effected/github`](https://npmjs.com) — mint in `pre`, use in `main`,
69
- * revoke in `post`. The three phases are **three separate processes**, so
67
+ * One of the two seams between this package and `@effected/github` — mint in
68
+ * `pre`, use in `main`, revoke in `post`. The three phases are **three separate processes**, so
70
69
  * nothing survives between them except what `GITHUB_STATE` carries, which is
71
70
  * why this is a persistence problem rather than a `Scope`.
72
71
  *
@@ -78,16 +77,14 @@ const identified = (app, options, minted) => Effect.gen(function* () {
78
77
  * **The token lives about an hour, and no later phase can re-mint one.** The
79
78
  * credentials that could are the app's private key, and persisting *that*
80
79
  * through `GITHUB_STATE` — a plaintext file by GitHub's protocol — would trade
81
- * a one-hour token for a permanent one. So the v1 contract is stated rather
82
- * than worked around: {@link GitHubToken.read} fails typed when the persisted
80
+ * a one-hour token for a permanent one. So the contract is stated rather than
81
+ * worked around: {@link GitHubToken.read} fails typed when the persisted
83
82
  * token is spent, and a phase that can outlive the hour calls
84
83
  * {@link GitHubToken.provision} itself.
85
84
  *
86
- * Grouped statics on one module reaching one dependency — the carve-out to the
87
- * no-namespace-objects rule, not the hazard it names. Every member's `R` says
88
- * exactly which services it needs, and the members below say which parts of
89
- * each they touch, so a partial double is built from the documentation rather
90
- * than from a stack trace:
85
+ * Every member's `R` says exactly which services it needs, and this table says
86
+ * which parts of each it touches, so a partial double is built from the
87
+ * documentation rather than from a stack trace:
91
88
  *
92
89
  * | Member | `ActionState` | `ActionOutputs` | `GitHubApp` |
93
90
  * | --- | --- | --- | --- |
@@ -152,10 +149,11 @@ var GitHubToken = class GitHubToken {
152
149
  * The token an earlier phase persisted.
153
150
  *
154
151
  * @remarks
155
- * Fails typed when it is spent. The package this replaces persisted the
156
- * expiry and read it nowhere, so a `main` phase that outlived the hour simply
157
- * started answering `401` with no explanation — which is the single hardest
158
- * failure in this whole lifecycle to diagnose from a workflow log.
152
+ * Fails with {@link GitHubTokenError} when it is spent, and with
153
+ * {@link ActionStateError} when no token was persisted. Without that check a
154
+ * `main` phase that outlived the hour would simply start answering `401` with
155
+ * no explanation — the hardest failure in this lifecycle to diagnose from a
156
+ * workflow log.
159
157
  */
160
158
  static read = Effect.fn("GitHubToken.read")(function* (options = {}) {
161
159
  const token = yield* (yield* ActionState).get(options.stateKey ?? DEFAULT_KEY, InstallationToken);
@@ -103,9 +103,8 @@ const structural = (failure) => new ManagedDocumentError({
103
103
  * region carrying the latest content, which is what makes a check that
104
104
  * resolves many times safe to re-render.
105
105
  *
106
- * The region engine is `@effected/templates`' pure `SectionDocument` —
107
- * reused, not reinvented — fixed to the HTML comment style and a
108
- * region-specific phrase. Everything here is string → string; the GitHub
106
+ * The region engine is `@effected/templates`' pure `SectionDocument`, fixed to
107
+ * the HTML comment style and a region-specific phrase. Everything here is string → string; the GitHub
109
108
  * write path composes this with `PullRequestComment` (whose `CommentMarker`
110
109
  * renders the *same* sentinel for the same namespace and key) or a PR-body
111
110
  * update in `@effected/github`.
@@ -222,7 +221,7 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
222
221
  * call keeps its content *and* its metadata untouched — and never
223
222
  * participates in region addressability: a region is found by key alone,
224
223
  * so changing metadata updates the region in place. A two-element entry
225
- * behaves exactly as it always has. Names must match
224
+ * supplies no metadata. Names must match
226
225
  * `[A-Za-z][A-Za-z0-9_-]*` and values may not contain `"` or a line
227
226
  * break; violations fail typed as `invalidAttribute`.
228
227
  *
@@ -126,9 +126,7 @@ const make = Effect.gen(function* () {
126
126
  * `Redacted.value` does not appear in this module at all — the package's
127
127
  * declassification invariant is that `Secret.ts` is the only place a secret
128
128
  * becomes a string, and a wrap-then-immediately-unwrap here would be a
129
- * genuine exception to it rather than a cosmetic one. A structural test over
130
- * `src/` enforces this, and it caught exactly this call when the first draft
131
- * did it the obvious way.
129
+ * genuine exception to it rather than a cosmetic one.
132
130
  */
133
131
  const issue = Effect.fn("OidcTokenIssuer.token")(function* (audience) {
134
132
  const bearer = yield* required(REQUEST_TOKEN);
@@ -179,16 +177,22 @@ const dies = unstubbed("OidcTokenIssuer.makeTest");
179
177
  * @public
180
178
  */
181
179
  var OidcTokenIssuer = class OidcTokenIssuer extends Context.Service()("@effected/github-actions/OidcTokenIssuer") {
180
+ /**
181
+ * The live issuer, requesting tokens from the runner's token service.
182
+ *
183
+ * @remarks
184
+ * Fails with {@link OidcTokenError} (`unavailable`) at use when the workflow
185
+ * lacks `permissions: id-token: write`.
186
+ */
182
187
  static layer = Layer.effect(this, make);
183
188
  /**
184
189
  * An **unsigned** JWT carrying these claims.
185
190
  *
186
191
  * @remarks
187
192
  * For building test doubles, and nothing else: the signature segment is a
188
- * placeholder, so this token would fail any verifier. It exists because the
189
- * alternative — a double returning a synthetic non-JWT — is what made the
190
- * provenance path structurally unreachable in the source package, drawing
191
- * four separate apologetic comments from one consumer.
193
+ * placeholder, so this token would fail any verifier. It exists because a
194
+ * double returning a synthetic non-JWT would make the provenance path
195
+ * structurally unreachable in a test.
192
196
  */
193
197
  static unsignedTokenFor = (claims) => Redacted.make(unsignedJwt({
194
198
  alg: "RS256",
@@ -353,9 +353,8 @@ const make = Effect.gen(function* () {
353
353
  /**
354
354
  * Whether a file is still pnpm's shebang-less placeholder (a `#`-led text
355
355
  * file that is not a `#!` script) rather than the native binary meant to
356
- * overlay it — the mark of an entry written by a version of this module
357
- * that predates the overlay, or by a foreign writer that ran no lifecycle
358
- * scripts. Anything else (a shebang, an executable's magic, an unreadable
356
+ * overlay it — the mark of an entry written without the overlay, by an
357
+ * older writer or a foreign one that ran no lifecycle scripts. Anything else (a shebang, an executable's magic, an unreadable
359
358
  * or empty file) is left to the ordinary layout checks.
360
359
  */
361
360
  const isPlaceholder = (file) => Effect.scoped(Effect.flatMap(fs.open(file), (handle) => handle.readAlloc(2))).pipe(Effect.map((head) => Option.match(head, {
@@ -715,6 +714,14 @@ const dies = unstubbed("PackageManagerInstaller.makeTest");
715
714
  * @public
716
715
  */
717
716
  var PackageManagerInstaller = class PackageManagerInstaller extends Context.Service()("@effected/github-actions/PackageManagerInstaller") {
717
+ /**
718
+ * The live installer, caching through {@link ToolInstaller}.
719
+ *
720
+ * @remarks
721
+ * Fails with {@link PackageManagerInstallerError}; requires the runner's
722
+ * environment, a filesystem, `Path`, a child-process spawner and a
723
+ * `ToolInstaller`.
724
+ */
718
725
  static layer = Layer.effect(this, make);
719
726
  /** A test double. The unstubbed member dies rather than reporting an install that did not happen. */
720
727
  static makeTest = (overrides = {}) => ({
package/README.md CHANGED
@@ -7,10 +7,10 @@
7
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
- > **Pre-release.** This package is part of the `@effected/*` kit, in pre-`1.0.0`
11
- > development against a single pinned Effect v4 prerelease. Packages graduate to
12
- > `1.0.0` once Effect `4.0.0` ships. To hold your own `effect` versions at
13
- > exactly the ones the kit is built and tested against, install
10
+ > **Pre-`1.0.0`.** This package is part of the `@effected/*` kit, built on stable
11
+ > Effect v4 (`effect` `^4.0.0`) and still in `0.x` development. Stable Effect
12
+ > makes a kit `1.0.0` possible, not automatic. To keep your `effect` and
13
+ > `@effect/*` versions on the line the kit is built and tested against, install
14
14
  > [`@effected/pnpm-plugin-effect`](https://www.npmjs.com/package/@effected/pnpm-plugin-effect).
15
15
  >
16
16
  > **Stability: unstable.** This package's API surface is not yet considered
package/Secret.js CHANGED
@@ -61,8 +61,7 @@ var Secret = class Secret {
61
61
  * **Detached workers invert the masking model.** Masking works because the
62
62
  * runner parses this process's stdout; a *detached* child's stdout is a log
63
63
  * file no runner parses, so a mask emitted **inside** the worker is inert
64
- * and writes the plaintext verbatim into that log — a shipped incident, not
65
- * a hypothetical. The rule: **the parent masks, before the spawn** — call
64
+ * and writes the plaintext verbatim into that log. The rule: **the parent masks, before the spawn** — call
66
65
  * this member in the parent, where the runner is listening, and hand the
67
66
  * already-masked plaintext to the worker's environment. Inside the worker,
68
67
  * compose `ActionOutputs.layerDetached`, whose `setSecret` is a documented
@@ -121,9 +120,8 @@ var Secret = class Secret {
121
120
  * **In a detached worker, the mask this member emits is a leak.** The
122
121
  * `::add-mask::` command only masks when the runner parses this process's
123
122
  * stdout; a detached worker's stdout is a log file no runner parses, so
124
- * there the command masks nothing *and* spells the plaintext into the log
125
- * — a signing key shipped exactly that way for one round. A worker that
126
- * signs (an S3-style `BlobStore` is the canonical case) must compose
123
+ * there the command masks nothing *and* spells the plaintext into the log.
124
+ * A worker that signs (an S3-style `BlobStore` is the canonical case) must compose
127
125
  * `ActionOutputs.layerDetached`, under which this member still returns the
128
126
  * raw key for the HMAC but the mask is a documented no-op; the masking
129
127
  * itself is the **parent's** job, done before the spawn via
@@ -143,8 +141,7 @@ var Secret = class Secret {
143
141
  * secret enters the ambient environment without wading through signing
144
142
  * keys and runner files.
145
143
  *
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
144
+ * **This package never mutates `process.env`.** Reads are seeded once, at layer
148
145
  * construction, through `ActionEnvironment`, and nothing here writes back
149
146
  * — a write from inside the kit would be invisible to that seeding and to
150
147
  * every consumer's assumptions about when the environment is stable. This
@@ -166,13 +163,10 @@ var Secret = class Secret {
166
163
  * Masking without declassification: the value is registered with
167
164
  * `::add-mask::` through the same route as every other member, and the
168
165
  * 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.
166
+ * value at all. Prefer it to calling {@link Secret.forSigning} and discarding
167
+ * the result, which would mislabel a masking site as a signing one. The names
168
+ * are the audit vocabulary: a grep for this member finds every register-only
169
+ * site, and a grep for `forSigning` finds only signing.
176
170
  *
177
171
  * The canonical caller masks every supplied credential input
178
172
  * unconditionally, before the logic that decides which of them will
@@ -187,9 +181,8 @@ var Secret = class Secret {
187
181
  * **In a detached worker, the mask this member emits is a leak.** The
188
182
  * `::add-mask::` command only masks when the runner parses this process's
189
183
  * 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
184
+ * there the command masks nothing *and* spells the plaintext into the log.
185
+ * A worker must compose `ActionOutputs.layerDetached`, under which the mask is a
193
186
  * documented no-op; the masking itself is the **parent's** job, done
194
187
  * before the spawn via {@link Secret.forChildEnv} under the real layer.
195
188
  */
package/ToolInstaller.js CHANGED
@@ -96,8 +96,8 @@ const make = Effect.gen(function* () {
96
96
  * installed". Discarding it is what makes an extraction failure unactionable.
97
97
  *
98
98
  * **One spawn, not two — load-bearing**, and why is spelled once in
99
- * `internal/spawn.ts`: the double run failed 5/5 on real Windows runners
100
- * because .NET's `ZipFile.ExtractToDirectory` refuses to overwrite.
99
+ * `internal/spawn.ts`: a second run fails on Windows because .NET's
100
+ * `ZipFile.ExtractToDirectory` refuses to overwrite.
101
101
  */
102
102
  const extractWith = (command, archive) => Effect.gen(function* () {
103
103
  const { output, code } = yield* spawnOnce(spawner, command).pipe(Effect.mapError((cause) => new ToolInstallerError({
@@ -287,7 +287,7 @@ const dies = unstubbed("ToolInstaller.makeTest");
287
287
  * environment because a double has no `ActionEnvironment` to ask. The literal
288
288
  * `/tmp/runner-tool-cache` is what `make`'s `path.join("/tmp", "runner-tool-cache")`
289
289
  * spells on POSIX, which is the only place a test double runs. Test-double
290
- * only; allowlisted as such in `__test__/ambientReads.test.ts`.
290
+ * only.
291
291
  */
292
292
  const testRoot = () => process.env.RUNNER_TOOL_CACHE ?? "/tmp/runner-tool-cache";
293
293
  /**
@@ -323,6 +323,13 @@ const testRoot = () => process.env.RUNNER_TOOL_CACHE ?? "/tmp/runner-tool-cache"
323
323
  * @public
324
324
  */
325
325
  var ToolInstaller = class ToolInstaller extends Context.Service()("@effected/github-actions/ToolInstaller") {
326
+ /**
327
+ * The live installer, over the runner's tool cache, `HttpClient` and `tar`.
328
+ *
329
+ * @remarks
330
+ * The cache root comes from `RUNNER_TOOL_CACHE`, resolved once at layer
331
+ * construction.
332
+ */
326
333
  static layer = Layer.effect(this, make);
327
334
  /**
328
335
  * Where a tool lives in the cache.
@@ -346,9 +353,8 @@ var ToolInstaller = class ToolInstaller extends Context.Service()("@effected/git
346
353
  * otherwise have to stub it in every test. The default is the static layout
347
354
  * over `RUNNER_TOOL_CACHE`, or the same off-runner root `make` resolves
348
355
  * (`internal/runner.ts`). **That is a read of the ambient environment**,
349
- * sanctioned here only because a double has no `ActionEnvironment` to ask —
350
- * test-double-only, and listed as such in the package's ambient-read
351
- * allowlist (`__test__/ambientReads.test.ts`).
356
+ * sanctioned here only because a double has no `ActionEnvironment` to ask,
357
+ * and limited to the test double.
352
358
  */
353
359
  static makeTest = (overrides = {}) => ({
354
360
  find: () => dies("find"),