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.

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