@effected/github 0.15.1 → 0.17.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 +91 -23
- package/index.d.ts +683 -28
- package/index.js +5 -3
- package/internal/crypto.js +1 -1
- package/package.json +3 -3
package/index.d.ts
CHANGED
|
@@ -1,9 +1,160 @@
|
|
|
1
1
|
import { BareLineReference, CLOSING_KEYWORDS, ClosingKeyword, IssueReference, harvestIssueReferences, parseBareLineReference } from "@effected/github-references";
|
|
2
|
-
import {
|
|
2
|
+
import { JwksResolver, JwtError } from "@effected/jwt";
|
|
3
|
+
import { Cause, Config, Context, Duration, Effect, Layer, Option, Redacted, Result, Schedule, Schema, Scope, Stream } from "effect";
|
|
3
4
|
import { PaginatingEndpoints } from "@octokit/plugin-paginate-rest";
|
|
4
5
|
import { Endpoints, RequestHeaders } from "@octokit/types";
|
|
5
6
|
import { SemVer } from "@effected/semver";
|
|
6
7
|
//#endregion
|
|
8
|
+
//#region src/ActionsOidc.d.ts
|
|
9
|
+
declare const ActionsOidcClaims_base: Schema.Class<ActionsOidcClaims, Schema.Struct<{
|
|
10
|
+
/** The issuer: always {@link ActionsOidc.issuer} once verified. */
|
|
11
|
+
readonly iss: Schema.String;
|
|
12
|
+
/** The subject, e.g. `repo:octo-org/octo-repo:ref:refs/heads/main`. */
|
|
13
|
+
readonly sub: Schema.String;
|
|
14
|
+
/** The audience or audiences the token was minted for. */
|
|
15
|
+
readonly aud: Schema.Union<readonly [Schema.String, Schema.$Array<Schema.String>]>;
|
|
16
|
+
/** Expiry, in seconds since the epoch. */
|
|
17
|
+
readonly exp: Schema.Number;
|
|
18
|
+
/** Issued at, in seconds since the epoch. */
|
|
19
|
+
readonly iat: Schema.Number;
|
|
20
|
+
/** Not before, in seconds since the epoch. */
|
|
21
|
+
readonly nbf: Schema.optionalKey<Schema.Number>;
|
|
22
|
+
/** The token's unique id. */
|
|
23
|
+
readonly jti: Schema.optionalKey<Schema.String>;
|
|
24
|
+
/** The repository, `owner/name`. */
|
|
25
|
+
readonly repository: Schema.String;
|
|
26
|
+
/** The repository's numeric id (wire key `repository_id`). */
|
|
27
|
+
readonly repositoryId: Schema.decodeTo<Schema.Int, Schema.String, never, never>;
|
|
28
|
+
/** The repository owner's login (wire key `repository_owner`). */
|
|
29
|
+
readonly repositoryOwner: Schema.String;
|
|
30
|
+
/** The repository owner's numeric id (wire key `repository_owner_id`). */
|
|
31
|
+
readonly repositoryOwnerId: Schema.decodeTo<Schema.Int, Schema.String, never, never>;
|
|
32
|
+
/** The workflow run's id (wire key `run_id`). */
|
|
33
|
+
readonly runId: Schema.decodeTo<Schema.Int, Schema.String, never, never>;
|
|
34
|
+
/** The run attempt, from 1 (wire key `run_attempt`). */
|
|
35
|
+
readonly runAttempt: Schema.decodeTo<Schema.Int, Schema.String, never, never>;
|
|
36
|
+
/** The calling workflow's ref, e.g. `octo-org/octo-repo/.github/workflows/ci.yml@refs/heads/main` (wire key `workflow_ref`). */
|
|
37
|
+
readonly workflowRef: Schema.String;
|
|
38
|
+
/** The ref of the workflow file the job runs, which differs from `workflowRef` for a reusable workflow (wire key `job_workflow_ref`). */
|
|
39
|
+
readonly jobWorkflowRef: Schema.String;
|
|
40
|
+
/** The git ref the run is for. */
|
|
41
|
+
readonly ref: Schema.String;
|
|
42
|
+
/** The commit SHA the run is for. */
|
|
43
|
+
readonly sha: Schema.String;
|
|
44
|
+
/** The event that triggered the run, e.g. `push` (wire key `event_name`). */
|
|
45
|
+
readonly eventName: Schema.String;
|
|
46
|
+
/** The login of the user who triggered the run. */
|
|
47
|
+
readonly actor: Schema.String;
|
|
48
|
+
/** That user's numeric id (wire key `actor_id`). */
|
|
49
|
+
readonly actorId: Schema.decodeTo<Schema.Int, Schema.String, never, never>;
|
|
50
|
+
/** The deployment environment, when the job names one. */
|
|
51
|
+
readonly environment: Schema.optionalKey<Schema.String>;
|
|
52
|
+
/** `github-hosted` or `self-hosted` (wire key `runner_environment`). */
|
|
53
|
+
readonly runnerEnvironment: Schema.String;
|
|
54
|
+
}>, {}>;
|
|
55
|
+
/**
|
|
56
|
+
* The claims of a verified GitHub Actions OIDC token.
|
|
57
|
+
*
|
|
58
|
+
* @remarks
|
|
59
|
+
* Field names are camelCase; the token carries them in snake_case
|
|
60
|
+
* (`repository_id`, `job_workflow_ref`, ...), and {@link ActionsOidc.verify}
|
|
61
|
+
* maps between the two. GitHub sends the numeric ids (`repository_id`,
|
|
62
|
+
* `repository_owner_id`, `run_id`, `run_attempt`, `actor_id`) as strings;
|
|
63
|
+
* they decode to numbers here, and a non-numeric one fails verification as
|
|
64
|
+
* `claims`.
|
|
65
|
+
*
|
|
66
|
+
* Claims GitHub adds that are not modelled here are dropped on decode.
|
|
67
|
+
*
|
|
68
|
+
* @public
|
|
69
|
+
*/
|
|
70
|
+
export declare class ActionsOidcClaims extends ActionsOidcClaims_base {}
|
|
71
|
+
/**
|
|
72
|
+
* Options for {@link ActionsOidc.verify}.
|
|
73
|
+
*
|
|
74
|
+
* @public
|
|
75
|
+
*/
|
|
76
|
+
interface ActionsOidcVerifyOptions {
|
|
77
|
+
/**
|
|
78
|
+
* The audience the token must have been minted for: what the workflow
|
|
79
|
+
* passed to `core.getIDToken(audience)`. Required and non-empty, because
|
|
80
|
+
* GitHub's one JWKS signs tokens for every relying party; without an
|
|
81
|
+
* audience check, any workflow's token for any service would verify.
|
|
82
|
+
*/
|
|
83
|
+
readonly audience: string;
|
|
84
|
+
/** Allowed clock skew for `exp`, `nbf` and `iat`; 60 seconds by default. */
|
|
85
|
+
readonly clockTolerance?: Duration.Input | undefined;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Verify GitHub Actions OIDC tokens: a workflow proving to your service which
|
|
89
|
+
* repository, workflow and run it is.
|
|
90
|
+
*
|
|
91
|
+
* @remarks
|
|
92
|
+
* {@link ActionsOidc.verify} checks the signature against GitHub's published
|
|
93
|
+
* JWKS (through `@effected/jwt`'s `JwksResolver`, which caches it), then `iss`,
|
|
94
|
+
* `aud`, `exp`, `nbf` and `iat`, then decodes the payload into
|
|
95
|
+
* {@link ActionsOidcClaims}. Every failure is a `JwtError`; route on its
|
|
96
|
+
* `reason` (`wrongAudience`, `expired`, `claims`, ...), never on its message.
|
|
97
|
+
*
|
|
98
|
+
* Authorizing the caller is still yours: a verified token proves who is
|
|
99
|
+
* calling, not that they may. Pin the **immutable ids**, `repositoryId` or
|
|
100
|
+
* `repositoryOwnerId`, together with `jobWorkflowRef` (or `workflowRef`),
|
|
101
|
+
* never the `repository` or `repositoryOwner` name alone: a deleted
|
|
102
|
+
* repository's or account's name can be registered again by someone else,
|
|
103
|
+
* whose workflows then mint tokens carrying the old name.
|
|
104
|
+
*
|
|
105
|
+
* Only the github.com issuer is accepted. A GitHub Enterprise Cloud
|
|
106
|
+
* enterprise's unique issuer (`https://token.actions.githubusercontent.com/<enterprise>`)
|
|
107
|
+
* and a GitHub Enterprise Server issuer (`https://<host>/_services/token`)
|
|
108
|
+
* are refused as `wrongIssuer`.
|
|
109
|
+
*
|
|
110
|
+
* This module does not reach octokit: a service that only verifies tokens
|
|
111
|
+
* links `@effected/jwt` and nothing else from this package.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* import { JwksResolver } from "@effected/jwt";
|
|
116
|
+
* import { ActionsOidc } from "@effected/github";
|
|
117
|
+
* import { Data, Effect } from "effect";
|
|
118
|
+
*
|
|
119
|
+
* class Forbidden extends Data.TaggedError("Forbidden")<{ readonly repositoryId: number }> {}
|
|
120
|
+
*
|
|
121
|
+
* // Pinned by id, which survives a rename and cannot be re-registered.
|
|
122
|
+
* const ALLOWED_REPOSITORY_ID = 123456789;
|
|
123
|
+
* const ALLOWED_WORKFLOW = "octo-org/octo-repo/.github/workflows/deploy.yml@refs/heads/main";
|
|
124
|
+
*
|
|
125
|
+
* const authorize = (bearer: string) =>
|
|
126
|
+
* Effect.gen(function* () {
|
|
127
|
+
* const claims = yield* ActionsOidc.verify(bearer, { audience: "https://my-service.example" });
|
|
128
|
+
* if (claims.repositoryId !== ALLOWED_REPOSITORY_ID || claims.jobWorkflowRef !== ALLOWED_WORKFLOW) {
|
|
129
|
+
* return yield* new Forbidden({ repositoryId: claims.repositoryId });
|
|
130
|
+
* }
|
|
131
|
+
* return claims;
|
|
132
|
+
* });
|
|
133
|
+
*
|
|
134
|
+
* // JwksResolver.layer needs an HttpClient and a JwksStore, provided once at the edge.
|
|
135
|
+
* export const program = authorize("eyJ...").pipe(Effect.provide(JwksResolver.layer));
|
|
136
|
+
* ```
|
|
137
|
+
*
|
|
138
|
+
* @public
|
|
139
|
+
*/
|
|
140
|
+
export declare const ActionsOidc: {
|
|
141
|
+
/** The Actions OIDC issuer, `https://token.actions.githubusercontent.com`. */
|
|
142
|
+
readonly issuer: "https://token.actions.githubusercontent.com";
|
|
143
|
+
/** Verify a runner-issued OIDC token: signature against GitHub's JWKS, iss, aud, exp/nbf, then typed claims. */
|
|
144
|
+
readonly verify: (token: string, options: ActionsOidcVerifyOptions) => Effect.Effect<ActionsOidcClaims, JwtError, JwksResolver>;
|
|
145
|
+
/**
|
|
146
|
+
* A valid claims payload for tests, overridable field by field.
|
|
147
|
+
*
|
|
148
|
+
* @remarks
|
|
149
|
+
* Wire-shaped (snake_case keys, string ids) and complete except for the
|
|
150
|
+
* time claims `iat` and `exp`, which depend on the clock: a signer stamps
|
|
151
|
+
* them, as `TestIssuer.sign` from `@effected/jwt/testing` does. `iss` is
|
|
152
|
+
* {@link ActionsOidc.issuer}; `aud` is a placeholder to override with the
|
|
153
|
+
* audience under test.
|
|
154
|
+
*/
|
|
155
|
+
readonly testClaims: (overrides?: Readonly<Record<string, unknown>>) => Record<string, unknown>;
|
|
156
|
+
};
|
|
157
|
+
//#endregion
|
|
7
158
|
//#region src/GitHubError.d.ts
|
|
8
159
|
/**
|
|
9
160
|
* Why a GitHub call failed, as a value you can branch on.
|
|
@@ -133,6 +284,27 @@ export declare class GitHubError extends GitHubError_base {
|
|
|
133
284
|
* under test.
|
|
134
285
|
*/
|
|
135
286
|
static fromOctokit(operation: string, error: unknown, nowMillis: number): GitHubError;
|
|
287
|
+
/**
|
|
288
|
+
* Classify a raw HTTP response: a status, its headers and its JSON body.
|
|
289
|
+
*
|
|
290
|
+
* @remarks
|
|
291
|
+
* For a response that never went through octokit, such as one a test
|
|
292
|
+
* records with `GitHubFixtures.failure` or one a consumer fetched itself. It
|
|
293
|
+
* reads the same facts {@link GitHubError.fromOctokit} reads (`reason` from
|
|
294
|
+
* `body.message`, else `HTTP <status>`, sanitized the same way; `validation`
|
|
295
|
+
* from `body.errors`) and runs the same classifier, so a raw response and
|
|
296
|
+
* the octokit error for it classify identically.
|
|
297
|
+
*
|
|
298
|
+
* Header names are matched in any case. `nowMillis` is required, as in
|
|
299
|
+
* {@link GitHubError.fromOctokit}: it turns an absolute rate-limit reset
|
|
300
|
+
* (an epoch second) into a delay, so it must be the real current time,
|
|
301
|
+
* usually `Clock.currentTimeMillis`. A zero would make the delay decades.
|
|
302
|
+
*/
|
|
303
|
+
static fromResponse(operation: string, response: {
|
|
304
|
+
readonly status: number;
|
|
305
|
+
readonly headers?: Readonly<Record<string, string>> | undefined;
|
|
306
|
+
readonly body?: unknown;
|
|
307
|
+
}, nowMillis: number): GitHubError;
|
|
136
308
|
/**
|
|
137
309
|
* A predicate over one or more kinds, for `Effect.catchIf`.
|
|
138
310
|
*
|
|
@@ -650,7 +822,7 @@ interface GitHubClientOptions {
|
|
|
650
822
|
}
|
|
651
823
|
/**
|
|
652
824
|
* One call served by {@link GitHubClient.layerFixture}, as recorded in
|
|
653
|
-
* {@link GitHubFixtures.requested}.
|
|
825
|
+
* {@link (GitHubFixtures:interface).requested}.
|
|
654
826
|
*
|
|
655
827
|
* @public
|
|
656
828
|
*/
|
|
@@ -664,12 +836,80 @@ interface RecordedCall {
|
|
|
664
836
|
/** The page size asked for; paginated reads only. */
|
|
665
837
|
readonly perPage?: number;
|
|
666
838
|
}
|
|
839
|
+
/**
|
|
840
|
+
* A raw HTTP failure recorded for {@link GitHubClient.layerFixture}, made by
|
|
841
|
+
* {@link (GitHubFixtures:variable).failure}.
|
|
842
|
+
*
|
|
843
|
+
* @remarks
|
|
844
|
+
* Branded, so a fixture's data payload that happens to have a `status` and a
|
|
845
|
+
* `body` is never mistaken for a failure: only a value built by
|
|
846
|
+
* `GitHubFixtures.failure` fails the call. The brand is a registered symbol
|
|
847
|
+
* rather than the class identity, so a failure built by another resolved copy
|
|
848
|
+
* of this package is still recognized.
|
|
849
|
+
*
|
|
850
|
+
* @public
|
|
851
|
+
*/
|
|
852
|
+
export declare class RawFailure {
|
|
853
|
+
/** The HTTP status. */
|
|
854
|
+
readonly status: number;
|
|
855
|
+
/** The response headers, in any case. */
|
|
856
|
+
readonly headers: Readonly<Record<string, string>> | undefined;
|
|
857
|
+
/** The JSON body GitHub would send. */
|
|
858
|
+
readonly body: unknown;
|
|
859
|
+
/** @internal */
|
|
860
|
+
constructor(response: {
|
|
861
|
+
readonly status: number;
|
|
862
|
+
readonly headers?: Readonly<Record<string, string>> | undefined;
|
|
863
|
+
readonly body?: unknown;
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Builders for {@link (GitHubFixtures:interface)} entries.
|
|
868
|
+
*
|
|
869
|
+
* @public
|
|
870
|
+
*/
|
|
871
|
+
export declare const GitHubFixtures: {
|
|
872
|
+
/**
|
|
873
|
+
* A raw failure: what GitHub answered, classified at call time by the same
|
|
874
|
+
* classifier the live client uses.
|
|
875
|
+
*
|
|
876
|
+
* @remarks
|
|
877
|
+
* Record it wherever a fixture accepts a `GitHubError` (`request`, and so
|
|
878
|
+
* `requestDecoded`, and `paginate`). Each call fails with
|
|
879
|
+
* `GitHubError.fromResponse(route, response, now)`, `now` read from `Clock`,
|
|
880
|
+
* so a rate-limit reset header is measured against the `TestClock`. Prefer it
|
|
881
|
+
* to a hand-built `GitHubError` when the test is about GitHub's actual
|
|
882
|
+
* response: the classification is then the package's, not the test's.
|
|
883
|
+
*
|
|
884
|
+
* @example
|
|
885
|
+
* ```ts
|
|
886
|
+
* import { GitHubClient, GitHubFixtures } from "@effected/github";
|
|
887
|
+
*
|
|
888
|
+
* const layer = GitHubClient.layerFixture({
|
|
889
|
+
* request: {
|
|
890
|
+
* "POST /repos/{owner}/{repo}/releases": GitHubFixtures.failure({
|
|
891
|
+
* status: 422,
|
|
892
|
+
* body: {
|
|
893
|
+
* message: "Validation Failed",
|
|
894
|
+
* errors: [{ resource: "Release", code: "already_exists", field: "tag_name" }],
|
|
895
|
+
* },
|
|
896
|
+
* }),
|
|
897
|
+
* },
|
|
898
|
+
* });
|
|
899
|
+
* ```
|
|
900
|
+
*/
|
|
901
|
+
readonly failure: (response: {
|
|
902
|
+
readonly status: number;
|
|
903
|
+
readonly headers?: Readonly<Record<string, string>> | undefined;
|
|
904
|
+
readonly body?: unknown;
|
|
905
|
+
}) => RawFailure;
|
|
906
|
+
};
|
|
667
907
|
/**
|
|
668
908
|
* A recorded response table for {@link GitHubClient.layerFixture}.
|
|
669
909
|
*
|
|
670
910
|
* @public
|
|
671
911
|
*/
|
|
672
|
-
interface GitHubFixtures {
|
|
912
|
+
export interface GitHubFixtures {
|
|
673
913
|
/**
|
|
674
914
|
* Keyed by route; the value is the `data` payload a request answers with.
|
|
675
915
|
*
|
|
@@ -677,14 +917,19 @@ interface GitHubFixtures {
|
|
|
677
917
|
* A recorded **`GitHubError` is the response**: the call fails with it. That
|
|
678
918
|
* is how a suite stubs a 404 (or a rate-limit, or a 422) deliberately,
|
|
679
919
|
* rather than relying on a route's absence to produce one — absence is a
|
|
680
|
-
* wiring mistake and {@link GitHubFixtures.unstubbed} treats it as
|
|
920
|
+
* wiring mistake and {@link (GitHubFixtures:interface).unstubbed} treats it as
|
|
921
|
+
* such. A {@link RawFailure} from {@link (GitHubFixtures:variable).failure}
|
|
922
|
+
* fails it too, classified from GitHub's raw response.
|
|
923
|
+
*
|
|
924
|
+
* A value of `undefined` counts as unstubbed, so stub a route that answers
|
|
925
|
+
* with no body (a 204) with `null`.
|
|
681
926
|
*/
|
|
682
927
|
readonly request?: Readonly<Record<string, unknown>> | undefined;
|
|
683
928
|
/**
|
|
684
929
|
* Keyed by route; the value is the whole collection, paged on demand — or a
|
|
685
|
-
* `GitHubError` the paginated read fails with.
|
|
930
|
+
* `GitHubError` or {@link RawFailure} the paginated read fails with.
|
|
686
931
|
*/
|
|
687
|
-
readonly paginate?: Readonly<Record<string, ReadonlyArray<unknown> | GitHubError>> | undefined;
|
|
932
|
+
readonly paginate?: Readonly<Record<string, ReadonlyArray<unknown> | GitHubError | RawFailure>> | undefined;
|
|
688
933
|
/** Keyed by document name; the value is the raw payload to decode. */
|
|
689
934
|
readonly graphql?: Readonly<Record<string, unknown>> | undefined;
|
|
690
935
|
/**
|
|
@@ -813,7 +1058,14 @@ export declare class GitHubClient extends GitHubClient_base {
|
|
|
813
1058
|
* `perPage` and `maxPages` cannot behave differently here than in production.
|
|
814
1059
|
*
|
|
815
1060
|
* `fixtures.requested` is appended to as the test runs, so a suite can assert
|
|
816
|
-
* which routes were walked and at what page size.
|
|
1061
|
+
* which routes were walked, with what params and at what page size.
|
|
1062
|
+
*
|
|
1063
|
+
* **It records every call it serves, including one that fails.** A call
|
|
1064
|
+
* answered by a recorded `GitHubError` or a
|
|
1065
|
+
* {@link (GitHubFixtures:variable).failure} is recorded before it fails, so
|
|
1066
|
+
* one fixture both records the calls and classifies a raw failure the way
|
|
1067
|
+
* the live client would; there is no need to classify by hand inside a
|
|
1068
|
+
* {@link GitHubClient.layerTest} handler.
|
|
817
1069
|
*/
|
|
818
1070
|
static readonly layerFixture: (fixtures: GitHubFixtures) => Layer.Layer<GitHubClient>;
|
|
819
1071
|
}
|
|
@@ -840,6 +1092,15 @@ declare const RepoRef_base: Schema.Class<RepoRef, Schema.Struct<{
|
|
|
840
1092
|
/**
|
|
841
1093
|
* Which repository an operation acts on.
|
|
842
1094
|
*
|
|
1095
|
+
* @remarks
|
|
1096
|
+
* **`RepoRef.make` validates at construction and throws** on an empty `owner`
|
|
1097
|
+
* or `repo`, so inside an `Effect` a bad value dies rather than failing. That
|
|
1098
|
+
* is right for a coordinate the program itself spells out, and wrong for one
|
|
1099
|
+
* built from data: for a slug read from a database row, a config file or a
|
|
1100
|
+
* webhook, use {@link RepoRef.parse}, which fails with a typed
|
|
1101
|
+
* {@link InvalidRepoRefError} instead (or {@link RepoRef.parseResult}
|
|
1102
|
+
* outside an `Effect`).
|
|
1103
|
+
*
|
|
843
1104
|
* @public
|
|
844
1105
|
*/
|
|
845
1106
|
export declare class RepoRef extends RepoRef_base {
|
|
@@ -852,7 +1113,13 @@ export declare class RepoRef extends RepoRef_base {
|
|
|
852
1113
|
* constructor, which is why string parsing is named rather than overloaded.
|
|
853
1114
|
*/
|
|
854
1115
|
static parseResult(slug: string): Result.Result<RepoRef, InvalidRepoRefError>;
|
|
855
|
-
/**
|
|
1116
|
+
/**
|
|
1117
|
+
* Parse `"owner/repo"`, failing with a typed {@link InvalidRepoRefError}.
|
|
1118
|
+
*
|
|
1119
|
+
* @remarks
|
|
1120
|
+
* The typed path for building a `Repo` from stored data, such as a stored
|
|
1121
|
+
* `full_name`: a malformed value fails here, where `make` would die.
|
|
1122
|
+
*/
|
|
856
1123
|
static readonly parse: (slug: string) => Effect.Effect<RepoRef, InvalidRepoRefError, never>;
|
|
857
1124
|
/** `"owner/repo"`. */
|
|
858
1125
|
get slug(): string;
|
|
@@ -1102,6 +1369,12 @@ declare const CheckRunRef_base: Schema.Class<CheckRunRef, Schema.Struct<{
|
|
|
1102
1369
|
/** The web URL. */
|
|
1103
1370
|
readonly url: Schema.String;
|
|
1104
1371
|
readonly status: Schema.String;
|
|
1372
|
+
/**
|
|
1373
|
+
* The integrator's own id for the run (wire `external_id`), when it has
|
|
1374
|
+
* one. GitHub reports a run created without one as `null` or `""`; both
|
|
1375
|
+
* leave this absent.
|
|
1376
|
+
*/
|
|
1377
|
+
readonly externalId: Schema.optionalKey<Schema.String>;
|
|
1105
1378
|
}>, {}>;
|
|
1106
1379
|
/**
|
|
1107
1380
|
* A check run as GitHub reports it.
|
|
@@ -1109,6 +1382,56 @@ declare const CheckRunRef_base: Schema.Class<CheckRunRef, Schema.Struct<{
|
|
|
1109
1382
|
* @public
|
|
1110
1383
|
*/
|
|
1111
1384
|
export declare class CheckRunRef extends CheckRunRef_base {}
|
|
1385
|
+
/**
|
|
1386
|
+
* Options for {@link CheckRunShape.create}.
|
|
1387
|
+
*
|
|
1388
|
+
* @public
|
|
1389
|
+
*/
|
|
1390
|
+
interface CreateCheckRunOptions {
|
|
1391
|
+
/**
|
|
1392
|
+
* The run's initial state. Defaults to `"in_progress"`, which also stamps
|
|
1393
|
+
* `started_at`; a `"queued"` run has not started, so it carries none.
|
|
1394
|
+
* Completing a run goes through {@link CheckRunShape.complete}.
|
|
1395
|
+
*/
|
|
1396
|
+
readonly status?: "queued" | "in_progress" | undefined;
|
|
1397
|
+
/**
|
|
1398
|
+
* Your own id for the run (wire `external_id`), for
|
|
1399
|
+
* {@link CheckRunShape.findByExternalId}. An empty string is treated as no
|
|
1400
|
+
* id and not sent, since that lookup never matches `""`.
|
|
1401
|
+
*/
|
|
1402
|
+
readonly externalId?: string | undefined;
|
|
1403
|
+
/** Where the integrator's full details live (wire `details_url`). */
|
|
1404
|
+
readonly detailsUrl?: string | undefined;
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Options for {@link CheckRunShape.update}.
|
|
1408
|
+
*
|
|
1409
|
+
* @public
|
|
1410
|
+
*/
|
|
1411
|
+
interface UpdateCheckRunOptions {
|
|
1412
|
+
/**
|
|
1413
|
+
* Move the run to `"queued"` or `"in_progress"`; `"in_progress"` also
|
|
1414
|
+
* stamps `started_at`, as {@link CheckRunShape.create} does. Completing it
|
|
1415
|
+
* goes through {@link CheckRunShape.complete}, which also records the
|
|
1416
|
+
* conclusion.
|
|
1417
|
+
*/
|
|
1418
|
+
readonly status?: "queued" | "in_progress" | undefined;
|
|
1419
|
+
/** Where the integrator's full details live (wire `details_url`). */
|
|
1420
|
+
readonly detailsUrl?: string | undefined;
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Options for {@link CheckRunShape.complete}.
|
|
1424
|
+
*
|
|
1425
|
+
* @public
|
|
1426
|
+
*/
|
|
1427
|
+
interface CompleteCheckRunOptions {
|
|
1428
|
+
/**
|
|
1429
|
+
* Where the integrator's full details live (wire `details_url`) — for
|
|
1430
|
+
* instance the workflow run that produced the verdict. Omitted, the run
|
|
1431
|
+
* keeps whatever details URL it already had.
|
|
1432
|
+
*/
|
|
1433
|
+
readonly detailsUrl?: string | undefined;
|
|
1434
|
+
}
|
|
1112
1435
|
/**
|
|
1113
1436
|
* Conclude the surrounding {@link CheckRunShape.withCheckRun} explicitly.
|
|
1114
1437
|
*
|
|
@@ -1139,13 +1462,48 @@ type ConcludeCheckRun = (conclusion: (typeof CheckConclusion.literals)[number],
|
|
|
1139
1462
|
* @public
|
|
1140
1463
|
*/
|
|
1141
1464
|
interface CheckRunShape {
|
|
1142
|
-
/** Start
|
|
1143
|
-
readonly create: (name: string, headSha: string) => Effect.Effect<CheckRunRef, GitHubError, Repo>;
|
|
1465
|
+
/** Start a check run against a commit: in progress, unless `options.status` queues it. */
|
|
1466
|
+
readonly create: (name: string, headSha: string, options?: CreateCheckRunOptions) => Effect.Effect<CheckRunRef, GitHubError, Repo>;
|
|
1144
1467
|
readonly get: (id: number) => Effect.Effect<CheckRunRef, GitHubError, Repo>;
|
|
1145
|
-
/**
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1468
|
+
/**
|
|
1469
|
+
* Update an in-flight run: its output, its status, its details URL, or any
|
|
1470
|
+
* combination.
|
|
1471
|
+
*
|
|
1472
|
+
* @remarks
|
|
1473
|
+
* Omit `output` (pass `undefined`) to change only the status or details
|
|
1474
|
+
* URL: no `output` key is sent, so the run keeps the output it has. That is
|
|
1475
|
+
* how a queued run moves to `"in_progress"` without rewriting its output.
|
|
1476
|
+
*
|
|
1477
|
+
* `update(id)` with neither still sends **one** PATCH, carrying nothing but
|
|
1478
|
+
* the run's coordinates. GitHub accepts it and changes nothing; it is not
|
|
1479
|
+
* skipped, so every call is exactly one request and a caller's error
|
|
1480
|
+
* handling sees a missing run or a revoked token the same way either way.
|
|
1481
|
+
*/
|
|
1482
|
+
readonly update: (id: number, output?: CheckRunOutput, options?: UpdateCheckRunOptions) => Effect.Effect<void, GitHubError, Repo>;
|
|
1483
|
+
/**
|
|
1484
|
+
* The newest run on `headSha` named `name` whose external id is
|
|
1485
|
+
* `externalId`; none when there is no such run.
|
|
1486
|
+
*
|
|
1487
|
+
* @remarks
|
|
1488
|
+
* Lists every run of the commit filtered by name on GitHub's side
|
|
1489
|
+
* (`filter: "all"`, not GitHub's default of only the latest run per name),
|
|
1490
|
+
* paging through all of them, then matches `external_id` here; "newest" is
|
|
1491
|
+
* the highest id.
|
|
1492
|
+
* An empty `externalId` is none without a request: GitHub reports a run
|
|
1493
|
+
* created without an external id as `""`, so matching on it would find
|
|
1494
|
+
* every such run.
|
|
1495
|
+
*/
|
|
1496
|
+
readonly findByExternalId: (headSha: string, name: string, externalId: string) => Effect.Effect<Option.Option<CheckRunRef>, GitHubError, Repo>;
|
|
1497
|
+
/**
|
|
1498
|
+
* Finish a run, stamping `completed_at` from `Clock`.
|
|
1499
|
+
*
|
|
1500
|
+
* @remarks
|
|
1501
|
+
* Omit `output` (pass `undefined`) to conclude without touching the run's
|
|
1502
|
+
* rendered output; a given output is cut to GitHub's byte limits first.
|
|
1503
|
+
* `options.detailsUrl` points the finished run somewhere, such as the
|
|
1504
|
+
* workflow run that produced it.
|
|
1505
|
+
*/
|
|
1506
|
+
readonly complete: (id: number, conclusion: (typeof CheckConclusion.literals)[number], output?: CheckRunOutput, options?: CompleteCheckRunOptions) => Effect.Effect<void, GitHubError, Repo>;
|
|
1149
1507
|
/**
|
|
1150
1508
|
* Run `use` inside a check run, concluding it however `use` exits.
|
|
1151
1509
|
*
|
|
@@ -1636,6 +1994,52 @@ export declare class GitCommit extends GitCommit_base {
|
|
|
1636
1994
|
static readonly layerTest: (overrides?: Partial<GitCommitShape>) => Layer.Layer<GitCommit>;
|
|
1637
1995
|
}
|
|
1638
1996
|
//#endregion
|
|
1997
|
+
//#region src/InstallationTokenStore.d.ts
|
|
1998
|
+
/**
|
|
1999
|
+
* The operations of an {@link InstallationTokenStore}.
|
|
2000
|
+
*
|
|
2001
|
+
* @public
|
|
2002
|
+
*/
|
|
2003
|
+
interface InstallationTokenStoreShape {
|
|
2004
|
+
/**
|
|
2005
|
+
* The encoded installation token stored for `installationId`, if one is held
|
|
2006
|
+
* and unexpired.
|
|
2007
|
+
*/
|
|
2008
|
+
readonly get: (installationId: number) => Effect.Effect<Option.Option<string>>;
|
|
2009
|
+
/** Hold `encoded` for `installationId` for `ttl`. */
|
|
2010
|
+
readonly set: (installationId: number, encoded: string, ttl: Duration.Duration) => Effect.Effect<void>;
|
|
2011
|
+
}
|
|
2012
|
+
declare const InstallationTokenStore_base: Context.ServiceClass<InstallationTokenStore, "@effected/github/InstallationTokenStore", InstallationTokenStoreShape>;
|
|
2013
|
+
/**
|
|
2014
|
+
* Where `GitHubApp.cachedToken` keeps installation tokens between request
|
|
2015
|
+
* scopes.
|
|
2016
|
+
*
|
|
2017
|
+
* @remarks
|
|
2018
|
+
* Entries are keyed by installation id, never by token text. The value is an
|
|
2019
|
+
* `InstallationToken` encoded as JSON, and it **contains the raw token**, so
|
|
2020
|
+
* encrypting it at rest is the store's job, not the caller's.
|
|
2021
|
+
*
|
|
2022
|
+
* {@link InstallationTokenStore.layerMemory} keeps tokens in process. A
|
|
2023
|
+
* Cloudflare Worker, whose isolates do not share memory, backs the store with
|
|
2024
|
+
* KV, a Durable Object or D1 instead. A store's own failures should be
|
|
2025
|
+
* swallowed by the store (a miss on `get`, a no-op on `set`): a cache must
|
|
2026
|
+
* never fail the call it serves. `cachedToken` also guards against a store
|
|
2027
|
+
* that fails or dies anyway; only interruption propagates.
|
|
2028
|
+
*
|
|
2029
|
+
* This module imports nothing but `effect`, so a store implementation never
|
|
2030
|
+
* links the JWT signer.
|
|
2031
|
+
*
|
|
2032
|
+
* @public
|
|
2033
|
+
*/
|
|
2034
|
+
export declare class InstallationTokenStore extends InstallationTokenStore_base {
|
|
2035
|
+
/** An in-process store over a `Ref`, expiring entries against `Clock`. */
|
|
2036
|
+
static readonly layerMemory: Layer.Layer<InstallationTokenStore>;
|
|
2037
|
+
/** An in-memory double; unstubbed members die naming themselves. */
|
|
2038
|
+
static readonly makeTest: (overrides?: Partial<InstallationTokenStoreShape>) => InstallationTokenStoreShape;
|
|
2039
|
+
/** {@link InstallationTokenStore.makeTest} behind a `Layer`. */
|
|
2040
|
+
static readonly layerTest: (overrides?: Partial<InstallationTokenStoreShape>) => Layer.Layer<InstallationTokenStore>;
|
|
2041
|
+
}
|
|
2042
|
+
//#endregion
|
|
1639
2043
|
//#region src/GitHubApp.d.ts
|
|
1640
2044
|
declare const GitHubAppError_base: Schema.Class<GitHubAppError, Schema.TaggedStruct<"GitHubAppError", {
|
|
1641
2045
|
/** Which step failed. */
|
|
@@ -1644,7 +2048,7 @@ declare const GitHubAppError_base: Schema.Class<GitHubAppError, Schema.TaggedStr
|
|
|
1644
2048
|
readonly reason: Schema.String;
|
|
1645
2049
|
/** The underlying failure, when there is one. */
|
|
1646
2050
|
readonly cause: Schema.optionalKey<Schema.Defect>;
|
|
1647
|
-
}>,
|
|
2051
|
+
}>, Cause.YieldableError>;
|
|
1648
2052
|
/**
|
|
1649
2053
|
* A GitHub App call failed.
|
|
1650
2054
|
*
|
|
@@ -1677,12 +2081,13 @@ interface AppCredentials {
|
|
|
1677
2081
|
* The app's private key, in PEM.
|
|
1678
2082
|
*
|
|
1679
2083
|
* @remarks
|
|
1680
|
-
* PKCS#1 (`-----BEGIN RSA PRIVATE KEY-----`, which is what github.com
|
|
1681
|
-
* you)
|
|
1682
|
-
*
|
|
1683
|
-
*
|
|
1684
|
-
*
|
|
1685
|
-
*
|
|
2084
|
+
* Both PKCS#1 (`-----BEGIN RSA PRIVATE KEY-----`, which is what github.com
|
|
2085
|
+
* hands you) and PKCS#8 (`-----BEGIN PRIVATE KEY-----`) are accepted on every
|
|
2086
|
+
* runtime with WebCrypto, Node and workerd alike: a PKCS#1 key is wrapped to
|
|
2087
|
+
* PKCS#8 in-process, so no conversion step is needed. Newlines may also
|
|
2088
|
+
* arrive escaped as the two characters backslash and `n`, the one-line form
|
|
2089
|
+
* an environment variable carries. The key must be RSA of at least 2048
|
|
2090
|
+
* bits; anything else fails with a `kind: "jwt"` error.
|
|
1686
2091
|
*/
|
|
1687
2092
|
readonly privateKey: Redacted.Redacted<string>;
|
|
1688
2093
|
}
|
|
@@ -1705,6 +2110,41 @@ interface TokenRequest extends AppCredentials {
|
|
|
1705
2110
|
*/
|
|
1706
2111
|
readonly owner?: string | undefined;
|
|
1707
2112
|
}
|
|
2113
|
+
/**
|
|
2114
|
+
* What to fetch a cached installation token for.
|
|
2115
|
+
*
|
|
2116
|
+
* @public
|
|
2117
|
+
*/
|
|
2118
|
+
interface CachedTokenRequest extends TokenRequest {
|
|
2119
|
+
/**
|
|
2120
|
+
* The installation. Required here: discovering it on every request would
|
|
2121
|
+
* cost a JWT mint and a paginated walk, which defeats the cache.
|
|
2122
|
+
*/
|
|
2123
|
+
readonly installationId: number;
|
|
2124
|
+
/**
|
|
2125
|
+
* How long before its expiry a stored token stops being served. Defaults to
|
|
2126
|
+
* five minutes; must be finite and not negative.
|
|
2127
|
+
*
|
|
2128
|
+
* @remarks
|
|
2129
|
+
* A served token has at least `margin` left to live unless GitHub issued it
|
|
2130
|
+
* with less than that, in which case it is returned once and not stored.
|
|
2131
|
+
* GitHub issues installation tokens for an hour, so a margin of an hour or
|
|
2132
|
+
* more guarantees that case: every call mints and nothing is cached.
|
|
2133
|
+
*/
|
|
2134
|
+
readonly margin?: Duration.Input | undefined;
|
|
2135
|
+
}
|
|
2136
|
+
/**
|
|
2137
|
+
* An installation token from {@link GitHubApp.cachedToken}, and where it came
|
|
2138
|
+
* from.
|
|
2139
|
+
*
|
|
2140
|
+
* @public
|
|
2141
|
+
*/
|
|
2142
|
+
interface CachedToken {
|
|
2143
|
+
/** The token. */
|
|
2144
|
+
readonly token: InstallationToken;
|
|
2145
|
+
/** `"cached"` when it was read from the store, `"minted"` when GitHub issued it just now. */
|
|
2146
|
+
readonly source: "cached" | "minted";
|
|
2147
|
+
}
|
|
1708
2148
|
declare const InstallationToken_base: Schema.Class<InstallationToken, Schema.Struct<{
|
|
1709
2149
|
/** The token. Decodes to `Redacted`, encodes back to the raw string. */
|
|
1710
2150
|
readonly token: Schema.RedactedFromValue<Schema.String>;
|
|
@@ -1806,12 +2246,36 @@ export declare class AppIdentity extends AppIdentity_base {
|
|
|
1806
2246
|
declare const Installation_base: Schema.Class<Installation, Schema.Struct<{
|
|
1807
2247
|
/** The installation id, which is what a token is minted against. */
|
|
1808
2248
|
readonly id: Schema.Int;
|
|
1809
|
-
/** The account the app is installed on, when GitHub reported one. */
|
|
2249
|
+
/** The account the app is installed on (its login), when GitHub reported one. */
|
|
1810
2250
|
readonly account: Schema.optionalKey<Schema.String>;
|
|
2251
|
+
/** The account kind, e.g. "Organization" or "User"; open-ended, because GitHub adds kinds. */
|
|
2252
|
+
readonly accountType: Schema.optionalKey<Schema.String>;
|
|
2253
|
+
/**
|
|
2254
|
+
* The account's numeric id.
|
|
2255
|
+
*
|
|
2256
|
+
* @remarks
|
|
2257
|
+
* Not paired with `account`: an enterprise installation carries
|
|
2258
|
+
* `accountId` (and may carry `accountType`) but has no login, so `account`
|
|
2259
|
+
* is absent.
|
|
2260
|
+
*/
|
|
2261
|
+
readonly accountId: Schema.optionalKey<Schema.Int>;
|
|
2262
|
+
/** When the installation was suspended; `Option.none()` when it is active. */
|
|
2263
|
+
readonly suspendedAt: Schema.optionalKey<Schema.OptionFromNullOr<Schema.DateTimeUtcFromString>>;
|
|
2264
|
+
/** When GitHub last changed the installation. */
|
|
2265
|
+
readonly updatedAt: Schema.optionalKey<Schema.DateTimeUtcFromString>;
|
|
1811
2266
|
}>, {}>;
|
|
1812
2267
|
/**
|
|
1813
2268
|
* One installation of the app.
|
|
1814
2269
|
*
|
|
2270
|
+
* @remarks
|
|
2271
|
+
* Every field but `id` is filled only when GitHub's response carries it and
|
|
2272
|
+
* it reads cleanly; an unreadable field is omitted rather than failing the
|
|
2273
|
+
* listing. A test double built with `Installation.make({ id })` stays valid.
|
|
2274
|
+
* An enterprise account carries `accountId` and `accountType` without a
|
|
2275
|
+
* login `account`. Encodable:
|
|
2276
|
+
* the dates encode to ISO strings and `suspendedAt` to an ISO string or
|
|
2277
|
+
* `null`, the same shape GitHub sends.
|
|
2278
|
+
*
|
|
1815
2279
|
* @public
|
|
1816
2280
|
*/
|
|
1817
2281
|
export declare class Installation extends Installation_base {}
|
|
@@ -1847,8 +2311,8 @@ declare const GitHubApp_base: Context.ServiceClass<GitHubApp, "@effected/github/
|
|
|
1847
2311
|
* already — `@effected/workspaces` ships `localExecLayer`, which builds
|
|
1848
2312
|
* `@effected/commands`' service, for the same reason.
|
|
1849
2313
|
*
|
|
1850
|
-
* The JWT signer is
|
|
1851
|
-
*
|
|
2314
|
+
* The JWT signer is `@effected/jwt` — WebCrypto RS256 with no runtime
|
|
2315
|
+
* dependencies, so App auth runs on any runtime that has `crypto.subtle`.
|
|
1852
2316
|
*
|
|
1853
2317
|
* @example
|
|
1854
2318
|
* ```ts
|
|
@@ -1904,6 +2368,128 @@ export declare class GitHubApp extends GitHubApp_base {
|
|
|
1904
2368
|
* would tax every caller for a case only this layer can produce.
|
|
1905
2369
|
*/
|
|
1906
2370
|
static readonly clientLayer: (request: TokenRequest, options?: GitHubAppOptions) => Layer.Layer<GitHubClient, GitHubAppError>;
|
|
2371
|
+
/**
|
|
2372
|
+
* A {@link GitHubClient} authenticated as the app itself, with an App JWT.
|
|
2373
|
+
*
|
|
2374
|
+
* @remarks
|
|
2375
|
+
* An App JWT authenticates only the app-level routes: `/app` and everything
|
|
2376
|
+
* under `/app/*` (the app's installations and their token mint, its webhook
|
|
2377
|
+
* configuration and deliveries), plus the three installation lookups that
|
|
2378
|
+
* require a JWT: `GET /repos/{owner}/{repo}/installation`,
|
|
2379
|
+
* `GET /orgs/{org}/installation` and `GET /users/{username}/installation`.
|
|
2380
|
+
* Installation-scoped routes (a repository's contents, issues, pulls and the
|
|
2381
|
+
* rest) answer 401 to it; reach those through {@link GitHubApp.clientLayer}
|
|
2382
|
+
* with an installation token instead.
|
|
2383
|
+
*
|
|
2384
|
+
* The motivating use is a webhook redelivery sweep, which lists recent
|
|
2385
|
+
* deliveries with `GET /app/hook/deliveries` and redelivers a failed one
|
|
2386
|
+
* with `POST /app/hook/deliveries/{delivery_id}/attempts`.
|
|
2387
|
+
*
|
|
2388
|
+
* The JWT is signed locally, never fetched, so building the layer makes no
|
|
2389
|
+
* request; a key that will not sign fails construction with
|
|
2390
|
+
* `GitHubAppError { kind: "jwt" }`. A JWT lives nine minutes and is
|
|
2391
|
+
* re-signed a minute before it expires, so a long-running sweep keeps
|
|
2392
|
+
* authenticating; calls in between reuse it. It cannot be revoked, so
|
|
2393
|
+
* release does nothing. A later signing failure surfaces to the caller as
|
|
2394
|
+
* `GitHubError { kind: "unauthorized" }` carrying the `GitHubAppError` as
|
|
2395
|
+
* its cause, as with {@link GitHubApp.clientLayer}.
|
|
2396
|
+
*
|
|
2397
|
+
* @example
|
|
2398
|
+
* ```ts
|
|
2399
|
+
* import { GitHubApp, GitHubClient } from "@effected/github";
|
|
2400
|
+
* import { Effect, Redacted } from "effect";
|
|
2401
|
+
*
|
|
2402
|
+
* const sweep = Effect.gen(function* () {
|
|
2403
|
+
* const client = yield* GitHubClient;
|
|
2404
|
+
* const deliveries = yield* client.request("GET /app/hook/deliveries", { per_page: 100 });
|
|
2405
|
+
* for (const delivery of deliveries) {
|
|
2406
|
+
* if (delivery.status_code >= 400) {
|
|
2407
|
+
* yield* client.request("POST /app/hook/deliveries/{delivery_id}/attempts", {
|
|
2408
|
+
* // octokit types a delivery id as number | bigint; the route takes a number.
|
|
2409
|
+
* delivery_id: Number(delivery.id),
|
|
2410
|
+
* });
|
|
2411
|
+
* }
|
|
2412
|
+
* }
|
|
2413
|
+
* });
|
|
2414
|
+
*
|
|
2415
|
+
* const layer = GitHubApp.appClientLayer({
|
|
2416
|
+
* appId: "12345",
|
|
2417
|
+
* privateKey: Redacted.make("-----BEGIN RSA PRIVATE KEY-----\n..."),
|
|
2418
|
+
* });
|
|
2419
|
+
*
|
|
2420
|
+
* Effect.runPromise(Effect.provide(sweep, layer));
|
|
2421
|
+
* ```
|
|
2422
|
+
*/
|
|
2423
|
+
static readonly appClientLayer: (credentials: AppCredentials, options?: GitHubAppOptions) => Layer.Layer<GitHubClient, GitHubAppError>;
|
|
2424
|
+
/**
|
|
2425
|
+
* An installation token, reused from an {@link InstallationTokenStore}
|
|
2426
|
+
* while it has `margin` (five minutes by default) left to live.
|
|
2427
|
+
*
|
|
2428
|
+
* @remarks
|
|
2429
|
+
* For a program that authenticates per request scope (a Worker handling a
|
|
2430
|
+
* webhook, say) and would otherwise mint a fresh token every time. The store
|
|
2431
|
+
* is keyed by installation id. A stored value that will not decode, or a
|
|
2432
|
+
* token within `margin` of expiry, is a miss: the token is minted with
|
|
2433
|
+
* {@link GitHubAppShape.token} and written back with a TTL of its expiry
|
|
2434
|
+
* minus `margin`. A token minted with less than `margin` to live is returned
|
|
2435
|
+
* but not stored.
|
|
2436
|
+
*
|
|
2437
|
+
* The store never fails this call: a `get` that fails or dies is a miss and
|
|
2438
|
+
* a `set` that fails or dies is ignored. Only interruption propagates.
|
|
2439
|
+
*
|
|
2440
|
+
* **A cached token is never revoked**: another scope or isolate may be using
|
|
2441
|
+
* it. It expires on its own, within the hour.
|
|
2442
|
+
*
|
|
2443
|
+
* Two concurrent calls that both miss will both mint, and the later write
|
|
2444
|
+
* wins. That is expected: the store may span isolates, so the kit cannot
|
|
2445
|
+
* lock it, and an extra token costs one call and expires on its own.
|
|
2446
|
+
*
|
|
2447
|
+
* The encoded value written to the store contains the raw token, so
|
|
2448
|
+
* encrypting it at rest is the store's job.
|
|
2449
|
+
*/
|
|
2450
|
+
static readonly cachedToken: (request: CachedTokenRequest) => Effect.Effect<CachedToken, GitHubAppError, GitHubApp | InstallationTokenStore>;
|
|
2451
|
+
/**
|
|
2452
|
+
* A {@link GitHubClient} authenticated with {@link GitHubApp.cachedToken}.
|
|
2453
|
+
*
|
|
2454
|
+
* @remarks
|
|
2455
|
+
* Built per request scope: provide it where the request is handled, over a
|
|
2456
|
+
* `GitHubApp` and an `InstallationTokenStore` provided once at the edge.
|
|
2457
|
+
* Unlike {@link GitHubApp.clientLayer} it **never revokes** the token, not
|
|
2458
|
+
* even on release, because the token is shared through the store, and it
|
|
2459
|
+
* does not rotate, so set `margin` longer than the scope's work can take;
|
|
2460
|
+
* the token is good for at least `margin` unless GitHub issued it with less
|
|
2461
|
+
* (see {@link CachedTokenRequest.margin}).
|
|
2462
|
+
*
|
|
2463
|
+
* `options` configures this client's transport only, not the mint: the
|
|
2464
|
+
* token is minted by whichever `GitHubApp` the edge provides. On GitHub
|
|
2465
|
+
* Enterprise, build that with the same API root, for example
|
|
2466
|
+
* `GitHubApp.layerWith({ baseUrl })`, or the mint goes to github.com.
|
|
2467
|
+
*
|
|
2468
|
+
* @example
|
|
2469
|
+
* ```ts
|
|
2470
|
+
* import { GitHubApp, GitHubClient, InstallationTokenStore } from "@effected/github";
|
|
2471
|
+
* import { Effect, Layer, Redacted } from "effect";
|
|
2472
|
+
*
|
|
2473
|
+
* const handle = (installationId: number) =>
|
|
2474
|
+
* Effect.flatMap(GitHubClient, (client) =>
|
|
2475
|
+
* client.request("GET /repos/{owner}/{repo}", { owner: "acme", repo: "widgets" }),
|
|
2476
|
+
* ).pipe(
|
|
2477
|
+
* Effect.provide(
|
|
2478
|
+
* GitHubApp.cachedClientLayer({
|
|
2479
|
+
* appId: "12345",
|
|
2480
|
+
* privateKey: Redacted.make("-----BEGIN RSA PRIVATE KEY-----\n..."),
|
|
2481
|
+
* installationId,
|
|
2482
|
+
* }),
|
|
2483
|
+
* ),
|
|
2484
|
+
* );
|
|
2485
|
+
*
|
|
2486
|
+
* // Once, at the edge. On GitHub Enterprise, use GitHubApp.layerWith({ baseUrl }).
|
|
2487
|
+
* const Live = Layer.mergeAll(GitHubApp.layer, InstallationTokenStore.layerMemory);
|
|
2488
|
+
*
|
|
2489
|
+
* Effect.runPromise(Effect.provide(handle(42), Live));
|
|
2490
|
+
* ```
|
|
2491
|
+
*/
|
|
2492
|
+
static readonly cachedClientLayer: (request: CachedTokenRequest, options?: GitHubAppOptions) => Layer.Layer<GitHubClient, GitHubAppError, GitHubApp | InstallationTokenStore>;
|
|
1907
2493
|
/** An in-memory double; unstubbed members die naming themselves. */
|
|
1908
2494
|
static readonly makeTest: (overrides?: Partial<GitHubAppShape>) => GitHubAppShape;
|
|
1909
2495
|
/** {@link GitHubApp.makeTest} behind a `Layer`. */
|
|
@@ -2105,6 +2691,12 @@ interface GitHubContentShape {
|
|
|
2105
2691
|
* Fails `notFound` when the path does not exist, and `rejected` when it is a
|
|
2106
2692
|
* directory, is not a regular file, or is too large for the contents API
|
|
2107
2693
|
* (which answers an empty body above roughly a megabyte).
|
|
2694
|
+
*
|
|
2695
|
+
* Fails `decode` when the payload is not valid base64. Only **standard,
|
|
2696
|
+
* padded** base64 is accepted, which is what the contents API sends; the
|
|
2697
|
+
* line breaks it wraps the payload with are ignored. Unpadded or URL-safe
|
|
2698
|
+
* (`-`/`_`) input is rejected rather than guessed at. The bytes decode as
|
|
2699
|
+
* UTF-8, keeping a leading BOM as content.
|
|
2108
2700
|
*/
|
|
2109
2701
|
readonly getFile: (path: string, options?: {
|
|
2110
2702
|
readonly ref?: string | undefined;
|
|
@@ -3661,6 +4253,25 @@ export declare class WorkflowRunStatus extends WorkflowRunStatus_base {
|
|
|
3661
4253
|
/** Has the run finished, whatever the outcome? */
|
|
3662
4254
|
get isDone(): boolean;
|
|
3663
4255
|
}
|
|
4256
|
+
declare const DispatchedRun_base: Schema.Class<DispatchedRun, Schema.Struct<{
|
|
4257
|
+
/** The created run's numeric id. */
|
|
4258
|
+
readonly runId: Schema.Int;
|
|
4259
|
+
/** The run's API URL. */
|
|
4260
|
+
readonly runUrl: Schema.String;
|
|
4261
|
+
/** The run's web URL. */
|
|
4262
|
+
readonly htmlUrl: Schema.String;
|
|
4263
|
+
}>, {}>;
|
|
4264
|
+
/**
|
|
4265
|
+
* The run a `workflow_dispatch` created, as GitHub reports it when asked.
|
|
4266
|
+
*
|
|
4267
|
+
* @remarks
|
|
4268
|
+
* Returned by {@link WorkflowDispatchShape.dispatchWithRun}. `runId` is usable
|
|
4269
|
+
* directly with {@link WorkflowDispatchShape.runStatus} and
|
|
4270
|
+
* {@link WorkflowDispatchShape.cancelRun}.
|
|
4271
|
+
*
|
|
4272
|
+
* @public
|
|
4273
|
+
*/
|
|
4274
|
+
export declare class DispatchedRun extends DispatchedRun_base {}
|
|
3664
4275
|
/**
|
|
3665
4276
|
* One workflow defined in the repository.
|
|
3666
4277
|
*
|
|
@@ -3701,10 +4312,47 @@ interface PollOptions {
|
|
|
3701
4312
|
* @public
|
|
3702
4313
|
*/
|
|
3703
4314
|
interface WorkflowDispatchShape {
|
|
3704
|
-
/**
|
|
4315
|
+
/**
|
|
4316
|
+
* Fire a `workflow_dispatch` event and discard GitHub's answer; use
|
|
4317
|
+
* {@link WorkflowDispatchShape.dispatchWithRun} when the run id is needed.
|
|
4318
|
+
*/
|
|
3705
4319
|
readonly dispatch: (workflow: string, ref: string, inputs?: Record<string, string>) => Effect.Effect<void, GitHubError, Repo>;
|
|
4320
|
+
/**
|
|
4321
|
+
* Fire a `workflow_dispatch` event and report the run it created.
|
|
4322
|
+
*
|
|
4323
|
+
* @remarks
|
|
4324
|
+
* Sends the same request as {@link WorkflowDispatchShape.dispatch} with
|
|
4325
|
+
* `return_run_details: true`. GitHub then answers 200 with the run's id and
|
|
4326
|
+
* URLs, which arrive as `Option.some` of a {@link DispatchedRun}; a 200 body
|
|
4327
|
+
* that does not have that shape fails with a `decode` `GitHubError`.
|
|
4328
|
+
*
|
|
4329
|
+
* A 204 with no body is `Option.none()`. That is what a GitHub Enterprise
|
|
4330
|
+
* Server predating the field answers — it ignores `return_run_details` and
|
|
4331
|
+
* still dispatches — so it is **not a failure**: the workflow was dispatched,
|
|
4332
|
+
* the server just did not say which run it created.
|
|
4333
|
+
*
|
|
4334
|
+
* Under `GitHubClient.layerFixture`, stub the 204 the way every other
|
|
4335
|
+
* no-body route is stubbed: answer the dispatch route with `null`. `""`
|
|
4336
|
+
* (what octokit itself hands back for an empty body) works too.
|
|
4337
|
+
*
|
|
4338
|
+
* Every failure of the request itself (a 404 for an unknown workflow, a 422
|
|
4339
|
+
* for a workflow without a `workflow_dispatch` trigger, …) is the
|
|
4340
|
+
* `GitHubError` the client classified, passed through unchanged.
|
|
4341
|
+
*/
|
|
4342
|
+
readonly dispatchWithRun: (workflow: string, ref: string, inputs?: Record<string, string>) => Effect.Effect<Option.Option<DispatchedRun>, GitHubError, Repo>;
|
|
3706
4343
|
/** Read one workflow run's status. */
|
|
3707
4344
|
readonly runStatus: (runId: number) => Effect.Effect<WorkflowRunStatus, GitHubError, Repo>;
|
|
4345
|
+
/**
|
|
4346
|
+
* Cancel a workflow run.
|
|
4347
|
+
*
|
|
4348
|
+
* @remarks
|
|
4349
|
+
* `"cancelled"` when GitHub accepted the cancellation (202; the run stops
|
|
4350
|
+
* shortly after, not necessarily before this returns). GitHub answers 409
|
|
4351
|
+
* when the run has already finished, which is `"alreadyCompleted"` rather
|
|
4352
|
+
* than a failure: the run is not running either way. Any other failure,
|
|
4353
|
+
* such as a 404 for an unknown run, is a `GitHubError`.
|
|
4354
|
+
*/
|
|
4355
|
+
readonly cancelRun: (runId: number) => Effect.Effect<"cancelled" | "alreadyCompleted", GitHubError, Repo>;
|
|
3708
4356
|
/**
|
|
3709
4357
|
* Every workflow defined in the repository.
|
|
3710
4358
|
*
|
|
@@ -3727,8 +4375,15 @@ interface WorkflowDispatchShape {
|
|
|
3727
4375
|
* The wait is `Effect.repeat` with a predicate over the **success** value, so
|
|
3728
4376
|
* "not finished yet" is never an error. If the run is not found finished
|
|
3729
4377
|
* within `poll.timeout`, it fails with a `rejected` `GitHubError` (status
|
|
3730
|
-
* 408).
|
|
3731
|
-
*
|
|
4378
|
+
* 408).
|
|
4379
|
+
*
|
|
4380
|
+
* The run is identified by {@link WorkflowDispatchShape.dispatchWithRun}:
|
|
4381
|
+
* when GitHub reports the run it created, exactly that run is polled, so
|
|
4382
|
+
* concurrent dispatches cannot be confused. When it does not (a 204 from a
|
|
4383
|
+
* GitHub Enterprise Server predating `return_run_details`), the run is found
|
|
4384
|
+
* instead by branch, creation time and workflow path — and on that fallback
|
|
4385
|
+
* alone, concurrent dispatches of the same workflow on the same ref can be
|
|
4386
|
+
* confused.
|
|
3732
4387
|
*/
|
|
3733
4388
|
readonly dispatchAndWait: (workflow: string, ref: string, options?: {
|
|
3734
4389
|
readonly inputs?: Record<string, string> | undefined;
|
|
@@ -3771,5 +4426,5 @@ export declare class WorkflowDispatch extends WorkflowDispatch_base {
|
|
|
3771
4426
|
static readonly layerTest: (overrides?: Partial<WorkflowDispatchShape>) => Layer.Layer<WorkflowDispatch>;
|
|
3772
4427
|
}
|
|
3773
4428
|
//#endregion
|
|
3774
|
-
export { type AppCredentials, type AppliedSettings, type ArtifactMetadataShape, type AttestationShape, type BareLineReference, type BranchOutcome, CLOSING_KEYWORDS, type CheckRunShape, type ClosingKeyword, type CodeScanningSetup, type CodeScanningShape, type ConcludeCheckRun, type DeploymentEnvironmentInfo, type DeploymentEnvironmentShape, type GitBranchShape, type GitCommitShape, type GitHubAppOptions, type GitHubAppShape, type GitHubClientOptions, type GitHubClientShape, type GitHubCommitShape, type GitHubContentShape, type
|
|
4429
|
+
export { type ActionsOidcVerifyOptions, type AppCredentials, type AppliedSettings, type ArtifactMetadataShape, type AttestationShape, type BareLineReference, type BranchOutcome, CLOSING_KEYWORDS, type CachedToken, type CachedTokenRequest, type CheckRunShape, type ClosingKeyword, type CodeScanningSetup, type CodeScanningShape, type CompleteCheckRunOptions, type ConcludeCheckRun, type CreateCheckRunOptions, type DeploymentEnvironmentInfo, type DeploymentEnvironmentShape, type GitBranchShape, type GitCommitShape, type GitHubAppOptions, type GitHubAppShape, type GitHubClientOptions, type GitHubClientShape, type GitHubCommitShape, type GitHubContentShape, type GitHubIssueShape, type GitHubReleaseShape, type GitHubRepositoryShape, type GitTagShape, type InstallationTokenStoreShape, type IssueReference, type LatestSemverOptions, type OwnerType, type PollOptions, type PullRequestCommentShape, type PullRequestShape, type RecordedCall, type RepositoryPatch, type RepositoryPatchDraft, type RepositorySecretShape, type RepositorySecurityShape, type RepositorySettings, type RepositoryVariableShape, type Data as RestData, type RequestExtras as RestExtras, type Item as RestItem, type PaginatingRoute as RestPaginatingRoute, type Params as RestParams, type Response as RestResponse, type Route as RestRoute, type RetryableFailure, type RulesetInfo, type RulesetPayload, type RulesetShape, type SecretInfo, type SecretScope, type TokenRequest, type UpdateCheckRunOptions, type UpsertedPullRequest, type VariableInfo, type VersionFromTag, type WorkflowDispatchShape, type WorkflowInfo, harvestIssueReferences, parseBareLineReference };
|
|
3775
4430
|
//# sourceMappingURL=index.d.ts.map
|