@effected/github 0.5.0 → 0.6.1

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/CheckRun.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { GitHubClient } from "./GitHubClient.js";
2
2
  import { Repo } from "./Repo.js";
3
+ import { numericId } from "./internal/ids.js";
3
4
  import { Cause, Context, Effect, Exit, Layer, Ref, Schema } from "effect";
4
5
 
5
6
  //#region src/CheckRun.ts
@@ -197,7 +198,7 @@ const concludeFor = (name, id, exit, recorded, complete) => {
197
198
  return Exit.isSuccess(exit) ? write : Effect.ignore(write);
198
199
  };
199
200
  const refOf = (raw) => CheckRunRef.make({
200
- id: raw.id,
201
+ id: numericId(raw.id),
201
202
  name: raw.name,
202
203
  url: raw.html_url ?? "",
203
204
  status: raw.status
package/GitHubApp.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { GitHubError } from "./GitHubError.js";
2
2
  import { GitHubGraphQLError } from "./GraphQL.js";
3
3
  import { GitHubClient, makeClientShape } from "./GitHubClient.js";
4
+ import { numericId } from "./internal/ids.js";
4
5
  import { Clock, Context, DateTime, Duration, Effect, Layer, Option, Redacted, Ref, Schema, Stream } from "effect";
5
6
  import githubAppJwt from "universal-github-app-jwt";
6
7
 
@@ -261,7 +262,7 @@ function makeApp(options) {
261
262
  return Effect.sync(() => {
262
263
  const installations = Effect.fn("GitHubApp.installations")(function* (credentials) {
263
264
  return (yield* (yield* asApp(credentials, options)).paginate("GET /app/installations", {}).pipe(Effect.catch(appFailure("installation")))).map((entry) => Installation.make({
264
- id: entry.id,
265
+ id: numericId(entry.id),
265
266
  ...entry.account !== null && entry.account !== void 0 && "login" in entry.account ? { account: entry.account.login } : {}
266
267
  }));
267
268
  });
@@ -304,7 +305,7 @@ function makeApp(options) {
304
305
  return AppIdentity.make({
305
306
  slug,
306
307
  name,
307
- ...Option.isSome(user) ? { userId: user.value.id } : {}
308
+ ...Option.isSome(user) ? { userId: numericId(user.value.id) } : {}
308
309
  });
309
310
  }),
310
311
  installations
package/GitHubIssue.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { GraphQLDocument } from "./GraphQL.js";
2
2
  import { GitHubClient } from "./GitHubClient.js";
3
3
  import { Repo } from "./Repo.js";
4
+ import { numericId } from "./internal/ids.js";
5
+ import { CommentRecord } from "./PullRequestComment.js";
4
6
  import { Context, Effect, Layer, Schema } from "effect";
5
7
 
6
8
  //#region src/GitHubIssue.ts
@@ -42,6 +44,22 @@ var LinkedIssue = class extends Schema.Class("LinkedIssue")({
42
44
  */
43
45
  userLinked: Schema.Boolean
44
46
  }) {};
47
+ /**
48
+ * What {@link GitHubIssueShape.commentOnce} found or wrote.
49
+ *
50
+ * @remarks
51
+ * `wrote` is the field the caller branches on: `true` means this call created
52
+ * the comment, `false` means the marker was already on the issue and nothing
53
+ * was posted. Either way `comment` is the marked comment itself.
54
+ *
55
+ * @public
56
+ */
57
+ var CommentOnceResult = class extends Schema.Class("CommentOnceResult")({
58
+ /** Did this call post the comment (`true`), or find it already there (`false`)? */
59
+ wrote: Schema.Boolean,
60
+ /** The marked comment — the one created, or the one that made us skip. */
61
+ comment: CommentRecord
62
+ }) {};
45
63
  const IssueNodes = Schema.Struct({
46
64
  id: Schema.String,
47
65
  number: Schema.Int,
@@ -98,6 +116,7 @@ var GitHubIssue = class GitHubIssue extends Context.Service()("@effected/github/
98
116
  list: overrides.list ?? (() => unstubbed("list")),
99
117
  close: overrides.close ?? (() => unstubbed("close")),
100
118
  comment: overrides.comment ?? (() => unstubbed("comment")),
119
+ commentOnce: overrides.commentOnce ?? (() => unstubbed("commentOnce")),
101
120
  linkedIssues: overrides.linkedIssues ?? (() => unstubbed("linkedIssues")),
102
121
  isCrossReferencedBy: overrides.isCrossReferencedBy ?? (() => unstubbed("isCrossReferencedBy"))
103
122
  });
@@ -194,13 +213,52 @@ const make = (client) => ({
194
213
  repo,
195
214
  number
196
215
  });
197
- return (yield* client.request("POST /repos/{owner}/{repo}/issues/{issue_number}/comments", {
216
+ const created = yield* client.request("POST /repos/{owner}/{repo}/issues/{issue_number}/comments", {
198
217
  owner,
199
218
  repo,
200
219
  issue_number: number,
201
220
  body,
202
221
  headers: API_VERSION_HEADERS
203
- })).id;
222
+ });
223
+ return numericId(created.id);
224
+ }),
225
+ commentOnce: Effect.fn("GitHubIssue.commentOnce")(function* (issueNumber, marker, body) {
226
+ const { owner, repo } = yield* Repo;
227
+ yield* Effect.annotateCurrentSpan({
228
+ owner,
229
+ repo,
230
+ issueNumber,
231
+ marker: marker.key
232
+ });
233
+ const existing = (yield* client.paginate("GET /repos/{owner}/{repo}/issues/{issue_number}/comments", {
234
+ owner,
235
+ repo,
236
+ issue_number: issueNumber,
237
+ headers: API_VERSION_HEADERS
238
+ })).find((comment) => marker.matches(comment.body ?? ""));
239
+ if (existing !== void 0) return CommentOnceResult.make({
240
+ wrote: false,
241
+ comment: CommentRecord.make({
242
+ id: numericId(existing.id),
243
+ body: existing.body ?? "",
244
+ url: existing.html_url
245
+ })
246
+ });
247
+ const created = yield* client.request("POST /repos/{owner}/{repo}/issues/{issue_number}/comments", {
248
+ owner,
249
+ repo,
250
+ issue_number: issueNumber,
251
+ body: `${body}\n\n${marker.html}`,
252
+ headers: API_VERSION_HEADERS
253
+ });
254
+ return CommentOnceResult.make({
255
+ wrote: true,
256
+ comment: CommentRecord.make({
257
+ id: numericId(created.id),
258
+ body: created.body ?? "",
259
+ url: created.html_url
260
+ })
261
+ });
204
262
  }),
205
263
  linkedIssues: Effect.fn("GitHubIssue.linkedIssues")(function* (prNumber) {
206
264
  const { owner, repo } = yield* Repo;
@@ -241,4 +299,4 @@ const make = (client) => ({
241
299
  });
242
300
 
243
301
  //#endregion
244
- export { GitHubIssue, IssueInfo, LinkedIssue };
302
+ export { CommentOnceResult, GitHubIssue, IssueInfo, LinkedIssue };
@@ -0,0 +1,126 @@
1
+ import { Option } from "effect";
2
+
3
+ //#region src/IssueReferences.ts
4
+ /**
5
+ * GitHub's closing-keyword issue-reference grammar, as pure functions.
6
+ *
7
+ * @remarks
8
+ * GitHub links an issue to a pull request when the PR's description carries
9
+ * `<keyword> #<number>` for one of nine documented keywords. Consumers speak
10
+ * that grammar in two distinct dialects, and this module models exactly those
11
+ * two — no service, no layer, nothing but strings in and values out:
12
+ *
13
+ * - **Inline-in-prose** ({@link harvestIssueReferences}): a reference may appear anywhere in
14
+ * running text — `"fixes #12 and closes #13"` — with mandatory whitespace
15
+ * and **no colon**, because that is the spelling GitHub itself scans PR
16
+ * bodies for. This is the dialect a release pipeline harvests from commit
17
+ * subjects and PR descriptions.
18
+ * - **Bare-line** ({@link parseBareLineReference}): the whole line, after trimming, *is*
19
+ * the reference — `"Closes: #12"` — with an **optional colon**, because a
20
+ * generated references region writes one reference per line and a colon
21
+ * reads better there. GitHub does not require the colon; the region format
22
+ * allows it, so the parser must too. The dialects differ because their
23
+ * producers do: prose is written by humans for GitHub's scanner, the region
24
+ * is written by tooling for humans.
25
+ *
26
+ * Deliberately out of scope this round: cross-repo references
27
+ * (`owner/repo#N`) and full-URL references
28
+ * (`https://github.com/owner/repo/issues/N`). Both are real GitHub spellings;
29
+ * neither dialect's consumers emit them yet, and guessing at their shape here
30
+ * would freeze an API nobody has driven.
31
+ */
32
+ /**
33
+ * The nine closing keywords GitHub documents, lowercased.
34
+ *
35
+ * @public
36
+ */
37
+ const CLOSING_KEYWORDS = [
38
+ "close",
39
+ "closes",
40
+ "closed",
41
+ "fix",
42
+ "fixes",
43
+ "fixed",
44
+ "resolve",
45
+ "resolves",
46
+ "resolved"
47
+ ];
48
+ /**
49
+ * Both patterns derive from {@link CLOSING_KEYWORDS} so the constant and the
50
+ * grammar cannot drift. Alternation order is safe: the regex engine backtracks
51
+ * through alternatives, so `close` matching inside `closes` fails at the
52
+ * mandatory `#`-introducer and retries the longer keyword.
53
+ */
54
+ const KEYWORDS = CLOSING_KEYWORDS.join("|");
55
+ /** Inline-in-prose: mandatory whitespace, no colon. */
56
+ const INLINE_PATTERN = new RegExp(`\\b(${KEYWORDS})\\s+#(\\d+)`, "gi");
57
+ /**
58
+ * Bare-line: optional colon, then mandatory same-line whitespace. `[ \t]`
59
+ * rather than `\s`, so an embedded newline cannot smuggle two lines past a
60
+ * parser whose contract is one.
61
+ */
62
+ const BARE_LINE_PATTERN = new RegExp(`^(${KEYWORDS}):?[ \\t]+#(\\d+)$`, "i");
63
+ /**
64
+ * `#<digits>` parsed to a number — or `undefined` when the digits exceed
65
+ * `Number.MAX_SAFE_INTEGER`, because a silently rounded issue number is worse
66
+ * than a skipped match. Callers skip such matches; they do not fail.
67
+ */
68
+ const safeIssueNumber = (digits) => {
69
+ const value = Number(digits);
70
+ return Number.isSafeInteger(value) ? value : void 0;
71
+ };
72
+ /**
73
+ * Every inline closing reference in `text`, in document order.
74
+ *
75
+ * @remarks
76
+ * The **inline-in-prose** dialect: case-insensitive `<keyword> #<number>`
77
+ * anywhere in the text, whitespace mandatory, colon not accepted — a colon
78
+ * spelling belongs to the bare-line dialect and {@link parseBareLineReference}.
79
+ * Duplicates are preserved: whether `fixes #1, fixes #1` means one reference
80
+ * or two is the caller's business, not a parser's.
81
+ *
82
+ * A match whose digits exceed `Number.MAX_SAFE_INTEGER` is skipped, not
83
+ * misparsed — see the module remarks for the scope boundary.
84
+ *
85
+ * @public
86
+ */
87
+ const harvestIssueReferences = (text) => {
88
+ const references = [];
89
+ for (const match of text.matchAll(INLINE_PATTERN)) {
90
+ const issueNumber = safeIssueNumber(match[2] ?? "");
91
+ if (issueNumber === void 0) continue;
92
+ references.push({
93
+ issueNumber,
94
+ keyword: (match[1] ?? "").toLowerCase(),
95
+ start: match.index,
96
+ end: match.index + match[0].length
97
+ });
98
+ }
99
+ return references;
100
+ };
101
+ /**
102
+ * The reference a whole line carries, or `Option.none()`.
103
+ *
104
+ * @remarks
105
+ * The **bare-line** dialect: after trimming, the entire line must be
106
+ * `<keyword>[:] #<number>` — keyword case-insensitive, colon optional,
107
+ * whitespace before the `#` mandatory. Trailing prose, a missing keyword, or
108
+ * anything else at all is a rejection, never a partial parse; a line carries
109
+ * one reference or none. Digits past `Number.MAX_SAFE_INTEGER` reject too,
110
+ * for the same reason {@link harvestIssueReferences} skips them.
111
+ *
112
+ * @public
113
+ */
114
+ const parseBareLineReference = (line) => {
115
+ const match = BARE_LINE_PATTERN.exec(line.trim());
116
+ if (match === null) return Option.none();
117
+ const issueNumber = safeIssueNumber(match[2] ?? "");
118
+ if (issueNumber === void 0) return Option.none();
119
+ return Option.some({
120
+ issueNumber,
121
+ keyword: (match[1] ?? "").toLowerCase()
122
+ });
123
+ };
124
+
125
+ //#endregion
126
+ export { CLOSING_KEYWORDS, harvestIssueReferences, parseBareLineReference };
@@ -1,5 +1,6 @@
1
1
  import { GitHubClient } from "./GitHubClient.js";
2
2
  import { Repo } from "./Repo.js";
3
+ import { numericId } from "./internal/ids.js";
3
4
  import { Context, Effect, Layer, Option, Schema } from "effect";
4
5
 
5
6
  //#region src/PullRequestComment.ts
@@ -61,7 +62,7 @@ const unstubbed = (member) => {
61
62
  throw new Error(`PullRequestComment.makeTest: ${member}() was called but not stubbed — pass an override.`);
62
63
  };
63
64
  const recordOf = (raw) => CommentRecord.make({
64
- id: raw.id,
65
+ id: numericId(raw.id),
65
66
  body: raw.body ?? "",
66
67
  url: raw.html_url
67
68
  });
package/README.md CHANGED
@@ -101,6 +101,45 @@ const program = Effect.gen(function* () {
101
101
 
102
102
  `GitTag.latestSemver` walks tags and picks the newest version-shaped one in a single pass, over `@effected/semver`'s synchronous comparator — no round trip per candidate. `PullRequest.upsert` and `PullRequestComment.upsert` (a marker-tagged sticky comment) follow the same one-call-one-intent shape.
103
103
 
104
+ ## Issues and closing references
105
+
106
+ `GitHubIssue` covers issues: read, list, create, close, comment, and the GraphQL side that answers which issues a pull request closes. `commentOnce` is the create-or-skip counterpart to `PullRequestComment.upsert` — it posts a marked comment and never edits one, which is what you want for an announcement that would read as a rewrite of history if it changed after the fact:
107
+
108
+ ```ts
109
+ import { CommentMarker, GitHubIssue } from "@effected/github";
110
+ import { Effect } from "effect";
111
+
112
+ const marker = CommentMarker.make({ namespace: "release-bot", key: "shipped" });
113
+
114
+ const program = Effect.gen(function* () {
115
+ const issues = yield* GitHubIssue;
116
+ return yield* issues.commentOnce(1873, marker, "Shipped in the release just published.");
117
+ });
118
+ // CommentOnceResult — `wrote: true` when this call posted the comment,
119
+ // `wrote: false` when the marker was already there and nothing was sent.
120
+ // `comment` is the marked comment either way.
121
+ ```
122
+
123
+ The existence check pages the issue's comments to the end and matches the marker in `upsert`'s exact spelling, so a comment either member writes stays findable by the other. It is a look-before-write: two runs racing on the same issue can both see no marker and both post, so treat the marker as the record rather than the check as a lock.
124
+
125
+ Whether a run should comment at all usually turns on which issues a pull request closes, and that is a grammar rather than an API call. `IssueReferences` is that grammar as plain functions — no service, no layer, nothing but strings in — in the two dialects consumers actually write:
126
+
127
+ ```ts
128
+ import { harvestIssueReferences, parseBareLineReference } from "@effected/github";
129
+
130
+ console.log(harvestIssueReferences("Fixes #12 and closes #13."));
131
+ // [ { issueNumber: 12, keyword: "fixes", start: 0, end: 9 },
132
+ // { issueNumber: 13, keyword: "closes", start: 14, end: 24 } ]
133
+
134
+ console.log(parseBareLineReference("Closes: #12"));
135
+ // Option.some({ issueNumber: 12, keyword: "closes" })
136
+
137
+ console.log(parseBareLineReference("closes #12 for real"));
138
+ // Option.none() — the bare-line dialect takes the whole line or nothing
139
+ ```
140
+
141
+ `harvestIssueReferences` reads the **inline-in-prose** dialect: one of the nine closing keywords (`CLOSING_KEYWORDS`) followed by whitespace and `#<number>`, anywhere in the text, no colon — the spelling GitHub itself scans a pull request body for. Duplicates come back as written, because whether `fixes #1, fixes #1` means one intent or two is the caller's question. `parseBareLineReference` reads the **bare-line** dialect, where the whole trimmed line is the reference and the colon is optional — the shape a generated references block writes, one per line. Cross-repo (`owner/repo#12`) and full-URL references are out of scope.
142
+
104
143
  ## Repository configuration
105
144
 
106
145
  Six services cover the half of a repository that is policy rather than content: `RepositorySecret`, `RepositoryVariable`, `Ruleset`, `DeploymentEnvironment`, `RepositorySecurity` and `CodeScanning`. They are shaped for a program applying the same configuration across a fleet, so every list read paginates to the end and a truncated page never becomes a wrong decision.
@@ -233,6 +272,8 @@ const TestClient = GitHubClient.layerFixture(fixtures);
233
272
  - `GitBranch` / `GitTag` — Git Database API refs, with `upsert` collapsing the create-or-reset dance to one call and `GitTag.latestSemver` picking the newest version-shaped tag in one pass.
234
273
  - `CheckRun` — `withCheckRun` concludes on every exit path; `CheckRunOutput.truncated()` cuts rendered output to GitHub's byte limits.
235
274
  - `PullRequest` / `PullRequestComment` — upserts for both, `listFiles` answering with the same full `CommitFile` records a commit read returns, `headSha`/`baseSha` on `PullRequestInfo`, plus `CommentMarker` for finding a sticky comment again.
275
+ - `GitHubIssue` — issues and their comments, including `commentOnce` for a marked comment that is posted exactly once and never edited, and `linkedIssues` for what a pull request closes.
276
+ - `IssueReferences` — GitHub's nine closing keywords as pure functions: `harvestIssueReferences` for references inline in prose, `parseBareLineReference` for one-per-line generated ones.
236
277
  - `GitHubRelease` — releases and asset uploads, including the one route (`uploadAsset`, with the endpoint's optional display label) outside GitHub's generated endpoint map.
237
278
  - `RepositorySecret` / `RepositoryVariable` — repository and environment secrets and variables, with the sealed-box encryption GitHub's secrets API demands kept in one module nothing else imports.
238
279
  - `Ruleset` / `DeploymentEnvironment` / `RepositorySecurity` / `CodeScanning` — the rest of the configuration tier: rulesets matched by name and scope, idempotent environment writes, the three security toggles GitHub keeps off the repository endpoint, and CodeQL default setup with the language detection that gates it.
package/index.d.ts CHANGED
@@ -1945,6 +1945,85 @@ declare class GitHubContent extends GitHubContent_base {
1945
1945
  static readonly layerTest: (overrides?: Partial<GitHubContentShape>) => Layer.Layer<GitHubContent>;
1946
1946
  }
1947
1947
  //#endregion
1948
+ //#region src/PullRequestComment.d.ts
1949
+ declare const CommentMarker_base: Schema.Class<CommentMarker, Schema.Struct<{
1950
+ /** Whose comments these are, e.g. your action's name. */
1951
+ readonly namespace: Schema.NonEmptyString;
1952
+ /** Which comment, within that namespace. */
1953
+ readonly key: Schema.NonEmptyString;
1954
+ }>, {}>;
1955
+ /**
1956
+ * The hidden marker that makes a comment findable again.
1957
+ *
1958
+ * @remarks
1959
+ * A pure class, not a hardcoded string. The surface this replaces baked
1960
+ * `<!-- savvy-web:${key} -->` into the library — one vendor's name, inside a
1961
+ * package meant to be general. Here the namespace is the caller's, the marker is
1962
+ * testable without a client, and the library has no opinion about whose comments
1963
+ * these are.
1964
+ *
1965
+ * @public
1966
+ */
1967
+ declare class CommentMarker extends CommentMarker_base {
1968
+ /** The HTML comment appended to a body so the comment can be found again. */
1969
+ get html(): string;
1970
+ /** Does this body carry the marker? */
1971
+ matches(body: string): boolean;
1972
+ }
1973
+ declare const CommentRecord_base: Schema.Class<CommentRecord, Schema.Struct<{
1974
+ readonly id: Schema.Int;
1975
+ readonly body: Schema.String;
1976
+ readonly url: Schema.String;
1977
+ }>, {}>;
1978
+ /**
1979
+ * A comment this package wrote or found.
1980
+ *
1981
+ * @public
1982
+ */
1983
+ declare class CommentRecord extends CommentRecord_base {}
1984
+ /**
1985
+ * Sticky comments on a pull request or issue.
1986
+ *
1987
+ * @public
1988
+ */
1989
+ interface PullRequestCommentShape {
1990
+ /** Post a new comment. */
1991
+ readonly create: (issueNumber: number, body: string) => Effect.Effect<CommentRecord, GitHubError, Repo>;
1992
+ /**
1993
+ * Update the marked comment if there is one, or post it.
1994
+ *
1995
+ * @remarks
1996
+ * The marker is appended to the body, so a comment written by `upsert` is
1997
+ * always findable by the same marker afterwards.
1998
+ */
1999
+ readonly upsert: (issueNumber: number, marker: CommentMarker, body: string) => Effect.Effect<CommentRecord, GitHubError, Repo>;
2000
+ /**
2001
+ * Find the marked comment.
2002
+ *
2003
+ * @remarks
2004
+ * **Paginates.** The version this replaces requested a single page of 100 and
2005
+ * stopped, so on a busy pull request the marker silently vanished and every
2006
+ * update posted a new comment instead.
2007
+ */
2008
+ readonly find: (issueNumber: number, marker: CommentMarker, options?: {
2009
+ readonly page?: PageOptions | undefined;
2010
+ }) => Effect.Effect<Option.Option<CommentRecord>, GitHubError, Repo>;
2011
+ readonly delete: (commentId: number) => Effect.Effect<void, GitHubError, Repo>;
2012
+ }
2013
+ declare const PullRequestComment_base: Context.ServiceClass<PullRequestComment, "@effected/github/PullRequestComment", PullRequestCommentShape>;
2014
+ /**
2015
+ * Sticky comments.
2016
+ *
2017
+ * @public
2018
+ */
2019
+ declare class PullRequestComment extends PullRequestComment_base {
2020
+ static readonly layer: Layer.Layer<PullRequestComment, never, GitHubClient>;
2021
+ /** An in-memory double; unstubbed members die naming themselves. */
2022
+ static readonly makeTest: (overrides?: Partial<PullRequestCommentShape>) => PullRequestCommentShape;
2023
+ /** {@link PullRequestComment.makeTest} behind a `Layer`. */
2024
+ static readonly layerTest: (overrides?: Partial<PullRequestCommentShape>) => Layer.Layer<PullRequestComment>;
2025
+ }
2026
+ //#endregion
1948
2027
  //#region src/GitHubIssue.d.ts
1949
2028
  declare const IssueInfo_base: Schema.Class<IssueInfo, Schema.Struct<{
1950
2029
  readonly number: Schema.Int;
@@ -1986,6 +2065,23 @@ declare const LinkedIssue_base: Schema.Class<LinkedIssue, Schema.Struct<{
1986
2065
  * @public
1987
2066
  */
1988
2067
  declare class LinkedIssue extends LinkedIssue_base {}
2068
+ declare const CommentOnceResult_base: Schema.Class<CommentOnceResult, Schema.Struct<{
2069
+ /** Did this call post the comment (`true`), or find it already there (`false`)? */
2070
+ readonly wrote: Schema.Boolean;
2071
+ /** The marked comment — the one created, or the one that made us skip. */
2072
+ readonly comment: typeof CommentRecord;
2073
+ }>, {}>;
2074
+ /**
2075
+ * What {@link GitHubIssueShape.commentOnce} found or wrote.
2076
+ *
2077
+ * @remarks
2078
+ * `wrote` is the field the caller branches on: `true` means this call created
2079
+ * the comment, `false` means the marker was already on the issue and nothing
2080
+ * was posted. Either way `comment` is the marked comment itself.
2081
+ *
2082
+ * @public
2083
+ */
2084
+ declare class CommentOnceResult extends CommentOnceResult_base {}
1989
2085
  /**
1990
2086
  * Issues.
1991
2087
  *
@@ -2001,6 +2097,28 @@ interface GitHubIssueShape {
2001
2097
  readonly close: (number: number, reason?: "completed" | "not_planned") => Effect.Effect<void, GitHubError, Repo>;
2002
2098
  /** Post a comment. */
2003
2099
  readonly comment: (number: number, body: string) => Effect.Effect<number, GitHubError, Repo>;
2100
+ /**
2101
+ * Post a marked comment once: create it, or skip if it already exists.
2102
+ *
2103
+ * @remarks
2104
+ * Create-or-skip, **never edit** — the counterpart to
2105
+ * `PullRequestComment.upsert`, which edits in place. The marker is appended
2106
+ * to the body exactly as `upsert` formats it, so a comment either member
2107
+ * writes stays findable by the other.
2108
+ *
2109
+ * The check-then-create is **not atomic** — GitHub offers no conditional
2110
+ * create, so two callers racing the same issue can both observe no marker
2111
+ * and both post. The guard is idempotence across *sequential* invocations
2112
+ * (a re-run workflow, the motivating case), not mutual exclusion across
2113
+ * concurrent ones; a caller needing the latter must serialize externally.
2114
+ *
2115
+ * The existence check is the comment itself: the issue's comments are
2116
+ * paginated and the first body carrying the marker means skip. That is the
2117
+ * guard {@link GitHubIssueShape.isCrossReferencedBy}'s docstring tells you
2118
+ * to build — an issue reached via `linkedIssues` is cross-referenced from
2119
+ * the outset, so only the marker answers "have I commented yet?".
2120
+ */
2121
+ readonly commentOnce: (issueNumber: number, marker: CommentMarker, body: string) => Effect.Effect<CommentOnceResult, GitHubError, Repo>;
2004
2122
  /** The issues a pull request closes, with `userLinked` telling you who linked them. */
2005
2123
  readonly linkedIssues: (prNumber: number) => Effect.Effect<ReadonlyArray<LinkedIssue>, GitHubGraphQLError, Repo>;
2006
2124
  /**
@@ -2431,6 +2549,108 @@ declare class GitTag extends GitTag_base {
2431
2549
  static readonly layerTest: (overrides?: Partial<GitTagShape>) => Layer.Layer<GitTag>;
2432
2550
  }
2433
2551
  //#endregion
2552
+ //#region src/IssueReferences.d.ts
2553
+ /**
2554
+ * GitHub's closing-keyword issue-reference grammar, as pure functions.
2555
+ *
2556
+ * @remarks
2557
+ * GitHub links an issue to a pull request when the PR's description carries
2558
+ * `<keyword> #<number>` for one of nine documented keywords. Consumers speak
2559
+ * that grammar in two distinct dialects, and this module models exactly those
2560
+ * two — no service, no layer, nothing but strings in and values out:
2561
+ *
2562
+ * - **Inline-in-prose** ({@link harvestIssueReferences}): a reference may appear anywhere in
2563
+ * running text — `"fixes #12 and closes #13"` — with mandatory whitespace
2564
+ * and **no colon**, because that is the spelling GitHub itself scans PR
2565
+ * bodies for. This is the dialect a release pipeline harvests from commit
2566
+ * subjects and PR descriptions.
2567
+ * - **Bare-line** ({@link parseBareLineReference}): the whole line, after trimming, *is*
2568
+ * the reference — `"Closes: #12"` — with an **optional colon**, because a
2569
+ * generated references region writes one reference per line and a colon
2570
+ * reads better there. GitHub does not require the colon; the region format
2571
+ * allows it, so the parser must too. The dialects differ because their
2572
+ * producers do: prose is written by humans for GitHub's scanner, the region
2573
+ * is written by tooling for humans.
2574
+ *
2575
+ * Deliberately out of scope this round: cross-repo references
2576
+ * (`owner/repo#N`) and full-URL references
2577
+ * (`https://github.com/owner/repo/issues/N`). Both are real GitHub spellings;
2578
+ * neither dialect's consumers emit them yet, and guessing at their shape here
2579
+ * would freeze an API nobody has driven.
2580
+ */
2581
+ /**
2582
+ * The nine closing keywords GitHub documents, lowercased.
2583
+ *
2584
+ * @public
2585
+ */
2586
+ declare const CLOSING_KEYWORDS: readonly ["close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"];
2587
+ /**
2588
+ * One of the nine documented closing keywords, in canonical lowercase form.
2589
+ *
2590
+ * @public
2591
+ */
2592
+ type ClosingKeyword = (typeof CLOSING_KEYWORDS)[number];
2593
+ /**
2594
+ * One closing reference found in prose by {@link harvestIssueReferences}.
2595
+ *
2596
+ * @public
2597
+ */
2598
+ interface IssueReference {
2599
+ /** The referenced issue number. */
2600
+ readonly issueNumber: number;
2601
+ /** The matched keyword, lowercased to its canonical form. */
2602
+ readonly keyword: ClosingKeyword;
2603
+ /** Offset of the first character of the whole match (`keyword` through digits). */
2604
+ readonly start: number;
2605
+ /** Offset one past the last character of the whole match. */
2606
+ readonly end: number;
2607
+ }
2608
+ /**
2609
+ * The closing reference a bare line carries, per {@link parseBareLineReference}.
2610
+ *
2611
+ * @remarks
2612
+ * No offsets: in the bare-line dialect the whole line is the reference, so
2613
+ * positions within it locate nothing a caller acts on.
2614
+ *
2615
+ * @public
2616
+ */
2617
+ interface BareLineReference {
2618
+ /** The referenced issue number. */
2619
+ readonly issueNumber: number;
2620
+ /** The matched keyword, lowercased to its canonical form. */
2621
+ readonly keyword: ClosingKeyword;
2622
+ }
2623
+ /**
2624
+ * Every inline closing reference in `text`, in document order.
2625
+ *
2626
+ * @remarks
2627
+ * The **inline-in-prose** dialect: case-insensitive `<keyword> #<number>`
2628
+ * anywhere in the text, whitespace mandatory, colon not accepted — a colon
2629
+ * spelling belongs to the bare-line dialect and {@link parseBareLineReference}.
2630
+ * Duplicates are preserved: whether `fixes #1, fixes #1` means one reference
2631
+ * or two is the caller's business, not a parser's.
2632
+ *
2633
+ * A match whose digits exceed `Number.MAX_SAFE_INTEGER` is skipped, not
2634
+ * misparsed — see the module remarks for the scope boundary.
2635
+ *
2636
+ * @public
2637
+ */
2638
+ declare const harvestIssueReferences: (text: string) => ReadonlyArray<IssueReference>;
2639
+ /**
2640
+ * The reference a whole line carries, or `Option.none()`.
2641
+ *
2642
+ * @remarks
2643
+ * The **bare-line** dialect: after trimming, the entire line must be
2644
+ * `<keyword>[:] #<number>` — keyword case-insensitive, colon optional,
2645
+ * whitespace before the `#` mandatory. Trailing prose, a missing keyword, or
2646
+ * anything else at all is a rejection, never a partial parse; a line carries
2647
+ * one reference or none. Digits past `Number.MAX_SAFE_INTEGER` reject too,
2648
+ * for the same reason {@link harvestIssueReferences} skips them.
2649
+ *
2650
+ * @public
2651
+ */
2652
+ declare const parseBareLineReference: (line: string) => Option.Option<BareLineReference>;
2653
+ //#endregion
2434
2654
  //#region src/PullRequest.d.ts
2435
2655
  /** How a pull request is merged. @public */
2436
2656
  declare const MergeMethod: Schema.Literals<readonly ["merge", "squash", "rebase"]>;
@@ -2602,85 +2822,6 @@ declare class PullRequest extends PullRequest_base {
2602
2822
  static readonly layerTest: (overrides?: Partial<PullRequestShape>) => Layer.Layer<PullRequest>;
2603
2823
  }
2604
2824
  //#endregion
2605
- //#region src/PullRequestComment.d.ts
2606
- declare const CommentMarker_base: Schema.Class<CommentMarker, Schema.Struct<{
2607
- /** Whose comments these are, e.g. your action's name. */
2608
- readonly namespace: Schema.NonEmptyString;
2609
- /** Which comment, within that namespace. */
2610
- readonly key: Schema.NonEmptyString;
2611
- }>, {}>;
2612
- /**
2613
- * The hidden marker that makes a comment findable again.
2614
- *
2615
- * @remarks
2616
- * A pure class, not a hardcoded string. The surface this replaces baked
2617
- * `<!-- savvy-web:${key} -->` into the library — one vendor's name, inside a
2618
- * package meant to be general. Here the namespace is the caller's, the marker is
2619
- * testable without a client, and the library has no opinion about whose comments
2620
- * these are.
2621
- *
2622
- * @public
2623
- */
2624
- declare class CommentMarker extends CommentMarker_base {
2625
- /** The HTML comment appended to a body so the comment can be found again. */
2626
- get html(): string;
2627
- /** Does this body carry the marker? */
2628
- matches(body: string): boolean;
2629
- }
2630
- declare const CommentRecord_base: Schema.Class<CommentRecord, Schema.Struct<{
2631
- readonly id: Schema.Int;
2632
- readonly body: Schema.String;
2633
- readonly url: Schema.String;
2634
- }>, {}>;
2635
- /**
2636
- * A comment this package wrote or found.
2637
- *
2638
- * @public
2639
- */
2640
- declare class CommentRecord extends CommentRecord_base {}
2641
- /**
2642
- * Sticky comments on a pull request or issue.
2643
- *
2644
- * @public
2645
- */
2646
- interface PullRequestCommentShape {
2647
- /** Post a new comment. */
2648
- readonly create: (issueNumber: number, body: string) => Effect.Effect<CommentRecord, GitHubError, Repo>;
2649
- /**
2650
- * Update the marked comment if there is one, or post it.
2651
- *
2652
- * @remarks
2653
- * The marker is appended to the body, so a comment written by `upsert` is
2654
- * always findable by the same marker afterwards.
2655
- */
2656
- readonly upsert: (issueNumber: number, marker: CommentMarker, body: string) => Effect.Effect<CommentRecord, GitHubError, Repo>;
2657
- /**
2658
- * Find the marked comment.
2659
- *
2660
- * @remarks
2661
- * **Paginates.** The version this replaces requested a single page of 100 and
2662
- * stopped, so on a busy pull request the marker silently vanished and every
2663
- * update posted a new comment instead.
2664
- */
2665
- readonly find: (issueNumber: number, marker: CommentMarker, options?: {
2666
- readonly page?: PageOptions | undefined;
2667
- }) => Effect.Effect<Option.Option<CommentRecord>, GitHubError, Repo>;
2668
- readonly delete: (commentId: number) => Effect.Effect<void, GitHubError, Repo>;
2669
- }
2670
- declare const PullRequestComment_base: Context.ServiceClass<PullRequestComment, "@effected/github/PullRequestComment", PullRequestCommentShape>;
2671
- /**
2672
- * Sticky comments.
2673
- *
2674
- * @public
2675
- */
2676
- declare class PullRequestComment extends PullRequestComment_base {
2677
- static readonly layer: Layer.Layer<PullRequestComment, never, GitHubClient>;
2678
- /** An in-memory double; unstubbed members die naming themselves. */
2679
- static readonly makeTest: (overrides?: Partial<PullRequestCommentShape>) => PullRequestCommentShape;
2680
- /** {@link PullRequestComment.makeTest} behind a `Layer`. */
2681
- static readonly layerTest: (overrides?: Partial<PullRequestCommentShape>) => Layer.Layer<PullRequestComment>;
2682
- }
2683
- //#endregion
2684
2825
  //#region src/RepositorySecret.d.ts
2685
2826
  /**
2686
2827
  * Which secret store an operation acts on.
@@ -3239,5 +3380,5 @@ declare class WorkflowDispatch extends WorkflowDispatch_base {
3239
3380
  static readonly layerTest: (overrides?: Partial<WorkflowDispatchShape>) => Layer.Layer<WorkflowDispatch>;
3240
3381
  }
3241
3382
  //#endregion
3242
- export { Annotation, AnnotationLevel, type AppCredentials, AppIdentity, type AppliedSettings, ArtifactMetadata, type ArtifactMetadataShape, Attestation, AttestationListEntry, AttestationRecord, type AttestationShape, BotIdentity, type BranchOutcome, CheckConclusion, CheckRun, CheckRunOutput, CheckRunRef, type CheckRunShape, CodeScanning, type CodeScanningSetup, type CodeScanningShape, CommentMarker, CommentRecord, CommitComparison, CommitFile, CommitRef, CommitSummary, type ConcludeCheckRun, DeploymentEnvironment, type DeploymentEnvironmentInfo, type DeploymentEnvironmentShape, ExtraPermission, FileChange, FileContent, FileDeletion, FileMode, FileStatus, GRAPHQL_ONLY_SETTINGS, GitBranch, type GitBranchShape, GitCommit, type GitCommitShape, GitHubApp, GitHubAppError, type GitHubAppOptions, type GitHubAppShape, GitHubClient, type GitHubClientOptions, type GitHubClientShape, GitHubCommit, type GitHubCommitShape, GitHubContent, type GitHubContentShape, GitHubError, GitHubErrorKind, type GitHubFixtures, GitHubGraphQLError, GitHubIssue, type GitHubIssueShape, GitHubRelease, type GitHubReleaseShape, GitHubRepository, type GitHubRepositoryShape, GitTag, type GitTagShape, GraphQLDocument, GraphQLErrorEntry, Installation, InstallationToken, InvalidRepoRefError, IssueInfo, type LatestSemverOptions, LinkedIssue, MergeMethod, type OwnerType, PageOptions, PermissionGap, PermissionLevel, PermissionResult, type PollOptions, PullRequest, PullRequestComment, type PullRequestCommentShape, PullRequestInfo, type PullRequestShape, RateLimitSnapshot, type RecordedCall, ReleaseAsset, ReleaseInfo, Repo, RepoRef, type RepositoryPatch, RepositorySecret, type RepositorySecretShape, RepositorySecurity, type RepositorySecurityShape, type RepositorySettings, RepositoryVariable, type RepositoryVariableShape, type Data as RestData, type RequestExtras as RestExtras, type Item as RestItem, type PaginatingRoute as RestPaginatingRoute, type Params as RestParams, type Response as RestResponse, type Route as RestRoute, RetryPolicy, type RetryableFailure, Ruleset, type RulesetInfo, type RulesetPayload, type RulesetShape, SECURITY_ANALYSIS_STATUS_FIELDS, type SecretInfo, type SecretScope, SemverTag, StorageRecordInput, TagRef, TokenPermissionError, TokenPermissions, type TokenRequest, type UpsertedPullRequest, type VariableInfo, type VersionFromTag, WorkflowDispatch, type WorkflowDispatchShape, type WorkflowInfo, WorkflowRunStatus, transformSecurityAndAnalysis, versionFromTag };
3383
+ export { Annotation, AnnotationLevel, type AppCredentials, AppIdentity, type AppliedSettings, ArtifactMetadata, type ArtifactMetadataShape, Attestation, AttestationListEntry, AttestationRecord, type AttestationShape, type BareLineReference, BotIdentity, type BranchOutcome, CLOSING_KEYWORDS, CheckConclusion, CheckRun, CheckRunOutput, CheckRunRef, type CheckRunShape, type ClosingKeyword, CodeScanning, type CodeScanningSetup, type CodeScanningShape, CommentMarker, CommentOnceResult, CommentRecord, CommitComparison, CommitFile, CommitRef, CommitSummary, type ConcludeCheckRun, DeploymentEnvironment, type DeploymentEnvironmentInfo, type DeploymentEnvironmentShape, ExtraPermission, FileChange, FileContent, FileDeletion, FileMode, FileStatus, GRAPHQL_ONLY_SETTINGS, GitBranch, type GitBranchShape, GitCommit, type GitCommitShape, GitHubApp, GitHubAppError, type GitHubAppOptions, type GitHubAppShape, GitHubClient, type GitHubClientOptions, type GitHubClientShape, GitHubCommit, type GitHubCommitShape, GitHubContent, type GitHubContentShape, GitHubError, GitHubErrorKind, type GitHubFixtures, GitHubGraphQLError, GitHubIssue, type GitHubIssueShape, GitHubRelease, type GitHubReleaseShape, GitHubRepository, type GitHubRepositoryShape, GitTag, type GitTagShape, GraphQLDocument, GraphQLErrorEntry, Installation, InstallationToken, InvalidRepoRefError, IssueInfo, type IssueReference, type LatestSemverOptions, LinkedIssue, MergeMethod, type OwnerType, PageOptions, PermissionGap, PermissionLevel, PermissionResult, type PollOptions, PullRequest, PullRequestComment, type PullRequestCommentShape, PullRequestInfo, type PullRequestShape, RateLimitSnapshot, type RecordedCall, ReleaseAsset, ReleaseInfo, Repo, RepoRef, type RepositoryPatch, RepositorySecret, type RepositorySecretShape, RepositorySecurity, type RepositorySecurityShape, type RepositorySettings, RepositoryVariable, type RepositoryVariableShape, type Data as RestData, type RequestExtras as RestExtras, type Item as RestItem, type PaginatingRoute as RestPaginatingRoute, type Params as RestParams, type Response as RestResponse, type Route as RestRoute, RetryPolicy, type RetryableFailure, Ruleset, type RulesetInfo, type RulesetPayload, type RulesetShape, SECURITY_ANALYSIS_STATUS_FIELDS, type SecretInfo, type SecretScope, SemverTag, StorageRecordInput, TagRef, TokenPermissionError, TokenPermissions, type TokenRequest, type UpsertedPullRequest, type VariableInfo, type VersionFromTag, WorkflowDispatch, type WorkflowDispatchShape, type WorkflowInfo, WorkflowRunStatus, harvestIssueReferences, parseBareLineReference, transformSecurityAndAnalysis, versionFromTag };
3243
3384
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -13,13 +13,14 @@ import { CommitRef, FileChange, FileContent, FileDeletion, FileMode, GitCommit }
13
13
  import { AppIdentity, BotIdentity, GitHubApp, GitHubAppError, Installation, InstallationToken } from "./GitHubApp.js";
14
14
  import { CommitComparison, CommitFile, CommitSummary, FileStatus, GitHubCommit } from "./GitHubCommit.js";
15
15
  import { GitHubContent } from "./GitHubContent.js";
16
- import { GitHubIssue, IssueInfo, LinkedIssue } from "./GitHubIssue.js";
16
+ import { CommentMarker, CommentRecord, PullRequestComment } from "./PullRequestComment.js";
17
+ import { CommentOnceResult, GitHubIssue, IssueInfo, LinkedIssue } from "./GitHubIssue.js";
17
18
  import { GitHubRelease, ReleaseAsset, ReleaseInfo } from "./GitHubRelease.js";
18
19
  import { GRAPHQL_ONLY_SETTINGS, GitHubRepository, SECURITY_ANALYSIS_STATUS_FIELDS, transformSecurityAndAnalysis } from "./GitHubRepository.js";
19
20
  import { GitTag, SemverTag, TagRef, versionFromTag } from "./GitTag.js";
21
+ import { CLOSING_KEYWORDS, harvestIssueReferences, parseBareLineReference } from "./IssueReferences.js";
20
22
  import { PageOptions } from "./Rest.js";
21
23
  import { MergeMethod, PullRequest, PullRequestInfo } from "./PullRequest.js";
22
- import { CommentMarker, CommentRecord, PullRequestComment } from "./PullRequestComment.js";
23
24
  import { RepositorySecret } from "./RepositorySecret.js";
24
25
  import { RepositorySecurity } from "./RepositorySecurity.js";
25
26
  import { RepositoryVariable } from "./RepositoryVariable.js";
@@ -27,4 +28,4 @@ import { Ruleset } from "./Ruleset.js";
27
28
  import { ExtraPermission, PermissionGap, PermissionLevel, PermissionResult, TokenPermissionError, TokenPermissions } from "./TokenPermissions.js";
28
29
  import { WorkflowDispatch, WorkflowRunStatus } from "./WorkflowDispatch.js";
29
30
 
30
- export { Annotation, AnnotationLevel, AppIdentity, ArtifactMetadata, Attestation, AttestationListEntry, AttestationRecord, BotIdentity, CheckConclusion, CheckRun, CheckRunOutput, CheckRunRef, CodeScanning, CommentMarker, CommentRecord, CommitComparison, CommitFile, CommitRef, CommitSummary, DeploymentEnvironment, ExtraPermission, FileChange, FileContent, FileDeletion, FileMode, FileStatus, GRAPHQL_ONLY_SETTINGS, GitBranch, GitCommit, GitHubApp, GitHubAppError, GitHubClient, GitHubCommit, GitHubContent, GitHubError, GitHubErrorKind, GitHubGraphQLError, GitHubIssue, GitHubRelease, GitHubRepository, GitTag, GraphQLDocument, GraphQLErrorEntry, Installation, InstallationToken, InvalidRepoRefError, IssueInfo, LinkedIssue, MergeMethod, PageOptions, PermissionGap, PermissionLevel, PermissionResult, PullRequest, PullRequestComment, PullRequestInfo, RateLimitSnapshot, ReleaseAsset, ReleaseInfo, Repo, RepoRef, RepositorySecret, RepositorySecurity, RepositoryVariable, RetryPolicy, Ruleset, SECURITY_ANALYSIS_STATUS_FIELDS, SemverTag, StorageRecordInput, TagRef, TokenPermissionError, TokenPermissions, WorkflowDispatch, WorkflowRunStatus, transformSecurityAndAnalysis, versionFromTag };
31
+ export { Annotation, AnnotationLevel, AppIdentity, ArtifactMetadata, Attestation, AttestationListEntry, AttestationRecord, BotIdentity, CLOSING_KEYWORDS, CheckConclusion, CheckRun, CheckRunOutput, CheckRunRef, CodeScanning, CommentMarker, CommentOnceResult, CommentRecord, CommitComparison, CommitFile, CommitRef, CommitSummary, DeploymentEnvironment, ExtraPermission, FileChange, FileContent, FileDeletion, FileMode, FileStatus, GRAPHQL_ONLY_SETTINGS, GitBranch, GitCommit, GitHubApp, GitHubAppError, GitHubClient, GitHubCommit, GitHubContent, GitHubError, GitHubErrorKind, GitHubGraphQLError, GitHubIssue, GitHubRelease, GitHubRepository, GitTag, GraphQLDocument, GraphQLErrorEntry, Installation, InstallationToken, InvalidRepoRefError, IssueInfo, LinkedIssue, MergeMethod, PageOptions, PermissionGap, PermissionLevel, PermissionResult, PullRequest, PullRequestComment, PullRequestInfo, RateLimitSnapshot, ReleaseAsset, ReleaseInfo, Repo, RepoRef, RepositorySecret, RepositorySecurity, RepositoryVariable, RetryPolicy, Ruleset, SECURITY_ANALYSIS_STATUS_FIELDS, SemverTag, StorageRecordInput, TagRef, TokenPermissionError, TokenPermissions, WorkflowDispatch, WorkflowRunStatus, harvestIssueReferences, parseBareLineReference, transformSecurityAndAnalysis, versionFromTag };
@@ -0,0 +1,17 @@
1
+ //#region src/internal/ids.ts
2
+ /**
3
+ * Narrows the `number | bigint` resource ids `@octokit/types` v17 declares
4
+ * back to `number`.
5
+ *
6
+ * GitHub's REST payloads arrive through `JSON.parse`, which never produces a
7
+ * bigint, so at runtime the value is always a number today — the union is
8
+ * upstream future-proofing for ids beyond 2^53. If GitHub ever crosses that
9
+ * line, the `id: number` fields on this package's public records have to be
10
+ * redesigned; a runtime coercion here could not paper over it.
11
+ *
12
+ * Leaf module: imports nothing.
13
+ */
14
+ const numericId = (id) => Number(id);
15
+
16
+ //#endregion
17
+ export { numericId };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/github",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "private": false,
5
5
  "description": "Typed GitHub REST and GraphQL services over the octokit core request surface, with app auth and resource helpers.",
6
6
  "keywords": [
@@ -40,8 +40,8 @@
40
40
  "dependencies": {
41
41
  "@effected/semver": "^0.5.0",
42
42
  "@octokit/core": "^7.0.6",
43
- "@octokit/plugin-paginate-rest": "^14.0.0",
44
- "@octokit/types": "^16.0.0",
43
+ "@octokit/plugin-paginate-rest": "^15.0.0",
44
+ "@octokit/types": "^17.0.0",
45
45
  "blakejs": "1.2.1",
46
46
  "tweetnacl": "1.0.3",
47
47
  "universal-github-app-jwt": "^2.2.2"