@shivaedev/github 0.9.0 → 0.11.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/CHANGELOG.md +36 -0
- package/README.md +115 -7
- package/dist/GitHub.d.ts +279 -19
- package/dist/GitHub.d.ts.map +1 -1
- package/dist/GitHub.js +22 -10
- package/dist/GitHub.js.map +1 -1
- package/dist/adapters/manualRedirects.d.ts +4 -0
- package/dist/adapters/manualRedirects.d.ts.map +1 -0
- package/dist/adapters/manualRedirects.js +6 -0
- package/dist/adapters/manualRedirects.js.map +1 -0
- package/dist/adapters/mergeResponseHeaders.d.ts +3 -0
- package/dist/adapters/mergeResponseHeaders.d.ts.map +1 -0
- package/dist/adapters/mergeResponseHeaders.js +5 -0
- package/dist/adapters/mergeResponseHeaders.js.map +1 -0
- package/dist/adapters/resolveApiUrl.d.ts +2 -0
- package/dist/adapters/resolveApiUrl.d.ts.map +1 -0
- package/dist/adapters/resolveApiUrl.js +6 -0
- package/dist/adapters/resolveApiUrl.js.map +1 -0
- package/dist/adapters/signAppJwt.d.ts +5 -0
- package/dist/adapters/signAppJwt.d.ts.map +1 -0
- package/dist/adapters/signAppJwt.js +15 -0
- package/dist/adapters/signAppJwt.js.map +1 -0
- package/dist/adapters/verifySignature.d.ts +4 -0
- package/dist/adapters/verifySignature.d.ts.map +1 -0
- package/dist/adapters/verifySignature.js +13 -0
- package/dist/adapters/verifySignature.js.map +1 -0
- package/dist/adapters/zipEntries.d.ts +17 -0
- package/dist/adapters/zipEntries.d.ts.map +1 -0
- package/dist/adapters/zipEntries.js +57 -0
- package/dist/adapters/zipEntries.js.map +1 -0
- package/dist/appJwt.d.ts +2 -3
- package/dist/appJwt.d.ts.map +1 -1
- package/dist/appJwt.js +3 -12
- package/dist/appJwt.js.map +1 -1
- package/dist/appTokens.d.ts.map +1 -1
- package/dist/appTokens.js +2 -1
- package/dist/appTokens.js.map +1 -1
- package/dist/artifactErrors.d.ts +34 -0
- package/dist/artifactErrors.d.ts.map +1 -0
- package/dist/artifactErrors.js +27 -0
- package/dist/artifactErrors.js.map +1 -0
- package/dist/artifactExtraction.d.ts +12 -0
- package/dist/artifactExtraction.d.ts.map +1 -0
- package/dist/artifactExtraction.js +40 -0
- package/dist/artifactExtraction.js.map +1 -0
- package/dist/artifactPaths.d.ts +4 -0
- package/dist/artifactPaths.d.ts.map +1 -0
- package/dist/artifactPaths.js +35 -0
- package/dist/artifactPaths.js.map +1 -0
- package/dist/artifacts.d.ts +13 -8
- package/dist/artifacts.d.ts.map +1 -1
- package/dist/artifacts.js +18 -10
- package/dist/artifacts.js.map +1 -1
- package/dist/authenticatedRequest.d.ts +5 -5
- package/dist/authenticatedRequest.d.ts.map +1 -1
- package/dist/authenticatedRequest.js +6 -5
- package/dist/authenticatedRequest.js.map +1 -1
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +2 -1
- package/dist/cache.js.map +1 -1
- package/dist/checkRuns.d.ts +4 -4
- package/dist/checkRuns.d.ts.map +1 -1
- package/dist/checkRuns.js +5 -3
- package/dist/checkRuns.js.map +1 -1
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +6 -0
- package/dist/errors.js.map +1 -1
- package/dist/gitSchemas.d.ts +195 -0
- package/dist/gitSchemas.d.ts.map +1 -0
- package/dist/gitSchemas.js +45 -0
- package/dist/gitSchemas.js.map +1 -0
- package/dist/installationClients.d.ts +291 -15
- package/dist/installationClients.d.ts.map +1 -1
- package/dist/installationClients.js +14 -5
- package/dist/installationClients.js.map +1 -1
- package/dist/markdown.d.ts +1 -0
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +3 -0
- package/dist/markdown.js.map +1 -1
- package/dist/markdownRows.d.ts +8 -1
- package/dist/markdownRows.d.ts.map +1 -1
- package/dist/markdownRows.js +39 -8
- package/dist/markdownRows.js.map +1 -1
- package/dist/pagedLists.d.ts +15 -0
- package/dist/pagedLists.d.ts.map +1 -0
- package/dist/pagedLists.js +16 -0
- package/dist/pagedLists.js.map +1 -0
- package/dist/repositoryAddress.d.ts +11 -0
- package/dist/repositoryAddress.d.ts.map +1 -0
- package/dist/repositoryAddress.js +23 -0
- package/dist/repositoryAddress.js.map +1 -0
- package/dist/repositoryReads.d.ts +262 -0
- package/dist/repositoryReads.d.ts.map +1 -0
- package/dist/repositoryReads.js +30 -0
- package/dist/repositoryReads.js.map +1 -0
- package/dist/repositorySchemas.d.ts +73 -0
- package/dist/repositorySchemas.d.ts.map +1 -0
- package/dist/repositorySchemas.js +38 -0
- package/dist/repositorySchemas.js.map +1 -0
- package/dist/representations.d.ts +5 -5
- package/dist/representations.d.ts.map +1 -1
- package/dist/representations.js +3 -2
- package/dist/representations.js.map +1 -1
- package/dist/requests.d.ts +10 -6
- package/dist/requests.d.ts.map +1 -1
- package/dist/requests.js +11 -6
- package/dist/requests.js.map +1 -1
- package/dist/status.d.ts +2 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +2 -3
- package/dist/status.js.map +1 -1
- package/dist/stickyComments.d.ts +1 -1
- package/dist/verifyWebhook.d.ts +2 -2
- package/dist/verifyWebhook.d.ts.map +1 -1
- package/dist/verifyWebhook.js +3 -10
- package/dist/verifyWebhook.js.map +1 -1
- package/dist/viewerRepositoryAccess.d.ts +46 -0
- package/dist/viewerRepositoryAccess.d.ts.map +1 -0
- package/dist/viewerRepositoryAccess.js +54 -0
- package/dist/viewerRepositoryAccess.js.map +1 -0
- package/package.json +13 -6
- package/src/GitHub.ts +34 -15
- package/src/adapters/manualRedirects.ts +6 -0
- package/src/adapters/mergeResponseHeaders.ts +8 -0
- package/src/adapters/resolveApiUrl.ts +5 -0
- package/src/adapters/signAppJwt.ts +16 -0
- package/src/adapters/verifySignature.ts +13 -0
- package/src/adapters/zipEntries.ts +69 -0
- package/src/appJwt.ts +3 -12
- package/src/appTokens.ts +2 -1
- package/src/artifactErrors.ts +26 -0
- package/src/artifactExtraction.ts +62 -0
- package/src/artifactPaths.ts +36 -0
- package/src/artifacts.ts +35 -40
- package/src/authenticatedRequest.ts +8 -9
- package/src/cache.ts +2 -1
- package/src/checkRuns.ts +5 -5
- package/src/errors.ts +6 -0
- package/src/gitSchemas.ts +52 -0
- package/src/installationClients.ts +18 -12
- package/src/markdown.ts +4 -0
- package/src/markdownRows.ts +54 -9
- package/src/pagedLists.ts +40 -0
- package/src/repositoryAddress.ts +32 -0
- package/src/repositoryReads.ts +62 -0
- package/src/repositorySchemas.ts +43 -0
- package/src/representations.ts +6 -5
- package/src/requests.ts +12 -14
- package/src/status.ts +3 -3
- package/src/verifyWebhook.ts +4 -10
- package/src/viewerRepositoryAccess.ts +66 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Effect, Stream } from "effect";
|
|
2
|
+
import { zipEntries } from "#adapters/zipEntries.ts";
|
|
3
|
+
import {
|
|
4
|
+
GitHubArtifactCompressionRatio,
|
|
5
|
+
GitHubArtifactExtractedTooLarge,
|
|
6
|
+
GitHubArtifactExtractionError,
|
|
7
|
+
GitHubArtifactSymlink,
|
|
8
|
+
} from "#artifactErrors.ts";
|
|
9
|
+
import { artifactPaths } from "#artifactPaths.ts";
|
|
10
|
+
|
|
11
|
+
export interface ArtifactExtractionOptions {
|
|
12
|
+
readonly compressionRatioLimit?: number;
|
|
13
|
+
readonly totalSizeLimit?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ArtifactFile {
|
|
17
|
+
readonly bytes: Uint8Array;
|
|
18
|
+
readonly path: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const extractArtifactArchive = Effect.fn("GitHub.extractArtifactArchive")(function* (bytes: Uint8Array, options: ArtifactExtractionOptions) {
|
|
22
|
+
const totalSizeLimit = options.totalSizeLimit ?? 100 * 1024 * 1024;
|
|
23
|
+
const compressionRatioLimit = options.compressionRatioLimit ?? 100;
|
|
24
|
+
if (!Number.isSafeInteger(totalSizeLimit) || totalSizeLimit < 0 || !Number.isFinite(compressionRatioLimit) || compressionRatioLimit <= 0) {
|
|
25
|
+
return yield* new GitHubArtifactExtractionError({ reason: "invalid-limits" });
|
|
26
|
+
}
|
|
27
|
+
const entries = yield* zipEntries(bytes);
|
|
28
|
+
const pathOf = artifactPaths();
|
|
29
|
+
const files: ArtifactFile[] = [];
|
|
30
|
+
let total = 0;
|
|
31
|
+
yield* entries.pipe(
|
|
32
|
+
Stream.runForEach((entry) =>
|
|
33
|
+
Effect.gen(function* () {
|
|
34
|
+
const path = yield* pathOf(entry.path, entry.directory);
|
|
35
|
+
yield* validateEntry(entry, total, { compressionRatioLimit, totalSizeLimit });
|
|
36
|
+
const content = yield* entry.read({ compressionRatioLimit, remaining: totalSizeLimit - total, totalSizeLimit });
|
|
37
|
+
total += content.byteLength;
|
|
38
|
+
if (!entry.directory) {
|
|
39
|
+
files.push({ bytes: content, path });
|
|
40
|
+
}
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
return files;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
type ZipEntry = Stream.Success<Effect.Success<ReturnType<typeof zipEntries>>>;
|
|
48
|
+
|
|
49
|
+
const validateEntry = Effect.fnUntraced(function* (entry: ZipEntry, total: number, options: Required<ArtifactExtractionOptions>) {
|
|
50
|
+
if (entry.symlink) {
|
|
51
|
+
return yield* new GitHubArtifactSymlink({ path: entry.path });
|
|
52
|
+
}
|
|
53
|
+
if (entry.directory && entry.uncompressedSize !== 0) {
|
|
54
|
+
return yield* new GitHubArtifactExtractionError({ reason: "invalid-archive" });
|
|
55
|
+
}
|
|
56
|
+
if (total + entry.uncompressedSize > options.totalSizeLimit) {
|
|
57
|
+
return yield* new GitHubArtifactExtractedTooLarge({ limit: options.totalSizeLimit });
|
|
58
|
+
}
|
|
59
|
+
if (entry.uncompressedSize > entry.compressedSize * options.compressionRatioLimit) {
|
|
60
|
+
return yield* new GitHubArtifactCompressionRatio({ limit: options.compressionRatioLimit, path: entry.path });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { GitHubArtifactDuplicateEntry, GitHubArtifactPathTraversal } from "#artifactErrors.ts";
|
|
3
|
+
|
|
4
|
+
const safeName = Effect.fnUntraced(function* (path: string, directory: boolean) {
|
|
5
|
+
const name = directory && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
6
|
+
const segments = name.split("/");
|
|
7
|
+
if (/[\\:\0]/u.test(name) || segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
8
|
+
return yield* new GitHubArtifactPathTraversal({ path });
|
|
9
|
+
}
|
|
10
|
+
return { name, segments };
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export function artifactPaths() {
|
|
14
|
+
const entries = new Set<string>();
|
|
15
|
+
const files = new Set<string>();
|
|
16
|
+
const directories = new Set<string>();
|
|
17
|
+
const registerParents = Effect.fnUntraced(function* (path: string, segments: readonly string[]) {
|
|
18
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
19
|
+
const parent = segments.slice(0, index).join("/");
|
|
20
|
+
if (files.has(parent)) {
|
|
21
|
+
return yield* new GitHubArtifactDuplicateEntry({ path });
|
|
22
|
+
}
|
|
23
|
+
directories.add(parent);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
return Effect.fnUntraced(function* (path: string, directory: boolean) {
|
|
27
|
+
const { name, segments } = yield* safeName(path, directory);
|
|
28
|
+
if (entries.has(name) || (!directory && directories.has(name))) {
|
|
29
|
+
return yield* new GitHubArtifactDuplicateEntry({ path });
|
|
30
|
+
}
|
|
31
|
+
yield* registerParents(path, segments);
|
|
32
|
+
entries.add(name);
|
|
33
|
+
(directory ? directories : files).add(name);
|
|
34
|
+
return name;
|
|
35
|
+
});
|
|
36
|
+
}
|
package/src/artifacts.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
-
import {
|
|
2
|
+
import { HttpClient, type HttpClientError, HttpClientRequest } from "effect/http";
|
|
3
3
|
import * as HttpStatus from "effect/http/HttpStatus";
|
|
4
4
|
import { readBoundedBody } from "@shivaedev/http/body.ts";
|
|
5
5
|
import type { RemoteStatusError } from "@shivaedev/http/errors.ts";
|
|
6
6
|
import { filterStatus } from "@shivaedev/http/filterStatus.ts";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import type
|
|
10
|
-
import
|
|
11
|
-
import
|
|
7
|
+
import { manualRedirects } from "#adapters/manualRedirects.ts";
|
|
8
|
+
import { resolveApiUrl } from "#adapters/resolveApiUrl.ts";
|
|
9
|
+
import { type ArtifactExtractionOptions, extractArtifactArchive } from "#artifactExtraction.ts";
|
|
10
|
+
import { authenticatedRequest } from "#authenticatedRequest.ts";
|
|
11
|
+
import { GitHubArtifactError, GitHubArtifactTooLarge, GitHubTransportError } from "#errors.ts";
|
|
12
|
+
import { GitHubToken } from "#GitHubToken.ts";
|
|
13
|
+
import type { RateLimits } from "#rateLimits.ts";
|
|
14
|
+
import { TokenRefresh } from "#TokenRefresh.ts";
|
|
12
15
|
|
|
13
|
-
export interface ArtifactArchiveOptions {
|
|
16
|
+
export interface ArtifactArchiveOptions extends ArtifactExtractionOptions {
|
|
14
17
|
readonly artifactId: number;
|
|
15
18
|
readonly byteLimit: number;
|
|
16
19
|
readonly owner: string;
|
|
@@ -23,41 +26,33 @@ function downloadFailure(path: string, cause: HttpClientError.HttpClientError |
|
|
|
23
26
|
: new GitHubArtifactError({ stage: "download", status: cause.status });
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
rates: RateLimits,
|
|
31
|
-
refresh: typeof TokenRefresh.Service = undefined,
|
|
32
|
-
) {
|
|
29
|
+
export const artifacts = Effect.fn("GitHub.artifacts")(function* (apiUrl: string, rates: RateLimits) {
|
|
30
|
+
const client = yield* HttpClient.HttpClient;
|
|
31
|
+
const token = yield* GitHubToken;
|
|
32
|
+
const refresh = yield* TokenRefresh;
|
|
33
33
|
const send = authenticatedRequest(HttpClient.withScope(client), token, rates, refresh);
|
|
34
34
|
const download = filterStatus(HttpClient.withScope(client));
|
|
35
35
|
return {
|
|
36
|
-
downloadArtifactArchive: (options: ArtifactArchiveOptions)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
RemoteGone: (error) => Effect.fail(new GitHubArtifactError({ stage: "redirect", status: error.status }, { cause: error })),
|
|
43
|
-
RemoteNotFound: (error) => Effect.fail(new GitHubArtifactError({ stage: "redirect", status: error.status }, { cause: error })),
|
|
44
|
-
}),
|
|
45
|
-
);
|
|
46
|
-
const location = redirect.headers.location;
|
|
47
|
-
if (redirect.status !== HttpStatus.fromLiteral("Found") || location === undefined) {
|
|
48
|
-
return yield* new GitHubArtifactError({ stage: "redirect", status: redirect.status });
|
|
49
|
-
}
|
|
50
|
-
const response = yield* download.get(location).pipe(
|
|
51
|
-
Effect.provideService(FetchHttpClient.RequestInit, { credentials: "omit", redirect: "manual" }),
|
|
52
|
-
Effect.mapError((cause) => downloadFailure(path, cause)),
|
|
53
|
-
);
|
|
54
|
-
return yield* readBoundedBody(response, options.byteLimit).pipe(
|
|
55
|
-
Effect.catchTags({
|
|
56
|
-
HttpClientError: (cause) => Effect.fail(new GitHubTransportError({ path }, { cause })),
|
|
57
|
-
ResponseBodyTooLarge: (cause) => Effect.fail(new GitHubArtifactTooLarge({ limit: cause.limit }, { cause })),
|
|
58
|
-
}),
|
|
59
|
-
);
|
|
36
|
+
downloadArtifactArchive: Effect.fn("GitHub.downloadArtifactArchive")(function* (options: ArtifactArchiveOptions) {
|
|
37
|
+
const path = `/repos/${encodeURIComponent(options.owner)}/${encodeURIComponent(options.repo)}/actions/artifacts/${options.artifactId}/zip`;
|
|
38
|
+
const redirect = yield* send(HttpClientRequest.get(resolveApiUrl(apiUrl, path), { accept: "application/vnd.github+json" }), "manual").pipe(
|
|
39
|
+
Effect.catchTags({
|
|
40
|
+
RemoteGone: (error) => Effect.fail(new GitHubArtifactError({ stage: "redirect", status: error.status }, { cause: error })),
|
|
41
|
+
RemoteNotFound: (error) => Effect.fail(new GitHubArtifactError({ stage: "redirect", status: error.status }, { cause: error })),
|
|
60
42
|
}),
|
|
61
|
-
)
|
|
43
|
+
);
|
|
44
|
+
const location = redirect.headers.location;
|
|
45
|
+
if (redirect.status !== HttpStatus.fromLiteral("Found") || location === undefined) {
|
|
46
|
+
return yield* new GitHubArtifactError({ stage: "redirect", status: redirect.status });
|
|
47
|
+
}
|
|
48
|
+
const response = yield* manualRedirects(download.get(location)).pipe(Effect.mapError((cause) => downloadFailure(path, cause)));
|
|
49
|
+
return yield* readBoundedBody(response, options.byteLimit).pipe(
|
|
50
|
+
Effect.catchTags({
|
|
51
|
+
HttpClientError: (cause) => Effect.fail(new GitHubTransportError({ path }, { cause })),
|
|
52
|
+
ResponseBodyTooLarge: (cause) => Effect.fail(new GitHubArtifactTooLarge({ limit: cause.limit }, { cause })),
|
|
53
|
+
}),
|
|
54
|
+
Effect.flatMap((bytes) => extractArtifactArchive(bytes, options)),
|
|
55
|
+
);
|
|
56
|
+
}, Effect.scoped),
|
|
62
57
|
};
|
|
63
|
-
}
|
|
58
|
+
});
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { DateTime, Effect, Option, Redacted, Stream } from "effect";
|
|
2
|
-
import {
|
|
2
|
+
import { type HttpClient, type HttpClientError, HttpClientRequest } from "effect/http";
|
|
3
3
|
import * as HttpStatus from "effect/http/HttpStatus";
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
4
|
+
import { manualRedirects } from "#adapters/manualRedirects.ts";
|
|
5
|
+
import { GitHubTransportError } from "#errors.ts";
|
|
6
|
+
import type { GitHubToken } from "#GitHubToken.ts";
|
|
7
|
+
import { failRateLimited, type RateLimits } from "#rateLimits.ts";
|
|
8
|
+
import { checkStatus, isUnauthorized, isUnchanged } from "#status.ts";
|
|
9
|
+
import type { TokenRefresh } from "#TokenRefresh.ts";
|
|
9
10
|
|
|
10
11
|
export function authenticatedRequest<R>(
|
|
11
12
|
client: HttpClient.HttpClient.With<HttpClientError.HttpClientError, R>,
|
|
@@ -23,9 +24,7 @@ export function authenticatedRequest<R>(
|
|
|
23
24
|
}
|
|
24
25
|
function send(current: Option.Option<Redacted.Redacted<string>>) {
|
|
25
26
|
const sent = client.execute(withCredential(input, current));
|
|
26
|
-
return (
|
|
27
|
-
mode === "manual" ? sent.pipe(Effect.provideService(FetchHttpClient.RequestInit, { credentials: "omit", redirect: "manual" })) : sent
|
|
28
|
-
).pipe(Effect.mapError((cause) => new GitHubTransportError({ path }, { cause })));
|
|
27
|
+
return (mode === "manual" ? manualRedirects(sent) : sent).pipe(Effect.mapError((cause) => new GitHubTransportError({ path }, { cause })));
|
|
29
28
|
}
|
|
30
29
|
const credential = yield* accessToken;
|
|
31
30
|
let response = yield* send(credential);
|
package/src/cache.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Schema } from "effect";
|
|
2
2
|
import type { HttpClientResponse } from "effect/http";
|
|
3
|
+
import { mergeResponseHeaders } from "#adapters/mergeResponseHeaders.ts";
|
|
3
4
|
|
|
4
5
|
export interface CachedResponse {
|
|
5
6
|
readonly bytes: Uint8Array;
|
|
@@ -9,7 +10,7 @@ export interface CachedResponse {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function revalidatedResponse(cached: CachedResponse, response: HttpClientResponse.HttpClientResponse): CachedResponse {
|
|
12
|
-
return { ...cached, etag: response.headers.etag ?? cached.etag, response };
|
|
13
|
+
return { ...cached, etag: response.headers.etag ?? cached.etag, response: mergeResponseHeaders(cached.response, response) };
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export function responseCache() {
|
package/src/checkRuns.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Effect, Option, Schema, Stream } from "effect";
|
|
2
|
+
import { decodedPages } from "#pagedLists.ts";
|
|
2
3
|
import type { Representations } from "#representations.ts";
|
|
3
4
|
|
|
4
5
|
export const CheckRunOutput = Schema.Struct({
|
|
@@ -50,8 +51,6 @@ export const CheckRun = Schema.Struct({
|
|
|
50
51
|
status: CheckRunStatus,
|
|
51
52
|
}).pipe(Schema.encodeKeys({ detailsUrl: "details_url", headSha: "head_sha" }));
|
|
52
53
|
|
|
53
|
-
const CheckRunPage = Schema.Struct({ checkRuns: Schema.Array(CheckRun) }).pipe(Schema.encodeKeys({ checkRuns: "check_runs" }));
|
|
54
|
-
|
|
55
54
|
export const FindCheckRun = Schema.Union([
|
|
56
55
|
Schema.Struct({ appId: Schema.optional(Schema.Number), headSha: Schema.String, name: Schema.String }),
|
|
57
56
|
Schema.Struct({ appId: Schema.Number, headSha: Schema.String, name: Schema.optional(Schema.String) }),
|
|
@@ -74,10 +73,11 @@ export function checkRuns(client: Representations) {
|
|
|
74
73
|
if (name !== undefined) {
|
|
75
74
|
query.set("check_name", name);
|
|
76
75
|
}
|
|
77
|
-
const pages =
|
|
76
|
+
const pages = decodedPages(client, `/repos/${owner}/${repo}/commits/${encodeURIComponent(headSha)}/check-runs?${query}`, CheckRun, {
|
|
77
|
+
arrayKey: "check_runs",
|
|
78
|
+
});
|
|
78
79
|
return yield* pages.pipe(
|
|
79
|
-
Stream.
|
|
80
|
-
Stream.flatMap((page) => Stream.fromIterable(page.checkRuns)),
|
|
80
|
+
Stream.flatMap((page) => Stream.fromIterable(page.items)),
|
|
81
81
|
Stream.filter((run) => (name === undefined || run.name === name) && (appId === undefined || run.app?.id === appId)),
|
|
82
82
|
Stream.runFold(
|
|
83
83
|
() => Option.none<typeof CheckRun.Type>(),
|
package/src/errors.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
2
|
|
|
3
|
+
export class GitHubAddressInvalid extends Schema.TaggedError<GitHubAddressInvalid>()("GitHubAddressInvalid", {}) {}
|
|
4
|
+
|
|
5
|
+
export class MarkdownBudgetTooSmall extends Schema.TaggedError<MarkdownBudgetTooSmall>()("MarkdownBudgetTooSmall", {
|
|
6
|
+
byteLimit: Schema.Number,
|
|
7
|
+
}) {}
|
|
8
|
+
|
|
3
9
|
export class GitHubPermissionDenied extends Schema.TaggedError<GitHubPermissionDenied>()("GitHubPermissionDenied", {
|
|
4
10
|
path: Schema.String,
|
|
5
11
|
}) {}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { PullRequestFile, User } from "#repositorySchemas.ts";
|
|
3
|
+
|
|
4
|
+
const CommitIdentity = Schema.Struct({ date: Schema.String, email: Schema.String, name: Schema.String });
|
|
5
|
+
const CommitData = Schema.Struct({
|
|
6
|
+
author: Schema.NullOr(CommitIdentity),
|
|
7
|
+
committer: Schema.NullOr(CommitIdentity),
|
|
8
|
+
message: Schema.String,
|
|
9
|
+
tree: Schema.Struct({ sha: Schema.String, url: Schema.String }),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const commitFields = {
|
|
13
|
+
author: Schema.NullOr(User),
|
|
14
|
+
commit: CommitData,
|
|
15
|
+
committer: Schema.NullOr(User),
|
|
16
|
+
htmlUrl: Schema.String,
|
|
17
|
+
parents: Schema.Array(Schema.Struct({ sha: Schema.String, url: Schema.String })),
|
|
18
|
+
sha: Schema.String,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const Commit = Schema.Struct(commitFields).pipe(Schema.encodeKeys({ htmlUrl: "html_url" }));
|
|
22
|
+
|
|
23
|
+
export const CommitDetail = Schema.Struct({
|
|
24
|
+
...commitFields,
|
|
25
|
+
files: Schema.optional(Schema.Array(PullRequestFile)),
|
|
26
|
+
stats: Schema.Struct({ additions: Schema.Number, deletions: Schema.Number, total: Schema.Number }),
|
|
27
|
+
}).pipe(Schema.encodeKeys({ htmlUrl: "html_url" }));
|
|
28
|
+
|
|
29
|
+
export const Comparison = Schema.Struct({
|
|
30
|
+
aheadBy: Schema.Number,
|
|
31
|
+
behindBy: Schema.Number,
|
|
32
|
+
commits: Schema.Array(Commit),
|
|
33
|
+
files: Schema.optional(Schema.Array(PullRequestFile)),
|
|
34
|
+
status: Schema.Literals(["diverged", "ahead", "behind", "identical"]),
|
|
35
|
+
totalCommits: Schema.Number,
|
|
36
|
+
}).pipe(Schema.encodeKeys({ aheadBy: "ahead_by", behindBy: "behind_by", totalCommits: "total_commits" }));
|
|
37
|
+
|
|
38
|
+
export const GitTree = Schema.Struct({
|
|
39
|
+
sha: Schema.String,
|
|
40
|
+
tree: Schema.Array(
|
|
41
|
+
Schema.Struct({
|
|
42
|
+
mode: Schema.String,
|
|
43
|
+
path: Schema.String,
|
|
44
|
+
sha: Schema.String,
|
|
45
|
+
size: Schema.optional(Schema.Number),
|
|
46
|
+
type: Schema.Literals(["blob", "tree", "commit"]),
|
|
47
|
+
url: Schema.optional(Schema.String),
|
|
48
|
+
}),
|
|
49
|
+
),
|
|
50
|
+
truncated: Schema.Boolean,
|
|
51
|
+
url: Schema.String,
|
|
52
|
+
});
|
|
@@ -5,34 +5,40 @@ import type { AppCredentials } from "#appJwt.ts";
|
|
|
5
5
|
import { appTokens } from "#appTokens.ts";
|
|
6
6
|
import { GitHub } from "#GitHub.ts";
|
|
7
7
|
import { GitHubConfig } from "#GitHubConfig.ts";
|
|
8
|
+
import { viewerRepositoryAccess } from "#viewerRepositoryAccess.ts";
|
|
8
9
|
|
|
9
10
|
export const GitHubAppCredentials = Context.Service<{ readonly credentials: AppCredentials }>("@shivaedev/github/AppCredentials");
|
|
10
11
|
|
|
11
12
|
export const GitHubApp = defineService({
|
|
12
13
|
id: "@shivaedev/github/GitHubApp",
|
|
13
14
|
initialize: Effect.gen(function* () {
|
|
14
|
-
const { credentials } = yield* GitHubAppCredentials;
|
|
15
|
+
const { credentials: supplied } = yield* GitHubAppCredentials;
|
|
16
|
+
const credentials = { appId: supplied.appId, privateKey: supplied.privateKey };
|
|
15
17
|
const http = yield* HttpClient.HttpClient;
|
|
16
18
|
const config = yield* GitHubConfig;
|
|
17
|
-
|
|
19
|
+
const environment = Layer.mergeAll(Layer.succeed(HttpClient.HttpClient)(http), Layer.succeed(GitHubConfig)(config));
|
|
20
|
+
const app = yield* Layer.build(GitHub.layer.pipe(Layer.provide(appTokens(credentials)), Layer.provide(environment))).pipe(
|
|
21
|
+
Effect.scoped,
|
|
22
|
+
Effect.map((context) => Context.get(context, GitHub)),
|
|
23
|
+
);
|
|
24
|
+
const clients = yield* Cache.make({
|
|
18
25
|
capacity: 100,
|
|
19
26
|
lookup: (installationId: number) =>
|
|
20
|
-
Layer.build(
|
|
21
|
-
GitHub.layer.pipe(
|
|
22
|
-
Layer.provide(appTokens({ ...credentials, installationId })),
|
|
23
|
-
Layer.provide(Layer.succeed(HttpClient.HttpClient)(http)),
|
|
24
|
-
Layer.provide(Layer.succeed(GitHubConfig)(config)),
|
|
25
|
-
),
|
|
26
|
-
).pipe(
|
|
27
|
+
Layer.build(GitHub.layer.pipe(Layer.provide(appTokens({ ...credentials, installationId })), Layer.provide(environment))).pipe(
|
|
27
28
|
Effect.scoped,
|
|
28
29
|
Effect.map((context) => Context.get(context, GitHub)),
|
|
29
30
|
),
|
|
30
31
|
timeToLive: "30 minutes",
|
|
31
32
|
});
|
|
33
|
+
function installation(id: number) {
|
|
34
|
+
return Cache.get(clients, id).pipe(Effect.tap((client) => Cache.set(clients, id, client)));
|
|
35
|
+
}
|
|
36
|
+
return { installation, viewers: yield* viewerRepositoryAccess(app, installation) };
|
|
32
37
|
}),
|
|
33
|
-
methods: (
|
|
34
|
-
installation
|
|
35
|
-
|
|
38
|
+
methods: ({ installation, viewers }) => ({
|
|
39
|
+
installation,
|
|
40
|
+
viewerRepositories: (account: number | string) =>
|
|
41
|
+
Cache.get(viewers, typeof account === "number" ? `id:${account}` : `login:${account.toLowerCase()}`),
|
|
36
42
|
}),
|
|
37
43
|
requires: [GitHubAppCredentials, HttpClient.HttpClient, GitHubConfig],
|
|
38
44
|
});
|
package/src/markdown.ts
CHANGED
|
@@ -23,3 +23,7 @@ export function tableCell(text: string, format: "text" | "code" = "text"): strin
|
|
|
23
23
|
const content = text.replace(/\r\n|[\r\n\u2028\u2029]/gu, " ");
|
|
24
24
|
return format === "code" ? codeSpan(content.replaceAll("|", "\\|")) : plainText(content);
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
export function details(summary: string, text: string): string {
|
|
28
|
+
return `<details>\n<summary>${plainText(summary)}</summary>\n\n${plainText(text)}\n\n</details>`;
|
|
29
|
+
}
|
package/src/markdownRows.ts
CHANGED
|
@@ -1,9 +1,45 @@
|
|
|
1
|
+
import { Result } from "effect";
|
|
2
|
+
import { MarkdownBudgetTooSmall } from "./errors.ts";
|
|
3
|
+
|
|
1
4
|
export interface MarkdownRowsOptions {
|
|
2
5
|
readonly byteLimit?: number;
|
|
3
6
|
readonly header?: string;
|
|
4
7
|
}
|
|
5
8
|
|
|
6
|
-
export
|
|
9
|
+
export interface MarkdownRowsSection {
|
|
10
|
+
readonly header?: string;
|
|
11
|
+
readonly rows: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function renderRows(
|
|
15
|
+
rows: readonly string[],
|
|
16
|
+
{ byteLimit = 60_000, header = "" }: MarkdownRowsOptions = {},
|
|
17
|
+
): Result.Result<string, MarkdownBudgetTooSmall> {
|
|
18
|
+
return prepareRows({ header, rows }).render(byteLimit);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function renderSections(
|
|
22
|
+
sections: readonly MarkdownRowsSection[],
|
|
23
|
+
{ byteLimit = 60_000 }: Pick<MarkdownRowsOptions, "byteLimit"> = {},
|
|
24
|
+
): Result.Result<string, MarkdownBudgetTooSmall> {
|
|
25
|
+
const prepared = sections.filter(({ rows, header = "" }) => rows.length > 0 || header !== "").map(prepareRows);
|
|
26
|
+
let reserved = prepared.reduce((total, section) => total + section.minimumBytes, 0) + Math.max(0, prepared.length - 1) * 2;
|
|
27
|
+
let remaining = byteLimit;
|
|
28
|
+
const bodies: string[] = [];
|
|
29
|
+
for (const section of prepared) {
|
|
30
|
+
reserved -= section.minimumBytes;
|
|
31
|
+
const body = section.render(remaining - reserved);
|
|
32
|
+
if (Result.isFailure(body)) {
|
|
33
|
+
return Result.fail(new MarkdownBudgetTooSmall({ byteLimit }));
|
|
34
|
+
}
|
|
35
|
+
bodies.push(body.success);
|
|
36
|
+
remaining -= new TextEncoder().encode(body.success).byteLength + 2;
|
|
37
|
+
reserved -= 2;
|
|
38
|
+
}
|
|
39
|
+
return Result.succeed(bodies.join("\n\n"));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function prepareRows({ rows, header = "" }: MarkdownRowsSection) {
|
|
7
43
|
const encoder = new TextEncoder();
|
|
8
44
|
const sizes = [0];
|
|
9
45
|
for (const row of rows) {
|
|
@@ -16,15 +52,24 @@ export function renderRows(rows: readonly string[], { byteLimit = 60_000, header
|
|
|
16
52
|
function body(kept: number) {
|
|
17
53
|
return [...(header === "" ? [] : [header]), ...rows.slice(0, kept)].join("\n");
|
|
18
54
|
}
|
|
19
|
-
|
|
20
|
-
return body(rows.length);
|
|
21
|
-
}
|
|
22
|
-
for (let kept = rows.length - 1; kept >= 0; kept -= 1) {
|
|
55
|
+
function omitted(kept: number) {
|
|
23
56
|
const notice = `and ${rows.length - kept} more`;
|
|
24
57
|
const separator = kept > 0 || header !== "" ? "\n\n" : "";
|
|
25
|
-
|
|
26
|
-
return body(kept) + separator + notice;
|
|
27
|
-
}
|
|
58
|
+
return { bytes: bodySize(kept) + separator.length + encoder.encode(notice).byteLength, notice, separator };
|
|
28
59
|
}
|
|
29
|
-
|
|
60
|
+
return {
|
|
61
|
+
minimumBytes: rows.length === 0 ? headerBytes : Math.min(bodySize(rows.length), omitted(0).bytes),
|
|
62
|
+
render(byteLimit: number): Result.Result<string, MarkdownBudgetTooSmall> {
|
|
63
|
+
if (bodySize(rows.length) <= byteLimit) {
|
|
64
|
+
return Result.succeed(body(rows.length));
|
|
65
|
+
}
|
|
66
|
+
for (let kept = rows.length - 1; kept >= 0; kept -= 1) {
|
|
67
|
+
const { notice, separator, bytes } = omitted(kept);
|
|
68
|
+
if (bytes <= byteLimit) {
|
|
69
|
+
return Result.succeed(body(kept) + separator + notice);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return Result.fail(new MarkdownBudgetTooSmall({ byteLimit }));
|
|
73
|
+
},
|
|
74
|
+
};
|
|
30
75
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Effect, Option, Schema, Stream } from "effect";
|
|
2
|
+
import type { ServiceUnavailable } from "@shivaedev/errors/taxonomy.ts";
|
|
3
|
+
import type { Representations } from "#representations.ts";
|
|
4
|
+
|
|
5
|
+
export interface ListOptions<E = ServiceUnavailable> {
|
|
6
|
+
readonly arrayKey?: string | undefined;
|
|
7
|
+
readonly beforeRequest?: (resource: string) => Effect.Effect<void, E>;
|
|
8
|
+
readonly onDecodeError?: "fail" | "skip" | undefined;
|
|
9
|
+
readonly responseByteLimit?: number | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function decodedPages<TSchema extends Schema.Constraint, E = ServiceUnavailable>(
|
|
13
|
+
client: Representations,
|
|
14
|
+
path: string,
|
|
15
|
+
schema: TSchema,
|
|
16
|
+
options: ListOptions<E> | number = {},
|
|
17
|
+
) {
|
|
18
|
+
const settings = typeof options === "number" ? { responseByteLimit: options } : options;
|
|
19
|
+
const decode = Schema.decodeUnknownEffect(schema);
|
|
20
|
+
return Stream.unwrap(client.list(path, settings.beforeRequest, settings.responseByteLimit)).pipe(
|
|
21
|
+
Stream.mapEffect((page) =>
|
|
22
|
+
Effect.gen(function* () {
|
|
23
|
+
const body = yield* page.decode(Schema.Unknown);
|
|
24
|
+
const source =
|
|
25
|
+
settings.arrayKey === undefined
|
|
26
|
+
? body
|
|
27
|
+
: (yield* Schema.decodeUnknownEffect(Schema.Record(Schema.String, Schema.Unknown))(body))[settings.arrayKey];
|
|
28
|
+
const values = yield* Schema.decodeUnknownEffect(Schema.Array(Schema.Unknown))(source);
|
|
29
|
+
const decoded = yield* Effect.forEach(values, (value) =>
|
|
30
|
+
decode(value).pipe(
|
|
31
|
+
Effect.map(Option.some),
|
|
32
|
+
Effect.catchTag("SchemaError", (error) => (settings.onDecodeError === "skip" ? Effect.succeed(Option.none()) : Effect.fail(error))),
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
const items = decoded.flatMap(Option.toArray);
|
|
36
|
+
return { hasNext: page.hasNext, items, skipped: values.length - items.length };
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Result } from "effect";
|
|
2
|
+
import { GitHubAddressInvalid } from "./errors.ts";
|
|
3
|
+
|
|
4
|
+
export interface RepositoryAddress {
|
|
5
|
+
readonly kind: "remote" | "shorthand" | "pull-request";
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly number: number | undefined;
|
|
8
|
+
readonly owner: string;
|
|
9
|
+
readonly repository: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const REPOSITORY =
|
|
13
|
+
/^(?:(?<remote>(?:git\+)?(?:https?:\/\/github\.com\/|ssh:\/\/git@github\.com\/|git:\/\/github\.com\/|git@github\.com:))|github:)?(?<owner>[A-Za-z0-9-]+)\/(?<name>[\w.-]+?)(?:\.git)?\/?$/u;
|
|
14
|
+
const PULL_REQUEST = /^https:\/\/github\.com\/(?<owner>[A-Za-z0-9-]+)\/(?<name>[\w.-]+)\/pull\/(?<number>[1-9]\d*)$/u;
|
|
15
|
+
|
|
16
|
+
export function parseRepositoryAddress(address: string): Result.Result<RepositoryAddress, GitHubAddressInvalid> {
|
|
17
|
+
const input = address.trim();
|
|
18
|
+
const groups = (PULL_REQUEST.exec(input) ?? REPOSITORY.exec(input))?.groups;
|
|
19
|
+
const owner = groups?.owner;
|
|
20
|
+
const name = groups?.name;
|
|
21
|
+
const number = groups?.number === undefined ? undefined : Number(groups.number);
|
|
22
|
+
if (owner === undefined || name === undefined || name === "." || name === ".." || (number !== undefined && !Number.isSafeInteger(number))) {
|
|
23
|
+
return Result.fail(new GitHubAddressInvalid({}));
|
|
24
|
+
}
|
|
25
|
+
let kind: RepositoryAddress["kind"] = "shorthand";
|
|
26
|
+
if (number !== undefined) {
|
|
27
|
+
kind = "pull-request";
|
|
28
|
+
} else if (groups?.remote !== undefined) {
|
|
29
|
+
kind = "remote";
|
|
30
|
+
}
|
|
31
|
+
return Result.succeed({ kind, name, number, owner, repository: `${owner}/${name}` });
|
|
32
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { ServiceUnavailable } from "@shivaedev/errors/taxonomy.ts";
|
|
3
|
+
import { Commit, CommitDetail, Comparison, GitTree } from "#gitSchemas.ts";
|
|
4
|
+
import { decodedPages, type ListOptions } from "#pagedLists.ts";
|
|
5
|
+
import { CollaboratorPermission, Issue, PullRequestFile, User } from "#repositorySchemas.ts";
|
|
6
|
+
import type { Representations } from "#representations.ts";
|
|
7
|
+
|
|
8
|
+
export interface RepositoryListOptions<E = ServiceUnavailable> extends ListOptions<E> {
|
|
9
|
+
readonly query?: Readonly<Record<string, string | number | boolean | undefined>>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function repositoryReads(client: Representations) {
|
|
13
|
+
function repository(owner: string, repo: string) {
|
|
14
|
+
return `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
|
|
15
|
+
}
|
|
16
|
+
function pathWithQuery<E>(path: string, options?: RepositoryListOptions<E>) {
|
|
17
|
+
const query = new URLSearchParams({ "per_page": "100" });
|
|
18
|
+
for (const [key, value] of Object.entries(options?.query ?? {})) {
|
|
19
|
+
if (value !== undefined) {
|
|
20
|
+
query.set(key, String(value));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return `${path}?${query}`;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
compareCommits: (owner: string, repo: string, base: string, head: string, byteLimit?: number) =>
|
|
27
|
+
Effect.flatMap(client.read(`${repository(owner, repo)}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`, byteLimit), (page) =>
|
|
28
|
+
page.decode(Comparison),
|
|
29
|
+
),
|
|
30
|
+
getCollaboratorPermission: (owner: string, repo: string, username: string, byteLimit?: number) =>
|
|
31
|
+
Effect.flatMap(client.read(`${repository(owner, repo)}/collaborators/${encodeURIComponent(username)}/permission`, byteLimit), (page) =>
|
|
32
|
+
page.decode(CollaboratorPermission),
|
|
33
|
+
),
|
|
34
|
+
getCommit: (owner: string, repo: string, ref: string, byteLimit?: number) =>
|
|
35
|
+
Effect.flatMap(client.read(`${repository(owner, repo)}/commits/${encodeURIComponent(ref)}`, byteLimit), (page) => page.decode(CommitDetail)),
|
|
36
|
+
getIssue: (owner: string, repo: string, number: number, byteLimit?: number) =>
|
|
37
|
+
Effect.flatMap(client.read(`${repository(owner, repo)}/issues/${number}`, byteLimit), (page) => page.decode(Issue)),
|
|
38
|
+
getTree: (
|
|
39
|
+
owner: string,
|
|
40
|
+
repo: string,
|
|
41
|
+
sha: string,
|
|
42
|
+
options?: { readonly recursive?: boolean; readonly responseByteLimit?: number | undefined },
|
|
43
|
+
) =>
|
|
44
|
+
Effect.flatMap(
|
|
45
|
+
client.read(
|
|
46
|
+
`${repository(owner, repo)}/git/trees/${encodeURIComponent(sha)}${options?.recursive ? "?recursive=1" : ""}`,
|
|
47
|
+
options?.responseByteLimit,
|
|
48
|
+
),
|
|
49
|
+
(page) => page.decode(GitTree),
|
|
50
|
+
),
|
|
51
|
+
getUser: (username?: string, byteLimit?: number) =>
|
|
52
|
+
Effect.flatMap(client.read(username === undefined ? "/user" : `/users/${encodeURIComponent(username)}`, byteLimit), (page) =>
|
|
53
|
+
page.decode(User),
|
|
54
|
+
),
|
|
55
|
+
listCommits: <E = ServiceUnavailable>(owner: string, repo: string, options?: RepositoryListOptions<E>) =>
|
|
56
|
+
Effect.succeed(decodedPages(client, pathWithQuery(`${repository(owner, repo)}/commits`, options), Commit, options)),
|
|
57
|
+
listIssues: <E = ServiceUnavailable>(owner: string, repo: string, options?: RepositoryListOptions<E>) =>
|
|
58
|
+
Effect.succeed(decodedPages(client, pathWithQuery(`${repository(owner, repo)}/issues`, options), Issue, options)),
|
|
59
|
+
listPullRequestFiles: <E = ServiceUnavailable>(owner: string, repo: string, number: number, options?: RepositoryListOptions<E>) =>
|
|
60
|
+
Effect.succeed(decodedPages(client, pathWithQuery(`${repository(owner, repo)}/pulls/${number}/files`, options), PullRequestFile, options)),
|
|
61
|
+
};
|
|
62
|
+
}
|