@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,161 @@
1
+ import { GITHUB_WORLD_USER, githubActivityEvents, githubNotFound, paginate, seedUser, toGithubNotification, toGithubRepo, toGithubUser } from "../github-twin.js";
2
+ import { announce, asView, gh, put, send } from "./shared.js";
3
+ /** Mark one notification thread read (or unread), keeping the fields it was stored with. */
4
+ function markThread(ctx, n, unread) {
5
+ return put(ctx, { operation: 'notification.read', subjectType: 'notification', subjectId: `notification:${n.id}`, fields: { id: n.id, repository: n.repository, subject_title: n.subject_title, subject_type: n.subject_type, reason: n.reason, unread, updated_at: ctx.occurredAt } });
6
+ }
7
+ /** The inbox as GitHub lists it: the most recently updated thread first. */
8
+ const latestFirst = (threads) => threads.map((n, i) => [n, i]).sort((a, b) => String(b[0].updated_at ?? '').localeCompare(String(a[0].updated_at ?? '')) || b[1] - a[1]).map(([n]) => n);
9
+ /** A thread's subscription as GitHub answers it. */
10
+ function subscriptionOf(n) {
11
+ const sub = n.subscription === null ? { subscribed: false, ignored: false, created_at: undefined } : (n.subscription ?? { subscribed: true, ignored: false, created_at: n.updated_at });
12
+ return { subscribed: sub.subscribed, ignored: sub.ignored, reason: null, created_at: sub.created_at ?? null, url: `https://api.github.com/notifications/threads/${n.id}/subscription`, thread_url: `https://api.github.com/notifications/threads/${n.id}` };
13
+ }
14
+ /** Who the credential names: its user, or the World's own for its shared token. */
15
+ export const me = (ctx) => gh(ctx).caller ?? GITHUB_WORLD_USER;
16
+ /** GET .../stargazers and .../subscribers: everyone who stars or watches the repo. */
17
+ const repoFans = (stars) => async (ctx) => {
18
+ const { repo, query, state } = gh(ctx);
19
+ const st = state();
20
+ const items = (stars ? st.starEdges : st.watchEdges).filter((e) => e.to === repo).map((e) => asView('simple-user', toGithubUser(seedUser(e.from))));
21
+ return ctx.reply(paginate(items, query));
22
+ };
23
+ const star = (starred) => async (ctx) => {
24
+ const { seg } = gh(ctx);
25
+ const repo = `${seg[2]}/${seg[3]}`;
26
+ await put(ctx, { operation: starred ? 'star.add' : 'star.remove', subjectType: 'star', subjectId: `star:${me(ctx)}:${repo}`, fields: { repository: repo, starred } });
27
+ await announce(ctx, [{ event: 'star', action: starred ? 'created' : 'deleted', repository: repo, number: 0 }]);
28
+ return send(ctx, { status: 204, body: undefined });
29
+ };
30
+ const watch = (subscribed) => async (ctx) => {
31
+ const { repo } = gh(ctx);
32
+ await put(ctx, { operation: subscribed ? 'repo_subscription.watch' : 'repo_subscription.unwatch', subjectType: 'repo_subscription', subjectId: `watch:${me(ctx)}:${repo}`, fields: { repository: repo, subscribed } });
33
+ if (!subscribed)
34
+ return send(ctx, { status: 204, body: undefined });
35
+ return ctx.reply({ subscribed: true, ignored: false, reason: null, repository_url: `https://api.github.com/repos/${repo}` });
36
+ };
37
+ export function activitySemantics() {
38
+ const activityGetFeeds = async (ctx) => {
39
+ const api = 'https://github.com';
40
+ const link = (href) => ({ href, type: 'application/atom+xml' });
41
+ return ctx.reply({
42
+ timeline_url: `${api}/timeline`, user_url: `${api}/{user}`,
43
+ current_user_public_url: `${api}/octocat`, current_user_url: `${api}/octocat.private?token=t`,
44
+ current_user_actor_url: `${api}/octocat.private.actor?token=t`,
45
+ current_user_organization_url: '', current_user_organization_urls: [],
46
+ security_advisories_url: `${api}/security-advisories`,
47
+ _links: { timeline: link(`${api}/timeline`), user: link(`${api}/{user}`), current_user_public: link(`${api}/octocat`), security_advisories: link(`${api}/security-advisories`) },
48
+ });
49
+ };
50
+ const activityListPublicEvents = async (ctx) => {
51
+ const { query, state } = gh(ctx);
52
+ return ctx.reply(paginate(githubActivityEvents(state()), query));
53
+ };
54
+ const activityListRepoEvents = async (ctx) => {
55
+ const { repo, query, state } = gh(ctx);
56
+ return ctx.reply(paginate(githubActivityEvents(state()).filter((e) => e.repo.name === repo), query));
57
+ };
58
+ const activityListNotificationsForAuthenticatedUser = async (ctx) => {
59
+ const { query, state } = gh(ctx);
60
+ const all = new URLSearchParams((query ?? '').replace(/^\?/, '')).get('all') === 'true';
61
+ return ctx.reply(paginate(latestFirst(state().notifications.filter((n) => all || n.unread)), query).map(toGithubNotification));
62
+ };
63
+ const activityListRepoNotificationsForAuthenticatedUser = async (ctx) => {
64
+ const { repo, query, state } = gh(ctx);
65
+ const all = new URLSearchParams((query ?? '').replace(/^\?/, '')).get('all') === 'true';
66
+ return ctx.reply(paginate(latestFirst(state().notifications.filter((n) => n.repository === repo && (all || n.unread))), query).map(toGithubNotification));
67
+ };
68
+ const activityGetThread = async (ctx) => {
69
+ const { seg, state } = gh(ctx);
70
+ const n = state().notifications.find((x) => x.id === decodeURIComponent(seg[2]));
71
+ return send(ctx, n ? { status: 200, body: toGithubNotification(n) } : githubNotFound());
72
+ };
73
+ const activityGetThreadSubscriptionForAuthenticatedUser = async (ctx) => {
74
+ const { seg, state } = gh(ctx);
75
+ const n = state().notifications.find((x) => x.id === decodeURIComponent(seg[2]));
76
+ return send(ctx, n ? { status: 200, body: subscriptionOf(n) } : githubNotFound());
77
+ };
78
+ const activitySetThreadSubscription = async (ctx) => {
79
+ const { seg, body, state, occurredAt } = gh(ctx);
80
+ const n = state().notifications.find((x) => x.id === decodeURIComponent(seg[2]));
81
+ if (!n)
82
+ return send(ctx, githubNotFound());
83
+ const subscription = { subscribed: body.ignored !== true, ignored: body.ignored === true, created_at: occurredAt };
84
+ await put(ctx, { operation: 'notification.subscribe', subjectType: 'notification', subjectId: `notification:${n.id}`, fields: { id: n.id, subscription, ...(subscription.ignored ? { unread: false } : {}) } });
85
+ return ctx.reply(subscriptionOf({ ...n, subscription }));
86
+ };
87
+ const activityDeleteThreadSubscription = async (ctx) => {
88
+ const { seg, state } = gh(ctx);
89
+ const n = state().notifications.find((x) => x.id === decodeURIComponent(seg[2]));
90
+ if (!n)
91
+ return send(ctx, githubNotFound());
92
+ await put(ctx, { operation: 'notification.unsubscribe', subjectType: 'notification', subjectId: `notification:${n.id}`, fields: { id: n.id, subscription: null } });
93
+ return send(ctx, { status: 204, body: undefined });
94
+ };
95
+ const activityMarkThreadAsRead = async (ctx) => {
96
+ const { seg, state } = gh(ctx);
97
+ const n = state().notifications.find((x) => x.id === decodeURIComponent(seg[2]));
98
+ if (!n)
99
+ return send(ctx, githubNotFound());
100
+ await markThread(ctx, n, false);
101
+ return send(ctx, { status: 205, body: undefined });
102
+ };
103
+ const activityMarkNotificationsAsRead = async (ctx) => {
104
+ for (const n of gh(ctx).state().notifications.filter((x) => x.unread))
105
+ await markThread(ctx, n, false);
106
+ return send(ctx, { status: 205, body: undefined });
107
+ };
108
+ const activityMarkRepoNotificationsAsRead = async (ctx) => {
109
+ const { repo, state } = gh(ctx);
110
+ for (const n of state().notifications.filter((x) => x.repository === repo && x.unread))
111
+ await markThread(ctx, n, false);
112
+ return send(ctx, { status: 205, body: undefined });
113
+ };
114
+ const activityListReposStarredByAuthenticatedUser = async (ctx) => {
115
+ const { query, state } = gh(ctx);
116
+ const st = state();
117
+ const mine = st.starEdges.filter((e) => e.from === me(ctx)).map((e) => e.to);
118
+ const items = mine.map((full) => st.repos.find((r) => r.full_name === full)).filter((r) => !!r).map((r) => asView('repository', toGithubRepo(r)));
119
+ return ctx.reply(paginate(items, query));
120
+ };
121
+ const activityCheckRepoIsStarredByAuthenticatedUser = async (ctx) => {
122
+ const { seg, state } = gh(ctx);
123
+ return send(ctx, state().starEdges.some((e) => e.from === me(ctx) && e.to === `${seg[2]}/${seg[3]}`) ? { status: 204, body: undefined } : githubNotFound());
124
+ };
125
+ const activityGetRepoSubscription = async (ctx) => {
126
+ const { repo, state } = gh(ctx);
127
+ return send(ctx, state().watchEdges.some((e) => e.from === me(ctx) && e.to === repo) ? { status: 200, body: { subscribed: true, ignored: false, reason: null, repository_url: `https://api.github.com/repos/${repo}` } } : githubNotFound());
128
+ };
129
+ return {
130
+ // the feed-URL catalog (timeline/user/security-advisories links); stateless
131
+ 'activity/get-feeds': activityGetFeeds,
132
+ // the public timeline: every modeled event
133
+ 'activity/list-public-events': activityListPublicEvents,
134
+ // the same projection scoped to one repo
135
+ 'activity/list-repo-events': activityListRepoEvents,
136
+ // the inbox: unread threads, or every thread with `all=true`
137
+ 'activity/list-notifications-for-authenticated-user': activityListNotificationsForAuthenticatedUser,
138
+ 'activity/list-repo-notifications-for-authenticated-user': activityListRepoNotificationsForAuthenticatedUser,
139
+ 'activity/get-thread': activityGetThread,
140
+ 'activity/get-thread-subscription-for-authenticated-user': activityGetThreadSubscriptionForAuthenticatedUser,
141
+ // subscribe, or ignore (mute) the thread, which also takes it out of the inbox
142
+ 'activity/set-thread-subscription': activitySetThreadSubscription,
143
+ // unsubscribe: muted until the person is next @mentioned
144
+ 'activity/delete-thread-subscription': activityDeleteThreadSubscription,
145
+ // mark a thread read (205)
146
+ 'activity/mark-thread-as-read': activityMarkThreadAsRead,
147
+ 'activity/mark-notifications-as-read': activityMarkNotificationsAsRead,
148
+ 'activity/mark-repo-notifications-as-read': activityMarkRepoNotificationsAsRead,
149
+ 'activity/list-stargazers-for-repo': repoFans(true),
150
+ 'activity/list-watchers-for-repo': repoFans(false),
151
+ // the authenticated user's starred repos, and the membership check (204 starred / 404 not)
152
+ 'activity/list-repos-starred-by-authenticated-user': activityListReposStarredByAuthenticatedUser,
153
+ 'activity/check-repo-is-starred-by-authenticated-user': activityCheckRepoIsStarredByAuthenticatedUser,
154
+ 'activity/star-repo-for-authenticated-user': star(true),
155
+ 'activity/unstar-repo-for-authenticated-user': star(false),
156
+ // the authenticated user's watch on a repo ({subscribed:true} or 404)
157
+ 'activity/get-repo-subscription': activityGetRepoSubscription,
158
+ 'activity/set-repo-subscription': watch(true),
159
+ 'activity/delete-repo-subscription': watch(false),
160
+ };
161
+ }
@@ -0,0 +1,2 @@
1
+ import type { Semantics } from '@volter/world-core';
2
+ export declare function appsSemantics(): Record<string, Semantics>;
@@ -0,0 +1,144 @@
1
+ // GitHub's Apps family: an app by its slug, the app's installations and installation access tokens. An app
2
+ // is registered through the twin's door (GitHub's settings UI has no API) and installed by a person on its
3
+ // page (src/screens/app-installation.tsx); a token is minted deterministically from its installation.
4
+ import { Buffer } from 'node:buffer';
5
+ import { createHash } from 'node:crypto';
6
+ import { githubApps, githubNotFound, paginate, toGithubApp, toGithubAppInstallation } from "../github-twin.js";
7
+ import { gh, put, send } from "./shared.js";
8
+ import { emitGithubAppEvent, githubAppDeliveries, githubAppDelivery } from "../github-events.js";
9
+ // An app speaks for itself with a JSON Web Token it signs with its private key: its client ID (or app ID) as `iss`,
10
+ // issued no later than now and expiring at most ten minutes after it was issued
11
+ // (docs.github.com/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app).
12
+ // The app's installations and their access tokens take only that token ("You must use a JWT to access this endpoint",
13
+ // docs.github.com/rest/apps/apps). Where the evidence stops: the private key is generated on the app's settings page
14
+ // and never leaves the app's server, so the twin holds no key to check the RS256 signature against; it reads the
15
+ // token's claims, and a token whose claims do not name a registered app, or that is expired, is refused.
16
+ const JWT_DOCS = 'https://docs.github.com/rest/apps/apps';
17
+ const claimsOf = (token) => {
18
+ const parts = (token ?? '').split('.');
19
+ try {
20
+ return parts.length === 3 ? JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8')) : undefined;
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ };
26
+ /** The app a request's JWT authenticates, or nothing when the token is not a live JWT of a registered app. */
27
+ function appOfJwt(ctx) {
28
+ const claims = claimsOf(gh(ctx).token);
29
+ const now = Math.floor(Date.parse(ctx.occurredAt) / 1000);
30
+ const live = typeof claims?.iat === 'number' && typeof claims.exp === 'number' && claims.iat <= now + 60 && claims.exp > now && claims.exp - claims.iat <= 600;
31
+ return live ? githubApps(ctx.root).find((a) => String(claims.iss) === a.client_id || String(claims.iss) === String(a.id)) : undefined;
32
+ }
33
+ /** GitHub's answer to a call on an app's own endpoints without a live JWT of the app. */
34
+ const jwtRefused = (ctx) => ctx.reply({ message: 'A JSON web token could not be decoded', documentation_url: JWT_DOCS }, 401);
35
+ /** An installation of the app the request's JWT authenticates, by its id. */
36
+ const ownInstallation = (ctx, app) => gh(ctx).state().appInstallations.find((x) => x.id === Number(decodeURIComponent(gh(ctx).seg[2])) && x.app_slug === app.slug);
37
+ export function appsSemantics() {
38
+ const appsGetBySlug = async (ctx) => {
39
+ const { seg, state } = gh(ctx);
40
+ const slug = decodeURIComponent(seg[1]);
41
+ const app = githubApps(ctx.root).find((a) => a.slug === slug);
42
+ return send(ctx, app ? { status: 200, body: toGithubApp(app, state().appInstallations.filter((i) => i.app_slug === slug).length, state()) } : githubNotFound());
43
+ };
44
+ const appsGetAuthenticated = async (ctx) => {
45
+ const app = appOfJwt(ctx);
46
+ if (!app)
47
+ return jwtRefused(ctx);
48
+ const { state } = gh(ctx);
49
+ return ctx.reply(toGithubApp(app, state().appInstallations.filter((i) => i.app_slug === app.slug).length, state()));
50
+ };
51
+ const appsListInstallations = async (ctx) => {
52
+ const app = appOfJwt(ctx);
53
+ if (!app)
54
+ return jwtRefused(ctx);
55
+ const { query, state } = gh(ctx);
56
+ return ctx.reply(paginate(state().appInstallations.filter((i) => i.app_slug === app.slug), query).map(toGithubAppInstallation));
57
+ };
58
+ const appsGetInstallation = async (ctx) => {
59
+ const app = appOfJwt(ctx);
60
+ if (!app)
61
+ return jwtRefused(ctx);
62
+ const i = ownInstallation(ctx, app);
63
+ return send(ctx, i ? { status: 200, body: toGithubAppInstallation(i) } : githubNotFound());
64
+ };
65
+ return {
66
+ // a public app's page data, by its slug
67
+ 'apps/get-by-slug': appsGetBySlug,
68
+ // The application that sent a person to register it from its manifest exchanges the temporary code GitHub
69
+ // redirected back with for the app's credentials: once, within an hour, with no other authentication (201)
70
+ // (docs.github.com/rest/apps/apps#create-a-github-app-from-a-manifest,
71
+ // docs.github.com/apps/sharing-github-apps/registering-a-github-app-from-a-manifest). A code used or expired is
72
+ // Not Found. Where the documentation stops and the twin decides: the private key is a stand-in of the PEM's shape
73
+ // that nothing verifies (a JWT's signature is not checked, see appOfJwt).
74
+ 'apps/create-from-manifest': async (ctx) => {
75
+ const { seg, occurredAt, state } = gh(ctx);
76
+ const code = decodeURIComponent(seg[1]);
77
+ const row = ctx.tree().find((r) => r.type === 'app_manifest_code' && r.code === code && r.deleted !== true);
78
+ const expired = row && Date.parse(occurredAt) - Date.parse(String(row.created_at)) > 60 * 60 * 1000;
79
+ const app = row && githubApps(ctx.root).find((a) => a.slug === row.app_slug);
80
+ if (!row || row.used === true || expired || !app)
81
+ return send(ctx, githubNotFound());
82
+ await put(ctx, { operation: 'app_manifest_code.use', subjectType: 'app_manifest_code', subjectId: `manifestcode:${code}`, fields: { used: true } });
83
+ const secret = (what) => createHash('sha256').update(`app:${app.slug}:${what}`).digest('hex');
84
+ const pem = `-----BEGIN RSA PRIVATE KEY-----\n${Buffer.from(secret('key') + secret('key2')).toString('base64').match(/.{1,64}/g).join('\n')}\n-----END RSA PRIVATE KEY-----\n`;
85
+ return ctx.reply({ ...toGithubApp(app, 0, state()), client_secret: secret('client').slice(0, 40), webhook_secret: app.webhook_secret ?? null, pem }, 201);
86
+ },
87
+ // the app the JWT authenticates (docs.github.com/rest/apps/apps#get-the-authenticated-app)
88
+ 'apps/get-authenticated': appsGetAuthenticated,
89
+ // the deliveries of the app's own webhook, and one in detail with the payload it carried
90
+ // (docs.github.com/rest/apps/webhooks#list-deliveries-for-an-app-webhook)
91
+ 'apps/list-webhook-deliveries': async (ctx) => {
92
+ const app = appOfJwt(ctx);
93
+ if (!app)
94
+ return jwtRefused(ctx);
95
+ return ctx.reply(paginate(githubAppDeliveries(app.id, ctx.root), gh(ctx).query));
96
+ },
97
+ 'apps/get-webhook-delivery': async (ctx) => {
98
+ const app = appOfJwt(ctx);
99
+ if (!app)
100
+ return jwtRefused(ctx);
101
+ const d = githubAppDelivery(app.id, Number(decodeURIComponent(gh(ctx).seg[3])), ctx.root);
102
+ return send(ctx, d ? { status: 200, body: d } : githubNotFound());
103
+ },
104
+ // the app uninstalls itself from an account: the installation and its tokens are gone (204)
105
+ // (docs.github.com/rest/apps/apps#delete-an-installation-for-the-authenticated-app)
106
+ 'apps/delete-installation': async (ctx) => {
107
+ const app = appOfJwt(ctx);
108
+ if (!app)
109
+ return jwtRefused(ctx);
110
+ const i = ownInstallation(ctx, app);
111
+ if (!i)
112
+ return send(ctx, githubNotFound());
113
+ await put(ctx, { operation: 'app_installation.delete', subjectType: 'app_installation', subjectId: `installation:${i.id}`, fields: { deleted: true } });
114
+ for (const t of ctx.tree().filter((r) => r.type === 'app_token' && r.installation_id === i.id && r.deleted !== true)) {
115
+ await put(ctx, { operation: 'app_token.revoke', subjectType: 'app_token', subjectId: String(t.id), fields: { deleted: true } });
116
+ }
117
+ try {
118
+ await emitGithubAppEvent(app, 'installation', { action: 'deleted', installation: toGithubAppInstallation(i), sender: { login: `${app.slug}[bot]`, type: 'Bot' } }, { occurredAt: ctx.occurredAt, ...(ctx.root !== undefined ? { root: ctx.root } : {}) });
119
+ }
120
+ catch { /* a delivery failure never fails the deletion */ }
121
+ return send(ctx, { status: 204, body: undefined });
122
+ },
123
+ // the installations of the app the JWT authenticates
124
+ 'apps/list-installations': appsListInstallations,
125
+ 'apps/get-installation': appsGetInstallation,
126
+ // A deterministic installation token (ghs_…) bound to the installation's permissions. Real tokens
127
+ // expire in about an hour; the twin answers a fixed far-future expiry (never the clock).
128
+ 'apps/create-installation-access-token': async (ctx) => {
129
+ const app = appOfJwt(ctx);
130
+ if (!app)
131
+ return jwtRefused(ctx);
132
+ const { body } = gh(ctx);
133
+ const i = ownInstallation(ctx, app);
134
+ if (!i)
135
+ return send(ctx, githubNotFound());
136
+ const id = i.id;
137
+ const token = `ghs_${createHash('sha256').update(`insttoken:${id}`).digest('hex').slice(0, 36)}`;
138
+ const perms = body.permissions ?? i.permissions;
139
+ // the token is the app's credential for this installation: what it may do, and on which repositories
140
+ await put(ctx, { operation: 'app_token.create', subjectType: 'app_token', subjectId: `apptoken:${token}`, fields: { token, installation_id: id, app_slug: i.app_slug, account_login: i.account_login, permissions: perms, repository_selection: i.repository_selection, repositories: i.repositories ?? [] } });
141
+ return ctx.reply({ token, expires_at: '2100-01-01T00:00:00Z', permissions: perms, repository_selection: i.repository_selection }, 201);
142
+ },
143
+ };
144
+ }
@@ -0,0 +1,2 @@
1
+ import type { Semantics } from '@volter/world-core';
2
+ export declare function branchesSemantics(): Record<string, Semantics>;
@@ -0,0 +1,136 @@
1
+ import { githubNotFound, isAncestorInPlane, mergeIntoBranchInPlane, normalizeRequiredStatusChecks, paginate, resolvePlaneRef, toGithubBranch, toGithubBranchProtection, updateGitRefInPlane } from "../github-twin.js";
2
+ import { announce, gh, put, send } from "./shared.js";
3
+ const SYNC_DOCS = 'https://docs.github.com/rest/branches/branches#sync-a-fork-branch-with-the-upstream-repository';
4
+ /** A fork's branch brought up to date with the same branch of the repository it was forked from: fast-forwarded when
5
+ * it has nothing of its own, merged when it has, 409 when the merge conflicts, 422 when the repository is no fork or
6
+ * names no branch (docs.github.com/rest/branches/branches#sync-a-fork-branch-with-the-upstream-repository). */
7
+ const syncFork = async (ctx) => {
8
+ const { repo, body, root, occurredAt, state } = gh(ctx);
9
+ const fork = state().repos.find((r) => r.full_name === repo);
10
+ if (!fork)
11
+ return send(ctx, githubNotFound());
12
+ const branch = typeof body.branch === 'string' ? body.branch : '';
13
+ const upstream = fork.parent_full_name;
14
+ const theirs = upstream && branch ? await resolvePlaneRef(upstream, `refs/heads/${branch}`, branch, root) : null;
15
+ const ours = branch ? await resolvePlaneRef(repo, `refs/heads/${branch}`, branch, root) : null;
16
+ if (!upstream || !theirs || !ours)
17
+ return ctx.reply({ message: 'Validation Failed', errors: [{ resource: 'Repository', field: 'branch', code: 'invalid' }], documentation_url: SYNC_DOCS }, 422);
18
+ const base_branch = `${upstream.split('/')[0]}:${branch}`;
19
+ if (ours === theirs || await isAncestorInPlane(repo, theirs, ours, root))
20
+ return ctx.reply({ message: `This branch is not behind the upstream ${base_branch}.`, merge_type: 'none', base_branch });
21
+ const forward = await isAncestorInPlane(repo, ours, theirs, root);
22
+ const landed = forward ? (await updateGitRefInPlane(repo, `refs/heads/${branch}`, theirs, root, ours) ? { sha: theirs } : null) : await mergeIntoBranchInPlane(repo, branch, theirs, `Merge branch '${branch}' of github.com:${upstream}`, 'merge', occurredAt, root);
23
+ if (!landed || 'conflict' in landed)
24
+ return ctx.reply({ message: 'There are merge conflicts', documentation_url: SYNC_DOCS }, 409);
25
+ await put(ctx, { operation: 'git_ref.update', subjectType: 'git_ref', subjectId: `gitref:${repo}#refs/heads/${branch}`, fields: { repository: repo, ref: `refs/heads/${branch}`, object_sha: landed.sha, object_type: 'commit' } });
26
+ await put(ctx, { operation: 'branch.create', subjectType: 'branch', subjectId: `branch:${repo}#${branch}`, fields: { repository: repo, name: branch, commit_sha: landed.sha } });
27
+ await announce(ctx, [{ event: 'push', action: '', repository: repo, number: 0, ref: `refs/heads/${branch}`, before: ours, after: landed.sha }]);
28
+ return ctx.reply({ message: forward ? `Successfully fetched and fast-forwarded from upstream ${base_branch}.` : `Successfully fetched and merged from upstream ${base_branch}.`, merge_type: forward ? 'fast-forward' : 'merge', base_branch });
29
+ };
30
+ const notProtected = { status: 404, body: { message: 'Branch not protected', documentation_url: 'https://docs.github.com/rest/branches/branch-protection' } };
31
+ /** The branch a protection path names (a name may hold slashes). */
32
+ function branchOf(ctx) {
33
+ const { repo, seg, state } = gh(ctx);
34
+ const name = decodeURIComponent(seg[4]);
35
+ return { name, b: state().branches.find((x) => x.repository === repo && x.name === name) };
36
+ }
37
+ const protectionUrl = (repo, name) => `https://api.github.com/repos/${repo}/branches/${encodeURIComponent(name)}/protection`;
38
+ /** Write a protected branch's config. */
39
+ const protect = (ctx, name, b, protection_config) => put(ctx, { operation: 'branch.protect', subjectType: 'branch', subjectId: `branch:${gh(ctx).repo}#${name}`, fields: { repository: gh(ctx).repo, name, commit_sha: b.commit_sha, protected: true, protection_config } });
40
+ export function branchesSemantics() {
41
+ const reposListBranches = async (ctx) => {
42
+ const { repo, query, state } = gh(ctx);
43
+ return ctx.reply(paginate(state().branches.filter((b) => b.repository === repo), query).map(toGithubBranch));
44
+ };
45
+ const reposGetBranchProtection = async (ctx) => {
46
+ const { b } = branchOf(ctx);
47
+ return send(ctx, b?.protected ? { status: 200, body: toGithubBranchProtection(b) } : notProtected);
48
+ };
49
+ const reposGetStatusChecksProtection = async (ctx) => {
50
+ const { repo } = gh(ctx);
51
+ const { name, b } = branchOf(ctx);
52
+ if (!b?.protected)
53
+ return send(ctx, notProtected);
54
+ const rsc = b.protection_config?.required_status_checks;
55
+ if (!rsc)
56
+ return ctx.reply({ message: 'Required status checks not enabled', documentation_url: 'https://docs.github.com/rest/branches/branch-protection' }, 404);
57
+ const base = protectionUrl(repo, name);
58
+ return ctx.reply({ url: `${base}/required_status_checks`, strict: rsc.strict, contexts: rsc.contexts, contexts_url: `${base}/required_status_checks/contexts`, checks: rsc.checks });
59
+ };
60
+ const reposGetAdminBranchProtection = async (ctx) => {
61
+ const { repo } = gh(ctx);
62
+ const { name, b } = branchOf(ctx);
63
+ if (!b?.protected)
64
+ return send(ctx, notProtected);
65
+ return ctx.reply({ url: `${protectionUrl(repo, name)}/enforce_admins`, enabled: b.protection_config?.enforce_admins ?? false });
66
+ };
67
+ const reposUpdateStatusCheckProtection = async (ctx) => {
68
+ const { repo, body } = gh(ctx);
69
+ const { name, b } = branchOf(ctx);
70
+ if (!b?.protected)
71
+ return send(ctx, githubNotFound());
72
+ const prev = b.protection_config?.required_status_checks ?? { strict: false, contexts: [], checks: [] };
73
+ const next = normalizeRequiredStatusChecks({ strict: body.strict ?? prev.strict, contexts: body.contexts ?? (body.checks === undefined ? prev.contexts : undefined), checks: body.checks });
74
+ const base = protectionUrl(repo, name);
75
+ await protect(ctx, name, b, { ...(b.protection_config ?? {}), required_status_checks: next });
76
+ return ctx.reply({ url: `${base}/required_status_checks`, strict: next.strict, contexts: next.contexts, contexts_url: `${base}/required_status_checks/contexts`, checks: next.checks });
77
+ };
78
+ return {
79
+ 'repos/merge-upstream': syncFork,
80
+ 'repos/list-branches': reposListBranches,
81
+ // a branch name may hold slashes
82
+ 'repos/get-branch': async (ctx) => {
83
+ const { repo, seg, state } = gh(ctx);
84
+ const name = decodeURIComponent(seg.slice(4).join('/'));
85
+ const b = state().branches.find((x) => x.repository === repo && x.name === name);
86
+ return send(ctx, b ? { status: 200, body: toGithubBranch(b) } : githubNotFound());
87
+ },
88
+ // the persisted config back out; 404 "Branch not protected" when protection is off
89
+ 'repos/get-branch-protection': reposGetBranchProtection,
90
+ 'repos/get-status-checks-protection': reposGetStatusChecksProtection,
91
+ 'repos/get-admin-branch-protection': reposGetAdminBranchProtection,
92
+ // Protect and persist the config. GitHub 422s unless all four keys are present (each nullable,
93
+ // never omittable). A branch the twin has not seen is created by its first protection write.
94
+ 'repos/update-branch-protection': async (ctx) => protection(ctx, true),
95
+ 'repos/delete-branch-protection': async (ctx) => protection(ctx, false),
96
+ // update the required checks of a protected branch (404 when not protected)
97
+ 'repos/update-status-check-protection': reposUpdateStatusCheckProtection,
98
+ 'repos/set-admin-branch-protection': async (ctx) => enforceAdmins(ctx, true),
99
+ 'repos/delete-admin-branch-protection': async (ctx) => enforceAdmins(ctx, false),
100
+ };
101
+ }
102
+ async function protection(ctx, protecting) {
103
+ const { repo, body, state } = gh(ctx);
104
+ const { name, b } = branchOf(ctx);
105
+ // a branch must exist to be protected (docs.github.com/rest/branches/branch-protection#update-branch-protection)
106
+ if (!b)
107
+ return ctx.reply({ message: 'Branch not found', documentation_url: 'https://docs.github.com/rest/branches/branch-protection#update-branch-protection' }, 404);
108
+ if (protecting) {
109
+ const missingKeys = ['required_status_checks', 'enforce_admins', 'required_pull_request_reviews', 'restrictions'].filter((k) => !(k in body));
110
+ if (missingKeys.length)
111
+ return ctx.reply({ message: 'Validation Failed', errors: missingKeys.map((field) => ({ resource: 'BranchProtection', field, code: 'missing_field' })) }, 422);
112
+ }
113
+ const protection_config = protecting
114
+ ? {
115
+ required_status_checks: normalizeRequiredStatusChecks(body.required_status_checks),
116
+ enforce_admins: body.enforce_admins === true,
117
+ required_pull_request_reviews: body.required_pull_request_reviews && typeof body.required_pull_request_reviews === 'object' ? body.required_pull_request_reviews : null,
118
+ restrictions: body.restrictions && typeof body.restrictions === 'object' ? body.restrictions : null,
119
+ }
120
+ : null;
121
+ await put(ctx, { operation: protecting ? 'branch.protect' : 'branch.unprotect', subjectType: 'branch', subjectId: `branch:${repo}#${name}`, fields: { repository: repo, name, commit_sha: b.commit_sha, protected: protecting, protection_config } });
122
+ if (!protecting)
123
+ return send(ctx, { status: 204, body: undefined });
124
+ return ctx.reply(toGithubBranchProtection(state().branches.find((x) => x.repository === repo && x.name === name)));
125
+ }
126
+ /** Toggle admin enforcement on a protected branch (404 when not protected). */
127
+ async function enforceAdmins(ctx, enabled) {
128
+ const { repo } = gh(ctx);
129
+ const { name, b } = branchOf(ctx);
130
+ if (!b?.protected)
131
+ return send(ctx, githubNotFound());
132
+ await protect(ctx, name, b, { ...(b.protection_config ?? {}), enforce_admins: enabled });
133
+ if (!enabled)
134
+ return send(ctx, { status: 204, body: undefined });
135
+ return ctx.reply({ url: `${protectionUrl(repo, name)}/enforce_admins`, enabled });
136
+ }
@@ -0,0 +1,2 @@
1
+ import type { Semantics } from '@volter/world-core';
2
+ export declare function checksSemantics(): Record<string, Semantics>;
@@ -0,0 +1,176 @@
1
+ import { combineStatuses, githubNotFound, paginate, planeCommitBody, toGithubAnnotation, toGithubCheckRun, toGithubCheckSuite, toGithubStatus, } from "../github-twin.js";
2
+ import { gh, nextSeq, put, send } from "./shared.js";
3
+ const refOf = (ctx) => decodeURIComponent(gh(ctx).seg[4]);
4
+ /** The app a check-run write comes from, or GitHub's refusal when it is not an installed app allowed checks:write on
5
+ * this repository. */
6
+ function appWriting(ctx) {
7
+ const { token, repo } = gh(ctx);
8
+ const docs = 'https://docs.github.com/rest/checks/runs#create-a-check-run';
9
+ const grant = token ? ctx.tree().find((r) => r.type === 'app_token' && r.token === token && r.deleted !== true) : undefined;
10
+ if (!grant)
11
+ return { refused: ctx.reply({ message: 'You must authenticate via a GitHub App.', documentation_url: docs }, 403) };
12
+ const names = grant.repositories ?? [];
13
+ const covers = grant.repository_selection !== 'selected' ? repo.split('/')[0] === grant.account_login : names.some((n) => n === repo || `${String(grant.account_login)}/${n}` === repo);
14
+ if (grant.permissions?.checks !== 'write' || !covers)
15
+ return { refused: ctx.reply({ message: 'Resource not accessible by integration', documentation_url: docs }, 403) };
16
+ return { app: String(grant.app_slug) };
17
+ }
18
+ /** The commit a suite checks, as GitHub's simple-commit, read from the repository's git plane when it holds it. */
19
+ async function headCommit(repo, sha, root) {
20
+ const c = await planeCommitBody(repo, sha, root);
21
+ if (!c?.commit)
22
+ return {};
23
+ const who = (p) => ({ name: p?.name ?? '', email: p?.email ?? '' });
24
+ return { head_commit: { id: sha, tree_id: c.commit.tree?.sha ?? '', message: c.commit.message ?? '', timestamp: c.commit.author?.date ?? '', author: who(c.commit.author), committer: who(c.commit.committer) } };
25
+ }
26
+ export function checksSemantics() {
27
+ const reposGetCombinedStatusForRef = async (ctx) => {
28
+ const { repo, state } = gh(ctx);
29
+ return ctx.reply(combineStatuses(repo, refOf(ctx), state().statuses));
30
+ };
31
+ const reposListCommitStatusesForRef = async (ctx) => {
32
+ const { repo, query, state } = gh(ctx);
33
+ const ref = refOf(ctx);
34
+ return ctx.reply(paginate(state().statuses.filter((s) => s.repository === repo && s.sha === ref).sort((a, b) => b.id - a.id), query).map(toGithubStatus));
35
+ };
36
+ const reposCreateCommitStatus = async (ctx) => {
37
+ const { repo, body, occurredAt } = gh(ctx);
38
+ const sha = refOf(ctx);
39
+ if (!body.state)
40
+ return ctx.reply({ message: 'Validation Failed', errors: [{ resource: 'Status', field: 'state', code: 'missing_field' }] }, 422);
41
+ if (!['error', 'failure', 'pending', 'success'].includes(body.state))
42
+ return ctx.reply({ message: 'Validation Failed', errors: [{ resource: 'Status', field: 'state', code: 'invalid' }] }, 422);
43
+ const seq = nextSeq(ctx, 'status');
44
+ const fields = { repository: repo, sha, state: body.state, context: body.context ?? 'default', description: body.description, target_url: body.target_url, created_at: occurredAt, updated_at: occurredAt };
45
+ await put(ctx, { operation: 'commit_status.create', subjectType: 'commit_status', subjectId: `status:${seq}`, fields });
46
+ return ctx.reply(toGithubStatus({ id: seq, ...fields }), 201);
47
+ };
48
+ const checksListSuitesForRef = async (ctx) => {
49
+ const { repo, state, root } = gh(ctx);
50
+ const ref = refOf(ctx);
51
+ const st = state();
52
+ const apps = [...new Set(st.checkRuns.filter((c) => c.repository === repo && c.head_sha === ref).map((c) => c.app_slug))];
53
+ const head = await headCommit(repo, ref, root);
54
+ const suites = apps.map((app) => ({ ...toGithubCheckSuite(repo, ref, st.checkRuns, app, st), ...head }));
55
+ return ctx.reply({ total_count: suites.length, check_suites: suites });
56
+ };
57
+ const checksGet = async (ctx) => {
58
+ const { repo, state } = gh(ctx);
59
+ const cr = state().checkRuns.find((c) => c.repository === repo && c.id === Number(refOf(ctx)));
60
+ return send(ctx, cr ? { status: 200, body: toGithubCheckRun(cr, state()) } : githubNotFound());
61
+ };
62
+ const checksListAnnotations = async (ctx) => {
63
+ const { repo, query, state } = gh(ctx);
64
+ const cr = state().checkRuns.find((c) => c.repository === repo && c.id === Number(refOf(ctx)));
65
+ if (!cr)
66
+ return send(ctx, githubNotFound());
67
+ return ctx.reply(paginate(cr.annotations ?? [], query).map((an) => toGithubAnnotation(cr, an)));
68
+ };
69
+ const checksGetSuite = async (ctx) => {
70
+ const { repo, state, root } = gh(ctx);
71
+ const st = state();
72
+ const id = Number(refOf(ctx));
73
+ const cr = st.checkRuns.find((c) => c.repository === repo && (c.check_suite_id ?? 0) === id);
74
+ return send(ctx, cr ? { status: 200, body: { ...toGithubCheckSuite(repo, cr.head_sha, st.checkRuns, cr.app_slug, st), ...(await headCommit(repo, cr.head_sha, root)) } } : githubNotFound());
75
+ };
76
+ const checksListForSuite = async (ctx) => {
77
+ const { repo, query, state } = gh(ctx);
78
+ const st = state();
79
+ const id = Number(refOf(ctx));
80
+ if (!st.checkRuns.find((c) => c.repository === repo && (c.check_suite_id ?? 0) === id))
81
+ return send(ctx, githubNotFound());
82
+ const items = st.checkRuns.filter((c) => c.repository === repo && (c.check_suite_id ?? 0) === id).sort((a, b) => a.id - b.id);
83
+ return ctx.reply({ total_count: items.length, check_runs: paginate(items, query).map((c) => toGithubCheckRun(c, state())) });
84
+ };
85
+ const checksUpdate = async (ctx) => {
86
+ const writer = appWriting(ctx);
87
+ if ('refused' in writer)
88
+ return writer.refused;
89
+ const { repo, body, occurredAt, state } = gh(ctx);
90
+ const id = Number(refOf(ctx));
91
+ const existing = state().checkRuns.find((c) => c.repository === repo && c.id === id);
92
+ if (!existing)
93
+ return send(ctx, githubNotFound());
94
+ // an app updates only the check runs it created
95
+ if (existing.app_slug && existing.app_slug !== writer.app)
96
+ return ctx.reply({ message: 'Resource not accessible by integration', documentation_url: 'https://docs.github.com/rest/checks/runs#update-a-check-run' }, 403);
97
+ const fields = { repository: repo, head_sha: existing.head_sha, name: existing.name, app_slug: existing.app_slug ?? writer.app };
98
+ if (body.status !== undefined)
99
+ fields.status = String(body.status);
100
+ if (body.conclusion !== undefined) {
101
+ fields.conclusion = String(body.conclusion);
102
+ fields.status = body.status ?? 'completed';
103
+ fields.completed_at = body.completed_at ?? occurredAt;
104
+ }
105
+ fields.started_at = existing.started_at;
106
+ const output = body.output;
107
+ if (output) {
108
+ if (output.title !== undefined)
109
+ fields.output_title = String(output.title);
110
+ if (output.summary !== undefined)
111
+ fields.output_summary = String(output.summary);
112
+ if (output.text !== undefined)
113
+ fields.output_text = String(output.text);
114
+ const anns = Array.isArray(output.annotations) ? output.annotations : (existing.annotations ?? []);
115
+ fields.annotations = anns.map((an) => ({
116
+ path: String(an.path), start_line: Number(an.start_line), end_line: Number(an.end_line),
117
+ annotation_level: String(an.annotation_level), message: String(an.message),
118
+ title: an.title, raw_details: an.raw_details, start_column: an.start_column, end_column: an.end_column,
119
+ }));
120
+ }
121
+ else if (existing.annotations) {
122
+ fields.annotations = existing.annotations;
123
+ }
124
+ await put(ctx, { operation: 'check_run.update', subjectType: 'check_run', subjectId: `check:${id}`, fields });
125
+ return ctx.reply(toGithubCheckRun(state().checkRuns.find((c) => c.id === id), state()));
126
+ };
127
+ const checksRerequestSuite = async (ctx) => {
128
+ const { repo, state } = gh(ctx);
129
+ const id = Number(refOf(ctx));
130
+ if (!state().checkRuns.find((c) => c.repository === repo && (c.check_suite_id ?? 0) === id))
131
+ return send(ctx, githubNotFound());
132
+ return ctx.reply({}, 201);
133
+ };
134
+ return {
135
+ 'repos/get-combined-status-for-ref': reposGetCombinedStatusForRef,
136
+ // newest first
137
+ 'repos/list-commit-statuses-for-ref': reposListCommitStatusesForRef,
138
+ // 422 without a state or with one outside the enum
139
+ 'repos/create-commit-status': reposCreateCommitStatus,
140
+ // {total_count, check_runs}, paginated over check_runs
141
+ 'checks/list-for-ref': async (ctx) => {
142
+ const { repo, query, state } = gh(ctx);
143
+ const ref = refOf(ctx);
144
+ const items = state().checkRuns.filter((c) => c.repository === repo && c.head_sha === ref).sort((a, b) => a.id - b.id);
145
+ return ctx.reply({ total_count: items.length, check_runs: paginate(items, query).map((c) => toGithubCheckRun(c, state())) });
146
+ },
147
+ // one suite per app that ran checks on the commit
148
+ 'checks/list-suites-for-ref': checksListSuitesForRef,
149
+ 'checks/get': checksGet,
150
+ 'checks/list-annotations': checksListAnnotations,
151
+ 'checks/get-suite': checksGetSuite,
152
+ 'checks/list-for-suite': checksListForSuite,
153
+ // queued unless it carries a conclusion (then completed)
154
+ 'checks/create': async (ctx) => {
155
+ const writer = appWriting(ctx);
156
+ if ('refused' in writer)
157
+ return writer.refused;
158
+ const { repo, body, occurredAt, state } = gh(ctx);
159
+ const missing = ['name', 'head_sha'].filter((k) => !body[k]);
160
+ if (missing.length)
161
+ return ctx.reply({ message: 'Validation Failed', errors: missing.map((field) => ({ resource: 'CheckRun', field, code: 'missing_field' })) }, 422);
162
+ const seq = nextSeq(ctx, 'check');
163
+ const status = body.status ?? (body.conclusion ? 'completed' : 'queued');
164
+ const completed_at = body.conclusion ? (body.completed_at ?? occurredAt) : undefined;
165
+ const fields = { repository: repo, head_sha: body.head_sha, name: body.name, status, conclusion: body.conclusion, details_url: body.details_url, started_at: body.started_at ?? occurredAt, app_slug: writer.app, ...(body.external_id !== undefined ? { external_id: String(body.external_id) } : {}) };
166
+ if (completed_at !== undefined)
167
+ fields.completed_at = completed_at;
168
+ await put(ctx, { operation: 'check_run.create', subjectType: 'check_run', subjectId: `check:${seq}`, fields });
169
+ return ctx.reply(toGithubCheckRun(state().checkRuns.find((c) => c.id === seq), state()), 201);
170
+ },
171
+ // status, conclusion (which completes the run), and output with its annotations
172
+ 'checks/update': checksUpdate,
173
+ // 201; nothing reruns
174
+ 'checks/rerequest-suite': checksRerequestSuite,
175
+ };
176
+ }
@@ -0,0 +1,2 @@
1
+ import type { Semantics } from '@volter/world-core';
2
+ export declare const codeScanningUpload: Record<string, Semantics>;