@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.
@@ -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 };