@volter/twin-github 0.1.2 → 2.0.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.
Files changed (215) hide show
  1. package/README.md +88 -36
  2. package/client/github-mirror.css +277 -319
  3. package/client/github-mirror.d.ts +418 -0
  4. package/client/github-mirror.js +485 -0
  5. package/client/github-mirror.tsx +153 -357
  6. package/client/pulls-rest.ts +159 -0
  7. package/client/pulls-workspace.tsx +841 -0
  8. package/dist/client/github-mirror.bundle.js +239 -0
  9. package/dist/client/github-mirror.css +916 -0
  10. package/dist/client/github-mirror.d.ts +418 -0
  11. package/dist/client/github-mirror.js +485 -0
  12. package/dist/client/github-mirror.tsx +1315 -0
  13. package/dist/client/pulls-rest.d.ts +42 -0
  14. package/dist/client/pulls-rest.js +140 -0
  15. package/dist/client/pulls-rest.ts +159 -0
  16. package/dist/client/pulls-workspace.bundle.js +22 -0
  17. package/dist/client/pulls-workspace.d.ts +114 -0
  18. package/dist/client/pulls-workspace.js +418 -0
  19. package/dist/client/pulls-workspace.tsx +841 -0
  20. package/dist/src/cli.d.ts +2 -0
  21. package/dist/src/cli.js +40 -0
  22. package/dist/src/generated/graphql-sdl.gen.json +1 -0
  23. package/dist/src/generated/graphql.gen.json +1 -0
  24. package/dist/src/generated/surface.gen.json +1 -0
  25. package/dist/src/generated/ui.gen.json +1 -0
  26. package/dist/src/github-budget.d.ts +69 -0
  27. package/dist/src/github-budget.js +172 -0
  28. package/dist/src/github-capabilities.d.ts +5 -0
  29. package/dist/src/github-capabilities.js +4468 -0
  30. package/dist/src/github-conformance.d.ts +43 -0
  31. package/dist/src/github-conformance.js +76 -0
  32. package/dist/src/github-connector.d.ts +307 -0
  33. package/dist/src/github-connector.js +1398 -0
  34. package/dist/src/github-events.d.ts +41 -0
  35. package/dist/src/github-events.js +232 -0
  36. package/dist/src/github-git-http.d.ts +49 -0
  37. package/dist/src/github-git-http.js +185 -0
  38. package/dist/src/github-git-plane.d.ts +114 -0
  39. package/dist/src/github-git-plane.js +407 -0
  40. package/dist/src/github-mirror-state.d.ts +2 -0
  41. package/dist/src/github-mirror-state.js +335 -0
  42. package/dist/src/github-mirror-ui.d.ts +20 -0
  43. package/dist/src/github-mirror-ui.js +101 -0
  44. package/dist/src/github-server.d.ts +14 -0
  45. package/dist/src/github-server.js +240 -0
  46. package/dist/src/github-shared.d.ts +12 -0
  47. package/dist/src/github-shared.js +21 -0
  48. package/dist/src/github-twin.d.ts +1411 -0
  49. package/dist/src/github-twin.js +4084 -0
  50. package/dist/src/github-ui-conformance.d.ts +4 -0
  51. package/dist/src/github-ui-conformance.js +105 -0
  52. package/dist/src/github-ui-structure.d.ts +18 -0
  53. package/dist/src/github-ui-structure.js +251 -0
  54. package/dist/src/graphql-wire.d.ts +22 -0
  55. package/dist/src/graphql-wire.js +89 -0
  56. package/dist/src/index.d.ts +17 -0
  57. package/dist/src/index.js +104 -0
  58. package/dist/src/manifest.d.ts +8 -0
  59. package/dist/src/manifest.js +597 -0
  60. package/dist/src/npm-registry.d.ts +7 -0
  61. package/dist/src/npm-registry.js +47 -0
  62. package/dist/src/screens/app-installation.d.ts +3 -0
  63. package/dist/src/screens/app-installation.js +166 -0
  64. package/dist/src/screens/app-manifest.d.ts +3 -0
  65. package/dist/src/screens/app-manifest.js +81 -0
  66. package/dist/src/screens/oauth.d.ts +15 -0
  67. package/dist/src/screens/oauth.js +257 -0
  68. package/dist/src/screens/session.d.ts +8 -0
  69. package/dist/src/screens/session.js +170 -0
  70. package/dist/src/semantics/actions.d.ts +2 -0
  71. package/dist/src/semantics/actions.js +413 -0
  72. package/dist/src/semantics/activity.d.ts +4 -0
  73. package/dist/src/semantics/activity.js +161 -0
  74. package/dist/src/semantics/apps.d.ts +2 -0
  75. package/dist/src/semantics/apps.js +144 -0
  76. package/dist/src/semantics/branches.d.ts +2 -0
  77. package/dist/src/semantics/branches.js +136 -0
  78. package/dist/src/semantics/checks.d.ts +2 -0
  79. package/dist/src/semantics/checks.js +176 -0
  80. package/dist/src/semantics/code-scanning-upload.d.ts +2 -0
  81. package/dist/src/semantics/code-scanning-upload.js +97 -0
  82. package/dist/src/semantics/codespaces.d.ts +2 -0
  83. package/dist/src/semantics/codespaces.js +58 -0
  84. package/dist/src/semantics/commits.d.ts +2 -0
  85. package/dist/src/semantics/commits.js +109 -0
  86. package/dist/src/semantics/contents.d.ts +2 -0
  87. package/dist/src/semantics/contents.js +131 -0
  88. package/dist/src/semantics/deployments.d.ts +2 -0
  89. package/dist/src/semantics/deployments.js +127 -0
  90. package/dist/src/semantics/gists.d.ts +2 -0
  91. package/dist/src/semantics/gists.js +86 -0
  92. package/dist/src/semantics/git.d.ts +2 -0
  93. package/dist/src/semantics/git.js +235 -0
  94. package/dist/src/semantics/graphql.d.ts +7 -0
  95. package/dist/src/semantics/graphql.js +512 -0
  96. package/dist/src/semantics/index.d.ts +5 -0
  97. package/dist/src/semantics/index.js +62 -0
  98. package/dist/src/semantics/issues.d.ts +2 -0
  99. package/dist/src/semantics/issues.js +456 -0
  100. package/dist/src/semantics/keys.d.ts +2 -0
  101. package/dist/src/semantics/keys.js +66 -0
  102. package/dist/src/semantics/labels.d.ts +2 -0
  103. package/dist/src/semantics/labels.js +100 -0
  104. package/dist/src/semantics/meta.d.ts +12 -0
  105. package/dist/src/semantics/meta.js +144 -0
  106. package/dist/src/semantics/notifications.d.ts +4 -0
  107. package/dist/src/semantics/notifications.js +62 -0
  108. package/dist/src/semantics/orgs.d.ts +2 -0
  109. package/dist/src/semantics/orgs.js +420 -0
  110. package/dist/src/semantics/packages.d.ts +2 -0
  111. package/dist/src/semantics/packages.js +55 -0
  112. package/dist/src/semantics/pages.d.ts +2 -0
  113. package/dist/src/semantics/pages.js +176 -0
  114. package/dist/src/semantics/projects.d.ts +2 -0
  115. package/dist/src/semantics/projects.js +239 -0
  116. package/dist/src/semantics/pulls.d.ts +2 -0
  117. package/dist/src/semantics/pulls.js +462 -0
  118. package/dist/src/semantics/push-reactions.d.ts +101 -0
  119. package/dist/src/semantics/push-reactions.js +513 -0
  120. package/dist/src/semantics/releases.d.ts +19 -0
  121. package/dist/src/semantics/releases.js +231 -0
  122. package/dist/src/semantics/repo-invitations.d.ts +2 -0
  123. package/dist/src/semantics/repo-invitations.js +98 -0
  124. package/dist/src/semantics/repos.d.ts +17 -0
  125. package/dist/src/semantics/repos.js +390 -0
  126. package/dist/src/semantics/rulesets.d.ts +2 -0
  127. package/dist/src/semantics/rulesets.js +111 -0
  128. package/dist/src/semantics/search.d.ts +2 -0
  129. package/dist/src/semantics/search.js +84 -0
  130. package/dist/src/semantics/security.d.ts +2 -0
  131. package/dist/src/semantics/security.js +177 -0
  132. package/dist/src/semantics/shared.d.ts +61 -0
  133. package/dist/src/semantics/shared.js +151 -0
  134. package/dist/src/semantics/users.d.ts +2 -0
  135. package/dist/src/semantics/users.js +255 -0
  136. package/dist/src/semantics/webhooks.d.ts +2 -0
  137. package/dist/src/semantics/webhooks.js +107 -0
  138. package/dist/test-fixtures/github-a11y-reference.pr-list.SOURCE.md +56 -0
  139. package/dist/test-fixtures/github-a11y-reference.pr-list.json +1447 -0
  140. package/dist/test-fixtures/github-comment-schema.SOURCE.md +11 -0
  141. package/dist/test-fixtures/github-comment-schema.json +702 -0
  142. package/dist/test-fixtures/github-known-deviations.json +94 -0
  143. package/dist/test-fixtures/github-openapi-operations.SOURCE.md +76 -0
  144. package/dist/test-fixtures/github-openapi-operations.json +362 -0
  145. package/dist/test-fixtures/github-pull-schema.SOURCE.md +37 -0
  146. package/dist/test-fixtures/github-pull-schema.json +3601 -0
  147. package/dist/test-fixtures/github-review-schema.SOURCE.md +12 -0
  148. package/dist/test-fixtures/github-review-schema.json +236 -0
  149. package/package.json +20 -11
  150. package/src/cli.ts +7 -6
  151. package/src/generated/graphql-sdl.gen.json +1 -0
  152. package/src/generated/graphql.gen.json +1 -0
  153. package/src/generated/surface.gen.json +1 -0
  154. package/src/generated/ui.gen.json +1 -0
  155. package/src/github-a11y-snapshot.uitest.ts +5 -5
  156. package/src/github-budget.ts +4 -4
  157. package/src/github-capabilities.ts +1993 -556
  158. package/src/github-conformance.ts +12 -7
  159. package/src/github-connector.ts +108 -96
  160. package/src/github-events.ts +225 -95
  161. package/src/github-git-http.ts +48 -84
  162. package/src/github-git-plane.ts +247 -385
  163. package/src/github-journey.uitest.ts +28 -44
  164. package/src/github-mirror-state.ts +23 -61
  165. package/src/github-mirror-ui.ts +16 -10
  166. package/src/github-server.ts +158 -60
  167. package/src/github-shared.ts +1 -1
  168. package/src/github-twin.ts +1525 -4494
  169. package/src/github-ui-conformance.ts +7 -8
  170. package/src/github-ui-structure.ts +19 -18
  171. package/src/graphql-wire.ts +115 -0
  172. package/src/index.ts +50 -7
  173. package/src/manifest.ts +605 -0
  174. package/src/npm-registry.ts +43 -0
  175. package/src/screens/app-installation.tsx +217 -0
  176. package/src/screens/app-manifest.tsx +97 -0
  177. package/src/screens/oauth.tsx +258 -0
  178. package/src/screens/session.tsx +193 -0
  179. package/src/semantics/actions.ts +395 -0
  180. package/src/semantics/activity.ts +171 -0
  181. package/src/semantics/apps.ts +133 -0
  182. package/src/semantics/branches.ts +133 -0
  183. package/src/semantics/checks.ts +163 -0
  184. package/src/semantics/code-scanning-upload.ts +92 -0
  185. package/src/semantics/codespaces.ts +58 -0
  186. package/src/semantics/commits.ts +109 -0
  187. package/src/semantics/contents.ts +112 -0
  188. package/src/semantics/deployments.ts +116 -0
  189. package/src/semantics/gists.ts +85 -0
  190. package/src/semantics/git.ts +226 -0
  191. package/src/semantics/graphql.ts +505 -0
  192. package/src/semantics/index.ts +68 -0
  193. package/src/semantics/issues.ts +434 -0
  194. package/src/semantics/keys.ts +66 -0
  195. package/src/semantics/labels.ts +91 -0
  196. package/src/semantics/meta.ts +139 -0
  197. package/src/semantics/notifications.ts +58 -0
  198. package/src/semantics/orgs.ts +383 -0
  199. package/src/semantics/packages.ts +59 -0
  200. package/src/semantics/pages.ts +154 -0
  201. package/src/semantics/projects.ts +246 -0
  202. package/src/semantics/pulls.ts +421 -0
  203. package/src/semantics/push-reactions.ts +471 -0
  204. package/src/semantics/releases.ts +213 -0
  205. package/src/semantics/repo-invitations.ts +112 -0
  206. package/src/semantics/repos.ts +373 -0
  207. package/src/semantics/rulesets.ts +107 -0
  208. package/src/semantics/search.ts +83 -0
  209. package/src/semantics/security.ts +153 -0
  210. package/src/semantics/shared.ts +181 -0
  211. package/src/semantics/users.ts +252 -0
  212. package/src/semantics/webhooks.ts +101 -0
  213. package/test-fixtures/github-known-deviations.json +7 -8
  214. package/test-fixtures/github-openapi-operations.json +46 -227
  215. package/src/github-graphql.ts +0 -398
@@ -0,0 +1,462 @@
1
+ import { authorAssociation, applyTriageFields, GITHUB_WORLD_USER, githubNotFound, hasBareRepo, implicitReviewId, listGitPlaneRefs, mergePullRequest, nextRepoNumber, mergeIntoBranchInPlane, normalizeLogins, paginate, planeAncestors, syncPrHeadsToBranchTip, planeCommitBody, prPlaneDiffFiles, readGitCommitFromPlane, resolvePlaneRef, reviewStateForEvent, serializeReviewComment, toAssignees, toGithubIssue, toGithubPullFile, toGithubRest, toGithubReview, } from "../github-twin.js";
2
+ import { announce, asView, gh, nextSeq, put, send, writer } from "./shared.js";
3
+ import { assignable } from "./repos.js";
4
+ import { closingIssueNumbers } from "./push-reactions.js";
5
+ const numberOf = (g) => Number(decodeURIComponent(g.seg[4]));
6
+ /** `owner`'s repository in `repo`'s network (the forks of one root, docs.github.com/pull-requests/collaborating-with-
7
+ * pull-requests/working-with-forks/about-forks), or nothing. */
8
+ function forkInNetwork(st, repo, owner) {
9
+ const rootOf = (name) => {
10
+ let at = name;
11
+ for (let hops = 0; hops < 50; hops++) {
12
+ const parent = st.repos.find((r) => r.full_name === at)?.parent_full_name;
13
+ if (!parent)
14
+ break;
15
+ at = parent;
16
+ }
17
+ return at;
18
+ };
19
+ const network = rootOf(repo);
20
+ return st.repos.find((r) => r.owner.toLowerCase() === owner.toLowerCase() && r.full_name !== repo && rootOf(r.full_name) === network)?.full_name;
21
+ }
22
+ /** The pull request a path names, as a pull request (never an issue's counter shell). */
23
+ const pullOf = (g) => {
24
+ const n = numberOf(g);
25
+ return g.state().prs.find((p) => p.repository === g.repo && p.number === n && p.is_pull_request);
26
+ };
27
+ /** A door INTO a pull request 404s when nobody opened it: an empty list would say it is real and quiet. */
28
+ const intoPull = (serve) => async (ctx) => {
29
+ const g = gh(ctx);
30
+ const pr = pullOf(g);
31
+ return pr ? serve(ctx, g, pr, pr.number) : send(ctx, githubNotFound());
32
+ };
33
+ /** A pull request as GitHub answers it whole: its fields, the counts GitHub computes (commits, changed files,
34
+ * additions, deletions, issue comments and review comments) each from what the list endpoints answer unless
35
+ * the stored pull request carries it (an observed one's pulled counts), and its mergeability. */
36
+ async function fullPull(g, pr) {
37
+ const st = g.state();
38
+ const files = await filesOf(g, pr);
39
+ const commits = (await planeCommitsOf(g, pr))?.length ?? pr.commits ?? 0;
40
+ const on = (kind) => st.comments.filter((c) => c.repository === pr.repository && c.number === pr.number && c.kind === kind).length;
41
+ return {
42
+ ...toGithubRest(pr, st.milestones),
43
+ commits: pr.commits ?? commits,
44
+ changed_files: pr.changed_files ?? files.length,
45
+ additions: pr.additions ?? files.reduce((n, f) => n + (Number(f.additions) || 0), 0),
46
+ deletions: pr.deletions ?? files.reduce((n, f) => n + (Number(f.deletions) || 0), 0),
47
+ comments: on('issue'),
48
+ review_comments: on('review'),
49
+ ...(await mergeability(g, pr)),
50
+ };
51
+ }
52
+ /** Write a pull request's fields and answer it as GitHub renders it (whole, or as the operation's view), announcing `action`. */
53
+ async function writePull(ctx, g, number, operation, fields, action, status = 200, view) {
54
+ const id = `${g.repo}#${number}`;
55
+ await put(ctx, { operation, subjectType: 'pull_request', subjectId: id, fields: { number, repository: g.repo, ...fields } });
56
+ const st = g.state();
57
+ const found = st.prs.find((p) => p.id === id);
58
+ const out = found ? toGithubRest(found, st.milestones) : { number };
59
+ await announce(ctx, [{ event: 'pull_request', action, repository: g.repo, number, pr: out }]);
60
+ return ctx.reply(view ? asView(view, out) : found ? await fullPull(g, found) : out, status);
61
+ }
62
+ export function pullsSemantics() {
63
+ return {
64
+ // GitHub's filters: state (open|closed|all, default open), head (owner:branch or branch), base.
65
+ // The default order is newest first; a pull request's number is its creation order.
66
+ 'pulls/list': async (ctx) => {
67
+ const { repo, query, state } = gh(ctx);
68
+ const st = state();
69
+ const lp = new URLSearchParams((query ?? '').replace(/^\?/, ''));
70
+ const stateF = (lp.get('state') ?? 'open').toLowerCase();
71
+ const headF = lp.get('head') ?? undefined;
72
+ const baseF = lp.get('base') ?? undefined;
73
+ const headBranch = headF !== undefined ? (headF.includes(':') ? headF.slice(headF.indexOf(':') + 1) : headF) : undefined;
74
+ // `head` is user:ref-name; the user is whoever holds the head branch (the fork's owner for a pull request from a fork)
75
+ const headUser = headF?.includes(':') ? headF.slice(0, headF.indexOf(':')).toLowerCase() : undefined;
76
+ const filtered = st.prs.filter((p) => p.repository === repo && p.is_pull_request).filter((p) => {
77
+ const prState = p.merged ? 'closed' : (p.state ?? 'open');
78
+ if (stateF !== 'all' && prState !== stateF)
79
+ return false;
80
+ if (headBranch !== undefined && p.head_ref !== headBranch && p.head_sha !== headBranch)
81
+ return false;
82
+ if (headUser !== undefined && (p.head_repo ?? p.repository).split('/')[0].toLowerCase() !== headUser)
83
+ return false;
84
+ if (baseF !== undefined && p.base_ref !== baseF)
85
+ return false;
86
+ return true;
87
+ });
88
+ const dirP = (lp.get('direction') ?? 'desc').toLowerCase() === 'asc' ? 1 : -1;
89
+ return ctx.reply(paginate([...filtered].sort((a, b) => (a.number - b.number) * dirP), query).map((p) => asView('pull-request-simple', toGithubRest(p, st.milestones))));
90
+ },
91
+ // GitHub's pull request carries its counts (commits, changed_files, additions, deletions, comments,
92
+ // review_comments; all required by the schema): each from what the list endpoints answer, unless the
93
+ // stored pull request already carries it (an observed one's pulled counts)
94
+ 'pulls/get': async (ctx) => {
95
+ const g = gh(ctx);
96
+ const pr = pullOf(g);
97
+ if (!pr)
98
+ return send(ctx, githubNotFound());
99
+ return send(ctx, { status: 200, body: await fullPull(g, pr) });
100
+ },
101
+ // 204 when merged, else 404
102
+ 'pulls/check-if-merged': async (ctx) => send(ctx, pullOf(gh(ctx))?.merged ? { status: 204, body: undefined } : githubNotFound()),
103
+ // one review comment, addressed without its pull request
104
+ 'pulls/get-review-comment': async (ctx) => {
105
+ const { repo, seg, state } = gh(ctx);
106
+ const cid = Number(decodeURIComponent(seg[5]));
107
+ const c = state().comments.find((x) => x.repository === repo && x.kind === 'review' && x.id === cid);
108
+ return send(ctx, c ? { status: 200, body: serializeReviewComment(c) } : githubNotFound());
109
+ },
110
+ // The IMPLICIT REVIEW rides the list, as on GitHub: a review comment posted on its own is wrapped
111
+ // in a review, synthesized from the comments naming it (submitted at the earliest, authored by the
112
+ // first, COMMENTED), never stored; the projection counts it into the review count the same way.
113
+ 'pulls/list-reviews': intoPull(async (ctx, g, _pr, n) => {
114
+ const st = g.state();
115
+ const items = st.reviews.filter((r) => r.repository === g.repo && r.number === n);
116
+ const wrapperId = implicitReviewId(g.repo, n);
117
+ const wrapped = st.comments.filter((c) => c.repository === g.repo && c.number === n && c.kind === 'review' && c.review_id === wrapperId);
118
+ if (wrapped.length > 0) {
119
+ const dated = wrapped.map((c) => c.created_at).filter((d) => d !== undefined).sort();
120
+ const author = wrapped.find((c) => c.user_login !== undefined);
121
+ items.push({
122
+ id: wrapperId, repository: g.repo, number: n, state: 'COMMENTED', body: '',
123
+ ...(dated[0] !== undefined ? { submitted_at: dated[0] } : {}),
124
+ ...(author?.user_login !== undefined ? { user_login: author.user_login } : {}),
125
+ ...(author?.user_type !== undefined ? { user_type: author.user_type } : {}),
126
+ });
127
+ }
128
+ return ctx.reply(paginate(items, g.query).map(toGithubReview));
129
+ }),
130
+ // one review, on the (repo, number, id) the dismiss and edit writes use
131
+ 'pulls/get-review': intoPull(async (ctx, g, _pr, n) => {
132
+ const rid = Number(decodeURIComponent(g.seg[6]));
133
+ const r = g.state().reviews.find((x) => x.repository === g.repo && x.number === n && x.id === rid);
134
+ return send(ctx, r ? { status: 200, body: toGithubReview(r) } : githubNotFound());
135
+ }),
136
+ // the diff-thread comments (with their diff anchors when a local write set them)
137
+ 'pulls/list-review-comments': intoPull(async (ctx, g, _pr, n) => ctx.reply(paginate(g.state().comments.filter((c) => c.repository === g.repo && c.number === n && c.kind === 'review'), g.query).map(serializeReviewComment))),
138
+ // The plane's real diff when the base and head both exist there (an observed pull request with a
139
+ // pulled count but no per-file evidence keeps none, so the list never contradicts its own count).
140
+ 'pulls/list-files': intoPull(async (ctx, g, pr) => ctx.reply(paginate(await filesOf(g, pr), g.query).map((f) => toGithubPullFile(g.repo, pr.head_sha ?? '', f)))),
141
+ // In a repository with history, the plane's commits: those the head reaches that the base did not
142
+ // hold (a merged one's base is its merge commit's first parent), oldest first; none without history.
143
+ 'pulls/list-commits': intoPull(async (ctx, g, pr) => {
144
+ const plane = await planeCommitsOf(g, pr);
145
+ if (plane) {
146
+ const bodies = [];
147
+ for (const sha of paginate(plane, g.query)) {
148
+ const b = await planeCommitBody(g.repo, sha, g.root);
149
+ if (b)
150
+ bodies.push(b);
151
+ }
152
+ return ctx.reply(bodies);
153
+ }
154
+ return ctx.reply([]);
155
+ }),
156
+ // {users, teams}: teams are not modeled
157
+ 'pulls/list-requested-reviewers': intoPull(async (ctx, _g, pr) => ctx.reply({ users: toAssignees(pr.requested_reviewers), teams: [] })),
158
+ // 422 without title, head and base. `head` may be owner:branch, a branch, a ref or a sha: the branch
159
+ // is the head ref; its tip (a registered ref or branch) is head_sha. In a repository whose history
160
+ // is in the plane, a head branch that does not exist there is GitHub's 422. An `owner:branch` head whose owner
161
+ // is not this repository's names that owner's fork in the same network, and the branch is read there
162
+ // (docs.github.com/rest/pulls/pulls#create-a-pull-request: "For cross-repository pull requests in the same
163
+ // network, namespace head with a user like this: username:branch"); an owner with no fork in the network is 422.
164
+ 'pulls/create': async (ctx) => {
165
+ const { repo, body, occurredAt, caller, root, state } = gh(ctx);
166
+ const missing = ['title', 'head', 'base'].filter((k) => !body[k]);
167
+ if (missing.length)
168
+ return ctx.reply({ message: 'Validation Failed', errors: missing.map((field) => ({ resource: 'PullRequest', field, code: 'missing_field' })) }, 422);
169
+ const number = nextRepoNumber(state(), repo);
170
+ const id = `${repo}#${number}`;
171
+ const headRaw = String(body.head);
172
+ const headRef = headRaw.includes(':') ? headRaw.slice(headRaw.indexOf(':') + 1) : headRaw;
173
+ const branchName = headRef.replace(/^refs\/heads\//, '');
174
+ const invalidHead = () => ctx.reply({ message: 'Validation Failed', errors: [{ resource: 'PullRequest', field: 'head', code: 'invalid' }], documentation_url: 'https://docs.github.com/rest/pulls/pulls#create-a-pull-request' }, 422);
175
+ const headOwner = headRaw.includes(':') ? headRaw.slice(0, headRaw.indexOf(':')) : undefined;
176
+ let headRepo = repo;
177
+ if (headOwner !== undefined && headOwner.toLowerCase() !== repo.split('/')[0].toLowerCase()) {
178
+ const fork = forkInNetwork(state(), repo, headOwner);
179
+ if (!fork)
180
+ return invalidHead();
181
+ headRepo = fork;
182
+ }
183
+ let resolvedHeadSha = headRaw;
184
+ if (!/^[0-9a-f]{40}$/i.test(headRaw)) {
185
+ const st0 = state();
186
+ const tip = st0.gitRefs.find((x) => x.repository === headRepo && x.ref === `refs/heads/${branchName}`)?.object_sha
187
+ ?? st0.branches.find((b) => b.repository === headRepo && b.name === branchName)?.commit_sha;
188
+ if (tip)
189
+ resolvedHeadSha = tip;
190
+ else if (headRepo !== repo || (hasBareRepo(repo, root) && !(await listGitPlaneRefs(repo, root)).some((r) => r.ref === `refs/heads/${branchName}`)))
191
+ return invalidHead();
192
+ }
193
+ const fields = { number, repository: repo, ...(headRepo !== repo ? { head_repo: headRepo } : {}), title: body.title, body: body.body, base_ref: body.base, head_sha: resolvedHeadSha, head_ref: branchName, state: 'open', user_login: caller ?? 'octocat', user_type: 'User', created_at: occurredAt, author_association: authorAssociation(state(), repo, caller ?? 'octocat'), maintainer_can_modify: body.maintainer_can_modify === true };
194
+ if (body.draft !== undefined)
195
+ fields.draft = body.draft === true;
196
+ await put(ctx, { operation: 'pull_request.create', subjectType: 'pull_request', subjectId: id, fields });
197
+ // read back through the projection, so the derived counts agree with the carried lists
198
+ const st = state();
199
+ const created = st.prs.find((p) => p.id === id) ?? { id, number, repository: repo, review_count: 0, comment_count: 0, title: body.title, body: body.body, base_ref: body.base, head_sha: resolvedHeadSha, head_ref: branchName, state: 'open' };
200
+ const out = toGithubRest(created, st.milestones);
201
+ await announce(ctx, [{ event: 'pull_request', action: 'opened', repository: repo, number, pr: out }]);
202
+ const stored = st.prs.find((p) => p.id === id);
203
+ return ctx.reply(stored ? await fullPull(gh(ctx), stored) : out, 201);
204
+ },
205
+ // REST has no draft parameter on an edit (draft transitions are GraphQL mutations)
206
+ 'pulls/update': intoPull(async (ctx, g, pr, number) => {
207
+ const { body } = g;
208
+ const fields = {};
209
+ if (body.title !== undefined)
210
+ fields.title = body.title;
211
+ if (body.body !== undefined)
212
+ fields.body = body.body;
213
+ if (body.base !== undefined)
214
+ fields.base_ref = body.base;
215
+ if (body.state !== undefined)
216
+ fields.state = body.state;
217
+ applyTriageFields(fields, body);
218
+ const reopening = body.state === 'open' && (pr.state ?? 'open') === 'closed';
219
+ return writePull(ctx, g, number, 'pull_request.update', fields, body.state === 'closed' ? 'closed' : reopening ? 'reopened' : 'edited');
220
+ }),
221
+ // A review binds to the pull request's head at submission (its commit_id; a caller may pin one),
222
+ // which is the exact-head semantic merge gates rely on. Inline `comments` become review comments
223
+ // wrapped by THIS review.
224
+ 'pulls/create-review': intoPull(async (ctx, g, pr, number) => {
225
+ const { repo, body, occurredAt, caller } = g;
226
+ const seq = nextSeq(ctx, 'review');
227
+ const commitId = body.commit_id !== undefined ? String(body.commit_id) : pr.head_sha;
228
+ const fields = { repository: repo, number, state: reviewStateForEvent(body.event), body: body.body, submitted_at: occurredAt, user_login: caller ?? GITHUB_WORLD_USER, author_association: authorAssociation(g.state(), repo, caller ?? GITHUB_WORLD_USER) };
229
+ if (commitId !== undefined)
230
+ fields.commit_id = commitId;
231
+ await put(ctx, { operation: 'pull_request_review.submit', subjectType: 'pull_request_review', subjectId: `review:${seq}`, fields });
232
+ if (Array.isArray(body.comments)) {
233
+ for (const raw of body.comments) {
234
+ const cSeq = nextSeq(ctx, 'comment');
235
+ const cFields = { repository: repo, number, body: raw.body, created_at: occurredAt, updated_at: occurredAt, review_id: seq, user_login: caller ?? GITHUB_WORLD_USER, author_association: authorAssociation(g.state(), repo, caller ?? GITHUB_WORLD_USER), ...(commitId !== undefined ? { commit_id: commitId } : {}) };
236
+ if (raw.path !== undefined)
237
+ cFields.path = String(raw.path);
238
+ if (raw.line !== undefined)
239
+ cFields.line = Number(raw.line);
240
+ if (raw.side !== undefined)
241
+ cFields.side = String(raw.side);
242
+ if (raw.start_line !== undefined)
243
+ cFields.start_line = Number(raw.start_line);
244
+ await put(ctx, { operation: 'pull_request_review_comment.create', subjectType: 'pull_request_review_comment', subjectId: `comment:${cSeq}`, fields: cFields });
245
+ }
246
+ }
247
+ const review = toGithubReview(g.state().reviews.find((r) => r.repository === repo && r.number === number && r.id === seq));
248
+ await announce(ctx, [{ event: 'pull_request_review', action: 'submitted', repository: repo, number, review }]);
249
+ return ctx.reply(review);
250
+ }),
251
+ // dismiss (a message is required) or edit a submitted review's body: an overlay on the review
252
+ 'pulls/dismiss-review': async (ctx) => reviewOverlay(ctx, 'dismiss'),
253
+ 'pulls/update-review': async (ctx) => reviewOverlay(ctx, 'update'),
254
+ // A reply IN a review thread: it inherits the root comment's diff anchor and threads by
255
+ // in_reply_to; it rides the pull request's implicit review (a reply is not the reviewer's verdict).
256
+ 'pulls/create-reply-for-review-comment': async (ctx) => {
257
+ const g = gh(ctx);
258
+ const { repo, seg, body, occurredAt, caller, state } = g;
259
+ const number = numberOf(g);
260
+ const rootId = Number(decodeURIComponent(seg[6]));
261
+ const parent = state().comments.find((c) => c.repository === repo && c.number === number && c.kind === 'review' && c.id === rootId);
262
+ if (!parent)
263
+ return send(ctx, githubNotFound());
264
+ // an empty body is refused as a missing one is
265
+ if (body.body === undefined || String(body.body) === '')
266
+ return ctx.reply({ message: 'Validation Failed', errors: [{ resource: 'PullRequestReviewComment', field: 'body', code: 'missing_field' }] }, 422);
267
+ const seq = nextSeq(ctx, 'comment');
268
+ const anchor = {};
269
+ if (parent.commit_id !== undefined)
270
+ anchor.commit_id = parent.commit_id;
271
+ if (parent.path !== undefined)
272
+ anchor.path = parent.path;
273
+ if (parent.line !== undefined)
274
+ anchor.line = parent.line;
275
+ if (parent.side !== undefined)
276
+ anchor.side = parent.side;
277
+ if (parent.diff_hunk !== undefined)
278
+ anchor.diff_hunk = parent.diff_hunk;
279
+ const fields = { repository: repo, number, body: body.body, created_at: occurredAt, updated_at: occurredAt, in_reply_to: rootId, review_id: implicitReviewId(repo, number), user_login: caller ?? GITHUB_WORLD_USER, author_association: authorAssociation(state(), repo, caller ?? GITHUB_WORLD_USER), ...anchor };
280
+ await put(ctx, { operation: 'pull_request_review_comment.create', subjectType: 'pull_request_review_comment', subjectId: `comment:${seq}`, fields });
281
+ const comment = serializeReviewComment(state().comments.find((c) => c.repository === repo && c.kind === 'review' && c.id === seq));
282
+ await announce(ctx, [{ event: 'pull_request_review_comment', action: 'created', repository: repo, number, comment }]);
283
+ return ctx.reply(comment, 201);
284
+ },
285
+ // A review (diff-thread) comment, wrapped in the pull request's implicit review, bound to the head
286
+ // (or its parent's commit when it replies), with the diff anchor a local write gives it.
287
+ 'pulls/create-review-comment': intoPull(async (ctx, g, _pr, number) => {
288
+ const { repo, body, occurredAt, caller, state } = g;
289
+ const seq = nextSeq(ctx, 'comment');
290
+ const fields = { repository: repo, number, body: body.body, created_at: occurredAt, updated_at: occurredAt, review_id: implicitReviewId(repo, number), user_login: caller ?? GITHUB_WORLD_USER, author_association: authorAssociation(state(), repo, caller ?? GITHUB_WORLD_USER) };
291
+ const parent = body.in_reply_to === undefined ? undefined : state().comments.find((c) => c.repository === repo && c.number === number && c.kind === 'review' && c.id === Number(body.in_reply_to));
292
+ const commitId = parent ? parent.commit_id : body.commit_id ?? state().prs.find((p) => p.repository === repo && p.number === number)?.head_sha;
293
+ if (commitId !== undefined)
294
+ fields.commit_id = String(commitId);
295
+ if (body.path !== undefined)
296
+ fields.path = String(body.path);
297
+ if (body.line !== undefined)
298
+ fields.line = Number(body.line);
299
+ if (body.side !== undefined)
300
+ fields.side = String(body.side);
301
+ if (body.start_line !== undefined)
302
+ fields.start_line = Number(body.start_line);
303
+ if (body.diff_hunk !== undefined)
304
+ fields.diff_hunk = String(body.diff_hunk);
305
+ if (body.in_reply_to !== undefined)
306
+ fields.in_reply_to = Number(body.in_reply_to);
307
+ await put(ctx, { operation: 'pull_request_review_comment.create', subjectType: 'pull_request_review_comment', subjectId: `comment:${seq}`, fields });
308
+ const comment = serializeReviewComment(state().comments.find((c) => c.repository === repo && c.kind === 'review' && c.id === seq));
309
+ await announce(ctx, [{ event: 'pull_request_review_comment', action: 'created', repository: repo, number, comment }]);
310
+ return ctx.reply(comment, 201);
311
+ }),
312
+ // Merge through the gates: 405 when merged or closed or a draft; 409 when `sha` names a head the
313
+ // branch has moved from (the caller's optimistic precondition); then the required checks and the
314
+ // rulesets, and a real merge commit on the plane.
315
+ 'pulls/merge': intoPull(async (ctx, g, pr, number) => {
316
+ const { repo, body, occurredAt, caller, root, state } = g;
317
+ const refused = ctx.legal('pull-request', 'merged', 'pulls/merge', pr.merged)
318
+ ?? ctx.legal('pull-request', 'state', 'pulls/merge', pr.state ?? 'open')
319
+ ?? ctx.legal('pull-request', 'draft', 'pulls/merge', pr.draft);
320
+ if (refused)
321
+ return ctx.refuse(refused);
322
+ if (body.sha !== undefined && String(body.sha) !== pr.head_sha)
323
+ return ctx.reply({ message: 'Head branch was modified. Review and try the merge again.' }, 409);
324
+ const method = body.merge_method ? String(body.merge_method) : 'merge';
325
+ if (!['merge', 'squash', 'rebase'].includes(method))
326
+ return ctx.reply({ message: 'Validation Failed', errors: [{ resource: 'PullRequest', field: 'merge_method', code: 'invalid' }] }, 422);
327
+ const base = pr.base_ref ?? 'main';
328
+ const before = hasBareRepo(repo, root) ? await resolvePlaneRef(repo, base, base, root) : null;
329
+ // what the pull request says it closes, read before the merge moves its base: its body and its commits' messages
330
+ const messages = [];
331
+ for (const sha of (await planeCommitsOf(g, pr)) ?? [])
332
+ messages.push(String((await planeCommitBody(repo, sha, root))?.commit?.message ?? ''));
333
+ const closes = closingIssueNumbers([pr.body ?? '', ...messages]);
334
+ const landed = await mergePullRequest(repo, pr, method, occurredAt, root, caller, writer(ctx));
335
+ if (!landed.ok)
336
+ return ctx.reply({ message: landed.message }, landed.status);
337
+ const st = state();
338
+ const merged = st.prs.find((p) => p.id === `${repo}#${number}`);
339
+ await announce(ctx, [{ event: 'pull_request', action: 'closed', repository: repo, number, pr: toGithubRest(merged, st.milestones) }]);
340
+ // The merge moved the base branch: a push to it, which GitHub's processes read as any push (workflows on push,
341
+ // Dependabot's manifests, the branch's files) (docs.github.com/webhooks/webhook-events-and-payloads#push: "a
342
+ // commit or tag is pushed", as a merge's commit is to the base branch).
343
+ if (before && before !== landed.sha)
344
+ await announce(ctx, [{ event: 'push', action: '', repository: repo, number: 0, ref: `refs/heads/${base}`, before, after: landed.sha }]);
345
+ // A pull request merged into the default branch closes the issues it links with a closing keyword
346
+ // (docs.github.com/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue).
347
+ if (base === (st.repos.find((r) => r.full_name === repo)?.default_branch ?? 'main')) {
348
+ for (const n of closes) {
349
+ const issue = state().issues.find((i) => i.repository === repo && i.number === n);
350
+ // the close is the issue's move by the merge (manifest.ts)
351
+ if (!issue || (issue.state ?? 'open') !== 'open' || ctx.legal('issue', 'state', 'pulls/merge', issue.state ?? 'open', 'closed', issue.id))
352
+ continue;
353
+ await put(ctx, { operation: 'issue.update', subjectType: 'issue', subjectId: issue.id, fields: { number: n, repository: repo, state: 'closed', state_reason: 'completed', updated_at: occurredAt } });
354
+ const now = state();
355
+ const closed = now.issues.find((i) => i.id === issue.id);
356
+ await announce(ctx, [{ event: 'issues', action: 'closed', repository: repo, number: n, ...(closed ? { issue: toGithubIssue(closed, now.milestones, undefined, now.comments) } : {}) }]);
357
+ }
358
+ }
359
+ return ctx.reply({ sha: landed.sha, merged: true, message: 'Pull Request successfully merged' });
360
+ }),
361
+ // a review is requested from someone with access to the repository, never from the pull request's author; a
362
+ // repository the twin knows only through its pull requests (an observed or seeded World) has no access list to check
363
+ 'pulls/request-reviewers': intoPull(async (ctx, g, pr, number) => {
364
+ const asked = normalizeLogins(g.body.reviewers);
365
+ const docs = 'https://docs.github.com/rest/pulls/review-requests#request-reviewers-for-a-pull-request';
366
+ if (pr.user_login && asked.includes(pr.user_login))
367
+ return ctx.reply({ message: 'Review cannot be requested from pull request author.', documentation_url: docs }, 422);
368
+ const st = g.state();
369
+ const access = new Set(assignable(st, g.repo));
370
+ if (st.repos.some((r) => r.full_name === g.repo) && asked.some((login) => !access.has(login)))
371
+ return ctx.reply({ message: `Reviews may only be requested from collaborators. One or more of the users or teams you specified is not a collaborator of the ${g.repo} repository.`, documentation_url: docs }, 422);
372
+ const merged = [...(pr.requested_reviewers ?? [])];
373
+ for (const login of asked)
374
+ if (!merged.includes(login))
375
+ merged.push(login);
376
+ return writePull(ctx, g, number, 'pull_request.request_reviewers', { requested_reviewers: merged }, 'review_requested', 201, 'pull-request-simple');
377
+ }),
378
+ 'pulls/remove-requested-reviewers': intoPull(async (ctx, g, pr, number) => {
379
+ const remove = new Set(normalizeLogins(g.body.reviewers));
380
+ return writePull(ctx, g, number, 'pull_request.remove_requested_reviewers', { requested_reviewers: (pr.requested_reviewers ?? []).filter((login) => !remove.has(login)) }, 'review_request_removed', 200, 'pull-request-simple');
381
+ }),
382
+ // brings the base's latest commits into the head branch with a merge commit, as GitHub's Update branch does (202);
383
+ // a head already up to date stays as it is, and one that conflicts is refused
384
+ // (the head branch is written where it lives: the fork, for a pull request from one)
385
+ 'pulls/update-branch': intoPull(async (ctx, g, pr, number) => {
386
+ const headRef = pr.head_ref ?? '';
387
+ const headRepo = pr.head_repo ?? g.repo;
388
+ const baseTip = await resolvePlaneRef(g.repo, pr.base_ref ?? 'main', pr.base_ref ?? 'main', g.root);
389
+ if (baseTip && headRef) {
390
+ const merged = await mergeIntoBranchInPlane(headRepo, headRef, baseTip, `Merge branch '${pr.base_ref ?? 'main'}' into ${headRef}`, 'merge', g.occurredAt, g.root);
391
+ if (merged && 'conflict' in merged)
392
+ return ctx.reply({ message: 'merge conflict between base and head', documentation_url: 'https://docs.github.com/rest/pulls/pulls#update-a-pull-request-branch' }, 422);
393
+ if (merged && merged.sha !== pr.head_sha) {
394
+ await put(ctx, { operation: 'git_ref.update', subjectType: 'git_ref', subjectId: `gitref:${headRepo}#refs/heads/${headRef}`, fields: { repository: headRepo, ref: `refs/heads/${headRef}`, object_sha: merged.sha, object_type: 'commit' } });
395
+ await put(ctx, { operation: 'branch.create', subjectType: 'branch', subjectId: `branch:${headRepo}#${headRef}`, fields: { repository: headRepo, name: headRef, commit_sha: merged.sha } });
396
+ await syncPrHeadsToBranchTip(headRepo, headRef, merged.sha, g.root, g.occurredAt);
397
+ }
398
+ }
399
+ return ctx.reply({ message: 'Updating pull request branch.', url: `https://api.github.com/repos/${g.repo}/pulls/${number}` }, 202);
400
+ }),
401
+ };
402
+ }
403
+ /** Dismiss a review (PUT .../reviews/:rid/dismissals, a message required) or edit its body
404
+ * (PUT .../reviews/:rid), as an overlay on the submitted review. */
405
+ async function reviewOverlay(ctx, how) {
406
+ const g = gh(ctx);
407
+ const { repo, seg, body, state } = g;
408
+ const number = numberOf(g);
409
+ const rid = Number(decodeURIComponent(seg[6]));
410
+ const submitted = state().reviews.find((r) => r.repository === repo && r.number === number && r.id === rid);
411
+ if (!submitted)
412
+ return send(ctx, githubNotFound());
413
+ if (how === 'dismiss') {
414
+ if (!body.message)
415
+ return ctx.reply({ message: 'Validation Failed', errors: [{ resource: 'PullRequestReview', field: 'message', code: 'missing_field' }] }, 422);
416
+ // the dismissal is the review's move (manifest.ts), kept as an overlay on the submitted review
417
+ const refused = ctx.legal('pull-request-review', 'state', 'pulls/dismiss-review', submitted.state, 'DISMISSED', String(rid));
418
+ if (refused)
419
+ return ctx.reply({ message: refused.message }, refused.status);
420
+ await put(ctx, { operation: 'pull_request_review_overlay.dismiss', subjectType: 'pull_request_review_overlay', subjectId: `reviewoverlay:${rid}`, fields: { review_id: rid, state: 'DISMISSED', dismissed_message: String(body.message) } });
421
+ }
422
+ else {
423
+ if (body.body === undefined)
424
+ return ctx.reply({ message: 'Validation Failed', errors: [{ resource: 'PullRequestReview', field: 'body', code: 'missing_field' }] }, 422);
425
+ await put(ctx, { operation: 'pull_request_review_overlay.update', subjectType: 'pull_request_review_overlay', subjectId: `reviewoverlay:${rid}`, fields: { review_id: rid, body: String(body.body) } });
426
+ }
427
+ const review = toGithubReview(state().reviews.find((x) => x.id === rid));
428
+ await announce(ctx, [{ event: 'pull_request_review', action: how === 'dismiss' ? 'dismissed' : 'edited', repository: repo, number, review }]);
429
+ return ctx.reply(review);
430
+ }
431
+ /** A pull request's changed files: the plane's real diff when its base and head both exist there; never fabricated. */
432
+ async function filesOf(g, pr) {
433
+ return (await prPlaneDiffFiles(g.state(), g.repo, pr, g.root)) ?? [];
434
+ }
435
+ /** An open pull request's mergeability in a repository with history, as GitHub computes it when the pull request is
436
+ * read (docs.github.com/rest/guides/using-the-rest-api-to-interact-with-your-git-database#checking-mergeability-of-pull-
437
+ * requests): whether its head merges into its base without conflicts. Where the documentation stops and the twin
438
+ * decides: a conflict-free merge reads `clean`, a conflicted one `dirty`; the computation is never pending. Nothing
439
+ * when the pull request is closed or its repository has no history (what it carries stands). */
440
+ async function mergeability(g, pr) {
441
+ const { repo, root } = g;
442
+ if (pr.merged || (pr.state ?? 'open') !== 'open' || !pr.head_sha || !hasBareRepo(repo, root))
443
+ return {};
444
+ const merged = await mergeIntoBranchInPlane(repo, pr.base_ref ?? 'main', pr.head_sha, '', 'merge', g.occurredAt, root, undefined, true);
445
+ if (!merged)
446
+ return {};
447
+ return 'conflict' in merged ? { mergeable: false, mergeable_state: 'dirty' } : { mergeable: true, mergeable_state: 'clean' };
448
+ }
449
+ /** In a repository with history, the commits a pull request's head reaches that its base did not hold (a merged
450
+ * one's base is its merge commit's first parent), oldest first; undefined in a repository without history. */
451
+ async function planeCommitsOf(g, pr) {
452
+ const { repo, root } = g;
453
+ if (!pr.head_sha || !hasBareRepo(repo, root))
454
+ return undefined;
455
+ const mergeCommit = pr.merge_commit_sha ? await readGitCommitFromPlane(repo, pr.merge_commit_sha, root) : null;
456
+ const base = mergeCommit?.parents[0] ?? await resolvePlaneRef(repo, pr.base_ref ?? 'main', pr.base_ref ?? 'main', root);
457
+ const held = base ? await planeAncestors(repo, base, root) : new Map();
458
+ // oldest first: by commit time, and within one second (git's resolution) a parent before its child — the walk
459
+ // from the head meets a commit's parents after it, so a later walk position is the older commit
460
+ const walked = [...(await planeAncestors(repo, pr.head_sha, root))].map(([sha, at], i) => ({ sha, at, i }));
461
+ return walked.filter(({ sha }) => !held.has(sha)).sort((a, b) => (a.at - b.at) || (b.i - a.i)).map(({ sha }) => sha);
462
+ }
@@ -0,0 +1,101 @@
1
+ import type { SemanticsContext } from '@volter/world-core';
2
+ import { type GithubState, type GithubWorkflow } from '../github-twin.js';
3
+ /** A workflow's jobs, in file order: each job's id (its key under `jobs:`) and the name GitHub shows for it, its `name:`
4
+ * or else its id (docs.github.com/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idname). */
5
+ export declare function jobsOf(source: string): Array<{
6
+ id: string;
7
+ name: string;
8
+ }>;
9
+ /** The permissions a workflow grants its GITHUB_TOKEN: its `permissions:` blocks (`contents: write`, or `read-all` /
10
+ * `write-all`), over the default a repository created today has, read access to contents and packages
11
+ * (docs.github.com/actions/security-for-github-actions/security-guides/automatic-token-authentication#permissions-for-the-github_token). */
12
+ export declare function tokenPermissions(source: string): Record<string, string>;
13
+ /** A queued run of a workflow, with a job per job of its file at the run's commit (one named for the workflow when
14
+ * the file cannot be read): what a dispatch, a push or a pull request starts. A run from a fork that needs a
15
+ * maintainer's approval first is created waiting, `action_required`, with no jobs until it is approved
16
+ * (approveRun). */
17
+ export declare function queueRun(ctx: SemanticsContext, st: GithubState, repo: string, wf: GithubWorkflow, branch: string, headSha: string, event: string, actor?: string, opts?: {
18
+ awaitingApproval?: boolean;
19
+ headRepo?: string;
20
+ }): Promise<number>;
21
+ /** The check run an Actions job reports on its commit: named for the job, under the GitHub Actions app's check suite,
22
+ * with the job's status and conclusion, and the job's id (a job's check_run_url names it)
23
+ * (docs.github.com/rest/guides/using-the-rest-api-to-interact-with-checks: GitHub Actions reports each job as a check
24
+ * run; docs.github.com/rest/actions/workflow-jobs). */
25
+ export declare function actionsCheckRun(job: {
26
+ id: number;
27
+ repository: string;
28
+ head_sha: string;
29
+ name: string;
30
+ run_id: number;
31
+ }, status: 'queued' | 'completed', conclusion: string | null, at: string): {
32
+ operation: string;
33
+ subjectType: string;
34
+ subjectId: string;
35
+ fields: Record<string, unknown>;
36
+ };
37
+ /** A run's jobs, queued: one per job of the workflow file at the run's commit. The first keeps the id a run's single
38
+ * job has always had. */
39
+ export declare function queueJobs(ctx: SemanticsContext, st: GithubState, repo: string, wf: GithubWorkflow, runId: number, runNumber: number, branch: string, headSha: string): Promise<void>;
40
+ /** The issue numbers texts link with GitHub's closing keywords, `close`, `closes`, `closed`, `fix`, `fixes`, `fixed`,
41
+ * `resolve`, `resolves` or `resolved` before `#n` (docs.github.com/issues/tracking-your-work-with-issues/using-issues/
42
+ * linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword). */
43
+ export declare function closingIssueNumbers(texts: string[]): number[];
44
+ /** Whether a pull request's author needs a maintainer to approve its workflow runs: by default a first-time
45
+ * contributor from a fork, "users who have never had a commit or pull request merged into the repository"
46
+ * (docs.github.com/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/
47
+ * managing-github-actions-settings-for-a-repository#controlling-changes-from-forks-to-workflows-in-public-repositories).
48
+ * Where the documentation stops and the twin decides: someone with write access to the repository (its owner, an
49
+ * owner of its organization, a collaborator with write or more) is not a first-time contributor. */
50
+ export declare function needsApproval(st: GithubState, repo: string, author: string | null | undefined): boolean;
51
+ /** Whether `login` has write access to `repo`: its owner, an owner of its organization, or a collaborator with write
52
+ * or more. */
53
+ export declare function canWrite(st: GithubState, repo: string, login: string): boolean;
54
+ /** A pull request opened, reopened or pushed to runs the base repository's workflows whose `on:` includes
55
+ * pull_request, as its head commit has them (docs.github.com/actions/writing-workflows/choosing-when-your-workflow-
56
+ * runs/events-that-trigger-workflows#pull_request: by default on opened, synchronize and reopened). A first-time
57
+ * contributor's run from a fork waits for approval (needsApproval). */
58
+ export declare function reactToPullRequest(ctx: SemanticsContext, repo: string, number: number, action: string): Promise<void>;
59
+ /** A direct push to a branch whose rulesets require pull requests fails those rules, and the push is refused when
60
+ * one is active (docs.github.com/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/
61
+ * available-rules-for-rulesets#require-a-pull-request-before-merging). In evaluate mode the push lands and its rule
62
+ * suite records what would have failed (docs.github.com/.../managing-rulesets/managing-rulesets-for-a-repository#viewing-insights-for-rulesets). */
63
+ export declare function directPushRules(st: GithubState, repo: string, branch: string): {
64
+ refused: string | undefined;
65
+ evaluations: Array<{
66
+ ruleset_id: number;
67
+ ruleset_name: string;
68
+ rule_type: string;
69
+ enforcement: string;
70
+ result: string;
71
+ details: string | null;
72
+ }>;
73
+ };
74
+ /** GitHub's reactions to a branch of `repo` moving from `before` to `after`. */
75
+ export declare function reactToPush(ctx: SemanticsContext, repo: string, ref: string, before: string, after: string): Promise<void>;
76
+ /** Whether Dependabot alerts are on for a repository: off until someone with admin access turns them on
77
+ * (docs.github.com/code-security/dependabot/dependabot-alerts/configuring-dependabot-alerts,
78
+ * docs.github.com/rest/repos/repos#enable-vulnerability-alerts). */
79
+ export declare function dependabotOn(ctx: SemanticsContext, repo: string): boolean;
80
+ /** Dependabot reads package.json manifests at a commit of the default branch: an alert opens for a dependency an
81
+ * advisory names at a vulnerable version, and an open one is fixed once the manifest no longer asks for one
82
+ * (docs.github.com/code-security/dependabot/dependabot-alerts/about-dependabot-alerts). */
83
+ export declare function dependabotReads(ctx: SemanticsContext, repo: string, commit: string, manifests: string[], removed?: string[]): Promise<void>;
84
+ /** Every package.json at a commit, for Dependabot's first read when its alerts are turned on. */
85
+ export declare function manifestsAt(repo: string, commit: string, root?: string): Promise<string[]>;
86
+ /** What a run's workflow steps uploaded, read from the workflow file at the run's commit: its artifacts, uploaded while
87
+ * it runs, and its caches, saved when its job ends. */
88
+ export declare function runOutputs(repo: string, run: {
89
+ id: number;
90
+ workflow_id: number;
91
+ head_sha: string;
92
+ head_branch?: string;
93
+ }, st: GithubState, root: string | undefined, write: (w: {
94
+ operation: string;
95
+ subjectType: string;
96
+ subjectId: string;
97
+ fields: Record<string, unknown>;
98
+ }) => Promise<void>, nextId: (prefix: string) => number, at: string, what?: {
99
+ artifacts: boolean;
100
+ caches: boolean;
101
+ }): Promise<void>;