@effected/github 0.1.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/ArtifactMetadata.js +62 -0
- package/Attestation.js +106 -0
- package/CheckRun.js +286 -0
- package/GitBranch.js +171 -0
- package/GitCommit.js +183 -0
- package/GitHubApp.js +363 -0
- package/GitHubClient.js +192 -0
- package/GitHubCommit.js +198 -0
- package/GitHubContent.js +54 -0
- package/GitHubError.js +254 -0
- package/GitHubIssue.js +216 -0
- package/GitHubRelease.js +188 -0
- package/GitHubRepository.js +59 -0
- package/GitTag.js +191 -0
- package/GraphQL.js +183 -0
- package/LICENSE +21 -0
- package/PullRequest.js +300 -0
- package/PullRequestComment.js +132 -0
- package/Repo.js +107 -0
- package/Resilience.js +146 -0
- package/Rest.js +47 -0
- package/TokenPermissions.js +188 -0
- package/WorkflowDispatch.js +124 -0
- package/index.d.ts +2427 -0
- package/index.js +24 -0
- package/internal/headers.js +52 -0
- package/internal/octokit.js +115 -0
- package/internal/paginate.js +54 -0
- package/package.json +53 -0
- package/tsdoc-metadata.json +11 -0
package/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { GitHubError, GitHubErrorKind } from "./GitHubError.js";
|
|
2
|
+
import { GitHubGraphQLError, GraphQLDocument, GraphQLErrorEntry } from "./GraphQL.js";
|
|
3
|
+
import { RateLimitSnapshot, RetryPolicy } from "./Resilience.js";
|
|
4
|
+
import { GitHubClient } from "./GitHubClient.js";
|
|
5
|
+
import { ArtifactMetadata, StorageRecordInput } from "./ArtifactMetadata.js";
|
|
6
|
+
import { InvalidRepoRefError, Repo, RepoRef } from "./Repo.js";
|
|
7
|
+
import { Attestation, AttestationListEntry, AttestationRecord } from "./Attestation.js";
|
|
8
|
+
import { Annotation, AnnotationLevel, CheckConclusion, CheckRun, CheckRunOutput, CheckRunRef } from "./CheckRun.js";
|
|
9
|
+
import { GitBranch } from "./GitBranch.js";
|
|
10
|
+
import { CommitRef, FileChange, FileContent, FileDeletion, FileMode, GitCommit } from "./GitCommit.js";
|
|
11
|
+
import { AppIdentity, BotIdentity, GitHubApp, GitHubAppError, Installation, InstallationToken } from "./GitHubApp.js";
|
|
12
|
+
import { CommitComparison, CommitFile, CommitSummary, FileStatus, GitHubCommit } from "./GitHubCommit.js";
|
|
13
|
+
import { GitHubContent } from "./GitHubContent.js";
|
|
14
|
+
import { GitHubIssue, IssueInfo, LinkedIssue } from "./GitHubIssue.js";
|
|
15
|
+
import { GitHubRelease, ReleaseAsset, ReleaseInfo } from "./GitHubRelease.js";
|
|
16
|
+
import { GitHubRepository } from "./GitHubRepository.js";
|
|
17
|
+
import { GitTag, SemverTag, TagRef, versionFromTag } from "./GitTag.js";
|
|
18
|
+
import { PageOptions } from "./Rest.js";
|
|
19
|
+
import { MergeMethod, PullRequest, PullRequestInfo } from "./PullRequest.js";
|
|
20
|
+
import { CommentMarker, CommentRecord, PullRequestComment } from "./PullRequestComment.js";
|
|
21
|
+
import { ExtraPermission, PermissionGap, PermissionLevel, PermissionResult, TokenPermissionError, TokenPermissions } from "./TokenPermissions.js";
|
|
22
|
+
import { WorkflowDispatch, WorkflowRunStatus } from "./WorkflowDispatch.js";
|
|
23
|
+
|
|
24
|
+
export { Annotation, AnnotationLevel, AppIdentity, ArtifactMetadata, Attestation, AttestationListEntry, AttestationRecord, BotIdentity, CheckConclusion, CheckRun, CheckRunOutput, CheckRunRef, CommentMarker, CommentRecord, CommitComparison, CommitFile, CommitRef, CommitSummary, ExtraPermission, FileChange, FileContent, FileDeletion, FileMode, FileStatus, GitBranch, GitCommit, GitHubApp, GitHubAppError, GitHubClient, GitHubCommit, GitHubContent, GitHubError, GitHubErrorKind, GitHubGraphQLError, GitHubIssue, GitHubRelease, GitHubRepository, GitTag, GraphQLDocument, GraphQLErrorEntry, Installation, InstallationToken, InvalidRepoRefError, IssueInfo, LinkedIssue, MergeMethod, PageOptions, PermissionGap, PermissionLevel, PermissionResult, PullRequest, PullRequestComment, PullRequestInfo, RateLimitSnapshot, ReleaseAsset, ReleaseInfo, Repo, RepoRef, RetryPolicy, SemverTag, StorageRecordInput, TagRef, TokenPermissionError, TokenPermissions, WorkflowDispatch, WorkflowRunStatus, versionFromTag };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region src/internal/headers.ts
|
|
2
|
+
/**
|
|
3
|
+
* Reading GitHub's response headers.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* octokit hands headers back as a plain object whose values may be `string`,
|
|
7
|
+
* `number` or absent depending on the fetch implementation, so every read here
|
|
8
|
+
* is defensive about both. Header names are already lowercased by octokit.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
/** A header's value as a string, when it is present and non-empty. */
|
|
13
|
+
const headerString = (headers, name) => {
|
|
14
|
+
const value = headers?.[name];
|
|
15
|
+
if (typeof value === "string") return value.length > 0 ? value : void 0;
|
|
16
|
+
if (typeof value === "number") return String(value);
|
|
17
|
+
};
|
|
18
|
+
/** A header's value as a finite integer, when it parses as one. */
|
|
19
|
+
const headerNumber = (headers, name) => {
|
|
20
|
+
const raw = headerString(headers, name);
|
|
21
|
+
if (raw === void 0) return void 0;
|
|
22
|
+
const parsed = Number(raw);
|
|
23
|
+
return Number.isFinite(parsed) ? Math.trunc(parsed) : void 0;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* How long GitHub asked us to wait, in milliseconds, or `undefined` when it
|
|
27
|
+
* did not ask.
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* Two mechanisms, checked in GitHub's own order of specificity:
|
|
31
|
+
*
|
|
32
|
+
* 1. `retry-after` — whole seconds, sent for secondary rate limits and abuse
|
|
33
|
+
* detection. Authoritative when present.
|
|
34
|
+
* 2. `x-ratelimit-remaining: 0` plus `x-ratelimit-reset` — the primary rate
|
|
35
|
+
* limit, where the reset is an absolute epoch **second**. Only meaningful
|
|
36
|
+
* when the budget is actually exhausted: every successful response carries a
|
|
37
|
+
* reset header too, and treating that as a delay would make every call look
|
|
38
|
+
* rate-limited.
|
|
39
|
+
*
|
|
40
|
+
* `nowMillis` is a parameter rather than a `Date.now()` read so this stays pure
|
|
41
|
+
* and so a test can pin it.
|
|
42
|
+
*/
|
|
43
|
+
const retryAfterMillisFrom = (headers, nowMillis) => {
|
|
44
|
+
const retryAfterSeconds = headerNumber(headers, "retry-after");
|
|
45
|
+
if (retryAfterSeconds !== void 0 && retryAfterSeconds >= 0) return retryAfterSeconds * 1e3;
|
|
46
|
+
const remaining = headerNumber(headers, "x-ratelimit-remaining");
|
|
47
|
+
const reset = headerNumber(headers, "x-ratelimit-reset");
|
|
48
|
+
if (remaining !== void 0 && remaining <= 0 && reset !== void 0) return Math.max(0, reset * 1e3 - nowMillis);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { headerNumber, headerString, retryAfterMillisFrom };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { GitHubError, readRateLimitHeaders } from "../GitHubError.js";
|
|
2
|
+
import { GitHubGraphQLError } from "../GraphQL.js";
|
|
3
|
+
import { RateLimitSnapshot } from "../Resilience.js";
|
|
4
|
+
import { Clock, Effect, Option, Redacted, Ref } from "effect";
|
|
5
|
+
import { Octokit } from "@octokit/core";
|
|
6
|
+
import { composePaginateRest } from "@octokit/plugin-paginate-rest";
|
|
7
|
+
|
|
8
|
+
//#region src/internal/octokit.ts
|
|
9
|
+
const SILENT_LOG = {
|
|
10
|
+
debug: () => {},
|
|
11
|
+
info: () => {},
|
|
12
|
+
warn: () => {},
|
|
13
|
+
error: () => {}
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Build the octokit instance.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* `auth` takes the raw token string. `@octokit/auth-token` inspects it and
|
|
20
|
+
* emits `bearer` for a three-segment JWT and `token` otherwise — exactly the
|
|
21
|
+
* distinction an app JWT and an installation token need, so the App path
|
|
22
|
+
* requires no separate auth strategy.
|
|
23
|
+
*
|
|
24
|
+
* `log` is silenced. The package this replaces installed
|
|
25
|
+
* `plugin-request-log` and then rerouted every line of it into an Actions
|
|
26
|
+
* `::debug::` workflow command, which is how GitHub Actions knowledge ended up
|
|
27
|
+
* inside a GitHub API client. Here retries log through `Effect.logDebug`, and an
|
|
28
|
+
* application maps Effect's logs to whatever its runtime wants.
|
|
29
|
+
*/
|
|
30
|
+
const makeOctokit = (options) => new Octokit({
|
|
31
|
+
...options.token !== void 0 ? { auth: Redacted.value(options.token) } : {},
|
|
32
|
+
...options.baseUrl !== void 0 ? { baseUrl: options.baseUrl } : {},
|
|
33
|
+
...options.userAgent !== void 0 ? { userAgent: options.userAgent } : {},
|
|
34
|
+
...options.fetch !== void 0 ? { request: { fetch: options.fetch } } : {},
|
|
35
|
+
log: SILENT_LOG
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* Build a transport over a freshly constructed octokit instance.
|
|
39
|
+
*
|
|
40
|
+
* @remarks
|
|
41
|
+
* The rate-limit cell lives here, in the closure of the layer that writes it.
|
|
42
|
+
* The package this replaces made it an optional shared `Ref` **service** that
|
|
43
|
+
* the writer and the reader both resolved through `Effect.serviceOption`, so an
|
|
44
|
+
* application that forgot to provide it got two private cells, a limiter that
|
|
45
|
+
* never saw the client's headers, and no error, warning or type signal saying
|
|
46
|
+
* so.
|
|
47
|
+
*/
|
|
48
|
+
const makeTransport = (options) => Effect.gen(function* () {
|
|
49
|
+
const octokit = makeOctokit(options);
|
|
50
|
+
const snapshot = yield* Ref.make(Option.none());
|
|
51
|
+
const policy = options.retry;
|
|
52
|
+
const record = (headers) => {
|
|
53
|
+
const parsed = readRateLimitHeaders(headers);
|
|
54
|
+
return parsed === void 0 ? Effect.void : Ref.set(snapshot, Option.some(RateLimitSnapshot.make(parsed)));
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Run one promise-producing call, classifying whatever it throws.
|
|
58
|
+
*
|
|
59
|
+
* @remarks
|
|
60
|
+
* The `AbortSignal` `Effect.tryPromise` supplies is threaded into octokit's
|
|
61
|
+
* `request.signal`, so interrupting the fiber aborts the in-flight HTTP
|
|
62
|
+
* request rather than leaving it running unobserved.
|
|
63
|
+
*/
|
|
64
|
+
const attempt = (call, classify) => Effect.tryPromise({
|
|
65
|
+
try: call,
|
|
66
|
+
catch: (error) => error
|
|
67
|
+
}).pipe(Effect.catch((error) => Effect.gen(function* () {
|
|
68
|
+
const now = yield* Clock.currentTimeMillis;
|
|
69
|
+
yield* record(readThrownHeaders(error));
|
|
70
|
+
return yield* Effect.fail(classify(error, now));
|
|
71
|
+
})));
|
|
72
|
+
const withRetry = (operation, effect) => effect.pipe(Effect.tapError((error) => policy.retries(error) ? Effect.logDebug("github.retry").pipe(Effect.annotateLogs({ operation })) : Effect.void), Effect.retry(policy.schedule()));
|
|
73
|
+
const request = (operation, route, params) => withRetry(operation, attempt((signal) => octokit.request(route, withSignal(params, signal)), (error, now) => GitHubError.fromOctokit(operation, error, now)).pipe(Effect.tap((response) => record(response.headers))));
|
|
74
|
+
const pageSource = (operation, route, params) => {
|
|
75
|
+
const iterator = composePaginateRest.iterator(octokit, route, params)[Symbol.asyncIterator]();
|
|
76
|
+
let finished = false;
|
|
77
|
+
return { next: Effect.suspend(() => finished ? Effect.succeed(Option.none()) : withRetry(operation, attempt(() => iterator.next(), (error, now) => GitHubError.fromOctokit(operation, error, now)).pipe(Effect.flatMap((result) => {
|
|
78
|
+
if (result.done === true || result.value === void 0) {
|
|
79
|
+
finished = true;
|
|
80
|
+
return Effect.succeed(Option.none());
|
|
81
|
+
}
|
|
82
|
+
const response = result.value;
|
|
83
|
+
return record(response.headers).pipe(Effect.as(Option.some(response.data)));
|
|
84
|
+
})))) };
|
|
85
|
+
};
|
|
86
|
+
const graphql = (operation, document, variables) => withRetry(operation, attempt(() => octokit.graphql(document, variables), (error, now) => GitHubGraphQLError.fromThrowable(operation, error, now)));
|
|
87
|
+
return {
|
|
88
|
+
request,
|
|
89
|
+
pageSource,
|
|
90
|
+
graphql,
|
|
91
|
+
rateLimit: Ref.get(snapshot)
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
/** Merge our abort signal into octokit's per-request options without clobbering them. */
|
|
95
|
+
const withSignal = (params, signal) => {
|
|
96
|
+
const existing = typeof params.request === "object" && params.request !== null ? params.request : {};
|
|
97
|
+
return {
|
|
98
|
+
...params,
|
|
99
|
+
request: {
|
|
100
|
+
...existing,
|
|
101
|
+
signal
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
/** Response headers off a throwable, when it carried any. */
|
|
106
|
+
const readThrownHeaders = (error) => {
|
|
107
|
+
if (typeof error !== "object" || error === null) return void 0;
|
|
108
|
+
const response = error.response;
|
|
109
|
+
if (typeof response !== "object" || response === null) return void 0;
|
|
110
|
+
const headers = response.headers;
|
|
111
|
+
return typeof headers === "object" && headers !== null ? headers : void 0;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
export { makeTransport };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Effect, Option, Stream } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/paginate.ts
|
|
4
|
+
/**
|
|
5
|
+
* Walk a {@link PageSource} into a stream of its items.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* **This is the only pagination implementation in the package**, and it is why
|
|
9
|
+
* the fixture double cannot drift from the live client: both build a
|
|
10
|
+
* `PageSource` and hand it here, so `maxPages` and item flattening have exactly
|
|
11
|
+
* one behavior. The package this replaces had a live loop that honored
|
|
12
|
+
* `maxPages` and a test double whose equivalent parameters were named
|
|
13
|
+
* `_options` and ignored — which made every truncation path structurally
|
|
14
|
+
* untestable.
|
|
15
|
+
*
|
|
16
|
+
* `maxPages` bounds **requests, not items**: the walk stops issuing them rather
|
|
17
|
+
* than fetching everything and slicing.
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
const paginate = (openSource, maxPages) => Stream.suspend(() => {
|
|
22
|
+
const source = openSource();
|
|
23
|
+
return Stream.paginate(0, (pagesTaken) => Effect.map(source.next, (page) => {
|
|
24
|
+
if (Option.isNone(page)) return [[], Option.none()];
|
|
25
|
+
const taken = pagesTaken + 1;
|
|
26
|
+
const exhausted = maxPages !== void 0 && taken >= maxPages;
|
|
27
|
+
return [page.value, exhausted ? Option.none() : Option.some(taken)];
|
|
28
|
+
}));
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* A {@link PageSource} over an already-collected array, sliced into pages.
|
|
32
|
+
*
|
|
33
|
+
* @remarks
|
|
34
|
+
* What the fixture double records. Slicing here rather than inside the double
|
|
35
|
+
* keeps "what is a page" in one place: a recorded fixture of 250 items with
|
|
36
|
+
* `perPage: 100` pages exactly as GitHub would, so a test can assert a caller's
|
|
37
|
+
* `maxPages` truncation against real page boundaries.
|
|
38
|
+
*
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
const fromArray = (items, perPage) => {
|
|
42
|
+
let offset = 0;
|
|
43
|
+
let finished = false;
|
|
44
|
+
return { next: Effect.sync(() => {
|
|
45
|
+
if (finished) return Option.none();
|
|
46
|
+
const page = items.slice(offset, offset + perPage);
|
|
47
|
+
offset += perPage;
|
|
48
|
+
if (page.length < perPage) finished = true;
|
|
49
|
+
return page.length === 0 ? Option.none() : Option.some(page);
|
|
50
|
+
}) };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
54
|
+
export { fromArray, paginate };
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@effected/github",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Typed GitHub REST and GraphQL services over the octokit core request surface, with app auth and resource helpers.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"github",
|
|
8
|
+
"octokit",
|
|
9
|
+
"rest",
|
|
10
|
+
"graphql",
|
|
11
|
+
"app-auth",
|
|
12
|
+
"effect",
|
|
13
|
+
"effected"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/spencerbeggs/effected/tree/main/packages/github#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/spencerbeggs/effected/issues"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/spencerbeggs/effected.git",
|
|
22
|
+
"directory": "packages/github"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": {
|
|
26
|
+
"name": "C. Spencer Beggs",
|
|
27
|
+
"email": "spencer@beggs.codes",
|
|
28
|
+
"url": "https://spencerbeg.gs"
|
|
29
|
+
},
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"type": "module",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./index.d.ts",
|
|
35
|
+
"import": "./index.js",
|
|
36
|
+
"default": "./index.js"
|
|
37
|
+
},
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@effected/semver": "~0.2.1",
|
|
42
|
+
"@octokit/core": "^7.0.6",
|
|
43
|
+
"@octokit/plugin-paginate-rest": "^14.0.0",
|
|
44
|
+
"@octokit/types": "^16.0.0",
|
|
45
|
+
"universal-github-app-jwt": "^2.2.2"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"effect": "4.0.0-beta.101"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=24.11.0"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// This file is read by tools that parse documentation comments conforming to the TSDoc standard.
|
|
2
|
+
// It should be published with your NPM package. It should not be tracked by Git.
|
|
3
|
+
{
|
|
4
|
+
"tsdocVersion": "0.12",
|
|
5
|
+
"toolPackages": [
|
|
6
|
+
{
|
|
7
|
+
"packageName": "@microsoft/api-extractor",
|
|
8
|
+
"packageVersion": "7.58.12"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|