| 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" | "org-owner" | "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; 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 | |
| 75 | const 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 | |
| 80 | export 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 | |
| 1455 | function 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. */ |
| 1473 | function 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. */ |
| 1482 | function 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 | } |