@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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { GitHubClient } from "./GitHubClient.js";
|
|
2
|
+
import { Context, Effect, Layer, Schema } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/ArtifactMetadata.ts
|
|
5
|
+
/**
|
|
6
|
+
* What to record about a published artifact.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* These are the fields the endpoint actually accepts. The version this replaces
|
|
10
|
+
* declared a `version` field the endpoint has no notion of — a fabricated key
|
|
11
|
+
* that a `Record<string, unknown>` body accepted silently and the generated
|
|
12
|
+
* types reject outright.
|
|
13
|
+
*
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
var StorageRecordInput = class extends Schema.Class("StorageRecordInput")({
|
|
17
|
+
/** The artifact's package URL (purl). */
|
|
18
|
+
name: Schema.NonEmptyString,
|
|
19
|
+
/** Its content digest, as `algorithm:hex`. */
|
|
20
|
+
digest: Schema.NonEmptyString,
|
|
21
|
+
/** The registry's base URL. */
|
|
22
|
+
registryUrl: Schema.NonEmptyString,
|
|
23
|
+
/** The repository name **within the registry**. */
|
|
24
|
+
repository: Schema.NonEmptyString,
|
|
25
|
+
/** Where the artifact is stored, when there is a direct URL. */
|
|
26
|
+
artifactUrl: Schema.optionalKey(Schema.String),
|
|
27
|
+
/** The artifact's path within the registry, when there is one. */
|
|
28
|
+
path: Schema.optionalKey(Schema.String)
|
|
29
|
+
}) {};
|
|
30
|
+
/**
|
|
31
|
+
* Artifact metadata.
|
|
32
|
+
*
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
var ArtifactMetadata = class ArtifactMetadata extends Context.Service()("@effected/github/ArtifactMetadata") {
|
|
36
|
+
static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
|
|
37
|
+
/** An in-memory double; unstubbed members die naming themselves. */
|
|
38
|
+
static makeTest = (overrides = {}) => ({ createStorageRecord: overrides.createStorageRecord ?? (() => unstubbed("createStorageRecord")) });
|
|
39
|
+
/** {@link ArtifactMetadata.makeTest} behind a `Layer`. */
|
|
40
|
+
static layerTest = (overrides = {}) => Layer.succeed(ArtifactMetadata, ArtifactMetadata.makeTest(overrides));
|
|
41
|
+
};
|
|
42
|
+
const unstubbed = (member) => {
|
|
43
|
+
throw new Error(`ArtifactMetadata.makeTest: ${member}() was called but not stubbed — pass an override.`);
|
|
44
|
+
};
|
|
45
|
+
const make = (client) => ({ createStorageRecord: Effect.fn("ArtifactMetadata.createStorageRecord")(function* (org, input) {
|
|
46
|
+
yield* Effect.annotateCurrentSpan({
|
|
47
|
+
org,
|
|
48
|
+
artifact: input.name
|
|
49
|
+
});
|
|
50
|
+
return ((yield* client.request("POST /orgs/{org}/artifacts/metadata/storage-record", {
|
|
51
|
+
org,
|
|
52
|
+
name: input.name,
|
|
53
|
+
digest: input.digest,
|
|
54
|
+
registry_url: input.registryUrl,
|
|
55
|
+
repository: input.repository,
|
|
56
|
+
...input.artifactUrl !== void 0 ? { artifact_url: input.artifactUrl } : {},
|
|
57
|
+
...input.path !== void 0 ? { path: input.path } : {}
|
|
58
|
+
})).storage_records ?? []).flatMap((record) => typeof record.id === "number" ? [record.id] : []);
|
|
59
|
+
}) });
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
export { ArtifactMetadata, StorageRecordInput };
|
package/Attestation.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { GitHubClient } from "./GitHubClient.js";
|
|
2
|
+
import { Repo } from "./Repo.js";
|
|
3
|
+
import { Context, Effect, Layer, Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/Attestation.ts
|
|
6
|
+
/**
|
|
7
|
+
* The api-version this surface pins.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* The legacy shape, which inlines the whole bundle in the listing, is deprecated
|
|
11
|
+
* with a stated sunset. Pinning the version means the response is on a contract
|
|
12
|
+
* the generated types do not describe — which is why this module is the one that
|
|
13
|
+
* uses `requestDecoded` with owned schemas rather than the route table.
|
|
14
|
+
*/
|
|
15
|
+
const API_VERSION = "2026-03-10";
|
|
16
|
+
/**
|
|
17
|
+
* A stored attestation.
|
|
18
|
+
*
|
|
19
|
+
* @public
|
|
20
|
+
*/
|
|
21
|
+
var AttestationRecord = class extends Schema.Class("AttestationRecord")({
|
|
22
|
+
/** GitHub's id for it, when the response carried one. */
|
|
23
|
+
id: Schema.optionalKey(Schema.Int),
|
|
24
|
+
/** Where a human can look at it. */
|
|
25
|
+
url: Schema.String
|
|
26
|
+
}) {};
|
|
27
|
+
/**
|
|
28
|
+
* One entry from an attestation listing.
|
|
29
|
+
*
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
var AttestationListEntry = class extends Schema.Class("AttestationListEntry")({
|
|
33
|
+
/** Where the bundle lives. */
|
|
34
|
+
url: Schema.String,
|
|
35
|
+
/** The in-toto predicate type, when the listing reported one. */
|
|
36
|
+
predicateType: Schema.optionalKey(Schema.String)
|
|
37
|
+
}) {};
|
|
38
|
+
const UploadResponse = Schema.Struct({ id: Schema.optionalKey(Schema.Int) });
|
|
39
|
+
const ListResponse = Schema.Struct({ attestations: Schema.optionalKey(Schema.Array(Schema.Struct({
|
|
40
|
+
id: Schema.optionalKey(Schema.Int),
|
|
41
|
+
bundle_url: Schema.optionalKey(Schema.String),
|
|
42
|
+
predicate_type: Schema.optionalKey(Schema.String)
|
|
43
|
+
}))) });
|
|
44
|
+
/**
|
|
45
|
+
* Attestations.
|
|
46
|
+
*
|
|
47
|
+
* @public
|
|
48
|
+
*/
|
|
49
|
+
var Attestation = class Attestation extends Context.Service()("@effected/github/Attestation") {
|
|
50
|
+
static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
|
|
51
|
+
/** An in-memory double; unstubbed members die naming themselves. */
|
|
52
|
+
static makeTest = (overrides = {}) => ({
|
|
53
|
+
upload: overrides.upload ?? (() => unstubbed("upload")),
|
|
54
|
+
listForSubject: overrides.listForSubject ?? (() => unstubbed("listForSubject"))
|
|
55
|
+
});
|
|
56
|
+
/** {@link Attestation.makeTest} behind a `Layer`. */
|
|
57
|
+
static layerTest = (overrides = {}) => Layer.succeed(Attestation, Attestation.makeTest(overrides));
|
|
58
|
+
};
|
|
59
|
+
const unstubbed = (member) => {
|
|
60
|
+
throw new Error(`Attestation.makeTest: ${member}() was called but not stubbed — pass an override.`);
|
|
61
|
+
};
|
|
62
|
+
const make = (client) => ({
|
|
63
|
+
upload: Effect.fn("Attestation.upload")(function* (bundle) {
|
|
64
|
+
const { owner, repo } = yield* Repo;
|
|
65
|
+
yield* Effect.annotateCurrentSpan({
|
|
66
|
+
owner,
|
|
67
|
+
repo
|
|
68
|
+
});
|
|
69
|
+
const stored = yield* client.requestDecoded("POST /repos/{owner}/{repo}/attestations", {
|
|
70
|
+
owner,
|
|
71
|
+
repo,
|
|
72
|
+
bundle,
|
|
73
|
+
headers: { "x-github-api-version": API_VERSION }
|
|
74
|
+
}, UploadResponse);
|
|
75
|
+
return AttestationRecord.make({
|
|
76
|
+
...stored.id !== void 0 ? { id: stored.id } : {},
|
|
77
|
+
url: `https://github.com/${owner}/${repo}/attestations/${stored.id ?? ""}`
|
|
78
|
+
});
|
|
79
|
+
}),
|
|
80
|
+
listForSubject: Effect.fn("Attestation.listForSubject")(function* (sha256, options) {
|
|
81
|
+
const { owner, repo } = yield* Repo;
|
|
82
|
+
const digest = sha256.startsWith("sha256:") ? sha256 : `sha256:${sha256}`;
|
|
83
|
+
yield* Effect.annotateCurrentSpan({
|
|
84
|
+
owner,
|
|
85
|
+
repo,
|
|
86
|
+
digest
|
|
87
|
+
});
|
|
88
|
+
return ((yield* client.requestDecoded("GET /repos/{owner}/{repo}/attestations/{subject_digest}", {
|
|
89
|
+
owner,
|
|
90
|
+
repo,
|
|
91
|
+
subject_digest: digest,
|
|
92
|
+
...options?.predicateType !== void 0 ? { predicate_type: options.predicateType } : {},
|
|
93
|
+
headers: { "x-github-api-version": API_VERSION }
|
|
94
|
+
}, ListResponse).pipe(Effect.catchIf((error) => error.kind === "notFound" || error.status === 422 && error.kind === "rejected", () => Effect.succeed({ attestations: [] })))).attestations ?? []).flatMap((entry) => {
|
|
95
|
+
const url = entry.bundle_url ?? (entry.id !== void 0 ? `https://github.com/${owner}/${repo}/attestations/${entry.id}` : void 0);
|
|
96
|
+
if (url === void 0) return [];
|
|
97
|
+
return [AttestationListEntry.make({
|
|
98
|
+
url,
|
|
99
|
+
...entry.predicate_type !== void 0 ? { predicateType: entry.predicate_type } : {}
|
|
100
|
+
})];
|
|
101
|
+
});
|
|
102
|
+
})
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
//#endregion
|
|
106
|
+
export { Attestation, AttestationListEntry, AttestationRecord };
|
package/CheckRun.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { GitHubClient } from "./GitHubClient.js";
|
|
2
|
+
import { Repo } from "./Repo.js";
|
|
3
|
+
import { Cause, Context, Effect, Exit, Layer, Ref, Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/CheckRun.ts
|
|
6
|
+
/** How a check run finished. @public */
|
|
7
|
+
const CheckConclusion = Schema.Literals([
|
|
8
|
+
"success",
|
|
9
|
+
"failure",
|
|
10
|
+
"neutral",
|
|
11
|
+
"cancelled",
|
|
12
|
+
"timed_out",
|
|
13
|
+
"action_required",
|
|
14
|
+
"skipped"
|
|
15
|
+
]);
|
|
16
|
+
/** How serious an annotation is. @public */
|
|
17
|
+
const AnnotationLevel = Schema.Literals([
|
|
18
|
+
"notice",
|
|
19
|
+
"warning",
|
|
20
|
+
"failure"
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* One annotation on a check run.
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
var Annotation = class extends Schema.Class("Annotation")({
|
|
28
|
+
/** Repository-relative path. */
|
|
29
|
+
path: Schema.String,
|
|
30
|
+
/** First line of the range, 1-based. */
|
|
31
|
+
startLine: Schema.Int,
|
|
32
|
+
/** Last line of the range, 1-based. */
|
|
33
|
+
endLine: Schema.Int,
|
|
34
|
+
level: AnnotationLevel,
|
|
35
|
+
message: Schema.String,
|
|
36
|
+
title: Schema.optionalKey(Schema.String)
|
|
37
|
+
}) {};
|
|
38
|
+
/**
|
|
39
|
+
* A check run's rendered output.
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* GitHub's limits are **byte** limits, and that distinction is the whole reason
|
|
43
|
+
* this class exists rather than a struct: `✅`, `❌`, `🦋` and `│` cost several
|
|
44
|
+
* bytes each, so a character-count check passes while the request comes back
|
|
45
|
+
* 422 saying *"summary exceeds a maximum bytesize of 65535"*. One consumer hit
|
|
46
|
+
* exactly that and wrote the truncation by hand.
|
|
47
|
+
*
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
var CheckRunOutput = class CheckRunOutput extends Schema.Class("CheckRunOutput")({
|
|
51
|
+
title: Schema.String,
|
|
52
|
+
/** Markdown shown under the title. Capped at 65535 **bytes**. */
|
|
53
|
+
summary: Schema.String,
|
|
54
|
+
/** Longer markdown. Capped at 65535 **bytes**. */
|
|
55
|
+
text: Schema.optionalKey(Schema.String),
|
|
56
|
+
/** At most 50 per request; the rest are dropped by {@link CheckRunOutput.truncated}. */
|
|
57
|
+
annotations: Schema.optionalKey(Schema.Array(Annotation))
|
|
58
|
+
}) {
|
|
59
|
+
/** GitHub's cap on `summary` and `text`, in UTF-8 bytes. */
|
|
60
|
+
static LIMIT_BYTES = 65535;
|
|
61
|
+
/** GitHub's cap on annotations per request. */
|
|
62
|
+
static MAX_ANNOTATIONS = 50;
|
|
63
|
+
/** Appended when a field had to be cut. */
|
|
64
|
+
static NOTICE = "\n\n_…truncated (exceeded GitHub's 65535-byte check limit)._";
|
|
65
|
+
/**
|
|
66
|
+
* This output, cut to fit GitHub's limits.
|
|
67
|
+
*
|
|
68
|
+
* @remarks
|
|
69
|
+
* Pure, so the byte arithmetic is testable with no client, no layer and no
|
|
70
|
+
* network — which is what lets a property test hammer it with arbitrary
|
|
71
|
+
* multi-byte input.
|
|
72
|
+
*/
|
|
73
|
+
truncated() {
|
|
74
|
+
const annotations = this.annotations;
|
|
75
|
+
return CheckRunOutput.make({
|
|
76
|
+
title: this.title,
|
|
77
|
+
summary: capBytes(this.summary),
|
|
78
|
+
...this.text !== void 0 ? { text: capBytes(this.text) } : {},
|
|
79
|
+
...annotations !== void 0 ? { annotations: annotations.slice(0, CheckRunOutput.MAX_ANNOTATIONS) } : {}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Cut `value` to GitHub's byte budget without leaving a broken code point.
|
|
85
|
+
*
|
|
86
|
+
* @remarks
|
|
87
|
+
* Slicing a UTF-8 buffer mid-character decodes to U+FFFD. Splitting a four-byte
|
|
88
|
+
* code point can produce **more than one** replacement character, so the trim
|
|
89
|
+
* loops rather than dropping a single one — which is the hardening this needed
|
|
90
|
+
* over the hand-written version it replaces.
|
|
91
|
+
*/
|
|
92
|
+
const capBytes = (value) => {
|
|
93
|
+
if (Buffer.byteLength(value, "utf8") <= CheckRunOutput.LIMIT_BYTES) return value;
|
|
94
|
+
const budget = CheckRunOutput.LIMIT_BYTES - Buffer.byteLength(CheckRunOutput.NOTICE, "utf8");
|
|
95
|
+
let cut = Buffer.from(value, "utf8").subarray(0, budget).toString("utf8");
|
|
96
|
+
while (cut.endsWith("�")) cut = cut.slice(0, -1);
|
|
97
|
+
return `${cut}${CheckRunOutput.NOTICE}`;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* A check run as GitHub reports it.
|
|
101
|
+
*
|
|
102
|
+
* @public
|
|
103
|
+
*/
|
|
104
|
+
var CheckRunRef = class extends Schema.Class("CheckRunRef")({
|
|
105
|
+
id: Schema.Int,
|
|
106
|
+
name: Schema.String,
|
|
107
|
+
/** The web URL. */
|
|
108
|
+
url: Schema.String,
|
|
109
|
+
status: Schema.String
|
|
110
|
+
}) {};
|
|
111
|
+
/**
|
|
112
|
+
* Check runs.
|
|
113
|
+
*
|
|
114
|
+
* @public
|
|
115
|
+
*/
|
|
116
|
+
var CheckRun = class CheckRun extends Context.Service()("@effected/github/CheckRun") {
|
|
117
|
+
static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
|
|
118
|
+
/** An in-memory double; unstubbed members die naming themselves. */
|
|
119
|
+
static makeTest = (overrides = {}) => ({
|
|
120
|
+
create: overrides.create ?? (() => unstubbed("create")),
|
|
121
|
+
get: overrides.get ?? (() => unstubbed("get")),
|
|
122
|
+
update: overrides.update ?? (() => unstubbed("update")),
|
|
123
|
+
complete: overrides.complete ?? (() => unstubbed("complete")),
|
|
124
|
+
withCheckRun: overrides.withCheckRun ?? (() => unstubbed("withCheckRun"))
|
|
125
|
+
});
|
|
126
|
+
/** {@link CheckRun.makeTest} behind a `Layer`. */
|
|
127
|
+
static layerTest = (overrides = {}) => Layer.succeed(CheckRun, CheckRun.makeTest(overrides));
|
|
128
|
+
};
|
|
129
|
+
const unstubbed = (member) => {
|
|
130
|
+
throw new Error(`CheckRun.makeTest: ${member}() was called but not stubbed — pass an override.`);
|
|
131
|
+
};
|
|
132
|
+
const wireOutput = (output) => {
|
|
133
|
+
const capped = output.truncated();
|
|
134
|
+
return {
|
|
135
|
+
title: capped.title,
|
|
136
|
+
summary: capped.summary,
|
|
137
|
+
...capped.text !== void 0 ? { text: capped.text } : {},
|
|
138
|
+
...capped.annotations !== void 0 ? { annotations: capped.annotations.map((annotation) => ({
|
|
139
|
+
path: annotation.path,
|
|
140
|
+
start_line: annotation.startLine,
|
|
141
|
+
end_line: annotation.endLine,
|
|
142
|
+
annotation_level: annotation.level,
|
|
143
|
+
message: annotation.message,
|
|
144
|
+
...annotation.title !== void 0 ? { title: annotation.title } : {}
|
|
145
|
+
})) } : {}
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* What the bracket concludes when `use` recorded nothing.
|
|
150
|
+
*
|
|
151
|
+
* @remarks
|
|
152
|
+
* **Exit-aware, because a `tap`/`tapError` pair is not.** Those two fire on
|
|
153
|
+
* success and on a *typed* failure; an interrupted `use` — a cancelled
|
|
154
|
+
* workflow, a job timeout, a losing branch of a race — and a defect hit
|
|
155
|
+
* neither, and the run stayed `in_progress` forever. GitHub never reaps such a
|
|
156
|
+
* run, so it blocks branch protection until a human deletes it by hand.
|
|
157
|
+
*/
|
|
158
|
+
const defaultConclusion = (name, exit) => {
|
|
159
|
+
if (Exit.isSuccess(exit)) return {
|
|
160
|
+
conclusion: "success",
|
|
161
|
+
output: CheckRunOutput.make({
|
|
162
|
+
title: name,
|
|
163
|
+
summary: "Completed successfully."
|
|
164
|
+
})
|
|
165
|
+
};
|
|
166
|
+
const cancelled = Cause.hasInterruptsOnly(exit.cause);
|
|
167
|
+
return {
|
|
168
|
+
conclusion: cancelled ? "cancelled" : "failure",
|
|
169
|
+
output: CheckRunOutput.make({
|
|
170
|
+
title: name,
|
|
171
|
+
summary: cancelled ? "Cancelled before completion." : "Failed."
|
|
172
|
+
})
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Conclude a bracketed run: the verdict `use` recorded, or the exit's default.
|
|
177
|
+
*
|
|
178
|
+
* @remarks
|
|
179
|
+
* **`recorded` wins on every exit path**, including failure and interruption.
|
|
180
|
+
* How the *check* ran and how the surrounding *program* ended are different
|
|
181
|
+
* questions, and only `use` knows the first one — a findings-derived
|
|
182
|
+
* `"neutral"` must not be overwritten by a `"cancelled"` just because the job
|
|
183
|
+
* was torn down afterwards.
|
|
184
|
+
*
|
|
185
|
+
* `Effect.onExit` runs its finalizer **uninterruptibly**, which is what lets
|
|
186
|
+
* the concluding request survive the interrupt that triggered it.
|
|
187
|
+
*
|
|
188
|
+
* Only the success path keeps the error channel: failing to record a success
|
|
189
|
+
* is a real failure the caller should see. On the other paths the call is
|
|
190
|
+
* ignored, because neither an interrupt nor an existing failure should be
|
|
191
|
+
* replaced by whatever went wrong while reporting it — and that choice is the
|
|
192
|
+
* **exit's**, independent of whose verdict is being written.
|
|
193
|
+
*/
|
|
194
|
+
const concludeFor = (name, id, exit, recorded, complete) => {
|
|
195
|
+
const settled = recorded ?? defaultConclusion(name, exit);
|
|
196
|
+
const write = complete(id, settled.conclusion, settled.output);
|
|
197
|
+
return Exit.isSuccess(exit) ? write : Effect.ignore(write);
|
|
198
|
+
};
|
|
199
|
+
const refOf = (raw) => CheckRunRef.make({
|
|
200
|
+
id: raw.id,
|
|
201
|
+
name: raw.name,
|
|
202
|
+
url: raw.html_url ?? "",
|
|
203
|
+
status: raw.status
|
|
204
|
+
});
|
|
205
|
+
const make = (client) => {
|
|
206
|
+
const create = Effect.fn("CheckRun.create")(function* (name, headSha) {
|
|
207
|
+
const { owner, repo } = yield* Repo;
|
|
208
|
+
yield* Effect.annotateCurrentSpan({
|
|
209
|
+
owner,
|
|
210
|
+
repo,
|
|
211
|
+
name,
|
|
212
|
+
headSha
|
|
213
|
+
});
|
|
214
|
+
const created = yield* client.request("POST /repos/{owner}/{repo}/check-runs", {
|
|
215
|
+
owner,
|
|
216
|
+
repo,
|
|
217
|
+
name,
|
|
218
|
+
head_sha: headSha,
|
|
219
|
+
status: "in_progress",
|
|
220
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
221
|
+
});
|
|
222
|
+
return refOf(created);
|
|
223
|
+
});
|
|
224
|
+
const complete = Effect.fn("CheckRun.complete")(function* (id, conclusion, output) {
|
|
225
|
+
const { owner, repo } = yield* Repo;
|
|
226
|
+
yield* Effect.annotateCurrentSpan({
|
|
227
|
+
owner,
|
|
228
|
+
repo,
|
|
229
|
+
id,
|
|
230
|
+
conclusion
|
|
231
|
+
});
|
|
232
|
+
yield* client.request("PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}", {
|
|
233
|
+
owner,
|
|
234
|
+
repo,
|
|
235
|
+
check_run_id: id,
|
|
236
|
+
status: "completed",
|
|
237
|
+
conclusion,
|
|
238
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
239
|
+
...output !== void 0 ? { output: wireOutput(output) } : {}
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
return {
|
|
243
|
+
create,
|
|
244
|
+
complete,
|
|
245
|
+
get: Effect.fn("CheckRun.get")(function* (id) {
|
|
246
|
+
const { owner, repo } = yield* Repo;
|
|
247
|
+
yield* Effect.annotateCurrentSpan({
|
|
248
|
+
owner,
|
|
249
|
+
repo,
|
|
250
|
+
id
|
|
251
|
+
});
|
|
252
|
+
const raw = yield* client.request("GET /repos/{owner}/{repo}/check-runs/{check_run_id}", {
|
|
253
|
+
owner,
|
|
254
|
+
repo,
|
|
255
|
+
check_run_id: id
|
|
256
|
+
});
|
|
257
|
+
return refOf(raw);
|
|
258
|
+
}),
|
|
259
|
+
update: Effect.fn("CheckRun.update")(function* (id, output) {
|
|
260
|
+
const { owner, repo } = yield* Repo;
|
|
261
|
+
yield* Effect.annotateCurrentSpan({
|
|
262
|
+
owner,
|
|
263
|
+
repo,
|
|
264
|
+
id
|
|
265
|
+
});
|
|
266
|
+
yield* client.request("PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}", {
|
|
267
|
+
owner,
|
|
268
|
+
repo,
|
|
269
|
+
check_run_id: id,
|
|
270
|
+
output: wireOutput(output)
|
|
271
|
+
});
|
|
272
|
+
}),
|
|
273
|
+
withCheckRun: (name, headSha, use) => Effect.gen(function* () {
|
|
274
|
+
const run = yield* create(name, headSha);
|
|
275
|
+
const recorded = yield* Ref.make(void 0);
|
|
276
|
+
const conclude = (conclusion, output) => Ref.set(recorded, {
|
|
277
|
+
conclusion,
|
|
278
|
+
output
|
|
279
|
+
});
|
|
280
|
+
return yield* use(run.id, conclude).pipe(Effect.onExit((exit) => Effect.flatMap(Ref.get(recorded), (chosen) => concludeFor(name, run.id, exit, chosen, complete))));
|
|
281
|
+
})
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
//#endregion
|
|
286
|
+
export { Annotation, AnnotationLevel, CheckConclusion, CheckRun, CheckRunOutput, CheckRunRef };
|
package/GitBranch.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { GitHubError } from "./GitHubError.js";
|
|
2
|
+
import { GraphQLDocument } from "./GraphQL.js";
|
|
3
|
+
import { GitHubClient } from "./GitHubClient.js";
|
|
4
|
+
import { Repo } from "./Repo.js";
|
|
5
|
+
import { Context, Effect, Layer, Option, Schema } from "effect";
|
|
6
|
+
|
|
7
|
+
//#region src/GitBranch.ts
|
|
8
|
+
const CreateLinkedBranch = GraphQLDocument.make({
|
|
9
|
+
name: "createLinkedBranch",
|
|
10
|
+
document: `mutation ($issueId: ID!, $name: String!, $oid: GitObjectID!, $repositoryId: ID!) {
|
|
11
|
+
createLinkedBranch(input: { issueId: $issueId, name: $name, oid: $oid, repositoryId: $repositoryId }) {
|
|
12
|
+
linkedBranch { id }
|
|
13
|
+
}
|
|
14
|
+
}`,
|
|
15
|
+
response: Schema.Struct({})
|
|
16
|
+
})();
|
|
17
|
+
/**
|
|
18
|
+
* Branches, as refs.
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
var GitBranch = class GitBranch extends Context.Service()("@effected/github/GitBranch") {
|
|
23
|
+
/**
|
|
24
|
+
* @remarks
|
|
25
|
+
* The callback is written `(client) => make(client)` rather than passed as
|
|
26
|
+
* `make` directly, and that is load-bearing: a static initializer runs while
|
|
27
|
+
* the module body is still evaluating, so naming a `const` declared further
|
|
28
|
+
* down throws `Cannot access 'make' before initialization` **at import time**,
|
|
29
|
+
* with a clean typecheck. Wrapping it in an arrow defers the read to when the
|
|
30
|
+
* layer is built.
|
|
31
|
+
*/
|
|
32
|
+
static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
|
|
33
|
+
/** An in-memory double; unstubbed members die naming themselves. */
|
|
34
|
+
static makeTest = (overrides = {}) => ({
|
|
35
|
+
create: overrides.create ?? (() => unstubbed("create")),
|
|
36
|
+
upsert: overrides.upsert ?? (() => unstubbed("upsert")),
|
|
37
|
+
exists: overrides.exists ?? (() => unstubbed("exists")),
|
|
38
|
+
sha: overrides.sha ?? (() => unstubbed("sha")),
|
|
39
|
+
shaOption: overrides.shaOption ?? (() => unstubbed("shaOption")),
|
|
40
|
+
reset: overrides.reset ?? (() => unstubbed("reset")),
|
|
41
|
+
delete: overrides.delete ?? (() => unstubbed("delete")),
|
|
42
|
+
createLinked: overrides.createLinked ?? (() => unstubbed("createLinked"))
|
|
43
|
+
});
|
|
44
|
+
/** {@link GitBranch.makeTest} behind a `Layer`. */
|
|
45
|
+
static layerTest = (overrides = {}) => Layer.succeed(GitBranch, GitBranch.makeTest(overrides));
|
|
46
|
+
};
|
|
47
|
+
const unstubbed = (member) => {
|
|
48
|
+
throw new Error(`GitBranch.makeTest: ${member}() was called but not stubbed — pass an override.`);
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* `main`, `refs/heads/main` and `heads/main` all name the same branch.
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* GitHub's own API is inconsistent here — creation wants the full
|
|
55
|
+
* `refs/heads/x`, every other operation wants `heads/x` — so callers routinely
|
|
56
|
+
* pass whichever they last saw. Normalizing once is cheaper than documenting it
|
|
57
|
+
* seven times.
|
|
58
|
+
*/
|
|
59
|
+
const shortName = (name) => name.replace(/^refs\/heads\//, "").replace(/^heads\//, "");
|
|
60
|
+
const rejectEmpty = (operation, name) => {
|
|
61
|
+
const short = shortName(name).trim();
|
|
62
|
+
return short === "" ? Effect.fail(GitHubError.rejected(operation, 422, "a branch name is required")) : Effect.succeed(short);
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Every method resolves {@link Repo} per call rather than once at layer
|
|
66
|
+
* construction.
|
|
67
|
+
*
|
|
68
|
+
* @remarks
|
|
69
|
+
* This is what makes `Repo.provide(other)` mean something. Capturing the
|
|
70
|
+
* coordinate when the layer is built would give each method `R = never` — the
|
|
71
|
+
* house pattern for a stable dependency — but it would also make a scoped
|
|
72
|
+
* override silently do nothing, because the resource would already hold the
|
|
73
|
+
* repository it was built with. The repository is precisely the dependency that
|
|
74
|
+
* is *not* stable: silk-sync-action loops one program over many target
|
|
75
|
+
* repositories.
|
|
76
|
+
*
|
|
77
|
+
* The client stays resolved at construction, so this costs one context read per
|
|
78
|
+
* call and nothing else.
|
|
79
|
+
*/
|
|
80
|
+
const make = (client) => {
|
|
81
|
+
const create = Effect.fn("GitBranch.create")(function* (branch, sha) {
|
|
82
|
+
const { owner, repo } = yield* Repo;
|
|
83
|
+
const short = yield* rejectEmpty("GitBranch.create", branch);
|
|
84
|
+
yield* Effect.annotateCurrentSpan({
|
|
85
|
+
owner,
|
|
86
|
+
repo,
|
|
87
|
+
branch: short
|
|
88
|
+
});
|
|
89
|
+
yield* client.request("POST /repos/{owner}/{repo}/git/refs", {
|
|
90
|
+
owner,
|
|
91
|
+
repo,
|
|
92
|
+
ref: `refs/heads/${short}`,
|
|
93
|
+
sha
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
const reset = Effect.fn("GitBranch.reset")(function* (branch, sha) {
|
|
97
|
+
const { owner, repo } = yield* Repo;
|
|
98
|
+
const short = yield* rejectEmpty("GitBranch.reset", branch);
|
|
99
|
+
yield* Effect.annotateCurrentSpan({
|
|
100
|
+
owner,
|
|
101
|
+
repo,
|
|
102
|
+
branch: short
|
|
103
|
+
});
|
|
104
|
+
yield* client.request("PATCH /repos/{owner}/{repo}/git/refs/{ref}", {
|
|
105
|
+
owner,
|
|
106
|
+
repo,
|
|
107
|
+
ref: `heads/${short}`,
|
|
108
|
+
sha,
|
|
109
|
+
force: true
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
const upsert = Effect.fn("GitBranch.upsert")(function* (branch, sha) {
|
|
113
|
+
const short = yield* rejectEmpty("GitBranch.upsert", branch);
|
|
114
|
+
yield* Effect.annotateCurrentSpan({ branch: short });
|
|
115
|
+
return yield* create(short, sha).pipe(Effect.as("created"), Effect.catchIf(GitHubError.hasKind("alreadyExists"), () => Effect.as(reset(short, sha), "reset")));
|
|
116
|
+
});
|
|
117
|
+
const readRef = (operation, branch) => Effect.gen(function* () {
|
|
118
|
+
const { owner, repo } = yield* Repo;
|
|
119
|
+
const short = yield* rejectEmpty(operation, branch);
|
|
120
|
+
return yield* client.request("GET /repos/{owner}/{repo}/git/ref/{ref}", {
|
|
121
|
+
owner,
|
|
122
|
+
repo,
|
|
123
|
+
ref: `heads/${short}`
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
const shaOption = Effect.fn("GitBranch.shaOption")(function* (branch) {
|
|
127
|
+
yield* Effect.annotateCurrentSpan({ branch });
|
|
128
|
+
return yield* readRef("GitBranch.shaOption", branch).pipe(Effect.map((ref) => Option.some(ref.object.sha)), Effect.catchIf(GitHubError.hasKind("notFound"), () => Effect.succeed(Option.none())));
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
create,
|
|
132
|
+
upsert,
|
|
133
|
+
reset,
|
|
134
|
+
exists: Effect.fn("GitBranch.exists")(function* (branch) {
|
|
135
|
+
yield* Effect.annotateCurrentSpan({ branch });
|
|
136
|
+
return yield* Effect.map(shaOption(branch), Option.isSome);
|
|
137
|
+
}),
|
|
138
|
+
sha: Effect.fn("GitBranch.sha")(function* (branch) {
|
|
139
|
+
yield* Effect.annotateCurrentSpan({ branch });
|
|
140
|
+
return (yield* readRef("GitBranch.sha", branch)).object.sha;
|
|
141
|
+
}),
|
|
142
|
+
shaOption,
|
|
143
|
+
createLinked: Effect.fn("GitBranch.createLinked")(function* (input) {
|
|
144
|
+
const short = shortName(input.name);
|
|
145
|
+
yield* Effect.annotateCurrentSpan({ branch: short });
|
|
146
|
+
yield* client.graphql(CreateLinkedBranch, {
|
|
147
|
+
issueId: input.issueNodeId,
|
|
148
|
+
repositoryId: input.repositoryNodeId,
|
|
149
|
+
name: short,
|
|
150
|
+
oid: input.sha
|
|
151
|
+
});
|
|
152
|
+
}),
|
|
153
|
+
delete: Effect.fn("GitBranch.delete")(function* (branch) {
|
|
154
|
+
const { owner, repo } = yield* Repo;
|
|
155
|
+
const short = yield* rejectEmpty("GitBranch.delete", branch);
|
|
156
|
+
yield* Effect.annotateCurrentSpan({
|
|
157
|
+
owner,
|
|
158
|
+
repo,
|
|
159
|
+
branch: short
|
|
160
|
+
});
|
|
161
|
+
yield* client.request("DELETE /repos/{owner}/{repo}/git/refs/{ref}", {
|
|
162
|
+
owner,
|
|
163
|
+
repo,
|
|
164
|
+
ref: `heads/${short}`
|
|
165
|
+
});
|
|
166
|
+
})
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
//#endregion
|
|
171
|
+
export { GitBranch };
|