@effected/github-actions 0.13.0 → 0.13.2

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.
@@ -1,4 +1,5 @@
1
- import { Effect, Schema } from "effect";
1
+ import { unstubbed } from "./internal/unstubbed.js";
2
+ import { Effect, Schedule, Schema } from "effect";
2
3
  import { HttpClient } from "effect/unstable/http";
3
4
  import { ChildProcessSpawner } from "effect/unstable/process";
4
5
  import { spawn } from "node:child_process";
@@ -116,9 +117,7 @@ const ProcessId = Schema.Number.pipe(Schema.check(Schema.makeFilter((value) => N
116
117
  /** Whether a thrown value is a Node errno error with the given code. */
117
118
  const isErrno = (cause, code) => typeof cause === "object" && cause !== null && cause.code === code;
118
119
  /** See {@link DetachedProcess.makeTestOps}: an unstubbed member dies naming itself. */
119
- const unimplemented = (member) => {
120
- throw new Error(`DetachedProcess.makeTestOps: ${member}() was called but not stubbed — pass a \`${member}\` override.`);
121
- };
120
+ const dies = unstubbed("DetachedProcess.makeTestOps");
122
121
  /**
123
122
  * A long-lived child that outlives the phase that started it.
124
123
  *
@@ -213,14 +212,11 @@ var DetachedProcess = class DetachedProcess {
213
212
  * timeout six seconds later with nothing to show for it.
214
213
  */
215
214
  static awaitReady = Effect.fn("DetachedProcess.awaitReady")(function* (probe, options) {
216
- const interval = options?.interval ?? "150 millis";
217
- const attempts = options?.attempts ?? 40;
218
- const poll = (remaining) => Effect.flatMap(probe, (ready) => {
219
- if (ready) return Effect.void;
220
- if (remaining <= 0) return Effect.fail(new DetachedNotReadyError({}));
221
- return Effect.flatMap(Effect.sleep(interval), () => poll(remaining - 1));
222
- });
223
- return yield* poll(attempts);
215
+ if (!(yield* Effect.repeat(probe, {
216
+ schedule: Schedule.spaced(options?.interval ?? "150 millis"),
217
+ times: options?.attempts ?? 40,
218
+ until: (answer) => answer
219
+ }))) return yield* Effect.fail(new DetachedNotReadyError({}));
224
220
  });
225
221
  /**
226
222
  * A readiness probe over HTTP: `true` when a `GET` answers 2xx.
@@ -310,9 +306,9 @@ var DetachedProcess = class DetachedProcess {
310
306
  * production form signals a real pid.
311
307
  */
312
308
  static makeTestOps = (overrides = {}) => ({
313
- spawn: () => Effect.sync(() => unimplemented("spawn")),
314
- awaitReady: () => Effect.sync(() => unimplemented("awaitReady")),
315
- reap: () => Effect.sync(() => unimplemented("reap")),
309
+ spawn: () => dies("spawn"),
310
+ awaitReady: () => dies("awaitReady"),
311
+ reap: () => dies("reap"),
316
312
  ...overrides
317
313
  });
318
314
  };
package/DryRun.js CHANGED
@@ -32,15 +32,6 @@ const make = (enabled) => ({
32
32
  * @public
33
33
  */
34
34
  var DryRun = class DryRun extends Context.Service()("@effected/github-actions/DryRun") {
35
- /**
36
- * Driven by the `dry-run` action input, defaulting to a real run.
37
- *
38
- * @remarks
39
- * Fails with a `ConfigError` when the input is present but is not a YAML 1.2
40
- * core-schema boolean — a workflow that writes `dry-run: yes` should stop,
41
- * not quietly perform the mutations it meant to rehearse.
42
- */
43
- static layer = Layer.effect(this, Effect.map(ActionInput.boolean(DEFAULT_INPUT).pipe(Config.withDefault(false)), make));
44
35
  /**
45
36
  * Driven by a named input.
46
37
  *
@@ -50,6 +41,15 @@ var DryRun = class DryRun extends Context.Service()("@effected/github-actions/Dr
50
41
  * composition site.
51
42
  */
52
43
  static layerFromInput = (name) => Layer.effect(DryRun, Effect.map(ActionInput.boolean(name).pipe(Config.withDefault(false)), make));
44
+ /**
45
+ * Driven by the `dry-run` action input, defaulting to a real run.
46
+ *
47
+ * @remarks
48
+ * Fails with a `ConfigError` when the input is present but is not a YAML 1.2
49
+ * core-schema boolean — a workflow that writes `dry-run: yes` should stop,
50
+ * not quietly perform the mutations it meant to rehearse.
51
+ */
52
+ static layer = DryRun.layerFromInput(DEFAULT_INPUT);
53
53
  /** Driven by an explicit decision the caller has already made. */
54
54
  static layerFrom = (enabled) => Layer.succeed(DryRun, make(enabled));
55
55
  /**
package/GitHubMarkdown.js CHANGED
@@ -159,12 +159,12 @@ var GitHubMarkdown = class GitHubMarkdown {
159
159
  const columns = Object.keys(schema.fields).map((key) => {
160
160
  const field = schema.fields[key];
161
161
  const column = overrides[key];
162
- const format = column?.format;
163
- const encode = format === void 0 ? Schema.encodeSync(field) : void 0;
162
+ const encode = Schema.encodeSync(field);
163
+ const project = column?.format ?? ((value) => encode(value) ?? "");
164
164
  return {
165
165
  key,
166
166
  header: column?.header ?? SchemaAST.resolveTitle(field.ast) ?? key,
167
- cell: (value) => value === void 0 ? "" : format !== void 0 ? format(value) : encode?.(value) ?? ""
167
+ cell: (value) => value === void 0 ? "" : project(value)
168
168
  };
169
169
  });
170
170
  const headers = columns.map((column) => column.header);
package/GitHubToken.js CHANGED
@@ -160,7 +160,7 @@ var GitHubToken = class GitHubToken {
160
160
  static read = Effect.fn("GitHubToken.read")(function* (options = {}) {
161
161
  const token = yield* (yield* ActionState).get(options.stateKey ?? DEFAULT_KEY, InstallationToken);
162
162
  const now = yield* DateTime.now;
163
- if (options.skew === void 0 ? token.isExpired(DateTime.toEpochMillis(now)) : token.isExpired(DateTime.toEpochMillis(now), options.skew)) return yield* Effect.fail(new GitHubTokenError({
163
+ if (token.isExpired(DateTime.toEpochMillis(now), options.skew)) return yield* Effect.fail(new GitHubTokenError({
164
164
  reason: "expired",
165
165
  expiresAt: DateTime.formatIso(token.expiresAt)
166
166
  }));
@@ -83,6 +83,12 @@ const STRUCTURAL_KIND = {
83
83
  overlappingSections: "overlappingRegions",
84
84
  duplicateSection: "duplicateRegion"
85
85
  };
86
+ /** A structurally corrupt region layout, in this surface's vocabulary. */
87
+ const structural = (failure) => new ManagedDocumentError({
88
+ kind: STRUCTURAL_KIND[failure.reason],
89
+ line: failure.line,
90
+ ...failure.key === void 0 ? {} : { key: failure.key }
91
+ });
86
92
  /**
87
93
  * A marker-delimited document: named regions a tool owns, inside text a human
88
94
  * may also edit.
@@ -145,21 +151,12 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
145
151
  * a silent choice would destroy content a human wrote.
146
152
  */
147
153
  static parseResult(source) {
148
- const text = source.text ?? "";
149
- const scanned = SectionDocument.parseResult(text, REGION_DIALECT);
150
- if (Result.isFailure(scanned)) {
151
- const failure = scanned.failure;
152
- return Result.fail(new ManagedDocumentError({
153
- kind: STRUCTURAL_KIND[failure.reason],
154
- line: failure.line,
155
- ...failure.key === void 0 ? {} : { key: failure.key }
156
- }));
157
- }
158
- return Result.succeed(ManagedDocument.make({
154
+ const document = ManagedDocument.make({
159
155
  namespace: source.namespace,
160
156
  key: source.key,
161
- text
162
- }));
157
+ text: source.text ?? ""
158
+ });
159
+ return Result.map(Result.mapError(document.scan(), structural), () => document);
163
160
  }
164
161
  /**
165
162
  * Read a document out of existing text, in `Effect`.
@@ -179,8 +176,7 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
179
176
  }
180
177
  /** The content of one region, if the document carries it. */
181
178
  region(key) {
182
- const found = this.ownRegions().find((entry) => entry.key === key);
183
- return found === void 0 ? Option.none() : Option.some(found.content);
179
+ return Option.map(this.entry(key), (found) => found.content);
184
180
  }
185
181
  /**
186
182
  * One region's content and metadata together, if the document carries it.
@@ -235,15 +231,8 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
235
231
  * surfacing in the typed channel.
236
232
  */
237
233
  withRegionsResult(entries) {
238
- const parsed = SectionDocument.parseResult(this.text, REGION_DIALECT);
239
- if (Result.isFailure(parsed)) {
240
- const failure = parsed.failure;
241
- return Result.fail(new ManagedDocumentError({
242
- kind: STRUCTURAL_KIND[failure.reason],
243
- line: failure.line,
244
- ...failure.key === void 0 ? {} : { key: failure.key }
245
- }));
246
- }
234
+ const parsed = this.scan();
235
+ if (Result.isFailure(parsed)) return Result.fail(structural(parsed.failure));
247
236
  const declared = entries.map(([key, content, meta]) => SectionId.make({
248
237
  key: this.wireKey(key),
249
238
  commentStyle: CommentStyle.html
@@ -275,14 +264,22 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
275
264
  withRegions(entries) {
276
265
  return Effect.fromResult(this.withRegionsResult(entries));
277
266
  }
267
+ #scanned;
268
+ scan() {
269
+ this.#scanned ??= SectionDocument.parseResult(this.text, REGION_DIALECT);
270
+ return this.#scanned;
271
+ }
272
+ /** What every wire key of this document's regions begins with. */
273
+ get wirePrefix() {
274
+ return `${this.namespace}.${this.key}.`;
275
+ }
278
276
  /** The marker key a region is written under: `namespace.key.region`. */
279
277
  wireKey(region) {
280
- return `${this.namespace}.${this.key}.${region}`;
278
+ return `${this.wirePrefix}${region}`;
281
279
  }
282
280
  /** The region key back out of a wire key, for error reporting. */
283
281
  regionKeyOf(wireKey) {
284
- const prefix = `${this.namespace}.${this.key}.`;
285
- return wireKey.startsWith(prefix) ? wireKey.slice(prefix.length) : wireKey;
282
+ return wireKey.startsWith(this.wirePrefix) ? wireKey.slice(this.wirePrefix.length) : wireKey;
286
283
  }
287
284
  /**
288
285
  * This document's own regions, in document order.
@@ -296,9 +293,9 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
296
293
  * never reported.
297
294
  */
298
295
  ownRegions() {
299
- const parsed = SectionDocument.parseResult(this.text, REGION_DIALECT);
296
+ const parsed = this.scan();
300
297
  if (Result.isFailure(parsed)) return [];
301
- const prefix = `${this.namespace}.${this.key}.`;
298
+ const prefix = this.wirePrefix;
302
299
  return parsed.success.sections.filter((placed) => placed.section.key.startsWith(prefix)).map((placed) => ({
303
300
  key: placed.section.key.slice(prefix.length),
304
301
  content: placed.section.content,
@@ -1,6 +1,8 @@
1
1
  import { ActionEnvironment } from "./ActionEnvironment.js";
2
+ import { unstubbed } from "./internal/unstubbed.js";
3
+ import { payloadOf, unsignedJwt } from "./internal/jwt.js";
2
4
  import { Context, Effect, Layer, Redacted, Schema } from "effect";
3
- import { HttpClient } from "effect/unstable/http";
5
+ import { HttpClient, HttpClientRequest } from "effect/unstable/http";
4
6
  import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
5
7
 
6
8
  //#region src/OidcTokenIssuer.ts
@@ -89,18 +91,6 @@ const TokenEnvelope = Schema.Struct({
89
91
  const REQUEST_TOKEN = "ACTIONS_ID_TOKEN_REQUEST_TOKEN";
90
92
  const REQUEST_URL = "ACTIONS_ID_TOKEN_REQUEST_URL";
91
93
  /**
92
- * Decode a JWT payload segment.
93
- *
94
- * @remarks
95
- * `base64url` states the intent, though it is not what makes this work: Node's
96
- * plain `base64` decoder happens to accept the url alphabet and unpadded input
97
- * too (probed at beta.101), so the two are interchangeable *here*. They are not
98
- * interchangeable everywhere — `atob` is not forgiving — which is why the
99
- * declaration is the strict one and there is a test whose payload actually
100
- * contains `-` and `_`.
101
- */
102
- const decodeSegment = (segment) => JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
103
- /**
104
94
  * Read the claims out of a JWT **without verifying its signature**.
105
95
  *
106
96
  * @remarks
@@ -108,20 +98,14 @@ const decodeSegment = (segment) => JSON.parse(Buffer.from(segment, "base64url").
108
98
  * {@link OidcTokenIssuerShape.claims}; do not "fix" it here.
109
99
  */
110
100
  const readClaims = (token) => Effect.gen(function* () {
111
- const parts = token.split(".");
112
- const payload = parts[1];
113
- if (parts.length !== 3 || payload === void 0 || payload === "") return yield* Effect.fail(new OidcTokenError({
101
+ const decoded = yield* Effect.fromResult(payloadOf(token)).pipe(Effect.mapError((failure) => failure.kind === "segments" ? new OidcTokenError({
114
102
  reason: "malformedToken",
115
- detail: `expected three segments, got ${parts.length}`
116
- }));
117
- const decoded = yield* Effect.try({
118
- try: () => decodeSegment(payload),
119
- catch: (cause) => new OidcTokenError({
120
- reason: "malformedToken",
121
- detail: "the payload is not base64url JSON",
122
- cause
123
- })
124
- });
103
+ detail: failure.detail
104
+ }) : new OidcTokenError({
105
+ reason: "malformedToken",
106
+ detail: failure.detail,
107
+ cause: failure.cause
108
+ })));
125
109
  return yield* Schema.decodeUnknownEffect(OidcClaims)(decoded).pipe(Effect.mapError((cause) => new OidcTokenError({
126
110
  reason: "missingClaims",
127
111
  cause
@@ -150,10 +134,7 @@ const make = Effect.gen(function* () {
150
134
  const bearer = yield* required(REQUEST_TOKEN);
151
135
  const base = yield* required(REQUEST_URL);
152
136
  const url = audience === void 0 ? base : `${base}&audience=${encodeURIComponent(audience)}`;
153
- const response = yield* http.get(url, { headers: {
154
- authorization: `Bearer ${bearer}`,
155
- accept: "application/json"
156
- } }).pipe(Effect.mapError((cause) => new OidcTokenError({
137
+ const response = yield* http.execute(HttpClientRequest.get(url).pipe(HttpClientRequest.bearerToken(bearer), HttpClientRequest.acceptJson)).pipe(Effect.mapError((cause) => new OidcTokenError({
157
138
  reason: "requestFailed",
158
139
  cause
159
140
  })));
@@ -173,9 +154,7 @@ const make = Effect.gen(function* () {
173
154
  })
174
155
  };
175
156
  });
176
- const unimplemented = (member) => {
177
- throw new Error(`OidcTokenIssuer.makeTest: ${member}() was called but not stubbed — pass a \`${member}\` override.`);
178
- };
157
+ const dies = unstubbed("OidcTokenIssuer.makeTest");
179
158
  /**
180
159
  * The runner's OIDC token service.
181
160
  *
@@ -211,17 +190,14 @@ var OidcTokenIssuer = class OidcTokenIssuer extends Context.Service()("@effected
211
190
  * provenance path structurally unreachable in the source package, drawing
212
191
  * four separate apologetic comments from one consumer.
213
192
  */
214
- static unsignedTokenFor = (claims) => {
215
- const segment = (value) => Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
216
- return Redacted.make(`${segment({
217
- alg: "RS256",
218
- typ: "JWT"
219
- })}.${segment(Schema.encodeUnknownSync(OidcClaims)(claims))}.unsigned`);
220
- };
193
+ static unsignedTokenFor = (claims) => Redacted.make(unsignedJwt({
194
+ alg: "RS256",
195
+ typ: "JWT"
196
+ }, Schema.encodeUnknownSync(OidcClaims)(claims)));
221
197
  /** A test double. Unstubbed members die rather than answering with a non-token. */
222
198
  static makeTest = (overrides = {}) => ({
223
- token: () => Effect.sync(() => unimplemented("token")),
224
- claims: () => Effect.sync(() => unimplemented("claims")),
199
+ token: () => dies("token"),
200
+ claims: () => dies("claims"),
225
201
  ...overrides
226
202
  });
227
203
  /** {@link OidcTokenIssuer.makeTest} behind `Layer.succeed`. */