@effected/github 0.15.1 → 0.16.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/ActionsOidc.js +180 -0
- package/CheckRun.js +87 -19
- package/GitHubApp.js +253 -35
- package/GitHubClient.js +52 -3
- package/GitHubContent.js +10 -2
- package/GitHubError.js +50 -20
- package/InstallationTokenStore.js +51 -0
- package/README.md +1 -1
- package/Repo.js +16 -1
- package/WorkflowDispatch.js +36 -20
- package/index.d.ts +628 -25
- package/index.js +4 -2
- package/internal/crypto.js +1 -1
- package/package.json +3 -3
package/ActionsOidc.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { JwksResolver, Jwt, JwtError } from "@effected/jwt";
|
|
2
|
+
import { Effect, Schema, SchemaTransformation } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/ActionsOidc.ts
|
|
5
|
+
/** The issuer of every GitHub Actions OIDC token on github.com. */
|
|
6
|
+
const ISSUER = "https://token.actions.githubusercontent.com";
|
|
7
|
+
/**
|
|
8
|
+
* A numeric id as GitHub's OIDC tokens carry it: a string of decimal digits,
|
|
9
|
+
* decoded to a safe integer. Anything else (an empty string, a sign, a
|
|
10
|
+
* fraction, an exponent, surrounding space) does not decode.
|
|
11
|
+
*/
|
|
12
|
+
const IdFromString = Schema.String.check(Schema.isPattern(/^\d+$/)).pipe(Schema.decodeTo(Schema.Int, SchemaTransformation.numberFromString));
|
|
13
|
+
/**
|
|
14
|
+
* The claims of a verified GitHub Actions OIDC token.
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* Field names are camelCase; the token carries them in snake_case
|
|
18
|
+
* (`repository_id`, `job_workflow_ref`, ...), and {@link ActionsOidc.verify}
|
|
19
|
+
* maps between the two. GitHub sends the numeric ids (`repository_id`,
|
|
20
|
+
* `repository_owner_id`, `run_id`, `run_attempt`, `actor_id`) as strings;
|
|
21
|
+
* they decode to numbers here, and a non-numeric one fails verification as
|
|
22
|
+
* `claims`.
|
|
23
|
+
*
|
|
24
|
+
* Claims GitHub adds that are not modelled here are dropped on decode.
|
|
25
|
+
*
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
28
|
+
var ActionsOidcClaims = class extends Schema.Class("ActionsOidcClaims")({
|
|
29
|
+
/** The issuer: always {@link ActionsOidc.issuer} once verified. */
|
|
30
|
+
iss: Schema.String,
|
|
31
|
+
/** The subject, e.g. `repo:octo-org/octo-repo:ref:refs/heads/main`. */
|
|
32
|
+
sub: Schema.String,
|
|
33
|
+
/** The audience or audiences the token was minted for. */
|
|
34
|
+
aud: Schema.Union([Schema.String, Schema.Array(Schema.String)]),
|
|
35
|
+
/** Expiry, in seconds since the epoch. */
|
|
36
|
+
exp: Schema.Number,
|
|
37
|
+
/** Issued at, in seconds since the epoch. */
|
|
38
|
+
iat: Schema.Number,
|
|
39
|
+
/** Not before, in seconds since the epoch. */
|
|
40
|
+
nbf: Schema.optionalKey(Schema.Number),
|
|
41
|
+
/** The token's unique id. */
|
|
42
|
+
jti: Schema.optionalKey(Schema.String),
|
|
43
|
+
/** The repository, `owner/name`. */
|
|
44
|
+
repository: Schema.String,
|
|
45
|
+
/** The repository's numeric id (wire key `repository_id`). */
|
|
46
|
+
repositoryId: IdFromString,
|
|
47
|
+
/** The repository owner's login (wire key `repository_owner`). */
|
|
48
|
+
repositoryOwner: Schema.String,
|
|
49
|
+
/** The repository owner's numeric id (wire key `repository_owner_id`). */
|
|
50
|
+
repositoryOwnerId: IdFromString,
|
|
51
|
+
/** The workflow run's id (wire key `run_id`). */
|
|
52
|
+
runId: IdFromString,
|
|
53
|
+
/** The run attempt, from 1 (wire key `run_attempt`). */
|
|
54
|
+
runAttempt: IdFromString,
|
|
55
|
+
/** The calling workflow's ref, e.g. `octo-org/octo-repo/.github/workflows/ci.yml@refs/heads/main` (wire key `workflow_ref`). */
|
|
56
|
+
workflowRef: Schema.String,
|
|
57
|
+
/** The ref of the workflow file the job runs, which differs from `workflowRef` for a reusable workflow (wire key `job_workflow_ref`). */
|
|
58
|
+
jobWorkflowRef: Schema.String,
|
|
59
|
+
/** The git ref the run is for. */
|
|
60
|
+
ref: Schema.String,
|
|
61
|
+
/** The commit SHA the run is for. */
|
|
62
|
+
sha: Schema.String,
|
|
63
|
+
/** The event that triggered the run, e.g. `push` (wire key `event_name`). */
|
|
64
|
+
eventName: Schema.String,
|
|
65
|
+
/** The login of the user who triggered the run. */
|
|
66
|
+
actor: Schema.String,
|
|
67
|
+
/** That user's numeric id (wire key `actor_id`). */
|
|
68
|
+
actorId: IdFromString,
|
|
69
|
+
/** The deployment environment, when the job names one. */
|
|
70
|
+
environment: Schema.optionalKey(Schema.String),
|
|
71
|
+
/** `github-hosted` or `self-hosted` (wire key `runner_environment`). */
|
|
72
|
+
runnerEnvironment: Schema.String
|
|
73
|
+
}) {};
|
|
74
|
+
/** The claims as they arrive in the token: snake_case keys, decoding to {@link ActionsOidcClaims}. */
|
|
75
|
+
const ClaimsFromWire = ActionsOidcClaims.pipe(Schema.encodeKeys({
|
|
76
|
+
repositoryId: "repository_id",
|
|
77
|
+
repositoryOwner: "repository_owner",
|
|
78
|
+
repositoryOwnerId: "repository_owner_id",
|
|
79
|
+
runId: "run_id",
|
|
80
|
+
runAttempt: "run_attempt",
|
|
81
|
+
workflowRef: "workflow_ref",
|
|
82
|
+
jobWorkflowRef: "job_workflow_ref",
|
|
83
|
+
eventName: "event_name",
|
|
84
|
+
actorId: "actor_id",
|
|
85
|
+
runnerEnvironment: "runner_environment"
|
|
86
|
+
}));
|
|
87
|
+
const verify = Effect.fn("ActionsOidc.verify")(function* (token, options) {
|
|
88
|
+
if (options.audience.length === 0) return yield* new JwtError({
|
|
89
|
+
reason: "wrongAudience",
|
|
90
|
+
detail: "no audience was given; an Actions OIDC token is never verified without one"
|
|
91
|
+
});
|
|
92
|
+
return yield* Jwt.verify(token, {
|
|
93
|
+
key: JwksResolver.forIssuer(ISSUER),
|
|
94
|
+
claims: ClaimsFromWire,
|
|
95
|
+
issuer: ISSUER,
|
|
96
|
+
audience: options.audience,
|
|
97
|
+
...options.clockTolerance !== void 0 ? { clockTolerance: options.clockTolerance } : {}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
const testClaims = (overrides = {}) => ({
|
|
101
|
+
iss: ISSUER,
|
|
102
|
+
sub: "repo:octo-org/octo-repo:ref:refs/heads/main",
|
|
103
|
+
aud: "https://github.com/octo-org",
|
|
104
|
+
repository: "octo-org/octo-repo",
|
|
105
|
+
repository_id: "123456789",
|
|
106
|
+
repository_owner: "octo-org",
|
|
107
|
+
repository_owner_id: "987654",
|
|
108
|
+
run_id: "4242424242",
|
|
109
|
+
run_attempt: "1",
|
|
110
|
+
workflow_ref: "octo-org/octo-repo/.github/workflows/ci.yml@refs/heads/main",
|
|
111
|
+
job_workflow_ref: "octo-org/octo-repo/.github/workflows/ci.yml@refs/heads/main",
|
|
112
|
+
ref: "refs/heads/main",
|
|
113
|
+
sha: "0123456789abcdef0123456789abcdef01234567",
|
|
114
|
+
event_name: "push",
|
|
115
|
+
actor: "octocat",
|
|
116
|
+
actor_id: "583231",
|
|
117
|
+
runner_environment: "github-hosted",
|
|
118
|
+
...overrides
|
|
119
|
+
});
|
|
120
|
+
/**
|
|
121
|
+
* Verify GitHub Actions OIDC tokens: a workflow proving to your service which
|
|
122
|
+
* repository, workflow and run it is.
|
|
123
|
+
*
|
|
124
|
+
* @remarks
|
|
125
|
+
* {@link ActionsOidc.verify} checks the signature against GitHub's published
|
|
126
|
+
* JWKS (through `@effected/jwt`'s `JwksResolver`, which caches it), then `iss`,
|
|
127
|
+
* `aud`, `exp`, `nbf` and `iat`, then decodes the payload into
|
|
128
|
+
* {@link ActionsOidcClaims}. Every failure is a `JwtError`; route on its
|
|
129
|
+
* `reason` (`wrongAudience`, `expired`, `claims`, ...), never on its message.
|
|
130
|
+
*
|
|
131
|
+
* Authorizing the caller is still yours: a verified token proves who is
|
|
132
|
+
* calling, not that they may. Pin the **immutable ids**, `repositoryId` or
|
|
133
|
+
* `repositoryOwnerId`, together with `jobWorkflowRef` (or `workflowRef`),
|
|
134
|
+
* never the `repository` or `repositoryOwner` name alone: a deleted
|
|
135
|
+
* repository's or account's name can be registered again by someone else,
|
|
136
|
+
* whose workflows then mint tokens carrying the old name.
|
|
137
|
+
*
|
|
138
|
+
* Only the github.com issuer is accepted. A GitHub Enterprise Cloud
|
|
139
|
+
* enterprise's unique issuer (`https://token.actions.githubusercontent.com/<enterprise>`)
|
|
140
|
+
* and a GitHub Enterprise Server issuer (`https://<host>/_services/token`)
|
|
141
|
+
* are refused as `wrongIssuer`.
|
|
142
|
+
*
|
|
143
|
+
* This module does not reach octokit: a service that only verifies tokens
|
|
144
|
+
* links `@effected/jwt` and nothing else from this package.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* import { JwksResolver } from "@effected/jwt";
|
|
149
|
+
* import { ActionsOidc } from "@effected/github";
|
|
150
|
+
* import { Data, Effect } from "effect";
|
|
151
|
+
*
|
|
152
|
+
* class Forbidden extends Data.TaggedError("Forbidden")<{ readonly repositoryId: number }> {}
|
|
153
|
+
*
|
|
154
|
+
* // Pinned by id, which survives a rename and cannot be re-registered.
|
|
155
|
+
* const ALLOWED_REPOSITORY_ID = 123456789;
|
|
156
|
+
* const ALLOWED_WORKFLOW = "octo-org/octo-repo/.github/workflows/deploy.yml@refs/heads/main";
|
|
157
|
+
*
|
|
158
|
+
* const authorize = (bearer: string) =>
|
|
159
|
+
* Effect.gen(function* () {
|
|
160
|
+
* const claims = yield* ActionsOidc.verify(bearer, { audience: "https://my-service.example" });
|
|
161
|
+
* if (claims.repositoryId !== ALLOWED_REPOSITORY_ID || claims.jobWorkflowRef !== ALLOWED_WORKFLOW) {
|
|
162
|
+
* return yield* new Forbidden({ repositoryId: claims.repositoryId });
|
|
163
|
+
* }
|
|
164
|
+
* return claims;
|
|
165
|
+
* });
|
|
166
|
+
*
|
|
167
|
+
* // JwksResolver.layer needs an HttpClient and a JwksStore, provided once at the edge.
|
|
168
|
+
* export const program = authorize("eyJ...").pipe(Effect.provide(JwksResolver.layer));
|
|
169
|
+
* ```
|
|
170
|
+
*
|
|
171
|
+
* @public
|
|
172
|
+
*/
|
|
173
|
+
const ActionsOidc = {
|
|
174
|
+
issuer: ISSUER,
|
|
175
|
+
verify,
|
|
176
|
+
testClaims
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
//#endregion
|
|
180
|
+
export { ActionsOidc, ActionsOidcClaims };
|
package/CheckRun.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { GitHubError } from "./GitHubError.js";
|
|
1
2
|
import { GitHubClient } from "./GitHubClient.js";
|
|
2
3
|
import { Repo } from "./Repo.js";
|
|
3
4
|
import { numericId } from "./internal/ids.js";
|
|
4
|
-
import { Cause, Context, Effect, Exit, Layer, Ref, Schema } from "effect";
|
|
5
|
+
import { Cause, Context, DateTime, Effect, Exit, Layer, Option, Ref, Schema } from "effect";
|
|
5
6
|
|
|
6
7
|
//#region src/CheckRun.ts
|
|
7
8
|
/** How a check run finished. @public */
|
|
@@ -81,6 +82,17 @@ var CheckRunOutput = class CheckRunOutput extends Schema.Class("CheckRunOutput")
|
|
|
81
82
|
});
|
|
82
83
|
}
|
|
83
84
|
};
|
|
85
|
+
/** UTF-8 encoder for the byte arithmetic; portable, unlike Node's `Buffer`. */
|
|
86
|
+
const utf8 = new TextEncoder();
|
|
87
|
+
/**
|
|
88
|
+
* UTF-8 decoder for the cut. Non-fatal, so a code point split by the cut decodes
|
|
89
|
+
* to U+FFFD for the trim loop to drop rather than throwing; `ignoreBOM` keeps a
|
|
90
|
+
* leading BOM as content, as `Buffer` did.
|
|
91
|
+
*/
|
|
92
|
+
const utf8Lenient = new TextDecoder("utf-8", {
|
|
93
|
+
fatal: false,
|
|
94
|
+
ignoreBOM: true
|
|
95
|
+
});
|
|
84
96
|
/**
|
|
85
97
|
* Cut `value` to GitHub's byte budget without leaving a broken code point.
|
|
86
98
|
*
|
|
@@ -88,11 +100,15 @@ var CheckRunOutput = class CheckRunOutput extends Schema.Class("CheckRunOutput")
|
|
|
88
100
|
* Slicing a UTF-8 buffer mid-character decodes to U+FFFD. Splitting a four-byte
|
|
89
101
|
* code point can produce **more than one** replacement character, so the trim
|
|
90
102
|
* loops rather than dropping a single one.
|
|
103
|
+
*
|
|
104
|
+
* `TextEncoder`/`TextDecoder` rather than `Buffer`, which is a Node global: a
|
|
105
|
+
* Worker without `nodejs_compat` would throw a `ReferenceError` here.
|
|
91
106
|
*/
|
|
92
107
|
const capBytes = (value) => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
108
|
+
const bytes = utf8.encode(value);
|
|
109
|
+
if (bytes.length <= CheckRunOutput.LIMIT_BYTES) return value;
|
|
110
|
+
const budget = CheckRunOutput.LIMIT_BYTES - utf8.encode(CheckRunOutput.NOTICE).length;
|
|
111
|
+
let cut = utf8Lenient.decode(bytes.subarray(0, budget));
|
|
96
112
|
while (cut.endsWith("�")) cut = cut.slice(0, -1);
|
|
97
113
|
return `${cut}${CheckRunOutput.NOTICE}`;
|
|
98
114
|
};
|
|
@@ -106,7 +122,13 @@ var CheckRunRef = class extends Schema.Class("CheckRunRef")({
|
|
|
106
122
|
name: Schema.String,
|
|
107
123
|
/** The web URL. */
|
|
108
124
|
url: Schema.String,
|
|
109
|
-
status: Schema.String
|
|
125
|
+
status: Schema.String,
|
|
126
|
+
/**
|
|
127
|
+
* The integrator's own id for the run (wire `external_id`), when it has
|
|
128
|
+
* one. GitHub reports a run created without one as `null` or `""`; both
|
|
129
|
+
* leave this absent.
|
|
130
|
+
*/
|
|
131
|
+
externalId: Schema.optionalKey(Schema.String)
|
|
110
132
|
}) {};
|
|
111
133
|
/**
|
|
112
134
|
* Create, update and conclude GitHub check runs, including the
|
|
@@ -126,6 +148,7 @@ var CheckRun = class CheckRun extends Context.Service()("@effected/github/CheckR
|
|
|
126
148
|
create: overrides.create ?? (() => unstubbed("create")),
|
|
127
149
|
get: overrides.get ?? (() => unstubbed("get")),
|
|
128
150
|
update: overrides.update ?? (() => unstubbed("update")),
|
|
151
|
+
findByExternalId: overrides.findByExternalId ?? (() => unstubbed("findByExternalId")),
|
|
129
152
|
complete: overrides.complete ?? (() => unstubbed("complete")),
|
|
130
153
|
withCheckRun: overrides.withCheckRun ?? (() => unstubbed("withCheckRun"))
|
|
131
154
|
});
|
|
@@ -202,32 +225,49 @@ const concludeFor = (name, id, exit, recorded, complete) => {
|
|
|
202
225
|
const write = complete(id, settled.conclusion, settled.output);
|
|
203
226
|
return Exit.isSuccess(exit) ? write : Effect.ignore(write);
|
|
204
227
|
};
|
|
205
|
-
const
|
|
228
|
+
const decodeRef = Schema.decodeUnknownEffect(CheckRunRef);
|
|
229
|
+
/**
|
|
230
|
+
* Project a check-run response onto {@link CheckRunRef}, **decoding** it.
|
|
231
|
+
*
|
|
232
|
+
* @remarks
|
|
233
|
+
* Decoded rather than built with `make`, which throws: a response missing a
|
|
234
|
+
* field (a hand-written double, or GitHub changing shape) is input, and input
|
|
235
|
+
* failures are a typed `decode` `GitHubError` naming the operation rather than
|
|
236
|
+
* a defect.
|
|
237
|
+
*/
|
|
238
|
+
const refOf = (operation, raw) => decodeRef({
|
|
206
239
|
id: numericId(raw.id),
|
|
207
240
|
name: raw.name,
|
|
208
241
|
url: raw.html_url ?? "",
|
|
209
|
-
status: raw.status
|
|
210
|
-
}
|
|
242
|
+
status: raw.status,
|
|
243
|
+
...raw.external_id ? { externalId: raw.external_id } : {}
|
|
244
|
+
}).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(GitHubError.decode(operation, "GitHub returned an unexpected check run", error))));
|
|
245
|
+
/** The current time as GitHub's ISO 8601 timestamp, from `Clock` so `TestClock` drives it. */
|
|
246
|
+
const isoNow = Effect.map(DateTime.now, DateTime.formatIso);
|
|
211
247
|
const make = (client) => {
|
|
212
|
-
const create = Effect.fn("CheckRun.create")(function* (name, headSha) {
|
|
248
|
+
const create = Effect.fn("CheckRun.create")(function* (name, headSha, options) {
|
|
213
249
|
const { owner, repo } = yield* Repo;
|
|
250
|
+
const status = options?.status ?? "in_progress";
|
|
214
251
|
yield* Effect.annotateCurrentSpan({
|
|
215
252
|
owner,
|
|
216
253
|
repo,
|
|
217
254
|
name,
|
|
218
|
-
headSha
|
|
255
|
+
headSha,
|
|
256
|
+
status
|
|
219
257
|
});
|
|
220
258
|
const created = yield* client.request("POST /repos/{owner}/{repo}/check-runs", {
|
|
221
259
|
owner,
|
|
222
260
|
repo,
|
|
223
261
|
name,
|
|
224
262
|
head_sha: headSha,
|
|
225
|
-
status
|
|
226
|
-
started_at:
|
|
263
|
+
status,
|
|
264
|
+
...status === "in_progress" ? { started_at: yield* isoNow } : {},
|
|
265
|
+
...options?.externalId ? { external_id: options.externalId } : {},
|
|
266
|
+
...options?.detailsUrl !== void 0 ? { details_url: options.detailsUrl } : {}
|
|
227
267
|
});
|
|
228
|
-
return refOf(created);
|
|
268
|
+
return yield* refOf("CheckRun.create", created);
|
|
229
269
|
});
|
|
230
|
-
const complete = Effect.fn("CheckRun.complete")(function* (id, conclusion, output) {
|
|
270
|
+
const complete = Effect.fn("CheckRun.complete")(function* (id, conclusion, output, options) {
|
|
231
271
|
const { owner, repo } = yield* Repo;
|
|
232
272
|
yield* Effect.annotateCurrentSpan({
|
|
233
273
|
owner,
|
|
@@ -241,8 +281,9 @@ const make = (client) => {
|
|
|
241
281
|
check_run_id: id,
|
|
242
282
|
status: "completed",
|
|
243
283
|
conclusion,
|
|
244
|
-
completed_at:
|
|
245
|
-
...output !== void 0 ? { output: wireOutput(output) } : {}
|
|
284
|
+
completed_at: yield* isoNow,
|
|
285
|
+
...output !== void 0 ? { output: wireOutput(output) } : {},
|
|
286
|
+
...options?.detailsUrl !== void 0 ? { details_url: options.detailsUrl } : {}
|
|
246
287
|
});
|
|
247
288
|
});
|
|
248
289
|
return {
|
|
@@ -260,9 +301,9 @@ const make = (client) => {
|
|
|
260
301
|
repo,
|
|
261
302
|
check_run_id: id
|
|
262
303
|
});
|
|
263
|
-
return refOf(raw);
|
|
304
|
+
return yield* refOf("CheckRun.get", raw);
|
|
264
305
|
}),
|
|
265
|
-
update: Effect.fn("CheckRun.update")(function* (id, output) {
|
|
306
|
+
update: Effect.fn("CheckRun.update")(function* (id, output, options) {
|
|
266
307
|
const { owner, repo } = yield* Repo;
|
|
267
308
|
yield* Effect.annotateCurrentSpan({
|
|
268
309
|
owner,
|
|
@@ -273,8 +314,35 @@ const make = (client) => {
|
|
|
273
314
|
owner,
|
|
274
315
|
repo,
|
|
275
316
|
check_run_id: id,
|
|
276
|
-
output: wireOutput(output)
|
|
317
|
+
...output !== void 0 ? { output: wireOutput(output) } : {},
|
|
318
|
+
...options?.status !== void 0 ? { status: options.status } : {},
|
|
319
|
+
...options?.status === "in_progress" ? { started_at: yield* isoNow } : {},
|
|
320
|
+
...options?.detailsUrl !== void 0 ? { details_url: options.detailsUrl } : {}
|
|
321
|
+
});
|
|
322
|
+
}),
|
|
323
|
+
findByExternalId: Effect.fn("CheckRun.findByExternalId")(function* (headSha, name, externalId) {
|
|
324
|
+
if (externalId === "") return Option.none();
|
|
325
|
+
const { owner, repo } = yield* Repo;
|
|
326
|
+
yield* Effect.annotateCurrentSpan({
|
|
327
|
+
owner,
|
|
328
|
+
repo,
|
|
329
|
+
headSha,
|
|
330
|
+
name,
|
|
331
|
+
externalId
|
|
332
|
+
});
|
|
333
|
+
const runs = yield* client.paginate("GET /repos/{owner}/{repo}/commits/{ref}/check-runs", {
|
|
334
|
+
owner,
|
|
335
|
+
repo,
|
|
336
|
+
ref: headSha,
|
|
337
|
+
check_name: name,
|
|
338
|
+
filter: "all"
|
|
277
339
|
});
|
|
340
|
+
let newest;
|
|
341
|
+
for (const run of runs) {
|
|
342
|
+
if (run.external_id !== externalId) continue;
|
|
343
|
+
if (newest === void 0 || numericId(run.id) > numericId(newest.id)) newest = run;
|
|
344
|
+
}
|
|
345
|
+
return newest === void 0 ? Option.none() : Option.some(yield* refOf("CheckRun.findByExternalId", newest));
|
|
278
346
|
}),
|
|
279
347
|
withCheckRun: (name, headSha, use) => Effect.gen(function* () {
|
|
280
348
|
const run = yield* create(name, headSha);
|