@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 +13 -12
- package/ActionCache.js +1 -2
- package/ActionEnvironment.js +21 -6
- package/ActionInput.js +37 -55
- package/ActionLogger.js +27 -10
- package/ActionOutputs.js +27 -8
- package/ActionState.js +31 -2
- package/ActionsIdentityToken.js +2 -1
- package/ActionsProvenance.js +2 -1
- package/Artifact.js +4 -8
- package/BlobEnvelope.js +41 -15
- package/BlobStore.githubCache.js +11 -4
- package/BlobStore.js +8 -7
- package/CacheKey.js +6 -7
- package/ChildEnv.js +2 -5
- package/DetachedProcess.js +8 -11
- package/GitHubMarkdown.js +8 -10
- package/GitHubToken.js +12 -14
- package/ManagedDocument.js +3 -4
- package/OidcTokenIssuer.js +11 -7
- package/PackageManagerInstaller.js +10 -3
- package/README.md +4 -4
- package/Secret.js +10 -17
- package/ToolInstaller.js +12 -6
- package/index.d.ts +334 -293
- package/index.js +1 -1
- package/internal/actionsResults.js +0 -12
- package/internal/archiveCommands.js +0 -66
- package/internal/cacheService.js +0 -16
- package/internal/digest.js +0 -13
- package/internal/jwt.js +0 -17
- package/internal/pnpmExe.js +0 -15
- package/internal/runnerFile.js +0 -16
- package/internal/sigv4.js +1 -4
- package/internal/unstubbed.js +0 -12
- package/package.json +12 -11
- package/WorkflowCommand.js +0 -100
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
|
|
89
|
-
* hand-rolling:
|
|
88
|
+
* Three properties earn the format:
|
|
90
89
|
*
|
|
91
|
-
* - **The magic prefix makes
|
|
92
|
-
* decode as
|
|
93
|
-
* holding
|
|
94
|
-
* read.
|
|
95
|
-
* - **The version lives in the blob, not in the key.** A
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
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
|
|
101
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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({}));
|
package/BlobStore.githubCache.js
CHANGED
|
@@ -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,
|
|
117
|
-
*
|
|
118
|
-
*
|
|
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
|
-
/**
|
|
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:
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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,
|
|
54
|
-
* backends already have one
|
|
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
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
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
|
|
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`.
|
|
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
|
*/
|
package/DetachedProcess.js
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
227
|
-
*
|
|
228
|
-
*
|
|
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
|
|
240
|
-
* A probe whose failures are meaningfully distinct should
|
|
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.
|
|
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
|
|
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
|
|
113
|
-
*
|
|
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
|
|
126
|
-
*
|
|
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.
|
|
232
|
-
*
|
|
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
|
-
*
|
|
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
|
|
82
|
-
*
|
|
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
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
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
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* failure in this
|
|
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);
|
package/ManagedDocument.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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
|
*
|
package/OidcTokenIssuer.js
CHANGED
|
@@ -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.
|
|
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
|
|
189
|
-
*
|
|
190
|
-
*
|
|
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
|
|
357
|
-
*
|
|
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
|
|
11
|
-
>
|
|
12
|
-
> `1.0.0`
|
|
13
|
-
>
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
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
|
-
*
|
|
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`:
|
|
100
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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"),
|