Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

1198 lines · 72.6 KBRaw History
1/**
2 * Hand-written notes for the generated REST API reference (/docs/api and
3 * /docs/api/openapi.json). The list of endpoints itself comes from the live
4 * router, so an endpoint can't be missing from the docs: one without an entry
5 * here still appears, with a summary derived from its method and path. A unit
6 * test fails when an entry here no longer matches a route.
7 *
8 * Keys are "METHOD /path" with OpenAPI-style {params}, relative to /api/v3.
9 */
10
11/** Who may call an endpoint: a repository role, or an account-level requirement. */
12export type Access = "public" | "account" | "read" | "triage" | "write" | "maintain" | "admin" | "org-admin" | "site-admin";
13
14/** [type, description, required?] */
15export type Field = [string, string, boolean?];
16
17export interface OpDoc {
18 summary: string;
19 description?: string;
20 access?: Access;
21 query?: Record<string, Field>;
22 body?: Record<string, Field>;
23 /** Success status, when it isn't 200. */
24 status?: number;
25}
26
27export type Scope = "read" | "write" | "admin";
28
29/**
30 * Endpoints whose token requirement differs from the default (GET needs a
31 * read token, anything else a write token). These are account-level changes,
32 * so a token limited to repositories is refused too, except where noted.
33 */
34export const ACCOUNT_SCOPES: Record<string, { scope: Scope; anyToken?: boolean }> = {
35 "POST /user/orgs": { scope: "admin" },
36 "POST /orgs/{org}/invitations": { scope: "admin" },
37 "POST /invitations": { scope: "admin" },
38 "PATCH /orgs/{org}": { scope: "admin" },
39 "GET /orgs/{org}/invitations": { scope: "read" },
40 "DELETE /orgs/{org}/invitations/{id}": { scope: "admin" },
41 "GET /invitations": { scope: "read" },
42 "DELETE /invitations/{id}": { scope: "admin" },
43 "POST /user/repos": { scope: "write" },
44 "POST /orgs/{org}/repos": { scope: "write" },
45 "GET /orgs/{org}/actions/secrets": { scope: "admin" },
46 "PUT /orgs/{org}/actions/secrets/{name}": { scope: "admin" },
47 "DELETE /orgs/{org}/actions/secrets/{name}": { scope: "admin" },
48 "POST /diffinery/tokens": { scope: "admin", anyToken: true },
49 "DELETE /diffinery/tokens/{id}": { scope: "admin" },
50 "POST /diffinery/agents": { scope: "admin" },
51 "POST /diffinery/agents/{login}/tokens": { scope: "admin" },
52 "DELETE /diffinery/agents/{login}": { scope: "admin" },
53 "PUT /notifications": { scope: "write" },
54 "PUT /diffinery/notification-settings": { scope: "write" },
55 "POST /diffinery/emails/test": { scope: "write" },
56 "POST /diffinery/admin/emails/samples": { scope: "admin" },
57 "PUT /user/avatar": { scope: "write" },
58 "DELETE /user/avatar": { scope: "write" },
59 "PUT /orgs/{org}/avatar": { scope: "write" },
60 "DELETE /orgs/{org}/avatar": { scope: "write" },
61};
62
63const page: Record<string, Field> = {
64 per_page: ["integer", "Results per page (default 30, at most 100)."],
65 page: ["integer", "Page number, starting at 1."],
66};
67
68export const API_DOCS: Record<string, OpDoc> = {
69 // ------------------------------------------------------------ meta, users and organizations
70 "GET /": { summary: "API root", description: "Names the API and links to these docs and the MCP endpoint.", access: "public" },
71 "GET /meta": { summary: "Instance metadata", description: "Whether this instance issues OIDC tokens to workflows.", access: "public" },
72 "GET /rate_limit": {
73 summary: "Get rate limit status",
74 description: "For GitHub clients that check their budget before calling. Diffinery doesn't meter API calls, so this always reports a full, constant budget (15000 core requests an hour).",
75 access: "public",
76 },
77 "GET /user": { summary: "Get the authenticated account", description: "Returns your account, including `email` and `site_admin`. Agents get their own account (type `Bot`).", access: "account" },
78 "PUT /user/avatar": {
79 summary: "Set your profile picture",
80 description:
81 "Diffinery only. The raw request body is the image (JPEG, PNG or WebP, at most 5 MB), not JSON. Returns the new `avatar_url`. A token needs `write` or `admin` scope and must not be limited to repositories. Audited.",
82 access: "account",
83 },
84 "DELETE /user/avatar": { summary: "Remove your profile picture", description: "Diffinery only. Goes back to generated initials and returns the `avatar_url`. Same token rules as setting one. Audited.", access: "account" },
85 "GET /users/{login}": { summary: "Get an account", description: "A user, organization or agent by login.", access: "account" },
86 "GET /user/orgs": { summary: "List your organizations", description: "Each organization carries your `role` in it (member, admin or owner).", access: "account" },
87 "POST /user/orgs": {
88 summary: "Create an organization",
89 access: "account",
90 status: 201,
91 body: { login: ["string", "Organization login (letters, digits and dashes).", true], name: ["string", "Display name (default: the login)."] },
92 },
93 "GET /orgs/{org}": {
94 summary: "Get an organization",
95 description: "Members, the organization's agents, agents of members, and site admins also see `default_repository_permission`: the base permission every member gets on its repositories.",
96 access: "account",
97 },
98 "PATCH /orgs/{org}": {
99 summary: "Update an organization",
100 description: "Owners only (or site admins). Every field is checked before anything is saved, so a bad value changes nothing. Audited.",
101 access: "org-admin",
102 body: {
103 name: ["string", "Display name; blank goes back to the login."],
104 default_repository_permission: ["string", "Base permission members get on every repository: `none`, `read`, `write` or `admin`. Owners and admins always get admin, and per-repository grants add to it."],
105 },
106 },
107 "GET /orgs/{org}/members": { summary: "List organization members", description: "Members only (or site admins). Each entry carries the member's `role`.", access: "account" },
108 "POST /orgs/{org}/invitations": {
109 summary: "Invite someone to an organization",
110 description: "Emails an invite link. The response holds the link too, so you can share it yourself.",
111 access: "org-admin",
112 status: 201,
113 body: { email: ["string", "Email address to invite.", true], role: ["string", "`member` (default), `admin` or `owner`."] },
114 },
115 "GET /orgs/{org}/invitations": { summary: "List an organization's pending invitations", description: "Owners and admins (or site admins). Each has an `id` to revoke it with.", access: "org-admin" },
116 "DELETE /orgs/{org}/invitations/{id}": {
117 summary: "Revoke an organization invitation",
118 description: "The link stops working and the address can be invited again. 404 when no pending invitation has this id. Audited.",
119 access: "org-admin",
120 status: 204,
121 },
122 "PUT /orgs/{org}/avatar": {
123 summary: "Set an organization's picture",
124 description:
125 "Diffinery only. The raw request body is the image (JPEG, PNG or WebP, at most 5 MB), not JSON. Returns the new `avatar_url`. A token needs `write` or `admin` scope and must not be limited to repositories. Audited.",
126 access: "org-admin",
127 },
128 "DELETE /orgs/{org}/avatar": { summary: "Remove an organization's picture", description: "Diffinery only. Goes back to generated initials and returns the `avatar_url`. Audited.", access: "org-admin" },
129 "POST /invitations": {
130 summary: "Invite someone to this instance",
131 description: "A site-wide invite with no organization. Site admins only.",
132 access: "site-admin",
133 status: 201,
134 body: { email: ["string", "Email address to invite.", true] },
135 },
136 "GET /invitations": { summary: "List pending invitations to this instance", description: "Site-wide invites only, not those to an organization or repository. Site admins only.", access: "site-admin" },
137 "DELETE /invitations/{id}": { summary: "Revoke an invitation to this instance", description: "Site admins only. 404 when no pending site-wide invitation has this id. Audited.", access: "site-admin", status: 204 },
138
139 // ------------------------------------------------------------ search
140 "GET /search/issues": {
141 summary: "Search issues and pull requests",
142 description:
143 "Searches the repositories you can read (only public ones without a token), at most 50 per query; `incomplete_results` is true when more were left out. Qualifiers: `repo:`, `user:`, `org:`, `owner:`, `is:`, `type:`, `state:`, `draft:`, `label:`, `no:`, `author:`, `assignee:`, `head:`, `base:`, `in:`, `created:`, `updated:`, `closed:` and `sort:`; others are searched as text. Only the first 1000 results are available, and every `score` is 1.",
144 access: "public",
145 query: {
146 q: ["string", "Search query, at most 256 characters.", true],
147 sort: ["string", "`created` (default), `updated` or `comments`."],
148 order: ["string", "`desc` (default) or `asc`."],
149 ...page,
150 },
151 },
152 "GET /search/repositories": {
153 summary: "Search repositories",
154 description:
155 "Matches names, descriptions and topics of the repositories you can see (only public ones without a token). Qualifiers: `user:`, `org:`, `owner:`, `repo:`, `topic:`, `is:public`, `is:private`, `archived:` and `in:name,description,topics`. `fork:`, `language:`, `stars:`, `forks:`, `size:`, `pushed:` and `created:` are accepted but don't filter anything. Every `score` is 1.",
156 access: "public",
157 query: {
158 q: ["string", "Search query, at most 256 characters.", true],
159 sort: ["string", "`updated` sorts by last push; anything else keeps the default order."],
160 order: ["string", "With `sort=updated`: `desc` (default) or `asc`."],
161 ...page,
162 },
163 },
164
165 // ------------------------------------------------------------ repositories
166 "GET /user/repos": { summary: "List repositories you can access", access: "account" },
167 "POST /user/repos": {
168 summary: "Create a repository for yourself",
169 description: "An agent's repositories belong to its owner.",
170 access: "account",
171 status: 201,
172 body: {
173 name: ["string", "Repository name.", true],
174 description: ["string", "Short description."],
175 visibility: ["string", "`private` (default) or `public`."],
176 default_branch: ["string", "Default branch name (default `main`)."],
177 },
178 },
179 "GET /users/{login}/repos": { summary: "List an account's repositories", description: "Only the ones you can see.", access: "account" },
180 "GET /orgs/{org}/repos": { summary: "List an organization's repositories", description: "Only the ones you can see.", access: "account" },
181 "POST /orgs/{org}/repos": {
182 summary: "Create an organization repository",
183 access: "account",
184 status: 201,
185 body: {
186 name: ["string", "Repository name.", true],
187 description: ["string", "Short description."],
188 visibility: ["string", "`private` (default) or `public`."],
189 default_branch: ["string", "Default branch name (default `main`)."],
190 },
191 },
192 "GET /repos/{owner}/{repo}": { summary: "Get a repository", description: "Includes your role (`permissions.role`) and repository statistics.", access: "read" },
193 "PATCH /repos/{owner}/{repo}": {
194 summary: "Update a repository",
195 access: "admin",
196 body: {
197 name: ["string", "New name."],
198 description: ["string", "New description."],
199 visibility: ["string", "`private` or `public`."],
200 default_branch: ["string", "New default branch (must exist)."],
201 archived: ["boolean", "Archive (read-only) or unarchive."],
202 },
203 },
204 "DELETE /repos/{owner}/{repo}": { summary: "Delete a repository", description: "People only; agents can't delete repositories. Audited.", access: "admin", status: 204 },
205 "GET /repos/{owner}/{repo}/collaborators": { summary: "List collaborators", description: "Each entry carries `role_name`.", access: "read" },
206 "PUT /repos/{owner}/{repo}/collaborators/{login}": {
207 summary: "Add or change a collaborator",
208 access: "admin",
209 status: 204,
210 body: { permission: ["string", "`read`, `triage`, `write` (default), `maintain` or `admin`; GitHub's `pull` and `push` work too."] },
211 },
212 "DELETE /repos/{owner}/{repo}/collaborators/{login}": { summary: "Remove a collaborator", access: "admin", status: 204 },
213 "GET /repos/{owner}/{repo}/invitations": { summary: "List pending repository invitations", description: "Invitations sent by email to people without an account. Each has an `id` to revoke it with.", access: "admin" },
214 "DELETE /repos/{owner}/{repo}/invitations/{id}": {
215 summary: "Revoke a repository invitation",
216 description: "The link stops working and the address can be invited again. 404 when no pending invitation to this repository has this id. Audited.",
217 access: "admin",
218 status: 204,
219 },
220 "GET /repos/{owner}/{repo}/assignees": { summary: "List assignable people", description: "Collaborators with triage access or more, plus the owner when the owner is a user.", access: "read", query: { ...page } },
221 "GET /repos/{owner}/{repo}/assignees/{login}": {
222 summary: "Check if someone can be assigned",
223 description: "204 when `login` is the repository's owner or a collaborator with triage access or more, 404 otherwise.",
224 access: "read",
225 status: 204,
226 },
227 "GET /repos/{owner}/{repo}/languages": {
228 summary: "List languages",
229 description: "Bytes of code per language on the default branch, largest first. Languages come from file extensions and a few well-known file names; vendored directories like `node_modules` and `vendor` are skipped.",
230 access: "read",
231 },
232 "GET /repos/{owner}/{repo}/topics": { summary: "Get topics", description: "Returns `{ names }`.", access: "read" },
233 "PUT /repos/{owner}/{repo}/topics": {
234 summary: "Replace topics",
235 description: "Names are lower-cased and deduplicated. Each starts with a letter or digit and has at most 50 letters, digits and dashes; at most 20 topics. Audited.",
236 access: "admin",
237 body: { names: ["string[]", "Every topic the repository should have (`[]` clears them).", true] },
238 },
239 "POST /repos/{owner}/{repo}/dispatches": {
240 summary: "Trigger a repository_dispatch event",
241 description: "Starts workflows on the default branch that run `on: repository_dispatch` with this type, and sends the `repository_dispatch` webhook. Fails with 422 on an empty repository.",
242 access: "write",
243 status: 204,
244 body: {
245 event_type: ["string", "Event type, at most 100 characters.", true],
246 client_payload: ["object", "Data for workflows as `github.event.client_payload`: at most 10 top-level keys and 64 KB."],
247 },
248 },
249
250 // ------------------------------------------------------------ branches, tags, refs and commits
251 "GET /repos/{owner}/{repo}/branches": { summary: "List branches", description: "Each branch says whether it is `protected` and the `default`.", access: "read" },
252 "GET /repos/{owner}/{repo}/branches/{branch}": { summary: "Get a branch", access: "read" },
253 "GET /repos/{owner}/{repo}/tags": { summary: "List tags", access: "read" },
254 "POST /repos/{owner}/{repo}/merges": {
255 summary: "Merge a branch",
256 description:
257 "Merges `head` into the `base` branch with a merge commit. Returns 201 with the commit, 204 when `base` already contains `head`, 409 on a conflict and 404 when either doesn't exist. Branch protection and agent rules apply as they do to a push.",
258 access: "write",
259 status: 201,
260 body: {
261 base: ["string", "Branch to merge into.", true],
262 head: ["string", "Branch, tag or commit to merge.", true],
263 commit_message: ["string", "Commit message (default: `Merge {head} into {base}`)."],
264 },
265 },
266 "GET /repos/{owner}/{repo}/git/refs/{path}": { summary: "List references", description: "Every ref under `refs/{path}`, for example `heads` or `tags/v1`.", access: "read" },
267 "POST /repos/{owner}/{repo}/git/refs": {
268 summary: "Create a reference",
269 description: "Branch protection and agent rules apply as they do to a push.",
270 access: "write",
271 status: 201,
272 body: { ref: ["string", "Full ref name, like `refs/heads/feature`.", true], sha: ["string", "Commit to point at.", true] },
273 },
274 "PATCH /repos/{owner}/{repo}/git/refs/{path}": {
275 summary: "Update a reference",
276 access: "write",
277 body: { sha: ["string", "New target commit.", true], force: ["boolean", "Allow a non-fast-forward update."] },
278 },
279 "DELETE /repos/{owner}/{repo}/git/refs/{path}": { summary: "Delete a reference", access: "write", status: 204 },
280 "GET /repos/{owner}/{repo}/commits": {
281 summary: "List commits",
282 access: "read",
283 query: { sha: ["string", "Branch, tag or commit to start from (default: the default branch)."], path: ["string", "Only commits that touch this path."], ...page },
284 },
285 "GET /repos/{owner}/{repo}/commits/{ref}": { summary: "Get a commit", description: "The commit with its diff.", access: "read" },
286 "GET /repos/{owner}/{repo}/commits/{sha}/pulls": {
287 summary: "List pull requests for a commit",
288 description: "Pull requests whose head or merge commit is this commit, plus open pull requests (the 50 newest) that contain it. 422 when the commit doesn't exist.",
289 access: "read",
290 query: { ...page },
291 },
292 "GET /repos/{owner}/{repo}/compare/{spec}": { summary: "Compare two refs", description: "`spec` is `base...head`.", access: "read" },
293 "GET /repos/{owner}/{repo}/raw/{ref}/{path}": { summary: "Download a raw file", description: "The file's bytes, served as text or `application/octet-stream`.", access: "read" },
294
295 // ------------------------------------------------------------ contents
296 "GET /repos/{owner}/{repo}/contents": { summary: "List the root directory", access: "read", query: { ref: ["string", "Branch, tag or commit (default: the default branch)."] } },
297 "GET /repos/{owner}/{repo}/contents/{path}": {
298 summary: "Get a file or directory",
299 description: "A directory returns an array of entries; a file returns its content base64-encoded.",
300 access: "read",
301 query: { ref: ["string", "Branch, tag or commit (default: the default branch)."] },
302 },
303 "PUT /repos/{owner}/{repo}/contents/{path}": {
304 summary: "Create or update a file",
305 description: "Commits one file.",
306 access: "write",
307 status: 201,
308 body: { message: ["string", "Commit message.", true], content: ["string", "New content, base64-encoded.", true], branch: ["string", "Branch to commit to (default: the default branch)."] },
309 },
310 "DELETE /repos/{owner}/{repo}/contents/{path}": {
311 summary: "Delete a file",
312 access: "write",
313 body: { message: ["string", "Commit message.", true], branch: ["string", "Branch to commit to (default: the default branch)."] },
314 },
315 "POST /repos/{owner}/{repo}/diffinery/commits": {
316 summary: "Commit several files at once",
317 description: "Diffinery only. Writes and deletes any number of files in one commit, without a git client. Pass `expected_head_sha` to fail instead of racing another writer.",
318 access: "write",
319 status: 201,
320 body: {
321 branch: ["string", "Branch to commit to.", true],
322 message: ["string", "Commit message.", true],
323 files: ["array", "Changes: `{ path, content, encoding: \"utf-8\" | \"base64\", mode: \"100644\" | \"100755\" }` or `{ path, delete: true }`.", true],
324 base: ["string", "Create `branch` from this ref when it doesn't exist."],
325 expected_head_sha: ["string", "Fail with 409 if the branch has moved."],
326 },
327 },
328 "GET /repos/{owner}/{repo}/readme": {
329 summary: "Get the README",
330 description: "The README in the root directory, base64-encoded like a file from the contents API. A Markdown README wins when there are several. Send `Accept: application/vnd.github.raw` for the raw bytes.",
331 access: "read",
332 query: { ref: ["string", "Branch, tag or commit (default: the default branch)."] },
333 },
334 "GET /repos/{owner}/{repo}/readme/{dir}": {
335 summary: "Get a directory's README",
336 description: "Like the root README, for the directory `dir`.",
337 access: "read",
338 query: { ref: ["string", "Branch, tag or commit (default: the default branch)."] },
339 },
340 ...archiveDocs("tarball", "a tarball", "`.tar.gz`"),
341 ...archiveDocs("zipball", "a zip archive", "`.zip` (at most 65535 files)"),
342
343 // ------------------------------------------------------------ releases
344 "GET /repos/{owner}/{repo}/releases": { summary: "List releases", description: "Newest first. Drafts are included only for people with write access.", access: "read", query: { ...page } },
345 "POST /repos/{owner}/{repo}/releases": {
346 summary: "Create a release",
347 description:
348 "A published release (not a draft) creates its tag at `target_commitish` when the tag doesn't exist, sends `release` webhooks and starts `on: release` workflows; a draft only sends the `created` webhook. 422 when the tag already has a published release. Upload assets to the release's `upload_url`. `discussion_category_name` is ignored. See [Releases](/docs/collaboration/releases).",
349 access: "write",
350 status: 201,
351 body: {
352 tag_name: ["string", "Tag to release.", true],
353 target_commitish: ["string", "Branch, tag or commit for a new tag (default: the default branch)."],
354 name: ["string", "Release title."],
355 body: ["string", "Markdown notes, at most 125000 characters."],
356 draft: ["boolean", "Save as a draft."],
357 prerelease: ["boolean", "Mark as a prerelease."],
358 make_latest: ["string", "`true` (default), `false`, or `legacy` (the newest by date)."],
359 generate_release_notes: ["boolean", "Append generated notes to `body`."],
360 },
361 },
362 "GET /repos/{owner}/{repo}/releases/{id}": { summary: "Get a release", description: "Drafts are 404 for people without write access.", access: "read" },
363 "PATCH /repos/{owner}/{repo}/releases/{id}": {
364 summary: "Update a release",
365 description: "Send only what changes. Setting `draft` to false publishes the release (creating its tag if needed); setting it back to true unpublishes it. Sends `release` webhooks.",
366 access: "write",
367 body: {
368 tag_name: ["string", "New tag."],
369 target_commitish: ["string", "Branch, tag or commit for a new tag."],
370 name: ["string", "Release title."],
371 body: ["string", "Markdown notes, at most 125000 characters."],
372 draft: ["boolean", "Draft or published."],
373 prerelease: ["boolean", "Mark as a prerelease."],
374 make_latest: ["string", "`true`, `false` or `legacy`."],
375 },
376 },
377 "DELETE /repos/{owner}/{repo}/releases/{id}": { summary: "Delete a release", description: "Deletes its assets too; the tag stays. Sends the `release` webhook.", access: "write", status: 204 },
378 "GET /repos/{owner}/{repo}/releases/{id}/assets": { summary: "List release assets", access: "read", query: { ...page } },
379 "GET /repos/{owner}/{repo}/releases/assets/{id}": {
380 summary: "Get a release asset",
381 description: "The asset's metadata, or the file itself with `Accept: application/octet-stream`.",
382 access: "read",
383 },
384 "PATCH /repos/{owner}/{repo}/releases/assets/{id}": {
385 summary: "Update a release asset",
386 description: "`state` is accepted and ignored.",
387 access: "write",
388 body: { name: ["string", "New file name (unique within the release)."], label: ["string", "Display label."] },
389 },
390 "DELETE /repos/{owner}/{repo}/releases/assets/{id}": { summary: "Delete a release asset", access: "write", status: 204 },
391 "POST /repos/{owner}/{repo}/releases/generate-notes": {
392 summary: "Generate release notes",
393 description: "Builds notes from the pull requests merged since the previous release, with categories from `.github/release.yml` (or `.yaml`). Nothing is saved; returns `{ name, body }`.",
394 access: "write",
395 body: {
396 tag_name: ["string", "Tag for the release.", true],
397 target_commitish: ["string", "Used when the tag doesn't exist yet (default: the default branch)."],
398 previous_tag_name: ["string", "Start from this tag (default: the newest earlier published release)."],
399 configuration_file_path: ["string", "Config file to use instead of `.github/release.yml`."],
400 },
401 },
402 "GET /repos/{owner}/{repo}/releases/latest": {
403 summary: "Get the latest release",
404 description: "The release marked latest, or else the newest published release that isn't a prerelease. 404 when there is none.",
405 access: "read",
406 },
407 "GET /repos/{owner}/{repo}/releases/tags/{tag}": { summary: "Get a release by tag", description: "Published releases only; drafts aren't found by tag.", access: "read" },
408 ...reactionDocs("/repos/{owner}/{repo}/releases/{id}", "a release", "Drafts are hidden from people without write access, and so are their reactions."),
409
410 // ------------------------------------------------------------ git database
411 "POST /repos/{owner}/{repo}/git/blobs": {
412 summary: "Create a blob",
413 description: "Push protection applies: content that looks like a credential is refused with 422. Returns `{ url, sha }`.",
414 access: "write",
415 status: 201,
416 body: { content: ["string", "The blob's content.", true], encoding: ["string", "`utf-8` (default) or `base64`."] },
417 },
418 "GET /repos/{owner}/{repo}/git/blobs/{sha}": { summary: "Get a blob", description: "Content is base64-encoded; send `Accept: application/vnd.github.raw` for the raw bytes.", access: "read" },
419 "POST /repos/{owner}/{repo}/git/trees": {
420 summary: "Create a tree",
421 description:
422 "Applies the entries on top of `base_tree`. An entry with `content` creates a blob (push protection applies); one with `sha: null` deletes the path. `type` is ignored: the mode decides. At most 10000 entries per request.",
423 access: "write",
424 status: 201,
425 body: {
426 tree: ["array", "Entries: `{ path, mode, sha }` or `{ path, mode, content }`. `mode` is `100644` (default), `100755`, `040000`, `160000` or `120000`.", true],
427 base_tree: ["string", "Tree to start from (default: an empty tree)."],
428 },
429 },
430 "GET /repos/{owner}/{repo}/git/trees/{sha}": {
431 summary: "Get a tree",
432 description: "`sha` may be a tree, a commit or a ref. `truncated` is true when a recursive listing stopped at 100000 entries.",
433 access: "read",
434 query: { recursive: ["string", "Any value but `0` or `false` lists every entry below, not just the top level."] },
435 },
436 "POST /repos/{owner}/{repo}/git/commits": {
437 summary: "Create a Git commit",
438 description: "Writes a commit object; no branch moves until you update a reference. `author` and `committer` default to you. Commits made by agents carry a `diffinery-agent` header.",
439 access: "write",
440 status: 201,
441 body: {
442 message: ["string", "Commit message.", true],
443 tree: ["string", "SHA of the commit's tree.", true],
444 parents: ["string[]", "Parent commit SHAs (none for a root commit)."],
445 author: ["object", "`{ name, email, date }`."],
446 committer: ["object", "`{ name, email, date }` (default: the author)."],
447 signature: ["string", "Signature to store in the commit as given."],
448 },
449 },
450 "GET /repos/{owner}/{repo}/git/commits/{sha}": { summary: "Get a Git commit", access: "read" },
451 "POST /repos/{owner}/{repo}/git/tags": {
452 summary: "Create a tag object",
453 description: "Writes an annotated tag object only; create its `refs/tags/...` reference with `POST /repos/{owner}/{repo}/git/refs` to publish it.",
454 access: "write",
455 status: 201,
456 body: {
457 tag: ["string", "Tag name.", true],
458 object: ["string", "SHA of the object to tag.", true],
459 message: ["string", "Tag message."],
460 type: ["string", "`commit`, `tree`, `blob` or `tag`; must match the object (default: its type)."],
461 tagger: ["object", "`{ name, email, date }` (default: you)."],
462 },
463 },
464 "GET /repos/{owner}/{repo}/git/tags/{sha}": { summary: "Get a tag object", description: "`verification.verified` is always false.", access: "read" },
465 "GET /repos/{owner}/{repo}/git/ref/{ref}": {
466 summary: "Get a reference",
467 description: "`ref` is the name after `refs/`, like `heads/main` or `tags/v1.0`, and must match exactly.",
468 access: "read",
469 },
470 "GET /repos/{owner}/{repo}/git/matching-refs/{ref}": {
471 summary: "List matching references",
472 description: "Refs whose names start with `refs/{ref}`, like `heads/feature` or `tags/v1.`. Pull request refs appear only when `ref` starts with `pull/`.",
473 access: "read",
474 query: { per_page: ["integer", "Results per page (default 100, at most 100)."], page: page.page! },
475 },
476 "GET /repos/{owner}/{repo}/git/refs": {
477 summary: "List all references",
478 description: "Every branch, tag and other ref, without pull request refs.",
479 access: "read",
480 query: { per_page: ["integer", "Results per page (default 100, at most 100)."], page: page.page! },
481 },
482
483 // ------------------------------------------------------------ branch protection
484 "GET /repos/{owner}/{repo}/branches/{branch}/protection": { summary: "Get a protection rule", description: "`branch` is the rule's pattern.", access: "admin" },
485 "PUT /repos/{owner}/{repo}/branches/{branch}/protection": {
486 summary: "Create or replace a protection rule",
487 description: "`branch` is a branch name or pattern. People only; agents can't change protection. See the branch protection guide for every field.",
488 access: "admin",
489 },
490 "DELETE /repos/{owner}/{repo}/branches/{branch}/protection": { summary: "Delete a protection rule", access: "admin", status: 204 },
491
492 // ------------------------------------------------------------ issues
493 "GET /repos/{owner}/{repo}/issues": {
494 summary: "List issues and pull requests",
495 description: "As on GitHub, this list includes pull requests. The total is in the `X-Total-Count` header.",
496 access: "read",
497 query: { state: ["string", "`open` (default), `closed` or `all`."], labels: ["string", "Only items with this label."], creator: ["string", "Only items opened by this login."], q: ["string", "Text search in titles."], ...page },
498 },
499 "POST /repos/{owner}/{repo}/issues": {
500 summary: "Create an issue",
501 access: "read",
502 status: 201,
503 body: { title: ["string", "Title.", true], body: ["string", "Markdown body."], labels: ["string[]", "Label names."], assignees: ["string[]", "Logins."] },
504 },
505 "GET /repos/{owner}/{repo}/issues/{number}": { summary: "Get an issue", access: "read" },
506 "PATCH /repos/{owner}/{repo}/issues/{number}": {
507 summary: "Update an issue",
508 access: "read",
509 body: {
510 title: ["string", "New title."],
511 body: ["string", "New body."],
512 state: ["string", "`open` or `closed`."],
513 state_reason: ["string", "Why it was closed, like `completed` or `not_planned`."],
514 labels: ["string[]", "Replace the labels."],
515 assignees: ["string[]", "Replace the assignees."],
516 },
517 },
518 "GET /repos/{owner}/{repo}/issues/{number}/comments": {
519 summary: "List comments",
520 description: "Conversation comments on an issue or pull request (not review comments).",
521 access: "read",
522 query: { since: ["string", "Only comments updated at or after this ISO timestamp."], ...page },
523 },
524 "POST /repos/{owner}/{repo}/issues/{number}/comments": { summary: "Add a comment", access: "read", status: 201, body: { body: ["string", "Markdown body.", true] } },
525 "GET /repos/{owner}/{repo}/issues/comments": {
526 summary: "List comments in a repository",
527 description: "Conversation comments on every issue and pull request.",
528 access: "read",
529 query: {
530 since: ["string", "Only comments updated at or after this ISO timestamp."],
531 sort: ["string", "`created` (default) or `updated`."],
532 direction: ["string", "`asc` (default) or `desc`."],
533 ...page,
534 },
535 },
536 "GET /repos/{owner}/{repo}/issues/comments/{id}": { summary: "Get a comment", description: "`id` is the numeric id or Diffinery's own.", access: "read" },
537 "PATCH /repos/{owner}/{repo}/issues/comments/{id}": { summary: "Edit a comment", access: "read", body: { body: ["string", "New Markdown body.", true] } },
538 "DELETE /repos/{owner}/{repo}/issues/comments/{id}": { summary: "Delete a comment", description: "Your own comments, or anyone's with maintain access.", access: "read", status: 204 },
539 ...reactionDocs("/repos/{owner}/{repo}/issues/{number}", "an issue", "Works for pull requests too."),
540 ...reactionDocs("/repos/{owner}/{repo}/issues/comments/{id}", "a comment"),
541 "POST /repos/{owner}/{repo}/issues/{number}/assignees": {
542 summary: "Add assignees",
543 description: "Adds to the current assignees, at most 10 in all. Logins aren't checked against who can be assigned. Returns the issue.",
544 access: "triage",
545 status: 201,
546 body: { assignees: ["string[]", "Logins to add."] },
547 },
548 "DELETE /repos/{owner}/{repo}/issues/{number}/assignees": { summary: "Remove assignees", description: "Returns the issue.", access: "triage", body: { assignees: ["string[]", "Logins to remove."] } },
549 "PUT /repos/{owner}/{repo}/issues/{number}/lock": {
550 summary: "Lock a conversation",
551 description: "While locked, only people with triage access can comment, review or react.",
552 access: "triage",
553 status: 204,
554 body: { lock_reason: ["string", "`off-topic`, `too heated`, `resolved` or `spam`."] },
555 },
556 "DELETE /repos/{owner}/{repo}/issues/{number}/lock": { summary: "Unlock a conversation", access: "triage", status: 204 },
557 "GET /repos/{owner}/{repo}/issues/{number}/events": { summary: "List an issue's events", description: "Labels, assignments, state changes, locks, review requests and the like, oldest first.", access: "read", query: { ...page } },
558 "GET /repos/{owner}/{repo}/issues/{number}/timeline": {
559 summary: "List an issue's timeline",
560 description: "Events, comments (`commented`) and reviews (`reviewed`) together, oldest first.",
561 access: "read",
562 query: { ...page },
563 },
564 "GET /repos/{owner}/{repo}/issues/events": { summary: "List events in a repository", description: "Events on every issue and pull request, newest first, each with its `issue`.", access: "read", query: { ...page } },
565 "GET /repos/{owner}/{repo}/issues/events/{id}": { summary: "Get an event", access: "read" },
566 "GET /repos/{owner}/{repo}/labels": { summary: "List labels", access: "read" },
567 "POST /repos/{owner}/{repo}/labels": {
568 summary: "Create a label",
569 description: "422 when a label with that name exists; change one with `PATCH /repos/{owner}/{repo}/labels/{name}`.",
570 access: "triage",
571 status: 201,
572 body: { name: ["string", "Label name, at most 100 characters.", true], color: ["string", "6-digit hex color, with or without `#` (default `ededed`)."], description: ["string", "Description."] },
573 },
574 "GET /repos/{owner}/{repo}/labels/{name}": { summary: "Get a label", access: "read" },
575 "PATCH /repos/{owner}/{repo}/labels/{name}": {
576 summary: "Update a label",
577 description: "Renaming it renames it on every issue too.",
578 access: "triage",
579 body: { new_name: ["string", "New name."], color: ["string", "6-digit hex color, with or without `#`."], description: ["string", "Description."] },
580 },
581 "DELETE /repos/{owner}/{repo}/labels/{name}": { summary: "Delete a label", description: "Also removes it from every issue.", access: "triage", status: 204 },
582 "GET /repos/{owner}/{repo}/issues/{number}/labels": { summary: "List an issue's labels", access: "read", query: { ...page } },
583 "POST /repos/{owner}/{repo}/issues/{number}/labels": {
584 summary: "Add labels to an issue",
585 description: "Labels that don't exist yet are created. At most 100 per issue. Returns the issue's labels.",
586 access: "triage",
587 body: { labels: ["string[]", "Label names (`{ name }` objects, or a bare array as the body, work too).", true] },
588 },
589 "PUT /repos/{owner}/{repo}/issues/{number}/labels": {
590 summary: "Replace an issue's labels",
591 description: "Labels that don't exist yet are created; `[]` removes them all. Returns the issue's labels.",
592 access: "triage",
593 body: { labels: ["string[]", "Label names (`{ name }` objects, or a bare array as the body, work too).", true] },
594 },
595 "DELETE /repos/{owner}/{repo}/issues/{number}/labels": { summary: "Remove all labels from an issue", access: "triage", status: 204 },
596 "DELETE /repos/{owner}/{repo}/issues/{number}/labels/{name}": {
597 summary: "Remove a label from an issue",
598 description: "Returns the labels left. 404 when the issue doesn't have it.",
599 access: "triage",
600 },
601
602 // ------------------------------------------------------------ pull requests
603 "GET /repos/{owner}/{repo}/pulls": {
604 summary: "List pull requests",
605 description: "The total is in the `X-Total-Count` header.",
606 access: "read",
607 query: { state: ["string", "`open` (default), `closed` or `all`."], labels: ["string", "Only pull requests with this label."], creator: ["string", "Only pull requests opened by this login."], q: ["string", "Text search in titles."], ...page },
608 },
609 "POST /repos/{owner}/{repo}/pulls": {
610 summary: "Open a pull request",
611 access: "write",
612 status: 201,
613 body: {
614 title: ["string", "Title.", true],
615 head: ["string", "Branch with the changes (`owner:branch` also works).", true],
616 base: ["string", "Branch to merge into (default: the default branch)."],
617 body: ["string", "Markdown description."],
618 intent: ["string", "Diffinery: what the change is meant to do, shown to reviewers."],
619 draft: ["boolean", "Open as a draft."],
620 reviewers: ["string[]", "Logins to request reviews from."],
621 },
622 },
623 "GET /repos/{owner}/{repo}/pulls/{number}": {
624 summary: "Get a pull request",
625 description: "Includes `mergeable`, `mergeable_state`, `merge_blockers` (why it can't merge yet), `approvals`, `required_approvals` and `checks`.",
626 access: "read",
627 },
628 "PATCH /repos/{owner}/{repo}/pulls/{number}": {
629 summary: "Update a pull request",
630 description:
631 "The author, or anyone with triage access. `base` isn't supported and is ignored. A merged pull request can't be reopened. A title or body change sends a `pull_request` `edited` event. `draft` is a Diffinery extension (GitHub only offers it through GraphQL): the author or anyone with write access can set it on an open pull request, which sends `ready_for_review` or `converted_to_draft`.",
632 access: "read",
633 body: {
634 title: ["string", "New title."],
635 body: ["string", "New description."],
636 state: ["string", "`open` or `closed`."],
637 draft: ["boolean", "`false` marks it ready for review; `true` converts it back to a draft."],
638 },
639 },
640 "PUT /repos/{owner}/{repo}/pulls/{number}/update-branch": {
641 summary: "Update a pull request's branch",
642 description:
643 "Merges the base branch into the head branch right away, then answers 202 as GitHub does. 422 when the base has no new commits, on a conflict, or when `expected_head_sha` doesn't match. Branch protection and agent rules apply to the head branch.",
644 access: "write",
645 status: 202,
646 body: { expected_head_sha: ["string", "Fail unless the head branch is at this commit."] },
647 },
648 "GET /repos/{owner}/{repo}/pulls/{number}/files": { summary: "List changed files", description: "Each file with its status, additions, deletions and patch.", access: "read" },
649 "GET /repos/{owner}/{repo}/pulls/{number}/commits": { summary: "List a pull request's commits", access: "read" },
650 "GET /repos/{owner}/{repo}/pulls/{number}/merge": { summary: "Check mergeability", description: "Whether the pull request can merge now, and if not, the blockers.", access: "read" },
651 "PUT /repos/{owner}/{repo}/pulls/{number}/merge": {
652 summary: "Merge a pull request",
653 description:
654 "Fails with 405 and the blockers when protection rules aren't met, and with 405 when a merge commit is asked for on a branch that requires linear history. A branch that requires a merge queue answers 405 too: add the pull request to the queue instead. A repository admin (a person, not an agent) can bypass unmet rules with `bypass_rules: true`, or the queue with `bypass_merge_queue: true`, when the rule doesn't include administrators, there are no conflicts and the pull request isn't a draft. A bypass that skipped an unmet rule is audited as `pull_request.merge_bypassed`.",
655 access: "write",
656 body: {
657 merge_method: ["string", "`merge`, `squash` or `rebase`."],
658 commit_title: ["string", "Title of the merge or squash commit."],
659 commit_message: ["string", "Body of the merge or squash commit."],
660 sha: ["string", "Fail if the head has moved from this commit."],
661 delete_branch: ["boolean", "Delete the head branch after merging."],
662 bypass_merge_queue: ["boolean", "Merge directly even when a queue is required (admins)."],
663 bypass_rules: ["boolean", "Merge even though protection rules aren't met (admins). Without it, an admin's merge is blocked like anyone else's."],
664 },
665 },
666 "GET /repos/{owner}/{repo}/merge-queue/{branch}": { summary: "Get a branch's merge queue", description: "Diffinery only (GitHub exposes this through GraphQL). Settings and every entry with its merge group and checks.", access: "read" },
667 "PUT /repos/{owner}/{repo}/pulls/{number}/merge-queue": {
668 summary: "Add a pull request to the merge queue",
669 description: "Diffinery only. Returns 201 with the entry when queued.",
670 access: "write",
671 status: 201,
672 body: { merge_method: ["string", "`merge`, `squash` or `rebase` (default: the queue's method)."], delete_branch: ["boolean", "Delete the head branch once merged."] },
673 },
674 "DELETE /repos/{owner}/{repo}/pulls/{number}/merge-queue": { summary: "Remove a pull request from the merge queue", access: "write", status: 204 },
675 "GET /repos/{owner}/{repo}/pulls/{number}/reviews": { summary: "List reviews", access: "read" },
676 "POST /repos/{owner}/{repo}/pulls/{number}/reviews": {
677 summary: "Submit a review",
678 description: "Approvals from agents don't count toward required approvals.",
679 access: "read",
680 status: 201,
681 body: {
682 event: ["string", "`APPROVE`, `REQUEST_CHANGES` or `COMMENT` (default)."],
683 body: ["string", "Review summary."],
684 comments: ["array", "Line comments: `{ path, line, side: \"LEFT\" | \"RIGHT\", body }`."],
685 },
686 },
687 "GET /repos/{owner}/{repo}/pulls/{number}/reviews/{id}": { summary: "Get a review", description: "`id` is the numeric id or Diffinery's own.", access: "read" },
688 "PUT /repos/{owner}/{repo}/pulls/{number}/reviews/{id}/dismissals": {
689 summary: "Dismiss a review",
690 description: "Only approvals and change requests can be dismissed. Needs maintain access; agents and the pull request's author can't dismiss. `event` is ignored. Returns the review.",
691 access: "maintain",
692 body: { message: ["string", "Why the review is dismissed.", true] },
693 },
694 "GET /repos/{owner}/{repo}/pulls/{number}/comments": { summary: "List review comments", description: "Line comments from reviews.", access: "read", query: { ...page } },
695 "POST /repos/{owner}/{repo}/pulls/{number}/comments": {
696 summary: "Create a review comment",
697 description:
698 "Adds one line comment as its own `COMMENTED` review, on the current head; `commit_id` is ignored and `position` is read as a line number, not a diff position. Multi-line comments aren't supported. With `in_reply_to`, replies to that comment instead. 422 on a closed pull request.",
699 access: "read",
700 status: 201,
701 body: {
702 body: ["string", "Markdown body.", true],
703 path: ["string", "File path (required unless replying)."],
704 line: ["integer", "Line number (required unless replying)."],
705 side: ["string", "`RIGHT` (default) or `LEFT`."],
706 position: ["integer", "Used as `line` when `line` is missing."],
707 commit_id: ["string", "Accepted and ignored."],
708 in_reply_to: ["integer", "Review comment id to reply to; its path and line are used."],
709 },
710 },
711 "POST /repos/{owner}/{repo}/pulls/{number}/comments/{id}/replies": {
712 summary: "Reply to a review comment",
713 description: "Adds a reply on the same line, as its own `COMMENTED` review.",
714 access: "read",
715 status: 201,
716 body: { body: ["string", "Markdown body.", true] },
717 },
718 "GET /repos/{owner}/{repo}/pulls/comments": {
719 summary: "List review comments in a repository",
720 access: "read",
721 query: {
722 since: ["string", "Only comments updated at or after this ISO timestamp."],
723 sort: ["string", "`created` (default) or `updated`."],
724 direction: ["string", "`asc` (default) or `desc`."],
725 ...page,
726 },
727 },
728 "GET /repos/{owner}/{repo}/pulls/comments/{id}": { summary: "Get a review comment", access: "read" },
729 "PATCH /repos/{owner}/{repo}/pulls/comments/{id}": {
730 summary: "Edit a review comment",
731 description: "Your own comments, or anyone's with maintain access.",
732 access: "read",
733 body: { body: ["string", "New Markdown body.", true] },
734 },
735 "DELETE /repos/{owner}/{repo}/pulls/comments/{id}": { summary: "Delete a review comment", description: "Your own comments, or anyone's with maintain access.", access: "read", status: 204 },
736 ...reactionDocs("/repos/{owner}/{repo}/pulls/comments/{id}", "a review comment"),
737 "GET /repos/{owner}/{repo}/pulls/{number}/requested_reviewers": { summary: "List requested reviewers", description: "Returns `{ users, teams }`; `teams` is always empty.", access: "read" },
738 "POST /repos/{owner}/{repo}/pulls/{number}/requested_reviewers": {
739 summary: "Request reviewers",
740 description: "The author, or anyone with triage access. Team reviewers aren't supported: a non-empty `team_reviewers` fails with 422. Sends the `pull_request` webhook (`review_requested`). Returns the pull request.",
741 access: "read",
742 status: 201,
743 body: { reviewers: ["string[]", "Logins."], team_reviewers: ["string[]", "Must be empty or absent."] },
744 },
745 "DELETE /repos/{owner}/{repo}/pulls/{number}/requested_reviewers": {
746 summary: "Remove requested reviewers",
747 description: "The author, or anyone with triage access. Teams aren't supported. Returns the pull request.",
748 access: "read",
749 body: { reviewers: ["string[]", "Logins to remove."] },
750 },
751
752 // ------------------------------------------------------------ commit statuses
753 "POST /repos/{owner}/{repo}/statuses/{sha}": {
754 summary: "Create a commit status",
755 description:
756 "`sha` may also be a branch or tag. Statuses show with the checks, and a required check in branch protection can name a `context`; one account's status never replaces another's for that. At most 1000 statuses per commit and context. Sends the `status` webhook. See [Checks and statuses](/docs/collaboration/checks-and-statuses).",
757 access: "write",
758 status: 201,
759 body: {
760 state: ["string", "`error`, `failure`, `pending` or `success`.", true],
761 target_url: ["string", "http(s) link to details."],
762 description: ["string", "Short description (cut at 1024 characters)."],
763 context: ["string", "Label that tells statuses apart (default `default`)."],
764 },
765 },
766 "GET /repos/{owner}/{repo}/commits/{ref}/statuses": { summary: "List commit statuses", description: "Every status for the commit, newest first, including older ones for the same context.", access: "read", query: { ...page } },
767 "GET /repos/{owner}/{repo}/statuses/{ref}": { summary: "List commit statuses for a ref", description: "Same as `GET /repos/{owner}/{repo}/commits/{ref}/statuses`.", access: "read", query: { ...page } },
768 "GET /repos/{owner}/{repo}/commits/{ref}/status": {
769 summary: "Get the combined status",
770 description: "The latest status per context and an overall `state`: `failure` if any is `error` or `failure`, `pending` if any is pending or there are none, else `success`. Check runs aren't included.",
771 access: "read",
772 query: { per_page: ["integer", "Statuses per page (default 100, at most 100)."], page: page.page! },
773 },
774
775 // ------------------------------------------------------------ checks
776 "GET /repos/{owner}/{repo}/commits/{ref}/check-runs": { summary: "List check runs for a commit", access: "read" },
777 "GET /repos/{owner}/{repo}/check-runs/{id}": { summary: "Get a check run", description: "`id` is the numeric id or Diffinery's own.", access: "read" },
778 "PATCH /repos/{owner}/{repo}/check-runs/{id}": {
779 summary: "Update a check run",
780 description:
781 "Only the account that created the check run can update it, and runs from Diffinery Actions can't be changed. A `conclusion` completes the run. Annotations add to the existing ones (50 per request, 1000 in all).",
782 access: "write",
783 body: {
784 name: ["string", "New name."],
785 status: ["string", "`queued`, `in_progress`, `completed`, `waiting`, `requested` or `pending`."],
786 conclusion: ["string", "`action_required`, `cancelled`, `failure`, `neutral`, `success`, `skipped`, `stale` or `timed_out`."],
787 details_url: ["string", "http(s) link to more detail."],
788 external_id: ["string", "Your own reference for the run."],
789 started_at: ["string", "ISO timestamp."],
790 completed_at: ["string", "ISO timestamp."],
791 output: ["object", "`{ title, summary, text, annotations }`."],
792 },
793 },
794 "GET /repos/{owner}/{repo}/check-runs/{id}/annotations": { summary: "List check run annotations", access: "read", query: { ...page } },
795 "GET /repos/{owner}/{repo}/commits/{ref}/check-suites": {
796 summary: "List check suites for a commit",
797 description: "Diffinery derives check suites from check runs: one per workflow run, and one per app (id `app-{app}-{sha}`) for external check runs. `head_branch` is always null and `pull_requests` empty.",
798 access: "read",
799 query: { ...page },
800 },
801 "GET /repos/{owner}/{repo}/check-suites/{id}": { summary: "Get a check suite", description: "`id` is a workflow run id or `app-{app}-{sha}`.", access: "read" },
802 "GET /repos/{owner}/{repo}/check-suites/{id}/check-runs": { summary: "List a check suite's check runs", access: "read", query: { ...page } },
803 "POST /repos/{owner}/{repo}/check-runs": {
804 summary: "Create or update a check run",
805 description: "For external CI and integrations. A check with the same name on the same commit is updated. Required checks in branch protection can name it.",
806 access: "write",
807 status: 201,
808 body: {
809 name: ["string", "Check name.", true],
810 head_sha: ["string", "Commit the check is for.", true],
811 status: ["string", "`queued` (default), `in_progress` or `completed`."],
812 conclusion: ["string", "With `completed`: `success`, `failure`, `neutral`, `cancelled`, `skipped` or `timed_out`."],
813 details_url: ["string", "Link to more detail."],
814 output: ["object", "`{ title, summary }`."],
815 },
816 },
817
818 // ------------------------------------------------------------ deployments
819 "GET /repos/{owner}/{repo}/deployments": {
820 summary: "List deployments",
821 description: "Newest first. Each filter matches exactly.",
822 access: "read",
823 query: {
824 sha: ["string", "Full commit SHA."],
825 ref: ["string", "Ref, as given when the deployment was created."],
826 task: ["string", "Task name."],
827 environment: ["string", "Environment name."],
828 ...page,
829 },
830 },
831 "POST /repos/{owner}/{repo}/deployments": {
832 summary: "Create a deployment",
833 description:
834 "Unless `required_contexts` is `[]`, every check and status on the commit (or each context you name) must have passed, else 409; checks and statuses reported by agents don't count toward the default set. Creates the environment if needed and sends the `deployment` webhook. `auto_merge` is ignored. See [Environments](/docs/actions/environments).",
835 access: "write",
836 status: 201,
837 body: {
838 ref: ["string", "Branch, tag or commit to deploy.", true],
839 task: ["string", "Task name (default `deploy`)."],
840 payload: ["object", "Extra data for the deployer (a string is kept as is)."],
841 environment: ["string", "Environment name (default `production`)."],
842 description: ["string", "Short description."],
843 transient_environment: ["boolean", "Transient deployments aren't made inactive by later successful ones."],
844 production_environment: ["boolean", "Default: true when the environment is `production`."],
845 required_contexts: ["string[]", "Contexts that must have passed; `[]` skips the check."],
846 },
847 },
848 "GET /repos/{owner}/{repo}/deployments/{id}": { summary: "Get a deployment", access: "read" },
849 "DELETE /repos/{owner}/{repo}/deployments/{id}": {
850 summary: "Delete a deployment",
851 description: "Refused with 422 while its latest status is `success`, unless it is the only deployment in its environment. Deletes its statuses too.",
852 access: "write",
853 status: 204,
854 },
855 "GET /repos/{owner}/{repo}/deployments/{id}/statuses": { summary: "List deployment statuses", description: "Newest first.", access: "read", query: { ...page } },
856 "POST /repos/{owner}/{repo}/deployments/{id}/statuses": {
857 summary: "Create a deployment status",
858 description:
859 "A `success` status marks older successful deployments to the same environment inactive (except transient and production ones) unless `auto_inactive` is false. A different `environment` moves the deployment there. Sends the `deployment_status` webhook.",
860 access: "write",
861 status: 201,
862 body: {
863 state: ["string", "`error`, `failure`, `inactive`, `in_progress`, `queued`, `pending` or `success`.", true],
864 log_url: ["string", "http(s) link to the deployment's output."],
865 target_url: ["string", "Older name for `log_url`, used when `log_url` is missing."],
866 environment_url: ["string", "http(s) link to the deployed environment."],
867 description: ["string", "Short description (cut at 140 characters)."],
868 environment: ["string", "Move the deployment to this environment."],
869 auto_inactive: ["boolean", "`false` keeps older deployments active (default `true`)."],
870 },
871 },
872 "GET /repos/{owner}/{repo}/deployments/{id}/statuses/{status}": { summary: "Get a deployment status", access: "read" },
873
874 // ------------------------------------------------------------ environments
875 "GET /repos/{owner}/{repo}/environments": { summary: "List environments", access: "read", query: { ...page } },
876 "GET /repos/{owner}/{repo}/environments/{env}": { summary: "Get an environment", access: "read" },
877 "PUT /repos/{owner}/{repo}/environments/{env}": {
878 summary: "Create or update an environment",
879 description:
880 "People only; agents can't change environments. Required reviewers and wait timers aren't supported: a non-zero `wait_timer` or any `reviewers` fails with 422, and `prevent_self_review` is ignored. Audited. See [Environments](/docs/actions/environments).",
881 access: "admin",
882 body: {
883 deployment_branch_policy: ["object", "`{ protected_branches, custom_branch_policies }` with exactly one true, or `null` to allow every ref."],
884 wait_timer: ["integer", "Must be 0 or absent."],
885 reviewers: ["array", "Must be empty or absent."],
886 },
887 },
888 "DELETE /repos/{owner}/{repo}/environments/{env}": { summary: "Delete an environment", description: "Deletes its secrets and variables too. People only. Audited.", access: "admin", status: 204 },
889 "GET /repos/{owner}/{repo}/environments/{env}/deployment-branch-policies": {
890 summary: "List deployment branch policies",
891 description: "The branch and tag patterns of an environment with `custom_branch_policies`.",
892 access: "read",
893 query: { ...page },
894 },
895 "POST /repos/{owner}/{repo}/environments/{env}/deployment-branch-policies": {
896 summary: "Create a deployment branch policy",
897 description: "The environment must have `custom_branch_policies`. Tag patterns never match tags last pushed by an agent. People only. Audited.",
898 access: "admin",
899 body: { name: ["string", "Branch or tag name pattern, like `release/*`.", true], type: ["string", "`branch` (default) or `tag`."] },
900 },
901 "DELETE /repos/{owner}/{repo}/environments/{env}/deployment-branch-policies/{id}": {
902 summary: "Delete a deployment branch policy",
903 description: "People only. Audited.",
904 access: "admin",
905 status: 204,
906 },
907 "GET /repos/{owner}/{repo}/environments/{env}/secrets": {
908 summary: "List environment secrets",
909 description: "Names and dates only; values are never returned.",
910 access: "admin",
911 query: { ...page },
912 },
913 "GET /repos/{owner}/{repo}/environments/{env}/secrets/{name}": { summary: "Get an environment secret", description: "Name and dates only.", access: "admin" },
914 "PUT /repos/{owner}/{repo}/environments/{env}/secrets/{name}": {
915 summary: "Set an environment secret",
916 description:
917 "People only. Names are upper-cased, and `GITHUB_` names are refused. The value is encrypted at rest; GitHub's sealed-box `encrypted_value` isn't supported. The first secret on an environment without a branch policy limits it to protected branches. Returns 201 for a new secret and 204 for a replaced one. Audited.",
918 access: "admin",
919 status: 201,
920 body: { value: ["string", "Secret value.", true] },
921 },
922 "DELETE /repos/{owner}/{repo}/environments/{env}/secrets/{name}": { summary: "Delete an environment secret", description: "People only. Audited.", access: "admin", status: 204 },
923 "GET /repos/{owner}/{repo}/environments/{env}/variables": {
924 summary: "List environment variables",
925 access: "write",
926 query: { per_page: ["integer", "Results per page (default 10, at most 100)."], page: page.page! },
927 },
928 "GET /repos/{owner}/{repo}/environments/{env}/variables/{name}": { summary: "Get an environment variable", access: "write" },
929 "POST /repos/{owner}/{repo}/environments/{env}/variables": {
930 summary: "Create an environment variable",
931 description: "409 when it exists. Names are upper-cased, and `GITHUB_` names are refused. People only.",
932 access: "admin",
933 status: 201,
934 body: { name: ["string", "Variable name.", true], value: ["string", "Variable value.", true] },
935 },
936 "PATCH /repos/{owner}/{repo}/environments/{env}/variables/{name}": {
937 summary: "Update an environment variable",
938 description: "People only.",
939 access: "admin",
940 status: 204,
941 body: { name: ["string", "New name (renames the variable)."], value: ["string", "New value."] },
942 },
943 "DELETE /repos/{owner}/{repo}/environments/{env}/variables/{name}": { summary: "Delete an environment variable", description: "People only.", access: "admin", status: 204 },
944
945 // ------------------------------------------------------------ actions
946 "GET /repos/{owner}/{repo}/actions/workflows": { summary: "List workflows", description: "Each workflow file with its triggers, and parse errors for invalid ones (`state: invalid`).", access: "read" },
947 "GET /repos/{owner}/{repo}/actions/workflows/{workflow}": {
948 summary: "Get a workflow",
949 description: "`workflow` is the file path or file name; the response's `id` is the path. `created_at` and `updated_at` are the time of the request, not of the file.",
950 access: "read",
951 },
952 "GET /repos/{owner}/{repo}/actions/workflows/{workflow}/runs": {
953 summary: "List a workflow's runs",
954 description: "No `Link` header; page with `total_count`.",
955 access: "read",
956 query: {
957 branch: ["string", "Head branch."],
958 status: ["string", "Run status or conclusion."],
959 event: ["string", "Triggering event."],
960 head_sha: ["string", "Head commit."],
961 ...page,
962 },
963 },
964 "POST /repos/{owner}/{repo}/actions/workflows/{workflow}/dispatches": {
965 summary: "Run a workflow (workflow_dispatch)",
966 description: "`workflow` is the file path or name, URL-encoded, like `.github%2Fworkflows%2Fdeploy.yml` or `deploy.yml`. Returns the new run's id.",
967 access: "write",
968 status: 201,
969 body: { ref: ["string", "Branch or tag to run on.", true], inputs: ["object", "Values for `on.workflow_dispatch.inputs`."] },
970 },
971 "POST /repos/{owner}/{repo}/actions/runners/claim": {
972 summary: "Claim a job for a self-hosted runner",
973 description: "Used by `diffinery runner`. Returns 204 when no queued job matches the labels.",
974 access: "admin",
975 body: { name: ["string", "Runner name."], labels: ["string[]", "Runner labels, matched against `runs-on`."] },
976 },
977 "GET /repos/{owner}/{repo}/actions/runs": {
978 summary: "List workflow runs",
979 access: "read",
980 query: {
981 workflow: ["string", "Workflow file path."],
982 branch: ["string", "Head branch."],
983 status: ["string", "Run status or conclusion."],
984 event: ["string", "Triggering event."],
985 head_sha: ["string", "Head commit."],
986 per_page: ["integer", "Results per page (default 25, at most 100)."],
987 page: ["integer", "Page number, starting at 1."],
988 },
989 },
990 "GET /repos/{owner}/{repo}/actions/runs/{id}": { summary: "Get a workflow run", description: "The run with its jobs.", access: "read" },
991 "GET /repos/{owner}/{repo}/actions/runs/{id}/attempts/{attempt}": {
992 summary: "Get a run attempt",
993 description: "Re-runs are stored as separate runs with the same run number; this returns attempt `attempt` of the run's number.",
994 access: "read",
995 },
996 "GET /repos/{owner}/{repo}/actions/runs/{id}/logs": {
997 summary: "Download a run's logs",
998 description: "A zip with one text file per job. Jobs past 24 MB of logs in total are left out.",
999 access: "read",
1000 },
1001 "GET /repos/{owner}/{repo}/actions/runs/{id}/jobs": { summary: "List a run's jobs", access: "read" },
1002 "POST /repos/{owner}/{repo}/actions/runs/{id}/cancel": { summary: "Cancel a run", access: "write", status: 202 },
1003 "POST /repos/{owner}/{repo}/actions/runs/{id}/rerun": { summary: "Re-run a workflow run", access: "write", status: 201 },
1004 "POST /repos/{owner}/{repo}/actions/runs/{id}/rerun-failed-jobs": { summary: "Re-run a run's failed jobs", access: "write", status: 201 },
1005 "GET /repos/{owner}/{repo}/actions/jobs/{id}": { summary: "Get a job", description: "The job with its steps.", access: "read" },
1006 "GET /repos/{owner}/{repo}/actions/jobs/{id}/logs": {
1007 summary: "Get a job's logs",
1008 description: "Plain text, one timestamped line each, or JSON with `format=json`.",
1009 access: "read",
1010 query: { format: ["string", "`json` for an array of `{ t, text }` lines."], after: ["integer", "Only lines after this sequence number (for following a live log)."] },
1011 },
1012 "GET /repos/{owner}/{repo}/actions/runs/{runId}/artifacts": {
1013 summary: "List a run's artifacts",
1014 description: "Each with an `archive_download_url` (a zip). Expired artifacts are listed with `expired: true`; their content is gone.",
1015 access: "read",
1016 query: { name: ["string", "Only artifacts with this name."] },
1017 },
1018 "GET /repos/{owner}/{repo}/actions/secrets": { summary: "List repository secrets", description: "Names and update times only; values are never returned.", access: "admin" },
1019 "PUT /repos/{owner}/{repo}/actions/secrets/{name}": {
1020 summary: "Set a repository secret",
1021 description: "People only. The value is encrypted at rest. GitHub's sealed-box `encrypted_value` isn't supported; send `value` over HTTPS.",
1022 access: "admin",
1023 status: 204,
1024 body: { value: ["string", "Secret value.", true] },
1025 },
1026 "DELETE /repos/{owner}/{repo}/actions/secrets/{name}": { summary: "Delete a repository secret", description: "People only.", access: "admin", status: 204 },
1027 "GET /repos/{owner}/{repo}/actions/variables": { summary: "List repository variables", access: "write", query: { ...page } },
1028 "POST /repos/{owner}/{repo}/actions/variables": {
1029 summary: "Create a repository variable",
1030 description: "409 when it exists. Names are stored upper-cased. People only.",
1031 access: "admin",
1032 status: 201,
1033 body: { name: ["string", "Variable name.", true], value: ["string", "Variable value.", true] },
1034 },
1035 "GET /repos/{owner}/{repo}/actions/variables/{name}": { summary: "Get a repository variable", description: "`created_at` and `updated_at` are always null.", access: "write" },
1036 "PATCH /repos/{owner}/{repo}/actions/variables/{name}": {
1037 summary: "Update a repository variable",
1038 description: "People only.",
1039 access: "admin",
1040 status: 204,
1041 body: { name: ["string", "New name (renames the variable)."], value: ["string", "New value."] },
1042 },
1043 "PUT /repos/{owner}/{repo}/actions/variables/{name}": {
1044 summary: "Set a repository variable",
1045 description: "Diffinery's form: creates the variable or replaces its value. GitHub clients use `POST` and `PATCH`. Names are stored upper-cased. People only.",
1046 access: "admin",
1047 status: 204,
1048 body: { value: ["string", "Variable value.", true] },
1049 },
1050 "DELETE /repos/{owner}/{repo}/actions/variables/{name}": {
1051 summary: "Delete a repository variable",
1052 description: "People only. `name` must match the stored name exactly, and names are stored upper-cased, so send it in upper case.",
1053 access: "admin",
1054 status: 204,
1055 },
1056 "GET /orgs/{org}/actions/secrets": { summary: "List organization secrets", description: "Names and update times only.", access: "org-admin" },
1057 "PUT /orgs/{org}/actions/secrets/{name}": {
1058 summary: "Set an organization secret",
1059 description: "Available to every repository in the organization. Names are upper-cased and must match `[A-Z_][A-Z0-9_]*`. People only.",
1060 access: "org-admin",
1061 status: 204,
1062 body: { value: ["string", "Secret value.", true] },
1063 },
1064 "DELETE /orgs/{org}/actions/secrets/{name}": { summary: "Delete an organization secret", access: "org-admin", status: 204 },
1065
1066 // ------------------------------------------------------------ webhooks
1067 "GET /repos/{owner}/{repo}/hooks": { summary: "List webhooks", description: "Secrets are never returned.", access: "admin" },
1068 "POST /repos/{owner}/{repo}/hooks": {
1069 summary: "Create a webhook",
1070 access: "admin",
1071 status: 201,
1072 body: { config: ["object", "`{ url, secret }`. The secret signs each delivery (`X-Hub-Signature-256`).", true], events: ["string[]", "Event names, or `[\"*\"]` (default) for all."] },
1073 },
1074 "DELETE /repos/{owner}/{repo}/hooks/{id}": { summary: "Delete a webhook", access: "admin", status: 204 },
1075
1076 // ------------------------------------------------------------ notifications and email
1077 "GET /notifications": { summary: "List notifications", access: "account", query: { all: ["boolean", "`true` to include read ones."] } },
1078 "PUT /notifications": { summary: "Mark all notifications as read", access: "account", status: 205 },
1079 "GET /repos/{owner}/{repo}/subscription": { summary: "Get your watch setting", description: "404 when you only get participating notifications (the default), as on GitHub.", access: "read" },
1080 "PUT /repos/{owner}/{repo}/subscription": {
1081 summary: "Watch or ignore a repository",
1082 access: "read",
1083 body: { subscribed: ["boolean", "`true` for all activity, `false` for participating only."], ignored: ["boolean", "`true` to ignore the repository."] },
1084 },
1085 "DELETE /repos/{owner}/{repo}/subscription": { summary: "Stop watching a repository", description: "Back to participating notifications.", access: "read", status: 204 },
1086 "GET /diffinery/notification-settings": { summary: "Get your notification settings", access: "account" },
1087 "PUT /diffinery/notification-settings": { summary: "Update your notification settings", description: "Send only the groups you want to change.", access: "account" },
1088 "GET /diffinery/emails": {
1089 summary: "List emails sent to you",
1090 access: "account",
1091 query: { kind: ["string", "Only this kind of email."], since: ["string", "ISO timestamp."], per_page: ["integer", "At most this many (default 50)."], all: ["string", "`1` for everyone's (site admins)."] },
1092 },
1093 "POST /diffinery/emails/test": { summary: "Send yourself a test email", access: "account" },
1094 "GET /diffinery/admin/email": { summary: "Email delivery status", description: "Whether sending is configured, and counts for the last 24 hours.", access: "site-admin" },
1095 "POST /diffinery/admin/emails/samples": { summary: "Send yourself every email template", access: "site-admin", body: { only: ["string[]", "Only these sample ids."] } },
1096
1097 // ------------------------------------------------------------ tokens, agents and sandboxes
1098 "GET /diffinery/tokens": { summary: "List your tokens", access: "account" },
1099 "POST /diffinery/tokens": {
1100 summary: "Create a token",
1101 description: "From a browser session or an admin-scoped token. The token value is returned once. Unknown scopes or an empty list fail with 422.",
1102 access: "account",
1103 status: 201,
1104 body: {
1105 name: ["string", "A name to recognise it by.", true],
1106 scopes: ["string[]", "`read`, `write` (default) and/or `admin`."],
1107 repositories: ["string[]", "Restrict to these `owner/name` repositories."],
1108 expires_in_days: ["integer", "Lifetime in days."],
1109 },
1110 },
1111 "DELETE /diffinery/tokens/{id}": { summary: "Revoke a token", access: "account", status: 204 },
1112 "GET /diffinery/agents": { summary: "List your agents", description: "Agents you own, and agents of organizations you administer.", access: "account" },
1113 "POST /diffinery/agents": {
1114 summary: "Create an agent",
1115 description: "People only. Returns the agent and its first token (write scope, 90 days, unless you say otherwise). Unknown scopes or an empty list fail with 422.",
1116 access: "account",
1117 status: 201,
1118 body: {
1119 login: ["string", "Agent login.", true],
1120 name: ["string", "Display name."],
1121 org: ["string", "Organization that owns the agent (default: you)."],
1122 policy: ["object", "Agent policy."],
1123 token: ["object", "`{ scopes, expires_in_days }` for the first token."],
1124 },
1125 },
1126 "POST /diffinery/agents/{login}/tokens": {
1127 summary: "Create a token for an agent",
1128 description: "The agent's owner, or an owner or admin of the organization that owns it. Default scope `write`; unknown scopes or an empty list fail with 422. Audited as `agent.token.create`.",
1129 access: "account",
1130 status: 201,
1131 body: { name: ["string", "Token name."], scopes: ["string[]", "`read`, `write` (default) and/or `admin`."], expires_in_days: ["integer", "Lifetime in days."] },
1132 },
1133 "DELETE /diffinery/agents/{login}": { summary: "Disable an agent", description: "Its tokens stop working.", access: "account", status: 204 },
1134 "POST /repos/{owner}/{repo}/diffinery/sandboxes": {
1135 summary: "Start a sandbox",
1136 description: "A container with the repository checked out, for agents to build and test in.",
1137 access: "write",
1138 status: 201,
1139 body: { ref: ["string", "Branch, tag or commit to check out (default: the default branch)."] },
1140 },
1141 "POST /repos/{owner}/{repo}/diffinery/sandboxes/{id}/{action}": {
1142 summary: "Use a sandbox",
1143 description: "`action` is `exec` (run a command), `read` or `write` (files), or `close`.",
1144 access: "write",
1145 },
1146
1147 // ------------------------------------------------------------ packages
1148 "GET /repos/{owner}/{repo}/packages": { summary: "List a repository's packages", description: "Container images linked to the repository.", access: "read" },
1149 ...packageDocs("/user/packages", "your"),
1150 ...packageDocs("/users/{owner}/packages", "a user's"),
1151 ...packageDocs("/orgs/{owner}/packages", "an organization's"),
1152};
1153
1154function packageDocs(prefix: string, whose: string): Record<string, OpDoc> {
1155 return {
1156 [`GET ${prefix}`]: { summary: `List ${whose} packages`, description: "Only packages you can pull.", access: "account" },
1157 [`GET ${prefix}/{type}/{name}`]: { summary: `Get one of ${whose} packages`, description: "`type` is `container`.", access: "read" },
1158 [`PATCH ${prefix}/{type}/{name}`]: {
1159 summary: `Update one of ${whose} packages`,
1160 description: "Change its visibility or the repository it is linked to.",
1161 access: "admin",
1162 body: { visibility: ["string", "`private` or `public`."], repository: ["string", "Repository name to link, or `null` to unlink."] },
1163 },
1164 [`DELETE ${prefix}/{type}/{name}`]: { summary: `Delete one of ${whose} packages`, access: "admin", status: 204 },
1165 [`GET ${prefix}/{type}/{name}/versions`]: { summary: "List package versions", access: "read", query: { state: ["string", "`tagged` for tagged versions only."], ...page } },
1166 [`GET ${prefix}/{type}/{name}/versions/{id}`]: { summary: "Get a package version", access: "read" },
1167 [`DELETE ${prefix}/{type}/{name}/versions/{id}`]: { summary: "Delete a package version", access: "admin", status: 204 },
1168 };
1169}
1170
1171/** GitHub's tarball and zipball endpoints, with and without a ref. */
1172function archiveDocs(kind: "tarball" | "zipball", what: string, format: string): Record<string, OpDoc> {
1173 const description = `A ${format} of the repository's files, served directly (GitHub redirects to a download host instead). Built once per commit, at most 20 new builds per repository per hour (429 after that); repositories with more than 24 MB of files can't be archived (413).`;
1174 return {
1175 [`GET /repos/{owner}/{repo}/${kind}`]: { summary: `Download ${what}`, description: `${description} Uses the default branch.`, access: "read" },
1176 [`GET /repos/{owner}/{repo}/${kind}/{ref}`]: { summary: `Download ${what} of a ref`, description, access: "read" },
1177 };
1178}
1179
1180/** Reactions on one kind of subject. `note` adds a sentence to the list endpoint. */
1181function reactionDocs(prefix: string, what: string, note?: string): Record<string, OpDoc> {
1182 return {
1183 [`GET ${prefix}/reactions`]: {
1184 summary: `List reactions to ${what}`,
1185 ...(note ? { description: note } : {}),
1186 access: "read",
1187 query: { content: ["string", "Only reactions with this content."], ...page },
1188 },
1189 [`POST ${prefix}/reactions`]: {
1190 summary: `Add a reaction to ${what}`,
1191 description: `Needs a signed-in account${what === "a release" ? "" : ", and triage access when the conversation is locked"}. Returns 201 for a new reaction, or 200 with the existing one when you had already reacted with that content.`,
1192 access: "read",
1193 status: 201,
1194 body: { content: ["string", "`+1`, `-1`, `laugh`, `confused`, `heart`, `hooray`, `rocket` or `eyes`.", true] },
1195 },
1196 [`DELETE ${prefix}/reactions/{reaction}`]: { summary: `Delete a reaction from ${what}`, description: "Your own reactions, or anyone's with admin access.", access: "read", status: 204 },
1197 };
1198}