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.

1499 lines · 104.4 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" | "org-owner" | "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; sessionOnly?: boolean }> = {
35 "POST /user/orgs": { scope: "admin" },
36 "PATCH /orgs/{org}": { scope: "admin" },
37 "POST /orgs/{org}/invitations": { scope: "admin" },
38 "POST /invitations": { scope: "admin" },
39 "DELETE /orgs/{org}": { scope: "admin" },
40 "POST /repos/{owner}/{repo}/transfer": { scope: "admin" },
41 "GET /orgs/{org}/invitations": { scope: "read" },
42 "DELETE /orgs/{org}/invitations/{id}": { scope: "admin" },
43 "GET /invitations": { scope: "read" },
44 "DELETE /invitations/{id}": { scope: "admin" },
45 "POST /user/repos": { scope: "write" },
46 "POST /orgs/{org}/repos": { scope: "write" },
47 "GET /orgs/{org}/actions/secrets": { scope: "admin" },
48 "PUT /orgs/{org}/actions/secrets/{name}": { scope: "admin" },
49 "DELETE /orgs/{org}/actions/secrets/{name}": { scope: "admin" },
50 "POST /diffinery/tokens": { scope: "admin", sessionOnly: true },
51 "DELETE /diffinery/tokens/{id}": { scope: "admin" },
52 "POST /diffinery/agents": { scope: "admin", sessionOnly: true },
53 "POST /diffinery/agents/{login}/tokens": { scope: "admin", sessionOnly: true },
54 "DELETE /diffinery/agents/{login}": { scope: "admin" },
55 "PUT /notifications": { scope: "write" },
56 "PUT /diffinery/notification-settings": { scope: "write" },
57 "POST /diffinery/emails/test": { scope: "write" },
58 "POST /diffinery/admin/emails/samples": { scope: "admin" },
59 "PUT /user/avatar": { scope: "write" },
60 "DELETE /user/avatar": { scope: "write" },
61 "PUT /orgs/{org}/avatar": { scope: "write" },
62 "DELETE /orgs/{org}/avatar": { scope: "write" },
63 "POST /orgs/{org}/actions/variables": { scope: "admin" },
64 "PATCH /orgs/{org}/actions/variables/{name}": { scope: "admin" },
65 "DELETE /orgs/{org}/actions/variables/{name}": { scope: "admin" },
66 "POST /diffinery/imports/{id}/cancel": { scope: "write" },
67 "POST /diffinery/imports/{id}/retry": { scope: "write" },
68 "POST /diffinery/imports/{id}/selection": { scope: "write" },
69 "POST /diffinery/user/linked-accounts/github/challenge": { scope: "admin" },
70 "POST /diffinery/user/linked-accounts/github": { scope: "admin" },
71 "DELETE /diffinery/user/linked-accounts/{id}": { scope: "admin" },
72 "DELETE /diffinery/admin/linked-accounts/{id}": { scope: "admin" },
73};
74
75const page: Record<string, Field> = {
76 per_page: ["integer", "Results per page (default 30, at most 100)."],
77 page: ["integer", "Page number, starting at 1."],
78};
79
80export const API_DOCS: Record<string, OpDoc> = {
81 // ------------------------------------------------------------ meta, users and organizations
82 "GET /": { summary: "API root", description: "Names the API and links to these docs and the MCP endpoint.", access: "public" },
83 "GET /meta": { summary: "Instance metadata", description: "Whether this instance issues OIDC tokens to workflows.", access: "public" },
84 "GET /rate_limit": {
85 summary: "Get rate limit status",
86 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).",
87 access: "public",
88 },
89 "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" },
90 "PUT /user/avatar": {
91 summary: "Set your profile picture",
92 description:
93 "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.",
94 access: "account",
95 },
96 "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" },
97 "GET /users/{login}": { summary: "Get an account", description: "A user, organization or agent by login.", access: "account" },
98 "GET /user/orgs": { summary: "List your organizations", description: "Each organization carries your `role` in it (member, admin or owner).", access: "account" },
99 "POST /user/orgs": {
100 summary: "Create an organization",
101 access: "account",
102 status: 201,
103 body: { login: ["string", "Organization login (letters, digits, `-` and `_`; no dots).", true], name: ["string", "Display name (default: the login)."] },
104 },
105 "GET /orgs/{org}": {
106 summary: "Get an organization",
107 description: "GitHub's organization object. Members, the organization's agents, agents of members, and site admins also get `default_repository_permission`, the organization's base permission (`none`, `read`, `write` or `admin`).",
108 access: "account",
109 },
110 "PATCH /orgs/{org}": {
111 summary: "Update an organization",
112 description:
113 "Organization owners (and site admins) only; admins and members get 403. Returns the updated organization. Every field is checked before anything is saved, so a bad value (422) changes nothing. Changing the base permission applies to every member right away; owners and admins always keep admin. Audited.",
114 access: "org-owner",
115 body: {
116 default_repository_permission: ["string", "Base permission every member gets on the organization's repositories: `none`, `read` (the default for new organizations), `write` or `admin`."],
117 name: ["string", "Display name. An empty value clears it, and pages show the login instead."],
118 },
119 },
120 "GET /orgs/{org}/members": { summary: "List organization members", description: "Members only (or site admins). Each entry carries the member's `role`.", access: "account" },
121 "POST /orgs/{org}/invitations": {
122 summary: "Invite someone to an organization",
123 description: "Emails an invite link. The response holds the link too, so you can share it yourself. Refused with 422 when the address already has a pending invitation to the organization (letter case doesn't matter) or belongs to a member.",
124 access: "org-admin",
125 status: 201,
126 body: { email: ["string", "Email address to invite.", true], role: ["string", "`member` (default), `admin` or `owner`."] },
127 },
128 "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" },
129 "DELETE /orgs/{org}": {
130 summary: "Delete an organization",
131 description:
132 "Owners only (or site admins). Refused with 422 while the organization still owns repositories (transfer or delete them first), container images, or an import in progress. Its members, pending invitations, secrets and variables are removed, its agents disabled and their tokens revoked, and its login freed. Repositories transferred out of it keep redirecting from their old URLs. Audited.",
133 access: "org-admin",
134 status: 202,
135 },
136 "DELETE /orgs/{org}/invitations/{id}": {
137 summary: "Revoke an organization invitation",
138 description: "The link stops working and the address can be invited again. 404 when no pending invitation has this id. Audited.",
139 access: "org-admin",
140 status: 204,
141 },
142 "PUT /orgs/{org}/avatar": {
143 summary: "Set an organization's picture",
144 description:
145 "Diffinery only. Organization owners (and site admins) 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.",
146 access: "org-owner",
147 },
148 "DELETE /orgs/{org}/avatar": { summary: "Remove an organization's picture", description: "Diffinery only. Organization owners (and site admins) only. Goes back to generated initials and returns the `avatar_url`. Audited.", access: "org-owner" },
149 "POST /invitations": {
150 summary: "Invite someone to this instance",
151 description: "A site-wide invite with no organization. Site admins only.",
152 access: "site-admin",
153 status: 201,
154 body: { email: ["string", "Email address to invite.", true] },
155 },
156 "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" },
157 "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 },
158
159 // ------------------------------------------------------------ search
160 "GET /search/issues": {
161 summary: "Search issues and pull requests",
162 description:
163 "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.",
164 access: "public",
165 query: {
166 q: ["string", "Search query, at most 256 characters.", true],
167 sort: ["string", "`created` (default), `updated` or `comments`."],
168 order: ["string", "`desc` (default) or `asc`."],
169 ...page,
170 },
171 },
172 "GET /search/repositories": {
173 summary: "Search repositories",
174 description:
175 "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.",
176 access: "public",
177 query: {
178 q: ["string", "Search query, at most 256 characters.", true],
179 sort: ["string", "`updated` sorts by last push; anything else keeps the default order."],
180 order: ["string", "With `sort=updated`: `desc` (default) or `asc`."],
181 ...page,
182 },
183 },
184
185 // ------------------------------------------------------------ repositories
186 "GET /user/repos": { summary: "List repositories you can access", access: "account" },
187 "POST /user/repos": {
188 summary: "Create a repository for yourself",
189 description: "An agent's repositories belong to its owner.",
190 access: "account",
191 status: 201,
192 body: {
193 name: ["string", "Repository name.", true],
194 description: ["string", "Short description."],
195 visibility: ["string", "`private` (default) or `public`."],
196 default_branch: ["string", "Default branch name (default `main`)."],
197 },
198 },
199 "GET /users/{login}/repos": { summary: "List an account's repositories", description: "Only the ones you can see.", access: "account" },
200 "GET /orgs/{org}/repos": { summary: "List an organization's repositories", description: "Only the ones you can see.", access: "account" },
201 "POST /orgs/{org}/repos": {
202 summary: "Create an organization repository",
203 access: "account",
204 status: 201,
205 body: {
206 name: ["string", "Repository name.", true],
207 description: ["string", "Short description."],
208 visibility: ["string", "`private` (default) or `public`."],
209 default_branch: ["string", "Default branch name (default `main`)."],
210 },
211 },
212 "GET /repos/{owner}/{repo}": { summary: "Get a repository", description: "Includes your role (`permissions.role`) and repository statistics.", access: "read" },
213 "PATCH /repos/{owner}/{repo}": {
214 summary: "Update a repository",
215 access: "admin",
216 description: "Works on an archived repository too, so an admin can unarchive it with `archived: false`. Audited as `repo.update`, `repo.archive` or `repo.unarchive`. A new `name` renames the repository: its old name keeps redirecting (see the transfer endpoint), until a repository is created there again. 422 when the owner already has a repository with that name.",
217 body: {
218 name: ["string", "New name."],
219 description: ["string", "New description."],
220 visibility: ["string", "`private` or `public`."],
221 default_branch: ["string", "New default branch (must exist)."],
222 archived: ["boolean", "Archive (read-only) or unarchive."],
223 },
224 },
225 "DELETE /repos/{owner}/{repo}": { summary: "Delete a repository", description: "People only; agents can't delete repositories. Works on an archived repository too. Audited.", access: "admin", status: 204 },
226 "POST /repos/{owner}/{repo}/transfer": {
227 summary: "Transfer a repository",
228 description:
229 "Moves the repository to `new_owner`: the caller's own account, or an organization where the caller is an owner or admin. Needs admin access to the repository (a personal repository: its owner) and a token with the admin scope; agents, job tokens (`GITHUB_TOKEN`) and sandbox tokens are refused. 422 when the new owner already has a repository (or a container image of a moved image's name) with that name. Git data, issues, pull requests, Actions runs, secrets, variables, environments, webhooks and caches move with it, and so do container images linked to it. Direct collaborators keep their access; grants that came from the old organization (its members, their agents, its agents) are dropped, and the new owner's organization secrets and variables apply. The old owner/name keeps working for anyone who can read the repository, as on GitHub: API calls get a 301 (reads) or 307 (anything else) to the new URL, web pages a 301, Git (with a `This repository moved` notice on push) and MCP are served under the old name, and image names resolve to the moved images. Creating a repository at the old name ends the redirect. Admins of both owners get an email. Audited as `repo.transfer`. Returns the repository.",
230 access: "admin",
231 status: 202,
232 body: { new_owner: ["string", "Login of the user or organization to transfer to.", true], new_name: ["string", "A new name for the repository (default: keep its name)."] },
233 },
234 "GET /repos/{owner}/{repo}/collaborators": { summary: "List collaborators", description: "Each entry carries `role_name`.", access: "read" },
235 "PUT /repos/{owner}/{repo}/collaborators/{login}": {
236 summary: "Add or change a collaborator",
237 description: "Audited as `repo.collaborator.set`.",
238 access: "admin",
239 status: 204,
240 body: { permission: ["string", "`read`, `triage`, `write` (default), `maintain` or `admin`; GitHub's `pull` and `push` work too."] },
241 },
242 "DELETE /repos/{owner}/{repo}/collaborators/{login}": { summary: "Remove a collaborator", description: "Audited as `repo.collaborator.remove`.", access: "admin", status: 204 },
243 "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" },
244 "DELETE /repos/{owner}/{repo}/invitations/{id}": {
245 summary: "Revoke a repository invitation",
246 description: "The link stops working and the address can be invited again. 404 when no pending invitation to this repository has this id. Audited.",
247 access: "admin",
248 status: 204,
249 },
250 "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 } },
251 "GET /repos/{owner}/{repo}/assignees/{login}": {
252 summary: "Check if someone can be assigned",
253 description: "204 when `login` is the repository's owner or a collaborator with triage access or more, 404 otherwise.",
254 access: "read",
255 status: 204,
256 },
257 "GET /repos/{owner}/{repo}/languages": {
258 summary: "List languages",
259 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.",
260 access: "read",
261 },
262 "GET /repos/{owner}/{repo}/topics": { summary: "Get topics", description: "Returns `{ names }`.", access: "read" },
263 "PUT /repos/{owner}/{repo}/topics": {
264 summary: "Replace topics",
265 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.",
266 access: "admin",
267 body: { names: ["string[]", "Every topic the repository should have (`[]` clears them).", true] },
268 },
269 "POST /repos/{owner}/{repo}/dispatches": {
270 summary: "Trigger a repository_dispatch event",
271 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.",
272 access: "write",
273 status: 204,
274 body: {
275 event_type: ["string", "Event type, at most 100 characters.", true],
276 client_payload: ["object", "Data for workflows as `github.event.client_payload`: at most 10 top-level keys and 64 KB."],
277 },
278 },
279
280 // ------------------------------------------------------------ branches, tags, refs and commits
281 "GET /repos/{owner}/{repo}/branches": { summary: "List branches", description: "Each branch says whether it is `protected` and the `default`.", access: "read" },
282 "GET /repos/{owner}/{repo}/branches/{branch}": { summary: "Get a branch", access: "read" },
283 "GET /repos/{owner}/{repo}/tags": { summary: "List tags", access: "read" },
284 "POST /repos/{owner}/{repo}/merges": {
285 summary: "Merge a branch",
286 description:
287 "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.",
288 access: "write",
289 status: 201,
290 body: {
291 base: ["string", "Branch to merge into.", true],
292 head: ["string", "Branch, tag or commit to merge.", true],
293 commit_message: ["string", "Commit message (default: `Merge {head} into {base}`)."],
294 },
295 },
296 "GET /repos/{owner}/{repo}/git/refs/{path}": { summary: "List references", description: "Every ref under `refs/{path}`, for example `heads` or `tags/v1`.", access: "read" },
297 "POST /repos/{owner}/{repo}/git/refs": {
298 summary: "Create a reference",
299 description: "Branch protection and agent rules apply as they do to a push.",
300 access: "write",
301 status: 201,
302 body: { ref: ["string", "Full ref name, like `refs/heads/feature`.", true], sha: ["string", "Commit to point at.", true] },
303 },
304 "PATCH /repos/{owner}/{repo}/git/refs/{path}": {
305 summary: "Update a reference",
306 access: "write",
307 body: { sha: ["string", "New target commit.", true], force: ["boolean", "Allow a non-fast-forward update."] },
308 },
309 "DELETE /repos/{owner}/{repo}/git/refs/{path}": { summary: "Delete a reference", access: "write", status: 204 },
310 "GET /repos/{owner}/{repo}/commits": {
311 summary: "List commits",
312 access: "read",
313 query: { sha: ["string", "Branch, tag or commit to start from (default: the default branch)."], path: ["string", "Only commits that touch this path."], ...page },
314 },
315 "GET /repos/{owner}/{repo}/commits/{ref}": { summary: "Get a commit", description: "The commit with its diff.", access: "read" },
316 "GET /repos/{owner}/{repo}/commits/{sha}/pulls": {
317 summary: "List pull requests for a commit",
318 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.",
319 access: "read",
320 query: { ...page },
321 },
322 "GET /repos/{owner}/{repo}/compare/{spec}": { summary: "Compare two refs", description: "`spec` is `base...head`.", access: "read" },
323 "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" },
324
325 // ------------------------------------------------------------ contents
326 "GET /repos/{owner}/{repo}/contents": { summary: "List the root directory", access: "read", query: { ref: ["string", "Branch, tag or commit (default: the default branch)."] } },
327 "GET /repos/{owner}/{repo}/contents/{path}": {
328 summary: "Get a file or directory",
329 description: "A directory returns an array of entries; a file returns its content base64-encoded.",
330 access: "read",
331 query: { ref: ["string", "Branch, tag or commit (default: the default branch)."] },
332 },
333 "PUT /repos/{owner}/{repo}/contents/{path}": {
334 summary: "Create or update a file",
335 description: "Commits one file.",
336 access: "write",
337 status: 201,
338 body: { message: ["string", "Commit message.", true], content: ["string", "New content, base64-encoded.", true], branch: ["string", "Branch to commit to (default: the default branch)."] },
339 },
340 "DELETE /repos/{owner}/{repo}/contents/{path}": {
341 summary: "Delete a file",
342 access: "write",
343 body: { message: ["string", "Commit message.", true], branch: ["string", "Branch to commit to (default: the default branch)."] },
344 },
345 "POST /repos/{owner}/{repo}/diffinery/commits": {
346 summary: "Commit several files at once",
347 description:
348 "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. Push protection scans the new content as it does for `git push`: content that looks like a credential is refused with 422 and audited as `api.blocked_secret`. A repository admin who is a person can commit it anyway with an `allow_secrets` reason, audited as `api.secret_bypassed` and emailed to the repository's admins.",
349 access: "write",
350 status: 201,
351 body: {
352 branch: ["string", "Branch to commit to.", true],
353 message: ["string", "Commit message.", true],
354 files: ["array", "Changes: `{ path, content, encoding: \"utf-8\" | \"base64\", mode: \"100644\" | \"100755\" }` or `{ path, delete: true }`.", true],
355 base: ["string", "Create `branch` from this ref when it doesn't exist."],
356 expected_head_sha: ["string", "Fail with 409 if the branch has moved."],
357 allow_secrets: ["string", "Why a detected secret may be committed anyway (repository admins who are people; at most 200 characters are kept)."],
358 },
359 },
360 "GET /repos/{owner}/{repo}/readme": {
361 summary: "Get the README",
362 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.",
363 access: "read",
364 query: { ref: ["string", "Branch, tag or commit (default: the default branch)."] },
365 },
366 "GET /repos/{owner}/{repo}/readme/{dir}": {
367 summary: "Get a directory's README",
368 description: "Like the root README, for the directory `dir`.",
369 access: "read",
370 query: { ref: ["string", "Branch, tag or commit (default: the default branch)."] },
371 },
372 ...archiveDocs("tarball", "a tarball", "`.tar.gz`"),
373 ...archiveDocs("zipball", "a zip archive", "`.zip` (at most 65535 files)"),
374
375 // ------------------------------------------------------------ releases
376 "GET /repos/{owner}/{repo}/releases": { summary: "List releases", description: "Newest first. Drafts are included only for people with write access.", access: "read", query: { ...page } },
377 "POST /repos/{owner}/{repo}/releases": {
378 summary: "Create a release",
379 description:
380 "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).",
381 access: "write",
382 status: 201,
383 body: {
384 tag_name: ["string", "Tag to release.", true],
385 target_commitish: ["string", "Branch, tag or commit for a new tag (default: the default branch)."],
386 name: ["string", "Release title."],
387 body: ["string", "Markdown notes, at most 125000 characters."],
388 draft: ["boolean", "Save as a draft."],
389 prerelease: ["boolean", "Mark as a prerelease."],
390 make_latest: ["string", "`true` (default), `false`, or `legacy` (the newest by date)."],
391 generate_release_notes: ["boolean", "Append generated notes to `body`."],
392 },
393 },
394 "GET /repos/{owner}/{repo}/releases/{id}": { summary: "Get a release", description: "Drafts are 404 for people without write access.", access: "read" },
395 "PATCH /repos/{owner}/{repo}/releases/{id}": {
396 summary: "Update a release",
397 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.",
398 access: "write",
399 body: {
400 tag_name: ["string", "New tag."],
401 target_commitish: ["string", "Branch, tag or commit for a new tag."],
402 name: ["string", "Release title."],
403 body: ["string", "Markdown notes, at most 125000 characters."],
404 draft: ["boolean", "Draft or published."],
405 prerelease: ["boolean", "Mark as a prerelease."],
406 make_latest: ["string", "`true`, `false` or `legacy`."],
407 },
408 },
409 "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 },
410 "GET /repos/{owner}/{repo}/releases/{id}/assets": { summary: "List release assets", access: "read", query: { ...page } },
411 "GET /repos/{owner}/{repo}/releases/assets/{id}": {
412 summary: "Get a release asset",
413 description: "The asset's metadata, or the file itself with `Accept: application/octet-stream`.",
414 access: "read",
415 },
416 "PATCH /repos/{owner}/{repo}/releases/assets/{id}": {
417 summary: "Update a release asset",
418 description: "`state` is accepted and ignored.",
419 access: "write",
420 body: { name: ["string", "New file name (unique within the release)."], label: ["string", "Display label."] },
421 },
422 "DELETE /repos/{owner}/{repo}/releases/assets/{id}": { summary: "Delete a release asset", access: "write", status: 204 },
423 "POST /repos/{owner}/{repo}/releases/generate-notes": {
424 summary: "Generate release notes",
425 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 }`.",
426 access: "write",
427 body: {
428 tag_name: ["string", "Tag for the release.", true],
429 target_commitish: ["string", "Used when the tag doesn't exist yet (default: the default branch)."],
430 previous_tag_name: ["string", "Start from this tag (default: the newest earlier published release)."],
431 configuration_file_path: ["string", "Config file to use instead of `.github/release.yml`."],
432 },
433 },
434 "GET /repos/{owner}/{repo}/releases/latest": {
435 summary: "Get the latest release",
436 description: "The release marked latest, or else the newest published release that isn't a prerelease. 404 when there is none.",
437 access: "read",
438 },
439 "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" },
440 ...reactionDocs("/repos/{owner}/{repo}/releases/{id}", "a release", "Drafts are hidden from people without write access, and so are their reactions."),
441
442 // ------------------------------------------------------------ git database
443 "POST /repos/{owner}/{repo}/git/blobs": {
444 summary: "Create a blob",
445 description: "Push protection applies: content that looks like a credential is refused with 422. Returns `{ url, sha }`.",
446 access: "write",
447 status: 201,
448 body: { content: ["string", "The blob's content.", true], encoding: ["string", "`utf-8` (default) or `base64`."] },
449 },
450 "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" },
451 "POST /repos/{owner}/{repo}/git/trees": {
452 summary: "Create a tree",
453 description:
454 "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.",
455 access: "write",
456 status: 201,
457 body: {
458 tree: ["array", "Entries: `{ path, mode, sha }` or `{ path, mode, content }`. `mode` is `100644` (default), `100755`, `040000`, `160000` or `120000`.", true],
459 base_tree: ["string", "Tree to start from (default: an empty tree)."],
460 },
461 },
462 "GET /repos/{owner}/{repo}/git/trees/{sha}": {
463 summary: "Get a tree",
464 description: "`sha` may be a tree, a commit or a ref. `truncated` is true when a recursive listing stopped at 100000 entries.",
465 access: "read",
466 query: { recursive: ["string", "Any value but `0` or `false` lists every entry below, not just the top level."] },
467 },
468 "POST /repos/{owner}/{repo}/git/commits": {
469 summary: "Create a Git commit",
470 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.",
471 access: "write",
472 status: 201,
473 body: {
474 message: ["string", "Commit message.", true],
475 tree: ["string", "SHA of the commit's tree.", true],
476 parents: ["string[]", "Parent commit SHAs (none for a root commit)."],
477 author: ["object", "`{ name, email, date }`."],
478 committer: ["object", "`{ name, email, date }` (default: the author)."],
479 signature: ["string", "Signature to store in the commit as given."],
480 },
481 },
482 "GET /repos/{owner}/{repo}/git/commits/{sha}": { summary: "Get a Git commit", access: "read" },
483 "POST /repos/{owner}/{repo}/git/tags": {
484 summary: "Create a tag object",
485 description: "Writes an annotated tag object only; create its `refs/tags/...` reference with `POST /repos/{owner}/{repo}/git/refs` to publish it.",
486 access: "write",
487 status: 201,
488 body: {
489 tag: ["string", "Tag name.", true],
490 object: ["string", "SHA of the object to tag.", true],
491 message: ["string", "Tag message."],
492 type: ["string", "`commit`, `tree`, `blob` or `tag`; must match the object (default: its type)."],
493 tagger: ["object", "`{ name, email, date }` (default: you)."],
494 },
495 },
496 "GET /repos/{owner}/{repo}/git/tags/{sha}": { summary: "Get a tag object", description: "`verification.verified` is always false.", access: "read" },
497 "GET /repos/{owner}/{repo}/git/ref/{ref}": {
498 summary: "Get a reference",
499 description: "`ref` is the name after `refs/`, like `heads/main` or `tags/v1.0`, and must match exactly.",
500 access: "read",
501 },
502 "GET /repos/{owner}/{repo}/git/matching-refs/{ref}": {
503 summary: "List matching references",
504 description: "Refs whose names start with `refs/{ref}`, like `heads/feature` or `tags/v1.`. Pull request refs appear only when `ref` starts with `pull/`.",
505 access: "read",
506 query: { per_page: ["integer", "Results per page (default 100, at most 100)."], page: page.page! },
507 },
508 "GET /repos/{owner}/{repo}/git/refs": {
509 summary: "List all references",
510 description: "Every branch, tag and other ref, without pull request refs.",
511 access: "read",
512 query: { per_page: ["integer", "Results per page (default 100, at most 100)."], page: page.page! },
513 },
514
515 // ------------------------------------------------------------ branch protection
516 "GET /repos/{owner}/{repo}/branches/{branch}/protection": { summary: "Get a protection rule", description: "`branch` is the rule's pattern.", access: "admin" },
517 "PUT /repos/{owner}/{repo}/branches/{branch}/protection": {
518 summary: "Create or replace a protection rule",
519 description:
520 "`branch` is a branch name or pattern. People only: agents and workflow tokens can't change protection. `allowAgentAutoMerge` (boolean) lets agents land pull requests through auto-merge or the merge queue; turning it on or off is audited as `repo.protection.agent_auto_merge`. `agentAutoMergeWithoutApproval` (boolean, needs `allowAgentAutoMerge`) lets those landings go through on green required checks without a person's approval, audited as `repo.protection.agent_approval_waiver`. See the branch protection guide for every field.",
521 access: "admin",
522 },
523 "DELETE /repos/{owner}/{repo}/branches/{branch}/protection": { summary: "Delete a protection rule", description: "Audited as `repo.protection.delete`.", access: "admin", status: 204 },
524
525 // ------------------------------------------------------------ issues
526 "GET /repos/{owner}/{repo}/issues": {
527 summary: "List issues and pull requests",
528 description: "As on GitHub, this list includes pull requests. The total is in the `X-Total-Count` header.",
529 access: "read",
530 query: { state: ["string", "`open` (default), `closed` or `all`."], labels: ["string", "Only items with this label."], creator: ["string", "Only items opened by this login."], milestone: ["string", "A milestone number, `none` (no milestone) or `*` (any milestone)."], q: ["string", "Text search in titles."], ...page },
531 },
532 "POST /repos/{owner}/{repo}/issues": {
533 summary: "Create an issue",
534 description: "Needs a signed-in caller, also on a public repository. As on GitHub, `labels` and `assignees` from a caller without triage access are dropped without an error.",
535 access: "read",
536 status: 201,
537 body: { title: ["string", "Title.", true], body: ["string", "Markdown body."], labels: ["string[]", "Label names."], assignees: ["string[]", "Logins."] },
538 },
539 "GET /repos/{owner}/{repo}/issues/{number}": { summary: "Get an issue", access: "read" },
540 "PATCH /repos/{owner}/{repo}/issues/{number}": {
541 summary: "Update an issue",
542 description: "The author can edit their own issue or pull request. As on GitHub, `labels`, `assignees` and `milestone` from a caller without triage access are dropped without an error.",
543 access: "read",
544 body: {
545 title: ["string", "New title."],
546 body: ["string", "New body."],
547 state: ["string", "`open` or `closed`."],
548 state_reason: ["string", "Why it was closed, like `completed` or `not_planned`."],
549 labels: ["string[]", "Replace the labels."],
550 assignees: ["string[]", "Replace the assignees."],
551 milestone: ["integer", "Milestone number, or `null` to remove the milestone. Ignored without triage access. Checked before anything is saved: 422 when it isn't a whole number or `null`, or when no milestone has that number."],
552 },
553 },
554 "GET /repos/{owner}/{repo}/milestones": {
555 summary: "List milestones",
556 description: "Every milestone in the state you ask for (no paging), ordered by due date (milestones without one last), then by number. The `id` is the milestone number. Milestones come from GitHub imports or are created with the endpoint below; the web UI sets an issue's milestone but doesn't create milestones.",
557 access: "read",
558 query: { state: ["string", "`open` (default), `closed` or `all`. Anything else is 422."] },
559 },
560 "GET /repos/{owner}/{repo}/milestones/{number}": { summary: "Get a milestone", description: "`number` is the milestone number. 404 `milestone not found`.", access: "read" },
561 "POST /repos/{owner}/{repo}/milestones": {
562 summary: "Create a milestone",
563 description: "New milestones are open unless `state` is `closed`. There is no endpoint to delete a milestone.",
564 access: "triage",
565 status: 201,
566 body: {
567 title: ["string", "1 to 255 characters.", true],
568 description: ["string", "At most 65,536 characters."],
569 state: ["string", "`open` (default) or `closed`."],
570 due_on: ["string", "ISO 8601 timestamp, or `null`."],
571 },
572 },
573 "PATCH /repos/{owner}/{repo}/milestones/{number}": {
574 summary: "Update a milestone",
575 description: "Send only the fields you change. Closing records `closed_at`; reopening clears it.",
576 access: "triage",
577 body: {
578 title: ["string", "1 to 255 characters."],
579 description: ["string", "At most 65,536 characters."],
580 state: ["string", "`open` or `closed`."],
581 due_on: ["string", "ISO 8601 timestamp, or `null` to clear it."],
582 },
583 },
584 "GET /repos/{owner}/{repo}/issues/{number}/comments": {
585 summary: "List comments",
586 description: "Conversation comments on an issue or pull request (not review comments).",
587 access: "read",
588 query: { since: ["string", "Only comments updated at or after this ISO timestamp."], ...page },
589 },
590 "POST /repos/{owner}/{repo}/issues/{number}/comments": { summary: "Add a comment", access: "read", status: 201, body: { body: ["string", "Markdown body.", true] } },
591 "GET /repos/{owner}/{repo}/issues/comments": {
592 summary: "List comments in a repository",
593 description: "Conversation comments on every issue and pull request.",
594 access: "read",
595 query: {
596 since: ["string", "Only comments updated at or after this ISO timestamp."],
597 sort: ["string", "`created` (default) or `updated`."],
598 direction: ["string", "`asc` (default) or `desc`."],
599 ...page,
600 },
601 },
602 "GET /repos/{owner}/{repo}/issues/comments/{id}": { summary: "Get a comment", description: "`id` is the numeric id or Diffinery's own.", access: "read" },
603 "PATCH /repos/{owner}/{repo}/issues/comments/{id}": { summary: "Edit a comment", access: "read", body: { body: ["string", "New Markdown body.", true] } },
604 "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 },
605 ...reactionDocs("/repos/{owner}/{repo}/issues/{number}", "an issue", "Works for pull requests too."),
606 ...reactionDocs("/repos/{owner}/{repo}/issues/comments/{id}", "a comment"),
607 "POST /repos/{owner}/{repo}/issues/{number}/assignees": {
608 summary: "Add assignees",
609 description: "Adds to the current assignees, at most 10 in all. Logins aren't checked against who can be assigned. Returns the issue.",
610 access: "triage",
611 status: 201,
612 body: { assignees: ["string[]", "Logins to add."] },
613 },
614 "DELETE /repos/{owner}/{repo}/issues/{number}/assignees": { summary: "Remove assignees", description: "Returns the issue.", access: "triage", body: { assignees: ["string[]", "Logins to remove."] } },
615 "PUT /repos/{owner}/{repo}/issues/{number}/lock": {
616 summary: "Lock a conversation",
617 description: "While locked, only people with triage access can comment, review or react.",
618 access: "triage",
619 status: 204,
620 body: { lock_reason: ["string", "`off-topic`, `too heated`, `resolved` or `spam`."] },
621 },
622 "DELETE /repos/{owner}/{repo}/issues/{number}/lock": { summary: "Unlock a conversation", access: "triage", status: 204 },
623 "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 } },
624 "GET /repos/{owner}/{repo}/issues/{number}/timeline": {
625 summary: "List an issue's timeline",
626 description: "Events, comments (`commented`) and reviews (`reviewed`) together, oldest first.",
627 access: "read",
628 query: { ...page },
629 },
630 "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 } },
631 "GET /repos/{owner}/{repo}/issues/events/{id}": { summary: "Get an event", access: "read" },
632 "GET /repos/{owner}/{repo}/labels": { summary: "List labels", access: "read" },
633 "POST /repos/{owner}/{repo}/labels": {
634 summary: "Create a label",
635 description: "422 when a label with that name exists; change one with `PATCH /repos/{owner}/{repo}/labels/{name}`.",
636 access: "triage",
637 status: 201,
638 body: { name: ["string", "Label name, at most 100 characters.", true], color: ["string", "6-digit hex color, with or without `#` (default `ededed`)."], description: ["string", "Description."] },
639 },
640 "GET /repos/{owner}/{repo}/labels/{name}": { summary: "Get a label", access: "read" },
641 "PATCH /repos/{owner}/{repo}/labels/{name}": {
642 summary: "Update a label",
643 description: "Renaming it renames it on every issue too.",
644 access: "triage",
645 body: { new_name: ["string", "New name."], color: ["string", "6-digit hex color, with or without `#`."], description: ["string", "Description."] },
646 },
647 "DELETE /repos/{owner}/{repo}/labels/{name}": { summary: "Delete a label", description: "Also removes it from every issue.", access: "triage", status: 204 },
648 "GET /repos/{owner}/{repo}/issues/{number}/labels": { summary: "List an issue's labels", access: "read", query: { ...page } },
649 "POST /repos/{owner}/{repo}/issues/{number}/labels": {
650 summary: "Add labels to an issue",
651 description: "Labels that don't exist yet are created. At most 100 per issue. Returns the issue's labels.",
652 access: "triage",
653 body: { labels: ["string[]", "Label names (`{ name }` objects, or a bare array as the body, work too).", true] },
654 },
655 "PUT /repos/{owner}/{repo}/issues/{number}/labels": {
656 summary: "Replace an issue's labels",
657 description: "Labels that don't exist yet are created; `[]` removes them all. Returns the issue's labels.",
658 access: "triage",
659 body: { labels: ["string[]", "Label names (`{ name }` objects, or a bare array as the body, work too).", true] },
660 },
661 "DELETE /repos/{owner}/{repo}/issues/{number}/labels": { summary: "Remove all labels from an issue", access: "triage", status: 204 },
662 "DELETE /repos/{owner}/{repo}/issues/{number}/labels/{name}": {
663 summary: "Remove a label from an issue",
664 description: "Returns the labels left. 404 when the issue doesn't have it.",
665 access: "triage",
666 },
667
668 // ------------------------------------------------------------ pull requests
669 "GET /repos/{owner}/{repo}/pulls": {
670 summary: "List pull requests",
671 description: "The total is in the `X-Total-Count` header.",
672 access: "read",
673 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 },
674 },
675 "POST /repos/{owner}/{repo}/pulls": {
676 summary: "Open a pull request",
677 description: "A workflow's `GITHUB_TOKEN` can open one only when the repository allows workflows to create pull requests (`can_approve_pull_request_reviews`, see `PUT /repos/{owner}/{repo}/actions/permissions/workflow`); otherwise 403.",
678 access: "write",
679 status: 201,
680 body: {
681 title: ["string", "Title.", true],
682 head: ["string", "Branch with the changes (`owner:branch` also works).", true],
683 base: ["string", "Branch to merge into (default: the default branch)."],
684 body: ["string", "Markdown description."],
685 intent: ["string", "Diffinery: what the change is meant to do, shown to reviewers."],
686 draft: ["boolean", "Open as a draft."],
687 reviewers: ["string[]", "Logins to request reviews from."],
688 },
689 },
690 "GET /repos/{owner}/{repo}/pulls/{number}": {
691 summary: "Get a pull request",
692 description: "Includes `mergeable`, `mergeable_state`, `merge_blockers` (why it can't merge yet), `approvals`, `required_approvals` and `checks`.",
693 access: "read",
694 },
695 "PATCH /repos/{owner}/{repo}/pulls/{number}": {
696 summary: "Update a pull request",
697 description:
698 "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. It records a `ready_for_review` or `convert_to_draft` timeline event and sends the `pull_request` webhook with the action `ready_for_review` or `converted_to_draft`.",
699 access: "read",
700 body: {
701 title: ["string", "New title."],
702 body: ["string", "New description."],
703 state: ["string", "`open` or `closed`."],
704 draft: ["boolean", "`false` marks it ready for review; `true` converts it back to a draft."],
705 },
706 },
707 "PUT /repos/{owner}/{repo}/pulls/{number}/update-branch": {
708 summary: "Update a pull request's branch",
709 description:
710 "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.",
711 access: "write",
712 status: 202,
713 body: { expected_head_sha: ["string", "Fail unless the head branch is at this commit."] },
714 },
715 "GET /repos/{owner}/{repo}/pulls/{number}/files": { summary: "List changed files", description: "Each file with its status, additions, deletions and patch.", access: "read" },
716 "GET /repos/{owner}/{repo}/pulls/{number}/commits": { summary: "List a pull request's commits", access: "read" },
717 "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" },
718 "PUT /repos/{owner}/{repo}/pulls/{number}/merge": {
719 summary: "Merge a pull request",
720 description:
721 "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 (without `merge_method`, such a branch squashes). 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 or a workflow) can bypass, when the rule doesn't include administrators, there are no conflicts and the pull request isn't a draft. Each flag covers only what it names: `bypass_rules: true` skips unmet rules and linear history, and `bypass_merge_queue: true` skips only the queue. A bypass that skipped an unmet rule, linear history or the queue is audited as `pull_request.merge_bypassed`. Agents can't merge into the default branch or a protected branch themselves (unless the rule sets `agentsMayMerge`); where it sets `allowAgentAutoMerge` they turn on auto-merge (`PUT .../auto-merge`) instead.",
722 access: "write",
723 body: {
724 merge_method: ["string", "`merge`, `squash` or `rebase`."],
725 commit_title: ["string", "Title of the merge or squash commit."],
726 commit_message: ["string", "Body of the merge or squash commit."],
727 sha: ["string", "Fail if the head has moved from this commit."],
728 delete_branch: ["boolean", "Delete the head branch after merging."],
729 bypass_merge_queue: ["boolean", "Merge directly even when a queue is required (admins). Doesn't skip unmet rules; send `bypass_rules` too for that."],
730 bypass_rules: ["boolean", "Merge even though protection rules (including linear history) aren't met (admins). Without it, an admin's merge is blocked like anyone else's."],
731 },
732 },
733 "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" },
734 "PUT /repos/{owner}/{repo}/pulls/{number}/merge-queue": {
735 summary: "Add a pull request to the merge queue",
736 description: "Diffinery only. Returns 201 with the entry when queued.",
737 access: "write",
738 status: 201,
739 body: { merge_method: ["string", "`merge`, `squash` or `rebase` (default: the queue's method)."], delete_branch: ["boolean", "Delete the head branch once merged."] },
740 },
741 "DELETE /repos/{owner}/{repo}/pulls/{number}/merge-queue": { summary: "Remove a pull request from the merge queue", access: "write", status: 204 },
742 "PUT /repos/{owner}/{repo}/pulls/{number}/auto-merge": {
743 summary: "Enable auto-merge",
744 description:
745 "Diffinery's REST form of GitHub's `enablePullRequestAutoMerge`. Diffinery merges the pull request by itself as soon as every rule of its base branch is met (required checks on the head, approvals from people, linear history), as the caller, never bypassing anything; into a branch with a merge queue it adds it to the queue instead. Returns `{ state, auto_merge, merge_commit_sha }`: `state` is `enabled` (waiting), `queued` or `merged` (it was ready). The pull request's `auto_merge` shows it until then. A push to the head branch by anyone else, or converting the pull request to a draft, turns it off. 422 for a draft, a closed or merged pull request, or one already in the merge queue. Agents and workflow tokens may use it on the default branch or a protected one only when its rule sets `allowAgentAutoMerge` (else 403) and has at least one required check (else 422); a workflow token needs `contents: write`. Audited as `pull_request.auto_merge_enabled`, and the merge as `pull_request.auto_merged`.",
746 access: "write",
747 body: {
748 merge_method: ["string", "`merge`, `squash` or `rebase` (default: the queue's method, `squash` where linear history is required, else `merge`)."],
749 commit_title: ["string", "Title of the merge or squash commit."],
750 commit_message: ["string", "Body of the merge or squash commit."],
751 },
752 },
753 "DELETE /repos/{owner}/{repo}/pulls/{number}/auto-merge": {
754 summary: "Disable auto-merge",
755 description: "GitHub's `disablePullRequestAutoMerge`. Anyone with write access may turn it off, whoever turned it on; a workflow token needs `contents: write`. 204 even when it was off, which records nothing. Otherwise it records an `auto_merge_disabled` event, sends a `pull_request` webhook with that action, and is audited as `pull_request.auto_merge_disabled`.",
756 access: "write",
757 status: 204,
758 },
759 "GET /repos/{owner}/{repo}/pulls/{number}/reviews": { summary: "List reviews", access: "read" },
760 "POST /repos/{owner}/{repo}/pulls/{number}/reviews": {
761 summary: "Submit a review",
762 description:
763 "Only people's approvals count toward required approvals: agents' and workflows' don't. A workflow's `GITHUB_TOKEN` can approve or request changes only when the repository allows it (`can_approve_pull_request_reviews`); otherwise 403.",
764 access: "read",
765 status: 201,
766 body: {
767 event: ["string", "`APPROVE`, `REQUEST_CHANGES` or `COMMENT` (default)."],
768 body: ["string", "Review summary."],
769 comments: ["array", "Line comments: `{ path, line, side: \"LEFT\" | \"RIGHT\", body }`."],
770 },
771 },
772 "GET /repos/{owner}/{repo}/pulls/{number}/reviews/{id}": { summary: "Get a review", description: "`id` is the numeric id or Diffinery's own.", access: "read" },
773 "PUT /repos/{owner}/{repo}/pulls/{number}/reviews/{id}/dismissals": {
774 summary: "Dismiss a review",
775 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.",
776 access: "maintain",
777 body: { message: ["string", "Why the review is dismissed.", true] },
778 },
779 "GET /repos/{owner}/{repo}/pulls/{number}/comments": { summary: "List review comments", description: "Line comments from reviews.", access: "read", query: { ...page } },
780 "POST /repos/{owner}/{repo}/pulls/{number}/comments": {
781 summary: "Create a review comment",
782 description:
783 "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.",
784 access: "read",
785 status: 201,
786 body: {
787 body: ["string", "Markdown body.", true],
788 path: ["string", "File path (required unless replying)."],
789 line: ["integer", "Line number (required unless replying)."],
790 side: ["string", "`RIGHT` (default) or `LEFT`."],
791 position: ["integer", "Used as `line` when `line` is missing."],
792 commit_id: ["string", "Accepted and ignored."],
793 in_reply_to: ["integer", "Review comment id to reply to; its path and line are used."],
794 },
795 },
796 "POST /repos/{owner}/{repo}/pulls/{number}/comments/{id}/replies": {
797 summary: "Reply to a review comment",
798 description: "Adds a reply on the same line, as its own `COMMENTED` review.",
799 access: "read",
800 status: 201,
801 body: { body: ["string", "Markdown body.", true] },
802 },
803 "GET /repos/{owner}/{repo}/pulls/comments": {
804 summary: "List review comments in a repository",
805 access: "read",
806 query: {
807 since: ["string", "Only comments updated at or after this ISO timestamp."],
808 sort: ["string", "`created` (default) or `updated`."],
809 direction: ["string", "`asc` (default) or `desc`."],
810 ...page,
811 },
812 },
813 "GET /repos/{owner}/{repo}/pulls/comments/{id}": { summary: "Get a review comment", access: "read" },
814 "PATCH /repos/{owner}/{repo}/pulls/comments/{id}": {
815 summary: "Edit a review comment",
816 description: "Your own comments, or anyone's with maintain access.",
817 access: "read",
818 body: { body: ["string", "New Markdown body.", true] },
819 },
820 "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 },
821 ...reactionDocs("/repos/{owner}/{repo}/pulls/comments/{id}", "a review comment"),
822 "GET /repos/{owner}/{repo}/pulls/{number}/requested_reviewers": { summary: "List requested reviewers", description: "Returns `{ users, teams }`; `teams` is always empty.", access: "read" },
823 "POST /repos/{owner}/{repo}/pulls/{number}/requested_reviewers": {
824 summary: "Request reviewers",
825 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.",
826 access: "read",
827 status: 201,
828 body: { reviewers: ["string[]", "Logins."], team_reviewers: ["string[]", "Must be empty or absent."] },
829 },
830 "DELETE /repos/{owner}/{repo}/pulls/{number}/requested_reviewers": {
831 summary: "Remove requested reviewers",
832 description: "The author, or anyone with triage access. Teams aren't supported. Returns the pull request.",
833 access: "read",
834 body: { reviewers: ["string[]", "Logins to remove."] },
835 },
836
837 // ------------------------------------------------------------ commit statuses
838 "POST /repos/{owner}/{repo}/statuses/{sha}": {
839 summary: "Create a commit status",
840 description:
841 "`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).",
842 access: "write",
843 status: 201,
844 body: {
845 state: ["string", "`error`, `failure`, `pending` or `success`.", true],
846 target_url: ["string", "http(s) link to details."],
847 description: ["string", "Short description (cut at 1024 characters)."],
848 context: ["string", "Label that tells statuses apart (default `default`)."],
849 },
850 },
851 "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 } },
852 "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 } },
853 "GET /repos/{owner}/{repo}/commits/{ref}/status": {
854 summary: "Get the combined status",
855 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.",
856 access: "read",
857 query: { per_page: ["integer", "Statuses per page (default 100, at most 100)."], page: page.page! },
858 },
859
860 // ------------------------------------------------------------ checks
861 "GET /repos/{owner}/{repo}/commits/{ref}/check-runs": { summary: "List check runs for a commit", access: "read" },
862 "GET /repos/{owner}/{repo}/check-runs/{id}": { summary: "Get a check run", description: "`id` is the numeric id, Diffinery's own, or the id of the Actions job that reports it (a job's `check_run_url` points here).", access: "read" },
863 "PATCH /repos/{owner}/{repo}/check-runs/{id}": {
864 summary: "Update a check run",
865 description:
866 "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).",
867 access: "write",
868 body: {
869 name: ["string", "New name."],
870 status: ["string", "`queued`, `in_progress`, `completed`, `waiting`, `requested` or `pending`."],
871 conclusion: ["string", "`action_required`, `cancelled`, `failure`, `neutral`, `success`, `skipped`, `stale` or `timed_out`."],
872 details_url: ["string", "http(s) link to more detail."],
873 external_id: ["string", "Your own reference for the run."],
874 started_at: ["string", "ISO timestamp."],
875 completed_at: ["string", "ISO timestamp."],
876 output: ["object", "`{ title, summary, text, annotations }`."],
877 },
878 },
879 "GET /repos/{owner}/{repo}/check-runs/{id}/annotations": { summary: "List check run annotations", access: "read", query: { ...page } },
880 "GET /repos/{owner}/{repo}/commits/{ref}/check-suites": {
881 summary: "List check suites for a commit",
882 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.",
883 access: "read",
884 query: { ...page },
885 },
886 "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" },
887 "GET /repos/{owner}/{repo}/check-suites/{id}/check-runs": { summary: "List a check suite's check runs", access: "read", query: { ...page } },
888 "POST /repos/{owner}/{repo}/check-runs": {
889 summary: "Create or update a check run",
890 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.",
891 access: "write",
892 status: 201,
893 body: {
894 name: ["string", "Check name.", true],
895 head_sha: ["string", "Commit the check is for.", true],
896 status: ["string", "`queued` (default), `in_progress` or `completed`."],
897 conclusion: ["string", "With `completed`: `success`, `failure`, `neutral`, `cancelled`, `skipped` or `timed_out`."],
898 details_url: ["string", "Link to more detail."],
899 output: ["object", "`{ title, summary }`."],
900 },
901 },
902
903 // ------------------------------------------------------------ deployments
904 "GET /repos/{owner}/{repo}/deployments": {
905 summary: "List deployments",
906 description: "Newest first. Each filter matches exactly.",
907 access: "read",
908 query: {
909 sha: ["string", "Full commit SHA."],
910 ref: ["string", "Ref, as given when the deployment was created."],
911 task: ["string", "Task name."],
912 environment: ["string", "Environment name."],
913 ...page,
914 },
915 },
916 "POST /repos/{owner}/{repo}/deployments": {
917 summary: "Create a deployment",
918 description:
919 "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. Environment names are case-insensitive: an existing environment keeps its spelling, and any spelling of `production` is a production environment. `auto_merge` is ignored. See [Environments](/docs/actions/environments).",
920 access: "write",
921 status: 201,
922 body: {
923 ref: ["string", "Branch, tag or commit to deploy.", true],
924 task: ["string", "Task name (default `deploy`)."],
925 payload: ["object", "Extra data for the deployer (a string is kept as is)."],
926 environment: ["string", "Environment name (default `production`)."],
927 description: ["string", "Short description."],
928 transient_environment: ["boolean", "Transient deployments aren't made inactive by later successful ones."],
929 production_environment: ["boolean", "Default: true when the environment is `production`."],
930 required_contexts: ["string[]", "Contexts that must have passed; `[]` skips the check."],
931 },
932 },
933 "GET /repos/{owner}/{repo}/deployments/{id}": { summary: "Get a deployment", access: "read" },
934 "DELETE /repos/{owner}/{repo}/deployments/{id}": {
935 summary: "Delete a deployment",
936 description: "Refused with 422 while its latest status is `success`, unless it is the only deployment in its environment. Deletes its statuses too.",
937 access: "write",
938 status: 204,
939 },
940 "GET /repos/{owner}/{repo}/deployments/{id}/statuses": { summary: "List deployment statuses", description: "Newest first.", access: "read", query: { ...page } },
941 "POST /repos/{owner}/{repo}/deployments/{id}/statuses": {
942 summary: "Create a deployment status",
943 description:
944 "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.",
945 access: "write",
946 status: 201,
947 body: {
948 state: ["string", "`error`, `failure`, `inactive`, `in_progress`, `queued`, `pending` or `success`.", true],
949 log_url: ["string", "http(s) link to the deployment's output."],
950 target_url: ["string", "Older name for `log_url`, used when `log_url` is missing."],
951 environment_url: ["string", "http(s) link to the deployed environment."],
952 description: ["string", "Short description (cut at 140 characters)."],
953 environment: ["string", "Move the deployment to this environment."],
954 auto_inactive: ["boolean", "`false` keeps older deployments active (default `true`)."],
955 },
956 },
957 "GET /repos/{owner}/{repo}/deployments/{id}/statuses/{status}": { summary: "Get a deployment status", access: "read" },
958
959 // ------------------------------------------------------------ environments
960 "GET /repos/{owner}/{repo}/environments": { summary: "List environments", access: "read", query: { ...page } },
961 "GET /repos/{owner}/{repo}/environments/{env}": { summary: "Get an environment", access: "read" },
962 "PUT /repos/{owner}/{repo}/environments/{env}": {
963 summary: "Create or update an environment",
964 description:
965 "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).",
966 access: "admin",
967 body: {
968 deployment_branch_policy: ["object", "`{ protected_branches, custom_branch_policies }` with exactly one true, or `null` to allow every ref."],
969 wait_timer: ["integer", "Must be 0 or absent."],
970 reviewers: ["array", "Must be empty or absent."],
971 },
972 },
973 "DELETE /repos/{owner}/{repo}/environments/{env}": { summary: "Delete an environment", description: "Deletes its secrets and variables too. People only. Audited.", access: "admin", status: 204 },
974 "GET /repos/{owner}/{repo}/environments/{env}/deployment-branch-policies": {
975 summary: "List deployment branch policies",
976 description: "The branch and tag patterns of an environment with `custom_branch_policies`.",
977 access: "read",
978 query: { ...page },
979 },
980 "POST /repos/{owner}/{repo}/environments/{env}/deployment-branch-policies": {
981 summary: "Create a deployment branch policy",
982 description: "The environment must have `custom_branch_policies`. Tag patterns never match tags last pushed by an agent. People only. Audited.",
983 access: "admin",
984 body: { name: ["string", "Branch or tag name pattern, like `release/*`.", true], type: ["string", "`branch` (default) or `tag`."] },
985 },
986 "DELETE /repos/{owner}/{repo}/environments/{env}/deployment-branch-policies/{id}": {
987 summary: "Delete a deployment branch policy",
988 description: "People only. Audited.",
989 access: "admin",
990 status: 204,
991 },
992 "GET /repos/{owner}/{repo}/environments/{env}/secrets": {
993 summary: "List environment secrets",
994 description: "Names and dates only; values are never returned.",
995 access: "admin",
996 query: { ...page },
997 },
998 "GET /repos/{owner}/{repo}/environments/{env}/secrets/{name}": { summary: "Get an environment secret", description: "Name and dates only.", access: "admin" },
999 "PUT /repos/{owner}/{repo}/environments/{env}/secrets/{name}": {
1000 summary: "Set an environment secret",
1001 description:
1002 "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.",
1003 access: "admin",
1004 status: 201,
1005 body: { value: ["string", "Secret value.", true] },
1006 },
1007 "DELETE /repos/{owner}/{repo}/environments/{env}/secrets/{name}": { summary: "Delete an environment secret", description: "People only. Audited.", access: "admin", status: 204 },
1008 "GET /repos/{owner}/{repo}/environments/{env}/variables": {
1009 summary: "List environment variables",
1010 access: "write",
1011 query: { per_page: ["integer", "Results per page (default 10, at most 100)."], page: page.page! },
1012 },
1013 "GET /repos/{owner}/{repo}/environments/{env}/variables/{name}": { summary: "Get an environment variable", access: "write" },
1014 "POST /repos/{owner}/{repo}/environments/{env}/variables": {
1015 summary: "Create an environment variable",
1016 description: "409 when it exists. Names are upper-cased, and `GITHUB_` names are refused. People only. Audited as `environment.variable.set`.",
1017 access: "admin",
1018 status: 201,
1019 body: { name: ["string", "Variable name.", true], value: ["string", "Variable value.", true] },
1020 },
1021 "PATCH /repos/{owner}/{repo}/environments/{env}/variables/{name}": {
1022 summary: "Update an environment variable",
1023 description: "People only. Renaming onto a name that exists fails with 409. Audited as `environment.variable.set`.",
1024 access: "admin",
1025 status: 204,
1026 body: { name: ["string", "New name (renames the variable)."], value: ["string", "New value."] },
1027 },
1028 "DELETE /repos/{owner}/{repo}/environments/{env}/variables/{name}": { summary: "Delete an environment variable", description: "People only. Audited as `environment.variable.delete`.", access: "admin", status: 204 },
1029
1030 // ------------------------------------------------------------ actions
1031 "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" },
1032 "GET /repos/{owner}/{repo}/actions/workflows/{workflow}": {
1033 summary: "Get a workflow",
1034 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.",
1035 access: "read",
1036 },
1037 "GET /repos/{owner}/{repo}/actions/workflows/{workflow}/runs": {
1038 summary: "List a workflow's runs",
1039 description: "No `Link` header; page with `total_count`.",
1040 access: "read",
1041 query: {
1042 branch: ["string", "Head branch."],
1043 status: ["string", "Run status or conclusion."],
1044 event: ["string", "Triggering event."],
1045 head_sha: ["string", "Head commit."],
1046 ...page,
1047 },
1048 },
1049 "POST /repos/{owner}/{repo}/actions/workflows/{workflow}/dispatches": {
1050 summary: "Run a workflow (workflow_dispatch)",
1051 description: "`workflow` is the file path or name, URL-encoded, like `.github%2Fworkflows%2Fdeploy.yml` or `deploy.yml`. Returns the new run's id.",
1052 access: "write",
1053 status: 201,
1054 body: { ref: ["string", "Branch or tag to run on.", true], inputs: ["object", "Values for `on.workflow_dispatch.inputs`."] },
1055 },
1056 "POST /repos/{owner}/{repo}/actions/runners/claim": {
1057 summary: "Claim a job for a self-hosted runner",
1058 description: "Used by `diffinery runner`. Returns 204 when no queued job matches the labels.",
1059 access: "admin",
1060 body: { name: ["string", "Runner name."], labels: ["string[]", "Runner labels, matched against `runs-on`."] },
1061 },
1062 "GET /repos/{owner}/{repo}/actions/runs": {
1063 summary: "List workflow runs",
1064 access: "read",
1065 query: {
1066 workflow: ["string", "Workflow file path."],
1067 branch: ["string", "Head branch."],
1068 status: ["string", "Run status or conclusion."],
1069 event: ["string", "Triggering event."],
1070 head_sha: ["string", "Head commit."],
1071 per_page: ["integer", "Results per page (default 25, at most 100)."],
1072 page: ["integer", "Page number, starting at 1."],
1073 },
1074 },
1075 "GET /repos/{owner}/{repo}/actions/runs/{id}": {
1076 summary: "Get a workflow run",
1077 description: "The run with its jobs. `id` is the run's id, or its number (`github.run_id` in workflows), which picks the latest attempt.",
1078 access: "read",
1079 },
1080 "GET /repos/{owner}/{repo}/actions/runs/{id}/attempts/{attempt}": {
1081 summary: "Get a run attempt",
1082 description: "Re-runs are stored as separate runs with the same run number; this returns attempt `attempt` of the run's number.",
1083 access: "read",
1084 },
1085 "GET /repos/{owner}/{repo}/actions/runs/{id}/logs": {
1086 summary: "Download a run's logs",
1087 description: "A zip with one text file per job. Jobs past 24 MB of logs in total are left out.",
1088 access: "read",
1089 },
1090 "GET /repos/{owner}/{repo}/actions/runs/{id}/jobs": { summary: "List a run's jobs", access: "read" },
1091 "POST /repos/{owner}/{repo}/actions/runs/{id}/cancel": { summary: "Cancel a run", access: "write", status: 202 },
1092 "POST /repos/{owner}/{repo}/actions/runs/{id}/rerun": { summary: "Re-run a workflow run", description: "A workflow's `GITHUB_TOKEN` can't re-run workflows (403).", access: "write", status: 201 },
1093 "POST /repos/{owner}/{repo}/actions/runs/{id}/rerun-failed-jobs": { summary: "Re-run a run's failed jobs", description: "A workflow's `GITHUB_TOKEN` can't re-run workflows (403).", access: "write", status: 201 },
1094 "GET /repos/{owner}/{repo}/actions/jobs/{id}": { summary: "Get a job", description: "The job with its steps.", access: "read" },
1095 "GET /repos/{owner}/{repo}/actions/jobs/{id}/logs": {
1096 summary: "Get a job's logs",
1097 description: "Plain text, one timestamped line each, or JSON with `format=json`.",
1098 access: "read",
1099 query: { format: ["string", "`json` for an array of `{ t, text }` lines."], after: ["integer", "Only lines after this sequence number (for following a live log)."] },
1100 },
1101 "GET /repos/{owner}/{repo}/actions/runs/{runId}/artifacts": {
1102 summary: "List a run's artifacts",
1103 description:
1104 "Newest first, in the same shape as `GET /repos/{owner}/{repo}/actions/artifacts`. `runId` is the run's id, or its number (`github.run_id` in workflows), which covers every attempt of the run. Expired artifacts are listed with `expired: true`; their content is gone.",
1105 access: "read",
1106 query: { name: ["string", "Only artifacts with exactly this name."], ...page },
1107 },
1108 "GET /repos/{owner}/{repo}/actions/artifacts": {
1109 summary: "List artifacts",
1110 description:
1111 "Every artifact in the repository, newest first. Each has a numeric `id`, `name`, `size_in_bytes`, `digest` (`sha256:...` when the uploader sent one), `created_at`, `expires_at`, `expired`, an `archive_download_url`, and `workflow_run` (its `id` is the run number). An artifact past its retention stays listed with `expired: true` for 90 more days; its content is already deleted. Returns `{ total_count, artifacts }`.",
1112 access: "read",
1113 query: { name: ["string", "Only artifacts with exactly this name."], ...page },
1114 },
1115 "GET /repos/{owner}/{repo}/actions/artifacts/{id}": { summary: "Get an artifact", description: "`id` is the artifact's numeric id. Expired artifacts are returned too, with `expired: true`.", access: "read" },
1116 "GET /repos/{owner}/{repo}/actions/artifacts/{id}/{format}": {
1117 summary: "Download an artifact",
1118 description:
1119 "`format` must be `zip` (anything else is 404). Redirects with 302 to a signed URL that works for 10 minutes and needs no token; follow redirects (`curl -L`). The content is a zip, or the file itself for an artifact uploaded as a single unzipped file. 410 `Artifact has expired` once the artifact has expired.",
1120 access: "read",
1121 status: 302,
1122 },
1123 "DELETE /repos/{owner}/{repo}/actions/artifacts/{id}": {
1124 summary: "Delete an artifact",
1125 description: "Deletes the artifact and its content, expired or not; it no longer counts toward the repository's artifact quota. 404 when there is no artifact with that id. Audited as `actions.artifact.delete`.",
1126 access: "write",
1127 status: 204,
1128 },
1129 "GET /repos/{owner}/{repo}/actions/caches": {
1130 summary: "List caches",
1131 description:
1132 "Saved cache entries; uploads in progress aren't listed. Each has a numeric `id`, `key`, `version`, `ref` (the ref it was saved for, like `refs/heads/main`), `size_in_bytes`, `created_at` and `last_accessed_at`. Returns `{ total_count, actions_caches }`.",
1133 access: "read",
1134 query: {
1135 key: ["string", "Only entries whose key starts with this."],
1136 ref: ["string", "Only entries saved for this full ref, like `refs/heads/main`."],
1137 sort: ["string", "`last_accessed_at` (default), `created_at` or `size_in_bytes`."],
1138 direction: ["string", "`desc` (default) or `asc`."],
1139 ...page,
1140 },
1141 },
1142 "DELETE /repos/{owner}/{repo}/actions/caches": {
1143 summary: "Delete caches by key",
1144 description:
1145 "Deletes every entry whose key is exactly `key` (all versions), or only those saved for `ref`. Returns the deleted entries as `{ total_count, actions_caches }`. 422 `key is required` without `key`, 404 when nothing matched. Audited as `actions.cache.delete`.",
1146 access: "write",
1147 query: { key: ["string", "The exact cache key.", true], ref: ["string", "Only entries saved for this full ref, like `refs/heads/main`."] },
1148 },
1149 "DELETE /repos/{owner}/{repo}/actions/caches/{id}": {
1150 summary: "Delete a cache",
1151 description: "By the entry's numeric id. 404 when there is none. Audited as `actions.cache.delete`.",
1152 access: "write",
1153 status: 204,
1154 },
1155 "GET /repos/{owner}/{repo}/actions/cache/usage": {
1156 summary: "Get cache usage",
1157 description: "`full_name`, `active_caches_size_in_bytes` and `active_caches_count` of the saved entries. A repository keeps up to 10 GB of caches; see [Limits](/docs/reference/limits#caches-and-artifacts).",
1158 access: "read",
1159 },
1160 "GET /repos/{owner}/{repo}/actions/permissions/workflow": {
1161 summary: "Get workflow token permissions",
1162 description:
1163 "The defaults for the `GITHUB_TOKEN` of the repository's workflows (GitHub's \"Workflow permissions\" setting). `default_workflow_permissions` is what a job without `permissions:` gets: `read` (contents and packages read, nothing else) or `write` (read and write on every scope except `id-token`). `can_approve_pull_request_reviews` says whether job tokens may open pull requests and approve or request changes. New repositories start with `read` and `false`; repositories created before the setting existed have `write` and `false`. See [Workflow permissions](/docs/actions/workflow-permissions).",
1164 access: "admin",
1165 },
1166 "PUT /repos/{owner}/{repo}/actions/permissions/workflow": {
1167 summary: "Set workflow token permissions",
1168 description:
1169 "Send only what changes. People only: agents get 403 `only people can change what workflows are allowed to do`. Applies to jobs that start afterwards. Each change is audited as `repo.actions_workflow_settings`. Even when allowed, a workflow's approval never counts toward required approvals.",
1170 access: "admin",
1171 status: 204,
1172 body: {
1173 default_workflow_permissions: ["string", "`read` or `write`."],
1174 can_approve_pull_request_reviews: ["boolean", "Allow workflows' `GITHUB_TOKEN` to open pull requests and to approve or request changes."],
1175 },
1176 },
1177 "GET /repos/{owner}/{repo}/actions/secrets": { summary: "List repository secrets", description: "Names and update times only; values are never returned.", access: "admin" },
1178 "PUT /repos/{owner}/{repo}/actions/secrets/{name}": {
1179 summary: "Set a repository secret",
1180 description: "People only. The value is encrypted at rest. GitHub's sealed-box `encrypted_value` isn't supported; send `value` over HTTPS. Audited as `repo.secret.set`.",
1181 access: "admin",
1182 status: 204,
1183 body: { value: ["string", "Secret value.", true] },
1184 },
1185 "DELETE /repos/{owner}/{repo}/actions/secrets/{name}": { summary: "Delete a repository secret", description: "People only. Audited as `repo.secret.delete`.", access: "admin", status: 204 },
1186 "GET /repos/{owner}/{repo}/actions/variables": {
1187 summary: "List repository variables",
1188 description: "The repository's own variables. Workflows in an organization's repositories also see the organization's variables (`GET /orgs/{org}/actions/variables`), and a repository variable with the same name wins.",
1189 access: "write",
1190 query: { ...page },
1191 },
1192 "POST /repos/{owner}/{repo}/actions/variables": {
1193 summary: "Create a repository variable",
1194 description: "409 when the repository already has a variable with this name. An organization variable of the same name doesn't count, and the repository's wins in workflows. Names are stored upper-cased. People only. Audited as `repo.variable.set`.",
1195 access: "admin",
1196 status: 201,
1197 body: { name: ["string", "Variable name.", true], value: ["string", "Variable value.", true] },
1198 },
1199 "GET /repos/{owner}/{repo}/actions/variables/{name}": { summary: "Get a repository variable", description: "404 for a name only the organization has. `created_at` and `updated_at` are always null.", access: "write" },
1200 "PATCH /repos/{owner}/{repo}/actions/variables/{name}": {
1201 summary: "Update a repository variable",
1202 description: "404 when the repository has no variable with this name; organization variables aren't changed here. People only. Renaming onto a name that exists fails with 409. Audited as `repo.variable.set`.",
1203 access: "admin",
1204 status: 204,
1205 body: { name: ["string", "New name (renames the variable)."], value: ["string", "New value."] },
1206 },
1207 "PUT /repos/{owner}/{repo}/actions/variables/{name}": {
1208 summary: "Set a repository variable",
1209 description: "Diffinery's form: creates the variable or replaces its value. GitHub clients use `POST` and `PATCH`. Names are stored upper-cased. People only. Audited as `repo.variable.set`.",
1210 access: "admin",
1211 status: 204,
1212 body: { value: ["string", "Variable value.", true] },
1213 },
1214 "DELETE /repos/{owner}/{repo}/actions/variables/{name}": {
1215 summary: "Delete a repository variable",
1216 description: "People only. Names are matched without regard to letter case. Audited as `repo.variable.delete`.",
1217 access: "admin",
1218 status: 204,
1219 },
1220 "GET /orgs/{org}/actions/secrets": { summary: "List organization secrets", description: "Names and update times only.", access: "org-admin" },
1221 "PUT /orgs/{org}/actions/secrets/{name}": {
1222 summary: "Set an organization secret",
1223 description: "Available to every repository in the organization. Names are upper-cased and must match `[A-Z_][A-Z0-9_]*`. People only. Audited as `org.secret.set`.",
1224 access: "org-admin",
1225 status: 204,
1226 body: { value: ["string", "Secret value.", true] },
1227 },
1228 "DELETE /orgs/{org}/actions/secrets/{name}": { summary: "Delete an organization secret", description: "People only. 404 when the organization has no secret with this name. Audited as `org.secret.delete`.", access: "org-admin", status: 204 },
1229 "GET /orgs/{org}/actions/variables": {
1230 summary: "List organization variables",
1231 description:
1232 "Actions variables shared by every repository of the organization: workflows read them as `vars.NAME`, and a repository variable with the same name wins. Returns `total_count` and every variable (no paging) with `name`, `value`, `visibility` (always `all`), `created_at` and `updated_at`. Organization owners and admins who are people (agents get 403 `organization admins only`); anyone outside the organization gets 404.",
1233 access: "org-admin",
1234 },
1235 "POST /orgs/{org}/actions/variables": {
1236 summary: "Create an organization variable",
1237 description:
1238 "Creates the variable, or replaces the value of one with that name (GitHub answers 409 instead). Names are stored upper-cased; they must start with a letter or `_`, contain only letters, digits and `_`, and not start with `GITHUB_` (422 `invalid variable name`). Values are strings of at most 49,152 characters (422 `value must be a string of at most 48 KB`). GitHub's `visibility` and `selected_repository_ids` are ignored: every repository of the organization gets the variable. People only. Audited as `org.variable_set`.",
1239 access: "org-admin",
1240 status: 201,
1241 body: { name: ["string", "Variable name.", true], value: ["string", "Variable value, at most 49,152 characters.", true] },
1242 },
1243 "GET /orgs/{org}/actions/variables/{name}": { summary: "Get an organization variable", description: "`name` is matched in upper case. 404 `variable not found`. Organization owners and admins who are people.", access: "org-admin" },
1244 "PATCH /orgs/{org}/actions/variables/{name}": {
1245 summary: "Update an organization variable",
1246 description: "Sets the value, creating the variable if it doesn't exist. Renaming isn't supported: a `name` in the body is ignored. Same name and value rules as creating. People only. Audited as `org.variable_set`.",
1247 access: "org-admin",
1248 status: 204,
1249 body: { value: ["string", "New value, at most 49,152 characters.", true] },
1250 },
1251 "DELETE /orgs/{org}/actions/variables/{name}": { summary: "Delete an organization variable", description: "404 when the organization has no variable with this name, and nothing is audited. People only. Audited as `org.variable_delete`.", access: "org-admin", status: 204 },
1252
1253 // ------------------------------------------------------------ webhooks
1254 "GET /repos/{owner}/{repo}/hooks": { summary: "List webhooks", description: "Secrets are never returned.", access: "admin" },
1255 "POST /repos/{owner}/{repo}/hooks": {
1256 summary: "Create a webhook",
1257 description: "Audited as `repo.webhook.create`.",
1258 access: "admin",
1259 status: 201,
1260 body: { config: ["object", "`{ url, secret }`. The secret signs each delivery (`X-Hub-Signature-256`).", true], events: ["string[]", "Event names, or `[\"*\"]` (default) for all."] },
1261 },
1262 "DELETE /repos/{owner}/{repo}/hooks/{id}": { summary: "Delete a webhook", description: "Audited as `repo.webhook.delete`.", access: "admin", status: 204 },
1263
1264 // ------------------------------------------------------------ notifications and email
1265 "GET /notifications": { summary: "List notifications", access: "account", query: { all: ["boolean", "`true` to include read ones."] } },
1266 "PUT /notifications": { summary: "Mark all notifications as read", access: "account", status: 205 },
1267 "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" },
1268 "PUT /repos/{owner}/{repo}/subscription": {
1269 summary: "Watch or ignore a repository",
1270 access: "read",
1271 body: { subscribed: ["boolean", "`true` for all activity, `false` for participating only."], ignored: ["boolean", "`true` to ignore the repository."] },
1272 },
1273 "DELETE /repos/{owner}/{repo}/subscription": { summary: "Stop watching a repository", description: "Back to participating notifications.", access: "read", status: 204 },
1274 "GET /diffinery/notification-settings": { summary: "Get your notification settings", access: "account" },
1275 "PUT /diffinery/notification-settings": { summary: "Update your notification settings", description: "Send only the groups you want to change.", access: "account" },
1276 "GET /diffinery/emails": {
1277 summary: "List emails sent to you",
1278 access: "account",
1279 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)."] },
1280 },
1281 "POST /diffinery/emails/test": { summary: "Send yourself a test email", access: "account" },
1282 "GET /diffinery/admin/email": { summary: "Email delivery status", description: "Whether sending is configured, and counts for the last 24 hours.", access: "site-admin" },
1283 "POST /diffinery/admin/emails/samples": { summary: "Send yourself every email template", access: "site-admin", body: { only: ["string[]", "Only these sample ids."] } },
1284
1285 // ------------------------------------------------------------ tokens, agents and sandboxes
1286 "GET /diffinery/tokens": { summary: "List your tokens", access: "account" },
1287 "POST /diffinery/tokens": {
1288 summary: "Create a token",
1289 description: "From a browser session only: no token can create a token. The token value is returned once. Unknown scopes or an empty list fail with 422, and so does a repository you can't see (`unknown repository owner/name`).",
1290 access: "account",
1291 status: 201,
1292 body: {
1293 name: ["string", "A name to recognise it by.", true],
1294 scopes: ["string[]", "`read`, `write` (default) and/or `admin`."],
1295 repositories: ["string[]", "Restrict to these `owner/name` repositories (only ones you can see)."],
1296 expires_in_days: ["integer", "Lifetime in days."],
1297 },
1298 },
1299 "DELETE /diffinery/tokens/{id}": { summary: "Revoke a token", access: "account", status: 204 },
1300 "GET /diffinery/agents": { summary: "List your agents", description: "Agents you own, and agents of organizations you administer.", access: "account" },
1301 "POST /diffinery/agents": {
1302 summary: "Create an agent",
1303 description: "People, from a browser session only (no token can create a token). Returns the agent and its first token (write scope, 90 days, unless you say otherwise). Unknown scopes or an empty list fail with 422.",
1304 access: "account",
1305 status: 201,
1306 body: {
1307 login: ["string", "Agent login.", true],
1308 name: ["string", "Display name."],
1309 org: ["string", "Organization that owns the agent (default: you)."],
1310 policy: ["object", "Agent policy."],
1311 token: ["object", "`{ scopes, expires_in_days }` for the first token."],
1312 },
1313 },
1314 "POST /diffinery/agents/{login}/tokens": {
1315 summary: "Create a token for an agent",
1316 description: "From a browser session only (no token can create a token): 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`.",
1317 access: "account",
1318 status: 201,
1319 body: { name: ["string", "Token name."], scopes: ["string[]", "`read`, `write` (default) and/or `admin`."], expires_in_days: ["integer", "Lifetime in days."] },
1320 },
1321 "DELETE /diffinery/agents/{login}": { summary: "Disable an agent", description: "Its tokens stop working.", access: "account", status: 204 },
1322 "POST /repos/{owner}/{repo}/diffinery/sandboxes": {
1323 summary: "Start a sandbox",
1324 description: "A container with the repository checked out, for agents to build and test in.",
1325 access: "write",
1326 status: 201,
1327 body: { ref: ["string", "Branch, tag or commit to check out (default: the default branch)."] },
1328 },
1329 "POST /repos/{owner}/{repo}/diffinery/sandboxes/{id}/{action}": {
1330 summary: "Use a sandbox",
1331 description: "`action` is `exec` (run a command), `read` or `write` (files), or `close`.",
1332 access: "write",
1333 },
1334
1335 // ------------------------------------------------------------ GitHub imports and linked accounts
1336 "POST /diffinery/imports": {
1337 summary: "Start a GitHub import",
1338 description:
1339 "Imports one GitHub repository (`source`), or many repositories of a GitHub organization or user (`source_owner`), on the server, and returns the import (`status: queued`). Git history with LFS objects is copied in a container; issues and pull requests (with their numbers), comments, reviews, labels, milestones, branch protection, Actions variables, deployment environments and releases come through GitHub's REST API. Secret values are never read: only secret names are listed in `summary`, for a person to set by hand. People can import into their own account or an organization they own or administer (site administrators: any user or organization). An agent can import only where its owner may (the owner's own account or an organization the owner owns or administers; an organization's agent, only into that organization), and only when its policy allows `write`; its imports always use `secrets: stop` and skip Actions variables and environments. Needs a `write` or `admin` token (403 `starting an import needs a token with write or admin scope`), or a browser session. At most one organization import and three repository imports run at a time per account and per target (429), and a `source` repository larger than `IMPORT_MAX_REPO_KB` (default 10 GB, by GitHub's reported size) is refused with 422. Other errors: 422 `<owner>/<name> already exists; choose another name`, 422 `GitHub repository <source> was not found or the token cannot read it`, 422 `GitHub rejected the token (expired or revoked?)`, 429 when GitHub's rate limit is reached, 502 `could not reach GitHub: ...`. Audited as `import.create`. See [Migrate from GitHub](/docs/migrate-from-github).",
1340 access: "account",
1341 status: 201,
1342 body: {
1343 source: ["string", "One repository: `owner/name`, a github.com or GitHub Enterprise URL, or an SSH remote such as `git@github.com:owner/name.git`. Send either `source` or `source_owner`."],
1344 source_owner: ["string", "A GitHub organization or user: imports its repositories (private ones too when the token can see them)."],
1345 owner: ["string", "The Diffinery user or organization that will own the new repositories.", true],
1346 name: ["string", "`source` only: the new repository's name. Default: the GitHub name. It must not exist yet."],
1347 visibility: ["string", "`keep` (default: public stays public, private and internal become private), `private` or `public`. `internal` is accepted and means `private`."],
1348 github_token: ["string", "A GitHub token that can read the source, at most 400 characters. Needed for private repositories; Actions variables, secret names and branch protection need a token with admin access to the source. Encrypted at rest, used only by this import, deleted when it ends, never returned."],
1349 github_api_url: ["string", "GitHub Enterprise Server: the REST API base, such as `https://ghe.example.com/api/v3` (default `https://api.github.com`). Must be HTTPS on a public host. Required, on the same host as `source`, when `source` names an Enterprise host and you send `github_token`."],
1350 options: ["object", "What to copy; each is `true` by default: `issues` (issues and pull requests with comments and reviews), `labels` (labels and milestones), `branch_protection`, `variables` (Actions variables, secret names and deployment environments), `releases`, `lfs`. Plus `secrets`: `bypass` (default: credentials found in old history are imported with an audited push protection bypass, which needs a person with admin on the target and an `admin` token or browser session) or `stop` (the import fails). Unknown keys are refused with 422."],
1351 repositories: ["string[]", "`source_owner` only: the repository names to import, at most 1,000. Default: every repository (up to 1,000)."],
1352 exclude_forks: ["boolean", "`source_owner` only: skip forks (default `false`)."],
1353 exclude_archived: ["boolean", "`source_owner` only: skip archived repositories (default `false`)."],
1354 concurrency: ["integer", "`source_owner` only: repositories imported at the same time, 1 to 8 (default 3)."],
1355 },
1356 },
1357 "GET /diffinery/imports": {
1358 summary: "List your imports",
1359 description: "The imports you started, newest first, at most 100. Repository imports that belong to an organization import aren't listed separately; read them through the organization import's `children`.",
1360 access: "account",
1361 },
1362 "GET /diffinery/imports/{id}": {
1363 summary: "Get an import",
1364 description:
1365 "Readable by the person or agent who started it, by people who could start an import into the same target (the account itself, or the organization's owners and admins), and by site administrators; others get 404 `import not found`. Fields: `kind` (`repository` or `organization`), `status` (`draft`, `queued`, `running`, `succeeded`, `failed` or `cancelled`), `attempt`, `source`, `target`, `visibility`, `options`, `steps` (each with `name`, `title`, `status`, `message`, `count` and times), `git` (the transfer's `phase`, `message`, `chunks`, `bytes` and `tries`), `summary` (counts, secret names, variables, environments, releases, push protection `findings`, linked and imported people), `warnings`, `error` and timestamps. An organization import adds `children` (each repository import's `id`, `source`, `target`, `status`, current `step` and `error`), and a draft adds `candidates`. Credentials are never included. Poll until `status` is `succeeded`, `failed` or `cancelled`.",
1366 access: "account",
1367 },
1368 "POST /diffinery/imports/{id}/cancel": {
1369 summary: "Cancel an import",
1370 description:
1371 "Stops a `draft`, `queued` or `running` import (409 `the import already <status>` otherwise): ends its workflow and container and revokes its push tokens. Cancelling an organization import cancels its unfinished repository imports. A repository the import already created is kept with what was copied so far, and is unlocked. Anyone who can read the import can cancel it. Audited as `import.cancel`.",
1372 access: "account",
1373 },
1374 "POST /diffinery/imports/{id}/retry": {
1375 summary: "Retry an import",
1376 description:
1377 "Runs a `failed` or `cancelled` import again as a new `attempt` (409 `only failed or cancelled imports can be retried` otherwise), with the same options. Only whoever started it can retry it (404 for anyone else), except that a person who may import into the target can also retry an import an agent started. A repository import inside an organization import can't be retried on its own (422 `retry the organization import this repository belongs to`); retrying the organization import re-runs its failed and cancelled repositories, except those that failed because the target name was taken. The GitHub token was deleted when the import ended, so send `github_token` again for a private source. The retry resumes into the repository the import created, and fails if someone else has written to it since (`repository already holds content not written by this import; start a new import`). An agent can't retry once the repository exists (403 `an agent cannot retry an import after the repository was created; a person who administers <owner> must retry it`). Audited as `import.retry`.",
1378 access: "account",
1379 status: 202,
1380 body: { github_token: ["string", "A GitHub token for this attempt (optional; at most 400 characters)."] },
1381 },
1382 "POST /diffinery/imports/{id}/selection": {
1383 summary: "Choose repositories for a draft import",
1384 description:
1385 "An organization import started from the web form waits as a `draft` with its `candidates` until its repositories are chosen; this starts it. Names are matched to the candidates ignoring case, and names that aren't candidates are dropped. 422 `choose at least one repository`, 409 `this import has already started`. Only whoever started it (404 for anyone else). Imports started through this API or MCP never wait as drafts. Unstarted drafts are cancelled after 7 days. Audited as `import.create`.",
1386 access: "account",
1387 status: 202,
1388 body: { repositories: ["string[]", "Names of the repositories to import, at most 1,000.", true] },
1389 },
1390 "POST /diffinery/imports/{id}/claim": {
1391 summary: "Claim an import's Git transfer (local development)",
1392 description:
1393 "Only when the Worker runs with `IMPORT_GIT_AGENT=external` (the `dev` environment, where containers don't run); 404 otherwise. Whoever started the import gets `{ token }`, a push token for the transfer, and runs the runner image's import mode by hand with `DIFFINERY_API_URL`, `DIFFINERY_IMPORT_ID` and `DIFFINERY_IMPORT_TOKEN`. 409 `this import is not waiting for an agent`.",
1394 access: "account",
1395 },
1396 "GET /diffinery/imports/{id}/agent/spec": {
1397 summary: "Get a Git transfer spec (import agent)",
1398 description:
1399 "Internal: called by the import container with the import's own push token, which works only on Git, Git LFS and these agent endpoints. Says what to clone and where to push, and includes the GitHub token, so no other credential is accepted (403 `this token does not belong to this import`, or 403 `this push token is no longer current for the import` for a token from an earlier try; 409 `this import is not running`).",
1400 access: "account",
1401 },
1402 "POST /diffinery/imports/{id}/agent/progress": {
1403 summary: "Report Git transfer progress (import agent)",
1404 description: "Internal: the import agent reports its phase, a message, and the chunks and bytes pushed so far. Same credential rules as the spec. 204.",
1405 access: "account",
1406 status: 204,
1407 },
1408 "POST /diffinery/imports/{id}/agent/complete": {
1409 summary: "Report a Git transfer's result (import agent)",
1410 description: "Internal: the import agent reports the result (branches, tags, pull request heads, LFS objects, push protection findings, warnings). Revokes the push token and wakes the import's workflow. Same credential rules as the spec. 204.",
1411 access: "account",
1412 status: 204,
1413 },
1414 "GET /diffinery/user/linked-accounts": {
1415 summary: "List your linked GitHub accounts",
1416 description:
1417 "Each link has `id`, `host` (`github.com` or the Enterprise host), `apiBase`, `login`, `externalId` (GitHub's numeric user id) and `createdAt`. Imports attribute a GitHub user's issues, pull requests, comments, reviews and releases to the Diffinery account linked to that user id on the same API base. Everyone else is imported as a mannequin: login `github:<login>`, `type: \"Mannequin\"`, linking to the GitHub profile.",
1418 access: "account",
1419 },
1420 "POST /diffinery/user/linked-accounts/github/challenge": {
1421 summary: "Get a GitHub link code",
1422 description:
1423 "Step 1 of linking a GitHub account without a token: returns `nonce` (`diffinery-link-` and 32 hex characters, the same each time for your account and host), `apiBase`, `host` and `gistsUrl`. Put the nonce in the description or a file of a public gist of the GitHub account, then call `POST /diffinery/user/linked-accounts/github`. People only (403 `only people can link GitHub accounts`).",
1424 access: "account",
1425 body: { api_url: ["string", "GitHub Enterprise REST API base, such as `https://ghe.example.com/api/v3`. Default: github.com."] },
1426 },
1427 "POST /diffinery/user/linked-accounts/github": {
1428 summary: "Link a GitHub account",
1429 description:
1430 "Step 2: reads the GitHub user's public profile and public gists without a token on the same API base, and links the account when one of the first 100 public gists GitHub lists for the user has your nonce in its description or in a file (file contents are read for at most 20 gists). Returns `{ id, host, login }`. 422 `create a PUBLIC gist whose description or a file contains <nonce>, then try again`, 422 `GitHub user <login> was not found`, 409 `GitHub user <login> is linked to another Diffinery account`, 429 when GitHub's rate limit is reached. People only. Audited as `account.link_github`.",
1431 access: "account",
1432 status: 201,
1433 body: { login: ["string", "Your GitHub username.", true], api_url: ["string", "The same API base you used for the challenge. Default: github.com."] },
1434 },
1435 "DELETE /diffinery/user/linked-accounts/{id}": {
1436 summary: "Unlink a GitHub account",
1437 description: "Removes one of your own links (an id that isn't yours changes nothing). Imports that already ran keep their attribution. Audited as `account.unlink_github`.",
1438 access: "account",
1439 status: 204,
1440 },
1441 "DELETE /diffinery/admin/linked-accounts/{id}": {
1442 summary: "Clear anyone's GitHub link",
1443 description: "For example after an account changes hands. `id` is the link id from `GET /diffinery/user/linked-accounts` of the account that owns it; there is no endpoint that lists other people's links. 404 `link not found`, 403 `site admin only`. Audited as `account.link_cleared`.",
1444 access: "site-admin",
1445 status: 204,
1446 },
1447
1448 // ------------------------------------------------------------ packages
1449 "GET /repos/{owner}/{repo}/packages": { summary: "List a repository's packages", description: "Container images linked to the repository.", access: "read" },
1450 ...packageDocs("/user/packages", "your"),
1451 ...packageDocs("/users/{owner}/packages", "a user's"),
1452 ...packageDocs("/orgs/{owner}/packages", "an organization's"),
1453};
1454
1455function packageDocs(prefix: string, whose: string): Record<string, OpDoc> {
1456 return {
1457 [`GET ${prefix}`]: { summary: `List ${whose} packages`, description: "Only packages you can pull.", access: "account" },
1458 [`GET ${prefix}/{type}/{name}`]: { summary: `Get one of ${whose} packages`, description: "`type` is `container`.", access: "read" },
1459 [`PATCH ${prefix}/{type}/{name}`]: {
1460 summary: `Update one of ${whose} packages`,
1461 description: "Change its visibility or the repository it is linked to.",
1462 access: "admin",
1463 body: { visibility: ["string", "`private` or `public`."], repository: ["string", "Repository name to link, or `null` to unlink."] },
1464 },
1465 [`DELETE ${prefix}/{type}/{name}`]: { summary: `Delete one of ${whose} packages`, access: "admin", status: 204 },
1466 [`GET ${prefix}/{type}/{name}/versions`]: { summary: "List package versions", access: "read", query: { state: ["string", "`tagged` for tagged versions only."], ...page } },
1467 [`GET ${prefix}/{type}/{name}/versions/{id}`]: { summary: "Get a package version", access: "read" },
1468 [`DELETE ${prefix}/{type}/{name}/versions/{id}`]: { summary: "Delete a package version", access: "admin", status: 204 },
1469 };
1470}
1471
1472/** GitHub's tarball and zipball endpoints, with and without a ref. */
1473function archiveDocs(kind: "tarball" | "zipball", what: string, format: string): Record<string, OpDoc> {
1474 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).`;
1475 return {
1476 [`GET /repos/{owner}/{repo}/${kind}`]: { summary: `Download ${what}`, description: `${description} Uses the default branch.`, access: "read" },
1477 [`GET /repos/{owner}/{repo}/${kind}/{ref}`]: { summary: `Download ${what} of a ref`, description, access: "read" },
1478 };
1479}
1480
1481/** Reactions on one kind of subject. `note` adds a sentence to the list endpoint. */
1482function reactionDocs(prefix: string, what: string, note?: string): Record<string, OpDoc> {
1483 return {
1484 [`GET ${prefix}/reactions`]: {
1485 summary: `List reactions to ${what}`,
1486 ...(note ? { description: note } : {}),
1487 access: "read",
1488 query: { content: ["string", "Only reactions with this content."], ...page },
1489 },
1490 [`POST ${prefix}/reactions`]: {
1491 summary: `Add a reaction to ${what}`,
1492 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.`,
1493 access: "read",
1494 status: 201,
1495 body: { content: ["string", "`+1`, `-1`, `laugh`, `confused`, `heart`, `hooray`, `rocket` or `eyes`.", true] },
1496 },
1497 [`DELETE ${prefix}/reactions/{reaction}`]: { summary: `Delete a reaction from ${what}`, description: "Your own reactions, or anyone's with admin access.", access: "read", status: 204 },
1498 };
1499}