Diffinery

codician/diffinerypublic

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

2189 lines · 172.6 KBRaw History
1/**
2 * Hand-written notes for the generated REST API reference (/docs/api and
3 * /docs/api/openapi.json). The list of endpoints itself comes from the live
4 * router, so an endpoint can't be missing from the docs: one without an entry
5 * here still appears, with a summary derived from its method and path. A unit
6 * test fails when an entry here no longer matches a route.
7 *
8 * Keys are "METHOD /path" with OpenAPI-style {params}, relative to /api/v3.
9 */
10
11/** Who may call an endpoint: a repository role, or an account-level requirement. */
12export type Access = "public" | "account" | "read" | "triage" | "write" | "maintain" | "admin" | "org-admin" | "org-owner" | "site-admin";
13
14/** [type, description, required?] */
15export type Field = [string, string, boolean?];
16
17export interface OpDoc {
18 summary: string;
19 description?: string;
20 access?: Access;
21 query?: Record<string, Field>;
22 body?: Record<string, Field>;
23 /** Success status, when it isn't 200. */
24 status?: number;
25}
26
27export type Scope = "read" | "write" | "admin";
28
29/**
30 * Endpoints whose token requirement differs from the default (GET needs a
31 * read token, anything else a write token). These are account-level changes,
32 * so a token limited to repositories is refused too, except where noted.
33 */
34export const ACCOUNT_SCOPES: Record<string, { scope: Scope; anyToken?: boolean; sessionOnly?: boolean }> = {
35 "POST /user/orgs": { scope: "admin" },
36 "PATCH /orgs/{org}": { scope: "admin" },
37 "POST /orgs/{org}/invitations": { scope: "admin" },
38 "POST /invitations": { scope: "admin" },
39 "DELETE /orgs/{org}": { scope: "admin" },
40 "POST /repos/{owner}/{repo}/transfer": { scope: "admin" },
41 "GET /orgs/{org}/invitations": { scope: "read" },
42 "GET /user/repository_invitations": { scope: "read" },
43 "PATCH /user/repository_invitations/{id}": { scope: "write" },
44 "DELETE /user/repository_invitations/{id}": { scope: "write" },
45 "GET /user/memberships/orgs": { scope: "read" },
46 "GET /user/memberships/orgs/{org}": { scope: "read" },
47 "PATCH /user/memberships/orgs/{org}": { scope: "write" },
48 "GET /user/usage": { scope: "read", anyToken: true },
49 "GET /orgs/{org}/usage": { scope: "read", anyToken: true },
50 "GET /admin/accounts/{login}/quotas": { scope: "read", anyToken: true },
51 "PATCH /admin/accounts/{login}/quotas": { scope: "admin" },
52 "GET /admin/repos/{owner}/{repo}/actions-suspension": { scope: "read" },
53 "PUT /admin/repos/{owner}/{repo}/actions-suspension": { scope: "admin" },
54 "DELETE /admin/repos/{owner}/{repo}/actions-suspension": { scope: "admin" },
55 "GET /admin/accounts/{login}/actions-suspension": { scope: "read" },
56 "PUT /admin/accounts/{login}/actions-suspension": { scope: "admin" },
57 "DELETE /admin/accounts/{login}/actions-suspension": { scope: "admin" },
58 "GET /admin/hosted-compute-pause": { scope: "read" },
59 "PUT /admin/hosted-compute-pause": { scope: "admin" },
60 "DELETE /admin/hosted-compute-pause": { scope: "admin" },
61 "DELETE /orgs/{org}/invitations/{id}": { scope: "admin" },
62 "GET /invitations": { scope: "read" },
63 "DELETE /invitations/{id}": { scope: "admin" },
64 "POST /user/repos": { scope: "write" },
65 "POST /orgs/{org}/repos": { scope: "write" },
66 "GET /orgs/{org}/actions/secrets": { scope: "admin" },
67 "PUT /orgs/{org}/actions/secrets/{name}": { scope: "admin" },
68 "DELETE /orgs/{org}/actions/secrets/{name}": { scope: "admin" },
69 "GET /orgs/{org}/actions/secrets/{name}": { scope: "admin" },
70 "GET /orgs/{org}/actions/secrets/{name}/repositories": { scope: "admin" },
71 "PUT /orgs/{org}/actions/secrets/{name}/repositories": { scope: "admin" },
72 "PUT /orgs/{org}/actions/secrets/{name}/repositories/{repoId}": { scope: "admin" },
73 "DELETE /orgs/{org}/actions/secrets/{name}/repositories/{repoId}": { scope: "admin" },
74 "POST /diffinery/tokens": { scope: "admin", sessionOnly: true },
75 "DELETE /diffinery/tokens/{id}": { scope: "admin" },
76 "POST /diffinery/agents": { scope: "admin", sessionOnly: true },
77 "PUT /diffinery/model-keys/{provider}": { scope: "admin", sessionOnly: true },
78 "DELETE /diffinery/model-keys/{provider}": { scope: "admin", sessionOnly: true },
79 "GET /diffinery/model-keys": { scope: "read", anyToken: true },
80 "GET /diffinery/model-usage": { scope: "read", anyToken: true },
81 "GET /orgs/{org}/diffinery/agent-policy": { scope: "read" },
82 "PUT /orgs/{org}/diffinery/agent-policy": { scope: "admin", sessionOnly: true },
83 "GET /orgs/{org}/diffinery/model-keys": { scope: "read" },
84 "PUT /orgs/{org}/diffinery/model-keys/{provider}": { scope: "admin", sessionOnly: true },
85 "DELETE /orgs/{org}/diffinery/model-keys/{provider}": { scope: "admin", sessionOnly: true },
86 "GET /orgs/{org}/diffinery/model-usage": { scope: "read" },
87 "GET /diffinery/tasks": { scope: "read", anyToken: true },
88 "POST /repos/{owner}/{repo}/diffinery/tasks": { scope: "write", sessionOnly: true },
89 "POST /repos/{owner}/{repo}/diffinery/tasks/{number}/messages": { scope: "write", sessionOnly: true },
90 "POST /repos/{owner}/{repo}/diffinery/tasks/{number}/retry": { scope: "write", sessionOnly: true },
91 "POST /repos/{owner}/{repo}/diffinery/tasks/{number}/plan/approve": { scope: "write", sessionOnly: true },
92 "POST /repos/{owner}/{repo}/diffinery/tasks/{number}/attempts/{attempt}/pick": { scope: "write", sessionOnly: true },
93 "PATCH /repos/{owner}/{repo}/diffinery/tasks/{number}": { scope: "write", sessionOnly: true },
94 "POST /repos/{owner}/{repo}/diffinery/tasks/{number}/plan/split": { scope: "write", sessionOnly: true },
95 "POST /repos/{owner}/{repo}/diffinery/project/posts": { scope: "write", sessionOnly: true },
96 "PUT /repos/{owner}/{repo}/diffinery/project/memory": { scope: "write", sessionOnly: true },
97 "PUT /repos/{owner}/{repo}/diffinery/project/settings": { scope: "write", sessionOnly: true },
98 "PUT /repos/{owner}/{repo}/diffinery/project/coordinator": { scope: "write", sessionOnly: true },
99 "DELETE /repos/{owner}/{repo}/diffinery/project/coordinator": { scope: "write", sessionOnly: true },
100 "POST /repos/{owner}/{repo}/diffinery/project/posts/{n}/handle": { scope: "write", sessionOnly: true },
101 "PUT /repos/{owner}/{repo}/diffinery/agent-review": { scope: "admin", sessionOnly: true },
102 "POST /diffinery/agents/{login}/tokens": { scope: "admin", sessionOnly: true },
103 "DELETE /diffinery/agents/{login}": { scope: "admin" },
104 "GET /diffinery/auto-merge": { scope: "read" },
105 "PUT /diffinery/auto-merge": { scope: "write" },
106 "PUT /diffinery/agents/{login}/auto-merge": { scope: "write" },
107 "PUT /notifications": { scope: "write" },
108 "PUT /diffinery/notification-settings": { scope: "write" },
109 "POST /diffinery/emails/test": { scope: "write" },
110 "POST /diffinery/emails/digest": { scope: "write" },
111 "POST /diffinery/admin/emails/samples": { scope: "admin" },
112 "POST /diffinery/email/verification": { scope: "write" },
113 "GET /diffinery/admin/email/suppressions": { scope: "read" },
114 "PUT /diffinery/admin/email/suppressions/{email}": { scope: "admin" },
115 "DELETE /diffinery/admin/email/suppressions/{email}": { scope: "admin" },
116 "GET /diffinery/admin/security-reports": { scope: "read" },
117 "GET /diffinery/admin/security-reports/{id}": { scope: "read" },
118 "PATCH /diffinery/admin/security-reports/{id}": { scope: "admin" },
119 "GET /diffinery/admin/early-access": { scope: "read" },
120 "GET /diffinery/admin/early-access/{id}": { scope: "read" },
121 "POST /diffinery/admin/early-access/{id}/invite": { scope: "admin" },
122 "PATCH /diffinery/admin/early-access/{id}": { scope: "admin" },
123 "DELETE /diffinery/admin/early-access/{id}": { scope: "admin" },
124 "GET /diffinery/admin/status/incidents": { scope: "read" },
125 "POST /diffinery/admin/status/incidents": { scope: "admin" },
126 "POST /diffinery/admin/status/incidents/{id}/updates": { scope: "admin" },
127 "PUT /user/avatar": { scope: "write" },
128 "DELETE /user/avatar": { scope: "write" },
129 "PUT /orgs/{org}/avatar": { scope: "write" },
130 "DELETE /orgs/{org}/avatar": { scope: "write" },
131 "POST /orgs/{org}/actions/variables": { scope: "admin" },
132 "PATCH /orgs/{org}/actions/variables/{name}": { scope: "admin" },
133 "DELETE /orgs/{org}/actions/variables/{name}": { scope: "admin" },
134 "POST /diffinery/imports/{id}/cancel": { scope: "write" },
135 "POST /diffinery/imports/{id}/retry": { scope: "write" },
136 "POST /diffinery/imports/{id}/selection": { scope: "write" },
137 "POST /diffinery/user/linked-accounts/github/challenge": { scope: "admin" },
138 "POST /diffinery/user/linked-accounts/github": { scope: "admin" },
139 "DELETE /diffinery/user/linked-accounts/{id}": { scope: "admin" },
140 "DELETE /diffinery/admin/linked-accounts/{id}": { scope: "admin" },
141 "GET /user/blocks": { scope: "read" },
142 "GET /user/blocks/{username}": { scope: "read" },
143 "PUT /user/blocks/{username}": { scope: "write" },
144 "DELETE /user/blocks/{username}": { scope: "write" },
145 "GET /orgs/{org}/blocks": { scope: "read" },
146 "GET /orgs/{org}/blocks/{username}": { scope: "read" },
147 "PUT /orgs/{org}/blocks/{username}": { scope: "admin" },
148 "DELETE /orgs/{org}/blocks/{username}": { scope: "admin" },
149 "POST /diffinery/reports": { scope: "write" },
150 "GET /admin/reports": { scope: "read" },
151 "GET /admin/reports/{id}": { scope: "read" },
152 "PATCH /admin/reports/{id}": { scope: "admin" },
153 "PUT /users/{username}/suspended": { scope: "admin" },
154 "DELETE /users/{username}/suspended": { scope: "admin" },
155};
156
157const page: Record<string, Field> = {
158 per_page: ["integer", "Results per page (default 30, at most 100)."],
159 page: ["integer", "Page number, starting at 1."],
160};
161
162export const API_DOCS: Record<string, OpDoc> = {
163 // ------------------------------------------------------------ meta, users and organizations
164 "GET /": { summary: "API root", description: "Names the API and links to these docs and the MCP endpoint.", access: "public" },
165 "GET /meta": { summary: "Instance metadata", description: "Whether this instance issues OIDC tokens to workflows, and the runner image its last deploy set (`runner_image`; each job reports the image it ran on as `runner.image`), and whether it tells search engines about changed public pages through IndexNow (`indexnow`: `enabled`, `key_location` and the time, URL count, IndexNow status and `endpoint` of the `last_submission`; see [Site administration](/docs/admin/site-administration#search-engines)).", access: "public" },
166 "GET /rate_limit": {
167 summary: "Get rate limit status",
168 description: "For GitHub clients that check their budget before calling. It always reports a full, constant budget (15000 core requests an hour). Diffinery's real limits are per minute (2,000 requests per account, 60 new repositories, issues, pull requests, comments and the like, 300 requests per IP address without signing in) and answer 429 with `retry-after: 60`; see /docs/reference/rest-api#rate-limits.",
169 access: "public",
170 },
171 "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" },
172 "PUT /user/avatar": {
173 summary: "Set your profile picture",
174 description:
175 "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.",
176 access: "account",
177 },
178 "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" },
179 "GET /users/{login}": { summary: "Get an account", description: "A user, organization or agent by login. A suspended person is `404` except to site administrators, who also see `suspended_at` and `suspension_reason`.", access: "account" },
180 "GET /user/usage": {
181 summary: "Get your quotas and usage",
182 description: "Hosted minutes used this month, hosted jobs running, sandboxes open, repositories owned and bytes stored, each with its `limit` (`-1` is unlimited), and `exempt` for accounts run by a site administrator. An agent gets its owner's. See [Limits](/docs/reference/limits#account-quotas).",
183 access: "account",
184 },
185 "GET /orgs/{org}/usage": { summary: "Get an organization's quotas and usage", description: "The same as `GET /user/usage`, for the organization's owners and admins.", access: "org-admin" },
186 "GET /admin/accounts/{login}/quotas": { summary: "Get an account's quotas and usage", description: "Site administrators only.", access: "site-admin" },
187 "PATCH /admin/accounts/{login}/quotas": {
188 summary: "Change an account's quotas",
189 description: "Site administrators only. Send only the quotas to change: a whole number, `-1` for unlimited, or `null` for the default. Audited as `account.quotas.set`.",
190 access: "site-admin",
191 body: {
192 hosted_minutes: ["integer", "Hosted minutes per calendar month (UTC)."],
193 hosted_jobs: ["integer", "Hosted Actions jobs running at once."],
194 sandboxes: ["integer", "Agent sandboxes open at once."],
195 repositories: ["integer", "Repositories the account owns."],
196 storage_bytes: ["integer", "Bytes stored: git objects, Git LFS objects, release assets and packages, not Actions artifacts or caches."],
197 },
198 },
199 "GET /admin/repos/{owner}/{repo}/actions-suspension": {
200 summary: "Get a repository's Actions suspension",
201 description: "Site administrators only. `suspended`, `reason`, `suspended_by`, `suspended_at`, and `owner_suspended` when its owner's Actions are suspended.",
202 access: "site-admin",
203 },
204 "PUT /admin/repos/{owner}/{repo}/actions-suspension": {
205 summary: "Suspend Actions for a repository",
206 description: "Site administrators only. No runs start, queued jobs fail and no sandboxes open; what runs now stops (`stopped`). Audited as `actions.suspend`.",
207 access: "site-admin",
208 body: { reason: ["string", "Why (up to 500 characters). Shown to the people affected and kept in the audit log.", true] },
209 },
210 "DELETE /admin/repos/{owner}/{repo}/actions-suspension": { summary: "Lift a repository's Actions suspension", description: "Site administrators only. New work can start again; nothing restarts. Audited as `actions.unsuspend`.", access: "site-admin" },
211 "GET /admin/accounts/{login}/actions-suspension": { summary: "Get an account's Actions suspension", description: "Site administrators only. `suspended`, `reason`, `suspended_by` and `suspended_at`.", access: "site-admin" },
212 "PUT /admin/accounts/{login}/actions-suspension": {
213 summary: "Suspend Actions for an account",
214 description: "Site administrators only. As for a repository, for every repository the person or organization owns; their running runs and sandboxes stop now. Audited as `actions.suspend`.",
215 access: "site-admin",
216 body: { reason: ["string", "Why (up to 500 characters). Shown to the people affected and kept in the audit log.", true] },
217 },
218 "DELETE /admin/accounts/{login}/actions-suspension": { summary: "Lift an account's Actions suspension", description: "Site administrators only. New work can start again; nothing restarts. Audited as `actions.unsuspend`.", access: "site-admin" },
219 "GET /admin/hosted-compute-pause": { summary: "Get the hosted compute pause", description: "Site administrators only. `paused`, `reason`, `paused_by` and `paused_at`.", access: "site-admin" },
220 "PUT /admin/hosted-compute-pause": {
221 summary: "Pause hosted compute",
222 description: "Site administrators only. Hosted jobs fail without starting and sandboxes are refused everywhere; running ones stop now. Self-hosted runners still work. Audited as `compute.pause`.",
223 access: "site-admin",
224 body: { reason: ["string", "Why (up to 500 characters). Shown to the people affected and kept in the audit log.", true] },
225 },
226 "DELETE /admin/hosted-compute-pause": { summary: "Resume hosted compute", description: "Site administrators only. New hosted jobs and sandboxes can start again; nothing restarts. Audited as `compute.unpause`.", access: "site-admin" },
227 "GET /user/orgs": { summary: "List your organizations", description: "Each organization carries your `role` in it (member, admin or owner).", access: "account" },
228 "GET /user/repository_invitations": { summary: "List your repository invitations", description: "Pending invitations to repositories, sent to your username or your email address.", access: "account" },
229 "PATCH /user/repository_invitations/{id}": { summary: "Accept a repository invitation", description: "You get the role it offers. Audited as `invite.accept`.", access: "account", status: 204 },
230 "DELETE /user/repository_invitations/{id}": { summary: "Decline a repository invitation", description: "The invitation is deleted; nobody is told. Audited as `invite.decline`.", access: "account", status: 204 },
231 "GET /user/memberships/orgs": { summary: "List your organization memberships", description: "Active memberships and pending invitations, each with `state` (`active` or `pending`), `role` (GitHub's `admin` or `member`) and `diffinery_role` (`owner`, `admin` or `member`).", access: "account", query: { state: ["string", "`active` or `pending`."] } },
232 "GET /user/memberships/orgs/{org}": { summary: "Get your organization membership", description: "404 when you are neither a member nor invited.", access: "account" },
233 "PATCH /user/memberships/orgs/{org}": { summary: "Accept an organization invitation", description: "Send `{\"state\": \"active\"}` to join with the role the invitation offers. Audited as `invite.accept`.", access: "account", body: { state: ["string", "`active`.", true] } },
234 "POST /user/orgs": {
235 summary: "Create an organization",
236 access: "account",
237 status: 201,
238 body: { login: ["string", "Organization login (letters, digits, `-` and `_`; no dots).", true], name: ["string", "Display name (default: the login)."] },
239 },
240 "GET /orgs/{org}": {
241 summary: "Get an organization",
242 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`).",
243 access: "account",
244 },
245 "PATCH /orgs/{org}": {
246 summary: "Update an organization",
247 description:
248 "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.",
249 access: "org-owner",
250 body: {
251 default_repository_permission: ["string", "Base permission every member gets on the organization's repositories: `none`, `read` (the default for new organizations), `write` or `admin`."],
252 name: ["string", "Display name. An empty value clears it, and pages show the login instead."],
253 },
254 },
255 "GET /orgs/{org}/members": { summary: "List organization members", description: "Members only (or site admins). Each entry carries the member's `role`.", access: "account" },
256 "POST /orgs/{org}/invitations": {
257 summary: "Invite someone to an organization",
258 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. You can send at most 50 invitations in 24 hours (site admins aren't limited); more answer 429 `you can send at most 50 invitations in 24 hours; try again later`.",
259 access: "org-admin",
260 status: 201,
261 body: { email: ["string", "Email address to invite.", true], role: ["string", "`member` (default), `admin` or `owner`."] },
262 },
263 "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" },
264 "DELETE /orgs/{org}": {
265 summary: "Delete an organization",
266 description:
267 "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. From a browser session whose sign-in is more than 2 hours old: 403 `Confirm it's you: sign in with your passkey again to do this.` with a `confirm` URL (see /docs/security/overview#confirming-its-you).",
268 access: "org-admin",
269 status: 202,
270 },
271 "DELETE /orgs/{org}/invitations/{id}": {
272 summary: "Revoke an organization invitation",
273 description: "The link stops working and the address can be invited again. 404 when no pending invitation has this id. Audited.",
274 access: "org-admin",
275 status: 204,
276 },
277 "PUT /orgs/{org}/avatar": {
278 summary: "Set an organization's picture",
279 description:
280 "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.",
281 access: "org-owner",
282 },
283 "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" },
284 "POST /invitations": {
285 summary: "Invite someone to this instance",
286 description: "A site-wide invite with no organization. Site admins only.",
287 access: "site-admin",
288 status: 201,
289 body: { email: ["string", "Email address to invite.", true] },
290 },
291 "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" },
292 "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 },
293
294 // ------------------------------------------------------------ moderation (lib/moderation.ts)
295 "GET /user/blocks": { summary: "List the people you blocked", description: "People only. See [Blocking and reporting](/docs/accounts/blocking-and-reporting).", access: "account" },
296 "GET /user/blocks/{username}": { summary: "Check whether you blocked a person", description: "`204` if you did, `404` if not.", access: "account", status: 204 },
297 "PUT /user/blocks/{username}": {
298 summary: "Block a person",
299 description: "In your repositories they (and their agents) can't open issues or pull requests, comment, review or react; they stop being a collaborator, pending invitations between you are cancelled and you get no notifications from them. Audited as `user.block`.",
300 access: "account",
301 status: 204,
302 },
303 "DELETE /user/blocks/{username}": { summary: "Unblock a person", description: "Audited as `user.unblock`.", access: "account", status: 204 },
304 "GET /orgs/{org}/blocks": { summary: "List the people an organization blocked", description: "Organization owners and site administrators.", access: "org-owner" },
305 "GET /orgs/{org}/blocks/{username}": { summary: "Check whether an organization blocked a person", description: "`204` if it did, `404` if not.", access: "org-owner", status: 204 },
306 "PUT /orgs/{org}/blocks/{username}": { summary: "Block a person from an organization", description: "As blocking for yourself, for every repository the organization owns. Members can't be blocked. Audited as `org.block`.", access: "org-owner", status: 204 },
307 "DELETE /orgs/{org}/blocks/{username}": { summary: "Unblock a person from an organization", description: "Audited as `org.unblock`.", access: "org-owner", status: 204 },
308 "POST /diffinery/reports": {
309 summary: "Report content to the site administrators",
310 description: "Diffinery only. People only, at most 20 a day and one open report per thing. Returns the report (`id`, `kind`, `target`, `html_url`, `reason`, `body`, `state`, `created_at`). Audited as `report.create`.",
311 access: "account",
312 status: 201,
313 body: {
314 kind: ["string", "`user`, `repository`, `issue`, `pull_request` or `comment`.", true],
315 login: ["string", "The person, for `user`."],
316 repository: ["string", "`owner/name`, for everything but `user`."],
317 number: ["integer", "The issue or pull request, for `issue` and `pull_request`."],
318 comment_id: ["string", "The comment, for `comment`."],
319 reason: ["string", "`spam`, `harassment`, `hate`, `violence`, `sexual`, `malware`, `impersonation` or `other`.", true],
320 body: ["string", "A note for the administrators (up to 2,000 characters; required for `other`)."],
321 },
322 },
323 "GET /admin/reports": {
324 summary: "List reports",
325 description: "Site administrators only. Newest first, with the reporter, the account reported, the excerpt kept when it was reported and the resolution. The total is in `x-total-count`.",
326 access: "site-admin",
327 query: { state: ["string", "`open` (default), `resolved` or `all`."], ...page },
328 },
329 "GET /admin/reports/{id}": { summary: "Get a report", description: "Site administrators only.", access: "site-admin" },
330 "PATCH /admin/reports/{id}": {
331 summary: "Resolve or reopen a report",
332 description: "Site administrators only. Audited as `report.resolve` or `report.reopen`.",
333 access: "site-admin",
334 body: { state: ["string", "`resolved` or `open`.", true], resolution: ["string", "What you did, for `resolved` (up to 2,000 characters)."] },
335 },
336 "PUT /users/{username}/suspended": {
337 summary: "Suspend a person",
338 description: "Site administrators only (GitHub Enterprise's endpoint). Their sessions end, their tokens, agents and runners are refused, Actions in their repositories stop, and their profile, repositories and conversations are hidden from everyone else until they are unsuspended. Audited as `user.suspend`.",
339 access: "site-admin",
340 status: 204,
341 body: { reason: ["string", "Why (up to 500 characters). Emailed to them and kept in the audit log.", true] },
342 },
343 "DELETE /users/{username}/suspended": { summary: "Unsuspend a person", description: "Site administrators only. Everything comes back, tokens included. Audited as `user.unsuspend`.", access: "site-admin", status: 204 },
344 "PUT /repos/{owner}/{repo}/diffinery/comments/{id}/minimized": {
345 summary: "Hide (minimize) a comment",
346 description: "Diffinery only (GitHub's GraphQL `minimizeComment`). Write access, people only. The comment is shown collapsed with its reason. Audited as `comment.minimize`.",
347 access: "write",
348 body: { reason: ["string", "`spam`, `abuse`, `off_topic`, `outdated`, `duplicate` or `resolved` (or GitHub's `classifier`, such as `OFF_TOPIC`).", true] },
349 },
350 "DELETE /repos/{owner}/{repo}/diffinery/comments/{id}/minimized": { summary: "Show a hidden comment again", description: "Diffinery only. Write access, people only. Audited as `comment.unminimize`.", access: "write" },
351
352 // ------------------------------------------------------------ search
353 "GET /search/issues": {
354 summary: "Search issues and pull requests",
355 description:
356 "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.",
357 access: "public",
358 query: {
359 q: ["string", "Search query, at most 256 characters.", true],
360 sort: ["string", "`created` (default), `updated` or `comments`."],
361 order: ["string", "`desc` (default) or `asc`."],
362 ...page,
363 },
364 },
365 "GET /search/repositories": {
366 summary: "Search repositories",
367 description:
368 "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.",
369 access: "public",
370 query: {
371 q: ["string", "Search query, at most 256 characters.", true],
372 sort: ["string", "`updated` sorts by last push; anything else keeps the default order."],
373 order: ["string", "With `sort=updated`: `desc` (default) or `asc`."],
374 ...page,
375 },
376 },
377
378 // ------------------------------------------------------------ repositories
379 "GET /user/repos": { summary: "List repositories you can access", description: "A token limited to repositories lists only those repositories.", access: "account" },
380 "POST /user/repos": {
381 summary: "Create a repository for yourself",
382 description: "An agent's repositories belong to its owner.",
383 access: "account",
384 status: 201,
385 body: {
386 name: ["string", "Repository name.", true],
387 description: ["string", "Short description."],
388 visibility: ["string", "`private` (default) or `public`."],
389 default_branch: ["string", "Default branch name (default `main`)."],
390 },
391 },
392 "GET /users/{login}/repos": { summary: "List an account's repositories", description: "Only the ones you can see. A token limited to repositories lists only those repositories.", access: "account" },
393 "GET /orgs/{org}/repos": { summary: "List an organization's repositories", description: "Only the ones you can see. A token limited to repositories lists only those repositories.", access: "account" },
394 "POST /orgs/{org}/repos": {
395 summary: "Create an organization repository",
396 access: "account",
397 status: 201,
398 body: {
399 name: ["string", "Repository name.", true],
400 description: ["string", "Short description."],
401 visibility: ["string", "`private` (default) or `public`."],
402 default_branch: ["string", "Default branch name (default `main`)."],
403 },
404 },
405 "GET /repos/{owner}/{repo}": { summary: "Get a repository", description: "Includes your role (`permissions.role`) and repository statistics.", access: "read" },
406 "PATCH /repos/{owner}/{repo}": {
407 summary: "Update a repository",
408 access: "admin",
409 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.",
410 body: {
411 name: ["string", "New name."],
412 description: ["string", "New description."],
413 visibility: ["string", "`private` or `public`."],
414 default_branch: ["string", "New default branch (must exist)."],
415 archived: ["boolean", "Archive (read-only) or unarchive."],
416 },
417 },
418 "POST /repos/{owner}/{repo}/diffinery/purge-unreachable": {
419 summary: "Purge unreachable objects",
420 description: "Permanently deletes every commit, tree and file that no branch, tag, pull request, merge queue entry or unfinished workflow run reaches, and empties the archive cache, so a secret force-pushed away can't be fetched by its SHA. Returns `{ objects, bytes }`. People only, not agents. Repositories of up to 300,000 objects (422 otherwise). From a browser session whose sign-in is more than 2 hours old: 403 `Confirm it's you: sign in with your passkey again to do this.` with a `confirm` URL. Audited as `repo.purge_unreachable`. See /docs/security/push-protection#removing-a-secret-that-was-pushed.",
421 access: "admin",
422 },
423 "POST /repos/{owner}/{repo}/diffinery/delete-closed-pulls": {
424 summary: "Delete closed pull requests",
425 description: "For starting over with fresh history after rewriting the branches. Permanently deletes every closed and merged pull request (not ones still in the merge queue) with its comments, reviews, review comments, timeline events, reactions and `refs/pull/{number}/*` refs, then purges unreachable objects as `POST .../diffinery/purge-unreachable` does. Issues, open pull requests, branches, tags and workflow runs stay; pull request numbers are not reused. Returns `{ pull_requests, objects, bytes }`. People only, not agents. Repositories of up to 300,000 objects (422 otherwise). From a browser session whose sign-in is more than 2 hours old: 403 `Confirm it's you: sign in with your passkey again to do this.` with a `confirm` URL. Audited as `repo.delete_closed_pulls`. See /docs/repositories/overview#start-over-with-fresh-history.",
426 access: "admin",
427 },
428 "DELETE /repos/{owner}/{repo}": { summary: "Delete a repository", description: "People only; agents can't delete repositories. Works on an archived repository too. Audited. From a browser session whose sign-in is more than 2 hours old: 403 `Confirm it's you: sign in with your passkey again to do this.` with a `confirm` URL (see /docs/security/overview#confirming-its-you).", access: "admin", status: 204 },
429 "POST /repos/{owner}/{repo}/transfer": {
430 summary: "Transfer a repository",
431 description:
432 "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. From a browser session whose sign-in is more than 2 hours old: 403 `Confirm it's you: sign in with your passkey again to do this.` with a `confirm` URL (see /docs/security/overview#confirming-its-you).",
433 access: "admin",
434 status: 202,
435 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)."] },
436 },
437 "GET /repos/{owner}/{repo}/collaborators": { summary: "List collaborators", description: "Each entry carries `role_name`.", access: "read" },
438 "PUT /repos/{owner}/{repo}/collaborators/{login}": {
439 summary: "Invite a collaborator, or change one's role",
440 description:
441 "As on GitHub, a person without access gets an invitation and has access once they accept it: the answer is `201` with the invitation (`id`, `repository`, `invitee`, `inviter`, `permissions`, `expires_at`), audited as `repo.invite`; asking again while it's pending changes its role. Agents, existing collaborators, members of the owning organization and yourself get the role at once: `204`, audited as `repo.collaborator.set`.",
442 access: "admin",
443 status: 201,
444 body: { permission: ["string", "`read`, `triage`, `write` (default), `maintain` or `admin`; GitHub's `pull` and `push` work too."] },
445 },
446 "DELETE /repos/{owner}/{repo}/collaborators/{login}": { summary: "Remove a collaborator", description: "Audited as `repo.collaborator.remove`.", access: "admin", status: 204 },
447 "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" },
448 "DELETE /repos/{owner}/{repo}/invitations/{id}": {
449 summary: "Revoke a repository invitation",
450 description: "The link stops working and the address can be invited again. 404 when no pending invitation to this repository has this id. Audited.",
451 access: "admin",
452 status: 204,
453 },
454 "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 } },
455 "GET /repos/{owner}/{repo}/assignees/{login}": {
456 summary: "Check if someone can be assigned",
457 description: "204 when `login` is the repository's owner or a collaborator with triage access or more, 404 otherwise.",
458 access: "read",
459 status: 204,
460 },
461 "GET /repos/{owner}/{repo}/languages": {
462 summary: "List languages",
463 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.",
464 access: "read",
465 },
466 "GET /repos/{owner}/{repo}/topics": { summary: "Get topics", description: "Returns `{ names }`.", access: "read" },
467 "PUT /repos/{owner}/{repo}/topics": {
468 summary: "Replace topics",
469 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.",
470 access: "admin",
471 body: { names: ["string[]", "Every topic the repository should have (`[]` clears them).", true] },
472 },
473 "POST /repos/{owner}/{repo}/dispatches": {
474 summary: "Trigger a repository_dispatch event",
475 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, and with 429 when the repository already has 500 unfinished workflow runs.",
476 access: "write",
477 status: 204,
478 body: {
479 event_type: ["string", "Event type, at most 100 characters.", true],
480 client_payload: ["object", "Data for workflows as `github.event.client_payload`: at most 10 top-level keys and 64 KB."],
481 },
482 },
483
484 // ------------------------------------------------------------ branches, tags, refs and commits
485 "GET /repos/{owner}/{repo}/branches": { summary: "List branches", description: "Each branch says whether it is `protected` and the `default`.", access: "read" },
486 "GET /repos/{owner}/{repo}/branches/{branch}": { summary: "Get a branch", access: "read" },
487 "GET /repos/{owner}/{repo}/tags": { summary: "List tags", access: "read" },
488 "POST /repos/{owner}/{repo}/merges": {
489 summary: "Merge a branch",
490 description:
491 "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.",
492 access: "write",
493 status: 201,
494 body: {
495 base: ["string", "Branch to merge into.", true],
496 head: ["string", "Branch, tag or commit to merge.", true],
497 commit_message: ["string", "Commit message (default: `Merge {head} into {base}`)."],
498 },
499 },
500 "GET /repos/{owner}/{repo}/git/refs/{path}": { summary: "List references", description: "Every ref under `refs/{path}`, for example `heads` or `tags/v1`.", access: "read" },
501 "POST /repos/{owner}/{repo}/git/refs": {
502 summary: "Create a reference",
503 description: "Branch protection and agent rules apply as they do to a push.",
504 access: "write",
505 status: 201,
506 body: { ref: ["string", "Full ref name, like `refs/heads/feature`.", true], sha: ["string", "Commit to point at.", true] },
507 },
508 "PATCH /repos/{owner}/{repo}/git/refs/{path}": {
509 summary: "Update a reference",
510 access: "write",
511 body: { sha: ["string", "New target commit.", true], force: ["boolean", "Allow a non-fast-forward update."] },
512 },
513 "DELETE /repos/{owner}/{repo}/git/refs/{path}": { summary: "Delete a reference", access: "write", status: 204 },
514 "GET /repos/{owner}/{repo}/commits": {
515 summary: "List commits",
516 access: "read",
517 query: { sha: ["string", "Branch, tag or commit to start from (default: the default branch)."], path: ["string", "Only commits that touch this path."], ...page },
518 },
519 "GET /repos/{owner}/{repo}/commits/{ref}": { summary: "Get a commit", description: "The commit with its diff.", access: "read" },
520 "GET /repos/{owner}/{repo}/commits/{sha}/pulls": {
521 summary: "List pull requests for a commit",
522 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.",
523 access: "read",
524 query: { ...page },
525 },
526 "GET /repos/{owner}/{repo}/compare/{spec}": { summary: "Compare two refs", description: "`spec` is `base...head`.", access: "read" },
527 "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" },
528
529 // ------------------------------------------------------------ contents
530 "GET /repos/{owner}/{repo}/contents": { summary: "List the root directory", access: "read", query: { ref: ["string", "Branch, tag or commit (default: the default branch)."] } },
531 "GET /repos/{owner}/{repo}/contents/{path}": {
532 summary: "Get a file or directory",
533 description: "A directory returns an array of entries; a file returns its content base64-encoded.",
534 access: "read",
535 query: { ref: ["string", "Branch, tag or commit (default: the default branch)."] },
536 },
537 "PUT /repos/{owner}/{repo}/contents/{path}": {
538 summary: "Create or update a file",
539 description: "Commits one file.",
540 access: "write",
541 status: 201,
542 body: { message: ["string", "Commit message.", true], content: ["string", "New content, base64-encoded.", true], branch: ["string", "Branch to commit to (default: the default branch)."] },
543 },
544 "DELETE /repos/{owner}/{repo}/contents/{path}": {
545 summary: "Delete a file",
546 access: "write",
547 body: { message: ["string", "Commit message.", true], branch: ["string", "Branch to commit to (default: the default branch)."] },
548 },
549 "POST /repos/{owner}/{repo}/diffinery/commits": {
550 summary: "Commit several files at once",
551 description:
552 "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.",
553 access: "write",
554 status: 201,
555 body: {
556 branch: ["string", "Branch to commit to.", true],
557 message: ["string", "Commit message.", true],
558 files: ["array", "Changes: `{ path, content, encoding: \"utf-8\" | \"base64\", mode: \"100644\" | \"100755\" }` or `{ path, delete: true }`.", true],
559 base: ["string", "Create `branch` from this ref when it doesn't exist."],
560 expected_head_sha: ["string", "Fail with 409 if the branch has moved."],
561 allow_secrets: ["string", "Why a detected secret may be committed anyway (repository admins who are people; at most 200 characters are kept)."],
562 },
563 },
564 "GET /repos/{owner}/{repo}/readme": {
565 summary: "Get the README",
566 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.",
567 access: "read",
568 query: { ref: ["string", "Branch, tag or commit (default: the default branch)."] },
569 },
570 "GET /repos/{owner}/{repo}/readme/{dir}": {
571 summary: "Get a directory's README",
572 description: "Like the root README, for the directory `dir`.",
573 access: "read",
574 query: { ref: ["string", "Branch, tag or commit (default: the default branch)."] },
575 },
576 ...archiveDocs("tarball", "a tarball", "`.tar.gz`"),
577 ...archiveDocs("zipball", "a zip archive", "`.zip` (at most 65535 files)"),
578
579 // ------------------------------------------------------------ releases
580 "GET /repos/{owner}/{repo}/releases": { summary: "List releases", description: "Newest first. Drafts are included only for people with write access.", access: "read", query: { ...page } },
581 "POST /repos/{owner}/{repo}/releases": {
582 summary: "Create a release",
583 description:
584 "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).",
585 access: "write",
586 status: 201,
587 body: {
588 tag_name: ["string", "Tag to release.", true],
589 target_commitish: ["string", "Branch, tag or commit for a new tag (default: the default branch)."],
590 name: ["string", "Release title."],
591 body: ["string", "Markdown notes, at most 125000 characters."],
592 draft: ["boolean", "Save as a draft."],
593 prerelease: ["boolean", "Mark as a prerelease."],
594 make_latest: ["string", "`true` (default), `false`, or `legacy` (the newest by date)."],
595 generate_release_notes: ["boolean", "Append generated notes to `body`."],
596 },
597 },
598 "GET /repos/{owner}/{repo}/releases/{id}": { summary: "Get a release", description: "Drafts are 404 for people without write access.", access: "read" },
599 "PATCH /repos/{owner}/{repo}/releases/{id}": {
600 summary: "Update a release",
601 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.",
602 access: "write",
603 body: {
604 tag_name: ["string", "New tag."],
605 target_commitish: ["string", "Branch, tag or commit for a new tag."],
606 name: ["string", "Release title."],
607 body: ["string", "Markdown notes, at most 125000 characters."],
608 draft: ["boolean", "Draft or published."],
609 prerelease: ["boolean", "Mark as a prerelease."],
610 make_latest: ["string", "`true`, `false` or `legacy`."],
611 },
612 },
613 "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 },
614 "GET /repos/{owner}/{repo}/releases/{id}/assets": { summary: "List release assets", access: "read", query: { ...page } },
615 "GET /repos/{owner}/{repo}/releases/assets/{id}": {
616 summary: "Get a release asset",
617 description: "The asset's metadata, or the file itself with `Accept: application/octet-stream`.",
618 access: "read",
619 },
620 "PATCH /repos/{owner}/{repo}/releases/assets/{id}": {
621 summary: "Update a release asset",
622 description: "`state` is accepted and ignored.",
623 access: "write",
624 body: { name: ["string", "New file name (unique within the release)."], label: ["string", "Display label."] },
625 },
626 "DELETE /repos/{owner}/{repo}/releases/assets/{id}": { summary: "Delete a release asset", access: "write", status: 204 },
627 "POST /repos/{owner}/{repo}/releases/generate-notes": {
628 summary: "Generate release notes",
629 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 }`.",
630 access: "write",
631 body: {
632 tag_name: ["string", "Tag for the release.", true],
633 target_commitish: ["string", "Used when the tag doesn't exist yet (default: the default branch)."],
634 previous_tag_name: ["string", "Start from this tag (default: the newest earlier published release)."],
635 configuration_file_path: ["string", "Config file to use instead of `.github/release.yml`."],
636 },
637 },
638 "GET /repos/{owner}/{repo}/releases/latest": {
639 summary: "Get the latest release",
640 description: "The release marked latest, or else the newest published release that isn't a prerelease. 404 when there is none.",
641 access: "read",
642 },
643 "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" },
644 ...reactionDocs("/repos/{owner}/{repo}/releases/{id}", "a release", "Drafts are hidden from people without write access, and so are their reactions."),
645
646 // ------------------------------------------------------------ git database
647 "POST /repos/{owner}/{repo}/git/blobs": {
648 summary: "Create a blob",
649 description: "Push protection applies: content that looks like a credential is refused with 422. Returns `{ url, sha }`.",
650 access: "write",
651 status: 201,
652 body: { content: ["string", "The blob's content.", true], encoding: ["string", "`utf-8` (default) or `base64`."] },
653 },
654 "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" },
655 "POST /repos/{owner}/{repo}/git/trees": {
656 summary: "Create a tree",
657 description:
658 "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.",
659 access: "write",
660 status: 201,
661 body: {
662 tree: ["array", "Entries: `{ path, mode, sha }` or `{ path, mode, content }`. `mode` is `100644` (default), `100755`, `040000`, `160000` or `120000`.", true],
663 base_tree: ["string", "Tree to start from (default: an empty tree)."],
664 },
665 },
666 "GET /repos/{owner}/{repo}/git/trees/{sha}": {
667 summary: "Get a tree",
668 description: "`sha` may be a tree, a commit or a ref. `truncated` is true when a recursive listing stopped at 100000 entries.",
669 access: "read",
670 query: { recursive: ["string", "Any value but `0` or `false` lists every entry below, not just the top level."] },
671 },
672 "POST /repos/{owner}/{repo}/git/commits": {
673 summary: "Create a Git commit",
674 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.",
675 access: "write",
676 status: 201,
677 body: {
678 message: ["string", "Commit message.", true],
679 tree: ["string", "SHA of the commit's tree.", true],
680 parents: ["string[]", "Parent commit SHAs (none for a root commit)."],
681 author: ["object", "`{ name, email, date }`."],
682 committer: ["object", "`{ name, email, date }` (default: the author)."],
683 signature: ["string", "Signature to store in the commit as given."],
684 },
685 },
686 "GET /repos/{owner}/{repo}/git/commits/{sha}": { summary: "Get a Git commit", access: "read" },
687 "POST /repos/{owner}/{repo}/git/tags": {
688 summary: "Create a tag object",
689 description: "Writes an annotated tag object only; create its `refs/tags/...` reference with `POST /repos/{owner}/{repo}/git/refs` to publish it.",
690 access: "write",
691 status: 201,
692 body: {
693 tag: ["string", "Tag name.", true],
694 object: ["string", "SHA of the object to tag.", true],
695 message: ["string", "Tag message."],
696 type: ["string", "`commit`, `tree`, `blob` or `tag`; must match the object (default: its type)."],
697 tagger: ["object", "`{ name, email, date }` (default: you)."],
698 },
699 },
700 "GET /repos/{owner}/{repo}/git/tags/{sha}": { summary: "Get a tag object", description: "`verification.verified` is always false.", access: "read" },
701 "GET /repos/{owner}/{repo}/git/ref/{ref}": {
702 summary: "Get a reference",
703 description: "`ref` is the name after `refs/`, like `heads/main` or `tags/v1.0`, and must match exactly.",
704 access: "read",
705 },
706 "GET /repos/{owner}/{repo}/git/matching-refs/{ref}": {
707 summary: "List matching references",
708 description: "Refs whose names start with `refs/{ref}`, like `heads/feature` or `tags/v1.`. Pull request refs appear only when `ref` starts with `pull/`.",
709 access: "read",
710 query: { per_page: ["integer", "Results per page (default 100, at most 100)."], page: page.page! },
711 },
712 "GET /repos/{owner}/{repo}/git/refs": {
713 summary: "List all references",
714 description: "Every branch, tag and other ref, without pull request refs.",
715 access: "read",
716 query: { per_page: ["integer", "Results per page (default 100, at most 100)."], page: page.page! },
717 },
718
719 // ------------------------------------------------------------ branch protection
720 "GET /repos/{owner}/{repo}/branches/{branch}/protection": { summary: "Get a protection rule", description: "`branch` is the rule's pattern.", access: "admin" },
721 "PUT /repos/{owner}/{repo}/branches/{branch}/protection": {
722 summary: "Create or replace a protection rule",
723 description:
724 "`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`. `autoMergeAgents` (agent logins, needs `allowAgentAutoMerge`) has Diffinery turn auto-merge on by itself, as the agent, for pull requests those agents open, mark ready or push to, with `autoMergeMethod` (`merge`, `squash` or `rebase`) for how they merge; the answer stores them as `{ id, login }`, and changes are audited as `repo.protection.agent_auto_merge_rule`. See the branch protection guide for every field.",
725 access: "admin",
726 },
727 "DELETE /repos/{owner}/{repo}/branches/{branch}/protection": { summary: "Delete a protection rule", description: "Audited as `repo.protection.delete`.", access: "admin", status: 204 },
728
729 // ------------------------------------------------------------ issues
730 "GET /repos/{owner}/{repo}/issues": {
731 summary: "List issues and pull requests",
732 description: "As on GitHub, this list includes pull requests. The total is in the `X-Total-Count` header.",
733 access: "read",
734 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 },
735 },
736 "POST /repos/{owner}/{repo}/issues": {
737 summary: "Create an issue",
738 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.",
739 access: "read",
740 status: 201,
741 body: { title: ["string", "Title.", true], body: ["string", "Markdown body."], labels: ["string[]", "Label names."], assignees: ["string[]", "Logins."] },
742 },
743 "GET /repos/{owner}/{repo}/issues/{number}": { summary: "Get an issue", access: "read" },
744 "PATCH /repos/{owner}/{repo}/issues/{number}": {
745 summary: "Update an issue",
746 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.",
747 access: "read",
748 body: {
749 title: ["string", "New title."],
750 body: ["string", "New body."],
751 state: ["string", "`open` or `closed`."],
752 state_reason: ["string", "Why it was closed, like `completed` or `not_planned`."],
753 labels: ["string[]", "Replace the labels."],
754 assignees: ["string[]", "Replace the assignees."],
755 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."],
756 },
757 },
758 "GET /repos/{owner}/{repo}/milestones": {
759 summary: "List milestones",
760 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.",
761 access: "read",
762 query: { state: ["string", "`open` (default), `closed` or `all`. Anything else is 422."] },
763 },
764 "GET /repos/{owner}/{repo}/milestones/{number}": { summary: "Get a milestone", description: "`number` is the milestone number. 404 `milestone not found`.", access: "read" },
765 "POST /repos/{owner}/{repo}/milestones": {
766 summary: "Create a milestone",
767 description: "New milestones are open unless `state` is `closed`. There is no endpoint to delete a milestone.",
768 access: "triage",
769 status: 201,
770 body: {
771 title: ["string", "1 to 255 characters.", true],
772 description: ["string", "At most 65,536 characters."],
773 state: ["string", "`open` (default) or `closed`."],
774 due_on: ["string", "ISO 8601 timestamp, or `null`."],
775 },
776 },
777 "PATCH /repos/{owner}/{repo}/milestones/{number}": {
778 summary: "Update a milestone",
779 description: "Send only the fields you change. Closing records `closed_at`; reopening clears it.",
780 access: "triage",
781 body: {
782 title: ["string", "1 to 255 characters."],
783 description: ["string", "At most 65,536 characters."],
784 state: ["string", "`open` or `closed`."],
785 due_on: ["string", "ISO 8601 timestamp, or `null` to clear it."],
786 },
787 },
788 "GET /repos/{owner}/{repo}/issues/{number}/comments": {
789 summary: "List comments",
790 description: "Conversation comments on an issue or pull request (not review comments).",
791 access: "read",
792 query: { since: ["string", "Only comments updated at or after this ISO timestamp."], ...page },
793 },
794 "POST /repos/{owner}/{repo}/issues/{number}/comments": { summary: "Add a comment", access: "read", status: 201, body: { body: ["string", "Markdown body.", true] } },
795 "GET /repos/{owner}/{repo}/issues/comments": {
796 summary: "List comments in a repository",
797 description: "Conversation comments on every issue and pull request.",
798 access: "read",
799 query: {
800 since: ["string", "Only comments updated at or after this ISO timestamp."],
801 sort: ["string", "`created` (default) or `updated`."],
802 direction: ["string", "`asc` (default) or `desc`."],
803 ...page,
804 },
805 },
806 "GET /repos/{owner}/{repo}/issues/comments/{id}": { summary: "Get a comment", description: "`id` is the numeric id or Diffinery's own.", access: "read" },
807 "PATCH /repos/{owner}/{repo}/issues/comments/{id}": { summary: "Edit a comment", access: "read", body: { body: ["string", "New Markdown body.", true] } },
808 "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 },
809 ...reactionDocs("/repos/{owner}/{repo}/issues/{number}", "an issue", "Works for pull requests too."),
810 ...reactionDocs("/repos/{owner}/{repo}/issues/comments/{id}", "a comment"),
811 "POST /repos/{owner}/{repo}/issues/{number}/assignees": {
812 summary: "Add assignees",
813 description: "Adds to the current assignees, at most 10 in all. Logins aren't checked against who can be assigned. Returns the issue.",
814 access: "triage",
815 status: 201,
816 body: { assignees: ["string[]", "Logins to add."] },
817 },
818 "DELETE /repos/{owner}/{repo}/issues/{number}/assignees": { summary: "Remove assignees", description: "Returns the issue.", access: "triage", body: { assignees: ["string[]", "Logins to remove."] } },
819 "PUT /repos/{owner}/{repo}/issues/{number}/lock": {
820 summary: "Lock a conversation",
821 description: "While locked, only people with triage access can comment, review or react.",
822 access: "triage",
823 status: 204,
824 body: { lock_reason: ["string", "`off-topic`, `too heated`, `resolved` or `spam`."] },
825 },
826 "DELETE /repos/{owner}/{repo}/issues/{number}/lock": { summary: "Unlock a conversation", access: "triage", status: 204 },
827 "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 } },
828 "GET /repos/{owner}/{repo}/issues/{number}/timeline": {
829 summary: "List an issue's timeline",
830 description: "Events, comments (`commented`) and reviews (`reviewed`) together, oldest first.",
831 access: "read",
832 query: { ...page },
833 },
834 "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 } },
835 "GET /repos/{owner}/{repo}/issues/events/{id}": { summary: "Get an event", access: "read" },
836 "GET /repos/{owner}/{repo}/labels": { summary: "List labels", access: "read" },
837 "POST /repos/{owner}/{repo}/labels": {
838 summary: "Create a label",
839 description: "422 when a label with that name exists; change one with `PATCH /repos/{owner}/{repo}/labels/{name}`.",
840 access: "triage",
841 status: 201,
842 body: { name: ["string", "Label name, at most 100 characters.", true], color: ["string", "6-digit hex color, with or without `#` (default `ededed`)."], description: ["string", "Description."] },
843 },
844 "GET /repos/{owner}/{repo}/labels/{name}": { summary: "Get a label", access: "read" },
845 "PATCH /repos/{owner}/{repo}/labels/{name}": {
846 summary: "Update a label",
847 description: "Renaming it renames it on every issue too.",
848 access: "triage",
849 body: { new_name: ["string", "New name."], color: ["string", "6-digit hex color, with or without `#`."], description: ["string", "Description."] },
850 },
851 "DELETE /repos/{owner}/{repo}/labels/{name}": { summary: "Delete a label", description: "Also removes it from every issue.", access: "triage", status: 204 },
852 "GET /repos/{owner}/{repo}/issues/{number}/labels": { summary: "List an issue's labels", access: "read", query: { ...page } },
853 "POST /repos/{owner}/{repo}/issues/{number}/labels": {
854 summary: "Add labels to an issue",
855 description: "Labels that don't exist yet are created. At most 100 per issue. Returns the issue's labels.",
856 access: "triage",
857 body: { labels: ["string[]", "Label names (`{ name }` objects, or a bare array as the body, work too).", true] },
858 },
859 "PUT /repos/{owner}/{repo}/issues/{number}/labels": {
860 summary: "Replace an issue's labels",
861 description: "Labels that don't exist yet are created; `[]` removes them all. Returns the issue's labels.",
862 access: "triage",
863 body: { labels: ["string[]", "Label names (`{ name }` objects, or a bare array as the body, work too).", true] },
864 },
865 "DELETE /repos/{owner}/{repo}/issues/{number}/labels": { summary: "Remove all labels from an issue", access: "triage", status: 204 },
866 "DELETE /repos/{owner}/{repo}/issues/{number}/labels/{name}": {
867 summary: "Remove a label from an issue",
868 description: "Returns the labels left. 404 when the issue doesn't have it.",
869 access: "triage",
870 },
871
872 // ------------------------------------------------------------ pull requests
873 "GET /repos/{owner}/{repo}/pulls": {
874 summary: "List pull requests",
875 description: "The total is in the `X-Total-Count` header.",
876 access: "read",
877 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 },
878 },
879 "POST /repos/{owner}/{repo}/pulls": {
880 summary: "Open a pull request",
881 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.",
882 access: "write",
883 status: 201,
884 body: {
885 title: ["string", "Title.", true],
886 head: ["string", "Branch with the changes (`owner:branch` also works).", true],
887 base: ["string", "Branch to merge into (default: the default branch)."],
888 body: ["string", "Markdown description."],
889 intent: ["string", "Diffinery: what the change is meant to do, shown to reviewers."],
890 draft: ["boolean", "Open as a draft."],
891 reviewers: ["string[]", "Logins to request reviews from."],
892 auto_merge: ["boolean", "Diffinery: `true` turns on auto-merge as it opens, `false` keeps it off where your setting would turn it on. When asked for and it stayed off (for example, the base branch has no required checks), the answer's `auto_merge_skipped` says why. See /docs/collaboration/merge-on-green."],
893 },
894 },
895 "GET /repos/{owner}/{repo}/pulls/{number}": {
896 summary: "Get a pull request",
897 description: "Includes `mergeable`, `mergeable_state`, `merge_blockers` (why it can't merge yet), `approvals`, `required_approvals` and `checks`.",
898 access: "read",
899 },
900 "PATCH /repos/{owner}/{repo}/pulls/{number}": {
901 summary: "Update a pull request",
902 description:
903 "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`.",
904 access: "read",
905 body: {
906 title: ["string", "New title."],
907 body: ["string", "New description."],
908 state: ["string", "`open` or `closed`."],
909 draft: ["boolean", "`false` marks it ready for review; `true` converts it back to a draft."],
910 },
911 },
912 "PUT /repos/{owner}/{repo}/pulls/{number}/update-branch": {
913 summary: "Update a pull request's branch",
914 description:
915 "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.",
916 access: "write",
917 status: 202,
918 body: { expected_head_sha: ["string", "Fail unless the head branch is at this commit."] },
919 },
920 "GET /repos/{owner}/{repo}/pulls/{number}/files": { summary: "List changed files", description: "Each file with its status, additions, deletions and patch.", access: "read" },
921 "GET /repos/{owner}/{repo}/pulls/{number}/commits": { summary: "List a pull request's commits", access: "read" },
922 "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" },
923 "PUT /repos/{owner}/{repo}/pulls/{number}/merge": {
924 summary: "Merge a pull request",
925 description:
926 "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.",
927 access: "write",
928 body: {
929 merge_method: ["string", "`merge`, `squash` or `rebase`."],
930 commit_title: ["string", "Title of the merge or squash commit."],
931 commit_message: ["string", "Body of the merge or squash commit."],
932 sha: ["string", "Fail if the head has moved from this commit."],
933 delete_branch: ["boolean", "Delete the head branch after merging."],
934 bypass_merge_queue: ["boolean", "Merge directly even when a queue is required (admins). Doesn't skip unmet rules; send `bypass_rules` too for that."],
935 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."],
936 },
937 },
938 "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" },
939 "PUT /repos/{owner}/{repo}/pulls/{number}/merge-queue": {
940 summary: "Add a pull request to the merge queue",
941 description: "Diffinery only. Returns 201 with the entry when queued.",
942 access: "write",
943 status: 201,
944 body: { merge_method: ["string", "`merge`, `squash` or `rebase` (default: the queue's method)."], delete_branch: ["boolean", "Delete the head branch once merged."] },
945 },
946 "DELETE /repos/{owner}/{repo}/pulls/{number}/merge-queue": { summary: "Remove a pull request from the merge queue", access: "write", status: 204 },
947 "PUT /repos/{owner}/{repo}/pulls/{number}/auto-merge": {
948 summary: "Enable auto-merge",
949 description:
950 "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`.",
951 access: "write",
952 body: {
953 merge_method: ["string", "`merge`, `squash` or `rebase` (default: the queue's method, `squash` where linear history is required, else `merge`)."],
954 commit_title: ["string", "Title of the merge or squash commit."],
955 commit_message: ["string", "Body of the merge or squash commit."],
956 delete_branch: ["boolean", "Delete the head branch once auto-merge lands the pull request, directly or through the queue (never the default branch or a protected one). 422 `delete_branch must be a boolean` otherwise."],
957 sha: ["string", "Turn auto-merge on only if the head is still this commit (409 if it moved)."],
958 },
959 },
960 "DELETE /repos/{owner}/{repo}/pulls/{number}/auto-merge": {
961 summary: "Disable auto-merge",
962 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`.",
963 access: "write",
964 status: 204,
965 },
966 "GET /repos/{owner}/{repo}/pulls/{number}/reviews": { summary: "List reviews", access: "read" },
967 "POST /repos/{owner}/{repo}/pulls/{number}/reviews": {
968 summary: "Submit a review",
969 description:
970 "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.",
971 access: "read",
972 status: 201,
973 body: {
974 event: ["string", "`APPROVE`, `REQUEST_CHANGES` or `COMMENT` (default)."],
975 body: ["string", "Review summary."],
976 comments: ["array", "Line comments: `{ path, line, side: \"LEFT\" | \"RIGHT\", body }`."],
977 },
978 },
979 "GET /repos/{owner}/{repo}/pulls/{number}/reviews/{id}": { summary: "Get a review", description: "`id` is the numeric id or Diffinery's own.", access: "read" },
980 "PUT /repos/{owner}/{repo}/pulls/{number}/reviews/{id}/dismissals": {
981 summary: "Dismiss a review",
982 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.",
983 access: "maintain",
984 body: { message: ["string", "Why the review is dismissed.", true] },
985 },
986 "GET /repos/{owner}/{repo}/pulls/{number}/comments": { summary: "List review comments", description: "Line comments from reviews.", access: "read", query: { ...page } },
987 "POST /repos/{owner}/{repo}/pulls/{number}/comments": {
988 summary: "Create a review comment",
989 description:
990 "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.",
991 access: "read",
992 status: 201,
993 body: {
994 body: ["string", "Markdown body.", true],
995 path: ["string", "File path (required unless replying)."],
996 line: ["integer", "Line number (required unless replying)."],
997 side: ["string", "`RIGHT` (default) or `LEFT`."],
998 position: ["integer", "Used as `line` when `line` is missing."],
999 commit_id: ["string", "Accepted and ignored."],
1000 in_reply_to: ["integer", "Review comment id to reply to; its path and line are used."],
1001 },
1002 },
1003 "POST /repos/{owner}/{repo}/pulls/{number}/comments/{id}/replies": {
1004 summary: "Reply to a review comment",
1005 description: "Adds a reply on the same line, as its own `COMMENTED` review.",
1006 access: "read",
1007 status: 201,
1008 body: { body: ["string", "Markdown body.", true] },
1009 },
1010 "GET /repos/{owner}/{repo}/pulls/comments": {
1011 summary: "List review comments in a repository",
1012 access: "read",
1013 query: {
1014 since: ["string", "Only comments updated at or after this ISO timestamp."],
1015 sort: ["string", "`created` (default) or `updated`."],
1016 direction: ["string", "`asc` (default) or `desc`."],
1017 ...page,
1018 },
1019 },
1020 "GET /repos/{owner}/{repo}/pulls/comments/{id}": { summary: "Get a review comment", access: "read" },
1021 "PATCH /repos/{owner}/{repo}/pulls/comments/{id}": {
1022 summary: "Edit a review comment",
1023 description: "Your own comments, or anyone's with maintain access.",
1024 access: "read",
1025 body: { body: ["string", "New Markdown body.", true] },
1026 },
1027 "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 },
1028 ...reactionDocs("/repos/{owner}/{repo}/pulls/comments/{id}", "a review comment"),
1029 "GET /repos/{owner}/{repo}/pulls/{number}/requested_reviewers": { summary: "List requested reviewers", description: "Returns `{ users, teams }`; `teams` is always empty.", access: "read" },
1030 "POST /repos/{owner}/{repo}/pulls/{number}/requested_reviewers": {
1031 summary: "Request reviewers",
1032 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.",
1033 access: "read",
1034 status: 201,
1035 body: { reviewers: ["string[]", "Logins."], team_reviewers: ["string[]", "Must be empty or absent."] },
1036 },
1037 "DELETE /repos/{owner}/{repo}/pulls/{number}/requested_reviewers": {
1038 summary: "Remove requested reviewers",
1039 description: "The author, or anyone with triage access. Teams aren't supported. Returns the pull request.",
1040 access: "read",
1041 body: { reviewers: ["string[]", "Logins to remove."] },
1042 },
1043
1044 // ------------------------------------------------------------ commit statuses
1045 "POST /repos/{owner}/{repo}/statuses/{sha}": {
1046 summary: "Create a commit status",
1047 description:
1048 "`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).",
1049 access: "write",
1050 status: 201,
1051 body: {
1052 state: ["string", "`error`, `failure`, `pending` or `success`.", true],
1053 target_url: ["string", "http(s) link to details."],
1054 description: ["string", "Short description (cut at 1024 characters)."],
1055 context: ["string", "Label that tells statuses apart (default `default`)."],
1056 },
1057 },
1058 "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 } },
1059 "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 } },
1060 "GET /repos/{owner}/{repo}/commits/{ref}/status": {
1061 summary: "Get the combined status",
1062 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.",
1063 access: "read",
1064 query: { per_page: ["integer", "Statuses per page (default 100, at most 100)."], page: page.page! },
1065 },
1066
1067 // ------------------------------------------------------------ checks
1068 "GET /repos/{owner}/{repo}/commits/{ref}/check-runs": { summary: "List check runs for a commit", access: "read" },
1069 "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" },
1070 "PATCH /repos/{owner}/{repo}/check-runs/{id}": {
1071 summary: "Update a check run",
1072 description:
1073 "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).",
1074 access: "write",
1075 body: {
1076 name: ["string", "New name."],
1077 status: ["string", "`queued`, `in_progress`, `completed`, `waiting`, `requested` or `pending`."],
1078 conclusion: ["string", "`action_required`, `cancelled`, `failure`, `neutral`, `success`, `skipped`, `stale` or `timed_out`."],
1079 details_url: ["string", "http(s) link to more detail."],
1080 external_id: ["string", "Your own reference for the run."],
1081 started_at: ["string", "ISO timestamp."],
1082 completed_at: ["string", "ISO timestamp."],
1083 output: ["object", "`{ title, summary, text, annotations }`."],
1084 },
1085 },
1086 "GET /repos/{owner}/{repo}/check-runs/{id}/annotations": { summary: "List check run annotations", access: "read", query: { ...page } },
1087 "GET /repos/{owner}/{repo}/commits/{ref}/check-suites": {
1088 summary: "List check suites for a commit",
1089 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.",
1090 access: "read",
1091 query: { ...page },
1092 },
1093 "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" },
1094 "GET /repos/{owner}/{repo}/check-suites/{id}/check-runs": { summary: "List a check suite's check runs", access: "read", query: { ...page } },
1095 "POST /repos/{owner}/{repo}/check-runs": {
1096 summary: "Create or update a check run",
1097 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.",
1098 access: "write",
1099 status: 201,
1100 body: {
1101 name: ["string", "Check name.", true],
1102 head_sha: ["string", "Commit the check is for.", true],
1103 status: ["string", "`queued` (default), `in_progress` or `completed`."],
1104 conclusion: ["string", "With `completed`: `success`, `failure`, `neutral`, `cancelled`, `skipped` or `timed_out`."],
1105 details_url: ["string", "Link to more detail."],
1106 output: ["object", "`{ title, summary }`."],
1107 },
1108 },
1109
1110 // ------------------------------------------------------------ deployments
1111 "GET /repos/{owner}/{repo}/deployments": {
1112 summary: "List deployments",
1113 description: "Newest first. Each filter matches exactly.",
1114 access: "read",
1115 query: {
1116 sha: ["string", "Full commit SHA."],
1117 ref: ["string", "Ref, as given when the deployment was created."],
1118 task: ["string", "Task name."],
1119 environment: ["string", "Environment name."],
1120 ...page,
1121 },
1122 },
1123 "POST /repos/{owner}/{repo}/deployments": {
1124 summary: "Create a deployment",
1125 description:
1126 "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).",
1127 access: "write",
1128 status: 201,
1129 body: {
1130 ref: ["string", "Branch, tag or commit to deploy.", true],
1131 task: ["string", "Task name (default `deploy`)."],
1132 payload: ["object", "Extra data for the deployer (a string is kept as is)."],
1133 environment: ["string", "Environment name (default `production`)."],
1134 description: ["string", "Short description."],
1135 transient_environment: ["boolean", "Transient deployments aren't made inactive by later successful ones."],
1136 production_environment: ["boolean", "Default: true when the environment is `production`."],
1137 required_contexts: ["string[]", "Contexts that must have passed; `[]` skips the check."],
1138 },
1139 },
1140 "GET /repos/{owner}/{repo}/deployments/{id}": { summary: "Get a deployment", access: "read" },
1141 "DELETE /repos/{owner}/{repo}/deployments/{id}": {
1142 summary: "Delete a deployment",
1143 description: "Refused with 422 while its latest status is `success`, unless it is the only deployment in its environment. Deletes its statuses too.",
1144 access: "write",
1145 status: 204,
1146 },
1147 "GET /repos/{owner}/{repo}/deployments/{id}/statuses": { summary: "List deployment statuses", description: "Newest first.", access: "read", query: { ...page } },
1148 "POST /repos/{owner}/{repo}/deployments/{id}/statuses": {
1149 summary: "Create a deployment status",
1150 description:
1151 "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.",
1152 access: "write",
1153 status: 201,
1154 body: {
1155 state: ["string", "`error`, `failure`, `inactive`, `in_progress`, `queued`, `pending` or `success`.", true],
1156 log_url: ["string", "http(s) link to the deployment's output."],
1157 target_url: ["string", "Older name for `log_url`, used when `log_url` is missing."],
1158 environment_url: ["string", "http(s) link to the deployed environment."],
1159 description: ["string", "Short description (cut at 140 characters)."],
1160 environment: ["string", "Move the deployment to this environment."],
1161 auto_inactive: ["boolean", "`false` keeps older deployments active (default `true`)."],
1162 },
1163 },
1164 "GET /repos/{owner}/{repo}/deployments/{id}/statuses/{status}": { summary: "Get a deployment status", access: "read" },
1165
1166 // ------------------------------------------------------------ environments
1167 "GET /repos/{owner}/{repo}/environments": { summary: "List environments", access: "read", query: { ...page } },
1168 "GET /repos/{owner}/{repo}/environments/{env}": { summary: "Get an environment", access: "read" },
1169 "PUT /repos/{owner}/{repo}/environments/{env}": {
1170 summary: "Create or update an environment",
1171 description:
1172 "People only; agents can't change environments. Required reviewers and a wait timer hold a job that names the environment before anything starts, until a reviewer approves and the timer has passed. Fields left out keep their values. Audited. See [Required reviewers and wait timers](/docs/actions/environments#required-reviewers-and-wait-timers).",
1173 access: "admin",
1174 body: {
1175 deployment_branch_policy: ["object", "`{ protected_branches, custom_branch_policies }` with exactly one true, or `null` to allow every ref."],
1176 wait_timer: ["integer", "Minutes a job waits before it deploys, 0 to 43200."],
1177 reviewers: ["array", "Up to 6 people with access, as `{type: \"User\", id}` (the numeric id the API shows) or `{type: \"User\", login}`. Never agents; Diffinery has no teams. `[]` or `null` removes them."],
1178 prevent_self_review: ["boolean", "Whoever started a run (or an earlier attempt, or owns the agent that did) can't approve it."],
1179 },
1180 },
1181 "DELETE /repos/{owner}/{repo}/environments/{env}": { summary: "Delete an environment", description: "Deletes its secrets and variables too. People only. Audited.", access: "admin", status: 204 },
1182 "GET /repos/{owner}/{repo}/environments/{env}/deployment-branch-policies": {
1183 summary: "List deployment branch policies",
1184 description: "The branch and tag patterns of an environment with `custom_branch_policies`.",
1185 access: "read",
1186 query: { ...page },
1187 },
1188 "POST /repos/{owner}/{repo}/environments/{env}/deployment-branch-policies": {
1189 summary: "Create a deployment branch policy",
1190 description: "The environment must have `custom_branch_policies`. Tag patterns never match tags last pushed by an agent. People only. Audited.",
1191 access: "admin",
1192 body: { name: ["string", "Branch or tag name pattern, like `release/*`.", true], type: ["string", "`branch` (default) or `tag`."] },
1193 },
1194 "DELETE /repos/{owner}/{repo}/environments/{env}/deployment-branch-policies/{id}": {
1195 summary: "Delete a deployment branch policy",
1196 description: "People only. Audited.",
1197 access: "admin",
1198 status: 204,
1199 },
1200 "GET /repos/{owner}/{repo}/environments/{env}/secrets": {
1201 summary: "List environment secrets",
1202 description: "Names and dates only; values are never returned.",
1203 access: "admin",
1204 query: { ...page },
1205 },
1206 "GET /repos/{owner}/{repo}/environments/{env}/secrets/{name}": { summary: "Get an environment secret", description: "Name and dates only.", access: "admin" },
1207 "PUT /repos/{owner}/{repo}/environments/{env}/secrets/{name}": {
1208 summary: "Set an environment secret",
1209 description:
1210 "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.",
1211 access: "admin",
1212 status: 201,
1213 body: { value: ["string", "Secret value.", true] },
1214 },
1215 "DELETE /repos/{owner}/{repo}/environments/{env}/secrets/{name}": { summary: "Delete an environment secret", description: "People only. Audited.", access: "admin", status: 204 },
1216 "GET /repos/{owner}/{repo}/environments/{env}/variables": {
1217 summary: "List environment variables",
1218 access: "write",
1219 query: { per_page: ["integer", "Results per page (default 10, at most 100)."], page: page.page! },
1220 },
1221 "GET /repos/{owner}/{repo}/environments/{env}/variables/{name}": { summary: "Get an environment variable", access: "write" },
1222 "POST /repos/{owner}/{repo}/environments/{env}/variables": {
1223 summary: "Create an environment variable",
1224 description: "409 when it exists. Names are upper-cased, and `GITHUB_` names are refused. People only. Audited as `environment.variable.set`.",
1225 access: "admin",
1226 status: 201,
1227 body: { name: ["string", "Variable name.", true], value: ["string", "Variable value.", true] },
1228 },
1229 "PATCH /repos/{owner}/{repo}/environments/{env}/variables/{name}": {
1230 summary: "Update an environment variable",
1231 description: "People only. Renaming onto a name that exists fails with 409. Audited as `environment.variable.set`.",
1232 access: "admin",
1233 status: 204,
1234 body: { name: ["string", "New name (renames the variable)."], value: ["string", "New value."] },
1235 },
1236 "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 },
1237
1238 // ------------------------------------------------------------ actions
1239 "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" },
1240 "GET /repos/{owner}/{repo}/actions/workflows/{workflow}": {
1241 summary: "Get a workflow",
1242 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.",
1243 access: "read",
1244 },
1245 "GET /repos/{owner}/{repo}/actions/workflows/{workflow}/runs": {
1246 summary: "List a workflow's runs",
1247 description: "No `Link` header; page with `total_count`.",
1248 access: "read",
1249 query: {
1250 branch: ["string", "Head branch."],
1251 status: ["string", "Run status or conclusion."],
1252 event: ["string", "Triggering event."],
1253 head_sha: ["string", "Head commit."],
1254 ...page,
1255 },
1256 },
1257 "POST /repos/{owner}/{repo}/actions/workflows/{workflow}/dispatches": {
1258 summary: "Run a workflow (workflow_dispatch)",
1259 description: "`workflow` is the file path or name, URL-encoded, like `.github%2Fworkflows%2Fdeploy.yml` or `deploy.yml`. Returns the new run's id. Fails with 429 when the repository already has 500 unfinished workflow runs.",
1260 access: "write",
1261 status: 201,
1262 body: { ref: ["string", "Branch or tag to run on.", true], inputs: ["object", "Values for `on.workflow_dispatch.inputs`."] },
1263 },
1264 "POST /repos/{owner}/{repo}/actions/runners/registration-token": {
1265 summary: "Create a runner registration token",
1266 description: "As on GitHub: a `drg_` token that registers any number of self-hosted runners with this repository for an hour. Returns `{ token, expires_at }`. People only: agents, job tokens and sandbox tokens get 403 `only repository admins can add self-hosted runners`. Audited as `runner.registration_token`. See /docs/actions/runners#manage-runners.",
1267 access: "admin",
1268 status: 201,
1269 },
1270 "POST /repos/{owner}/{repo}/actions/runners/register": {
1271 summary: "Register a self-hosted runner",
1272 description: "Trades a registration token for the runner's own `drn_` token, shown once, which can claim this repository's jobs and nothing else. No `Authorization` header: the registration token is the credential. A wrong or expired token, or a repository that doesn't exist, gets 401 `invalid or expired registration token`. At most 100 runners per repository and 20 labels per runner (429 past the runner limit). Returns `{ id, name, labels, ephemeral, token }`. Audited as `runner.register`, with the admin who created the registration token as the actor.",
1273 access: "public",
1274 status: 201,
1275 body: {
1276 registration_token: ["string", "A `drg_` token from `POST .../actions/runners/registration-token`.", true],
1277 name: ["string", "Runner name (default `runner`)."],
1278 labels: ["string[]", "Labels the runner offers, matched against `runs-on`."],
1279 ephemeral: ["boolean", "`true` for a single job: the runner's token stops working once it takes a job, and the runner is removed when the job finishes."],
1280 },
1281 },
1282 "GET /repos/{owner}/{repo}/actions/runners": {
1283 summary: "List self-hosted runners",
1284 description: "GitHub's shape: `{ total_count, runners }`, each with `id`, `name`, `os`, `status` (`online` when it polled in the last 90 seconds, otherwise `offline`), `busy`, `ephemeral` and `labels`.",
1285 access: "admin",
1286 },
1287 "DELETE /repos/{owner}/{repo}/actions/runners/{id}": {
1288 summary: "Remove a self-hosted runner",
1289 description: "Its runner token stops working at once. A job it is running carries on with its own job token. Audited as `runner.remove`.",
1290 access: "admin",
1291 status: 204,
1292 },
1293 "POST /repos/{owner}/{repo}/actions/runners/claim": {
1294 summary: "Claim a job for a self-hosted runner",
1295 description: "Used by `diffinery runner`. Send the runner's `drn_` token and an empty body: the runner offers the name and labels it registered with. Only registered runners claim: a personal token, even a repository admin's, gets 403 `personal tokens can't claim jobs; register this runner: ...`. Returns 204 when no queued job matches the labels, otherwise the job with a fresh job token. See /docs/actions/runners.",
1296 access: "admin",
1297 },
1298 "GET /repos/{owner}/{repo}/actions/runs": {
1299 summary: "List workflow runs",
1300 access: "read",
1301 query: {
1302 workflow: ["string", "Workflow file path."],
1303 branch: ["string", "Head branch."],
1304 status: ["string", "Run status or conclusion."],
1305 event: ["string", "Triggering event."],
1306 head_sha: ["string", "Head commit."],
1307 per_page: ["integer", "Results per page (default 25, at most 100)."],
1308 page: ["integer", "Page number, starting at 1."],
1309 },
1310 },
1311 "GET /repos/{owner}/{repo}/actions/runs/{id}": {
1312 summary: "Get a workflow run",
1313 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.",
1314 access: "read",
1315 },
1316 "GET /repos/{owner}/{repo}/actions/runs/{id}/attempts/{attempt}": {
1317 summary: "Get a run attempt",
1318 description: "Re-runs are stored as separate runs with the same run number; this returns attempt `attempt` of the run's number.",
1319 access: "read",
1320 },
1321 "GET /repos/{owner}/{repo}/actions/runs/{id}/logs": {
1322 summary: "Download a run's logs",
1323 description: "A zip with one text file per job. Jobs past 24 MB of logs in total are left out.",
1324 access: "read",
1325 },
1326 "DELETE /repos/{owner}/{repo}/actions/runs/{id}/logs": {
1327 summary: "Delete a run's logs",
1328 description: "Deletes every job's log and step summary for good; the run, its jobs, checks and artifacts stay. Only for a finished run (409 otherwise). An agent can delete only the logs of runs that didn't run on the default branch or a protected branch, and a workflow's `GITHUB_TOKEN` can't delete logs (403). Audited as `repo.actions.logs.delete`.",
1329 access: "write",
1330 status: 204,
1331 },
1332 "GET /repos/{owner}/{repo}/actions/runs/{id}/jobs": { summary: "List a run's jobs", description: "Each job as `GET .../actions/jobs/{id}` returns it, with `runner`, `placementTries` and `restarts`.", access: "read" },
1333 "GET /repos/{owner}/{repo}/actions/runs/{id}/pending_deployments": {
1334 summary: "List the environments a run waits on",
1335 description: "Environments whose required reviewers or wait timer hold the run's jobs, with `current_user_can_approve` for the caller. See [Required reviewers and wait timers](/docs/actions/environments#required-reviewers-and-wait-timers).",
1336 access: "read",
1337 },
1338 "POST /repos/{owner}/{repo}/actions/runs/{id}/pending_deployments": {
1339 summary: "Approve or reject a run's pending deployments",
1340 description:
1341 "Only a listed reviewer, signed in or with a `write` token: never an agent, a workflow's token or a sandbox token (403). With `prevent_self_review`, not whoever started the run (403). One decision per environment and run counts; 422 when nothing waits. Approved jobs start once their wait timer has passed; rejected ones fail. Returns the deployments of the jobs that were waiting. Audited as `deployment.approve` or `deployment.reject`.",
1342 access: "read",
1343 body: {
1344 environment_ids: ["array", "Ids of the environments to decide.", true],
1345 state: ["string", "`approved` or `rejected`.", true],
1346 comment: ["string", "Shown with the decision and in the audit log."],
1347 },
1348 },
1349 "GET /repos/{owner}/{repo}/actions/runs/{id}/approvals": { summary: "List a run's deployment reviews", description: "Each reviewer's decision, with the environment and comment.", access: "read" },
1350 "POST /repos/{owner}/{repo}/actions/runs/{id}/cancel": { summary: "Cancel a run", access: "write", status: 202 },
1351 "POST /repos/{owner}/{repo}/actions/runs/{id}/rerun": {
1352 summary: "Re-run a workflow run",
1353 description: "Starts a new attempt that runs every job again. Answers `{ run_id }`, the new attempt's id. A workflow's `GITHUB_TOKEN` can't re-run workflows (403).",
1354 access: "write",
1355 status: 201,
1356 body: { enable_debug_logging: ["boolean", "Accepted and ignored."] },
1357 },
1358 "POST /repos/{owner}/{repo}/actions/runs/{id}/rerun-failed-jobs": {
1359 summary: "Re-run a run's failed jobs",
1360 description:
1361 "Starts a new attempt that runs the jobs and matrix legs that failed, were cancelled or timed out, and every job that depends on them. The others are kept from the earlier attempt as they were, with their logs and artifacts. Answers `{ run_id }`, the new attempt's id. 409 `this run has no failed jobs to re-run`, or when the run hasn't finished. A workflow's `GITHUB_TOKEN` can't re-run workflows (403).",
1362 access: "write",
1363 status: 201,
1364 body: { enable_debug_logging: ["boolean", "Accepted and ignored."] },
1365 },
1366 "GET /repos/{owner}/{repo}/actions/jobs/{id}": { summary: "Get a job", description: "The job with its steps. GitHub's job fields, plus Diffinery's own: `runner` (the hosted container's `image`, `location` and `instance`; `null` on a self-hosted runner) and `placementTries` (how many times a hosted job went back in line before a runner took it: no free container, or a runner on a replaced image) and `restarts` (how many times it started over because the platform replaced its runner before its steps ran). See /docs/actions/runners.", access: "read" },
1367 "POST /repos/{owner}/{repo}/actions/jobs/{id}/rerun": {
1368 summary: "Re-run a job",
1369 description:
1370 "Starts a new attempt of the job's run that runs this job (this matrix leg) and every job that depends on it; the other jobs are kept from the earlier attempt as they were. Answers `{ run_id }`, the new attempt's id. 409 when the run hasn't finished. A workflow's `GITHUB_TOKEN` can't re-run workflows (403).",
1371 access: "write",
1372 status: 201,
1373 body: { enable_debug_logging: ["boolean", "Accepted and ignored."] },
1374 },
1375 "GET /repos/{owner}/{repo}/actions/jobs/{id}/logs": {
1376 summary: "Get a job's logs",
1377 description: "Plain text, one timestamped line each, or JSON with `format=json`.",
1378 access: "read",
1379 query: { format: ["string", "`json` for an array of `{ t, text }` lines."], after: ["integer", "Only lines after this sequence number (for following a live log)."] },
1380 },
1381 "GET /repos/{owner}/{repo}/actions/runs/{runId}/artifacts": {
1382 summary: "List a run's artifacts",
1383 description:
1384 "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.",
1385 access: "read",
1386 query: { name: ["string", "Only artifacts with exactly this name."], ...page },
1387 },
1388 "GET /repos/{owner}/{repo}/actions/artifacts": {
1389 summary: "List artifacts",
1390 description:
1391 "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 }`.",
1392 access: "read",
1393 query: { name: ["string", "Only artifacts with exactly this name."], ...page },
1394 },
1395 "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" },
1396 "GET /repos/{owner}/{repo}/actions/artifacts/{id}/{format}": {
1397 summary: "Download an artifact",
1398 description:
1399 "`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.",
1400 access: "read",
1401 status: 302,
1402 },
1403 "DELETE /repos/{owner}/{repo}/actions/artifacts/{id}": {
1404 summary: "Delete an artifact",
1405 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`.",
1406 access: "write",
1407 status: 204,
1408 },
1409 "GET /repos/{owner}/{repo}/actions/caches": {
1410 summary: "List caches",
1411 description:
1412 "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 }`.",
1413 access: "read",
1414 query: {
1415 key: ["string", "Only entries whose key starts with this."],
1416 ref: ["string", "Only entries saved for this full ref, like `refs/heads/main`."],
1417 sort: ["string", "`last_accessed_at` (default), `created_at` or `size_in_bytes`."],
1418 direction: ["string", "`desc` (default) or `asc`."],
1419 ...page,
1420 },
1421 },
1422 "DELETE /repos/{owner}/{repo}/actions/caches": {
1423 summary: "Delete caches by key",
1424 description:
1425 "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`.",
1426 access: "write",
1427 query: { key: ["string", "The exact cache key.", true], ref: ["string", "Only entries saved for this full ref, like `refs/heads/main`."] },
1428 },
1429 "DELETE /repos/{owner}/{repo}/actions/caches/{id}": {
1430 summary: "Delete a cache",
1431 description: "By the entry's numeric id. 404 when there is none. Audited as `actions.cache.delete`.",
1432 access: "write",
1433 status: 204,
1434 },
1435 "GET /repos/{owner}/{repo}/actions/cache/usage": {
1436 summary: "Get cache usage",
1437 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).",
1438 access: "read",
1439 },
1440 "GET /repos/{owner}/{repo}/actions/permissions/workflow": {
1441 summary: "Get workflow token permissions",
1442 description:
1443 "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. `agent_runs_use_secrets` (a Diffinery addition, `false` by default) says whether runs of code an agent wrote get the repository's and organization's secrets. New repositories start with `read` and `false`; repositories created before the setting existed have `write` and `false`. See [Workflow permissions](/docs/actions/workflow-permissions).",
1444 access: "admin",
1445 },
1446 "PUT /repos/{owner}/{repo}/actions/permissions/workflow": {
1447 summary: "Set workflow token permissions",
1448 description:
1449 "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.",
1450 access: "admin",
1451 status: 204,
1452 body: {
1453 default_workflow_permissions: ["string", "`read` or `write`."],
1454 can_approve_pull_request_reviews: ["boolean", "Allow workflows' `GITHUB_TOKEN` to open pull requests and to approve or request changes."],
1455 agent_runs_use_secrets: ["boolean", "Give runs of code an agent wrote (an agent triggered the run, pushed its branch last, or opened or pushed to its pull request) the repository's and organization's secrets. Default `false`."],
1456 },
1457 },
1458 "GET /repos/{owner}/{repo}/actions/secrets": { summary: "List repository secrets", description: "Names and update times only; values are never returned.", access: "admin" },
1459 "PUT /repos/{owner}/{repo}/actions/secrets/{name}": {
1460 summary: "Set a repository secret",
1461 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`.",
1462 access: "admin",
1463 status: 204,
1464 body: { value: ["string", "Secret value.", true] },
1465 },
1466 "DELETE /repos/{owner}/{repo}/actions/secrets/{name}": { summary: "Delete a repository secret", description: "People only. Audited as `repo.secret.delete`.", access: "admin", status: 204 },
1467 "GET /repos/{owner}/{repo}/actions/variables": {
1468 summary: "List repository variables",
1469 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.",
1470 access: "write",
1471 query: { ...page },
1472 },
1473 "POST /repos/{owner}/{repo}/actions/variables": {
1474 summary: "Create a repository variable",
1475 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`.",
1476 access: "admin",
1477 status: 201,
1478 body: { name: ["string", "Variable name.", true], value: ["string", "Variable value.", true] },
1479 },
1480 "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" },
1481 "PATCH /repos/{owner}/{repo}/actions/variables/{name}": {
1482 summary: "Update a repository variable",
1483 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`.",
1484 access: "admin",
1485 status: 204,
1486 body: { name: ["string", "New name (renames the variable)."], value: ["string", "New value."] },
1487 },
1488 "PUT /repos/{owner}/{repo}/actions/variables/{name}": {
1489 summary: "Set a repository variable",
1490 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`.",
1491 access: "admin",
1492 status: 204,
1493 body: { value: ["string", "Variable value.", true] },
1494 },
1495 "DELETE /repos/{owner}/{repo}/actions/variables/{name}": {
1496 summary: "Delete a repository variable",
1497 description: "People only. Names are matched without regard to letter case. Audited as `repo.variable.delete`.",
1498 access: "admin",
1499 status: 204,
1500 },
1501 "GET /orgs/{org}/actions/secrets": { summary: "List organization secrets", description: "Names, update times and `visibility` (`all`, `private` or `selected`), never values.", access: "org-admin" },
1502 "GET /orgs/{org}/actions/secrets/{name}": { summary: "Get an organization secret", description: "Its name, update time and `visibility`, never the value. 404 when there is no such secret.", access: "org-admin" },
1503 "PUT /orgs/{org}/actions/secrets/{name}": {
1504 summary: "Set an organization secret",
1505 description: "Creates or updates the secret. `visibility` says which of the organization's repositories get it: `all` (the default for a new secret), `private` (private and internal ones) or `selected` (those in `selected_repository_ids`). An update without them keeps the secret's visibility and selection. Names are upper-cased and must match `[A-Z_][A-Z0-9_]*`. People only. Audited as `org.secret.set`. See /docs/actions/secrets-and-variables#organization-secrets.",
1506 access: "org-admin",
1507 status: 204,
1508 body: {
1509 value: ["string", "Secret value.", true],
1510 visibility: ["string", "`all`, `private` or `selected` (422 otherwise)."],
1511 selected_repository_ids: ["integer[]", "With `selected`: the repositories' numeric ids. An id that isn't one of the organization's repositories gets 422."],
1512 },
1513 },
1514 "GET /orgs/{org}/actions/secrets/{name}/repositories": { summary: "List a secret's selected repositories", description: "`{ total_count, repositories }` for a secret with `visibility: selected`; 409 `the secret's visibility isn't selected` otherwise.", access: "org-admin" },
1515 "PUT /orgs/{org}/actions/secrets/{name}/repositories": {
1516 summary: "Replace a secret's selected repositories",
1517 description: "Only for a secret with `visibility: selected` (409 otherwise). A repository that leaves the selection stops getting the secret from its next job. Audited as `org.secret.repositories`.",
1518 access: "org-admin",
1519 status: 204,
1520 body: { selected_repository_ids: ["integer[]", "Every repository that should get the secret.", true] },
1521 },
1522 "PUT /orgs/{org}/actions/secrets/{name}/repositories/{repoId}": { summary: "Add a repository to a secret's selection", description: "Only for a secret with `visibility: selected` (409 otherwise). `repoId` is the repository's numeric id. Audited as `org.secret.repository.add`.", access: "org-admin", status: 204 },
1523 "DELETE /orgs/{org}/actions/secrets/{name}/repositories/{repoId}": { summary: "Remove a repository from a secret's selection", description: "Only for a secret with `visibility: selected` (409 otherwise). The repository stops getting the secret from its next job. Audited as `org.secret.repository.remove`.", access: "org-admin", status: 204 },
1524 "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 },
1525 "GET /orgs/{org}/actions/variables": {
1526 summary: "List organization variables",
1527 description:
1528 "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.",
1529 access: "org-admin",
1530 },
1531 "POST /orgs/{org}/actions/variables": {
1532 summary: "Create an organization variable",
1533 description:
1534 "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`.",
1535 access: "org-admin",
1536 status: 201,
1537 body: { name: ["string", "Variable name.", true], value: ["string", "Variable value, at most 49,152 characters.", true] },
1538 },
1539 "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" },
1540 "PATCH /orgs/{org}/actions/variables/{name}": {
1541 summary: "Update an organization variable",
1542 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`.",
1543 access: "org-admin",
1544 status: 204,
1545 body: { value: ["string", "New value, at most 49,152 characters.", true] },
1546 },
1547 "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 },
1548
1549 // ------------------------------------------------------------ webhooks
1550 "GET /repos/{owner}/{repo}/hooks": { summary: "List webhooks", description: "Secrets are never returned.", access: "admin" },
1551 "POST /repos/{owner}/{repo}/hooks": {
1552 summary: "Create a webhook",
1553 description: "`config.url` must be `https://` to a public host: `localhost`, single-label, `.local` and `.internal` names, private, loopback and link-local addresses get 422 `webhook URL must be a public host`, and Diffinery's own host 422 `webhook URL can't point at Diffinery itself`. At most 20 webhooks per repository (422). Audited as `repo.webhook.create`.",
1554 access: "admin",
1555 status: 201,
1556 body: { config: ["object", "`{ url, secret }`. The secret signs each delivery (`X-Hub-Signature-256`).", true], events: ["string[]", "Event names, or `[\"*\"]` (default) for all."] },
1557 },
1558 "DELETE /repos/{owner}/{repo}/hooks/{id}": { summary: "Delete a webhook", description: "Audited as `repo.webhook.delete`.", access: "admin", status: 204 },
1559
1560 // ------------------------------------------------------------ notifications and email
1561 "GET /notifications": { summary: "List notifications", description: "A token limited to repositories answers 403 `this token is limited to repositories and can't read notifications`, since notifications cover every repository.", access: "account", query: { all: ["boolean", "`true` to include read ones."] } },
1562 "PUT /notifications": { summary: "Mark all notifications as read", access: "account", status: 205 },
1563 "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" },
1564 "PUT /repos/{owner}/{repo}/subscription": {
1565 summary: "Watch or ignore a repository",
1566 access: "read",
1567 body: { subscribed: ["boolean", "`true` for all activity, `false` for participating only."], ignored: ["boolean", "`true` to ignore the repository."] },
1568 },
1569 "DELETE /repos/{owner}/{repo}/subscription": { summary: "Stop watching a repository", description: "Back to participating notifications.", access: "read", status: 204 },
1570 "GET /diffinery/notification-settings": { summary: "Get your notification settings", access: "account" },
1571 "PUT /diffinery/notification-settings": { summary: "Update your notification settings", description: "Send only the groups you want to change. The morning digest is `digest`: `email` (on or off), `hour` (0 to 23), `timeZone` (an IANA time zone such as `Europe/Istanbul` or `UTC`) and `weekends`; 422 for an hour or time zone it can't use. See /docs/agents/today#the-morning-digest.", access: "account" },
1572 "GET /diffinery/emails": {
1573 summary: "List emails sent to you",
1574 access: "account",
1575 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)."] },
1576 },
1577 "POST /diffinery/emails/test": { summary: "Send yourself a test email", description: "At most 5 an hour; more answer 429 `you can send at most 5 test emails an hour; try again later`.", access: "account" },
1578 "POST /diffinery/emails/digest": { summary: "Send yourself the morning digest now", description: "Your Today digest, sent now even when the digest is off or has nothing in it, covering what happened since the last one. At most 3 an hour; more answer 429. See /docs/agents/today#the-morning-digest.", access: "account" },
1579 "GET /diffinery/email": {
1580 summary: "Your email address and whether it is suppressed",
1581 description: "`suppressed` is null, or why Diffinery sends nothing to your address: `reason` (`hard-bounce`, `complaint` or `manual`), `detail`, `source`, `created_at` and `owner_can_lift`.",
1582 access: "account",
1583 },
1584 "POST /diffinery/email/verification": {
1585 summary: "Ask for the email that turns email back on",
1586 description: "When your address is suppressed by a bounce or complaint, sends one email to it, past the suppression list; following its link while signed in lifts the suppression. 409 when it isn't suppressed, 403 for a suppression a site administrator added, at most 3 an hour.",
1587 access: "account",
1588 },
1589 "GET /diffinery/admin/email": { summary: "Email delivery status", description: "Whether sending is configured, whether the signed bounce and complaint endpoint is on (`event_intake`), and counts for the last 24 hours.", access: "site-admin" },
1590 "GET /diffinery/admin/email/suppressions": { summary: "List suppressed email addresses", description: "Newest first: `email`, `reason` (`hard-bounce`, `complaint` or `manual`), `detail`, `source` (`cloudflare`, `webhook`, `send` or `admin`), `created_by` and `created_at`.", access: "site-admin" },
1591 "PUT /diffinery/admin/email/suppressions/{email}": { summary: "Suppress an email address", description: "Diffinery sends nothing to it until it is removed. Recorded in the audit log.", access: "site-admin", body: { note: ["string", "Why, kept with the entry."] } },
1592 "DELETE /diffinery/admin/email/suppressions/{email}": { summary: "Remove an address from the suppression list", description: "Whatever put it there. 404 when it isn't suppressed. Recorded in the audit log.", access: "site-admin", status: 204 },
1593 "GET /diffinery/admin/security-reports": {
1594 summary: "List security reports",
1595 description: "Private vulnerability reports sent through `/security/report`, newest first: `id`, `state` (`new`, `triaged` or `resolved`), `summary`, `details`, `area`, `reply_to`, `reporter` (a login, or null when not signed in), `note`, `updated_by`, `updated_at`, `created_at` and `html_url`. 404 for anyone but a site administrator.",
1596 access: "site-admin",
1597 query: { state: ["string", "Only reports in this state: `new`, `triaged` or `resolved`."], per_page: ["integer", "At most this many (default 100, up to 500)."] },
1598 },
1599 "GET /diffinery/admin/security-reports/{id}": { summary: "Get a security report", description: "One report, as in the list. 404 when there is none, or for anyone but a site administrator.", access: "site-admin" },
1600 "PATCH /diffinery/admin/security-reports/{id}": {
1601 summary: "Triage or resolve a security report",
1602 description: "Sets the report's state and note (a missing or empty note clears it). Recorded in the audit log as `security_report.triage`, `security_report.resolve` or `security_report.reopen`. 404 for anyone but a site administrator.",
1603 access: "site-admin",
1604 body: { state: ["string", "`triaged`, `resolved`, or `new` to reopen it.", true], note: ["string", "For the other site administrators, up to 2,000 characters."] },
1605 },
1606 "GET /diffinery/admin/early-access": {
1607 summary: "List early access requests",
1608 description:
1609 "Requests sent through `/early-access`, newest first: `id`, `state` (`unconfirmed` until the address follows the emailed link, then `confirmed`, `invited` or `declined`), `email`, `name`, `company`, `use_case`, `hosting` (`github`, `gitlab`, `bitbucket`, `azure`, `self-hosted`, `other`, `none` or null), `confirmed_at`, `invite_id`, `decided_by`, `decided_at`, `created_at`, `updated_at` and `html_url`. 404 for anyone but a site administrator. See [Early access](/docs/accounts/early-access).",
1610 access: "site-admin",
1611 query: { state: ["string", "Only requests in this state: `confirmed`, `unconfirmed`, `invited` or `declined`."], q: ["string", "Only requests whose address, name, company or use contains this text."], per_page: ["integer", "At most this many (default 100, up to 500)."], page: ["integer", "Page number, from 1."] },
1612 },
1613 "GET /diffinery/admin/early-access/{id}": { summary: "Get an early access request", description: "One request, as in the list. 404 when there is none, or for anyone but a site administrator.", access: "site-admin" },
1614 "POST /diffinery/admin/early-access/{id}/invite": {
1615 summary: "Invite an early access request",
1616 description:
1617 "Sends the request's address the instance's usual invitation email (it works once for 14 days) and marks the request `invited`. An address that already has a pending instance invitation keeps it; one that already belongs to an active account is refused (422). Recorded in the audit log as `early_access.invite`, and `invite.create` when a new invitation is sent. 404 for anyone but a site administrator.",
1618 access: "site-admin",
1619 },
1620 "PATCH /diffinery/admin/early-access/{id}": {
1621 summary: "Decline an early access request, or put it back",
1622 description: "`declined` declines it; `confirmed` puts a declined request back (as `unconfirmed` if its address never confirmed). Nobody is emailed. Recorded in the audit log as `early_access.decline` or `early_access.restore`. 404 for anyone but a site administrator.",
1623 access: "site-admin",
1624 body: { state: ["string", "`declined`, or `confirmed` to put a declined request back.", true] },
1625 },
1626 "DELETE /diffinery/admin/early-access/{id}": {
1627 summary: "Delete an early access request",
1628 description: "Removes the request, for someone who asks for their data to be removed. An invitation already sent from it stays; revoke it separately. Recorded in the audit log as `early_access.delete`, without the request's content or address. 404 for anyone but a site administrator.",
1629 access: "site-admin",
1630 status: 204,
1631 },
1632 "GET /diffinery/status": {
1633 summary: "Get the instance's status",
1634 description:
1635 "The status page as JSON, shaped like Statuspage's summary: `status` (`indicator`: `none`, `minor`, `major`, `critical` or `maintenance`, and a `description`), `components` (`id`, `name`, `status`: `operational`, `under_maintenance`, `degraded_performance`, `partial_outage` or `major_outage`, and a `note` when a kill switch or a database failure set it), `incidents` (the unresolved ones, newest first, each with its `updates`, newest first) and `updated_at`. Cached for 30 seconds. See [Status page](/docs/self-hosting/status-page).",
1636 access: "public",
1637 },
1638 "GET /diffinery/admin/status/incidents": {
1639 summary: "List status incidents",
1640 description: "Incidents on the status page, newest first, resolved or not: `id`, `title`, `impact`, `state`, `components`, `created_by`, `created_at`, `updated_at`, `resolved_at`, `updates` (each with `id`, `state`, `body`, `author` and `created_at`, newest first) and `html_url`. 404 for anyone but a site administrator.",
1641 access: "site-admin",
1642 query: { per_page: ["integer", "At most this many (default 50, up to 200)."] },
1643 },
1644 "POST /diffinery/admin/status/incidents": {
1645 summary: "Open a status incident",
1646 description: "Opens an incident with its first update and answers with it. While it isn't resolved, its impact sets the status of each component it names: `critical` is a major outage, `major` a partial outage, `minor` degraded performance and `maintenance` under maintenance. Recorded in the audit log as `status_incident.create`. 404 for anyone but a site administrator.",
1647 access: "site-admin",
1648 status: 201,
1649 body: {
1650 title: ["string", "One line, up to 200 characters.", true],
1651 impact: ["string", "`minor`, `major`, `critical` or `maintenance`.", true],
1652 components: ["array", "At least one of `web`, `git`, `actions`, `agents`, `packages` and `email`.", true],
1653 state: ["string", "`investigating`, `identified`, `monitoring` or `resolved`.", true],
1654 body: ["string", "The first update, plain text up to 5,000 characters. It is public.", true],
1655 },
1656 },
1657 "POST /diffinery/admin/status/incidents/{id}/updates": {
1658 summary: "Post an update to a status incident",
1659 description: "Adds an update and moves the incident to its state; `resolved` resolves it, and any other state reopens a resolved incident. Answers with the incident. Recorded in the audit log as `status_incident.resolve` when it resolves the incident and `status_incident.update` otherwise. 404 when there is no such incident, or for anyone but a site administrator.",
1660 access: "site-admin",
1661 status: 201,
1662 body: { state: ["string", "`investigating`, `identified`, `monitoring` or `resolved`.", true], body: ["string", "Plain text up to 5,000 characters. It is public.", true] },
1663 },
1664 "GET /diffinery/admin/scheduler": {
1665 summary: "Workflow scheduler state",
1666 description: "`tick_minutes`, `last_tick_at` (when the last check finished), `covered_until` (the last minute whose schedules were started) and `next_tick_at` (null when the timer isn't set). Tells a scheduler that stopped ticking from a run that failed to start.",
1667 access: "site-admin",
1668 },
1669 "GET /diffinery/admin/indexnow": {
1670 summary: "IndexNow submissions",
1671 description: "What this instance has told search engines through IndexNow: `enabled` (and `off_reason` when it isn't), `key_location`, how many `site_pages` and public `repositories` it tracks, how many are `queued`, `retry_at` (set after IndexNow refused or couldn't be reached), `repositories_checked_at`, and the newest `submissions` (`at`, `urls`, `status` from IndexNow or null when unreachable, `endpoint` (the host that answered), `ms`, `error`, and the first 50 `paths`). See [Search engines](/docs/admin/site-administration#search-engines).",
1672 access: "site-admin",
1673 query: { per_page: ["integer", "Submissions to return (default 50, up to 200)."] },
1674 },
1675 "GET /diffinery/admin/errors": {
1676 summary: "List unexpected server errors",
1677 description: "The newest answers with a 5xx status, newest first: `id` (the `request_id` the answer carried, also in its `x-request-id` header), `at`, `method`, `route`, `path`, `status`, `message` and `stack`. The newest 500 are kept. No request bodies, query strings or headers are kept, and tokens in a message are masked.",
1678 access: "site-admin",
1679 query: { per_page: ["integer", "At most this many (default 100, up to 500)."] },
1680 },
1681 "GET /diffinery/admin/errors/{id}": { summary: "Get a server error by its request ID", description: "The error a 5xx answer with this `request_id` recorded; 404 once it is no longer among the newest 500.", access: "site-admin" },
1682 "POST /diffinery/admin/emails/samples": { summary: "Send yourself every email template", access: "site-admin", body: { only: ["string[]", "Only these sample ids."] } },
1683
1684 // ------------------------------------------------------------ tokens, agents and sandboxes
1685 "GET /diffinery/tokens": { summary: "List your tokens", access: "account" },
1686 "POST /diffinery/tokens": {
1687 summary: "Create a token",
1688 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`). From a browser session whose sign-in is more than 2 hours old: 403 `Confirm it's you: sign in with your passkey again to do this.` with a `confirm` URL (see /docs/security/overview#confirming-its-you).",
1689 access: "account",
1690 status: 201,
1691 body: {
1692 name: ["string", "A name to recognise it by.", true],
1693 scopes: ["string[]", "`read`, `write` (default) and/or `admin`."],
1694 repositories: ["string[]", "Restrict to these `owner/name` repositories (only ones you can see)."],
1695 expires_in_days: ["integer", "Lifetime in days."],
1696 },
1697 },
1698 "DELETE /diffinery/tokens/{id}": { summary: "Revoke a token", access: "account", status: 204 },
1699 "GET /diffinery/today": {
1700 summary: "Your morning view",
1701 description: "Pull requests that need you, your agents' open pull requests with their state, and what merged, was released or failed in the last 24 hours, across the repositories you can see that were active in the last 30 days (at most 100). A token limited to repositories sees only those. See /docs/agents/today.",
1702 access: "account",
1703 },
1704 "GET /diffinery/agents": { summary: "List your agents", description: "Agents you own, and agents of organizations you administer.", access: "account" },
1705 "POST /diffinery/agents": {
1706 summary: "Create an agent",
1707 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. From a browser session whose sign-in is more than 2 hours old: 403 `Confirm it's you: sign in with your passkey again to do this.` with a `confirm` URL (see /docs/security/overview#confirming-its-you).",
1708 access: "account",
1709 status: 201,
1710 body: {
1711 login: ["string", "Agent login.", true],
1712 name: ["string", "Display name."],
1713 org: ["string", "Organization that owns the agent (default: you)."],
1714 policy: ["object", "Agent policy."],
1715 token: ["object", "`{ scopes, expires_in_days }` for the first token."],
1716 auto_merge: ["boolean", "Turn on auto-merge for the pull requests it opens (see `PUT /diffinery/agents/{login}/auto-merge`)."],
1717 },
1718 },
1719 "POST /diffinery/agents/{login}/tokens": {
1720 summary: "Create a token for an agent",
1721 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`. From a browser session whose sign-in is more than 2 hours old: 403 `Confirm it's you: sign in with your passkey again to do this.` with a `confirm` URL (see /docs/security/overview#confirming-its-you).",
1722 access: "account",
1723 status: 201,
1724 body: { name: ["string", "Token name."], scopes: ["string[]", "`read`, `write` (default) and/or `admin`."], expires_in_days: ["integer", "Lifetime in days."] },
1725 },
1726 "DELETE /diffinery/agents/{login}": { summary: "Disable an agent", description: "Its tokens stop working.", access: "account", status: 204 },
1727 "GET /diffinery/auto-merge": {
1728 summary: "Your auto-merge setting",
1729 description: "Whether Diffinery turns on auto-merge for the pull requests you open: `{ enabled, merge_method }`, where `merge_method` is `default` (as the branch merges), `merge`, `squash`, `rebase`, or null when off. An agent's token reads the agent's own. See /docs/collaboration/merge-on-green.",
1730 access: "account",
1731 },
1732 "PUT /diffinery/auto-merge": {
1733 summary: "Turn auto-merge on or off for pull requests you open",
1734 description:
1735 "People only. When on, Diffinery turns on auto-merge for you as you open a pull request (or mark your draft ready), wherever its base branch has at least one required check; it merges once every rule of the branch is met, approvals included. The body may also be just `true` or `false`. Returns the setting. Audited as `account.auto_merge`. See /docs/collaboration/merge-on-green.",
1736 access: "account",
1737 body: { enabled: ["boolean", "On or off.", true], merge_method: ["string", "`default` (as the branch merges, the default), `merge`, `squash` or `rebase`."] },
1738 },
1739 "PUT /diffinery/agents/{login}/auto-merge": {
1740 summary: "Turn auto-merge on or off for an agent's pull requests",
1741 description:
1742 "The agent's owner, or an owner or admin of the organization that owns it. Diffinery then turns on auto-merge, as the agent, for each pull request it opens; it lands only as the agent's own auto-merge would (the repository must allow agents' auto-merge, or the agent may merge what a person approved), and a workflow change still needs a person. Same body as `PUT /diffinery/auto-merge`. Audited as `agent.auto_merge`.",
1743 access: "account",
1744 body: { enabled: ["boolean", "On or off.", true], merge_method: ["string", "`default`, `merge`, `squash` or `rebase`."] },
1745 },
1746 "POST /repos/{owner}/{repo}/diffinery/agent-setup/checks": {
1747 summary: "Report an agent setup check",
1748 description: "What the hosted setup script (`/{owner}/{repo}/agent-setup.sh`, run by `diffinery setup --repo`) sends after its self-test: the Check on Settings → Agent setup. Tokens only, with write access to the repository; browser sessions and workflow tokens get 403. The report is green only when every line passed. Returns `ok` and the page's `html_url`. See [Cloud agent environments](/docs/connect/cloud-agents).",
1749 access: "write",
1750 status: 201,
1751 body: {
1752 checks: ["object[]", "1 to 30 lines of `{ name, ok, message }`: what was checked, whether it passed, and what to change when it didn't.", true],
1753 cli_version: ["string", "The CLI's version."],
1754 },
1755 },
1756 "GET /repos/{owner}/{repo}/diffinery/agent-setup": {
1757 summary: "Get the agent setup check",
1758 description: "The latest Check on Settings → Agent setup, or `check: null` before any: `ok`, `checked_at`, `first_ok_at` (when a check first passed), the `account` and `token` (`name`, `prefix`) that reported it, `cli_version` and the `checks` lines. See [Cloud agent environments](/docs/connect/cloud-agents).",
1759 access: "write",
1760 },
1761 "POST /repos/{owner}/{repo}/diffinery/sandboxes": {
1762 summary: "Start a sandbox",
1763 description: "A container with the repository checked out, for agents to build and test in.",
1764 access: "write",
1765 status: 201,
1766 body: { ref: ["string", "Branch, tag or commit to check out (default: the default branch)."] },
1767 },
1768 // ------------------------------------------------------------ model access and agent tasks (agentic/)
1769 "GET /diffinery/model-keys": {
1770 summary: "List your model keys",
1771 description: "Your saved model API keys for agent tasks: `provider` (`anthropic`, `openai` or `openrouter`), `hint` (the key's last four characters), `opted_in_at`, `created_at` and `updated_at`. The keys themselves are never returned. See /docs/agents/tasks#model-access.",
1772 access: "account",
1773 },
1774 "GET /diffinery/model-usage": {
1775 summary: "Get your model usage",
1776 description: "The model usage you paid for over the last `days`, as on Settings → Model access: `days`, `since`, and `repositories`, one line per repository (named as it was when its runs ended) and `kind` (`task`, `review` or `coordinator`) with `runs`, `reported_by_cli_runs`, `requests`, `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens` and `cost_usd` (`null` when no run reported a cost). A token limited to some repositories sees only their lines. See /docs/agents/tasks#usage-and-cost.",
1777 access: "account",
1778 query: { days: ["integer", "1 to 90 (default 30)."] },
1779 },
1780 "GET /orgs/{org}/diffinery/agent-policy": {
1781 summary: "Get an organization's agent policy",
1782 description: "Owners only (and site administrators); a token limited to repositories gets 403. `keys` (`personal`, `either` or `organization`: whose key pays), `runtimes` (`claude-code`, `codex`, `custom`), `providers` (`anthropic`, `openai`, `openrouter`), `max_running` (tasks at once per repository, 1 to 10), `terminals`, `terminal_tasks`, `review`, `member_monthly_tokens` (`null` for no allowance), `updated_by` and `updated_at`. See /docs/agents/organizations.",
1783 access: "org-owner",
1784 },
1785 "PUT /orgs/{org}/diffinery/agent-policy": {
1786 summary: "Change an organization's agent policy",
1787 description: "From a browser session only, by an owner. Fields left out stay as they are; answers with the whole policy. 422 when a value is out of range, or no runtime or provider is left allowed. Each repository's tasks at once are held to `max_running` right away. Audited as `org.agent_policy.update`, with the new values.",
1788 access: "org-owner",
1789 body: {
1790 keys: ["string", "`personal` (each person's own keys), `either` (the organization's keys when it has them) or `organization` (only the organization's keys)."],
1791 runtimes: ["array", "Allowed runtimes: `claude-code`, `codex`, `custom`. At least one."],
1792 providers: ["array", "Allowed providers: `anthropic`, `openai`, `openrouter`. At least one."],
1793 max_running: ["integer", "Most tasks at once per repository, 1 to 10."],
1794 terminals: ["boolean", "Allow web terminals."],
1795 terminal_tasks: ["boolean", "Let terminals run agent tasks."],
1796 review: ["boolean", "Allow agent review."],
1797 member_monthly_tokens: ["integer", "Tokens one person may spend of the organization's keys in a calendar month (UTC), 10,000 to 10,000,000,000, or `null` for no allowance."],
1798 },
1799 },
1800 "GET /orgs/{org}/diffinery/model-keys": {
1801 summary: "List an organization's model keys",
1802 description: "Owners only; a token limited to repositories gets 403. Each key as `GET /diffinery/model-keys` shows a personal one: `provider`, `hint` (last four characters), `opted_in_at`, `created_at` and `updated_at`. The keys themselves are never returned. See /docs/agents/organizations.",
1803 access: "org-owner",
1804 },
1805 "PUT /orgs/{org}/diffinery/model-keys/{provider}": {
1806 summary: "Save an organization's model key",
1807 description: "From a browser session with a sign-in from the last 2 hours, by an owner. `provider` is `anthropic`, `openai` or `openrouter`; `accept_terms: true` is required. The key is encrypted, never shown again and never sent to a sandbox. Every owner gets a security email. Audited as `org.model_key.set`.",
1808 access: "org-owner",
1809 body: { key: ["string", "The API key (`sk-ant-...`, `sk-...` or `sk-or-...`)."], accept_terms: ["boolean", "Must be `true`."] },
1810 },
1811 "DELETE /orgs/{org}/diffinery/model-keys/{provider}": {
1812 summary: "Delete an organization's model key",
1813 description: "From a browser session with a sign-in from the last 2 hours, by an owner. 404 when the organization has no key for that provider. Audited as `org.model_key.delete`.",
1814 access: "org-owner",
1815 status: 204,
1816 },
1817 "GET /orgs/{org}/diffinery/model-usage": {
1818 summary: "Get an organization's model usage",
1819 description: "Owners only; a token limited to repositories gets 403. The runs the organization's keys paid for over the last `days`, shaped like `GET /diffinery/model-usage` with a `person` on each line, and `month`: `month`, `member_monthly_tokens` and `people` (each `person` and their `tokens` on the organization's keys this month). See /docs/agents/organizations#usage.",
1820 access: "org-owner",
1821 query: { days: ["integer", "1 to 90 (default 30)."] },
1822 },
1823 "PUT /diffinery/model-keys/{provider}": {
1824 summary: "Save a model key",
1825 description: "From a browser session only: no token, agent, job or sandbox can set a model key. `provider` is `anthropic`, `openai` or `openrouter`. `accept_terms: true` records that you use that provider through Diffinery under your own account and its terms (422 without it). The key is encrypted, never shown again and never sent to a sandbox. Sends you a security email. Audited as `model_key.set`.",
1826 access: "account",
1827 body: { key: ["string", "The API key (`sk-ant-...`, `sk-...` or `sk-or-...`)."], accept_terms: ["boolean", "Must be `true`."] },
1828 },
1829 "DELETE /diffinery/model-keys/{provider}": { summary: "Delete a model key", description: "From a browser session only. Running tasks that use it lose model access. Audited as `model_key.delete`.", access: "account", status: 204 },
1830 "GET /diffinery/tasks": {
1831 summary: "List your agent tasks",
1832 description: "Agent tasks you started across repositories where you still have write access, most recently changed first (for an agent's token: the tasks it runs). A token limited to repositories sees only those.",
1833 access: "account",
1834 },
1835 "GET /repos/{owner}/{repo}/diffinery/tasks": {
1836 summary: "List agent tasks",
1837 description: "The repository's agent tasks, newest first. Needs write access: transcripts show an agent's reasoning and command output. `state` filters: `queued`, `running`, `ready`, `failed` or `stopped`, or `active` for running and queued together.",
1838 access: "write",
1839 query: { state: ["string", "`queued`, `running`, `ready`, `failed`, `stopped` or `active` (running or queued)."], per_page: ["integer", "At most 100 (default 50)."] },
1840 },
1841 "POST /repos/{owner}/{repo}/diffinery/tasks": {
1842 summary: "Start an agent task",
1843 description: "From a browser session only, by a person with write access, as one of their agents (they own it, or administer the organization that owns it) that has write access here: each run spends the person's own model key, so no token can start one. The agent works in a sandbox, then Diffinery pushes `agent/<number>-<slug>` and opens a pull request as the agent. 422 when the runtime needs a model key you haven't saved. Audited as `task.start`. See /docs/agents/tasks.",
1844 access: "write",
1845 status: 201,
1846 body: {
1847 goal: ["string", "What to do (required, at most 20,000 characters)."],
1848 runtime: ["string", "`claude-code` (default), `codex` or `custom`."],
1849 provider: ["string", "Where the runtime's model calls go: its own provider (`anthropic` for Claude Code, `openai` for Codex; the default) or `openrouter`."],
1850 model: ["string", "With `provider: openrouter`: the OpenRouter model id (default `~anthropic/claude-sonnet-latest` for Claude Code, `~openai/gpt-sol-latest` for Codex)."],
1851 agent: ["string", "The agent's login (required)."],
1852 base: ["string", "Branch to start from (default: the default branch)."],
1853 plan_first: ["boolean", "`true` to plan in a read-only sandbox before building, `false` to build at once. Left out, the project's default; a project that requires plans ignores `false`. See /docs/agents/tasks#plan-first."],
1854 attempts: ["integer", "1 (default) to 4: run the first build as that many attempts side by side, then pick one (422 above 1 for a task on a pull request's branch). See /docs/agents/tasks#best-of-n."],
1855 fix_checks: ["boolean", "Start a fix run when the pull request's checks fail (default `true`, `false` for a task on a pull request's branch). See /docs/agents/tasks#verify-and-land."],
1856 land: ["boolean", "Turn on auto-merge for its pull request as the task's agent, so it merges once approved and checks pass (default `false`)."],
1857 command: ["string", "For `custom`: the shell command to run."],
1858 },
1859 },
1860 "GET /repos/{owner}/{repo}/diffinery/project": {
1861 summary: "Get the project",
1862 description: "The repository's project: `settings` (`max_running`, `instructions`, `plan_first`), `memory` (`body`, `version`, `updated_by`, `updated_at`) and `board`, the agent tasks by column (`waiting`, `working`, `queued`, `review`, `landing`, `done`, `idle`). Agent reviews aren't on the board. Needs write access. See /docs/agents/projects.",
1863 access: "write",
1864 },
1865 "GET /repos/{owner}/{repo}/diffinery/project/posts": {
1866 summary: "List the project conversation",
1867 description: "Posts and task events, oldest first: those after `after`, or the latest 100. Needs write access.",
1868 access: "write",
1869 query: { after: ["integer", "Return posts after this id (default 0)."], per_page: ["integer", "At most 100 (default 100)."] },
1870 },
1871 "POST /repos/{owner}/{repo}/diffinery/project/posts": {
1872 summary: "Post to the project conversation",
1873 description: "Markdown, at most 20,000 characters (422 otherwise). If you have a coordinator in this project, it handles the post unless you send `coordinator: false`; the response's `coordinator` says whether it did. A person in a browser session only. 403 on an archived repository.",
1874 access: "write",
1875 status: 201,
1876 body: { body: ["string", "The post.", true], coordinator: ["boolean", "`false` to post without handing it to your coordinator."] },
1877 },
1878 "POST /repos/{owner}/{repo}/diffinery/project/posts/{n}/handle": {
1879 summary: "Hand a post to your coordinator",
1880 description: "Your coordinator handles post `n`, anyone's, on your model key. Each post is handled once. A person in a browser session only. Audited as `project.coordinate`. See /docs/agents/projects#coordinator.",
1881 access: "write",
1882 },
1883 "GET /repos/{owner}/{repo}/diffinery/project/coordinator": {
1884 summary: "Get your coordinator",
1885 description: "Your coordinator's settings in this project: `enabled`, `agent`, `runtime`, `command`, `provider`, `model`, `tokens_per_run`, `tokens_per_task`, `opted_in_at`, `updated_at`. Needs write access.",
1886 access: "write",
1887 },
1888 "GET /repos/{owner}/{repo}/diffinery/project/coordinators": {
1889 summary: "List the coordinators in a project",
1890 description: "Every person's coordinator in this project, oldest first: `person`, `agent`, `runtime`, `provider`, `model`, `opted_in_at`. Never their token budgets or custom command. Needs write access. See /docs/agents/projects#coordinator.",
1891 access: "write",
1892 },
1893 "PUT /repos/{owner}/{repo}/diffinery/project/coordinator": {
1894 summary: "Turn on or change your coordinator",
1895 description: "Your agent then reads your posts and turns them into tasks, on your model key. Each coordinator starts at most 5 tasks per post and 20 per 24 hours in the project. A person in a browser session only. Audited as `project.coordinator`. See /docs/agents/projects#coordinator.",
1896 access: "write",
1897 body: {
1898 agent: ["string", "The login of an agent you manage with write access to the repository.", true],
1899 runtime: ["string", "`claude-code` (default), `codex` or `custom`."],
1900 command: ["string", "For `custom`: the shell command."],
1901 provider: ["string", "`openrouter` to run Claude Code or Codex via OpenRouter."],
1902 model: ["string", "With `provider: openrouter`: the OpenRouter model id."],
1903 tokens_per_run: ["integer", "Token budget of each coordinator run, 100,000 to 50,000,000 (default 1,000,000)."],
1904 tokens_per_task: ["integer", "Token budget of each run of a task it starts, 100,000 to 50,000,000 (default 2,000,000)."],
1905 accept: ["boolean", "Must be `true`: confirms your coordinator and its tasks use your model key.", true],
1906 },
1907 },
1908 "DELETE /repos/{owner}/{repo}/diffinery/project/coordinator": { summary: "Turn off your coordinator", description: "A person in a browser session only. Audited as `project.coordinator`.", access: "write" },
1909 "GET /repos/{owner}/{repo}/diffinery/project/memory": {
1910 summary: "Get the project memory",
1911 description: "The current memory and its last 50 `versions` (`version`, `updated_by`, `updated_at`, `length`). Every agent task reads it as background notes; agent reviews don't. Needs write access.",
1912 access: "write",
1913 },
1914 "PUT /repos/{owner}/{repo}/diffinery/project/memory": {
1915 summary: "Save or restore the project memory",
1916 description: "Saves `body` (at most 40,000 characters) as a new version, or restores an earlier version as a new one with `restore` (404 `no such memory version`). A person in a browser session only. Audited as `project.memory`.",
1917 access: "write",
1918 body: { body: ["string", "The new memory (Markdown)."], restore: ["integer", "A version number to restore instead."] },
1919 },
1920 "PUT /repos/{owner}/{repo}/diffinery/project/settings": {
1921 summary: "Change the project settings",
1922 description: "Project instructions every agent task and agent review gets, how many tasks run at once, and whether tasks plan first. Tasks beyond `max_running` queue and start oldest first. A person in a browser session only. Audited as `project.settings`.",
1923 access: "admin",
1924 body: {
1925 max_running: ["integer", "1 to 10 (default 3)."],
1926 instructions: ["string", "At most 10,000 characters."],
1927 plan_first: ["string", "`off` (default: tasks build at once unless started with `plan_first: true`), `default` (tasks plan first unless started with `plan_first: false`) or `required` (every task plans first, including ones coordinators start)."],
1928 },
1929 },
1930 "GET /repos/{owner}/{repo}/diffinery/tasks/{number}": {
1931 summary: "Get an agent task",
1932 description: "The task, its runs and its pull request. Needs write access. A plan-first task also has `plan_state` (`planning`, `waiting`, `approved` or `split`) and `plan`; a task split from a plan has `parent_task`. A task with more than one attempt has `pick_state` (`running`, `waiting` or `picked`) and `picked_attempt`, and each run its `attempt`. Every task has `fix_checks`, `check_fixes` (fix runs started so far) and `land`, `paid_by` (`person`, or `organization` when an organization's key pays), and `usage` (the model usage of all its runs; each run has its own `usage`, with `source`: `proxy`, or `runtime` for a run in a terminal). See /docs/agents/tasks#usage-and-cost.",
1933 access: "write",
1934 },
1935 "PATCH /repos/{owner}/{repo}/diffinery/tasks/{number}": {
1936 summary: "Change a task's options",
1937 description: "From a browser session only, by the person who started the task. `fix_checks` starts a fix run when its pull request's checks fail (one per commit, at most 3 per task); turning `land` on turns on auto-merge for an open pull request as the task's agent, under the usual rules for agents. 422 when a value isn't `true` or `false`; 409 `only agent tasks have these options` for an agent review. Audited as `task.options`. See /docs/agents/tasks#verify-and-land.",
1938 access: "write",
1939 body: { fix_checks: ["boolean", "Fix failing checks."], land: ["boolean", "Land when approved."] },
1940 },
1941 "GET /repos/{owner}/{repo}/diffinery/tasks/{number}/attempts": {
1942 summary: "Compare a task's attempts",
1943 description: "Needs write access. One entry per attempt of a best-of-N task: `attempt`, `run`, `state`, `error`, `branch`, `head_sha`, `changes` (`files`, `additions`, `deletions`), `checks` and `failing_checks` on its branch, `model_requests`, `tokens`, `summary` (its last message) and `compare_url`. See /docs/agents/tasks#best-of-n.",
1944 access: "write",
1945 },
1946 "POST /repos/{owner}/{repo}/diffinery/tasks/{number}/attempts/{attempt}/pick": {
1947 summary: "Pick a task's attempt",
1948 description: "From a browser session only, by the person who started the task. The attempt's branch becomes the task's branch, with a pull request; the other attempts' branches are deleted. 409 `the attempts are still running`, `attempt <n> is already picked` or `this task has no attempts to pick from`; 404 `no such attempt`; 422 `attempt <n> has no work to pick: ...` when it changed nothing or didn't finish. Audited as `task.pick`.",
1949 access: "write",
1950 status: 202,
1951 },
1952 "POST /repos/{owner}/{repo}/diffinery/tasks/{number}/plan/approve": {
1953 summary: "Approve a task's plan",
1954 description: "From a browser session only, by the person who started the task. The build run starts with the goal and the plan, and every later run gets the plan as the approved one. 409 `the plan isn't ready yet` while it plans, `this task has no plan waiting for approval`, or `the task is running`. Audited as `task.plan_approve`. See /docs/agents/tasks#plan-first.",
1955 access: "write",
1956 status: 202,
1957 },
1958 "POST /repos/{owner}/{repo}/diffinery/tasks/{number}/plan/split": {
1959 summary: "Split a task's plan into tasks",
1960 description: "From a browser session only, by the person who started the task. Your coordinator gets the goal and the plan as a post of yours and starts tasks for its parts, each with `parent_task`; this task is then done with. Returns `post`, `task` (the coordinator's run) and `status` (`started` or `queued`). 422 `set up your coordinator for this project first` without a coordinator; the same 409s as approving. Audited as `task.plan_split`.",
1961 access: "write",
1962 status: 202,
1963 },
1964 "GET /repos/{owner}/{repo}/diffinery/tasks/{number}/transcript": {
1965 summary: "Read a task's transcript",
1966 description: "Lines after `after` (a line number), oldest first: `stream` is `out` and `err` (the runtime's output, Claude Code's `stream-json` or Codex's `--json` lines), `setup` (installing and pushing) or `note` (Diffinery's own). Model keys and git credentials are masked.",
1967 access: "write",
1968 query: { after: ["integer", "Return lines after this one."], per_page: ["integer", "At most 5000 (default 2000)."] },
1969 },
1970 "POST /repos/{owner}/{repo}/diffinery/tasks/{number}/messages": {
1971 summary: "Send a task a follow-up",
1972 description: "From a browser session only, by the person who started the task. Starts a new run on the task's branch with the message (while a run is active, it waits and starts when that run ends). Audited as `task.message`.",
1973 access: "write",
1974 status: 201,
1975 body: { body: ["string", "The message."] },
1976 },
1977 "POST /repos/{owner}/{repo}/diffinery/tasks/{number}/retry": { summary: "Retry a task", description: "From a browser session only, by the person who started the task, when no run is active (409 otherwise). Audited as `task.retry`.", access: "write", status: 201 },
1978 "POST /repos/{owner}/{repo}/diffinery/tasks/{number}/stop": {
1979 summary: "Stop a task",
1980 description: "The person who started it, its agent, or a repository admin. Stops the agent, closes the sandbox (its git credential stops working) and ends the run's model access. Work already pushed stays. Audited as `task.stop`.",
1981 access: "write",
1982 },
1983 "GET /repos/{owner}/{repo}/diffinery/agent-review": {
1984 summary: "Get agent review settings",
1985 description: "Whether agent review is on, which agent reviews with which runtime, whose model key every review uses (`uses_key_of`), when it reviews (`trigger`: `push`, `open` or `request`), which pull requests (`authors`: `all`, `people` or `agents`), `drafts`, `autofix`, `daily_limit` and `tokens_per_review`. `{\"enabled\": false}` when it was never turned on. Needs write access. See /docs/agents/review.",
1986 access: "write",
1987 },
1988 "PUT /repos/{owner}/{repo}/diffinery/agent-review": {
1989 summary: "Turn agent review on, change or off",
1990 description: "From a browser session only, by a repository admin who can run tasks as the agent and has the runtime's model key: every review on the repository, including ones other people ask for, spends that admin's key, so `accept_terms` must be true. `{\"enabled\": false}` turns it off. Audited as `review.settings`.",
1991 access: "admin",
1992 body: {
1993 agent: ["string", "The reviewing agent's login (required)."],
1994 runtime: ["string", "`claude-code` (default), `codex` or `custom`."],
1995 provider: ["string", "Where the runtime's model calls go: its own provider (`anthropic` for Claude Code, `openai` for Codex; the default) or `openrouter`."],
1996 model: ["string", "With `provider: openrouter`: the OpenRouter model id (default `~anthropic/claude-sonnet-latest` for Claude Code, `~openai/gpt-sol-latest` for Codex)."],
1997 command: ["string", "For `custom`: the shell command; it must end its output with the findings block the prompt describes."],
1998 trigger: ["string", "`push` (default: on open and every push), `open` (once per pull request) or `request` (only when asked)."],
1999 authors: ["string", "`all` (default), `people` or `agents`."],
2000 drafts: ["boolean", "Review drafts too (default false)."],
2001 autofix: ["boolean", "Start a fix task on the pull request's branch for blocking findings (default false; at most 3 times per pull request)."],
2002 daily_limit: ["integer", "Most reviews and autofix runs the repository starts in 24 hours, 1 to 1000 (default 50). Past it, pull requests get an action-required check, autofix waits, and you get an email."],
2003 tokens_per_review: ["integer", "Most tokens one review may use, counted from the provider's responses (cache reads count as a tenth), 100000 to 50000000 (default 2000000)."],
2004 accept_terms: ["boolean", "Must be true: reviews use your model key."],
2005 enabled: ["boolean", "`false` turns agent review off."],
2006 },
2007 },
2008 "GET /repos/{owner}/{repo}/pulls/{number}/diffinery/agent-review": {
2009 summary: "Get a pull request's agent review",
2010 description: "The pull request's review task (each review is a run of it), the last reviewed head (`reviewed_sha`) and every finding: `id`, `path`, `line`, `side`, `severity` (`blocking` or `not_blocking`), `title`, `body`, `suggestion`, `state` (`open`, `fixed` or `dismissed`), `fixed_in` and the review `comment_id`. Needs write access.",
2011 access: "write",
2012 },
2013 "POST /repos/{owner}/{repo}/pulls/{number}/diffinery/agent-review": {
2014 summary: "Ask for an agent review",
2015 description: "Reviews the pull request now, or once its current review ends, whatever the trigger setting. People with write access only (any of their tokens); agents can't ask. 422 when agent review is off; 429 past a daily limit (the repository's, or 20 reviews of one pull request in 24 hours). Audited as `review.request`.",
2016 access: "write",
2017 status: 202,
2018 },
2019 "POST /repos/{owner}/{repo}/diffinery/sandboxes/{id}/{action}": {
2020 summary: "Use a sandbox",
2021 description: "`action` is `exec` (run a command), `read` or `write` (files), or `close`.",
2022 access: "write",
2023 },
2024
2025 // ------------------------------------------------------------ GitHub imports and linked accounts
2026 "POST /diffinery/imports": {
2027 summary: "Start a GitHub import",
2028 description:
2029 "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).",
2030 access: "account",
2031 status: 201,
2032 body: {
2033 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`."],
2034 source_owner: ["string", "A GitHub organization or user: imports its repositories (private ones too when the token can see them)."],
2035 owner: ["string", "The Diffinery user or organization that will own the new repositories.", true],
2036 name: ["string", "`source` only: the new repository's name. Default: the GitHub name. It must not exist yet."],
2037 visibility: ["string", "`keep` (default: public stays public, private and internal become private), `private` or `public`. `internal` is accepted and means `private`."],
2038 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."],
2039 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`."],
2040 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."],
2041 repositories: ["string[]", "`source_owner` only: the repository names to import, at most 1,000. Default: every repository (up to 1,000)."],
2042 exclude_forks: ["boolean", "`source_owner` only: skip forks (default `false`)."],
2043 exclude_archived: ["boolean", "`source_owner` only: skip archived repositories (default `false`)."],
2044 concurrency: ["integer", "`source_owner` only: repositories imported at the same time, 1 to 8 (default 3)."],
2045 },
2046 },
2047 "GET /diffinery/imports": {
2048 summary: "List your imports",
2049 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`.",
2050 access: "account",
2051 },
2052 "GET /diffinery/imports/{id}": {
2053 summary: "Get an import",
2054 description:
2055 "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`.",
2056 access: "account",
2057 },
2058 "POST /diffinery/imports/{id}/cancel": {
2059 summary: "Cancel an import",
2060 description:
2061 "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`.",
2062 access: "account",
2063 },
2064 "POST /diffinery/imports/{id}/retry": {
2065 summary: "Retry an import",
2066 description:
2067 "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`.",
2068 access: "account",
2069 status: 202,
2070 body: { github_token: ["string", "A GitHub token for this attempt (optional; at most 400 characters)."] },
2071 },
2072 "POST /diffinery/imports/{id}/selection": {
2073 summary: "Choose repositories for a draft import",
2074 description:
2075 "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`.",
2076 access: "account",
2077 status: 202,
2078 body: { repositories: ["string[]", "Names of the repositories to import, at most 1,000.", true] },
2079 },
2080 "POST /diffinery/imports/{id}/claim": {
2081 summary: "Claim an import's Git transfer (local development)",
2082 description:
2083 "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`.",
2084 access: "account",
2085 },
2086 "GET /diffinery/imports/{id}/agent/spec": {
2087 summary: "Get a Git transfer spec (import agent)",
2088 description:
2089 "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`).",
2090 access: "account",
2091 },
2092 "POST /diffinery/imports/{id}/agent/progress": {
2093 summary: "Report Git transfer progress (import agent)",
2094 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.",
2095 access: "account",
2096 status: 204,
2097 },
2098 "POST /diffinery/imports/{id}/agent/complete": {
2099 summary: "Report a Git transfer's result (import agent)",
2100 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.",
2101 access: "account",
2102 status: 204,
2103 },
2104 "GET /diffinery/user/linked-accounts": {
2105 summary: "List your linked GitHub accounts",
2106 description:
2107 "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.",
2108 access: "account",
2109 },
2110 "POST /diffinery/user/linked-accounts/github/challenge": {
2111 summary: "Get a GitHub link code",
2112 description:
2113 "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`).",
2114 access: "account",
2115 body: { api_url: ["string", "GitHub Enterprise REST API base, such as `https://ghe.example.com/api/v3`. Default: github.com."] },
2116 },
2117 "POST /diffinery/user/linked-accounts/github": {
2118 summary: "Link a GitHub account",
2119 description:
2120 "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`.",
2121 access: "account",
2122 status: 201,
2123 body: { login: ["string", "Your GitHub username.", true], api_url: ["string", "The same API base you used for the challenge. Default: github.com."] },
2124 },
2125 "DELETE /diffinery/user/linked-accounts/{id}": {
2126 summary: "Unlink a GitHub account",
2127 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`.",
2128 access: "account",
2129 status: 204,
2130 },
2131 "DELETE /diffinery/admin/linked-accounts/{id}": {
2132 summary: "Clear anyone's GitHub link",
2133 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`.",
2134 access: "site-admin",
2135 status: 204,
2136 },
2137
2138 // ------------------------------------------------------------ packages
2139 "GET /repos/{owner}/{repo}/packages": { summary: "List a repository's packages", description: "Container images linked to the repository.", access: "read" },
2140 ...packageDocs("/user/packages", "your"),
2141 ...packageDocs("/users/{owner}/packages", "a user's"),
2142 ...packageDocs("/orgs/{owner}/packages", "an organization's"),
2143};
2144
2145function packageDocs(prefix: string, whose: string): Record<string, OpDoc> {
2146 return {
2147 [`GET ${prefix}`]: { summary: `List ${whose} packages`, description: "Only packages you can pull.", access: "account" },
2148 [`GET ${prefix}/{type}/{name}`]: { summary: `Get one of ${whose} packages`, description: "`type` is `container`.", access: "read" },
2149 [`PATCH ${prefix}/{type}/{name}`]: {
2150 summary: `Update one of ${whose} packages`,
2151 description: "Change its visibility or the repository it is linked to.",
2152 access: "admin",
2153 body: { visibility: ["string", "`private` or `public`."], repository: ["string", "Repository name to link, or `null` to unlink."] },
2154 },
2155 [`DELETE ${prefix}/{type}/{name}`]: { summary: `Delete one of ${whose} packages`, access: "admin", status: 204 },
2156 [`GET ${prefix}/{type}/{name}/versions`]: { summary: "List package versions", access: "read", query: { state: ["string", "`tagged` for tagged versions only."], ...page } },
2157 [`GET ${prefix}/{type}/{name}/versions/{id}`]: { summary: "Get a package version", access: "read" },
2158 [`DELETE ${prefix}/{type}/{name}/versions/{id}`]: { summary: "Delete a package version", access: "admin", status: 204 },
2159 };
2160}
2161
2162/** GitHub's tarball and zipball endpoints, with and without a ref. */
2163function archiveDocs(kind: "tarball" | "zipball", what: string, format: string): Record<string, OpDoc> {
2164 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).`;
2165 return {
2166 [`GET /repos/{owner}/{repo}/${kind}`]: { summary: `Download ${what}`, description: `${description} Uses the default branch.`, access: "read" },
2167 [`GET /repos/{owner}/{repo}/${kind}/{ref}`]: { summary: `Download ${what} of a ref`, description, access: "read" },
2168 };
2169}
2170
2171/** Reactions on one kind of subject. `note` adds a sentence to the list endpoint. */
2172function reactionDocs(prefix: string, what: string, note?: string): Record<string, OpDoc> {
2173 return {
2174 [`GET ${prefix}/reactions`]: {
2175 summary: `List reactions to ${what}`,
2176 ...(note ? { description: note } : {}),
2177 access: "read",
2178 query: { content: ["string", "Only reactions with this content."], ...page },
2179 },
2180 [`POST ${prefix}/reactions`]: {
2181 summary: `Add a reaction to ${what}`,
2182 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.`,
2183 access: "read",
2184 status: 201,
2185 body: { content: ["string", "`+1`, `-1`, `laugh`, `confused`, `heart`, `hooray`, `rocket` or `eyes`.", true] },
2186 },
2187 [`DELETE ${prefix}/reactions/{reaction}`]: { summary: `Delete a reaction from ${what}`, description: "Your own reactions, or anyone's with admin access.", access: "read", status: 204 },
2188 };
2189}