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.

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