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