Docs: GitHub importer, GITHUB_TOKEN permissions, auto-merge and everything shipped since #32 (#45)
Covers #28, #29, #31, #36, #37, #42, #43 and #44. Every REST route has hand-written notes; no token can create tokens or agents (browser session only). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016q5wwBFck8JHq14gHUM2CL
Canberk Demirci authored · GitHub committed signed1 parent 1e5d220commit 4ac92f20539c991989f65d0d55e9e70709a3c802Browse files
51 files changed+4021−1106
Files (51)
- apps/diffinery/docs/accounts/invitations.md+8−3
- apps/diffinery/docs/accounts/notifications.md+4−2
- apps/diffinery/docs/accounts/organizations.md+190−23
- apps/diffinery/docs/accounts/permissions.md+43−19
- apps/diffinery/docs/accounts/profile-and-preferences.md+45−11
- apps/diffinery/docs/accounts/tokens.md+15−10
- apps/diffinery/docs/actions/caching-and-artifacts.md+299−149
- apps/diffinery/docs/actions/compatibility.md+148−118
- apps/diffinery/docs/actions/docker.md+19−4
- apps/diffinery/docs/actions/environments.md+48−22
- apps/diffinery/docs/actions/events.md+48−22
- apps/diffinery/docs/actions/expressions.md+42−19
- apps/diffinery/docs/actions/oidc.md+21−6
- apps/diffinery/docs/actions/overview.md+46−16
- apps/diffinery/docs/actions/reusable-workflows.md+25−7
- apps/diffinery/docs/actions/runners.md+25−5
- apps/diffinery/docs/actions/secrets-and-variables.md+133−35
- apps/diffinery/docs/actions/workflow-permissions.md+343−0
- apps/diffinery/docs/actions/workflow-syntax.md+80−30
- apps/diffinery/docs/admin/site-administration.md+25−5
- apps/diffinery/docs/agents/mcp.md+57−7
- apps/diffinery/docs/agents/overview.md+46−17
- apps/diffinery/docs/agents/sandboxes.md+2−1
- apps/diffinery/docs/collaboration/checks-and-statuses.md+27−5
- apps/diffinery/docs/collaboration/issues.md+87−6
- apps/diffinery/docs/collaboration/merge-queue.md+17−6
- apps/diffinery/docs/collaboration/pull-requests.md+260−24
- apps/diffinery/docs/collaboration/releases.md+9−6
- apps/diffinery/docs/concepts.md+24−16
- apps/diffinery/docs/index.md+2−2
- apps/diffinery/docs/migrate-from-github.md+678−153
- apps/diffinery/docs/nav.json+1−0
- apps/diffinery/docs/packages/container-registry.md+6−4
- apps/diffinery/docs/quickstart.md+13−3
- apps/diffinery/docs/reference/limits.md+48−9
- apps/diffinery/docs/reference/rest-api.md+121−27
- apps/diffinery/docs/reference/webhook-events.md+118−42
- apps/diffinery/docs/repositories/branch-protection.md+26−12
- apps/diffinery/docs/repositories/browsing-code.md+5−1
- apps/diffinery/docs/repositories/git-lfs.md+15−10
- apps/diffinery/docs/repositories/git.md+30−7
- apps/diffinery/docs/repositories/overview.md+137−29
- apps/diffinery/docs/repositories/webhooks.md+9−7
- apps/diffinery/docs/security/audit-log.md+48−24
- apps/diffinery/docs/security/overview.md+26−18
- apps/diffinery/docs/security/push-protection.md+85−21
- apps/diffinery/docs/self-hosting/architecture.md+96−34
- apps/diffinery/docs/self-hosting/configuration.md+58−11
- apps/diffinery/docs/self-hosting/deploy.md+29−41
- apps/diffinery/src/docs/api-meta.ts+314−50
- apps/diffinery/src/docs/reference.ts+20−7
modifiedapps/diffinery/docs/accounts/invitations.md+8−3View file
| @@ -38,6 +38,8 @@ | ||
| 38 | 38 | |
| 39 | 39 | The page shows "Invite created for <email>" with the link and a copy button: "We emailed it to <email>; you can also send it yourself. It expires in 14 days and works once: they pick a username and create a passkey." |
| 40 | 40 | |
| 41 | +An address can have only one pending instance invitation at a time (letter case doesn't matter). A second one is refused with "friend@example.com already has a pending invitation; revoke it first to send a new one"; instance invitations can't be revoked, so send the person the first link again, or wait until it expires after 14 days. | |
| 42 | + | |
| 41 | 43 | ### Add people to an organization |
| 42 | 44 | |
| 43 | 45 | Organization owners and admins, and site administrators. The **Add people** form on the organization's **People** tab takes a username or an email address. |
| @@ -54,7 +56,7 @@ | ||
| 54 | 56 | |
| 55 | 57 | The invitation then appears under **Pending invitations** on the same tab, with who sent it, when, the expiry date and the role. See [Organizations](/docs/accounts/organizations#add-people) for every message the form can show. |
| 56 | 58 | |
| 57 | -You can send only one pending invitation per email address and organization from this form. To send a new one, revoke the old one first ("friend@example.com already has a pending invitation; revoke it first to send a new one"). | |
| 59 | +An email address can have only one pending invitation per organization, whether it came from this form, the CLI or the API. Letter case doesn't matter: `Friend@Example.com` and `friend@example.com` are the same address. To send a new one, revoke the old one first ("friend@example.com already has a pending invitation; revoke it first to send a new one"). | |
| 58 | 60 | |
| 59 | 61 | ### Revoke an invitation |
| 60 | 62 | |
| @@ -85,6 +87,7 @@ | ||
| 85 | 87 | | "acme is an organization; add its people instead" | Organizations can't be collaborators. | |
| 86 | 88 | | "diffy's account is disabled" | A site administrator disabled the account. | |
| 87 | 89 | | "enter a valid email address" | The text has an `@` but isn't a complete address. | |
| 90 | +| "friend@example.com already has a pending invitation; revoke it first to send a new one" | The address already has an open invitation to this repository (letter case doesn't matter). Repository invitations can't be revoked, so send the person the first link again, or wait until it is used or expires after 14 days. | | |
| 88 | 91 | |
| 89 | 92 | The repository's **Settings → Collaborators** page doesn't list pending invitations. See [Permissions](/docs/accounts/permissions) for what each role allows. |
| 90 | 93 | |
| @@ -116,7 +119,7 @@ | ||
| 116 | 119 | | `-r, --role ROLE` | `member` (default), `admin` or `owner`. Needs `--org`. | |
| 117 | 120 | | `--json` | Print the API response (`id` and `url`) as JSON. | |
| 118 | 121 | |
| 119 | -The command prints the invite link and reminds you that it is valid for 14 days. Unlike the web form, `diffinery invite` always creates an invitation, even when the address already has an account (they accept it with **Accept invite**; see [Accept an invite with an existing account](#accept-an-invite-with-an-existing-account)). The CLI can't create repository invites; use the repository's **Settings → Collaborators** page. See [CLI reference](/docs/reference/cli) for every command. | |
| 122 | +The command prints the invite link and reminds you that it is valid for 14 days. Unlike the web form, `diffinery invite` always creates an invitation, even when the address already has an account (they accept it with **Accept invite**; see [Accept an invite with an existing account](#accept-an-invite-with-an-existing-account)). It refuses an address that already has a pending invitation to the organization, and an address whose account is already a member of it (see [API errors](#api-errors)). The CLI can't create repository invites; use the repository's **Settings → Collaborators** page. See [CLI reference](/docs/reference/cli) for every command. | |
| 120 | 123 | |
| 121 | 124 | ## Invite with the API |
| 122 | 125 | |
| @@ -144,7 +147,7 @@ | ||
| 144 | 147 | { "id": "3f1c0e8a9b7d4c2e8f6a5b4c3d2e1f0a", "url": "https://diffinery.com/invite/..." } |
| 145 | 148 | ``` |
| 146 | 149 | |
| 147 | -The caller must be an owner or admin of the organization, or a site administrator. Only owners (and site administrators) can invite with the `owner` role. Like the CLI, this endpoint always creates an invitation; it doesn't add existing accounts directly. | |
| 150 | +The caller must be an owner or admin of the organization, or a site administrator. Only owners (and site administrators) can invite with the `owner` role. Like the CLI, this endpoint always creates an invitation; it doesn't add existing accounts directly. It refuses an address that already has a pending invitation to the organization (compared without regard to letter case) and the address of an account that is already a member. | |
| 148 | 151 | |
| 149 | 152 | ### Instance invite |
| 150 | 153 | |
| @@ -173,6 +176,8 @@ | ||
| 173 | 176 | | 404 | `not found` | The organization doesn't exist, or the name is not an organization. | |
| 174 | 177 | | 422 | `a valid email is required` | The `email` field is missing or isn't an email address. | |
| 175 | 178 | | 422 | `role must be member, admin or owner` | `role` has another value. | |
| 179 | +| 422 | `friend@example.com already has a pending invitation; revoke it first to send a new one` | The address already has an open invitation to this organization (or, for `POST /api/v3/invitations`, an open instance invitation). Letter case doesn't matter. Revoke the organization invitation on the **People** tab first. | | |
| 180 | +| 422 | `diffy is already a member of this organization` | The address belongs to an account that is already a member. Change their role on the **People** tab instead. | | |
| 176 | 181 | |
| 177 | 182 | Error bodies are JSON: `{"message": "...", "documentation_url": "..."}`. |
| 178 | 183 |
modifiedapps/diffinery/docs/accounts/notifications.md+4−2View file
| @@ -144,6 +144,8 @@ | ||
| 144 | 144 | -H "Authorization: Bearer dfy_..." |
| 145 | 145 | ``` |
| 146 | 146 | |
| 147 | +Watching, ignoring and subscribing to issues also work on archived repositories, which otherwise refuse every change. | |
| 148 | + | |
| 147 | 149 | `GET /api/v3/repos/{owner}/{repo}/subscription` returns `{"subscribed": true|false, "ignored": true|false, ...}`, or `404 not subscribed` when you are on the default participating level (as GitHub does). `PUT` with `{"subscribed": false}` also returns you to participating. Changing the level needs a `write` or `admin` token. |
| 148 | 150 | |
| 149 | 151 | ## Subscribe to or mute an issue or pull request |
| @@ -231,8 +233,8 @@ | ||
| 231 | 233 | | `token_created` | `[Diffinery] A personal access token was added to your account` (or `A token was created for the agent review-bot`) | Security | `security_alert` | A token was created for you or your agent, from the web UI, CLI or API | |
| 232 | 234 | | `token_expiring` | `[Diffinery] Your token "ci-deploy" expires on <date>` | Security | `security_alert` | About 7 days before a token expires (once per token, for tokens that last more than 8 days) | |
| 233 | 235 | | `agent_created` | `[Diffinery] A new agent was added: review-bot` | Security | `security_alert` | You created an agent | |
| 234 | -| `push_blocked` | `[acme/website] Your push was blocked: possible secrets` | Security | `security_alert` | Push protection rejected your push (for an agent's push, its owner is told) | | |
| 235 | -| `push_bypassed` | `[acme/website] Push protection was bypassed` | Security | `security_alert` | A repository admin pushed past push protection; sent to the pusher and the repository's admins | | |
| 236 | +| `push_blocked` | `[acme/website] Your push was blocked: possible secrets` | Security | `security_alert` | Push protection rejected your `git push` (for an agent's push, its owner is told). Commits refused in the web editor, the REST API or MCP are shown as an error and send no email. | | |
| 237 | +| `push_bypassed` | `[acme/website] Push protection was bypassed` | Security | `security_alert` | A repository admin pushed past push protection, or committed a flagged file through the commits API with `allow_secrets`; sent to that admin and the repository's admins | | |
| 236 | 238 | | `account_status` | `[Diffinery] Your account was disabled` or `Your account was enabled` | Security | `account` | A site administrator disabled your account, or enabled it again | |
| 237 | 239 | | `invite` | `@diffy invited you to join the acme organization on Diffinery` | Account | `invitation` | Someone invited your email address (organization, repository or instance) | |
| 238 | 240 | | `welcome` | `Welcome to Diffinery, @diffy` | Account | `account` | You created your account from an invite | |
modifiedapps/diffinery/docs/accounts/organizations.md+190−23View file
| @@ -1,19 +1,19 @@ | ||
| 1 | 1 | --- |
| 2 | 2 | title: Organizations |
| 3 | -description: Create organizations, add people by username or email, manage roles and invitations, set the organization's picture, and share repositories and secrets. | |
| 3 | +description: Create organizations, add people by username or email, manage roles and invitations, set the base permission and picture, and share repositories. | |
| 4 | 4 | --- |
| 5 | 5 | |
| 6 | -An organization is a shared account that owns repositories, agents, container images and Actions secrets on behalf of a group of people. Use one when more than one person works on the same code, so access doesn't depend on any one personal account. | |
| 6 | +An organization is a shared account that owns repositories, agents, container images and Actions secrets and variables on behalf of a group of people. Use one when more than one person works on the same code, so access doesn't depend on any one personal account. | |
| 7 | 7 | |
| 8 | 8 | ## Organization roles |
| 9 | 9 | |
| 10 | -Every member has one of three roles. The role decides what they can do in the organization and the role they get on every repository the organization owns. | |
| 10 | +Every member has one of three roles. The role decides what they can do in the organization and the role they get on every repository the organization owns: owners and admins always get `admin`, and members get the organization's [base permission](#base-permission). | |
| 11 | 11 | |
| 12 | 12 | | Capability | member | admin | owner | |
| 13 | 13 | | --- | --- | --- | --- | |
| 14 | -| Role on every organization repository | the [base permission](#base-permission) (read by default) | admin | admin | | |
| 15 | -| See the organization's private repositories | Unless the base permission is none | Yes | Yes | | |
| 16 | -| Create repositories in the organization (and administer the ones they create) | Yes | Yes | Yes | | |
| 14 | +| Role on every organization repository | The base permission (`read` for new organizations) | admin | admin | | |
| 15 | +| See the organization's private repositories | Yes, unless the base permission is `none` | Yes | Yes | | |
| 16 | +| Create repositories in the organization, and get `admin` on the ones you create | Yes | Yes | Yes | | |
| 17 | 17 | | Create container images in the organization and pull its images that aren't linked to a repository | Yes | Yes | Yes | |
| 18 | 18 | | Push to, delete and change the settings of an organization image that isn't linked to a repository | Only images they created | Yes | Yes | |
| 19 | 19 | | See the **People** and **Agents** tabs | Yes | Yes | Yes | |
| @@ -22,21 +22,115 @@ | ||
| 22 | 22 | | Change roles, remove members | No | Yes, except owners | Yes, including owners | |
| 23 | 23 | | Create and manage the organization's agents | No | Yes | Yes | |
| 24 | 24 | | Manage organization Actions secrets | No | Yes | Yes | |
| 25 | +| Read and manage organization Actions variables | No | Yes | Yes | | |
| 25 | 26 | | Open the **Settings** tab: rename, display name, base permission, picture | No | No | Yes | |
| 27 | +| Transfer repositories into the organization | No | Yes | Yes | | |
| 28 | +| Delete the organization (once it owns no repositories or images) | No | No | Yes | | |
| 29 | +| Change the display name, base permission and picture through the API | No | No | Yes | | |
| 26 | 30 | | Get emails when repositories are created or deleted in the organization | No | Yes | Yes | |
| 27 | 31 | |
| 28 | 32 | Site administrators can do everything an owner can in every organization, and have admin on every repository. |
| 29 | 33 | |
| 30 | 34 | A container image linked to a repository follows that repository's roles instead (read pulls, write pushes, admin deletes), so a member who can't see a private repository can't pull its images either. See [Container registry](/docs/packages/container-registry). |
| 31 | 35 | |
| 32 | -A member's role on a repository is the highest of their organization role's repository role and any role given to them directly on that repository. A direct collaborator role can raise an organization member's access (for example to `maintain` or `admin`) but never lowers it. See [Permissions](/docs/accounts/permissions) for what each repository role allows. | |
| 36 | +A member's role on a repository is the highest of the role their organization role gives them (the base permission for members, `admin` for owners and admins) and any role given to them directly on that repository. A direct collaborator role can raise an organization member's access (for example to `maintain` or `admin`) but never lowers it. See [Permissions](/docs/accounts/permissions) for what each repository role allows. | |
| 33 | 37 | |
| 34 | -### Base permission | |
| 38 | +## Base permission | |
| 35 | 39 | |
| 36 | -The base permission is the role every member gets on every repository the organization owns, like GitHub's base permissions: **None**, **Read**, **Write** or **Admin**. New organizations start at **Read**; organizations created before the setting existed keep **Write**. With **None**, members only see the repositories they were given access to and the ones they created. Owners and admins always get admin, and a collaborator role on a repository adds to the base permission. | |
| 40 | +The base permission is the repository role that every member of an organization gets on every repository the organization owns, like GitHub's "Base permissions" setting. It only applies to members: owners and admins always have `admin` on every organization repository, whatever the base permission is. | |
| 37 | 41 | |
| 38 | -Owners change it on the **Settings** tab under **Base permission**, or with `PATCH /api/v3/orgs/{org}` and `{"default_repository_permission": "write"}`. Members and their agents read it as `default_repository_permission` from `GET /api/v3/orgs/{org}`, or with the MCP tool `get_organization`. | |
| 42 | +| Value | Option in the web UI | What members get on every organization repository | | |
| 43 | +| --- | --- | --- | | |
| 44 | +| `none` | No permission: members see only repositories they're given | No role. Members see only public repositories, repositories they were added to as collaborators, and repositories they created. | | |
| 45 | +| `read` | Read: members can clone and view | `read`: view and clone, open issues and comment | | |
| 46 | +| `write` | Write: members can also push | `write`: also push, open and merge pull requests and run workflows | | |
| 47 | +| `admin` | Admin: members can also change settings | `admin`: also change settings, collaborators, branch protection, secrets and webhooks | | |
| 39 | 48 | |
| 49 | +`triage` and `maintain` can't be a base permission; give those roles per repository under **Settings → Collaborators**. See [Permissions](/docs/accounts/permissions) for everything each role allows. | |
| 50 | + | |
| 51 | +### Default base permission | |
| 52 | + | |
| 53 | +- A new organization starts with `read`, as on GitHub. | |
| 54 | +- Organizations that existed before Diffinery added the base permission were set to `write`, so their members kept the access they already had. | |
| 55 | + | |
| 56 | +### How the base permission combines with other access | |
| 57 | + | |
| 58 | +A member's role on an organization repository is the highest of: | |
| 59 | + | |
| 60 | +| Source | Role | | |
| 61 | +| --- | --- | | |
| 62 | +| The organization's base permission | `none`, `read`, `write` or `admin` | | |
| 63 | +| A collaborator role on that repository (**Settings → Collaborators**, an accepted repository invitation, or `PUT /api/v3/repos/{owner}/{repo}/collaborators/{username}`) | The role they were given | | |
| 64 | +| They created the repository | `admin` | | |
| 65 | +| The repository is public | `read` | | |
| 66 | + | |
| 67 | +A collaborator role can raise a member above the base permission but can't lower it, so to give most members less access and a few people more, set a low base permission and add those people as collaborators on the repositories they need. Diffinery has no teams. | |
| 68 | + | |
| 69 | +Any member can create repositories in the organization, whatever the base permission. As on GitHub, the member who creates a repository becomes its admin: Diffinery adds them as an `admin` collaborator, so they appear under the repository's **Settings → Collaborators**. When a member's personal agent creates the repository, the member gets that grant. Owners and admins already have `admin` everywhere, so no grant is added for them. | |
| 70 | + | |
| 71 | +The base permission also limits agents that belong to a member, because a personal agent never has more access than its owner. Organization agents aren't affected: their access comes from their **Maximum role** (see [Agents](/docs/agents/overview)). Container images that aren't linked to a repository aren't affected: every member can still create images in the organization and pull them. An image linked to a repository follows that repository's access, so with `none` a member can't pull images of private repositories they can't see, and the registry catalog leaves those images out (see [Container registry](/docs/packages/container-registry)). | |
| 72 | + | |
| 73 | +Roles are worked out on every request, so a change to the base permission applies to every member right away, including in Git and through tokens they already have. | |
| 74 | + | |
| 75 | +### Change the base permission in the web UI | |
| 76 | + | |
| 77 | +Organization owners and site administrators: | |
| 78 | + | |
| 79 | +1. Open the organization's page and click the **Settings** tab (`https://diffinery.com/acme?tab=settings`). | |
| 80 | +2. In the **Base permission** menu, pick **No permission**, **Read**, **Write** or **Admin**. The hint under it reads "What every member can do in all of this organization's repositories. Owners and admins always have admin access, and repository settings can give people more." | |
| 81 | +3. Click **Save**. You see "Organization updated." | |
| 82 | + | |
| 83 | +Admins and members can't open the **Settings** tab: they get `403 organization owners only`. The change is recorded in the audit log as `org.update`, with the new value as `base_role` (see [Audit log](/docs/security/audit-log)). | |
| 84 | + | |
| 85 | +### Change the base permission with the API | |
| 86 | + | |
| 87 | +The API uses GitHub's field name, `default_repository_permission`. `GET /api/v3/orgs/{org}` returns the organization; members of the organization, their agents and site administrators also see `default_repository_permission` in it. Agents can also read it with the MCP tool `get_organization`. Any signed-in account can read the organization, but other accounts don't get that field. | |
| 88 | + | |
| 89 | +```sh | |
| 90 | +curl https://diffinery.com/api/v3/orgs/acme \ | |
| 91 | + -H "Authorization: Bearer dfy_..." | |
| 92 | +``` | |
| 93 | + | |
| 94 | +```json | |
| 95 | +{ | |
| 96 | + "login": "acme", | |
| 97 | + "type": "Organization", | |
| 98 | + "html_url": "https://diffinery.com/acme", | |
| 99 | + "avatar_url": "https://diffinery.com/api/avatars/acme", | |
| 100 | + "name": "Acme Inc.", | |
| 101 | + "default_repository_permission": "read" | |
| 102 | +} | |
| 103 | +``` | |
| 104 | + | |
| 105 | +(Other fields of GitHub's user object, such as `id`, `node_id` and `url`, are left out here.) | |
| 106 | + | |
| 107 | +`PATCH /api/v3/orgs/{org}` changes it. Only owners and site administrators can call it, with a personal access token that has the `admin` scope and isn't restricted to repositories (or a browser session). The body takes: | |
| 108 | + | |
| 109 | +| Field | Type | Description | | |
| 110 | +| --- | --- | --- | | |
| 111 | +| `default_repository_permission` | string | `none`, `read`, `write` or `admin`. | | |
| 112 | +| `name` | string | The display name. An empty value clears it, and pages show the organization name instead. | | |
| 113 | + | |
| 114 | +```sh | |
| 115 | +curl -X PATCH https://diffinery.com/api/v3/orgs/acme \ | |
| 116 | + -H "Authorization: Bearer dfy_..." \ | |
| 117 | + -H "Content-Type: application/json" \ | |
| 118 | + -d '{"default_repository_permission": "write"}' | |
| 119 | +``` | |
| 120 | + | |
| 121 | +The response is the updated organization, including `default_repository_permission`. With the CLI, `diffinery api PATCH /orgs/acme -f default_repository_permission=write` sends the same request (it needs an `admin` token). There is no dedicated CLI command. | |
| 122 | + | |
| 123 | +| Status | Message | Cause | | |
| 124 | +| --- | --- | --- | | |
| 125 | +| 403 | `this token needs the admin scope` | The token's scope is `read` or `write`. | | |
| 126 | +| 403 | `this token is limited to repositories and can't change account settings` | The token was created with a repository list. | | |
| 127 | +| 403 | `only organization owners can change its settings` | You are an admin or member of the organization, not an owner. | | |
| 128 | +| 404 | `not found` | The organization doesn't exist, or you aren't a member of it. | | |
| 129 | +| 422 | `base permission must be none, read, write or admin` | `default_repository_permission` has another value. | | |
| 130 | +| 422 | `name must be a string` | `name` is a number, object or other non-string value. | | |
| 131 | + | |
| 132 | +Every field is checked before anything is saved, so a refused value changes nothing. | |
| 133 | + | |
| 40 | 134 | ## Create an organization |
| 41 | 135 | |
| 42 | 136 | Any active account can create an organization and becomes its first owner. |
| @@ -79,10 +173,10 @@ | ||
| 79 | 173 | | **Repositories** | Everyone signed in | Repositories you can see. Members get a **New repository** button. | |
| 80 | 174 | | **People** | Everyone signed in | Members and their roles. Owners and admins also get the **Add people** form and the **Pending invitations** list. | |
| 81 | 175 | | **Agents** | Everyone signed in | The organization's agents, their maximum role and status. Owners and admins get a **Manage agents** button. | |
| 82 | -| **Settings** | Owners | The organization's picture, name and display name. Others get `403 organization owners only`. | | |
| 176 | +| **Settings** | Owners | The organization's picture, name, display name and [base permission](#base-permission). Others get `403 organization owners only`. | | |
| 83 | 177 | | **Packages** | Everyone signed in | Container images the organization owns (`/acme/-/packages`). See [Container registry](/docs/packages/container-registry). | |
| 84 | 178 | |
| 85 | -People who aren't members only see repositories that are public, or that they were added to directly. | |
| 179 | +Members see every repository of the organization, unless the base permission is `none`: then they see public repositories and the ones they were given access to or created. People who aren't members only see repositories that are public, or that they were added to directly. | |
| 86 | 180 | |
| 87 | 181 | ## Add and remove members |
| 88 | 182 | |
| @@ -115,7 +209,7 @@ | ||
| 115 | 209 | | "review-bot is not a person; agents belong to an organization through Settings → Agents" | Agents and organizations can't be members. Create an organization agent instead (see [Organization agents](#organization-agents)). | |
| 116 | 210 | | "diffy's account is disabled" | A site administrator disabled the account. | |
| 117 | 211 | | "diffy is already a member of acme" | Change their role in the members table instead. | |
| 118 | -| "friend@example.com already has a pending invitation; revoke it first to send a new one" | Revoke the invitation under **Pending invitations**, then add the address again. | | |
| 212 | +| "friend@example.com already has a pending invitation; revoke it first to send a new one" | The address already has an open invitation to this organization. Letter case doesn't matter: `Friend@Example.com` counts as the same address. Revoke the invitation under **Pending invitations**, then add the address again. | | |
| 119 | 213 | | "only owners can add owners" | Admins can add members and admins only. | |
| 120 | 214 | | "organization admins only" | Only owners, admins and site administrators can add people. | |
| 121 | 215 | |
| @@ -154,7 +248,7 @@ | ||
| 154 | 248 | - **CLI:** `diffinery repo create website --org acme` (add `--public` for a public repository; the default is private). |
| 155 | 249 | - **API:** `POST /api/v3/orgs/acme/repos` with `{"name": "website", "visibility": "private"}`. The CLI and API need a token with the `write` or `admin` scope that isn't restricted to repositories; a `read` token gets `403 this token needs the write scope`. |
| 156 | 250 | |
| 157 | -Access to organization repositories comes from the organization role (members get write, owners and admins get admin), plus any collaborators added under the repository's **Settings → Collaborators**, which can include people outside the organization and agents. When a repository is created, the organization's other owners and admins get an email, and when one is deleted they are told who deleted it. | |
| 251 | +Access to organization repositories comes from the organization role (members get the [base permission](#base-permission), owners and admins get admin), plus any collaborators added under the repository's **Settings → Collaborators**, which can include people outside the organization and agents. A member who creates a repository is added to it as an `admin` collaborator, whatever the base permission. When a repository is created, the organization's other owners and admins get an email, and when one is deleted they are told who deleted it. | |
| 158 | 252 | |
| 159 | 253 | ## Organization secrets |
| 160 | 254 | |
| @@ -179,8 +273,31 @@ | ||
| 179 | 273 | | `PUT /api/v3/orgs/{org}/actions/secrets/{name}` | Create or update; body `{"value": "..."}`; returns `204` | |
| 180 | 274 | | `DELETE /api/v3/orgs/{org}/actions/secrets/{name}` | Delete; returns `204` | |
| 181 | 275 | |
| 182 | -Names are stored in upper case and must match `^[A-Z_][A-Z0-9_]*$` after upper-casing, otherwise the API returns `422 invalid secret name`. Values are encrypted at rest. See [Secrets and variables](/docs/actions/secrets-and-variables) for how workflows use them. | |
| 276 | +Names are stored in upper case and must match `^[A-Z_][A-Z0-9_]*$` after upper-casing, otherwise the API returns `422 invalid secret name`. Values are encrypted at rest. Deleting a secret that doesn't exist returns `404 secret not found` and isn't recorded. Each change is recorded in the [audit log](/docs/security/audit-log) as `org.secret.set` or `org.secret.delete`, with the organization as the target and the secret's name (never its value) in the detail. See [Secrets and variables](/docs/actions/secrets-and-variables) for how workflows use them. | |
| 183 | 277 | |
| 278 | +## Organization variables | |
| 279 | + | |
| 280 | +Organization Actions variables are available to workflows in every repository the organization owns, as `${{ vars.NAME }}`, including repositories created later. If a repository has a variable with the same name, the repository's value wins, and an environment variable wins over both. Unlike secrets, variable values are stored and returned in plain text. | |
| 281 | + | |
| 282 | +Owners and admins (and site administrators) manage them with the REST API; there is no page for them in the web UI and no CLI command. Agents can't read or manage them, whatever their role (`403 organization admins only`, or `404 not found` when neither the agent nor its owner belongs to the organization). Members who aren't owners or admins get `403 organization admins only`. Reading needs a token of any scope; creating, changing and deleting need a personal access token with the `admin` scope that isn't restricted to repositories. | |
| 283 | + | |
| 284 | +| Method and path | What it does | | |
| 285 | +| --- | --- | | |
| 286 | +| `GET /api/v3/orgs/{org}/actions/variables` | List variables with their values, sorted by name (not paged) | | |
| 287 | +| `GET /api/v3/orgs/{org}/actions/variables/{name}` | One variable, or `404 variable not found` | | |
| 288 | +| `POST /api/v3/orgs/{org}/actions/variables` | Create or replace; body `{"name": "...", "value": "..."}`; returns `201` | | |
| 289 | +| `PATCH /api/v3/orgs/{org}/actions/variables/{name}` | Set the value (creating the variable if needed); body `{"value": "..."}`; returns `204` | | |
| 290 | +| `DELETE /api/v3/orgs/{org}/actions/variables/{name}` | Delete; returns `204` | | |
| 291 | + | |
| 292 | +```sh | |
| 293 | +curl -X POST https://diffinery.com/api/v3/orgs/acme/actions/variables \ | |
| 294 | + -H "Authorization: Bearer dfy_..." \ | |
| 295 | + -H "Content-Type: application/json" \ | |
| 296 | + -d '{"name": "NPM_REGISTRY", "value": "https://registry.npmjs.org"}' | |
| 297 | +``` | |
| 298 | + | |
| 299 | +Names must match `^[A-Za-z_][A-Za-z0-9_]*$`, can't start with `GITHUB_` (`422 invalid variable name`) and are stored in upper case. Values are strings of at most 48 KB (`422 value must be a string of at most 48 KB`). Every variable applies to all of the organization's repositories (`"visibility": "all"`); there is no per-repository selection. Each change is recorded in the [audit log](/docs/security/audit-log) as `org.variable_set` or `org.variable_delete`, with the organization as the target and the variable's name in the detail. A [GitHub organization import](/docs/migrate-from-github) adds the GitHub organization's variables that don't exist here yet. See [Secrets and variables](/docs/actions/secrets-and-variables#organization-variables) for the full details and the differences from GitHub. | |
| 300 | + | |
| 184 | 301 | ## Organization agents |
| 185 | 302 | |
| 186 | 303 | An agent can be owned by an organization instead of a person, so it keeps working when people come and go. Owners and admins create one under **Settings → Agents** by choosing the organization as **Owner**, or with `diffinery agent create review-bot --org acme`. An organization agent can only reach that organization's repositories, and its security emails (new tokens, expiring tokens) go to the organization's owners and admins. See [Agents](/docs/agents/overview). |
| @@ -210,20 +327,66 @@ | ||
| 210 | 327 | |
| 211 | 328 | ## Rename an organization |
| 212 | 329 | |
| 213 | -Owners open the **Settings** tab, change **Organization name**, **Display name** or **Base permission** and click **Save**. You see "Organization updated." Every field is checked before anything is saved, so a value that's refused changes nothing. Renaming changes every repository URL in the organization, so existing clones need a new remote URL: | |
| 330 | +Owners open the **Settings** tab, change **Organization name**, **Display name** or **Base permission** and click **Save**. You see "Organization updated." Every field is checked before anything is saved, so a value that's refused changes nothing. If you clear **Display name**, pages show the organization name instead, and the API returns `"name": null`. The display name can also be changed with `PATCH /api/v3/orgs/{org}` and `{"name": "Acme Inc."}` (see [Change the base permission with the API](#change-the-base-permission-with-the-api)); the organization name can't. Renaming changes every repository URL in the organization, so existing clones need a new remote URL: | |
| 214 | 331 | |
| 215 | 332 | ```sh |
| 216 | 333 | git remote set-url origin https://diffinery.com/acme-inc/website.git |
| 217 | 334 | ``` |
| 218 | 335 | |
| 219 | -URLs with the old name keep working, like those of a [transferred repository](/docs/repositories/overview#transfer-a-repository): web pages and the API redirect, and Git and container images are served under the old name, until another person or organization takes it. | |
| 336 | +URLs with the old name keep working, like those of a [transferred repository](/docs/repositories/overview#old-urls-after-a-transfer-or-rename): web pages and the API redirect, and Git and container images are served under the old name, until another person or organization takes it. | |
| 220 | 337 | |
| 221 | 338 | ## Delete an organization |
| 222 | 339 | |
| 223 | -Owners open the **Settings** tab, scroll to **Danger zone → Delete this organization**, type the organization's name and click **Delete this organization**; with the API, `DELETE /api/v3/orgs/{org}` returns `202`. An organization that still owns repositories can't be deleted: transfer them (see [Transfer a repository](/docs/repositories/overview#transfer-a-repository)) or delete them first, or the request fails with `acme still owns 3 repositories; transfer or delete them before deleting the organization` (HTTP 422). Container images and an import in progress block it the same way. | |
| 340 | +Deleting an organization removes it for good. It is allowed only once the organization owns nothing that people still use: no repositories and no container images, and no GitHub import running into it. Unlike GitHub, deleting an organization never deletes its repositories along with it: move them out first. | |
| 224 | 341 | |
| 225 | -Deleting removes the organization's members, pending invitations, secrets and variables, disables its agents and revokes their tokens, and frees its name. Repositories transferred out of it keep redirecting from their old URLs. The deletion is recorded in the audit log as `org.delete`. | |
| 342 | +### Before you delete | |
| 226 | 343 | |
| 344 | +- **Who:** organization owners (and site administrators). Admins and members can't delete it. | |
| 345 | +- **Repositories:** [transfer](/docs/repositories/overview#transfer-a-repository) each repository to another owner, or delete it. Transferred repositories keep their old URLs working after the organization is gone. | |
| 346 | +- **Container images:** delete the organization's images (images linked to a repository move with it when you transfer the repository). See [Container registry](/docs/packages/container-registry). | |
| 347 | +- **Imports:** wait for an import into the organization to finish, or cancel it, including drafts and queued imports. | |
| 348 | + | |
| 349 | +### Delete in the web UI | |
| 350 | + | |
| 351 | +1. Open the organization's page and its **Settings** tab (owners only). | |
| 352 | +2. In the **Danger zone**, under **Delete this organization**, type the organization's login (for example `acme`) in the confirmation box. | |
| 353 | +3. Click **Delete this organization**. | |
| 354 | + | |
| 355 | +You see `Deleted acme.` and land on your home page. | |
| 356 | + | |
| 357 | +### Delete with the API | |
| 358 | + | |
| 359 | +```sh | |
| 360 | +curl -X DELETE https://diffinery.com/api/v3/orgs/acme \ | |
| 361 | + -H "Authorization: Bearer dfy_..." | |
| 362 | +``` | |
| 363 | + | |
| 364 | +It returns `202 Accepted` with an empty object (`{}`) once the organization is deleted. It needs a token with the `admin` scope that isn't limited to repositories. | |
| 365 | + | |
| 366 | +### What deleting does | |
| 367 | + | |
| 368 | +- Its members, pending invitations, organization secrets and organization variables are removed. | |
| 369 | +- Its agents are disabled, and their tokens revoked. | |
| 370 | +- Its login is freed: anyone can create a user or organization with that name afterwards. Older logins from earlier renames stop redirecting. | |
| 371 | +- Its page and API answer `404`. | |
| 372 | +- The audit log keeps the entries that name it, and records the deletion as `org.delete`. Its import history and agents still refer to it. | |
| 373 | + | |
| 374 | +Deleting can't be undone. To use the name again, create a new organization. | |
| 375 | + | |
| 376 | +### Errors when deleting | |
| 377 | + | |
| 378 | +| Message | Cause and fix | | |
| 379 | +| --- | --- | | |
| 380 | +| `acme still owns 3 repositories; transfer or delete them before deleting the organization` (422) | Transfer or delete the repositories first. | | |
| 381 | +| `acme still owns 2 container images; delete them before deleting the organization` (422) | Delete the images first. | | |
| 382 | +| `an import into acme is still in progress; wait for it to finish or cancel it first` (422) | Wait for the import, or cancel it. | | |
| 383 | +| `only organization owners can delete it` (403) | You are an admin or member. Ask an owner. | | |
| 384 | +| `not found` (404) | The organization doesn't exist, or you aren't a member of it. | | |
| 385 | +| `this token needs the admin scope` (403) | Use a token with the `admin` scope. | | |
| 386 | +| `this token is limited to repositories and can't change account settings` (403) | Use a token that isn't restricted to repositories. | | |
| 387 | +| `type acme to confirm deletion` | The web confirmation box didn't match the organization's login. | | |
| 388 | +| `organization owners only` | Only owners can delete the organization from the web UI. | | |
| 389 | + | |
| 227 | 390 | ## Organizations API |
| 228 | 391 | |
| 229 | 392 | | Method and path | Description | |
| @@ -232,7 +395,7 @@ | ||
| 232 | 395 | | `POST /api/v3/user/orgs` | Create an organization: `{"login": "acme", "name": "Acme Inc."}`. Needs an `admin` token. | |
| 233 | 396 | | `GET /api/v3/orgs/{org}` | The organization; members, their agents and site administrators also get `default_repository_permission` | |
| 234 | 397 | | `PATCH /api/v3/orgs/{org}` | Change `name` or `default_repository_permission` (`none`, `read`, `write` or `admin`). Owners only; needs an `admin` token. | |
| 235 | -| `DELETE /api/v3/orgs/{org}` | Delete the organization once it owns no repositories; returns `202`. Owners only; needs an `admin` token. | | |
| 398 | +| `DELETE /api/v3/orgs/{org}` | Delete the organization once it owns no repositories, container images or running imports; returns `202`. Owners only; needs an `admin` token. See [Delete an organization](#delete-an-organization). | | |
| 236 | 399 | | `GET /api/v3/orgs/{org}/members` | Members and their roles. Members and site administrators only; others get `404`. | |
| 237 | 400 | | `POST /api/v3/orgs/{org}/invitations` | Invite by email: `{"email": "...", "role": "member"}`. Needs an `admin` token. See [Invitations](/docs/accounts/invitations#invite-with-the-api). | |
| 238 | 401 | | `GET /api/v3/orgs/{org}/invitations` | Pending invitations, each with an `id`. Owners and admins. | |
| @@ -240,6 +403,7 @@ | ||
| 240 | 403 | | `GET /api/v3/orgs/{org}/repos` | Repositories you can see in the organization | |
| 241 | 404 | | `POST /api/v3/orgs/{org}/repos` | Create a repository in the organization. Needs a `write` or `admin` token. | |
| 242 | 405 | | `GET`, `PUT`, `DELETE /api/v3/orgs/{org}/actions/secrets[/{name}]` | Organization secrets. Needs an `admin` token. | |
| 406 | +| `GET`, `POST`, `PATCH`, `DELETE /api/v3/orgs/{org}/actions/variables[/{name}]` | Organization variables. Owners and admins; changes need an `admin` token. | | |
| 243 | 407 | | `PUT /api/v3/orgs/{org}/avatar` | Upload the organization's picture (raw image body). Owners only. | |
| 244 | 408 | | `DELETE /api/v3/orgs/{org}/avatar` | Remove the organization's picture. Owners only. | |
| 245 | 409 | |
| @@ -251,9 +415,12 @@ | ||
| 251 | 415 | |
| 252 | 416 | ## How organizations differ from GitHub |
| 253 | 417 | |
| 254 | -- Diffinery has three organization roles: owner, admin and member. Admins manage members, invitations, agents and secrets but can't manage owners or open the organization's **Settings** tab. | |
| 255 | -- The base permission works like GitHub's (none, read, write or admin) and starts at read; organizations created before Diffinery had it keep write. | |
| 418 | +- Diffinery has three organization roles: owner, admin and member. Admins manage members, invitations, agents, secrets and variables but can't manage owners, open the organization's **Settings** tab or change the organization's picture. | |
| 419 | +- The base permission works like GitHub's, with the same four values (`none`, `read`, `write`, `admin`) and the same default for new organizations (`read`). Organizations created before Diffinery had the setting use `write`. | |
| 420 | +- A member who creates a repository gets `admin` on it, as on GitHub. | |
| 256 | 421 | - There are no teams. Give extra access per repository under **Settings → Collaborators**. |
| 257 | 422 | - **Add people** works like GitHub's: enter a username or an email address. Unlike GitHub, someone who already has an account joins immediately, with no invitation to accept; only email addresses without an account get an invitation. |
| 258 | 423 | - Organization pages need you to be signed in. |
| 259 | -- Organization secrets apply to every repository in the organization; there is no per-repository selection. | |
| 424 | +- Organization secrets and variables apply to every repository in the organization; there is no per-repository selection. | |
| 425 | +- Deleting an organization is refused while it owns repositories or container images; GitHub deletes them with it. Transfer or delete them first. | |
| 426 | +- Repositories can be transferred only to your own account or to an organization you own or administer, so a transfer needs no acceptance by the new owner. |
modifiedapps/diffinery/docs/accounts/permissions.md+43−19View file
| @@ -30,7 +30,7 @@ | ||
| 30 | 30 | | See published releases, download release assets and source archives (zip and tar) | Yes | Yes | Yes | Yes | Yes | |
| 31 | 31 | | See commit statuses, check runs, deployments and environments | Yes | Yes | Yes | Yes | Yes | |
| 32 | 32 | | Watch the repository and subscribe to threads | Yes | Yes | Yes | Yes | Yes | |
| 33 | -| Create, apply and delete labels; set assignees | No | Yes | Yes | Yes | Yes | | |
| 33 | +| Create, apply and delete labels; set assignees (labels and assignees sent by someone below triage, when opening or editing an issue or pull request, are dropped) | No | Yes | Yes | Yes | Yes | | |
| 34 | 34 | | Edit, close or reopen anyone's issue or pull request | No | Yes | Yes | Yes | Yes | |
| 35 | 35 | | Request reviewers on anyone's pull request | No | Yes | Yes | Yes | Yes | |
| 36 | 36 | | Lock and unlock conversations; comment and react on locked ones | No | Yes | Yes | Yes | Yes | |
| @@ -38,7 +38,7 @@ | ||
| 38 | 38 | | Create, update and delete branches and refs through the API; commit files in the web editor or API | No | No | Yes | Yes | Yes | |
| 39 | 39 | | Open pull requests | No | No | Yes | Yes | Yes | |
| 40 | 40 | | Approve or request changes on a pull request (not your own) | No | No | Yes | Yes | Yes | |
| 41 | -| Merge pull requests, add them to or remove them from a merge queue | No | No | Yes | Yes | Yes | | |
| 41 | +| Merge pull requests, add them to or remove them from a merge queue, turn [auto-merge](/docs/collaboration/pull-requests#auto-merge) on or off | No | No | Yes | Yes | Yes | | |
| 42 | 42 | | Resolve review threads | No | No | Yes | Yes | Yes | |
| 43 | 43 | | Run, cancel and re-run workflows | No | No | Yes | Yes | Yes | |
| 44 | 44 | | Report check runs and commit statuses (`POST /repos/{owner}/{repo}/check-runs`, `POST /repos/{owner}/{repo}/statuses/{sha}`) | No | No | Yes | Yes | Yes | |
| @@ -54,23 +54,25 @@ | ||
| 54 | 54 | | Dismiss a review on someone else's pull request (people only) | No | No | No | Yes | Yes | |
| 55 | 55 | | Change settings: name, description, visibility, default branch | No | No | No | No | Yes | |
| 56 | 56 | | Archive, unarchive and delete the repository | No | No | No | No | Yes | |
| 57 | +| Transfer the repository to your own account or an organization you own or administer (people only; a personal repository only by its owner) | No | No | No | No | Yes | | |
| 57 | 58 | | Add, change and remove collaborators; invite by email | No | No | No | No | Yes | |
| 58 | 59 | | Manage branch protection rules | No | No | No | No | Yes | |
| 59 | 60 | | List, set and delete Actions secrets; set and delete variables | No | No | No | No | Yes | |
| 60 | 61 | | Create, change and delete environments, their deployment branch policies and secrets (people only) | No | No | No | No | Yes | |
| 61 | 62 | | Set and delete environment variables | No | No | No | No | Yes | |
| 62 | 63 | | Set repository topics | No | No | No | No | Yes | |
| 64 | +| Change the repository's workflow permissions setting: the default `GITHUB_TOKEN` permissions and whether workflows may create and approve pull requests (people only) | No | No | No | No | Yes | | |
| 63 | 65 | | Delete other people's reactions | No | No | No | No | Yes | |
| 64 | 66 | | Manage webhooks | No | No | No | No | Yes | |
| 65 | 67 | | Run self-hosted runners for the repository (`diffinery runner`) | No | No | No | No | Yes | |
| 66 | -| Bypass branch protection: push past it, or merge past unmet rules by asking to (`"bypass_rules": true`), unless the rule turns on **Do not allow administrators to bypass these rules** (people only for merges) | No | No | No | No | Yes | | |
| 67 | -| Push past push protection with `git push -o allow-secrets=<reason>` (people only) | No | No | No | No | Yes | | |
| 68 | +| Bypass branch protection: push past it, merge past unmet rules by asking to (`"bypass_rules": true`), or merge without the merge queue by asking to (`"bypass_merge_queue": true`), unless the rule turns on **Do not allow administrators to bypass these rules** (people only for merges) | No | No | No | No | Yes | | |
| 69 | +| Commit past push protection with `git push -o allow-secrets=<reason>`, or with `allow_secrets` on `POST /repos/{owner}/{repo}/diffinery/commits` (people only) | No | No | No | No | Yes | | |
| 68 | 70 | |
| 69 | 71 | Some actions have extra conditions on top of the role: |
| 70 | 72 | |
| 71 | -- Branch protection can block pushes and merges for every role below admin, and for admins too when the rule enforces administrators. An admin's merge only skips unmet rules when they ask for it explicitly (**Merge without waiting for requirements (bypass)** in the web UI, `"bypass_rules": true` in the API, `diffinery pr merge --admin`), and the bypass is written to the audit log as `pull_request.merge_bypassed`. See [Branch protection](/docs/repositories/branch-protection). | |
| 73 | +- Branch protection can block pushes and merges for every role below admin, and for admins too when the rule enforces administrators. An admin's merge only skips unmet rules when they ask for it explicitly (**Merge without waiting for requirements (bypass)** in the web UI, `"bypass_rules": true` in the API, `diffinery pr merge --admin`). Each flag covers only what it names: `bypass_rules` skips unmet rules (including a merge commit on a branch that requires linear history), and `bypass_merge_queue` skips only the merge queue. Every bypass, including a bypassed linear-history rule or merge queue, is written to the audit log as `pull_request.merge_bypassed`. See [Branch protection](/docs/repositories/branch-protection). | |
| 72 | 74 | - The `read` role's actions that change something (opening issues, commenting, reacting, comment-only reviews, watching) need a signed-in account. Anonymous visitors to a public repository can only read, and a `read`-scoped token can only read; see [Anonymous access](#anonymous-access) and [Token scopes cap your role](#token-scopes-cap-your-role). |
| 73 | -- An archived repository refuses pushes and most changes with "repository is archived (read-only)", for every role. Unarchiving, the admin settings (collaborators, branch protection, webhooks, secrets and variables) and, for now, opening and editing issues, comments, reviews and review requests through the REST API and MCP still work. See [Archive a repository](/docs/repositories/overview#archive-a-repository). | |
| 75 | +- An archived repository refuses every write with `403 repository is archived (read-only)`, for every role and from the web UI, Git, the REST API and MCP alike: pushes, issues, comments, reviews, settings, collaborators, branch protection, secrets, variables and webhooks. Only unarchiving, deleting the repository, watching it, and linking a container image to it still work. See [Archive a repository](/docs/repositories/overview#archive-a-repository). | |
| 74 | 76 | - You can't approve or request changes on your own pull request, and you can't dismiss reviews on it. |
| 75 | 77 | - Pull requests opened by agents always need at least one approval from a person. |
| 76 | 78 | |
| @@ -84,12 +86,13 @@ | ||
| 84 | 86 | | --- | --- | |
| 85 | 87 | | You own the repository (a personal repository) | admin | |
| 86 | 88 | | You are an owner or admin of the organization that owns it | admin | |
| 87 | -| You are a member of the organization that owns it | the organization's base permission: `read` by default, or `none`, `write` or `admin` as its owners set it | | |
| 89 | +| You are a member of the organization that owns it | the organization's base permission: `none`, `read`, `write` or `admin` (`read` for new organizations) | | |
| 90 | +| You created it in an organization as a member | admin (you are added as an `admin` collaborator) | | |
| 88 | 91 | | You were added as a collaborator on the repository (**Settings → Collaborators**, an accepted repository invite, or `PUT /repos/{owner}/{repo}/collaborators/{username}`) | the role you were given | |
| 89 | 92 | | The repository is public | read | |
| 90 | 93 | | You are a site administrator | admin on every repository | |
| 91 | 94 | |
| 92 | -Because the highest role wins, a collaborator entry can raise an organization member's access but can't lower it. Accounts that are pending or disabled have no role anywhere. | |
| 95 | +Because the highest role wins, a collaborator entry can raise an organization member's access above the base permission but can't lower it. Accounts that are pending or disabled have no role anywhere. | |
| 93 | 96 | |
| 94 | 97 | ### Collaborators |
| 95 | 98 | |
| @@ -116,15 +119,15 @@ | ||
| 116 | 119 | |
| 117 | 120 | ## Organization roles |
| 118 | 121 | |
| 119 | -Organizations have their own three roles, which map onto repository roles for every repository the organization owns: | |
| 122 | +Organizations have their own three roles, which map onto repository roles for every repository the organization owns. Members get the organization's base permission, which owners choose: | |
| 120 | 123 | |
| 121 | 124 | | Organization role | Role on organization repositories | Can also | |
| 122 | 125 | | --- | --- | --- | |
| 123 | -| member | the base permission (`read` by default) | Create repositories (which they administer) and container images in the organization | | |
| 126 | +| member | The base permission: `none`, `read`, `write` or `admin` | Create repositories (and get `admin` on the ones they create) and container images in the organization | | |
| 124 | 127 | | admin | admin | Manage members (except owners), invites, agents and organization secrets | |
| 125 | -| owner | admin | Everything an admin can, plus manage owners and the organization's **Settings** tab (name, display name, picture) | | |
| 128 | +| owner | admin | Everything an admin can, plus manage owners and the organization's **Settings** tab (name, display name, base permission, picture) | | |
| 126 | 129 | |
| 127 | -See [Organizations](/docs/accounts/organizations) for details. | |
| 130 | +New organizations start with a base permission of `read`; organizations that existed before the setting was added use `write`. Owners change it on the organization's **Settings** tab (**Base permission**) or with `PATCH /api/v3/orgs/{org}` and `{"default_repository_permission": "write"}`. With `none`, members don't see the organization's private repositories unless they are collaborators on them or created them. See [Base permission](/docs/accounts/organizations#base-permission) for details. | |
| 128 | 131 | |
| 129 | 132 | ## Site administrators |
| 130 | 133 | |
| @@ -166,10 +169,11 @@ | ||
| 166 | 169 | | Create a repository (`POST /api/v3/user/repos`, `POST /api/v3/orgs/{org}/repos`) | a `write` or `admin` token that isn't restricted to repositories, or a browser session | |
| 167 | 170 | | Mark notifications read, change notification settings, send a test email | a `write` or `admin` token that isn't restricted to repositories, or a browser session | |
| 168 | 171 | | Watch or unwatch a repository | a `write` or `admin` token, or a browser session | |
| 169 | -| Upload or remove your profile picture or an organization's (`PUT`/`DELETE /api/v3/user/avatar`, `/api/v3/orgs/{org}/avatar`) | a `write` or `admin` token that isn't restricted to repositories, or a browser session | | |
| 172 | +| Upload or remove your profile picture or an organization's (`PUT`/`DELETE /api/v3/user/avatar`, `/api/v3/orgs/{org}/avatar`; organization owners only) | a `write` or `admin` token that isn't restricted to repositories, or a browser session | | |
| 170 | 173 | | Create a token (`POST /api/v3/diffinery/tokens`, `diffinery token create`) | an `admin` token or a browser session | |
| 171 | 174 | | Revoke a token, create an organization, invite people, manage organization secrets, create agents and agent tokens, disable agents | an `admin` token that isn't restricted to repositories, or a browser session | |
| 172 | 175 | | Send every email template (site administrators) | an `admin` token that isn't restricted to repositories, or a browser session | |
| 176 | +| Change an organization's display name or base permission (`PATCH /api/v3/orgs/{org}`, owners only) | an `admin` token that isn't restricted to repositories, or a browser session | | |
| 173 | 177 | | Change your username, display name or appearance | a browser session; there is no API | |
| 174 | 178 | | Add or remove passkeys, sign out other browsers | a browser session; tokens can't do this | |
| 175 | 179 | |
| @@ -193,7 +197,7 @@ | ||
| 193 | 197 | |
| 194 | 198 | - Agents can't push to the default branch or any protected branch, unless a protection rule turns on **Allow agents to push directly** (`agentsMayPush`). They get "agents may not push to protected or default branches; open a pull request". |
| 195 | 199 | - Agents can't merge into the default branch or a protected branch, unless the rule turns on **Allow agents to merge pull requests** (`agentsMayMerge`). |
| 196 | -- A pull request opened by an agent needs at least one approving review from a person, and approvals from agents never count toward required approvals. | |
| 200 | +- A pull request opened by an agent needs at least one approving review from a person. Only people's approvals count toward required approvals: approvals from agents and from workflow tokens never do. | |
| 197 | 201 | - Agents can never bypass push protection, and the merge-protection bypass for admins (`"bypass_rules": true`) applies only to people. |
| 198 | 202 | - Check runs and commit statuses reported by agents never satisfy a required status check. |
| 199 | 203 | - Agents can't dismiss reviews. |
| @@ -209,16 +213,36 @@ | ||
| 209 | 213 | |
| 210 | 214 | | Token | Prefix | Where it comes from | What it can do on the repository | |
| 211 | 215 | | --- | --- | --- | --- | |
| 212 | -| Job token | `djt_` | Each workflow job gets one as `GITHUB_TOKEN` (also `DIFFINERY_TOKEN`) | Act as `diffinery-actions[bot]` with `read` on the repository the job runs in. Container images follow the job's `permissions: packages`: it pulls its owner's images and pushes only to images linked to its own repository (creating them, linked, on first push) | | |
| 213 | -| Sandbox token | `dst_` | Each agent sandbox gets one | Act as the person or agent that started the sandbox, capped at `write`, on that repository only | | |
| 216 | +| Job token | `djt_` | Each workflow job gets one as `GITHUB_TOKEN` (also `DIFFINERY_TOKEN`) | Act as `diffinery-actions[bot]` with what the workflow's `permissions:` grant (see below). Container images follow the job's `permissions: packages`: it pulls its owner's images and pushes only to images linked to its own repository (creating them, linked, on first push) | | |
| 217 | +| Sandbox token | `dst_` | Each agent sandbox gets one | Act as the person or agent that started the sandbox, capped at `write`, on that repository | | |
| 214 | 218 | |
| 215 | -Both work only for the repository they were issued for, and stop working when the job completes or the sandbox closes. See [Secrets and variables](/docs/actions/secrets-and-variables) and [Sandboxes](/docs/agents/sandboxes). | |
| 219 | +Both can also read other public repositories (as anyone can), but no other private repository. They stop working when the job completes or the sandbox closes. See [Secrets and variables](/docs/actions/secrets-and-variables) and [Sandboxes](/docs/agents/sandboxes). | |
| 216 | 220 | |
| 221 | +### What a job token may do | |
| 222 | + | |
| 223 | +A job token follows GitHub's `permissions:` model. Its permissions come from the job's `permissions:`, else the workflow's, else the repository's default: | |
| 224 | + | |
| 225 | +| Repository default | Permissions without `permissions:` | | |
| 226 | +| --- | --- | | |
| 227 | +| Read (the default for new repositories) | `contents` and `packages` read, nothing else | | |
| 228 | +| Read and write (repositories created before the setting existed) | Write on every permission except `id-token`, which stays off | | |
| 229 | + | |
| 230 | +An admin changes the default, and whether workflows may create and approve pull requests (off by default), on the repository's **Settings → Actions** tab or with `PUT /api/v3/repos/{owner}/{repo}/actions/permissions/workflow`. See [Workflow permissions](/docs/actions/workflow-permissions) for the details. | |
| 231 | + | |
| 232 | +On top of its permissions: | |
| 233 | + | |
| 234 | +- Each endpoint needs its permission (`issues`, `pull-requests`, `contents`, `statuses`, `checks`, `deployments`, `actions` and so on). A request without it gets `403 Resource not accessible by integration (the workflow's GITHUB_TOKEN needs permissions: <scope>: <read or write>)`. Reads of a public repository are always allowed. | |
| 235 | +- The token's role is at most `write`, so branch protection always applies to it. Repository settings, collaborators, secrets, variables, webhooks, runners and branch protection are out of its reach (`403 this endpoint is not available to a workflow's GITHUB_TOKEN`). | |
| 236 | +- It never creates, changes or deletes files under `.github/workflows/` or `.diffinery/workflows/`, by push, API commit or merge. The refusal reads: refusing to allow a GitHub App to create or update workflow `<path>` without `workflows` permission. | |
| 237 | +- It can't open, approve or request changes on pull requests unless the repository allows it, and even then its approvals don't count toward required approvals. | |
| 238 | +- A reusable workflow never gets more than its caller. | |
| 239 | +- A run whose code came from an agent (an agent triggered it, pushed the branch last, or opened the pull request, and any re-run of such a run, whoever starts it, or `workflow_run` run that follows it) acts as an agent, so the agent guardrails apply: its token can't push or merge to the default branch or a protected branch. | |
| 240 | + | |
| 217 | 241 | ## How permissions differ from GitHub |
| 218 | 242 | |
| 219 | 243 | - The five repository roles match GitHub's names. Diffinery's `maintain` adds the ability to move or delete tags and to edit other people's comments; repository settings stay with `admin`. |
| 220 | 244 | - Opening a pull request needs `write`: its head branch must be a branch in the same repository. |
| 221 | -- Organization members get the organization's base permission on every organization repository (`read` unless its owners change it), and organization admins get `admin`. There are no teams or custom roles. | |
| 245 | +- Organization members get the organization's base permission on every organization repository, as on GitHub, with the same values (`none`, `read`, `write`, `admin`) and the same default for new organizations (`read`). Organization owners and admins get `admin`. There are no teams or custom roles. | |
| 222 | 246 | - Personal access tokens have three scopes (`read`, `write`, `admin`) instead of GitHub's fine-grained permissions; the scope caps your role. |
| 223 | -- `GITHUB_TOKEN` in a workflow can read the repository and use the container registry; it can't push commits or call write endpoints. | |
| 247 | +- `GITHUB_TOKEN` in a workflow follows GitHub's `permissions:` model, but new repositories default to read-only tokens, the token never administers the repository or edits workflow files, and code that came from an agent runs under the agent rules. | |
| 224 | 248 | - Agents are a separate account type with their own caps and guardrails. |
modifiedapps/diffinery/docs/accounts/profile-and-preferences.md+45−11View file
| @@ -1,9 +1,9 @@ | ||
| 1 | 1 | --- |
| 2 | 2 | title: Profile and preferences |
| 3 | -description: Change your username and display name, upload or remove a profile picture for yourself or an organization, and choose light, dark or system appearance. | |
| 3 | +description: Change your username, display name and profile picture, choose light, dark or system appearance, and link your GitHub account for imports. | |
| 4 | 4 | --- |
| 5 | 5 | |
| 6 | -Your profile is how other people see you on Diffinery: your username, display name and profile picture. This page also covers the **Appearance** setting (light, dark or following your system) and points to every other per-user setting under **Settings**. | |
| 6 | +Your profile is how other people see you on Diffinery: your username, display name and profile picture. This page also covers the **Appearance** setting (light, dark or following your system), **Linked accounts** (your GitHub account, for imports) and points to every other per-user setting under **Settings**. | |
| 7 | 7 | |
| 8 | 8 | ## Where your settings are |
| 9 | 9 | |
| @@ -18,6 +18,8 @@ | ||
| 18 | 18 | | **Agents** | `/settings/agents` | Agent accounts you or your organizations own. See [Agents](/docs/agents/overview). | |
| 19 | 19 | | **Organizations** | `/settings/orgs` | Organizations you belong to, and creating new ones. See [Organizations](/docs/accounts/organizations). | |
| 20 | 20 | | **Notifications** | `/settings/notifications` | What you hear about in the notifications list and by email. See [Notifications](/docs/accounts/notifications). | |
| 21 | +| **Linked accounts** | `/settings/linked-accounts` | GitHub accounts linked to yours, so imported work is credited to you. See [Linked accounts](#linked-accounts). | | |
| 22 | +| **Imports** | `/imports` | The GitHub imports you started, and **Import from GitHub**. See [Migrate from GitHub](/docs/migrate-from-github). | | |
| 21 | 23 | |
| 22 | 24 | Site administrators also see a **Site administration** group with **People and invitations**, **Email** and **Audit log** (see [Site administration](/docs/admin/site-administration)). |
| 23 | 25 | |
| @@ -30,7 +32,7 @@ | ||
| 30 | 32 | | Field | Editable | Notes | |
| 31 | 33 | | --- | --- | --- | |
| 32 | 34 | | **Username** | Yes | Your login, the owner part of your repository URLs (`https://diffinery.com/diffy/...`) and your `@mention` handle | |
| 33 | -| **Display name** | Yes | Optional. Shown on your profile page and in the settings sidebar, with your username under it, and returned as `name` by the API. | | |
| 35 | +| **Display name** | Yes | Optional. Shown on your profile page and in the settings sidebar, with your username under it, and returned as `name` by the API. If you leave it blank (or enter only spaces), Diffinery shows your username in its place and the API returns `"name": null`. | | |
| 34 | 36 | | **Email** | No | Your account's email address. Notification emails go here. It can't be changed in Settings. | |
| 35 | 37 | | **Account type** | No | `user` for a person | |
| 36 | 38 | | **Joined** | No | When the account was created | |
| @@ -42,19 +44,19 @@ | ||
| 42 | 44 | |
| 43 | 45 | ### Change your username |
| 44 | 46 | |
| 45 | -A username starts with a letter or digit, then uses letters, digits, `-` and `_`. Dots aren't allowed, and the name can't be reserved (for example `api`, `settings`, `login`, `admin`, `docs`) or already used by a person, organization or agent. | |
| 47 | +A username starts with a letter or digit, then uses letters, digits, `-` and `_`, up to 100 characters. Dots aren't allowed, and the name can't be reserved (for example `api`, `settings`, `login`, `admin`, `docs`) or already used by a person, organization or agent. The **Username** field checks the characters in your browser before you save, with the same rules as the server; the organization and agent name fields work the same way. | |
| 46 | 48 | |
| 47 | -Renaming changes the URL of every repository you own. The old URLs keep working (see [Transfer a repository](/docs/repositories/overview#transfer-a-repository) for how redirects behave), but update existing clones: | |
| 49 | +Renaming changes the URL of every repository you own. The old URLs keep working (see [Old URLs after a transfer or rename](/docs/repositories/overview#old-urls-after-a-transfer-or-rename) for how each kind behaves), but update existing clones: | |
| 48 | 50 | |
| 49 | 51 | ```sh |
| 50 | 52 | git remote set-url origin https://diffinery.com/new-name/website.git |
| 51 | 53 | ``` |
| 52 | 54 | |
| 53 | -After a rename, `https://diffinery.com/old-name` and your repositories' old web, API and Git URLs redirect to the new name, until someone else takes the old name, which is free for them to take. Your picture moves with you: Diffinery forgets cached copies of the picture under the old name. | |
| 55 | +After a rename, `https://diffinery.com/old-name` and your repositories' old web and API URLs redirect to the new name, and Git and container images keep being served under the old name, until someone else takes the old name, which is free for them to take. Your picture moves with you: Diffinery forgets cached copies of the picture under the old name. | |
| 54 | 56 | |
| 55 | 57 | | Message | What to do | |
| 56 | 58 | | --- | --- | |
| 57 | -| "invalid or reserved login" | The name breaks the rules above (for example it contains a dot) or is reserved. Pick another. | | |
| 59 | +| "invalid or reserved login" | The name is reserved, or breaks the rules above. Pick another. | | |
| 58 | 60 | | "login already taken" | Another person, organization or agent uses it. Pick another. | |
| 59 | 61 | |
| 60 | 62 | There is no API for changing your username or display name; use the web UI. `GET /api/v3/user` returns them as `login` and `name`. |
| @@ -113,7 +115,7 @@ | ||
| 113 | 115 | 2. Under **Upload a new picture**, choose a file and click **Save picture**. You see "Organization picture updated." |
| 114 | 116 | 3. To go back to initials, click **Remove picture** and confirm "Remove the picture of acme?". You see "Organization picture removed." |
| 115 | 117 | |
| 116 | -Through the API, organization owners and admins (and site administrators) can change the picture; see the next section. | |
| 118 | +Through the API, organization owners (and site administrators) can change the picture; see the next section. Organization admins and members can't change it. | |
| 117 | 119 | |
| 118 | 120 | ### Agent pictures |
| 119 | 121 | |
| @@ -127,8 +129,8 @@ | ||
| 127 | 129 | | --- | --- | --- | |
| 128 | 130 | | `PUT /api/v3/user/avatar` | You (the token's account) | `200` with `{"avatar_url": "..."}` | |
| 129 | 131 | | `DELETE /api/v3/user/avatar` | You | `200` with `{"avatar_url": "..."}` (the initials URL) | |
| 130 | -| `PUT /api/v3/orgs/{org}/avatar` | Owners and admins of the organization, site administrators | `200` with `{"avatar_url": "..."}` | | |
| 131 | -| `DELETE /api/v3/orgs/{org}/avatar` | Owners and admins of the organization, site administrators | `200` with `{"avatar_url": "..."}` | | |
| 132 | +| `PUT /api/v3/orgs/{org}/avatar` | Owners of the organization, site administrators | `200` with `{"avatar_url": "..."}` | | |
| 133 | +| `DELETE /api/v3/orgs/{org}/avatar` | Owners of the organization, site administrators | `200` with `{"avatar_url": "..."}` | | |
| 132 | 134 | |
| 133 | 135 | The token needs the `write` or `admin` scope and must not be restricted to repositories. A browser session works too. |
| 134 | 136 | |
| @@ -170,7 +172,7 @@ | ||
| 170 | 172 | | --- | --- | --- | |
| 171 | 173 | | 401 | `sign in required` | No token was sent | |
| 172 | 174 | | 403 | `changing a profile picture needs a write or admin token that is not limited to repositories` | The token has only the `read` scope, or is restricted to repositories | |
| 173 | -| 403 | `only organization owners and admins can change its picture` | You are a member of the organization, but not an owner or admin | | |
| 175 | +| 403 | `only organization owners can change its picture` | You are an admin or member of the organization, but not an owner | | |
| 174 | 176 | | 404 | `not found` | The organization doesn't exist, or you aren't a member | |
| 175 | 177 | | 413 | `That picture is larger than 5 MB. Use a smaller file.` | The body is over 5 MB | |
| 176 | 178 | | 422 | `Send the picture as the request body.` | The request had no body | |
| @@ -240,6 +242,38 @@ | ||
| 240 | 242 | |
| 241 | 243 | There is no API or CLI command for the appearance setting. |
| 242 | 244 | |
| 245 | +## Linked accounts | |
| 246 | + | |
| 247 | +**Settings → Linked accounts** (`/settings/linked-accounts`) is the **Linked GitHub accounts** page. When someone [imports from GitHub](/docs/migrate-from-github), issues, pull requests, comments and reviews written by a linked GitHub account are credited to your Diffinery account instead of a placeholder ("mannequin") for the GitHub user. Link your account before the import runs: work that was already imported stays with the mannequin. | |
| 248 | + | |
| 249 | +### Link with a public gist | |
| 250 | + | |
| 251 | +No GitHub token is needed: | |
| 252 | + | |
| 253 | +1. Under **Step 1 · get a code**, leave **GitHub Enterprise API URL** blank for github.com, or enter your GitHub Enterprise API URL (for example `https://ghe.example.com/api/v3`), and click **Get my code**. | |
| 254 | +2. Copy the code (it starts with `diffinery-link-`). On GitHub, create a **public** gist whose description or one of whose files contains it. | |
| 255 | +3. Under **Step 2 · verify your gist**, enter **Your GitHub username on github.com** (or on your Enterprise host) and click **Verify and link**. You see "GitHub account linked." | |
| 256 | + | |
| 257 | +Diffinery reads your public profile and gists without a token, from that one GitHub host, and stores GitHub's numeric user id with the link, so a later rename of your GitHub account doesn't break it. You can delete the gist afterwards. | |
| 258 | + | |
| 259 | +### Link with GitHub OAuth | |
| 260 | + | |
| 261 | +If the site administrator configured a GitHub OAuth app, the page also has **Link with GitHub**: click **Continue to GitHub**, approve read-only access to your profile, and you come back with the account linked. OAuth links github.com accounts only; use a gist for GitHub Enterprise. | |
| 262 | + | |
| 263 | +### Manage your links | |
| 264 | + | |
| 265 | +- Each link shows the GitHub login, the host (`on github.com`) and when it was linked. Click **Unlink** and confirm "Unlink diffy?" to remove it; you see "Account unlinked." | |
| 266 | +- You can link several GitHub accounts (for example one on github.com and one on an Enterprise host). A GitHub account can be linked to only one Diffinery account. | |
| 267 | +- Only people can link accounts; agents can't. | |
| 268 | +- Linking, unlinking and a site administrator clearing a link are recorded in the audit log (`account.link_github`, `account.unlink_github`, `account.link_cleared`). | |
| 269 | +- The same actions are available in the REST API under `/api/v3/diffinery/user/linked-accounts` with a personal token that has the `admin` scope and isn't restricted to repositories. See [Link your GitHub account](/docs/migrate-from-github#link-your-github-account). | |
| 270 | + | |
| 271 | +| Message | What to do | | |
| 272 | +| --- | --- | | |
| 273 | +| "create a PUBLIC gist whose description or a file contains diffinery-link-..., then try again" | Make sure the gist is public (not secret) and contains the exact code, then click **Verify and link** again. | | |
| 274 | +| "GitHub user diffy was not found" | Check the username and, for Enterprise, the API URL. | | |
| 275 | +| "GitHub user diffy is linked to another Diffinery account" | Unlink it from that account first, or ask a site administrator to clear the link. | | |
| 276 | + | |
| 243 | 277 | ## Notification preferences |
| 244 | 278 | |
| 245 | 279 | Your notification preferences (which groups you hear about in the notifications list and by email, watching new repositories, emails about your own activity) are on **Settings → Notifications**, and can also be read and changed with `GET` and `PUT /api/v3/diffinery/notification-settings`. See [Notifications](/docs/accounts/notifications#notification-settings). |
modifiedapps/diffinery/docs/accounts/tokens.md+15−10View file
| @@ -27,7 +27,7 @@ | ||
| 27 | 27 | | --- | --- | --- | |
| 28 | 28 | | `read` | read | Clone and fetch, download Git LFS objects, read the API, CI that only downloads | |
| 29 | 29 | | `write` | maintain | Push, issues, pull requests, merging, running workflows; the right choice for most Git and CLI use | |
| 30 | -| `admin` | admin | Everything you can do, including repository settings, collaborators, branch protection, secrets, webhooks, self-hosted runners and creating more tokens | | |
| 30 | +| `admin` | admin | Everything you can do, including repository settings, collaborators, branch protection, secrets, webhooks, self-hosted runners and revoking tokens. No token can create tokens or agents | | |
| 31 | 31 | |
| 32 | 32 | The web form describes them as: "read: clone and view · write: push, issues, pull requests, runs (up to maintain) · admin: everything you can do, including settings." |
| 33 | 33 | |
| @@ -35,7 +35,7 @@ | ||
| 35 | 35 | |
| 36 | 36 | A `read` token only reads. Every request that changes something in a repository (any method other than `GET` or `HEAD`) is refused with `403 this token needs the write scope`, even actions the `read` role itself allows, such as opening an issue, commenting, reacting or leaving a comment-only review. Two Git operations use `POST` but only read, and keep working with a `read` token: `git fetch` and `git clone` (`git-upload-pack`) and Git LFS downloads (an LFS batch request for `download`). Pushing and LFS uploads need `write`. |
| 37 | 37 | |
| 38 | -The scope is checked before your role on the repository. Use a `write` token for anything that opens, comments, pushes or merges, and an `admin` token for repository settings. | |
| 38 | +The scope is checked after Diffinery has found a repository you can see, and before your role on it. So a `read` token that tries to change a private repository you can't see gets `404 repository not found`, the same answer as any other request for it, rather than a scope error that would reveal the repository exists. Use a `write` token for anything that opens, comments, pushes or merges, and an `admin` token for repository settings. | |
| 39 | 39 | |
| 40 | 40 | ### Account-level actions |
| 41 | 41 | |
| @@ -47,6 +47,7 @@ | ||
| 47 | 47 | | Mark notifications read (`PUT /api/v3/notifications`), change notification settings, send yourself a test email | `write` or `admin` | Refused | |
| 48 | 48 | | Watch or unwatch a repository (`PUT`/`DELETE /api/v3/repos/{owner}/{repo}/subscription`) | `write` or `admin` | Allowed for the listed repositories | |
| 49 | 49 | | Upload or remove a profile picture (`/api/v3/user/avatar`, `/api/v3/orgs/{org}/avatar`) | `write` or `admin` | Refused | |
| 50 | +| Change an organization's display name or base permission (`PATCH /api/v3/orgs/{org}`, owners only) | `admin` | Refused | | |
| 50 | 51 | | Create a token (`POST /api/v3/diffinery/tokens`, `diffinery token create`) | No token can: a browser session only | Refused | |
| 51 | 52 | | Revoke a token (`DELETE /api/v3/diffinery/tokens/{id}`, `diffinery token revoke`) | `admin` | Refused | |
| 52 | 53 | | Create an organization (`POST /api/v3/user/orgs`) | `admin` | Refused | |
| @@ -118,7 +119,7 @@ | ||
| 118 | 119 | | --- | --- | --- | |
| 119 | 120 | | `name` | string | Token name | |
| 120 | 121 | | `scopes` | array of strings | One or more of `read`, `write`, `admin`. Omit it (or send `null`) for the default `["write"]`. An empty list, a value that isn't a list, or an unknown name returns `422 scopes must be a non-empty list of: read, write, admin`. Repeated names are stored once. | |
| 121 | -| `repositories` | array of strings | `owner/name` of each repository to restrict to. Omit for every repository. An unknown repository returns `422 unknown repository <name>`. | | |
| 122 | +| `repositories` | array of strings | `owner/name` of each repository to restrict to. Omit for every repository. You can only list repositories your account can see: a repository that doesn't exist, or a private one you have no access to, returns `422 unknown repository <name>` (the same message for both, so the answer doesn't reveal private repositories). | | |
| 122 | 123 | | `expires_in_days` | number | 1 to 366 (values outside are clamped). Default 90. | |
| 123 | 124 | |
| 124 | 125 | Response (`201 Created`): |
| @@ -131,7 +132,7 @@ | ||
| 131 | 132 | |
| 132 | 133 | ## Restrict a token to repositories |
| 133 | 134 | |
| 134 | -A restricted token only works on the repositories you picked. Any other repository answers `404 repository not found`, as if it didn't exist, even if you could normally read it. Inside the listed repositories the usual role and scope cap apply. Account-level reads (such as `GET /api/v3/user`, listing your tokens or reading your notifications) still work, but account-level changes don't: creating repositories or organizations, inviting people, managing agents, organization secrets, notification settings and profile pictures, and revoking tokens all answer `403 this token is limited to repositories and can't change account settings` (profile pictures have their own message). See [Account-level actions](#account-level-actions). | |
| 135 | +A restricted token only works on the repositories you picked. Any other repository answers `404 repository not found`, as if it didn't exist, even if you could normally read it. Inside the listed repositories the usual role and scope cap apply. Account-level reads (such as `GET /api/v3/user`, listing your tokens or reading your notifications) still work, but account-level changes don't: creating repositories or organizations, inviting people, managing agents, organization secrets, notification settings and profile pictures, and creating or revoking tokens all answer `403 this token is limited to repositories and can't change account settings` (profile pictures have their own message). See [Account-level actions](#account-level-actions). | |
| 135 | 136 | |
| 136 | 137 | Restrict tokens you hand to CI systems, agents and third-party tools to the repositories they need. |
| 137 | 138 | |
| @@ -200,8 +201,10 @@ | ||
| 200 | 201 | | Token (GitHub style) | `Authorization: token dfy_...` | |
| 201 | 202 | | Basic | `Authorization: Basic base64(username:dfy_...)`. The password is used; the username can be anything. If the password is empty, the username is used as the token. | |
| 202 | 203 | |
| 203 | -Git uses Basic authentication; the CLI, MCP clients and most scripts use Bearer. They are equivalent. | |
| 204 | +Git uses Basic authentication; the CLI, MCP clients and most scripts use Bearer. They are equivalent. A `Basic` value that isn't valid base64 is refused with `401 malformed basic credentials`. | |
| 204 | 205 | |
| 206 | +If a request carries the `Authorization` header twice (Git LFS does this when you set `http.extraHeader` and it also sends its own header), Diffinery uses the first one. | |
| 207 | + | |
| 205 | 208 | ### The CLI |
| 206 | 209 | |
| 207 | 210 | `diffinery login` saves the server URL and token in `~/.config/diffinery/config.json` (or under `$XDG_CONFIG_HOME`). The environment variables `DIFFINERY_TOKEN` and `DIFFINERY_URL` override the saved values, which is handy in CI: |
| @@ -231,8 +234,8 @@ | ||
| 231 | 234 | |
| 232 | 235 | Two other kinds of token exist. You don't create them yourself: |
| 233 | 236 | |
| 234 | -- **Job tokens** (`djt_`) are given to each workflow job as `GITHUB_TOKEN` and `DIFFINERY_TOKEN`. They act as `diffinery-actions[bot]` with read access to the repository the job runs in, plus container registry access according to the job's `permissions: packages` (it pulls its owner's packages and pushes only to packages linked to its own repository), and stop working when the job finishes. See [Secrets and variables](/docs/actions/secrets-and-variables). | |
| 235 | -- **Sandbox tokens** (`dst_`) are given to each agent sandbox. They act as whoever started the sandbox, capped at write, on that repository only, and stop working when the sandbox closes. See [Sandboxes](/docs/agents/sandboxes). | |
| 237 | +- **Job tokens** (`djt_`) are given to each workflow job as `GITHUB_TOKEN` and `DIFFINERY_TOKEN`. They act as `diffinery-actions[bot]` on the repository the job runs in, with what the workflow's `permissions:` grant, as on GitHub: each API endpoint needs its permission (`contents`, `issues`, `pull-requests`, `statuses`, `checks` and so on), and a request without it gets `403 Resource not accessible by integration`. Without `permissions:` a job gets the repository's default, which is read-only (`contents` and `packages` read) for new repositories; an admin changes it with the repository's workflow permissions setting (`PUT /api/v3/repos/{owner}/{repo}/actions/permissions/workflow`). A job token's role never goes above write, so branch protection applies to it, and it never changes repository settings, secrets, variables, webhooks, runners or branch protection, or creates or changes workflow files. A run whose code came from an agent acts as an agent, so it can't push or merge to the default or protected branches. The token also reads other public repositories (not other private ones), uses the container registry according to `permissions: packages`, and stops working when the job finishes. See [Workflow permissions](/docs/actions/workflow-permissions) and [Secrets and variables](/docs/actions/secrets-and-variables). | |
| 238 | +- **Sandbox tokens** (`dst_`) are given to each agent sandbox. They act as whoever started the sandbox, capped at write, on that repository, can also read other public repositories, and stop working when the sandbox closes. See [Sandboxes](/docs/agents/sandboxes). | |
| 236 | 239 | |
| 237 | 240 | ## Security emails about tokens |
| 238 | 241 | |
| @@ -248,7 +251,7 @@ | ||
| 248 | 251 | |
| 249 | 252 | - Diffinery has one kind of personal token with three scopes, plus an optional repository list. GitHub has classic tokens with many scopes and fine-grained tokens with per-permission settings. |
| 250 | 253 | - Every token expires, after at most 366 days. |
| 251 | -- Tokens can only be created with the API or CLI when you already hold an `admin`-scoped token; the first one comes from the web UI. | |
| 254 | +- Tokens can only be created with the API or CLI when you already hold an `admin`-scoped token that isn't restricted to repositories; the first one comes from the web UI. | |
| 252 | 255 | - Git accepts a token as the password with any username, as on GitHub. |
| 253 | 256 | |
| 254 | 257 | ## Troubleshooting |
| @@ -258,11 +261,13 @@ | ||
| 258 | 261 | | `401 invalid, expired or revoked token` | The token is unknown, expired or revoked, or your account is disabled or not yet active. Create a new token. | |
| 259 | 262 | | `401 unrecognised token; create one under Settings → Access tokens` | The credential doesn't start with `dfy_` (or `djt_`, `dst_`), for example a GitHub token or a password. Use a Diffinery token from **Settings → Access tokens**. | |
| 260 | 263 | | `401 authentication required` | No token was sent to a private repository. Configure Git credentials or add the `Authorization` header. | |
| 261 | -| `404 repository not found` | You have no access, or the token is restricted to other repositories. | | |
| 264 | +| `404 repository not found` | You have no access, or the token is restricted to other repositories. A `read` token that tries to change a private repository it can't see also gets this. | | |
| 265 | +| `401 malformed basic credentials` | The `Authorization: Basic` value isn't valid base64. Encode `username:token` with base64, or send `Authorization: Bearer dfy_...` instead. | | |
| 266 | +| `422 unknown repository <name>` when creating a token | The repository doesn't exist, or it is private and your account can't see it. Check the `owner/name` spelling and your access. | | |
| 262 | 267 | | `401 authentication required` on a public repository | You sent no token for a change (opening an issue, commenting, reacting, watching). Anonymous callers can only read. Send a `write` token. | |
| 263 | 268 | | `403 write access required` (or another role) | Your role, or the token's scope, is too low. For `admin` actions, use an `admin` token. | |
| 264 | 269 | | `403 this token needs the write scope` | A `read` token tried to change something. Use a `write` or `admin` token. | |
| 265 | -| `403 this token needs the admin scope` | Account-level actions such as inviting, creating organizations or agents, organization secrets and revoking tokens need an `admin` token. | | |
| 270 | +| `403 this token needs the admin scope` | Account-level actions such as inviting, creating organizations or agents, organization secrets, and creating or revoking tokens need an `admin` token. | | |
| 266 | 271 | | `403 this token is limited to repositories and can't change account settings` | The token has a repository list. Use a token without one, or the web UI. | |
| 267 | 272 | | `422 scopes must be a non-empty list of: read, write, admin` | The `scopes` field was empty, not a list, or had an unknown name. | |
| 268 | 273 | | `403 creating tokens needs a browser session: sign in at https://diffinery.com/settings/tokens` | Tokens can't create tokens. Create it in the web UI. | |
modifiedapps/diffinery/docs/actions/caching-and-artifacts.md+299−149View file
| @@ -1,26 +1,40 @@ | ||
| 1 | 1 | --- |
| 2 | 2 | title: Caching and artifacts |
| 3 | -description: Speed up workflows with actions/cache, and pass files between jobs or out of a run with actions/upload-artifact and actions/download-artifact. | |
| 3 | +description: Speed up workflows with actions/cache, pass files between jobs and runs with upload-artifact and download-artifact, and manage both through the API. | |
| 4 | 4 | --- |
| 5 | 5 | |
| 6 | -Diffinery runners have built-in versions of `actions/cache`, `actions/upload-artifact` and `actions/download-artifact`. A cache keeps files such as dependency downloads between runs. An artifact keeps files produced by one run, so later jobs in that run can use them and you can download them afterwards. This page covers what each one supports today and how they differ from GitHub. | |
| 6 | +A cache keeps files such as dependency downloads between runs. An artifact keeps files produced by a run, so later jobs (in the same run or another one) can use them and you can download them afterwards. Diffinery serves the same cache and artifact services as GitHub, so the official `actions/cache`, `actions/upload-artifact` and `actions/download-artifact` work unmodified, and the runner also has faster built-in versions that share their storage and formats. | |
| 7 | 7 | |
| 8 | -## Built-in actions | |
| 8 | +## Built-in actions and the official actions | |
| 9 | 9 | |
| 10 | -The runner recognises these actions by name and runs its own implementation. The version after `@` is ignored, so `actions/cache@v3` and `actions/cache@v4` behave the same, and nothing is downloaded from GitHub. | |
| 10 | +For speed, the runner runs its own implementation of these actions instead of downloading them: | |
| 11 | 11 | |
| 12 | -| Action | What it does | | |
| 13 | -| --- | --- | | |
| 14 | -| `actions/cache` | Restores a cache entry, then saves one in a post step when the job ends | | |
| 15 | -| `actions/cache/restore` | Restores a cache entry only | | |
| 16 | -| `actions/cache/save` | Saves a cache entry only | | |
| 17 | -| `actions/upload-artifact` | Uploads files as a named artifact of the run | | |
| 18 | -| `actions/download-artifact` | Downloads a named artifact of the same run into the workspace | | |
| 12 | +| Action | Built-in for | What it does | | |
| 13 | +| --- | --- | --- | | |
| 14 | +| `actions/cache` | `v3` to `v6` | Restores a cache entry, then saves one in a post step when the job ends | | |
| 15 | +| `actions/cache/restore` | `v3` to `v6` | Restores a cache entry only | | |
| 16 | +| `actions/cache/save` | `v3` to `v6` | Saves a cache entry only | | |
| 17 | +| `actions/upload-artifact` | `v4` to `v7` | Uploads files as an artifact of the run | | |
| 18 | +| `actions/download-artifact` | `v4` to `v8` | Downloads artifacts of this run, or of another run with `run-id` and `github-token` | | |
| 19 | 19 | |
| 20 | -The `cache` input of `actions/setup-node`, `actions/setup-python` and `actions/setup-go` uses the same cache service. See [Dependency caching with setup actions](#dependency-caching-with-setup-actions). | |
| 20 | +The built-ins use the same services, archive formats, cache versions and artifact ids as the official actions. An entry saved by the built-in `actions/cache` restores with the official action and the other way round, and an artifact uploaded by either downloads with either. | |
| 21 | 21 | |
| 22 | -These built-ins work on hosted and [self-hosted runners](/docs/actions/runners#self-hosted-runners) alike. Both talk to `https://diffinery.com` with the job's token. | |
| 22 | +A built-in steps aside and the real action is downloaded from GitHub and run when: | |
| 23 | 23 | |
| 24 | +- the reference isn't a version tag in the range above (`vN`, `vN.N` or `vN.N.N`), for example a commit SHA, a branch, or a newer major version; | |
| 25 | +- the step passes an input the built-in doesn't know; | |
| 26 | +- the step's environment has `DIFFINERY_BUILTINS` set to `false`, `0`, `no` or `off` (in the workflow's, the job's or the step's `env:`); | |
| 27 | +- a self-hosted runner was started with `DIFFINERY_DISABLE_BUILTINS=1`. | |
| 28 | + | |
| 29 | +```yaml | |
| 30 | +env: | |
| 31 | + DIFFINERY_BUILTINS: false # every step in this workflow runs the real actions | |
| 32 | +``` | |
| 33 | + | |
| 34 | +The `cache` input of `actions/setup-node`, `actions/setup-python` and `actions/setup-go` uses the same service; see [Dependency caching with setup actions](#dependency-caching-with-setup-actions). | |
| 35 | + | |
| 36 | +Third-party actions that use the `@actions/cache` or `@actions/artifact` toolkit packages, and `docker/build-push-action` with `cache-from: type=gha` and `cache-to: type=gha`, work as well (see [The runtime services](#the-runtime-services)). | |
| 37 | + | |
| 24 | 38 | ## Caching dependencies |
| 25 | 39 | |
| 26 | 40 | A cache entry is a compressed archive of one or more paths, stored under a key you choose. A typical workflow restores the entry at the start of a job and saves it at the end: |
| @@ -45,32 +59,31 @@ | ||
| 45 | 59 | - run: npm test |
| 46 | 60 | ``` |
| 47 | 61 | |
| 48 | -On the first run there is no entry, so the step logs `Cache not found for input keys: ...` and the job continues. When the job succeeds, the post step **Post actions/cache@v4** archives `~/.npm` and saves it under the key. The next run with the same `package-lock.json` restores it. | |
| 62 | +On the first run there is no entry, so the step logs `Cache not found for input keys: ...` and the job continues. When the job succeeds, the post step **Post actions/cache@v4** archives `~/.npm` and saves it under the key (`Cache saved with key: <key>`). The next run with the same `package-lock.json` restores it (`Cache restored from key: <key>`). | |
| 49 | 63 | |
| 50 | 64 | ### Inputs for actions/cache |
| 51 | 65 | |
| 52 | 66 | | Input | Required | Default | Description | |
| 53 | 67 | | --- | --- | --- | --- | |
| 54 | -| `path` | Yes | | Files and directories to cache, one per line. Supports globs and `!` exclusions. Relative paths are relative to the workspace; `~/` expands to the home directory. | | |
| 55 | -| `key` | Yes | | Key to save the entry under and to look up first. At most 512 characters. | | |
| 56 | -| `restore-keys` | No | | Prefixes to try, one per line, in order, when `key` has no exact match. | | |
| 57 | -| `lookup-only` | No | `false` | Check whether an entry exists without downloading it. | | |
| 68 | +| `path` | Yes | | Files and directories to cache, one per line. Supports globs and `!` exclusions (`! pattern` is read as `!pattern`). Relative paths are relative to the workspace; `~/` expands to the home directory. | | |
| 69 | +| `key` | Yes | | Key to save the entry under and to look up first. At most 512 characters, no commas. | | |
| 70 | +| `restore-keys` | No | | Prefixes to try, one per line, in order, when `key` has no exact match. At most 20. | | |
| 71 | +| `lookup-only` | No | `false` | Check whether an entry exists without downloading it. Logs `Cache found and can be restored from key: <key>`. | | |
| 58 | 72 | | `fail-on-cache-miss` | No | `false` | Fail the step when neither `key` nor any restore key matches. | |
| 59 | 73 | | `save-always` | No | `false` | Save in the post step even when the job failed or was cancelled. | |
| 74 | +| `upload-chunk-size`, `enableCrossOsArchive` | No | | Accepted. The built-in uploads in 32 MiB blocks whatever `upload-chunk-size` says. | | |
| 60 | 75 | |
| 61 | -`actions/cache/restore` takes `path`, `key`, `restore-keys`, `lookup-only` and `fail-on-cache-miss`. `actions/cache/save` takes `path` and `key`. Other inputs, such as `enableCrossOsArchive` or `upload-chunk-size`, are ignored. | |
| 76 | +`actions/cache/restore` and `actions/cache/save` take the same inputs. If `key` or `path` is missing, the step fails with `Input required and not supplied: key` or `Input required and not supplied: path`. | |
| 62 | 77 | |
| 63 | -If `key` or `path` is missing, the step fails with `Input required and not supplied: key` or `Input required and not supplied: path`. | |
| 64 | - | |
| 65 | 78 | ### Outputs for actions/cache |
| 66 | 79 | |
| 67 | -| Output | Value | | |
| 68 | -| --- | --- | | |
| 69 | -| `cache-hit` | `true` only when an entry matching `key` exactly was found, otherwise `false`. A restore-key match gives `false`. | | |
| 70 | -| `cache-primary-key` | The `key` input. Set by `actions/cache` and `actions/cache/restore`. | | |
| 71 | -| `cache-matched-key` | The key of the entry that was restored, or an empty string on a miss. Set by `actions/cache` and `actions/cache/restore`. | | |
| 80 | +| Output | Set by | Value | | |
| 81 | +| --- | --- | --- | | |
| 82 | +| `cache-hit` | `actions/cache`, `actions/cache/restore` | `true` when the restored entry's key equals `key` (ignoring case), `false` when a restore key matched. Not set on a miss. | | |
| 83 | +| `cache-primary-key` | `actions/cache/restore` | The `key` input, always. | | |
| 84 | +| `cache-matched-key` | `actions/cache/restore` | The key of the entry that was restored. Not set on a miss. | | |
| 72 | 85 | |
| 73 | -Use `cache-hit` to skip work when the exact entry was restored: | |
| 86 | +Use `cache-hit` to skip work when the exact entry was restored. Compare with `'true'`, because the output is empty on a miss: | |
| 74 | 87 | |
| 75 | 88 | ```yaml |
| 76 | 89 | - uses: actions/cache@v4 |
| @@ -84,10 +97,19 @@ | ||
| 84 | 97 | |
| 85 | 98 | ### How keys and restore keys match |
| 86 | 99 | |
| 87 | -When a restore step runs, Diffinery looks up an entry in this order: | |
| 100 | +An entry has a key, a version and a scope. The version is a hash of the `path` lines and the compression, computed the way `@actions/cache` computes it, so an entry only restores for the same paths saved with the same compression. The scope is the ref of the job that saved it. | |
| 88 | 101 | |
| 102 | +When a restore step runs, Diffinery searches these scopes in order, and stops at the first match: | |
| 103 | + | |
| 104 | +1. The job's own ref, for example `refs/heads/feature-x`, `refs/tags/v1.2.0` or, for a `pull_request` run, `refs/pull/7/merge`. | |
| 105 | +2. For pull request runs, the pull request's base branch. For merge queue runs, the queue's target branch (`merge_group.base_ref`). | |
| 106 | +3. The default branch. | |
| 107 | + | |
| 108 | +In each scope it tries: | |
| 109 | + | |
| 89 | 110 | 1. An entry whose key equals `key` exactly. |
| 90 | -2. For each line of `restore-keys`, in order, the newest entry whose key starts with that line. The first restore key with any match wins. | |
| 111 | +2. The newest entry whose key starts with `key`. | |
| 112 | +3. For each line of `restore-keys`, in order, the newest entry whose key starts with that line. | |
| 91 | 113 | |
| 92 | 114 | ```yaml |
| 93 | 115 | key: build-${{ github.ref_name }}-${{ hashFiles('**/go.sum') }} |
| @@ -96,19 +118,23 @@ | ||
| 96 | 118 | build- |
| 97 | 119 | ``` |
| 98 | 120 | |
| 99 | -With this configuration, a run first looks for the exact key, then for the newest entry saved on the same branch, then for the newest `build-` entry from any branch. | |
| 121 | +With this configuration, a run on a feature branch first looks for the exact key and the newest `build-` entry saved on that branch, then on the default branch. | |
| 100 | 122 | |
| 101 | -Keys are case-sensitive. Each restore key is trimmed of surrounding spaces, and because restore keys are sent to the server as a comma-separated list, a restore key can't contain a comma. Keep commas out of keys you intend to match by prefix. | |
| 123 | +### Scope | |
| 102 | 124 | |
| 125 | +A job saves only to its own ref. Branches, tags and pull requests read their own entries and those of the default branch (and pull requests those of their base branch), but never each other's. This is the same isolation GitHub has, so a pull request can't plant a cache entry that a release job on the default branch later restores. | |
| 126 | + | |
| 127 | +Different repositories never share entries, even within one organization. | |
| 128 | + | |
| 103 | 129 | ### When the cache is saved |
| 104 | 130 | |
| 105 | 131 | `actions/cache` saves in a post step that runs after all other steps of the job: |
| 106 | 132 | |
| 107 | 133 | - It runs only if the job succeeded so far. With `save-always: true` it runs even after a failure or cancellation. |
| 108 | -- It's skipped when the restore found an exact match for `key`, since that entry already exists. | |
| 134 | +- It's skipped when the restore found an exact match for `key`: `Cache hit occurred on the primary key <key>, not saving cache.` | |
| 109 | 135 | - It archives whatever exists at `path` at that moment. If none of the paths exist, it logs the warning `Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved.` |
| 110 | 136 | |
| 111 | -Entries are immutable. If another job saved the same key first, the save logs `Cache entry with key <key> already exists; not saving.` and does nothing. To refresh an entry, change its key, for example by including a hash of the lock file. | |
| 137 | +Entries are immutable. If an entry with the same key and version already exists in the job's scope, or another running job is saving it, the save logs `Failed to save: Unable to reserve cache with key <key>, another job may be creating this cache.` and the job continues. To refresh an entry, change its key, for example by including a hash of the lock file. An upload left unfinished by a job that has ended is discarded and the key can be saved again. | |
| 112 | 138 | |
| 113 | 139 | `actions/cache/save` saves immediately when its step runs, so you can save a cache mid-job or from a step with its own `if:`: |
| 114 | 140 | |
| @@ -127,54 +153,104 @@ | ||
| 127 | 153 | key: ${{ steps.restore.outputs.cache-primary-key }} |
| 128 | 154 | ``` |
| 129 | 155 | |
| 130 | -### Archive format and paths | |
| 156 | +### Archive format | |
| 131 | 157 | |
| 132 | -The runner creates a gzip-compressed tar archive of the matched paths, stored with their absolute paths, and extracts it to the same absolute paths on restore. As a result: | |
| 158 | +The built-in creates the same archive as `@actions/cache`: a POSIX tar file of the matched paths, stored relative to the workspace, compressed with zstd when `zstd` is installed (it is on hosted runners) and with gzip otherwise. Restoring extracts it to the same paths relative to the workspace. | |
| 133 | 159 | |
| 134 | -- A cache saved on one machine restores into the same directories on another. If a self-hosted runner has a different home directory or workspace path than a hosted runner, an entry saved by one restores into paths the other doesn't use. Include `runner.environment` or a label in the key if you mix runner types. | |
| 160 | +- The compression is part of the version, so an entry saved on a runner with zstd doesn't restore on one without it. | |
| 161 | +- A path outside the workspace, such as `~/.npm`, is stored relative to the workspace too. A runner whose workspace sits at a different depth restores it into a different directory. Include `runner.environment` or a label in the key if you mix runner types. | |
| 135 | 162 | - The step log shows the archive size as `Cache Size: ~<n> MB (<bytes> B)` on both save and restore. |
| 136 | -- `.git` directories are skipped when a glob pattern walks a directory tree. | |
| 137 | 163 | |
| 138 | -### Scope | |
| 164 | +### Size and eviction | |
| 139 | 165 | |
| 140 | -A cache entry belongs to the repository. Every workflow, branch, tag and event in the repository can restore any entry, including runs triggered by `pull_request`. There is no per-branch isolation. | |
| 166 | +| Limit | Value | | |
| 167 | +| --- | --- | | |
| 168 | +| Key | 512 characters, no commas. A longer key or a comma fails the restore step with `Key Validation Error: <key> cannot be larger than 512 characters.` or `Key Validation Error: <key> cannot contain commas.` | | |
| 169 | +| Restore keys | 20 per lookup | | |
| 170 | +| One entry | 10 GB. A larger archive isn't saved: `Cache size of ~<n> MB (<bytes> B) is over the 10GB limit, not saving cache.` | | |
| 171 | +| Repository total | 10 GB of entries, counting uploads in progress | | |
| 172 | +| One upload request | 256 MiB. The built-ins and the official actions upload large archives in smaller blocks, so this only matters for your own clients. | | |
| 141 | 173 | |
| 142 | -> **Important:** Because any branch can save an entry that other branches then restore, don't cache anything you later execute without verification in a sensitive job, such as a release build. Give release jobs keys that other branches don't produce, and don't use broad restore keys there. | |
| 174 | +Diffinery deletes entries in two ways: | |
| 143 | 175 | |
| 144 | -Different repositories never share entries, even within one organization. | |
| 176 | +- Entries that no job has restored for 7 days are deleted the next time a job saves a cache in the repository. | |
| 177 | +- When a save needs room above the 10 GB total, the least recently used entries are deleted until it fits. Restoring an entry counts as using it. | |
| 145 | 178 | |
| 146 | -### Size and eviction | |
| 179 | +If uploads still in progress use the whole budget, the save is refused (`uploads in progress use the repository's whole cache budget`) and the job continues with a warning. | |
| 147 | 180 | |
| 148 | -| Limit | Value | | |
| 181 | +### Manage caches with the API | |
| 182 | + | |
| 183 | +List, inspect and delete cache entries with the REST API. Reading needs read access to the repository; deleting needs write access. For a job's `GITHUB_TOKEN`, these endpoints need the `actions` permission. | |
| 184 | + | |
| 185 | +List entries: | |
| 186 | + | |
| 187 | +```http | |
| 188 | +GET /api/v3/repos/acme/website/actions/caches?key=npm-&ref=refs/heads/main | |
| 189 | +Authorization: Bearer dfy_... | |
| 190 | +``` | |
| 191 | + | |
| 192 | +```json | |
| 193 | +{ | |
| 194 | + "total_count": 1, | |
| 195 | + "actions_caches": [ | |
| 196 | + { | |
| 197 | + "id": 42, | |
| 198 | + "ref": "refs/heads/main", | |
| 199 | + "key": "npm-Linux-3f1c...", | |
| 200 | + "version": "a1b2c3...", | |
| 201 | + "last_accessed_at": "2026-10-12T09:41:07Z", | |
| 202 | + "created_at": "2026-10-10T08:02:51Z", | |
| 203 | + "size_in_bytes": 48211301 | |
| 204 | + } | |
| 205 | + ] | |
| 206 | +} | |
| 207 | +``` | |
| 208 | + | |
| 209 | +| Query parameter | Meaning | | |
| 149 | 210 | | --- | --- | |
| 150 | -| Key length | 512 characters. A longer key is rejected with `422 cache key too long`, and the save logs a warning instead of failing the job. | | |
| 151 | -| One entry | Each entry is uploaded in a single request, which Diffinery's front end refuses above 100 MB. Keep each compressed archive under 100 MB. | | |
| 152 | -| Repository total | 10 GB. When a save pushes the total over 10 GB, Diffinery deletes the least recently used entries until it's under again. | | |
| 211 | +| `key` | Only entries whose key starts with this | | |
| 212 | +| `ref` | Only entries saved for this ref, for example `refs/heads/main` or `refs/pull/7/merge` | | |
| 213 | +| `sort` | `last_accessed_at` (default), `created_at` or `size_in_bytes` | | |
| 214 | +| `direction` | `desc` (default) or `asc` | | |
| 215 | +| `per_page`, `page` | Paging; `per_page` defaults to 30, at most 100 | | |
| 153 | 216 | |
| 154 | -Restoring an entry counts as using it. Entries that no workflow restores are the first to go when the repository reaches its limit. | |
| 217 | +Other endpoints: | |
| 155 | 218 | |
| 156 | -There is no API or web page for listing or deleting cache entries today. To stop restoring an entry, change the key prefix in your workflow. | |
| 219 | +| Request | Result | | |
| 220 | +| --- | --- | | |
| 221 | +| `GET /repos/{owner}/{repo}/actions/cache/usage` | `{"full_name": "acme/website", "active_caches_size_in_bytes": ..., "active_caches_count": ...}` | | |
| 222 | +| `DELETE /repos/{owner}/{repo}/actions/caches?key=<key>&ref=<ref>` | Deletes the entries with exactly this key (in every scope, or only `ref`'s). Returns `{"total_count": n, "actions_caches": [...]}` with the deleted entries. `422 key is required` without `key`; `404 Not Found` when nothing matches. | | |
| 223 | +| `DELETE /repos/{owner}/{repo}/actions/caches/{cache_id}` | Deletes one entry. `204 No Content`, or `404 Not Found`. | | |
| 157 | 224 | |
| 225 | +```sh | |
| 226 | +diffinery api GET "/repos/acme/website/actions/caches?key=npm-" | |
| 227 | +diffinery api DELETE "/repos/acme/website/actions/caches?key=npm-Linux-3f1c..." | |
| 228 | +``` | |
| 229 | + | |
| 230 | +Deletions are recorded in the [audit log](/docs/security/audit-log) as `actions.cache.delete`. | |
| 231 | + | |
| 158 | 232 | ### Errors never fail the job |
| 159 | 233 | |
| 160 | -Cache problems are reported as warnings and the job carries on, as on GitHub: | |
| 234 | +Cache service problems are reported as warnings and the job carries on, as on GitHub: | |
| 161 | 235 | |
| 162 | -- `Failed to look up cache: <reason>` or `Failed to restore cache: <reason>` on restore. The step behaves like a miss. | |
| 163 | -- `Failed to save cache: <reason>` on save, for example when the archive is too large. | |
| 236 | +- `Failed to restore: <reason>` on restore. The step behaves like a miss. | |
| 237 | +- `Failed to save: <reason>` on save. | |
| 238 | +- `The cache service is not available to this job; skipping.` when the job has no runtime services. | |
| 164 | 239 | |
| 165 | -The exception is `fail-on-cache-miss: true`, which fails the step with `Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: <key>`. | |
| 240 | +The exceptions are a key validation error (see the table above) and `fail-on-cache-miss: true`, which fails the step with `Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: <key>`. | |
| 166 | 241 | |
| 167 | 242 | ## Dependency caching with setup actions |
| 168 | 243 | |
| 169 | -`actions/setup-node`, `actions/setup-python` and `actions/setup-go` can restore and save a dependency cache for you. They use the same service, scope and limits as `actions/cache`. The key is built from a fixed prefix and the hash of your lock files, there are no restore keys, and the save happens in a post step only when the job succeeds and the exact key wasn't found. | |
| 244 | +`actions/setup-node`, `actions/setup-python` and `actions/setup-go` can restore and save a dependency cache for you. They use the same service, scopes and limits as `actions/cache`, and the same keys and paths as the official setup actions, so a cache saved by the built-in restores with the official action and the other way round. The save happens in a post step only when the job succeeds, the exact key wasn't restored, and the cache directory exists. | |
| 170 | 245 | |
| 171 | -| Action | Input | Cached directory | Lock files hashed by default | Key on a hosted runner | | |
| 246 | +| Action | Input | Cached directory | Files hashed by default | Key on a hosted runner | | |
| 172 | 247 | | --- | --- | --- | --- | --- | |
| 173 | -| `actions/setup-node` | `cache: npm`, `pnpm` or `yarn` | `npm config get cache`, `pnpm store path`, or the Yarn cache folder | `**/package-lock.json` and `**/npm-shrinkwrap.json`, `**/pnpm-lock.yaml`, or `**/yarn.lock` | `node-cache-Linux-x64-<manager>-<hash>` | | |
| 174 | -| `actions/setup-python` | `cache: pip` | `pip cache dir` | `**/requirements*.txt` and `**/pyproject.toml` | `setup-python-Linux-x64-python-<version>-pip-<hash>` | | |
| 175 | -| `actions/setup-go` | `cache` (on by default) | `GOMODCACHE` and `GOCACHE` | `**/go.sum` | `setup-go-Linux-amd64-go-<version>-<hash>` | | |
| 248 | +| `actions/setup-node` | `cache: npm`, `pnpm` or `yarn` | `npm config get cache`, `pnpm store path`, or the Yarn cache folder | The first of `package-lock.json`, `npm-shrinkwrap.json`, `yarn.lock` in the workspace root (npm); `pnpm-lock.yaml` (pnpm); `yarn.lock` (yarn) | `node-cache-Linux-x64-<manager>-<hash>` | | |
| 249 | +| `actions/setup-python` | `cache: pip` | `pip cache dir` | `**/requirements.txt`, or `**/pyproject.toml` when none match | `setup-python-Linux-x64-<os version>-<os name>-python-<version>-pip-<hash>`, with the part before the hash as a restore key | | |
| 250 | +| `actions/setup-python` | `cache: pipenv` | `~/.local/share/virtualenvs` | `**/Pipfile.lock` | `setup-python-Linux-x64-python-<version>-pipenv-<hash>` | | |
| 251 | +| `actions/setup-go` | `cache` (on by default) | `GOMODCACHE` and `GOCACHE` | `go.sum` in the workspace root | `setup-go-Linux-x64-ubuntu24-go-<version>-<hash>` | | |
| 176 | 252 | |
| 177 | -All three accept `cache-dependency-path` to hash different files, one pattern per line. Each sets the `cache-hit` output. | |
| 253 | +All three accept `cache-dependency-path` to hash different files, one pattern per line, and set the `cache-hit` output (`true` or `false`). `actions/setup-node` also sets `cache-primary-key`. | |
| 178 | 254 | |
| 179 | 255 | ```yaml |
| 180 | 256 | - uses: actions/setup-node@v4 |
| @@ -184,28 +260,32 @@ | ||
| 184 | 260 | cache-dependency-path: apps/web/pnpm-lock.yaml |
| 185 | 261 | ``` |
| 186 | 262 | |
| 187 | -Differences to be aware of: | |
| 263 | +Details to be aware of: | |
| 188 | 264 | |
| 189 | -- If `cache` is set and no file matches the lock file patterns, setup-node and setup-python fail with `Dependencies lock file is not found in <workspace>. Supported file patterns: <patterns>`. | |
| 190 | -- `actions/setup-go` skips the cache with `go.sum not found; skipping dependency cache.` when there's no `go.sum` at the workspace root and no `cache-dependency-path`. Set `cache: false` to turn it off. | |
| 191 | -- `actions/setup-python` caches pip only. `cache: pipenv` or `cache: poetry` fails with `Caching for '<manager>' is not supported by the Diffinery setup-python built-in (only pip).` | |
| 192 | -- `actions/setup-node` with any value other than `npm`, `pnpm` or `yarn` fails with `Caching for '<manager>' is not supported`. | |
| 265 | +- `actions/setup-node@v5` and later turn on npm caching by themselves when `package.json` names npm in `packageManager` or `devEngines.packageManager`, and log `Detected npm as the package manager from package.json's packageManager field. ...`. Set `package-manager-cache: false` to turn it off. | |
| 266 | +- If setup-node's lock file is missing, the step fails with `Dependencies lock file is not found in <workspace>. Supported file patterns: <files>`. If `cache-dependency-path` matches nothing, it fails with `Some specified paths were not resolved, unable to cache dependencies.` | |
| 267 | +- If setup-python's patterns match nothing, the step fails with `No file in <workspace> matched to [<patterns>], make sure you have checked out the target repository`. | |
| 268 | +- `actions/setup-go` doesn't fail without a `go.sum`: it logs the warning `Restore cache failed: Dependencies lock file is not found in <workspace>. Supported file patterns: go.sum` and continues. Set `cache: false` to turn caching off. | |
| 269 | +- On a miss the log says `<manager> cache is not found`, for example `npm cache is not found`. | |
| 270 | +- `cache` values the built-ins don't handle (for example `cache: poetry` in setup-python) run the official action instead. | |
| 193 | 271 | |
| 194 | 272 | ## Artifacts |
| 195 | 273 | |
| 196 | -An artifact is a named archive of files that belongs to a workflow run. Jobs in the same run can download it, and anyone with read access to the repository can download it from the run page or the API. | |
| 274 | +An artifact is a named file, normally a zip archive, that belongs to a workflow run. Jobs in the same run download it with `actions/download-artifact`, other runs can download it with `run-id` and a token, and anyone with read access to the repository can download it from the run page or the API until it expires. | |
| 197 | 275 | |
| 198 | 276 | ### Upload an artifact |
| 199 | 277 | |
| 200 | 278 | ```yaml |
| 201 | 279 | - run: npm run build |
| 202 | 280 | - uses: actions/upload-artifact@v4 |
| 281 | + id: upload | |
| 203 | 282 | with: |
| 204 | 283 | name: site |
| 205 | 284 | path: | |
| 206 | 285 | dist/ |
| 207 | 286 | !dist/**/*.map |
| 208 | 287 | if-no-files-found: error |
| 288 | + retention-days: 14 | |
| 209 | 289 | ``` |
| 210 | 290 | |
| 211 | 291 | | Input | Required | Default | Description | |
| @@ -213,15 +293,25 @@ | ||
| 213 | 293 | | `name` | No | `artifact` | Artifact name. It can't contain `"`, `:`, `<`, `>`, `\|`, `*`, `?`, `\`, `/`, carriage return or line feed. | |
| 214 | 294 | | `path` | Yes | | Files, directories or globs to upload, one per line. Lines starting with `!` exclude matches, and lines starting with `#` are ignored. Relative paths are relative to the workspace. | |
| 215 | 295 | | `if-no-files-found` | No | `warn` | `warn` logs a warning, `error` fails the step, `ignore` logs a plain message. | |
| 296 | +| `retention-days` | No | 90 | Days to keep the artifact, from 1 to 90. | | |
| 297 | +| `compression-level` | No | `6` | Zip compression level, `0` (none) to `9` (best). Use `0` for files that are already compressed. | | |
| 298 | +| `overwrite` | No | `false` | Replace an artifact with the same name in the run instead of failing. | | |
| 216 | 299 | | `include-hidden-files` | No | `false` | Include files and directories whose names start with `.`. | |
| 300 | +| `archive` | No | `true` | `false` uploads a single file as it is, without a zip archive. The artifact is then named after the file. | | |
| 217 | 301 | |
| 218 | -`retention-days`, `overwrite`, `compression-level` and other inputs are accepted but have no effect. See [Differences from GitHub](#differences-from-github). | |
| 302 | +The step logs `With the provided path, there will be <n> file(s) uploaded`, then `Artifact <name> has been successfully uploaded! Final size is <n> bytes. Artifact ID is <id>`. | |
| 219 | 303 | |
| 220 | -The step logs the number of files and the root directory, then `Artifact '<name>' uploaded (<bytes> bytes compressed).` | |
| 304 | +### Outputs of upload-artifact | |
| 221 | 305 | |
| 306 | +| Output | Value | | |
| 307 | +| --- | --- | | |
| 308 | +| `artifact-id` | The artifact's numeric id | | |
| 309 | +| `artifact-digest` | The SHA-256 digest of the uploaded file, in hex | | |
| 310 | +| `artifact-url` | `https://diffinery.com/acme/website/actions/runs/<run number>/artifacts/<id>`, a link that downloads the artifact | | |
| 311 | + | |
| 222 | 312 | ### What goes into the archive |
| 223 | 313 | |
| 224 | -The runner collects the matching files, then archives them relative to the deepest directory that contains all of them. This matches `actions/upload-artifact`: | |
| 314 | +The runner collects the matching files, then archives them relative to the deepest directory that contains all of them, as `actions/upload-artifact` does: | |
| 225 | 315 | |
| 226 | 316 | | `path` | Files matched | Paths inside the artifact | |
| 227 | 317 | | --- | --- | --- | |
| @@ -229,13 +319,13 @@ | ||
| 229 | 319 | | `dist/index.html` | `dist/index.html` | `index.html` | |
| 230 | 320 | | `dist/` and `reports/junit.xml` | `dist/index.html`, `reports/junit.xml` | `dist/index.html`, `reports/junit.xml` | |
| 231 | 321 | |
| 232 | -Only files are uploaded; empty directories are dropped. Hidden files are left out unless `include-hidden-files: true`. | |
| 322 | +Only files are uploaded; empty directories are dropped. Hidden files are left out unless `include-hidden-files: true`. The archive is a zip file. | |
| 233 | 323 | |
| 234 | -The archive is a gzip-compressed tar file (`.tar.gz`), not a zip file. | |
| 324 | +With `archive: false`, exactly one file may match; otherwise the step fails with `When 'archive' is set to false, only a single file can be uploaded. Found <n> files to upload.` The file is stored with a content type from its extension, for example `text/html` for `.html`. | |
| 235 | 325 | |
| 236 | -### Download an artifact in a later job | |
| 326 | +### Download artifacts in a later job | |
| 237 | 327 | |
| 238 | -Give `actions/download-artifact` the artifact's `name`. The job must run after the job that uploaded it, so list that job in `needs`: | |
| 328 | +The job must run after the job that uploaded the artifact, so list that job in `needs`: | |
| 239 | 329 | |
| 240 | 330 | ```yaml |
| 241 | 331 | jobs: |
| @@ -261,33 +351,58 @@ | ||
| 261 | 351 | - run: ls -R ${{ steps.get-site.outputs.download-path }} |
| 262 | 352 | ``` |
| 263 | 353 | |
| 264 | -| Input | Required | Default | Description | | |
| 265 | -| --- | --- | --- | --- | | |
| 266 | -| `name` | Yes | | Name of the artifact to download. | | |
| 267 | -| `path` | No | the workspace | Directory to extract into, relative to the workspace. It's created if missing. | | |
| 354 | +| Input | Default | Description | | |
| 355 | +| --- | --- | --- | | |
| 356 | +| `name` | | Download only the artifact with this name. | | |
| 357 | +| `artifact-ids` | | Download only these artifacts, by id, separated by commas. Can't be combined with `name`. | | |
| 358 | +| `pattern` | | Download the artifacts whose names match this glob, for example `junit-*`. | | |
| 359 | +| `path` | The workspace | Directory to extract into, relative to the workspace. It's created if missing. | | |
| 360 | +| `merge-multiple` | `false` | Extract every selected artifact into `path` itself instead of one directory per artifact. | | |
| 361 | +| `skip-decompress` | `false` | Save the zip file instead of extracting it. | | |
| 362 | +| `digest-mismatch` | `error` for `v8` and later, `warn` before | What to do when a download doesn't match the artifact's digest: `ignore`, `info`, `warn` or `error`. | | |
| 363 | +| `github-token`, `run-id`, `repository` | | Download from another run or repository; see below. | | |
| 268 | 364 | |
| 269 | -The step sets the output `download-path` to the absolute directory it extracted into, and logs `Artifact '<name>' downloaded to <path>`. | |
| 365 | +With no `name`, `artifact-ids` or `pattern`, every artifact of the run is downloaded. Where they go: | |
| 270 | 366 | |
| 271 | -`name` is required. Without it, the step fails with: | |
| 367 | +- With `name`, or `merge-multiple: true`, files go straight into `path`. | |
| 368 | +- Otherwise each artifact goes into `<path>/<artifact name>`. From `v5` on, a download that selects exactly one artifact goes straight into `path`. | |
| 272 | 369 | |
| 273 | -```text | |
| 274 | -download-artifact without `name` (download all / `pattern`) is not supported on Diffinery runners yet; specify `name`. | |
| 275 | -``` | |
| 370 | +The step sets the output `download-path` to the absolute directory, and logs `Artifact '<name>' downloaded to <path>` for each artifact. | |
| 276 | 371 | |
| 277 | -To download several artifacts, add one step per name. | |
| 372 | +### Download from another run or repository | |
| 278 | 373 | |
| 279 | -### Which artifacts a job can see | |
| 374 | +Pass `run-id` and a token in `github-token` to read another run's artifacts through the REST API. Add `repository` for a run in another repository: | |
| 280 | 375 | |
| 281 | -`actions/download-artifact` reads artifacts from the job's own run only: | |
| 376 | +```yaml | |
| 377 | +jobs: | |
| 378 | + report: | |
| 379 | + runs-on: ubuntu-latest | |
| 380 | + permissions: | |
| 381 | + actions: read | |
| 382 | + steps: | |
| 383 | + - uses: actions/download-artifact@v4 | |
| 384 | + with: | |
| 385 | + name: coverage | |
| 386 | + run-id: ${{ github.event.workflow_run.id }} | |
| 387 | + github-token: ${{ secrets.GITHUB_TOKEN }} | |
| 388 | +``` | |
| 282 | 389 | |
| 283 | -- Jobs of a [reusable workflow](/docs/actions/reusable-workflows) are part of the caller's run, so the caller and the called workflow can exchange artifacts. | |
| 284 | -- The `run-id`, `repository` and `github-token` inputs aren't supported, so a job can't download artifacts from another run or another repository. To pass files between runs, use a cache entry with a known key, a [package](/docs/packages/container-registry), or the [API](#download-artifacts-with-the-api) from a `run:` step with a personal token stored as a secret. | |
| 285 | -- A re-run is a new run, so artifacts uploaded by the original run aren't visible to it. | |
| 390 | +- `run-id` is a run number, such as `github.run_id` or `github.event.workflow_run.id`. It covers every attempt of that run. | |
| 391 | +- The job's `GITHUB_TOKEN` can read another run of its own repository when the job has `actions: read` (or the repository default gives it). It can read artifacts of other **public** repositories on Diffinery, but not of other private ones; use a personal or agent token with read access for those. See [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions). | |
| 392 | +- `run-id` without `github-token` is ignored, and the current run is read. | |
| 286 | 393 | |
| 394 | +Without these inputs, `actions/download-artifact` reads artifacts from the job's own run only. Jobs of a [reusable workflow](/docs/actions/reusable-workflows) are part of the caller's run, so the caller and the called workflow can exchange artifacts. A re-run attempt is a separate run for this purpose: its jobs don't see the artifacts of earlier attempts without `run-id`. | |
| 395 | + | |
| 287 | 396 | ### Uploading the same name twice |
| 288 | 397 | |
| 289 | -Artifact names are unique within a run. Uploading a name that already exists in the run replaces the earlier artifact without an error, even without `overwrite: true`. In a matrix, give each job its own name: | |
| 398 | +Artifact names are unique within a run. Uploading a name that already exists fails: | |
| 290 | 399 | |
| 400 | +```text | |
| 401 | +Failed to CreateArtifact: an artifact with the name 'site' already exists on the workflow run. Set 'overwrite: true' to replace it. | |
| 402 | +``` | |
| 403 | + | |
| 404 | +Set `overwrite: true` to delete the old artifact and upload the new one. In a matrix, give each job its own name: | |
| 405 | + | |
| 291 | 406 | ```yaml |
| 292 | 407 | strategy: |
| 293 | 408 | matrix: |
| @@ -300,102 +415,137 @@ | ||
| 300 | 415 | path: junit.xml |
| 301 | 416 | ``` |
| 302 | 417 | |
| 303 | -### Size | |
| 418 | +A later job can collect them all with `pattern: junit-*` and `merge-multiple: true`. | |
| 304 | 419 | |
| 305 | -Each artifact is uploaded in a single request, which Diffinery's front end refuses above 100 MB. Keep each compressed artifact under 100 MB. If a build output is larger, split it into several artifacts or push it to the [container registry](/docs/packages/container-registry), which accepts larger layers from hosted runners. | |
| 420 | +### Retention and expiry | |
| 306 | 421 | |
| 422 | +Each artifact keeps its `retention-days` from 1 to 90 days, 90 when you don't set one. A value above 90 is lowered with the warning `Retention days cannot be greater than the maximum allowed retention set within the repository. Using 90 instead.` | |
| 423 | + | |
| 424 | +When an artifact expires: | |
| 425 | + | |
| 426 | +- Its content is deleted, and it no longer counts toward the storage quota. | |
| 427 | +- The run page no longer lists it, and download-artifact no longer finds it. | |
| 428 | +- The REST API still lists it, with `"expired": true`, for another 90 days. Downloading it answers `410 Artifact has expired`. | |
| 429 | + | |
| 430 | +### Storage quota | |
| 431 | + | |
| 432 | +A repository can store up to 10 GB of artifacts on diffinery.com. Live artifacts and uploads in progress count; expired and deleted artifacts don't. An upload beyond the quota fails the step with a message that ends in: | |
| 433 | + | |
| 434 | +```text | |
| 435 | +the repository's artifact storage quota (10737418240 bytes) is used up; delete artifacts or wait for them to expire | |
| 436 | +``` | |
| 437 | + | |
| 438 | +Delete artifacts you no longer need with the API (see below). A job's unfinished uploads are removed when the job ends. On a self-hosted Diffinery instance the quota is set with `ARTIFACT_QUOTA_BYTES` (see [Configuration](/docs/self-hosting/configuration)). | |
| 439 | + | |
| 307 | 440 | ## Download artifacts from the web |
| 308 | 441 | |
| 309 | -Open the run at `https://diffinery.com/<owner>/<repo>/actions/runs/<run id>`. When the run has artifacts, an **Artifacts** box lists each one with its size and upload time. Select a name to download it as `<name>.tar.gz`. | |
| 442 | +Open the run at `https://diffinery.com/<owner>/<repo>/actions/runs/<run number>`. When the run has artifacts that haven't expired, an **Artifacts** box lists each one with its size and upload time. Select a name to download it as `<name>.zip`, or as the file itself for an artifact uploaded with `archive: false`. | |
| 310 | 443 | |
| 311 | -Extract it with tar: | |
| 444 | +The download link is `https://diffinery.com/<owner>/<repo>/actions/runs/<run>/artifacts/<name or id>`, the same form as the `artifact-url` output. Extract a zip file with: | |
| 312 | 445 | |
| 313 | 446 | ```sh |
| 314 | -tar -xzf site.tar.gz -C site | |
| 447 | +unzip site.zip -d site | |
| 315 | 448 | ``` |
| 316 | 449 | |
| 317 | -You need read access to the repository. For a public repository, anyone can download. | |
| 450 | +You need read access to the repository. For a public repository, anyone can download. Artifacts uploaded by older Diffinery runner versions download as `<name>.tar.gz`. | |
| 318 | 451 | |
| 319 | 452 | ## Download artifacts with the API |
| 320 | 453 | |
| 321 | -List a run's artifacts: | |
| 454 | +The REST API has GitHub's artifact endpoints. Reading needs read access to the repository; deleting needs write access. For a job's `GITHUB_TOKEN`, they need the `actions` permission. | |
| 322 | 455 | |
| 323 | -```http | |
| 324 | -GET /api/v3/repos/acme/website/actions/runs/{run_id}/artifacts | |
| 325 | -Authorization: Bearer dfy_... | |
| 326 | -``` | |
| 456 | +| Request | Result | | |
| 457 | +| --- | --- | | |
| 458 | +| `GET /repos/{owner}/{repo}/actions/artifacts` | Every artifact of the repository, newest first, expired ones included. Takes `name`, `per_page` (default 30, at most 100) and `page`. | | |
| 459 | +| `GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts` | The artifacts of one run, expired ones included. `run_id` is the run number (every attempt) or a run's id. Takes `name`, `per_page` and `page`. | | |
| 460 | +| `GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id}` | One artifact. | | |
| 461 | +| `GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id}/zip` | `302 Found` to a signed download URL valid for 10 minutes. `410 Artifact has expired` for an expired artifact. | | |
| 462 | +| `DELETE /repos/{owner}/{repo}/actions/artifacts/{artifact_id}` | Deletes the artifact. `204 No Content`. Recorded in the [audit log](/docs/security/audit-log) as `actions.artifact.delete`. | | |
| 327 | 463 | |
| 464 | +Unknown ids answer `404 Not Found`. An artifact looks like this: | |
| 465 | + | |
| 328 | 466 | ```json |
| 329 | 467 | { |
| 330 | - "total_count": 1, | |
| 331 | - "artifacts": [ | |
| 332 | - { | |
| 333 | - "name": "site", | |
| 334 | - "size_in_bytes": 482113, | |
| 335 | - "archive_download_url": "https://diffinery.com/acme/website/actions/runs/{run_id}/artifacts/site", | |
| 336 | - "expired": false, | |
| 337 | - "created_at": "2026-10-12T09:41:07Z", | |
| 338 | - "updated_at": "2026-10-12T09:41:07Z", | |
| 339 | - "workflow_run": { "id": "{run_id}" } | |
| 340 | - } | |
| 341 | - ] | |
| 468 | + "id": 1187, | |
| 469 | + "node_id": "MDg6QXJ0aWZhY3Q1187", | |
| 470 | + "name": "site", | |
| 471 | + "size_in_bytes": 482113, | |
| 472 | + "url": "https://diffinery.com/api/v3/repos/acme/website/actions/artifacts/1187", | |
| 473 | + "archive_download_url": "https://diffinery.com/api/v3/repos/acme/website/actions/artifacts/1187/zip", | |
| 474 | + "expired": false, | |
| 475 | + "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", | |
| 476 | + "created_at": "2026-10-12T09:41:07Z", | |
| 477 | + "updated_at": "2026-10-12T09:41:07Z", | |
| 478 | + "expires_at": "2026-10-26T09:41:07Z", | |
| 479 | + "workflow_run": { | |
| 480 | + "id": 128, | |
| 481 | + "repository_id": "...", | |
| 482 | + "head_repository_id": "...", | |
| 483 | + "head_branch": "main", | |
| 484 | + "head_sha": "9fceb02d0ae598e95dc970b74767f19372d61af8" | |
| 485 | + } | |
| 342 | 486 | } |
| 343 | 487 | ``` |
| 344 | 488 | |
| 345 | -Artifacts are sorted by name. The list takes `per_page` (default 30, at most 100) and `page`, and sends a `Link` header for the next page, so `octokit.paginate` works; `total_count` counts every artifact of the run. Unlike GitHub, entries have no `id` or `expires_at` field, `expired` is always `false`, `workflow_run.id` is Diffinery's run id (a string), and there is no endpoint to delete an artifact or to list artifacts across runs. | |
| 489 | +`workflow_run.id` is the run number. Download with the CLI's API command or curl; curl follows the redirect with `-L`: | |
| 346 | 490 | |
| 347 | -`archive_download_url` returns the gzip-compressed tar file directly, with no redirect. Send the same token: | |
| 491 | +```sh | |
| 492 | +curl -fsSL -H "Authorization: Bearer dfy_..." \ | |
| 493 | + https://diffinery.com/api/v3/repos/acme/website/actions/artifacts/1187/zip -o site.zip | |
| 494 | +``` | |
| 348 | 495 | |
| 496 | +Install the CLI with `curl -fsSL https://diffinery.com/-/cli/diffinery.mjs -o ~/.local/bin/diffinery && chmod +x ~/.local/bin/diffinery` (Node.js 22 or later), then list a run's artifacts: | |
| 497 | + | |
| 349 | 498 | ```sh |
| 350 | -curl -fsSL -H "Authorization: Bearer $DIFFINERY_TOKEN" \ | |
| 351 | - "https://diffinery.com/acme/website/actions/runs/$RUN_ID/artifacts/site" \ | |
| 352 | - -o site.tar.gz | |
| 353 | -tar -xzf site.tar.gz | |
| 499 | +diffinery api GET /repos/acme/website/actions/runs/128/artifacts | |
| 354 | 500 | ``` |
| 355 | 501 | |
| 356 | -A token with read access to the repository is enough. Find run IDs with `diffinery run list` or the runs API; see [the API reference](/docs/api) and [the CLI reference](/docs/reference/cli). | |
| 502 | +Artifacts uploaded by older Diffinery runner versions appear on the run page but not in these endpoints. | |
| 357 | 503 | |
| 358 | -| Status | Body | Meaning | | |
| 359 | -| --- | --- | --- | | |
| 360 | -| 404 | `{"message":"artifact not found"}` | The run has no artifact with that name. | | |
| 361 | -| 410 | `{"message":"artifact expired"}` | The artifact is listed but its file is no longer stored. | | |
| 504 | +## The runtime services | |
| 362 | 505 | |
| 363 | -## Differences from GitHub | |
| 506 | +Diffinery gives each job the same runtime services GitHub does, through these environment variables: | |
| 364 | 507 | |
| 365 | -| Area | GitHub Actions | Diffinery | | |
| 366 | -| --- | --- | --- | | |
| 367 | -| Cache scope | Per branch, with fallback to the default branch | Whole repository, every branch and event | | |
| 368 | -| Cache management | Web page and API to list and delete entries | None; change the key to stop using an entry | | |
| 369 | -| Cache size per repository | 10 GB | 10 GB, least recently used entries evicted | | |
| 370 | -| Single cache entry or artifact | Uploaded in chunks, several GB | One request, under 100 MB compressed | | |
| 371 | -| Artifact format | Zip file | gzip-compressed tar file | | |
| 372 | -| `retention-days`, `overwrite` | Supported | Ignored; a same-name upload in the run always replaces | | |
| 373 | -| `artifact-id`, `artifact-url` outputs | Set | Not set | | |
| 374 | -| Download all artifacts, `pattern`, `merge-multiple` | Supported | Not supported; `name` is required | | |
| 375 | -| Download from another run or repository | `run-id`, `repository`, `github-token` | Not supported | | |
| 376 | -| Third-party actions that use the cache or artifact toolkit | Work | Don't work; see below | | |
| 508 | +| Variable | Value | | |
| 509 | +| --- | --- | | |
| 510 | +| `ACTIONS_RUNTIME_TOKEN` | A token for this job's cache and artifact requests. It works only while the job runs and expires 30 minutes after the job's timeout. | | |
| 511 | +| `ACTIONS_RESULTS_URL` | `https://diffinery.com/` | | |
| 512 | +| `ACTIONS_CACHE_URL`, `ACTIONS_RUNTIME_URL` | `https://diffinery.com/_runtime/` | | |
| 513 | +| `ACTIONS_CACHE_SERVICE_V2` | `true` | | |
| 377 | 514 | |
| 378 | -### Third-party actions and BuildKit | |
| 515 | +As on GitHub, they're given to JavaScript actions, Docker container actions and the built-ins, not to `run:` steps. So `docker buildx build --cache-to type=gha` in a `run:` step finds no cache service, while `docker/build-push-action`, a JavaScript action, runs the build with them. | |
| 379 | 516 | |
| 380 | -Diffinery runners don't set `ACTIONS_RUNTIME_TOKEN`, `ACTIONS_CACHE_URL` or `ACTIONS_RESULTS_URL`. Actions that use the `@actions/cache` or `@actions/artifact` npm packages directly, rather than through the built-ins above, fail or skip caching. The same applies to Docker BuildKit's `type=gha` cache backend. | |
| 517 | +The services speak the protocols the `@actions/cache` and `@actions/artifact` toolkit packages use: cache service v2 and artifact service v4, the older cache REST API, and signed upload and download URLs. A signed download URL is valid for one hour. Requests from a job that has finished fail with `401 the job is not running`. | |
| 381 | 518 | |
| 382 | -For Docker layer caching, keep the cache in an image in the [container registry](/docs/packages/container-registry) instead, for example with BuildKit's `type=registry` cache pointing at a tag such as `diffinery.com/acme/website:buildcache`. | |
| 519 | +For Docker layer caching you can use `type=gha` through `docker/build-push-action`, which stores the layers as cache entries under the same 10 GB budget, or keep the cache in the [container registry](/docs/packages/container-registry) with `type=registry`, for example `diffinery.com/acme/website:buildcache`. See [Docker in workflows](/docs/actions/docker). | |
| 383 | 520 | |
| 384 | -See [Docker in workflows](/docs/actions/docker) for signing in to the registry from a job, and [Compatibility with GitHub Actions](/docs/actions/compatibility) for everything else that differs. | |
| 521 | +## Differences from GitHub | |
| 385 | 522 | |
| 523 | +| Area | GitHub Actions | Diffinery | | |
| 524 | +| --- | --- | --- | | |
| 525 | +| How the actions run | Downloaded from GitHub | Built into the runner for the listed versions; the real action otherwise | | |
| 526 | +| Cache scope, size and eviction | Per ref with fallbacks, 10 GB, 7-day idle eviction | The same | | |
| 527 | +| Artifact storage | Counted against the account's storage | 10 GB per repository on diffinery.com, then uploads are refused | | |
| 528 | +| Artifact retention | Set per repository or organization | 1 to 90 days, 90 by default | | |
| 529 | +| Web page for caches | Yes | No; use the API | | |
| 530 | +| `artifact-url` | Points at github.com | Points at Diffinery | | |
| 531 | +| `run-id` in download-artifact | The run's id | The run number (`github.run_id`), which is the same thing on Diffinery | | |
| 532 | + | |
| 386 | 533 | ## Troubleshooting |
| 387 | 534 | |
| 388 | 535 | | Symptom | Cause and fix | |
| 389 | 536 | | --- | --- | |
| 390 | -| `Cache not found for input keys: ...` on every run | The save never happened. Check the post step's log: the job may have failed (use `save-always: true`), the paths may not exist, or the archive may be over 100 MB. | | |
| 391 | -| `Cache entry with key ... already exists; not saving.` | Entries are immutable. Include something that changes, such as `hashFiles(...)` or `github.run_id`, in the key, and use `restore-keys` to pick up the latest. | | |
| 392 | -| `Failed to save cache: PUT ... -> 413 ...` | The compressed archive is over 100 MB. Cache fewer paths, or split them into several entries with different keys. | | |
| 393 | -| `Failed to save cache: ... 422 cache key too long` | Shorten the key to 512 characters or fewer. | | |
| 394 | -| `cache-hit` is `false` although files were restored | A restore key matched, not `key`. That's expected; `cache-hit` is `true` only for an exact match. | | |
| 395 | -| Restored files appear in an unexpected directory | Entries store absolute paths. A runner with a different home or workspace directory restores into the original paths. Add the runner type to the key. | | |
| 396 | -| `The artifact name is not valid: ...` | Remove `/`, `\`, `:` and the other characters listed in [Upload an artifact](#upload-an-artifact). | | |
| 537 | +| `Cache not found for input keys: ...` on every run | The save never happened, or it's in a scope this run can't read. Check the post step's log: the job may have failed (use `save-always: true`), the paths may not exist. An entry saved on a feature branch isn't visible to other branches; save on the default branch for shared entries. | | |
| 538 | +| `Failed to save: Unable to reserve cache with key <key>, another job may be creating this cache.` | An entry with this key already exists for this ref, or another job is saving it. Include something that changes in the key, such as `hashFiles(...)`, and use `restore-keys` to pick up the latest. | | |
| 539 | +| `Key Validation Error: ... cannot contain commas.` | Remove commas from `key` and `restore-keys`. | | |
| 540 | +| `cache-hit` is `false` although files were restored | A restore key matched, not `key`. That's expected. | | |
| 541 | +| A cache saved by one runner doesn't restore on another | The version includes the paths and the compression. Use the same `path` lines, and make sure `zstd` is installed on both or neither. | | |
| 542 | +| Restored files appear in an unexpected directory | Paths are stored relative to the workspace. A runner with a different workspace depth restores into other directories. Add the runner type to the key. | | |
| 543 | +| `The artifact name is not valid: <name>. Contains the following character: ...` | Remove `/`, `\`, `:` and the other characters listed in [Upload an artifact](#upload-an-artifact). | | |
| 397 | 544 | | `No files were found with the provided path: ... No artifacts will be uploaded.` | The paths don't match any files, or only hidden files. Check the paths relative to the workspace, or set `include-hidden-files: true`. | |
| 398 | -| `Failed to upload artifact '<name>': ... 413 ...` | The compressed artifact is over 100 MB. Split it into several artifacts. | | |
| 399 | -| `Unable to download artifact '<name>': ... 404 artifact not found` | No job in this run has uploaded that name yet. Add the uploading job to `needs`, and check the name's spelling and case. | | |
| 400 | -| An artifact contains another matrix job's files | Matrix jobs uploaded the same name and the last one won. Include matrix values in the name. | | |
| 401 | -| A third-party action logs that the cache service is unavailable | It uses the cache toolkit directly. Replace it with `actions/cache` or the setup action's `cache` input. | | |
| 545 | +| `Failed to CreateArtifact: an artifact with the name '<name>' already exists on the workflow run. ...` | Set `overwrite: true`, or include matrix values in the name. | | |
| 546 | +| `... artifact storage quota ... is used up; delete artifacts or wait for them to expire` | Delete old artifacts with `DELETE /repos/{owner}/{repo}/actions/artifacts/{artifact_id}`, or use shorter `retention-days`. | | |
| 547 | +| `Unable to download artifact(s): Artifact not found for name: <name>` | No job in this run has uploaded that name yet, or it expired. Add the uploading job to `needs`, and check the name's spelling and case. | | |
| 548 | +| `Inputs 'name' and 'artifact-ids' cannot be used together. Please specify only one.` | Remove one of the two inputs. | | |
| 549 | +| `Digest validation failed for artifact(s): ...` | The downloaded file doesn't match the digest recorded at upload. Download again; set `digest-mismatch: warn` to continue anyway. | | |
| 550 | +| `Listing artifacts of run <n> in <repo> failed: HTTP 403` | The token in `github-token` lacks access. Give the job `permissions: actions: read`, or use a token that can read the other repository. | | |
| 551 | +| `410 Artifact has expired` | The artifact passed its retention period. Its content is gone. | |
modifiedapps/diffinery/docs/actions/compatibility.md+148−118View file
| @@ -15,15 +15,15 @@ | ||
| 15 | 15 | | Hosted runners | Linux x64 only (Ubuntu 24.04) | [Runners](#runners) | |
| 16 | 16 | | Self-hosted runners | Supported, through `diffinery runner` | [Runners](/docs/actions/runners) | |
| 17 | 17 | | Matrices, `needs`, outputs, job summaries | Supported | [Jobs and steps](#jobs-and-steps) | |
| 18 | -| `actions/checkout`, `actions/setup-node`, `actions/setup-python`, `actions/setup-go` | Built into the runner, most inputs supported | [Built-in actions](#built-in-actions) | | |
| 19 | -| `actions/cache`, `actions/upload-artifact`, `actions/download-artifact` | Built into the runner, with limits | [Caching and artifacts](/docs/actions/caching-and-artifacts) | | |
| 20 | -| `actions/github-script` | Not supported yet | [actions/github-script](#actionsgithub-script) | | |
| 21 | -| Other JavaScript, composite and Docker actions | Supported, downloaded from GitHub | [Other actions](#other-actions) | | |
| 18 | +| `actions/checkout`, `actions/setup-node`, `actions/setup-python`, `actions/setup-go` | Built into the runner for speed; the real action runs for versions or inputs the built-in doesn't cover | [Built-in actions](#built-in-actions) | | |
| 19 | +| `actions/cache`, `actions/upload-artifact`, `actions/download-artifact` | Built into the runner, sharing storage and formats with the official actions, which also work | [Caching and artifacts](/docs/actions/caching-and-artifacts) | | |
| 20 | +| `actions/github-script` | Supported (the real action runs) | [actions/github-script](#actionsgithub-script) | | |
| 21 | +| Other JavaScript, composite and Docker actions | Supported, downloaded from GitHub, with the cache and artifact runtime services | [Other actions](#other-actions) | | |
| 22 | 22 | | Job containers and service containers | Supported | [Docker in workflows](/docs/actions/docker) | |
| 23 | 23 | | Reusable workflows | Supported, except a matrix on the calling job | [Reusable workflows](/docs/actions/reusable-workflows) | |
| 24 | -| Secrets and variables | Repository, organization and environment secrets; repository and environment variables | [Secrets and variables](/docs/actions/secrets-and-variables) | | |
| 24 | +| Secrets and variables | Repository, organization and environment secrets and variables | [Secrets and variables](/docs/actions/secrets-and-variables) | | |
| 25 | 25 | | Environments | Supported: branch policies, environment secrets and variables, deployments. No required reviewers or wait timers. | [Secrets, variables and environments](#secrets-variables-and-environments) | |
| 26 | -| `GITHUB_TOKEN` | Read-only on its own repository, whatever `permissions` says | [GITHUB_TOKEN and permissions](#github_token-and-permissions) | | |
| 26 | +| `GITHUB_TOKEN` | Follows `permissions:` per endpoint, as on GitHub; read-only by default in new repositories | [GITHUB_TOKEN and permissions](#github_token-and-permissions) | | |
| 27 | 27 | | OIDC | Supported, with Diffinery as the issuer | [OIDC](/docs/actions/oidc) | |
| 28 | 28 | | Windows and macOS hosted runners | Not supported yet | [Runners](#runners) | |
| 29 | 29 | |
| @@ -51,7 +51,7 @@ | ||
| 51 | 51 | | `schedule` | Supported | Checked every 5 minutes, so a schedule starts within about 5 minutes after it's due. Runs have the actor `diffinery-scheduler`. | |
| 52 | 52 | | `workflow_run` | Supported | Activity types `requested` and `completed` (not `in_progress`), `workflows` is required, and chains go at most 3 levels deep. | |
| 53 | 53 | | `release` | Supported | Same activity types. No runs for draft releases, as on GitHub. Without `types`, a new published release starts three runs (`created`, `published`, `released`), as on GitHub. Only `types` filters; `branches`, `tags` and `paths` aren't applied. | |
| 54 | -| `repository_dispatch` | Supported | `POST /repos/{owner}/{repo}/dispatches` answers `204`. The payload has `action`, `branch` and `client_payload`, but no `repository` or `sender`. | | |
| 54 | +| `repository_dispatch` | Supported | `POST /repos/{owner}/{repo}/dispatches` answers `204`. The payload has `action`, `branch`, `client_payload`, `repository` and `sender`. | | |
| 55 | 55 | | `workflow_call` | Supported | See [Reusable workflows](/docs/actions/reusable-workflows). | |
| 56 | 56 | | Every other GitHub event (`issues`, `issue_comment`, `pull_request_review`, `create`, `delete`, `status`, `deployment`, `deployment_status` and the rest) | Accepted, never fires | The workflow parses, but Diffinery never starts a run for these events. (Webhooks do get `status`, `deployment` and `deployment_status`.) | |
| 57 | 57 | | A name that isn't a GitHub event | Warning | `unknown event '<name>'`. | |
| @@ -64,16 +64,13 @@ | ||
| 64 | 64 | |
| 65 | 65 | | Key | What happens on Diffinery | |
| 66 | 66 | | --- | --- | |
| 67 | -| `jobs.<id>.runs-on` | Can't use the `matrix` context: `runs-on: ${{ matrix.os }}` evaluates to `null`, and the job fails with the "No Diffinery runner matches runs-on" message. The `group` key is ignored. | | |
| 68 | -| `jobs.<id>.environment` | Supported: the branch policy is enforced, the environment's secrets and variables are added and a deployment is recorded. Required reviewers and wait timers aren't supported, `name` and `url` can't use `needs`, and `url` can't use step outputs. See [environment](/docs/actions/workflow-syntax#environment). | | |
| 69 | -| `jobs.<id>.concurrency` | Accepted and ignored. Workflow-level `concurrency` works. | | |
| 70 | -| `jobs.<id>.continue-on-error` | Accepted, no effect. A failing job fails the run. Use step-level `continue-on-error`, which works. | | |
| 67 | +| `jobs.<id>.runs-on` | Supported, including the `matrix` context (`runs-on: ${{ matrix.os }}`), but only Linux labels are hosted. The `group` key is ignored. | | |
| 68 | +| `jobs.<id>.environment` | Supported: the branch policy is enforced, the environment's secrets and variables are added and a deployment is recorded. Required reviewers and wait timers aren't supported, and `url` can't use step outputs. See [environment](/docs/actions/workflow-syntax#environment). | | |
| 71 | 69 | | `jobs.<id>.snapshot` | Accepted and ignored. | |
| 72 | -| `jobs.<id>.permissions`, `permissions` | Only `id-token` and `packages` have an effect. See [GITHUB_TOKEN and permissions](#github_token-and-permissions). | | |
| 73 | 70 | | `jobs.<id>.timeout-minutes` | Default and maximum 360. Larger values are capped at 360. | |
| 74 | 71 | | `jobs.<id>.strategy.matrix` | At most 256 jobs per matrix. | |
| 75 | 72 | | `jobs.<id>.uses` with `strategy.matrix` | Not supported yet. The run fails with `jobs.<id>: a matrix on a job that calls a reusable workflow is not supported yet`. | |
| 76 | -| `concurrency` | Several runs can wait in a group; GitHub keeps only the newest pending one. See [Concurrency](/docs/actions/overview#concurrency). | | |
| 73 | +| `concurrency` (workflow level) | Several runs can wait in a group; GitHub keeps only the newest pending one. Job-level `concurrency` keeps only the newest pending job, as on GitHub. See [Concurrency](/docs/actions/overview#concurrency). | | |
| 77 | 74 | | `defaults.run.shell`, `jobs.<id>.steps[*].shell` | `cmd` isn't supported. `pwsh` and `powershell` need PowerShell, which hosted runners don't have. | |
| 78 | 75 | |
| 79 | 76 | An unknown key gives a warning such as `unknown job key '<key>'` or `unknown step key '<key>'`, and the key is ignored. The full reference is [Workflow syntax](/docs/actions/workflow-syntax). |
| @@ -90,7 +87,7 @@ | ||
| 90 | 87 | |
| 91 | 88 | What's different about the hosted image: |
| 92 | 89 | |
| 93 | -- It's much smaller than GitHub's image. Git, Git LFS, curl, jq, build-essential, Python 3.12, Node.js 22 with npm, pnpm and yarn, and Docker are preinstalled. Go, Java, Ruby, Rust, .NET, PowerShell, browsers and cloud CLIs aren't. See [The Ubuntu 24.04 image](/docs/actions/runners#the-ubuntu-2404-image). | |
| 90 | +- It's much smaller than GitHub's image. Git, Git LFS, curl, jq, zstd, build-essential, Python 3.12, Node.js 22 with npm, pnpm and yarn, Docker and the GitHub CLI (`gh`) are preinstalled. `ImageOS` is `ubuntu24`, as on GitHub's `ubuntu-24.04` image. Go, Java, Ruby, Rust, .NET, PowerShell, browsers and cloud CLIs aren't. See [The Ubuntu 24.04 image](/docs/actions/runners#the-ubuntu-2404-image). | |
| 94 | 91 | - Jobs run as `root` and `sudo` isn't installed. Drop `sudo` from commands. |
| 95 | 92 | - The apt package lists are removed. Run `apt-get update` before `apt-get install`. |
| 96 | 93 | - Every job starts in a fresh container. Nothing carries over between jobs except caches and artifacts. |
| @@ -99,14 +96,12 @@ | ||
| 99 | 96 | |
| 100 | 97 | ## Jobs and steps |
| 101 | 98 | |
| 102 | -These work the same as on GitHub: `needs`, job outputs, `if` conditions, status check functions, matrices with `include` and `exclude`, `fail-fast`, `max-parallel`, step `continue-on-error` and `timeout-minutes`, `working-directory`, environment files (`GITHUB_OUTPUT`, `GITHUB_ENV`, `GITHUB_PATH`, `GITHUB_STEP_SUMMARY`, `GITHUB_STATE`), workflow commands, job summaries, masking and debug logging with `ACTIONS_STEP_DEBUG`. | |
| 99 | +These work the same as on GitHub: `needs` (a job starts as soon as the jobs it needs finish), job outputs, `if` conditions, status check functions (a skipped or failed job further up the `needs` chain skips the jobs below it unless their `if` says otherwise), matrices with `include` and `exclude`, `fail-fast`, `max-parallel`, job and step `continue-on-error`, job-level `concurrency`, `timeout-minutes`, `working-directory`, environment files (`GITHUB_OUTPUT`, `GITHUB_ENV`, `GITHUB_PATH`, `GITHUB_STEP_SUMMARY`, `GITHUB_STATE`), workflow commands, job summaries, masking and debug logging with `ACTIONS_STEP_DEBUG` and `ACTIONS_RUNNER_DEBUG`. | |
| 103 | 100 | |
| 104 | 101 | Differences: |
| 105 | 102 | |
| 106 | 103 | | Behaviour | GitHub | Diffinery | |
| 107 | 104 | | --- | --- | --- | |
| 108 | -| A job whose `needs` includes a skipped job | Skipped | Runs; a skipped job counts as a success. | | |
| 109 | -| `failure()` in a job's `if` | Looks at all upstream jobs | Looks only at the jobs in the job's own `needs`. | | |
| 110 | 105 | | Default shell for `run` | `bash -e {0}` | `bash --noprofile --norc -eo pipefail {0}`, so a failing command in a pipeline fails the step. | |
| 111 | 106 | | `::add-matcher::` problem matchers | Turn matching output into annotations | Not supported; the line is printed as is. | |
| 112 | 107 | | Annotations (`::error file=...`) | Shown on pull request files | Shown in the job log only. | |
| @@ -117,36 +112,63 @@ | ||
| 117 | 112 | |
| 118 | 113 | ## Built-in actions |
| 119 | 114 | |
| 120 | -The runner has its own implementation of these actions. They're matched by name, whatever the version after `@`, and nothing is downloaded: | |
| 115 | +For speed, the runner has its own implementation of these actions and runs it instead of downloading the action, when the step uses a version tag in the listed range: | |
| 121 | 116 | |
| 122 | -- `actions/checkout` | |
| 123 | -- `actions/setup-node`, `actions/setup-python`, `actions/setup-go` | |
| 124 | -- `actions/cache`, `actions/cache/restore`, `actions/cache/save` | |
| 125 | -- `actions/upload-artifact`, `actions/download-artifact` | |
| 126 | -- `actions/github-script` (fails with a message; see below) | |
| 117 | +| Action | Built-in for | | |
| 118 | +| --- | --- | | |
| 119 | +| `actions/checkout` | `v2` to `v6` | | |
| 120 | +| `actions/setup-node` | `v3` to `v7` | | |
| 121 | +| `actions/setup-python`, `actions/setup-go` | `v4` to `v6` | | |
| 122 | +| `actions/cache`, `actions/cache/restore`, `actions/cache/save` | `v3` to `v6` | | |
| 123 | +| `actions/upload-artifact` | `v4` to `v7` | | |
| 124 | +| `actions/download-artifact` | `v4` to `v8` | | |
| 127 | 125 | |
| 126 | +The built-ins mirror the official actions' inputs, outputs and storage: a cache or artifact saved by a built-in works with the official action and the other way round. A built-in steps aside, and the real action is downloaded from GitHub and run, when: | |
| 127 | + | |
| 128 | +- the reference isn't `vN`, `vN.N` or `vN.N.N` in the range, for example a commit SHA, a branch or a newer major version; | |
| 129 | +- the step passes an input the built-in doesn't list (see the tables below); | |
| 130 | +- an input has a value the built-in doesn't handle: `ssh-key` for checkout, an `architecture` other than the runner's, a `cache` value the built-in doesn't support, a PyPy or GraalPy version, or `update-environment: false` for setup-python; | |
| 131 | +- the workflow, job or step `env:` sets `DIFFINERY_BUILTINS` to `false`, `0`, `no` or `off`; | |
| 132 | +- a self-hosted runner was started with `DIFFINERY_DISABLE_BUILTINS=1`. | |
| 133 | + | |
| 134 | +```yaml | |
| 135 | +jobs: | |
| 136 | + build: | |
| 137 | + runs-on: ubuntu-latest | |
| 138 | + env: | |
| 139 | + DIFFINERY_BUILTINS: false # this job runs the real actions | |
| 140 | + steps: | |
| 141 | + - uses: actions/checkout@v4 | |
| 142 | +``` | |
| 143 | + | |
| 128 | 144 | Built-in actions run on the runner, not inside a job container. In a job with `container:`, they work on the shared workspace. |
| 129 | 145 | |
| 130 | 146 | ### actions/checkout |
| 131 | 147 | |
| 132 | 148 | Checks out a repository on the same Diffinery instance into the workspace. |
| 133 | 149 | |
| 134 | -| Input | Support | Notes | | |
| 150 | +| Input | Default | Notes | | |
| 135 | 151 | | --- | --- | --- | |
| 136 | -| `repository` | Supported | `owner/name` of a repository on Diffinery, not on GitHub. Default: the workflow's repository. | | |
| 137 | -| `ref` | Supported | Branch, tag, full ref or 40-character commit SHA. Default: the commit that triggered the run, or the default branch of another repository. A short SHA is treated as a branch or tag name. | | |
| 138 | -| `token` | Supported | Default: the job's token, which can only read the workflow's own repository. To check out another repository, pass a personal or agent token from a secret. | | |
| 139 | -| `fetch-depth` | Supported | Default `1`. `0` fetches all branches and tags with full history. | | |
| 140 | -| `fetch-tags` | Supported | Default `false`. | | |
| 141 | -| `path` | Supported | Relative to the workspace. | | |
| 142 | -| `clean` | Supported | Default `true`: runs `git clean -ffdx` and `git reset --hard HEAD` on an existing checkout. | | |
| 143 | -| `submodules` | Supported | `true` or `recursive`. Submodules are fetched with the same token; a failure gives the warning `Failed to update submodules (best effort).` | | |
| 144 | -| `persist-credentials` | Supported | Default `true`: the token stays in the local Git config so later `git` commands authenticate. A post step removes it at the end of the job. | | |
| 145 | -| `lfs` | Not supported | `lfs: true` logs `Git LFS is not supported by the Diffinery checkout built-in; LFS pointers will not be resolved.` Run `git lfs pull` in a later step; see [Use LFS files in workflows](/docs/repositories/git-lfs#use-lfs-files-in-workflows). | | |
| 146 | -| `sparse-checkout` | Not supported | Logs `sparse-checkout is not supported by the Diffinery checkout built-in; doing a full checkout.` | | |
| 147 | -| `filter` | Not supported | Ignored. Partial clone filters aren't supported yet. | | |
| 148 | -| `ssh-key`, `ssh-known-hosts`, `ssh-strict`, `ssh-user`, `set-safe-directory`, `show-progress`, `github-server-url`, `sparse-checkout-cone-mode` | Ignored | Checkout always uses HTTPS with a token. Every directory is already a Git safe directory on hosted runners. | | |
| 152 | +| `repository` | The workflow's repository | `owner/name` of a repository on Diffinery. Anything else fails with `Invalid repository '<value>'. Expected format {owner}/{repo}.` | | |
| 153 | +| `ref` | The commit that triggered the run, or the other repository's default branch | Branch, tag, full ref or 40-character commit SHA. A short SHA is treated as a branch or tag name. | | |
| 154 | +| `token` | The job's token | The job's token reads the workflow's own repository (with `contents: read`) and any public repository on Diffinery. For another private repository, pass a personal or agent token from a secret. The job's token is only ever sent to Diffinery; a different `github-server-url` needs an explicit `token`. | | |
| 155 | +| `fetch-depth` | `1` | `0` fetches all branches and tags with full history. | | |
| 156 | +| `fetch-tags` | `false` | | | |
| 157 | +| `path` | The workspace | Relative to the workspace. A path outside it fails with `Repository path '<path>' is not under '<workspace>'`. | | |
| 158 | +| `clean` | `true` | Runs `git clean -ffdx` and `git reset --hard HEAD` on an existing checkout. | | |
| 159 | +| `filter` | | A partial clone filter such as `blob:none`, passed to `git fetch`. Diffinery's git server doesn't support partial clone yet and sends everything. | | |
| 160 | +| `sparse-checkout` | | Patterns, one per line. Sets up a sparse checkout and fetches with `filter: blob:none` unless you set `filter`. | | |
| 161 | +| `sparse-checkout-cone-mode` | `true` | `false` uses non-cone patterns. | | |
| 162 | +| `lfs` | `false` | Fetches Git LFS objects for the checked-out commit (and runs `git lfs pull` in submodules). See [Git LFS](/docs/repositories/git-lfs). | | |
| 163 | +| `submodules` | `false` | `true` or `recursive`. Submodules on Diffinery are fetched with the same credentials. A failed submodule update fails the step. | | |
| 164 | +| `persist-credentials` | `true` | Writes the token into the repository's local Git config at the end, so later `git` commands authenticate. A post step removes it. With `false`, nothing is written. | | |
| 165 | +| `show-progress` | `true` | Passes `--progress` to `git fetch` and `git checkout`. | | |
| 166 | +| `set-safe-directory` | `true` | Marks the checkout as a Git safe directory for checkout's own commands. | | |
| 167 | +| `github-server-url` | Diffinery | Check out from another server, for example `https://github.com`, with an explicit `token` for private repositories. | | |
| 168 | +| `ssh-key` | | Not handled by the built-in: the real action runs. `ssh-known-hosts`, `ssh-strict` and `ssh-user` are accepted and ignored. | | |
| 149 | 169 | |
| 170 | +While fetching, the token reaches git through the environment, so a checkout that fails halfway leaves no credentials in `.git/config`. Credentials a reused workspace still holds from another job are removed first. | |
| 171 | + | |
| 150 | 172 | Outputs: `commit` (the checked-out SHA) and `ref`. |
| 151 | 173 | |
| 152 | 174 | As on GitHub, a `pull_request` run checks out the test merge commit (`refs/pull/<number>/merge`) by default, and a `pull_request_target` run checks out the base branch. |
| @@ -159,42 +181,43 @@ | ||
| 159 | 181 | |
| 160 | 182 | ### actions/setup-node |
| 161 | 183 | |
| 162 | -| Input | Support | Notes | | |
| 163 | -| --- | --- | --- | | |
| 164 | -| `node-version` | Supported | A version or range (`22`, `20.x`, `>=20.10`), `lts/*`, `lts/<codename>`, `latest`, `current` or `node`. | | |
| 165 | -| `node-version-file` | Supported | `package.json` (reads `volta.node`, then `devEngines.runtime.version`, then `engines.node`), `.tool-versions` (`nodejs` or `node`), or a file whose first non-comment line is the version, such as `.nvmrc` or `.node-version`. | | |
| 166 | -| `check-latest` | Supported | Download the newest matching version even when the preinstalled one satisfies the request. | | |
| 167 | -| `registry-url`, `scope` | Supported | Writes an `.npmrc` that reads `NODE_AUTH_TOKEN`, and sets `NPM_CONFIG_USERCONFIG`. | | |
| 168 | -| `cache`, `cache-dependency-path` | Supported | `npm`, `pnpm` or `yarn`. See [Dependency caching with setup actions](/docs/actions/caching-and-artifacts#dependency-caching-with-setup-actions). | | |
| 169 | -| `architecture`, `always-auth`, `token`, `mirror`, `mirror-token`, `package-manager-cache` | Ignored | | | |
| 184 | +| Input | Notes | | |
| 185 | +| --- | --- | | |
| 186 | +| `node-version` | A version or range (`22`, `20.x`, `>=20.10`), `lts/*`, `lts/<codename>`, `lts/-1`, `latest`, `current` or `node`. | | |
| 187 | +| `node-version-file` | `package.json` (reads `volta.node`, then `devEngines.runtime.version`, then `engines.node`), `.tool-versions` (`nodejs` or `node`), or a file whose first non-comment line is the version, such as `.nvmrc` or `.node-version`. | | |
| 188 | +| `check-latest` | Download the newest matching version even when the preinstalled one satisfies the request. | | |
| 189 | +| `registry-url`, `scope`, `always-auth` | Writes an `.npmrc` that reads `NODE_AUTH_TOKEN`, and sets `NPM_CONFIG_USERCONFIG`. For `npm.pkg.github.com` the scope defaults to the repository owner. `always-auth` is written for `v4` and earlier, or when you set it. | | |
| 190 | +| `cache`, `cache-dependency-path` | `npm`, `pnpm` or `yarn`. See [Dependency caching with setup actions](/docs/actions/caching-and-artifacts#dependency-caching-with-setup-actions). | | |
| 191 | +| `package-manager-cache` | `v5` and later cache npm automatically when `package.json` names npm as its package manager; `false` turns that off. | | |
| 192 | +| `architecture`, `token` | Accepted. An `architecture` other than the runner's runs the real action. | | |
| 170 | 193 | |
| 171 | -Hosted runners have Node.js 22.22.0. A request that it satisfies, or that names major version 22, uses it without a download. Any other version is downloaded from `nodejs.org` into the tool cache. Without `node-version` or `node-version-file`, the step logs `No node-version specified; using the pre-installed node <version>.` | |
| 194 | +Hosted runners have Node.js 22.22.0. A request that it satisfies uses it without a download; any other version is downloaded from `nodejs.org` into the tool cache. Without `node-version` or `node-version-file`, the step logs `No node-version specified; using the pre-installed node <version>.` Other inputs, such as `mirror`, run the real action. | |
| 172 | 195 | |
| 173 | -Outputs: `node-version` (for example `v22.22.0`) and `cache-hit`. | |
| 196 | +Outputs: `node-version` (for example `v22.22.0`), `cache-hit` and `cache-primary-key`. | |
| 174 | 197 | |
| 175 | 198 | ### actions/setup-python |
| 176 | 199 | |
| 177 | -| Input | Support | Notes | | |
| 178 | -| --- | --- | --- | | |
| 179 | -| `python-version` | Supported | One CPython version or range, such as `3.12`, `3.11.9` or `3.x`. Only stable releases are installed. | | |
| 180 | -| `python-version-file` | Supported | `.python-version`, `.tool-versions` (`python`), `pyproject.toml` (`requires-python`), or a file whose first non-comment line is the version. Without either input, `.python-version` in the workspace is used if present. | | |
| 181 | -| `cache`, `cache-dependency-path` | Partly | `pip` only. Other values fail with `Caching for '<manager>' is not supported by the Diffinery setup-python built-in (only pip).` | | |
| 182 | -| PyPy and GraalPy versions | Not supported | Fails with `'<version>' is not supported by the Diffinery setup-python built-in.` | | |
| 183 | -| `architecture`, `check-latest`, `token`, `update-environment`, `allow-prereleases`, `freethreaded` | Ignored | | | |
| 200 | +| Input | Notes | | |
| 201 | +| --- | --- | | |
| 202 | +| `python-version` | One CPython version or range, such as `3.12`, `3.11.9`, `3.x` or `3.14-dev`. PyPy and GraalPy versions run the real action. | | |
| 203 | +| `python-version-file` | `.python-version`, `.tool-versions` (`python`), `pyproject.toml` (`requires-python`), or a file whose first non-comment line is the version. Without either input, `.python-version` in the workspace is used if present. | | |
| 204 | +| `allow-prereleases` | Falls back to a pre-release build when no stable release matches. A version ending in `-dev` does the same. | | |
| 205 | +| `cache`, `cache-dependency-path` | `pip` or `pipenv`. Other values run the real action. See [Dependency caching with setup actions](/docs/actions/caching-and-artifacts#dependency-caching-with-setup-actions). | | |
| 206 | +| `architecture`, `check-latest`, `token`, `update-environment` | Accepted. An `architecture` other than the runner's, or `update-environment: false`, runs the real action. | | |
| 184 | 207 | |
| 185 | -Hosted runners have Python 3.12. A request that it satisfies uses it. Other versions are installed from the `actions/python-versions` builds that GitHub publishes, and the step sets `pythonLocation`, `Python_ROOT_DIR`, `Python3_ROOT_DIR`, `PKG_CONFIG_PATH` and `LD_LIBRARY_PATH` like the original action. | |
| 208 | +Hosted runners have Python 3.12. A request that it satisfies uses it. Other versions are installed from the `actions/python-versions` builds that GitHub publishes, and the step sets `pythonLocation`, `Python_ROOT_DIR`, `Python3_ROOT_DIR`, `PKG_CONFIG_PATH` and `LD_LIBRARY_PATH` like the original action. Other inputs, such as `freethreaded`, run the real action. | |
| 186 | 209 | |
| 187 | 210 | Outputs: `python-version`, `python-path` and `cache-hit`. |
| 188 | 211 | |
| 189 | 212 | ### actions/setup-go |
| 190 | 213 | |
| 191 | -| Input | Support | Notes | | |
| 192 | -| --- | --- | --- | | |
| 193 | -| `go-version` | Supported | A version or range, `stable` or `oldstable`. | | |
| 194 | -| `go-version-file` | Supported | `go.mod` or `go.work` (`toolchain` line first, then `go`), `.tool-versions` (`golang` or `go`), or a file whose first line is the version. | | |
| 195 | -| `check-latest` | Supported | | | |
| 196 | -| `cache`, `cache-dependency-path` | Supported | On by default. Skipped with `go.sum not found; skipping dependency cache.` when there's no `go.sum`. | | |
| 197 | -| `architecture`, `token` | Ignored | | | |
| 214 | +| Input | Notes | | |
| 215 | +| --- | --- | | |
| 216 | +| `go-version` | A version or range, `stable` or `oldstable`. | | |
| 217 | +| `go-version-file` | `go.mod` or `go.work` (`toolchain` line first, then `go`), `.tool-versions` (`golang` or `go`), or a file whose first line is the version. | | |
| 218 | +| `check-latest` | Download the newest matching version. | | |
| 219 | +| `cache`, `cache-dependency-path` | On by default. Without a `go.sum` in the workspace root, the step warns `Restore cache failed: ...` and continues. | | |
| 220 | +| `architecture`, `token` | Accepted. An `architecture` other than the runner's runs the real action. | | |
| 198 | 221 | |
| 199 | 222 | Go isn't preinstalled on hosted runners, so the step downloads it from `go.dev`. Without a version on a runner that has no Go, it fails with `No go-version specified and Go is not installed on this runner.` |
| 200 | 223 | |
| @@ -202,33 +225,36 @@ | ||
| 202 | 225 | |
| 203 | 226 | ### actions/cache and artifacts |
| 204 | 227 | |
| 205 | -`actions/cache`, `actions/cache/restore`, `actions/cache/save`, `actions/upload-artifact` and `actions/download-artifact` support most of their inputs. The main differences are that caches are shared by every branch of the repository, each cache entry or artifact must be under 100 MB compressed, artifacts are `.tar.gz` files, and `download-artifact` needs a `name` and only reads the current run. [Caching and artifacts](/docs/actions/caching-and-artifacts) has the full list. | |
| 228 | +`actions/cache`, `actions/cache/restore`, `actions/cache/save`, `actions/upload-artifact` and `actions/download-artifact` support the official actions' inputs and outputs, including `retention-days`, `overwrite`, `compression-level`, `archive`, downloading all artifacts, `pattern`, `merge-multiple`, `artifact-ids`, and `run-id` with `github-token`. Caches are scoped per ref as on GitHub, each repository keeps up to 10 GB of caches, and artifacts are zip files kept for up to 90 days. [Caching and artifacts](/docs/actions/caching-and-artifacts) has the details. | |
| 206 | 229 | |
| 207 | 230 | ### actions/github-script |
| 208 | 231 | |
| 209 | -`actions/github-script` isn't supported yet. The step fails with: | |
| 232 | +`actions/github-script` isn't a built-in: the real action runs, and its `github` client talks to Diffinery's REST API (`GITHUB_API_URL`) with the job token. What it may do follows the job's `permissions:`: | |
| 210 | 233 | |
| 211 | -```text | |
| 212 | -actions/github-script is not supported on Diffinery runners yet. Use a `run:` step with `node` or `curl` against $GITHUB_API_URL instead. | |
| 213 | -``` | |
| 214 | - | |
| 215 | -Replace it with a `run:` step. For example, instead of reading a pull request with `github.rest.pulls.get`: | |
| 216 | - | |
| 217 | 234 | ```yaml |
| 218 | - - name: Read the pull request | |
| 219 | - env: | |
| 220 | - TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| 221 | - run: | | |
| 222 | - curl -fsSL -H "Authorization: Bearer $TOKEN" \ | |
| 223 | - "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/pulls/${{ github.event.pull_request.number }}" \ | |
| 224 | - | jq '{title, state}' | |
| 235 | +jobs: | |
| 236 | + label: | |
| 237 | + runs-on: ubuntu-latest | |
| 238 | + permissions: | |
| 239 | + issues: write | |
| 240 | + pull-requests: write | |
| 241 | + steps: | |
| 242 | + - uses: actions/github-script@v7 | |
| 243 | + with: | |
| 244 | + script: | | |
| 245 | + await github.rest.issues.addLabels({ | |
| 246 | + owner: context.repo.owner, | |
| 247 | + repo: context.repo.repo, | |
| 248 | + issue_number: context.issue.number, | |
| 249 | + labels: ["needs-review"], | |
| 250 | + }); | |
| 225 | 251 | ``` |
| 226 | 252 | |
| 227 | -The job token can only read. For calls that write, such as adding a comment or a label, store a personal or agent token as a secret and use that instead. See [the REST API reference](/docs/reference/rest-api) for the endpoints Diffinery has. | |
| 253 | +`github.graphql` fails, because Diffinery has no GraphQL API yet. REST calls to endpoints Diffinery doesn't have get a JSON `404 Not Found`. See [the REST API reference](/docs/reference/rest-api) for the endpoints Diffinery has. | |
| 228 | 254 | |
| 229 | 255 | ## Other actions |
| 230 | 256 | |
| 231 | -Any other `uses: owner/repo[/path]@ref` is downloaded from GitHub (`codeload.github.com`) when the step runs and executed according to its `action.yml` or `action.yaml`. | |
| 257 | +Any other `uses: owner/repo[/path]@ref` (and a built-in action the built-in doesn't cover) is downloaded from GitHub (`codeload.github.com`, or with `git` from `github.com` when that endpoint can't be reached) when the step runs and executed according to its `action.yml` or `action.yaml`. | |
| 232 | 258 | |
| 233 | 259 | ### JavaScript actions |
| 234 | 260 | |
| @@ -262,7 +288,7 @@ | ||
| 262 | 288 | - uses: ./.actions/my-action |
| 263 | 289 | ``` |
| 264 | 290 | |
| 265 | -`ACTIONS_READ_TOKEN` is a personal or agent token with read access to `acme/my-action`, stored as a secret, because the job token can't read other repositories. | |
| 291 | +`ACTIONS_READ_TOKEN` is a personal or agent token with read access to `acme/my-action`, stored as a secret, because the job token can't read other private repositories. If `acme/my-action` is public, leave `token` out: the job token can read public repositories. | |
| 266 | 292 | |
| 267 | 293 | ### Unsupported action runtimes |
| 268 | 294 | |
| @@ -272,22 +298,24 @@ | ||
| 272 | 298 | |
| 273 | 299 | ### Actions that call the GitHub API |
| 274 | 300 | |
| 275 | -Actions built on `@actions/github` or Octokit read `GITHUB_API_URL` and `GITHUB_SERVER_URL`, which point at Diffinery, and authenticate with the job token. Diffinery's REST API uses GitHub's paths, JSON shapes, `Link` header pagination and error format, so read-only calls to endpoints Diffinery has work (see [REST API](/docs/reference/rest-api)). Calls to the GraphQL API fail, because GraphQL isn't supported yet. Actions that write to the repository (comment on pull requests, add labels, push commits or tags) fail, because the job token is read-only. Give such actions a personal or agent token from a secret, through their `token` or `github-token` input. | |
| 301 | +Actions built on `@actions/github` or Octokit read `GITHUB_API_URL` and `GITHUB_SERVER_URL`, which point at Diffinery, and authenticate with the job token. Diffinery's REST API uses GitHub's paths, JSON shapes, `Link` header pagination and error format, so calls to endpoints Diffinery has work (see [REST API](/docs/reference/rest-api)), within the job's `permissions:` (see [GITHUB_TOKEN and permissions](#github_token-and-permissions)). Calls to the GraphQL API fail, because GraphQL isn't supported yet. | |
| 276 | 302 | |
| 277 | -### Actions that use the cache or artifact services directly | |
| 303 | +### Actions that use the cache or artifact services | |
| 278 | 304 | |
| 279 | -Diffinery doesn't set `ACTIONS_RUNTIME_TOKEN`, `ACTIONS_RUNTIME_URL`, `ACTIONS_CACHE_URL` or `ACTIONS_RESULTS_URL`. Actions that use the `@actions/cache` or `@actions/artifact` packages directly, rather than through the built-in actions, can't reach a cache or artifact service. This includes Docker BuildKit's `type=gha` cache. Use the built-in actions, or the setup actions' `cache` input, instead. | |
| 305 | +Diffinery sets `ACTIONS_RUNTIME_TOKEN`, `ACTIONS_RESULTS_URL`, `ACTIONS_CACHE_URL`, `ACTIONS_RUNTIME_URL` and `ACTIONS_CACHE_SERVICE_V2` for JavaScript actions, Docker container actions and the built-ins (not for `run:` steps, as on GitHub). Actions that use the `@actions/cache` or `@actions/artifact` packages directly, and `docker/build-push-action` with `cache-from: type=gha` and `cache-to: type=gha`, store their caches and artifacts in Diffinery. See [The runtime services](/docs/actions/caching-and-artifacts#the-runtime-services). | |
| 280 | 306 | |
| 307 | +The official `actions/upload-artifact` and `actions/download-artifact` refuse to run on a server that looks like GitHub Enterprise Server, so the runner shows them `GITHUB_SERVER_URL=https://github.com`. Their API calls still go to Diffinery, and their `artifact-url` output is rewritten to point at Diffinery. | |
| 308 | + | |
| 281 | 309 | ## Contexts and environment variables |
| 282 | 310 | |
| 283 | 311 | | Value | GitHub | Diffinery | |
| 284 | 312 | | --- | --- | --- | |
| 285 | 313 | | `github.server_url`, `GITHUB_SERVER_URL` | `https://github.com` | `https://diffinery.com` | |
| 286 | 314 | | `github.api_url`, `GITHUB_API_URL` | `https://api.github.com` | `https://diffinery.com/api/v3` | |
| 287 | -| `github.run_id`, `GITHUB_RUN_ID` | A number | A 32-character hexadecimal string | | |
| 315 | +| `github.run_id`, `GITHUB_RUN_ID` | A number, unique per run | A number: the run's number, the same as `github.run_number`. The API accepts it wherever it takes a run id. | | |
| 288 | 316 | | `github.repository_id` | A number | Diffinery's repository ID, a string | |
| 289 | 317 | | `github.ref_protected` | `true` for protected branches | Always `false` | |
| 290 | -| `github.event.repository.private` | The repository's visibility | Always `true` (`release` payloads have no `private` field) | | |
| 318 | +| `github.event.repository` | GitHub's repository object | The same object, with Diffinery's values; `id` is numeric and `private` and `visibility` reflect the repository | | |
| 291 | 319 | | `github.actor` for scheduled runs | The last person who edited the cron | `diffinery-scheduler` | |
| 292 | 320 | | `github.token`, `secrets.GITHUB_TOKEN` | The job token | The job token, also available as `secrets.DIFFINERY_TOKEN` | |
| 293 | 321 | | `github.actor_id`, `github.repository_owner_id`, `github.workflow_sha` | Set | Not set | |
| @@ -307,27 +335,36 @@ | ||
| 307 | 335 | |
| 308 | 336 | ## GITHUB_TOKEN and permissions |
| 309 | 337 | |
| 310 | -Every job gets a token, available as `secrets.GITHUB_TOKEN`, `secrets.DIFFINERY_TOKEN` and `github.token`. On Diffinery it: | |
| 338 | +Every job gets a token, available as `secrets.GITHUB_TOKEN`, `secrets.DIFFINERY_TOKEN` and `github.token`. As on GitHub: | |
| 311 | 339 | |
| 312 | -- has read access to the workflow's own repository only, whatever `permissions` says. It can clone and read through the API, but it can't push, comment, label or merge, and it can't read other repositories, even public ones. | |
| 313 | -- works with the [container registry](/docs/packages/container-registry) according to the `packages` permission. | |
| 314 | -- can request OIDC tokens when `id-token: write` is granted. | |
| 315 | -- stops working when the job completes. | |
| 340 | +- Its permissions come from the job's `permissions:`, else the workflow's, else the repository's default. `read-all`, `write-all`, `{}` and per-scope maps work as on GitHub, and a map sets every scope it doesn't list to `none`. | |
| 341 | +- Each REST endpoint needs its scope (`contents`, `issues`, `pull-requests`, `statuses`, `checks`, `deployments`, `actions`, `packages` and so on). A request without it gets `403 Resource not accessible by integration (the workflow's GITHUB_TOKEN needs permissions: <scope>: <level>)`. | |
| 342 | +- It never administers the repository, never reads or changes secrets and variables, and never creates or changes workflow files. Branch protection applies to it. | |
| 343 | +- It can open and approve pull requests only when the repository allows it, and its approvals don't count toward required reviews. | |
| 344 | +- It can read any public repository on Diffinery, but no other private repository. | |
| 345 | +- What it does starts no new runs, except `workflow_dispatch`. | |
| 346 | +- It stops working when the job completes. | |
| 316 | 347 | |
| 317 | -Other `permissions` scopes are accepted and have no effect. For anything that writes to a repository, store a personal access token or an agent token as a secret. See [GITHUB_TOKEN and the job token](/docs/actions/secrets-and-variables#github_token-and-the-job-token). | |
| 348 | +Differences: | |
| 318 | 349 | |
| 350 | +- New repositories default to read-only tokens (`contents` and `packages` read), as GitHub does for new repositories. Repositories created before this setting existed default to read and write. An admin changes it on the repository's **Settings → Actions** tab or with `PUT /repos/{owner}/{repo}/actions/permissions/workflow`. | |
| 351 | +- A run whose code came from an agent is held to the agent rules, so its token can't push or merge to the default branch or protected branches. | |
| 352 | + | |
| 353 | +[GITHUB_TOKEN permissions](/docs/actions/workflow-permissions) has the full model, the scope each endpoint needs, and how to change the default. | |
| 354 | + | |
| 319 | 355 | ## Secrets, variables and environments |
| 320 | 356 | |
| 321 | 357 | | Feature | Status | |
| 322 | 358 | | --- | --- | |
| 323 | 359 | | Repository secrets and variables | Supported | |
| 324 | 360 | | Organization secrets | Supported (CLI and API) | |
| 325 | -| Organization variables | Not available | | |
| 361 | +| Organization variables | Supported, through the REST API. Every repository in the organization sees them (no `private` or `selected` visibility). See [Organization variables](/docs/actions/secrets-and-variables#organization-variables). | | |
| 326 | 362 | | Environments with deployment branch policies (protected branches only, or branch and tag patterns) | Supported, through the REST API. See [Environments](/docs/actions/environments). | |
| 327 | 363 | | Environment secrets and variables | Supported, through the REST API. They win over repository and organization values with the same name. Pull request runs never get environment secrets, and runs started by agents get them only from environments limited to protected branches. | |
| 328 | 364 | | Environment protection rules: required reviewers, wait timers | Not supported yet. The API refuses them with `required reviewers and wait timers are not supported yet`. | |
| 329 | 365 | | Setting secrets with a sealed-box `encrypted_value` | Not supported yet. Send the plain `value` over HTTPS; it's encrypted at rest. | |
| 330 | 366 | | Secrets in `pull_request` runs | Provided, because every pull request comes from a branch of the same repository | |
| 367 | +| Changing secrets and variables with an agent's token | Refused: only people manage secrets and variables | | |
| 331 | 368 | | Secret names starting with `GITHUB_` | Rejected for repository secrets, as on GitHub | |
| 332 | 369 | |
| 333 | 370 | See [Secrets and variables](/docs/actions/secrets-and-variables). |
| @@ -344,24 +381,17 @@ | ||
| 344 | 381 | |
| 345 | 382 | 1. Run `diffinery workflow validate` on each file and fix any errors. Read the warnings for ignored keys. |
| 346 | 383 | 2. Push and watch the first run with `diffinery run watch` or on the **Actions** tab. |
| 347 | -3. Replace `runs-on: windows-*` and `macos-*` with Linux, or with `[self-hosted, ...]` labels and a [self-hosted runner](/docs/actions/runners#self-hosted-runners). | |
| 348 | -4. Replace `runs-on: ${{ matrix.os }}` with separate jobs, one per runner type. | |
| 349 | -5. Remove `sudo`, and add `apt-get update` before `apt-get install`. | |
| 350 | -6. Install tools the hosted image doesn't have (Go, Java, Ruby, Rust, PowerShell, cloud CLIs) with a setup action or `apt-get`. | |
| 351 | -7. Replace `actions/github-script` with `run:` steps using `curl` or `node`. | |
| 352 | -8. Find every step that writes with `GITHUB_TOKEN` (git push, tags, comments, labels, merges) and give it a personal or agent token from a secret. | |
| 353 | -9. Give `actions/checkout` of another repository a token from a secret. | |
| 354 | -10. Recreate your environments, their branch policies, secrets and variables through the REST API (see [Environments](/docs/actions/environments)), and remove any reliance on environment approvals or wait timers. | |
| 355 | -11. Add `git lfs pull` after `actions/checkout` if the job needs LFS files. | |
| 356 | -12. Change `download-artifact` steps without `name` into one step per artifact, and make artifact names unique across matrix jobs. | |
| 357 | -13. Keep each cache entry and artifact under 100 MB compressed. | |
| 358 | -14. Replace actions that use the cache or artifact toolkit directly, and BuildKit `type=gha` caches. | |
| 359 | -15. Replace `ghcr.io` with `diffinery.com` in image names and logins; see [Container registry](/docs/packages/container-registry#migrating-from-ghcrio). | |
| 360 | -16. Replace hard-coded `github.com` and `api.github.com` URLs with `${{ github.server_url }}` and `${{ github.api_url }}`. | |
| 361 | -17. Update OIDC trust policies in your cloud accounts to the Diffinery issuer and subject format. | |
| 362 | -18. Move job-level `concurrency` to the workflow level if you rely on it, and replace job-level `continue-on-error` with step-level `continue-on-error` or `if: always()` on dependent jobs. | |
| 363 | -19. Check jobs that depend on jobs which can be skipped: on Diffinery they run instead of being skipped. | |
| 364 | -20. Move actions from private GitHub repositories into a Diffinery repository and use them as local actions. | |
| 384 | +3. Replace `runs-on: windows-*` and `macos-*` with Linux, or with `[self-hosted, ...]` labels and a [self-hosted runner](/docs/actions/runners#self-hosted-runners). This includes `matrix.os` values used in `runs-on`. | |
| 385 | +4. Remove `sudo`, and add `apt-get update` before `apt-get install`. | |
| 386 | +5. Install tools the hosted image doesn't have (Go, Java, Ruby, Rust, PowerShell, cloud CLIs) with a setup action or `apt-get`. | |
| 387 | +6. Check what each job does with `GITHUB_TOKEN`. In a new repository the default token only reads, so give jobs that push, tag, comment, label or merge the `permissions:` they need, or have an admin set the repository default to write (see [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions)). Jobs that change workflow files, or that open or approve pull requests without the repository allowing it, need a personal or agent token from a secret. | |
| 388 | +7. Give `actions/checkout` of another private repository a token from a secret. | |
| 389 | +8. Recreate your environments, their branch policies, secrets and variables through the REST API (see [Environments](/docs/actions/environments)), and remove any reliance on environment approvals or wait timers. | |
| 390 | +9. Replace `github.graphql` calls in `actions/github-script` with REST calls. | |
| 391 | +10. Replace `ghcr.io` with `diffinery.com` in image names and logins; see [Container registry](/docs/packages/container-registry#migrating-from-ghcrio). | |
| 392 | +11. Replace hard-coded `github.com` and `api.github.com` URLs with `${{ github.server_url }}` and `${{ github.api_url }}`. | |
| 393 | +12. Update OIDC trust policies in your cloud accounts to the Diffinery issuer and subject format. | |
| 394 | +13. Move actions from private GitHub repositories into a Diffinery repository and use them as local actions. | |
| 365 | 395 | |
| 366 | 396 | [Migrate from GitHub](/docs/migrate-from-github#workflows) covers moving the rest of the repository. |
| 367 | 397 | |
| @@ -369,20 +399,20 @@ | ||
| 369 | 399 | |
| 370 | 400 | | Message | Cause and fix | |
| 371 | 401 | | --- | --- | |
| 372 | -| `No Diffinery runner matches runs-on: <labels>. ...` | The labels aren't hosted Linux labels and don't include `self-hosted`. Use `ubuntu-latest`, or `[self-hosted, ...]` with a registered runner. A `matrix` value in `runs-on` causes this too. | | |
| 373 | -| `actions/github-script is not supported on Diffinery runners yet. ...` | Replace the step with a `run:` step. See [actions/github-script](#actionsgithub-script). | | |
| 402 | +| `No Diffinery runner matches runs-on: <labels>. ...` | The labels aren't hosted Linux labels and don't include `self-hosted`. Use `ubuntu-latest`, or `[self-hosted, ...]` with a registered runner. For a matrix job, check the value `runs-on` got for that combination. | | |
| 403 | +| `Error evaluating the job for this matrix combination: <message>` | An expression in `runs-on`, `name`, `timeout-minutes`, `continue-on-error` or `concurrency` failed for one matrix combination. | | |
| 404 | +| `Resource not accessible by integration (the workflow's GITHUB_TOKEN needs permissions: ...)` | Add the scope to the job's `permissions:`. See [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions). | | |
| 405 | +| `refusing to allow a GitHub App to create or update workflow ...` | Job tokens never change workflow files. Use a personal or agent token from a secret. | | |
| 406 | +| `GitHub Actions is not permitted to create pull requests. ...` | An admin allows it with `can_approve_pull_request_reviews`, and the job needs `pull-requests: write`. | | |
| 374 | 407 | | `Unable to resolve action '<owner>/<repo>@<ref>', repository or version not found` | The action or version doesn't exist on GitHub, or the repository is private. | |
| 375 | 408 | | `Can't find 'action.yml' or 'action.yaml' under '<dir>'. ...` | Check out the repository before using a local action, and check the path. | |
| 376 | 409 | | `Unsupported action runtime 'runs.using: <value>' in <action>` | The action uses a runtime other than `node*`, `composite` or `docker`. | |
| 377 | 410 | | ``The `cmd` shell is not supported on Linux runners.`` | Use `bash` or `sh`. | |
| 378 | 411 | | `sudo: command not found` | Jobs run as root on hosted runners. Remove `sudo`. | |
| 379 | -| `Failed to fetch ref '<ref>' from <owner>/<repo>. Check that the ref exists and the token has read access.` | Checking out another repository needs a token from a secret, or the ref doesn't exist. | | |
| 380 | -| `Git LFS is not supported by the Diffinery checkout built-in; ...` | Add a `git lfs pull` step after the checkout. | | |
| 412 | +| `Failed to fetch ref '<ref>' from <owner>/<repo>. Check that the ref exists and the token has read access.` | Checking out another private repository needs a token from a secret, the job lacks `contents: read`, or the ref doesn't exist. | | |
| 381 | 413 | | `No go-version specified and Go is not installed on this runner.` | Set `go-version` or `go-version-file`. | |
| 382 | -| `'<version>' is not supported by the Diffinery setup-python built-in.` | PyPy and GraalPy aren't available. Use CPython, or install PyPy in a step. | | |
| 383 | -| ``download-artifact without `name` ... is not supported on Diffinery runners yet; specify `name`.`` | Add `name`, one step per artifact. | | |
| 384 | 414 | | `jobs.<id>: a matrix on a job that calls a reusable workflow is not supported yet` | Move the matrix into the called workflow, or write one calling job per value. | |
| 385 | 415 | | `A matrix may produce at most 256 jobs.` | Split the matrix. | |
| 386 | 416 | | `Composite action nesting is too deep (max 10).` | Flatten nested composite actions. | |
| 387 | -| A `git push` with `GITHUB_TOKEN` is refused | The job token is read-only. Use a personal or agent token from a secret. | | |
| 388 | -| A job runs although a job it needs was skipped | A skipped job counts as a success on Diffinery. Check `needs.<job>.result == 'success'` in `if`. | | |
| 417 | +| A built-in action behaves differently from the real one | Run the real action: set `DIFFINERY_BUILTINS: false` in the step's `env:`, or pin the action to a commit SHA. | | |
| 418 | +| A job is skipped although its `if:` looks true | A job further up its `needs` chain failed, was cancelled or was skipped, and the `if:` doesn't call a status function. Use `if: always() && ...` or `if: ${{ !cancelled() && !failure() }}`. | |
modifiedapps/diffinery/docs/actions/docker.md+19−4View file
| @@ -57,7 +57,7 @@ | ||
| 57 | 57 | 3. The runner's work directory is mounted into the container at the same path, so `GITHUB_WORKSPACE`, `$GITHUB_OUTPUT`, `$GITHUB_ENV` and the other file commands work unchanged. `HOME` is `/github/home` inside the container. |
| 58 | 58 | 4. Each `run:` step is executed with `docker exec` in the container, with the step's environment. The default shell is `bash` if the image has it, otherwise `sh`. `PATH` comes from the image unless the workflow sets `PATH`. |
| 59 | 59 | 5. JavaScript actions run inside the container with the runner's own Node.js binary, mounted at `/__e/node`. That binary needs glibc, so JavaScript actions don't work in musl-based images such as Alpine (the same limitation GitHub has). |
| 60 | -6. The built-in actions (`actions/checkout`, `actions/cache`, `actions/upload-artifact`, `actions/download-artifact`, `actions/setup-node`, `actions/setup-python`, `actions/setup-go`) run on the runner itself and work on the shared workspace. | |
| 60 | +6. The built-in actions (`actions/checkout`, `actions/cache`, `actions/upload-artifact`, `actions/download-artifact`, `actions/setup-node`, `actions/setup-python`, `actions/setup-go`) run on the runner itself and work on the shared workspace. When the runner uses the real action instead (for a version or input the built-in doesn't cover, see [Compatibility with GitHub Actions](/docs/actions/compatibility#built-in-actions)), it runs like any other JavaScript action, inside the container. | |
| 61 | 61 | 7. The Docker socket is mounted into the job container when the runner has one at `/var/run/docker.sock`, so a step in the container can use Docker if the image includes the `docker` CLI. |
| 62 | 62 | 8. When the job ends, every container the job started and its network are removed. |
| 63 | 63 | |
| @@ -178,19 +178,33 @@ | ||
| 178 | 178 | |
| 179 | 179 | `docker/setup-buildx-action`, `docker/login-action`, `docker/metadata-action` and `docker/build-push-action` also work. On hosted runners, `docker buildx create` with the `docker-container` driver (what `docker/setup-buildx-action` runs) starts BuildKit on the host network and registers it as a remote builder, because the regular driver can't run inside a Cloudflare Container. You don't need to change your workflow for this. |
| 180 | 180 | |
| 181 | -> **Note:** BuildKit's `type=gha` cache (`cache-from: type=gha`, `cache-to: type=gha`) needs GitHub's cache service, which Diffinery doesn't provide. Remove those lines when you port a workflow. | |
| 181 | +BuildKit's `type=gha` cache works with `docker/build-push-action`. The action runs with the job's cache service credentials and hands them to BuildKit, and the layers are stored in the repository's Actions cache, with the same scope, size limit and eviction as `actions/cache`. See [Caching and artifacts](/docs/actions/caching-and-artifacts). | |
| 182 | 182 | |
| 183 | +```yaml | |
| 184 | +- uses: docker/setup-buildx-action@v3 | |
| 185 | +- uses: docker/build-push-action@v6 | |
| 186 | + with: | |
| 187 | + context: . | |
| 188 | + push: false | |
| 189 | + cache-from: type=gha | |
| 190 | + cache-to: type=gha,mode=max | |
| 191 | +``` | |
| 192 | + | |
| 193 | +> **Note:** The cache service credentials (`ACTIONS_RUNTIME_TOKEN` and `ACTIONS_RESULTS_URL`) are given only to JavaScript actions, Docker container actions and the built-in actions, never to `run:` steps. `docker buildx build --cache-to type=gha` in a `run:` step has no credentials, so use `docker/build-push-action` for the gha cache. | |
| 194 | + | |
| 183 | 195 | ## Push images to the Diffinery registry |
| 184 | 196 | |
| 185 | 197 | Diffinery hosts a container registry at `diffinery.com`, in place of ghcr.io. A workflow signs in with its own `GITHUB_TOKEN`; the job's `packages` permission decides what the token can do: |
| 186 | 198 | |
| 187 | 199 | | `permissions` in the workflow or job | `GITHUB_TOKEN` can | |
| 188 | 200 | |---|---| |
| 189 | -| No `permissions` block, or `write-all` | Push and pull | | |
| 190 | -| `packages: write` | Push and pull | | |
| 201 | +| No `permissions` block | What the repository default allows: pull for repositories with the `read` default (the default for new repositories), push and pull for those with `write` | | |
| 202 | +| `write-all`, or `packages: write` | Push and pull | | |
| 191 | 203 | | `packages: read`, or `read-all` | Pull | |
| 192 | 204 | | A mapping without `packages` | Nothing | |
| 193 | 205 | |
| 206 | +A job's own `permissions:` replaces the workflow's. To push from a repository with the `read` default, add `packages: write` to the job or the workflow. See [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions). | |
| 207 | + | |
| 194 | 208 | With write, the token pushes only to packages linked to the workflow's own repository, and creates new packages under the repository's owner (linked to the repository). Other packages of the same owner, including ones that aren't linked to any repository, are pull-only for it. |
| 195 | 209 | |
| 196 | 210 | Image names are `diffinery.com/<owner>/<name>` and must be lowercase. `github.repository` keeps the owner's and repository's capitals, so lowercase it in a shell step with `${GITHUB_REPOSITORY,,}`. |
| @@ -375,3 +389,4 @@ | ||
| 375 | 389 | | `the --network option is not supported` | Remove `--network` from `options`. Services are already on the job's network. | |
| 376 | 390 | | A JavaScript action fails inside an Alpine job container | The runner's Node.js needs glibc. Use a Debian or Ubuntu based image, or run the action outside the container. | |
| 377 | 391 | | A registry error ending in `the workflow's GITHUB_TOKEN has no packages permission; add permissions: packages: write` | The job's `permissions` mapping leaves out `packages`. Add `packages: write` to push, or `packages: read` to pull. | |
| 392 | +| A push fails with a registry error ending in `the workflow's GITHUB_TOKEN has packages: read; add permissions: packages: write to push` | The job has only read access to packages, often because it has no `permissions:` and the repository default is `read`. Add `packages: write`. | |
modifiedapps/diffinery/docs/actions/environments.md+48−22View file
| @@ -21,7 +21,7 @@ | ||
| 21 | 21 | 1. A repository admin creates `production`, limits it to protected branches, and stores the deploy credentials as environment secrets. |
| 22 | 22 | 2. The deploy job in a workflow says `environment: production`. |
| 23 | 23 | 3. When the job starts on `main` (a protected branch), Diffinery records a deployment with an `in_progress` status, and the job gets the `production` secrets and variables. |
| 24 | -4. When the job finishes, the deployment gets a final status (`success`, `failure` or `inactive`), and webhooks receive `deployment_status` events. | |
| 24 | +4. When the job finishes, the deployment gets a final status (`success`, `failure` or `inactive`). Webhooks receive a `deployment` event when the deployment is created and `deployment_status` events for each status. | |
| 25 | 25 | |
| 26 | 26 | There's no web page for environments or deployments yet. You manage them with the REST API, which follows GitHub's paths and JSON shapes, so `gh api`, Octokit and existing scripts work after you point them at `https://diffinery.com/api/v3`. |
| 27 | 27 | |
| @@ -38,7 +38,7 @@ | ||
| 38 | 38 | ### Environment names |
| 39 | 39 | |
| 40 | 40 | - 1 to 255 characters, with no control characters, `/` or `\`. Anything else fails with `422 invalid environment name`. |
| 41 | -- Names are case-insensitive: `Production` and `production` are the same environment. | |
| 41 | +- Names are case-insensitive: `Production` and `production` are the same environment. Once an environment exists, deployments, statuses and jobs that name it in another case use its existing spelling, and filtering deployments with `?environment=` ignores case. | |
| 42 | 42 | - URL-encode the name in API paths, for example `environments/QA%20cluster`. |
| 43 | 43 | |
| 44 | 44 | ### Create one with the API |
| @@ -67,7 +67,7 @@ | ||
| 67 | 67 | "node_id": "RW52aXJvbm1lbnQ6YWNtZS93ZWJzaXRlLzE", |
| 68 | 68 | "name": "production", |
| 69 | 69 | "url": "https://diffinery.com/api/v3/repos/acme/website/environments/production", |
| 70 | - "html_url": "https://diffinery.com/acme/website/deployments/activity_log?environments_filter=production", | |
| 70 | + "html_url": "https://diffinery.com/acme/website/actions", | |
| 71 | 71 | "created_at": "2026-10-13T09:00:00Z", |
| 72 | 72 | "updated_at": "2026-10-13T09:00:00Z", |
| 73 | 73 | "can_admins_bypass": true, |
| @@ -80,7 +80,7 @@ | ||
| 80 | 80 | |
| 81 | 81 | - `protection_rules` holds one `branch_policy` entry when the environment has a branch policy, and is empty otherwise. Required reviewers and wait timers don't exist (see [What isn't supported](#what-isnt-supported)). |
| 82 | 82 | - `can_admins_bypass` is always `true` for compatibility, but nobody bypasses the branch policy: it applies to every run, whoever started it. |
| 83 | -- `html_url` is returned for compatibility. Diffinery has no web page for environments yet, so it doesn't open anything useful. | |
| 83 | +- `html_url` points to the repository's Actions page. Diffinery has no web page for environments yet; deployments made by workflow jobs show on the runs that made them. | |
| 84 | 84 | |
| 85 | 85 | `PUT` on an existing environment updates it. Fields you leave out keep their values. The body accepts: |
| 86 | 86 | |
| @@ -133,7 +133,7 @@ | ||
| 133 | 133 | |
| 134 | 134 | A branch counts as protected when one of the repository's branch protection rules matches its name (see [Branch protection](/docs/repositories/branch-protection)). The default branch is only protected for this purpose when a rule matches it, so add a rule for `main` if production deploys from `main`. |
| 135 | 135 | |
| 136 | -This is the setting to use for environments whose secrets matter: changes reach protected branches through reviewed pull requests, and it's the only setting under which runs started by agents receive environment secrets. | |
| 136 | +This is the setting to use for environments whose secrets matter: changes reach protected branches through reviewed pull requests, and it's the only setting under which runs whose code came from an agent receive environment secrets. | |
| 137 | 137 | |
| 138 | 138 | ### Selected branches and tags |
| 139 | 139 | |
| @@ -235,7 +235,7 @@ | ||
| 235 | 235 | | `GET /api/v3/repos/{owner}/{repo}/environments/{env}/variables` | write role | Returns `{"total_count": 1, "variables": [{"name", "value", "created_at", "updated_at"}]}`. `per_page` defaults to 10 (at most 100). | |
| 236 | 236 | | `GET /api/v3/repos/{owner}/{repo}/environments/{env}/variables/{name}` | write role | Returns one variable, or `404 Not Found`. | |
| 237 | 237 | | `POST /api/v3/repos/{owner}/{repo}/environments/{env}/variables` with `{"name": "...", "value": "..."}` | admin role, people only | Creates a variable (`201`). An existing name fails with `409 Already exists - Variable already exists`. | |
| 238 | -| `PATCH /api/v3/repos/{owner}/{repo}/environments/{env}/variables/{name}` with `{"value": "..."}` and optionally a new `"name"` | admin role, people only | Updates or renames a variable (`204`). | | |
| 238 | +| `PATCH /api/v3/repos/{owner}/{repo}/environments/{env}/variables/{name}` with `{"value": "..."}` and optionally a new `"name"` | admin role, people only | Updates or renames a variable (`204`), or `404 Not Found`. Renaming onto a name that already exists fails with `409 Already exists - Variable already exists`. | | |
| 239 | 239 | | `DELETE /api/v3/repos/{owner}/{repo}/environments/{env}/variables/{name}` | admin role, people only | Deletes a variable (`204`). | |
| 240 | 240 | |
| 241 | 241 | ```sh |
| @@ -253,14 +253,24 @@ | ||
| 253 | 253 | |
| 254 | 254 | 1. The environment's branch policy allows the job's ref (otherwise the job fails, see [Deployment branch policies](#deployment-branch-policies)). |
| 255 | 255 | 2. The run isn't on a pull request ref (`refs/pull/<number>/merge`). Environment secrets never go to `pull_request` runs, whatever the policy says. |
| 256 | -3. If the run was started by an agent (for example by an agent's push or workflow dispatch), the environment is set to **protected branches only**. With no policy or with custom patterns, runs started by agents get no environment secrets. | |
| 256 | +3. If the run's code came from an agent, the environment is set to **protected branches only**. With no policy or with custom patterns, those runs get no environment secrets. | |
| 257 | 257 | |
| 258 | +A run's code came from an agent when any of these is true: | |
| 259 | + | |
| 260 | +- An agent started the run, for example with a push, a `workflow_dispatch` or a re-run. | |
| 261 | +- The run is for a branch whose last push was made by an agent. | |
| 262 | +- The run is for a pull request that an agent opened, or whose head branch an agent pushed to last. | |
| 263 | +- The run is a re-run of a run whose code came from an agent, whoever starts the re-run. | |
| 264 | +- The run was started by `workflow_run` after a run whose code came from an agent (it acts on that run's artifacts and outputs). | |
| 265 | + | |
| 266 | +In such a run the job also acts as an agent: its deployment's creator is recorded as an agent, and its `GITHUB_TOKEN` follows the rules for agents (see [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions#runs-whose-code-came-from-an-agent)). | |
| 267 | + | |
| 258 | 268 | When a rule withholds the secrets, the job still runs with the environment's variables, and `${{ secrets.NAME }}` for an environment secret evaluates to an empty string. |
| 259 | 269 | |
| 260 | 270 | | Run | No policy | Protected branches only | Selected branches and tags | |
| 261 | 271 | |---|---|---|---| |
| 262 | 272 | | Started by a person, on an allowed branch or tag | Secrets | Secrets | Secrets | |
| 263 | -| Started by an agent, on an allowed branch | No secrets | Secrets | No secrets | | |
| 273 | +| Code from an agent, on an allowed branch | No secrets | Secrets | No secrets | | |
| 264 | 274 | | `pull_request` run (`refs/pull/<n>/merge`) | Job runs, no secrets | Job fails (ref not allowed) | Job fails (ref not allowed) | |
| 265 | 275 | | Ref not allowed by the policy | Not applicable | Job fails | Job fails | |
| 266 | 276 | |
| @@ -309,10 +319,10 @@ | ||
| 309 | 319 | | `name` | The environment's name (required in the mapping form). Expressions are allowed. | |
| 310 | 320 | | `url` | Optional. Stored as the deployment status's `environment_url` when it's an `http://` or `https://` URL; other values are ignored. Expressions are allowed. | |
| 311 | 321 | |
| 312 | -Both values can use the `github`, `inputs`, `vars` and `matrix` contexts. Other contexts evaluate to empty: | |
| 322 | +Both values can use the `github`, `inputs`, `vars`, `matrix`, `strategy` and `needs` contexts. Other contexts evaluate to empty: | |
| 313 | 323 | |
| 314 | 324 | - A `name` that evaluates to an empty string fails the job with `invalid environment name ''`. |
| 315 | -- A `url` that refers to `steps` (for example `${{ steps.deploy.outputs.url }}`, which GitHub allows) is ignored, so the deployment has no environment URL. Compute the URL from `inputs`, `vars` or `matrix` instead, or post a deployment status yourself (see [Deployment statuses](#deployment-statuses)). | |
| 325 | +- A `url` that refers to `steps` (for example `${{ steps.deploy.outputs.url }}`, which GitHub allows) is ignored, so the deployment has no environment URL. Compute the URL from `inputs`, `vars`, `matrix` or a job output in `needs` instead, or post a deployment status yourself (see [Deployment statuses](#deployment-statuses)). | |
| 316 | 326 | |
| 317 | 327 | ```yaml |
| 318 | 328 | jobs: |
| @@ -337,8 +347,8 @@ | ||
| 337 | 347 | 1. Evaluates `name` and `url`. |
| 338 | 348 | 2. Creates the environment if it doesn't exist (with no branch policy). |
| 339 | 349 | 3. Checks the branch policy against the run's ref, and fails the job if the ref isn't allowed. |
| 340 | -4. Creates a deployment for the job: `ref` is the branch or tag name (or `refs/pull/<n>/merge`), `sha` is the commit the run checked out, `task` is `deploy`, `payload` is `{}`, the creator is the person or agent who started the run, and `production_environment` is `true` when the environment is named `production`. | |
| 341 | -5. Adds an `in_progress` status, with `log_url` set to the job's page (`https://diffinery.com/acme/website/actions/runs/<run id>/job/<job id>`) and `environment_url` from `url`. | |
| 350 | +4. Creates a deployment for the job: `ref` is the branch or tag name (or `refs/pull/<n>/merge`), `sha` is the commit the run checked out, `task` is `deploy`, `payload` is `{}`, the creator is the person or agent who started the run (an agent when the run's code came from an agent), and `production_environment` is `true` when the environment is named `production` in any case (`Production` counts). | |
| 351 | +5. Sends a `deployment` webhook event with the action `created`, then adds an `in_progress` status, with `log_url` set to the job's page (`https://diffinery.com/acme/website/actions/runs/<run id>/job/<job id>`) and `environment_url` from `url`. | |
| 342 | 352 | 6. Passes the environment's variables to the job, and its secrets when [the rules](#who-receives-environment-secrets) allow it. |
| 343 | 353 | |
| 344 | 354 | A job creates one deployment, even if its runner fetches the job more than once. A re-run creates a new deployment, because it's a new job. The deployment's commit statuses and checks aren't required to have passed, unlike [deployments created with the API](#required-contexts). |
| @@ -353,7 +363,7 @@ | ||
| 353 | 363 | | `cancelled` or `skipped` | `inactive` | |
| 354 | 364 | | Anything else (`failure`, timed out) | `failure` | |
| 355 | 365 | |
| 356 | -A `success` status also marks earlier successful deployments to the same environment as `inactive`, unless they are transient or production deployments. See [auto_inactive](#deployment-statuses). | |
| 366 | +A `success` status also marks older deployments to the same environment as `inactive` when their latest status is `success`, unless they are transient or production deployments. Deployments created after this one are left alone. See [auto_inactive](#deployment-statuses). | |
| 357 | 367 | |
| 358 | 368 | ### Reusable workflows |
| 359 | 369 | |
| @@ -491,8 +501,25 @@ | ||
| 491 | 501 | |
| 492 | 502 | You can create deployments yourself, for example from a deploy tool outside Diffinery Actions, and report their progress with statuses. The endpoints follow GitHub's REST API. Creating deployments and statuses needs the write role and a token with the `write` scope; reading needs read access. Agents with write access can create deployments and statuses. |
| 493 | 503 | |
| 494 | -A workflow's `GITHUB_TOKEN` is read-only, so a step can't create deployments with it (`403 write access required`). Use `environment:` on the job, which records the deployment for you, or a personal token stored as a secret. | |
| 504 | +A workflow step can create deployments and statuses with `GITHUB_TOKEN` when the job has `deployments: write` in its `permissions:`. Without it, the request fails with `403 Resource not accessible by integration (the workflow's GITHUB_TOKEN needs permissions: deployments: write)`, or `403 write access required` when the token has no write scope at all. Often `environment:` on the job is simpler, because it records the deployment for you. See [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions). | |
| 495 | 505 | |
| 506 | +```yaml | |
| 507 | +jobs: | |
| 508 | + deploy: | |
| 509 | + runs-on: ubuntu-latest | |
| 510 | + permissions: | |
| 511 | + contents: read | |
| 512 | + deployments: write | |
| 513 | + steps: | |
| 514 | + - run: | | |
| 515 | + curl -fsS -X POST "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/deployments" \ | |
| 516 | + -H "Authorization: Bearer $GITHUB_TOKEN" \ | |
| 517 | + -H "Content-Type: application/json" \ | |
| 518 | + -d '{"ref": "${{ github.sha }}", "environment": "preview", "required_contexts": []}' | |
| 519 | + env: | |
| 520 | + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| 521 | +``` | |
| 522 | + | |
| 496 | 523 | ### Create a deployment |
| 497 | 524 | |
| 498 | 525 | ```sh |
| @@ -511,7 +538,7 @@ | ||
| 511 | 538 | | `description` | `null` | Free text. Only the first 1,000 characters are kept. | |
| 512 | 539 | | `required_contexts` | every context | See [Required contexts](#required-contexts). Up to 100 strings. | |
| 513 | 540 | | `transient_environment` | `false` | Marks the environment as temporary (it's skipped by `auto_inactive`). | |
| 514 | -| `production_environment` | `true` when `environment` is `production` | Marks a production deployment (it's skipped by `auto_inactive`). | | |
| 541 | +| `production_environment` | `true` when `environment` is `production`, in any case | Marks a production deployment (it's skipped by `auto_inactive`). | | |
| 515 | 542 | | `auto_merge` | | Accepted and ignored. Diffinery never merges the default branch into `ref`. | |
| 516 | 543 | |
| 517 | 544 | The response is `201 Created` with the deployment (some user fields left out here): |
| @@ -600,7 +627,7 @@ | ||
| 600 | 627 | | `environment_url` | Link to the deployed environment. | |
| 601 | 628 | | `description` | Up to 140 characters; longer text is cut. | |
| 602 | 629 | | `environment` | Moves the deployment to another environment (created if needed). The deployment's `original_environment` keeps the first name. | |
| 603 | -| `auto_inactive` | Default `true`. With a `success` state, other deployments to the same environment whose latest status is `success` get an `inactive` status, except transient and production deployments. Send `false` to keep them. | | |
| 630 | +| `auto_inactive` | Default `true`. With a `success` state, older deployments to the same environment whose latest status is `success` get an `inactive` status, except transient and production deployments. Deployments created after this one are left alone. Send `false` to keep them all. | | |
| 604 | 631 | |
| 605 | 632 | URLs must start with `http://` or `https://` (`422 URLs must be http(s)`). The response is `201 Created`: |
| 606 | 633 | |
| @@ -680,7 +707,7 @@ | ||
| 680 | 707 | } |
| 681 | 708 | ``` |
| 682 | 709 | |
| 683 | -Deployments that workflow jobs create for `environment:` don't send a `deployment` event; follow them with `deployment_status`. | |
| 710 | +Deployments that workflow jobs create for `environment:` send this event too, when the job starts. Their `description` is `null`, `task` is `deploy` and `payload` is `{}`. | |
| 684 | 711 | |
| 685 | 712 | ### deployment_status |
| 686 | 713 | |
| @@ -715,7 +742,7 @@ | ||
| 715 | 742 | - Agents can't create, change or delete environments or their branch policies, whatever their role. An agent with the write role gets `403 admin access required`; one that was given the admin role gets `403 only repository admins (people, not agents) can manage environments`. |
| 716 | 743 | - Agents can't set or delete environment secrets: an agent with the admin role gets `403 agents cannot manage secrets`. |
| 717 | 744 | - Agents can't create, change or delete environment variables: an agent with the admin role gets `403 agents cannot manage variables`. |
| 718 | -- Runs started by agents only get environment secrets from environments set to protected branches only, and never on pull request refs (see [Who receives environment secrets](#who-receives-environment-secrets)). | |
| 745 | +- Runs whose code came from an agent only get environment secrets from environments set to protected branches only, and never on pull request refs (see [Who receives environment secrets](#who-receives-environment-secrets)). | |
| 719 | 746 | - Tags that an agent created or moved never match an environment's tag patterns. |
| 720 | 747 | - Agents with write access can create deployments and deployment statuses with the API (with a `write` token), and can read environments and environment variables. |
| 721 | 748 | |
| @@ -758,7 +785,6 @@ | ||
| 758 | 785 | | Pull request runs | Fork pull requests get no secrets | No `pull_request` run gets environment secrets | |
| 759 | 786 | | Agents | No equivalent | Agents can't manage environments, get environment secrets only under "protected branches only", and their tags never match tag patterns | |
| 760 | 787 | | `environment.url` | Can use `steps` outputs | `steps` references are ignored | |
| 761 | -| Jobs with `environment:` | Send `deployment` and `deployment_status` events | Send `deployment_status` events only | | |
| 762 | 788 | | Deployment `auto_merge` | Merges the default branch into the ref | Ignored | |
| 763 | 789 | | Branch patterns | `fnmatch` syntax with character classes | `*`, `**` and `?` only, unlike workflow filters | |
| 764 | 790 | | OIDC | `environment` claim and an environment-based `sub` | No `environment` claim | |
| @@ -769,8 +795,8 @@ | ||
| 769 | 795 | |---|---| |
| 770 | 796 | | `<ref> is not allowed to deploy to <env> due to environment protection rules.` | The environment's branch policy doesn't allow the run's ref. Check the policy with `GET .../environments/<env>`: with protected branches only, add a branch protection rule for the branch; with custom patterns, add a matching pattern. A tag last pushed by an agent never matches. | |
| 771 | 797 | | The deploy job fails on a pull request | Pull request refs never pass a branch policy. Add `if: github.event_name == 'push'` (or a branch check) to the deploy job. | |
| 772 | -| `invalid environment name ''` | `environment.name` evaluated to an empty string, for example because it uses `needs` or `steps`. Use `inputs`, `vars`, `matrix` or a literal. | | |
| 773 | -| An environment secret is empty in the job | The run is on a pull request ref, or an agent started it and the environment isn't set to protected branches only, or the name is wrong. Variables are still set. | | |
| 798 | +| `invalid environment name ''` | `environment.name` evaluated to an empty string, for example because it uses `steps`, or a `needs` output that's empty. Use `inputs`, `vars`, `matrix`, `needs` or a literal. | | |
| 799 | +| An environment secret is empty in the job | The run is on a pull request ref, or the run's code came from an agent and the environment isn't set to protected branches only, or the name is wrong. Variables are still set. | | |
| 774 | 800 | | A new environment suddenly refuses a branch | Adding its first secret set the policy to protected branches only. Add a branch protection rule, or switch to custom patterns. | |
| 775 | 801 | | `403 only repository admins (people, not agents) can manage environments` | Use a person's token with the `admin` scope and the admin role on the repository. | |
| 776 | 802 | | `403 agents cannot manage secrets` | Use a person's token. | |
| @@ -781,6 +807,6 @@ | ||
| 781 | 807 | | `422 enable custom_branch_policies on the environment first` | Set `custom_branch_policies: true` with `PUT` before adding patterns. | |
| 782 | 808 | | `409 Conflict: Commit status checks failed for <ref>.` | A context on the commit hasn't passed. Wait for it, or pass `required_contexts` (`[]` to skip the check). | |
| 783 | 809 | | `422 We cannot delete an active deployment unless it is the only deployment in a given environment.` | Post an `inactive` status to the deployment first. | |
| 784 | -| `403 write access required` when a workflow step creates a deployment | `GITHUB_TOKEN` is read-only. Use `environment:` on the job, or a personal token stored as a secret. | | |
| 810 | +| `403 Resource not accessible by integration` or `403 write access required` when a workflow step creates a deployment | The job lacks `deployments: write`. Add it to the job's `permissions:`, or use `environment:` on the job. | | |
| 785 | 811 | | `403 admin access required` | Managing environments, branch patterns, environment secrets and variable changes needs the admin role and an `admin`-scoped token. | |
| 786 | 812 | | The deployment has no environment URL | `environment.url` used `steps`, or wasn't an `http(s)` URL. | |
modifiedapps/diffinery/docs/actions/events.md+48−22View file
| @@ -131,8 +131,8 @@ | ||
| 131 | 131 | | `created`, `deleted`, `forced` | Whether the push created the ref, deleted it (always `false` for runs) or was a force push | |
| 132 | 132 | | `head_commit` | `id`, `message`, `timestamp` (committer time) and `author` (`name`, `email`) of the pushed commit | |
| 133 | 133 | | `commits` | A list with the pushed commit only (`id`, `message`). GitHub lists up to 20 commits. | |
| 134 | -| `pusher` | `name`: the login of the account that pushed | | |
| 135 | -| `sender` | `login`, and `type`: `Bot` for agents, `User` otherwise | | |
| 134 | +| `pusher` | `name`: the login of the account that pushed, and `email`: `null` | | |
| 135 | +| `sender` | See [The sender object](#the-sender-object) | | |
| 136 | 136 | | `repository` | See [The repository object](#the-repository-object) | |
| 137 | 137 | |
| 138 | 138 | ## pull_request |
| @@ -199,21 +199,28 @@ | ||
| 199 | 199 | | `action` | The activity type | |
| 200 | 200 | | `number` | The pull request number | |
| 201 | 201 | | `pull_request.number`, `pull_request.id` | The pull request number (both) | |
| 202 | +| `pull_request.node_id` | An opaque ID | | |
| 202 | 203 | | `pull_request.state` | `open` or `closed` | |
| 204 | +| `pull_request.locked` | `false` | | |
| 203 | 205 | | `pull_request.title`, `pull_request.body` | Title and description | |
| 204 | 206 | | `pull_request.draft`, `pull_request.merged` | Booleans | |
| 205 | 207 | | `pull_request.merged_at` | When it merged, or `null` | |
| 206 | 208 | | `pull_request.merged_by` | `{ "login": ... }` or `null` | |
| 207 | 209 | | `pull_request.merge_commit_sha` | The merge commit once merged, otherwise the test merge commit (or `null` while it conflicts) | |
| 208 | -| `pull_request.user` | `{ "login": ... }` of the author | | |
| 210 | +| `pull_request.user` | The author, as a user object (see [The sender object](#the-sender-object)) | | |
| 209 | 211 | | `pull_request.labels` | `[{ "name": ... }]` | |
| 210 | 212 | | `pull_request.assignees` | `[{ "login": ... }]` | |
| 211 | -| `pull_request.head` | `ref` (branch), `sha` (head commit) and `repo` | | |
| 212 | -| `pull_request.base` | `ref` (branch), `sha` (base commit) and `repo` | | |
| 213 | +| `pull_request.requested_reviewers` | `[]` | | |
| 214 | +| `pull_request.author_association` | `MEMBER` | | |
| 215 | +| `pull_request.head` | `label` (`<owner>:<branch>`), `ref` (branch), `sha` (head commit), `user` (the repository owner) and `repo` (a repository object) | | |
| 216 | +| `pull_request.base` | The same fields for the base branch | | |
| 213 | 217 | | `pull_request.html_url` | `https://diffinery.com/acme/website/pull/<n>` | |
| 214 | 218 | | `pull_request.url` | `https://diffinery.com/api/v3/repos/acme/website/pulls/<n>` | |
| 219 | +| `pull_request.diff_url`, `pull_request.patch_url` | `https://diffinery.com/acme/website/pull/<n>.diff` and `.patch` | | |
| 220 | +| `pull_request.issue_url`, `pull_request.commits_url`, `pull_request.review_comments_url`, `pull_request.comments_url` | API URLs, as on GitHub | | |
| 215 | 221 | | `repository` | See [The repository object](#the-repository-object) | |
| 216 | -| `sender` | `{ "login": ... }` of whoever caused the event | | |
| 222 | +| `organization` | For repositories owned by an organization; see [The repository object](#the-repository-object) | | |
| 223 | +| `sender` | Whoever caused the event; see [The sender object](#the-sender-object) | | |
| 217 | 224 | |
| 218 | 225 | ## pull_request_target |
| 219 | 226 | |
| @@ -391,6 +398,7 @@ | ||
| 391 | 398 | - `branches` and `branches-ignore` filter on the other run's head branch: the pushed branch, or the pull request's head branch for pull request runs. |
| 392 | 399 | - Like on GitHub, the triggered workflow is read from the default branch and runs there, whatever branch the other run was on. |
| 393 | 400 | - Chains stop after three levels: if A triggers B, B triggers C and C triggers D, D's runs don't trigger anything. |
| 401 | +- If the run it follows came from an agent (for example CI on a branch an agent pushed), the `workflow_run` run counts as agent code too, although it runs on the default branch: its `GITHUB_TOKEN` acts as an agent and environment secrets follow the agent rules. A follower acts on what the other run produced (artifacts, outputs), so it can't be used to carry agent-written results past the agent guardrails. See [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions#runs-whose-code-came-from-an-agent). | |
| 394 | 402 | |
| 395 | 403 | | Variable | Value | |
| 396 | 404 | | --- | --- | |
| @@ -402,7 +410,7 @@ | ||
| 402 | 410 | | Field | Value | |
| 403 | 411 | | --- | --- | |
| 404 | 412 | | `action` | `requested` or `completed` | |
| 405 | -| `workflow_run.id` | The other run's ID | | |
| 413 | +| `workflow_run.id` | The other run's number, which is also its `github.run_id` and what `download-artifact`'s `run-id` takes | | |
| 406 | 414 | | `workflow_run.name`, `workflow_run.path` | The other workflow's name and file path | |
| 407 | 415 | | `workflow_run.display_title` | The other run's title | |
| 408 | 416 | | `workflow_run.run_number`, `workflow_run.run_attempt` | Its number and attempt | |
| @@ -479,10 +487,10 @@ | ||
| 479 | 487 | | `release.url`, `release.assets_url`, `release.upload_url` | API URLs. Upload assets to `upload_url` (with the `{?name,label}` template removed). | |
| 480 | 488 | | `release.tarball_url`, `release.zipball_url` | Source archives of the tag | |
| 481 | 489 | | `release.node_id`, `release.reactions` | As on GitHub | |
| 482 | -| `repository` | `id` (a number), `node_id`, `name`, `full_name`, `owner.login`, `default_branch`, `html_url` and `clone_url` | | |
| 483 | -| `sender` | `login`, and `type`: `User` for people, `Bot` otherwise | | |
| 490 | +| `repository` | See [The repository object](#the-repository-object) | | |
| 491 | +| `sender` | See [The sender object](#the-sender-object) | | |
| 484 | 492 | |
| 485 | -Uploading an asset needs a token with write access. The job's `GITHUB_TOKEN` is read-only, so store a personal or agent token as a secret for that: | |
| 493 | +Uploading an asset needs `contents: write`. Give the job that permission and use its `GITHUB_TOKEN`, or use a personal or agent token stored as a secret (see [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions)): | |
| 486 | 494 | |
| 487 | 495 | ```yaml |
| 488 | 496 | on: |
| @@ -566,9 +574,9 @@ | ||
| 566 | 574 | | `action` | The `event_type` | |
| 567 | 575 | | `branch` | The default branch | |
| 568 | 576 | | `client_payload` | The `client_payload` you sent | |
| 577 | +| `repository` | See [The repository object](#the-repository-object) | | |
| 578 | +| `sender` | The account whose token sent the event; see [The sender object](#the-sender-object) | | |
| 569 | 579 | |
| 570 | -Unlike GitHub, and unlike the other events on this page, the `repository_dispatch` payload has no `repository` or `sender` field. Use `github.repository` and `github.actor` instead. | |
| 571 | - | |
| 572 | 580 | ## workflow_call |
| 573 | 581 | |
| 574 | 582 | Makes the workflow reusable: other workflows call it from a job with `uses:`. It never starts a run on its own. |
| @@ -608,28 +616,47 @@ | ||
| 608 | 616 | |
| 609 | 617 | ## The repository object |
| 610 | 618 | |
| 611 | -Every payload except `repository_dispatch` has a `repository` field. For `push`, `pull_request`, `pull_request_target`, `merge_group`, `workflow_dispatch`, `schedule` and `workflow_run` it has: | |
| 619 | +Every payload has a `repository` field with GitHub's full repository object, the same one the REST API returns for `GET /repos/{owner}/{repo}`. The fields workflows read most: | |
| 612 | 620 | |
| 613 | 621 | | Field | Value | |
| 614 | 622 | | --- | --- | |
| 615 | -| `id` | The repository's ID | | |
| 623 | +| `id` | The repository's ID, a number | | |
| 624 | +| `node_id` | An opaque ID | | |
| 616 | 625 | | `name` | `website` | |
| 617 | 626 | | `full_name` | `acme/website` | |
| 618 | -| `owner.login` | `acme` | | |
| 627 | +| `private` | `true` for a private repository, `false` for a public one | | |
| 628 | +| `visibility` | `public` or `private` | | |
| 629 | +| `owner` | A user object for the owner (`login`, a numeric `id`, `type`: `User` or `Organization`, and URLs) | | |
| 619 | 630 | | `default_branch` | `main` | |
| 631 | +| `archived` | Whether the repository is archived | | |
| 632 | +| `topics` | The repository's topics | | |
| 620 | 633 | | `html_url` | `https://diffinery.com/acme/website` | |
| 621 | 634 | | `clone_url` | `https://diffinery.com/acme/website.git` | |
| 622 | -| `private` | Always `true`, whatever the repository's visibility | | |
| 635 | +| `url` and the `*_url` templates | API URLs under `https://diffinery.com/api/v3/repos/acme/website` | | |
| 636 | +| `created_at`, `updated_at`, `pushed_at` | Timestamps | | |
| 623 | 637 | |
| 624 | -For `release`, `repository.id` is a number (as in the REST API), there is also a `node_id`, and there is no `private` field. | |
| 638 | +For a repository owned by an organization, payloads also have an `organization` field with `login`, `id` (a number), `node_id`, `url`, `repos_url`, `avatar_url` and `description`. | |
| 625 | 639 | |
| 626 | -The payload is also written to the file at `GITHUB_EVENT_PATH` for tools that read it from disk. | |
| 640 | +The payload is also written to the file at `GITHUB_EVENT_PATH` for tools that read it from disk, such as `@actions/github`'s `context.payload` in `actions/github-script`. | |
| 627 | 641 | |
| 642 | +## The sender object | |
| 643 | + | |
| 644 | +`sender` is the account that caused the event, as a GitHub user object: | |
| 645 | + | |
| 646 | +| Field | Value | | |
| 647 | +| --- | --- | | |
| 648 | +| `login` | The account's login | | |
| 649 | +| `id` | A number derived from the account's ID, like GitHub's numeric user IDs | | |
| 650 | +| `node_id` | An opaque ID | | |
| 651 | +| `type` | `User` for people, `Bot` for agents and Diffinery itself (for example `diffinery-scheduler`) | | |
| 652 | +| `site_admin` | `false` | | |
| 653 | +| `avatar_url`, `html_url`, `url` | The avatar, the profile page and the API URL | | |
| 654 | + | |
| 628 | 655 | ## Runs started from inside a workflow |
| 629 | 656 | |
| 630 | -As on GitHub, an event caused by a job's own `GITHUB_TOKEN` doesn't start new workflow runs, so workflows can't trigger themselves in a loop. `repository_dispatch` is the exception, as on GitHub: it is meant for starting workflows on purpose. | |
| 657 | +As on GitHub, an event caused by a job's own `GITHUB_TOKEN` doesn't start new workflow runs, so workflows can't trigger themselves in a loop. A push, a pull request or a release made with a job token starts nothing. The two exceptions, as on GitHub, are `workflow_dispatch` (a job with `permissions: actions: write` can start another workflow with `POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches`) and `repository_dispatch` (a job with `contents: write` can send `POST /repos/{owner}/{repo}/dispatches`). | |
| 631 | 658 | |
| 632 | -On Diffinery the job's `GITHUB_TOKEN` has read access only, so in practice it can't push, open pull requests, publish releases or send `repository_dispatch` events in the first place (the API answers `403 write access required`). To do any of that from a job, use a personal or agent token stored as a secret. Events caused by such a token are treated like any other event from that account and do start workflows: a push starts `push` workflows, a published release starts `release` workflows, and a dispatch starts `repository_dispatch` workflows. | |
| 659 | +Events caused by a personal or agent token stored as a secret are treated like any other event from that account and do start workflows: a push starts `push` workflows, a published release starts `release` workflows, and a dispatch starts `repository_dispatch` workflows. | |
| 633 | 660 | |
| 634 | 661 | ```yaml |
| 635 | 662 | - name: Ask the deploy workflow to run |
| @@ -650,8 +677,7 @@ | ||
| 650 | 677 | - `pull_request` and `pull_request_target` fire for `opened`, `synchronize`, `reopened`, `closed`, `edited`, `ready_for_review` and `converted_to_draft` only, and the `edited` payload has no `changes` object. |
| 651 | 678 | - `workflow_run` fires for `requested` and `completed`, not `in_progress`, and requires `workflows`. |
| 652 | 679 | - `release` workflows get no `branches`, `tags` or `paths` filtering, and a published release whose tag doesn't exist any more starts no runs. |
| 653 | -- `repository_dispatch` payloads have no `repository` or `sender` field. | |
| 654 | -- `repository.private` is always `true` in payloads (except `release`, which has no `private` field). | |
| 680 | +- `deployment` and `deployment_status` events go to webhooks (a job with `environment:` sends both), but don't start workflow runs. | |
| 655 | 681 | - `push` payloads list only the pushed commit in `commits`. |
| 656 | 682 | - Path filters pass when a push creates a branch. |
| 657 | 683 | - `github.event.inputs` keeps input types. |
modifiedapps/diffinery/docs/actions/expressions.md+42−19View file
| @@ -129,15 +129,17 @@ | ||
| 129 | 129 | |
| 130 | 130 | ### Status check functions |
| 131 | 131 | |
| 132 | -In a job's `if:`: | |
| 132 | +In a job's `if:`, the functions look at all of the job's ancestors: the jobs in its `needs`, the jobs those need, and so on up the chain, as on GitHub. | |
| 133 | 133 | |
| 134 | 134 | | Function | True when | |
| 135 | 135 | | --- | --- | |
| 136 | -| `success()` | The run hasn't been cancelled and every job in `needs` succeeded or was skipped. | | |
| 137 | -| `failure()` | A job in `needs` failed or timed out. | | |
| 136 | +| `success()` | The run hasn't been cancelled and every ancestor succeeded. A skipped or cancelled ancestor makes it false. | | |
| 137 | +| `failure()` | An ancestor failed or timed out. | | |
| 138 | 138 | | `cancelled()` | The run has been cancelled. | |
| 139 | 139 | | `always()` | Always. | |
| 140 | 140 | |
| 141 | +A matrix job's result combines its legs: any failed leg fails it, otherwise any cancelled leg cancels it, and it's skipped when every leg was skipped. A leg that failed with `continue-on-error: true` counts as a success. A reusable workflow call's result combines the jobs inside it the same way. | |
| 142 | + | |
| 141 | 143 | In a step's `if:`: |
| 142 | 144 | |
| 143 | 145 | | Function | True when | |
| @@ -165,8 +167,28 @@ | ||
| 165 | 167 | - run: echo "test finished with ${{ needs.test.result }}" |
| 166 | 168 | ``` |
| 167 | 169 | |
| 168 | -> **Important:** Two differences from GitHub at the job level. On GitHub, when a job is skipped, every job that needs it is skipped too; on Diffinery a skipped job counts as a success, so the jobs that need it run. And `failure()` only looks at the jobs listed in the job's own `needs`, not at the jobs those depend on. If a job must not run after an earlier failure further up the chain, list that job in `needs` or check its result, for example `if: needs.build.result == 'success'`. | |
| 170 | +Because a job's default `if:` is `success()`, a skipped job skips every job that depends on it, directly or further down the chain. To run a job anyway, give it a status function, for example `if: always()` or `if: ${{ !cancelled() && needs.build.result != 'failure' }}`: | |
| 169 | 171 | |
| 172 | +```yaml | |
| 173 | +jobs: | |
| 174 | + build: | |
| 175 | + if: github.event_name == 'push' | |
| 176 | + runs-on: ubuntu-latest | |
| 177 | + steps: | |
| 178 | + - run: make | |
| 179 | + test: | |
| 180 | + needs: build # skipped when build is skipped | |
| 181 | + runs-on: ubuntu-latest | |
| 182 | + steps: | |
| 183 | + - run: make test | |
| 184 | + report: | |
| 185 | + needs: test | |
| 186 | + if: ${{ !cancelled() }} # runs even when build and test were skipped | |
| 187 | + runs-on: ubuntu-latest | |
| 188 | + steps: | |
| 189 | + - run: echo "test result was ${{ needs.test.result }}" | |
| 190 | +``` | |
| 191 | + | |
| 170 | 192 | ## Contexts |
| 171 | 193 | |
| 172 | 194 | ### Where each context is available |
| @@ -177,9 +199,9 @@ | ||
| 177 | 199 | | --- | --- | |
| 178 | 200 | | `run-name`, top-level `concurrency` | `github`, `inputs`, `vars` | |
| 179 | 201 | | `jobs.<id>.if` | `github`, `needs`, `inputs`, `vars`, `env` (workflow-level values, unevaluated), status functions | |
| 180 | -| `jobs.<id>.runs-on`, `jobs.<id>.strategy` | `github`, `needs`, `inputs`, `vars`, `env` | | |
| 181 | -| `jobs.<id>.name`, `jobs.<id>.timeout-minutes` | `github`, `needs`, `inputs`, `vars`, `env`, `matrix`, `strategy` | | |
| 182 | -| `jobs.<id>.environment` (`name` and `url`) | `github`, `inputs`, `vars`, `matrix`. A `url` that refers to `steps.` is ignored. | | |
| 202 | +| `jobs.<id>.strategy` | `github`, `needs`, `inputs`, `vars`, `env` | | |
| 203 | +| `jobs.<id>.name`, `jobs.<id>.runs-on`, `jobs.<id>.timeout-minutes`, `jobs.<id>.continue-on-error`, `jobs.<id>.concurrency` | `github`, `needs`, `inputs`, `vars`, `env`, `matrix`, `strategy`. These are evaluated for each matrix combination. | | |
| 204 | +| `jobs.<id>.environment` (`name` and `url`) | `github`, `needs`, `inputs`, `vars`, `matrix`, `strategy`. A `url` that refers to `steps.` is ignored. | | |
| 183 | 205 | | `jobs.<id>.with` (reusable workflow call) | `github`, `needs`, `inputs`, `vars` | |
| 184 | 206 | | `jobs.<id>.secrets` (reusable workflow call) | `github`, `needs`, `inputs`, `vars`, `secrets` | |
| 185 | 207 | | `jobs.<id>.env`, `container`, `services` | `github`, `needs`, `inputs`, `vars`, `secrets`, `matrix`, `strategy`, `runner`, `job` | |
| @@ -210,7 +232,7 @@ | ||
| 210 | 232 | | `github.repositoryUrl` | `https://diffinery.com/acme/website.git` | |
| 211 | 233 | | `github.actor` | The login of the account that triggered the run (for a re-run, who re-ran it): for `release`, whoever published, edited or deleted the release; for `repository_dispatch`, the account whose token sent the event. Scheduled runs use `diffinery-scheduler`; merge queue groups use the login of whoever queued the pull request. | |
| 212 | 234 | | `github.triggering_actor` | Same as `github.actor` | |
| 213 | -| `github.run_id` | The run's ID, a 32-character hexadecimal string | | |
| 235 | +| `github.run_id` | The run number, as a string, for example `128`. It stays the same on a re-run, and the REST API accepts it wherever a run ID goes, so actions that parse it as a number work. | | |
| 214 | 236 | | `github.run_number` | The run's number, as a string | |
| 215 | 237 | | `github.run_attempt` | `1`, or higher for re-runs, as a string | |
| 216 | 238 | | `github.workflow` | The workflow's `name`, or its path when it has none | |
| @@ -229,7 +251,7 @@ | ||
| 229 | 251 | |
| 230 | 252 | GitHub properties Diffinery doesn't set include `github.actor_id`, `github.repository_owner_id`, `github.workflow_sha`, `github.event_path` (use the `GITHUB_EVENT_PATH` variable), `github.env`, `github.path`, `github.output`, `github.state` and `github.step_summary` (use the environment variables of the same names). |
| 231 | 253 | |
| 232 | -> **Note:** `github.run_number` counts runs of all workflows in the repository, not each workflow separately as on GitHub, and `github.run_id` is a hexadecimal string, not a number. | |
| 254 | +> **Note:** `github.run_number` counts runs of all workflows in the repository, not each workflow separately as on GitHub. Because `github.run_id` is the run number, the two are always equal. | |
| 233 | 255 | |
| 234 | 256 | ### env |
| 235 | 257 | |
| @@ -254,8 +276,8 @@ | ||
| 254 | 276 | |
| 255 | 277 | | Property | Value | |
| 256 | 278 | | --- | --- | |
| 257 | -| `secrets.<NAME>` | A repository secret, or an organization secret of the repository's owner. A repository secret wins over an organization secret with the same name. In a job with `environment:`, the environment's secrets win over both (pull request runs, and some runs started by agents, don't get environment secrets; see [environment](/docs/actions/workflow-syntax#environment)). | | |
| 258 | -| `secrets.GITHUB_TOKEN` | The job's token. It has read access to the repository; see [Workflow syntax](/docs/actions/workflow-syntax#permissions). | | |
| 279 | +| `secrets.<NAME>` | A repository secret, or an organization secret of the repository's owner. A repository secret wins over an organization secret with the same name. In a job with `environment:`, the environment's secrets win over both (pull request runs, and some runs whose code came from an agent, don't get environment secrets; see [environment](/docs/actions/workflow-syntax#environment)). | | |
| 280 | +| `secrets.GITHUB_TOKEN` | The job's token. What it may do follows the job's `permissions:`; see [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions). | | |
| 259 | 281 | | `secrets.DIFFINERY_TOKEN` | The same job token, under a Diffinery name. | |
| 260 | 282 | |
| 261 | 283 | Secret names are stored in upper case and lookups ignore case; an unset secret is `''`. Inside a reusable workflow, `secrets` holds only what the caller passed (or everything with `secrets: inherit`), plus the two tokens. Secret values are masked in logs. |
| @@ -303,7 +325,7 @@ | ||
| 303 | 325 | | `runner.temp` | A temporary directory for the job; `/workspace/_temp` on hosted runners, where every job starts in a fresh container | |
| 304 | 326 | | `runner.tool_cache` | Where setup actions install tools; `/opt/hostedtoolcache` on hosted runners | |
| 305 | 327 | | `runner.workspace` | The parent of `GITHUB_WORKSPACE`; `/workspace` on hosted runners | |
| 306 | -| `runner.debug` | `1` when debug logging is on (secret or variable `ACTIONS_STEP_DEBUG` set to `true`), otherwise `''` | | |
| 328 | +| `runner.debug` | `1` when debug logging is on (secret or variable `ACTIONS_STEP_DEBUG` set to `true`), otherwise `''`. `ACTIONS_RUNNER_DEBUG` doesn't change it. | | |
| 307 | 329 | |
| 308 | 330 | ### job |
| 309 | 331 | |
| @@ -338,7 +360,7 @@ | ||
| 338 | 360 | | `GITHUB_REF_TYPE` | `branch` or `tag` | |
| 339 | 361 | | `GITHUB_HEAD_REF` | The head branch for pull request events, otherwise empty | |
| 340 | 362 | | `GITHUB_BASE_REF` | The base branch for pull request events, otherwise empty | |
| 341 | -| `GITHUB_RUN_ID` | The run's ID | | |
| 363 | +| `GITHUB_RUN_ID` | The run number, as in `github.run_id` | | |
| 342 | 364 | | `GITHUB_RUN_NUMBER` | The run's number | |
| 343 | 365 | | `GITHUB_RUN_ATTEMPT` | The attempt number | |
| 344 | 366 | | `GITHUB_JOB` | The job's ID | |
| @@ -363,12 +385,15 @@ | ||
| 363 | 385 | | `RUNNER_TOOL_CACHE` | `/opt/hostedtoolcache` | |
| 364 | 386 | | `RUNNER_WORKSPACE` | `/workspace` | |
| 365 | 387 | | `AGENT_TOOLSDIRECTORY` | Same as `RUNNER_TOOL_CACHE` | |
| 388 | +| `ImageOS` | `ubuntu24`, as on GitHub's `ubuntu-24.04` image | | |
| 389 | +| `ImageVersion` | `20250101.1` | | |
| 366 | 390 | |
| 367 | 391 | Set only in some cases: |
| 368 | 392 | |
| 369 | 393 | | Variable | When | |
| 370 | 394 | | --- | --- | |
| 371 | -| `RUNNER_DEBUG` | `1` when the secret or variable `ACTIONS_STEP_DEBUG` is `true` | | |
| 395 | +| `RUNNER_DEBUG` | `1` when the secret or variable `ACTIONS_STEP_DEBUG` or `ACTIONS_RUNNER_DEBUG` is `true` | | |
| 396 | +| `ACTIONS_RUNTIME_TOKEN`, `ACTIONS_RUNTIME_URL`, `ACTIONS_RESULTS_URL`, `ACTIONS_CACHE_URL`, `ACTIONS_CACHE_SERVICE_V2` | In JavaScript actions, Docker container actions and the built-in actions only, never in `run:` steps. They give access to the job's cache and artifact services and stop working when the job ends. See [Caching and artifacts](/docs/actions/caching-and-artifacts#the-runtime-services). | | |
| 372 | 397 | | `ACTIONS_ID_TOKEN_REQUEST_URL`, `ACTIONS_ID_TOKEN_REQUEST_TOKEN` | When the job has `permissions: id-token: write`. Used by OIDC clients; see [OIDC](/docs/actions/oidc). | |
| 373 | 398 | | `GITHUB_ACTION_PATH`, `GITHUB_ACTION_REPOSITORY`, `GITHUB_ACTION_REF` | In JavaScript and Docker actions, and in the steps of composite actions | |
| 374 | 399 | | `INPUT_<NAME>` | In JavaScript and Docker actions: each input, with the name upper-cased and spaces replaced by `_` | |
| @@ -376,7 +401,7 @@ | ||
| 376 | 401 | |
| 377 | 402 | Hosted runners also set `LANG=C.UTF-8`, `LC_ALL=C.UTF-8`, `PIP_BREAK_SYSTEM_PACKAGES=1` (so `pip install` works with Ubuntu's Python) and `COREPACK_ENABLE_DOWNLOAD_PROMPT=0`. |
| 378 | 403 | |
| 379 | -GitHub variables that Diffinery doesn't set include `GITHUB_ACTOR_ID`, `GITHUB_REPOSITORY_ID`, `GITHUB_REPOSITORY_OWNER_ID`, `GITHUB_WORKFLOW_SHA`, `GITHUB_REF_PROTECTED`, `ImageOS` and `ImageVersion`. `GITHUB_TOKEN` isn't an environment variable unless you set it, as on GitHub: | |
| 404 | +GitHub variables that Diffinery doesn't set include `GITHUB_ACTOR_ID`, `GITHUB_REPOSITORY_ID`, `GITHUB_REPOSITORY_OWNER_ID`, `GITHUB_WORKFLOW_SHA` and `GITHUB_REF_PROTECTED`. `GITHUB_TOKEN` isn't an environment variable unless you set it, as on GitHub: | |
| 380 | 405 | |
| 381 | 406 | ```yaml |
| 382 | 407 | - run: gh api repos/${{ github.repository }} |
| @@ -386,13 +411,11 @@ | ||
| 386 | 411 | |
| 387 | 412 | ## Differences from GitHub |
| 388 | 413 | |
| 389 | -- A skipped job counts as a success for the jobs that need it, so they run; on GitHub they're skipped. `failure()` in a job's `if:` only looks at its direct `needs`. | |
| 390 | -- `runs-on` can't use the `matrix` context. | |
| 391 | -- `jobs.<id>.environment` can't use the `needs` or `strategy` contexts, and its `url` can't use step outputs. | |
| 414 | +- `jobs.<id>.environment.url` can't use step outputs. | |
| 392 | 415 | - Job-level `if:` can also read the workflow-level `env`. |
| 393 | 416 | - `hashFiles()` returns `''` outside steps and other runner-evaluated values. |
| 394 | 417 | - `github.event.inputs` keeps the types of dispatch inputs; on GitHub its values are strings. |
| 395 | -- `github.run_id` is a hexadecimal string, and `github.run_number` counts runs across all workflows of the repository. | |
| 418 | +- `github.run_number` counts runs across all workflows of the repository, and `github.run_id` is the same number. | |
| 396 | 419 | - On a re-run, `github.actor` is whoever started the re-run (and `github.triggering_actor` matches it). |
| 397 | 420 | - `github.ref_protected` is always `false`, and `secrets.DIFFINERY_TOKEN` is an extra name for the job token. |
| 398 | 421 | - `GITHUB_*` and `RUNNER_*` defaults can't be overridden, but `CI` and `DIFFINERY` can. |
modifiedapps/diffinery/docs/actions/oidc.md+21−6View file
| @@ -7,9 +7,10 @@ | ||
| 7 | 7 | |
| 8 | 8 | ## Requirements |
| 9 | 9 | |
| 10 | -- The job needs `permissions: id-token: write`, in the workflow's top-level `permissions`, in the job's `permissions`, or through `permissions: write-all`. | |
| 10 | +- The job's effective permissions must include `id-token: write`. A job's own `permissions:` replaces the workflow's top-level `permissions:` entirely, as on GitHub, so a job that sets `permissions:` must list `id-token: write` itself. `permissions: write-all` includes it. | |
| 11 | +- The repository's default permissions never include `id-token`, and neither does `read-all`. `id-token: read` grants nothing. | |
| 11 | 12 | - It works on hosted and self-hosted runners. |
| 12 | -- In a reusable workflow, the **called** workflow (or its job) must grant `id-token: write`; the calling job's `permissions` isn't used. | |
| 13 | +- In a reusable workflow, the called jobs get the calling job's permissions, and anything the called workflow or its jobs declare is lowered to the calling job's permissions. So the calling job's effective permissions must include `id-token: write`, and so must the called workflow's or job's `permissions:` when it sets one. See [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions#reusable-workflows). | |
| 13 | 14 | |
| 14 | 15 | ```yaml |
| 15 | 16 | permissions: |
| @@ -17,6 +18,22 @@ | ||
| 17 | 18 | contents: read |
| 18 | 19 | ``` |
| 19 | 20 | |
| 21 | +This job gets no OIDC token, because its own `permissions:` replaces the workflow's: | |
| 22 | + | |
| 23 | +```yaml | |
| 24 | +permissions: | |
| 25 | + id-token: write | |
| 26 | + contents: read | |
| 27 | + | |
| 28 | +jobs: | |
| 29 | + deploy: | |
| 30 | + runs-on: ubuntu-latest | |
| 31 | + permissions: | |
| 32 | + contents: read # id-token is now none for this job | |
| 33 | + steps: | |
| 34 | + - run: echo "no ACTIONS_ID_TOKEN_REQUEST_URL here" | |
| 35 | +``` | |
| 36 | + | |
| 20 | 37 | Without the permission, the variables below aren't set, and `core.getIDToken()` in an action fails because `ACTIONS_ID_TOKEN_REQUEST_URL` is missing. |
| 21 | 38 | |
| 22 | 39 | ## The issuer |
| @@ -92,7 +109,7 @@ | ||
| 92 | 109 | | `sha` | `9fceb02d0ae598e95dc970b74767f19372d61af8` | The commit the run checked out. | |
| 93 | 110 | | `workflow` | `Deploy` | The workflow's `name`, or its file path when it has none. | |
| 94 | 111 | | `event_name` | `push` | The event that started the run. | |
| 95 | -| `run_id` | `01J9Z...` | Diffinery's run id (an opaque string, not a number). | | |
| 112 | +| `run_id` | `128` | The run number, as a string. It's the same value as `github.run_id`, and stays the same when the run is re-run. | | |
| 96 | 113 | | `actor` | `diffy` | Who started the run. | |
| 97 | 114 | | `job_workflow_ref` | `acme/website/.github/workflows/deploy.yml@refs/heads/main` | The run's top-level workflow file and ref. | |
| 98 | 115 | |
| @@ -334,9 +351,7 @@ | ||
| 334 | 351 | | `sub` | Varies by event (for example `repo:o/r:pull_request`, `repo:o/r:environment:prod`) | Always `repo:<owner>/<repo>:ref:<ref>`; pull requests use `refs/pull/<n>/merge` | |
| 335 | 352 | | Claims | Many, including `repository_id`, `workflow_ref`, `ref_type`, `environment`, `runner_environment` | Only the claims listed in [Claims](#claims) | |
| 336 | 353 | | Jobs with `environment:` | `environment` claim, and `sub` is `repo:o/r:environment:<name>` | No `environment` claim; `sub` is unchanged (see [Jobs that use an environment](#jobs-that-use-an-environment)) | |
| 337 | -| `run_id` | A number | An opaque string | | |
| 338 | 354 | | `job_workflow_ref` | The reusable workflow's file when the job is in one | Always the run's top-level workflow | |
| 339 | -| `permissions` | A job's `permissions` replaces the workflow's | `id-token: write` at either level (or `write-all`) grants it | | |
| 340 | 355 | | Token lifetime | About 5 to 10 minutes | 10 minutes | |
| 341 | 356 | |
| 342 | 357 | When you port a trust policy from GitHub, change the issuer host (`token.actions.githubusercontent.com` to `diffinery.com`) in provider URLs and condition keys, and rewrite `sub` patterns to the `repo:<owner>/<repo>:ref:<ref>` form. |
| @@ -345,7 +360,7 @@ | ||
| 345 | 360 | |
| 346 | 361 | | Problem | Fix | |
| 347 | 362 | |---|---| |
| 348 | -| `Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable` from an action | Add `permissions: id-token: write` to the workflow or the job (in a reusable workflow, to the called workflow). | | |
| 363 | +| `Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable` from an action | The job's effective permissions lack `id-token: write`. Add it to the job's `permissions:` if the job has one, otherwise to the workflow's. In a reusable workflow, the calling job needs it too. See [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions). | | |
| 349 | 364 | | AWS: `Not authorized to perform sts:AssumeRoleWithWebIdentity` | The trust policy's `sub` or `aud` doesn't match. Decode the token (its middle part is base64url JSON) and compare `sub` and `aud` with the policy. | |
| 350 | 365 | | AWS: `No OpenIDConnect provider found in your account for https://diffinery.com` | Create the identity provider with URL `https://diffinery.com`. | |
| 351 | 366 | | Google Cloud: `The given credential is rejected by the attribute condition` | The `attribute-condition` on the provider doesn't match the token's claims. | |
modifiedapps/diffinery/docs/actions/overview.md+46−16View file
| @@ -18,7 +18,7 @@ | ||
| 18 | 18 | | Check | The result of one job, reported on the commit the run is about. Branch protection can require checks to pass before merging. | |
| 19 | 19 | | Environment | A named deployment target, such as `production`, with its own secrets, variables and branch policy. A job that sets `environment:` gets them and records a deployment. See [Environments](/docs/actions/environments). | |
| 20 | 20 | |
| 21 | -Workflows are compatible with GitHub Actions: the same triggers, expressions, matrices, `needs`, outputs, caches, artifacts, secrets, environments, OIDC, `actions/checkout`, `actions/setup-node`, `actions/setup-python`, `actions/setup-go`, and remote JavaScript, composite and Docker actions. Differences are called out on each page and collected in [GitHub Actions compatibility](/docs/actions/compatibility). | |
| 21 | +Workflows are compatible with GitHub Actions: the same triggers, expressions, matrices, `needs`, outputs, caches, artifacts, secrets, environments, OIDC, a `GITHUB_TOKEN` that follows `permissions:`, the official actions (`actions/checkout`, `actions/cache`, `actions/upload-artifact`, `actions/download-artifact`, `actions/github-script`, `actions/setup-node`, `actions/setup-python`, `actions/setup-go` and the rest), and remote JavaScript, composite and Docker actions. Differences are called out on each page and collected in [GitHub Actions compatibility](/docs/actions/compatibility). | |
| 22 | 22 | |
| 23 | 23 | ## Your first workflow |
| 24 | 24 | |
| @@ -108,10 +108,10 @@ | ||
| 108 | 108 | |
| 109 | 109 | 1. **An event happens.** Diffinery loads the workflow files from the commit the event is about (see the table above) and checks each workflow's `on:` section, including branch, tag and path filters. |
| 110 | 110 | 2. **A run is created** for each matching workflow, with status `queued`. It gets the next run number and a title: the `run-name` if the workflow sets one, otherwise the first line of the commit message (push), the pull request title (pull request events), `Merge group for #<n>: <title>` (merge groups), the release's name or tag (release), the `event_type` (repository_dispatch) or the workflow name (dispatch, schedule, workflow_run). If the workflow has a `concurrency` group, other runs in the group may be cancelled or this run may wait (see [Concurrency](#concurrency)). |
| 111 | -3. **The run is orchestrated durably** by a Cloudflare Workflow. It resolves the job graph from `needs` into levels, starts every job of a level, waits for all of them to finish, then moves on to the next level. The orchestration survives restarts of the platform, and each wait for a job lasts at most 6 hours. | |
| 112 | -4. **Each job is evaluated** just before it starts: its `if:` condition, its `runs-on` labels and its matrix. A job whose condition is false is recorded as `skipped`. A matrix job becomes one job per combination (at most 256), started up to `max-parallel` at a time. | |
| 111 | +3. **The run is orchestrated durably** by a Cloudflare Workflow. It walks the job graph from `needs`: each job starts as soon as the jobs it needs have finished, without waiting for unrelated jobs. The orchestration survives restarts of the platform, and each wait for a job lasts at most 6 hours. | |
| 112 | +4. **Each job is evaluated** just before it starts: its `if:` condition and its matrix. A job whose condition is false is recorded as `skipped`; without a status function in its `if:`, a job is also skipped when any job above it in the `needs` chain failed, was cancelled or was skipped. A matrix job becomes one job per combination (at most 256), started up to `max-parallel` at a time, and each combination gets its own `name`, `runs-on`, `timeout-minutes`, `continue-on-error` and job `concurrency` group, evaluated with its `matrix` values. A job with a `concurrency` group waits as `pending` while another job of the group runs. | |
| 113 | 113 | 5. **Each hosted job gets its own container.** A fresh Cloudflare Container (Ubuntu 24.04, linux/amd64) starts with a job token that is valid only for that job. The runner agent inside fetches the job definition (for a job with `environment:`, this is when the environment's branch policy is checked and its secrets and variables are added), runs the steps in order, streams the log back as it goes, and reports each step's result and the job's conclusion. The container stops when the job ends. Jobs for self-hosted runners wait until a `diffinery runner` with matching labels claims them instead. |
| 114 | -6. **The run finishes** when every level is done. Its conclusion is `failure` if any job failed or timed out, otherwise `cancelled` if the run was cancelled or any job was cancelled, otherwise `success`. Skipped jobs don't make a run fail. | |
| 114 | +6. **The run finishes** when every job is done. Its conclusion is `failure` if any job failed or timed out (except jobs with `continue-on-error: true`), otherwise `cancelled` if the run was cancelled or any job was cancelled, otherwise `success`. Skipped jobs don't make a run fail. | |
| 115 | 115 | |
| 116 | 116 | ### Timeouts |
| 117 | 117 | |
| @@ -128,6 +128,7 @@ | ||
| 128 | 128 | | Status | Meaning | |
| 129 | 129 | | --- | --- | |
| 130 | 130 | | `queued` | The run or job has been created and hasn't started. | |
| 131 | +| `pending` | (Jobs) The job is held back by its job-level `concurrency` group until the group's running job finishes. Shown as **Pending**. | | |
| 131 | 132 | | `waiting` | (Jobs) The job is waiting for its container to start, or for a self-hosted runner to claim it. | |
| 132 | 133 | | `in_progress` | The run is executing, or a runner has picked up the job. | |
| 133 | 134 | | `completed` | Finished. See the conclusion. | |
| @@ -135,8 +136,8 @@ | ||
| 135 | 136 | | Conclusion | Meaning | |
| 136 | 137 | | --- | --- | |
| 137 | 138 | | `success` | Every step succeeded (steps with `continue-on-error: true` may have failed). | |
| 138 | -| `failure` | A step failed, the job could not start, or (for runs) a job failed. | | |
| 139 | -| `cancelled` | Someone cancelled the run, a newer run in the same concurrency group replaced it, or a sibling matrix job failed with `fail-fast` on. | | |
| 139 | +| `failure` | A step failed, the job could not start, or (for runs) a job without `continue-on-error: true` failed. | | |
| 140 | +| `cancelled` | Someone cancelled the run, a newer run or job in the same concurrency group replaced it, or a sibling matrix job failed or timed out with `fail-fast` on. | | |
| 140 | 141 | | `skipped` | The job's `if:` condition was false. | |
| 141 | 142 | | `timed_out` | (Jobs) Diffinery waited 6 hours for the job without a result. | |
| 142 | 143 | |
| @@ -156,11 +157,11 @@ | ||
| 156 | 157 | |
| 157 | 158 | ### Run page |
| 158 | 159 | |
| 159 | -Click a run to open `https://diffinery.com/acme/website/actions/runs/<run-id>`. It shows: | |
| 160 | +Click a run to open `https://diffinery.com/acme/website/actions/runs/<run-id>`. The run number works in place of the run ID (for example `.../actions/runs/128`) and opens its latest attempt. It shows: | |
| 160 | 161 | |
| 161 | 162 | - The run's **Status** and duration, what **Triggered** it and who, the **Commit** and ref (and pull request), and the **Workflow file** at that commit. |
| 162 | 163 | - One box per job id, listing the jobs it produced (a matrix job says how many, for example `4 jobs (matrix)`), each with its status, duration and, for a failed job, the first line of the failure reason. |
| 163 | -- **Artifacts**, when jobs uploaded any, with their size and age. Click a name to download it as a `.tar.gz` archive. | |
| 164 | +- **Artifacts**, when jobs uploaded any that haven't expired, with their size and age. Click a name to download it: a `.zip` file, or the file itself for an artifact uploaded with `archive: false`. See [Caching and artifacts](/docs/actions/caching-and-artifacts). | |
| 164 | 165 | - **Cancel run** while the run is not completed (you confirm with **Cancel this run?**), and **Re-run all jobs** once it has completed. Both need write access. |
| 165 | 166 | |
| 166 | 167 | ### Job page and logs |
| @@ -300,12 +301,12 @@ | ||
| 300 | 301 | | `GET /api/v3/repos/{owner}/{repo}/actions/runs/{run_id}/logs` | A zip file with one text file per job (`1_<job name>.txt`, `2_<job name>.txt`, ...), each line prefixed with its timestamp. Holds at most 24 MB of logs; jobs past that are left out. | |
| 301 | 302 | | `GET /api/v3/repos/{owner}/{repo}/actions/jobs/{job_id}` | One job, with its steps, outputs, failure and summary. | |
| 302 | 303 | | `GET /api/v3/repos/{owner}/{repo}/actions/jobs/{job_id}/logs` | The log as text (`<timestamp> <line>` per line). Add `format=json` for JSON lines, and `after=<seq>` to get only lines after a sequence number. | |
| 303 | -| `GET /api/v3/repos/{owner}/{repo}/actions/runs/{run_id}/artifacts` | The run's artifacts with `archive_download_url`. | | |
| 304 | +| `GET /api/v3/repos/{owner}/{repo}/actions/runs/{run_id}/artifacts` | The run's artifacts (every attempt when `run_id` is a run number), expired ones included. See [Download artifacts with the API](/docs/actions/caching-and-artifacts#download-artifacts-with-the-api) for the other artifact and cache endpoints. | | |
| 304 | 305 | | `POST /api/v3/repos/{owner}/{repo}/actions/runs/{run_id}/cancel` | Cancels the run (`202`). Needs write. | |
| 305 | 306 | | `POST /api/v3/repos/{owner}/{repo}/actions/runs/{run_id}/rerun` | Re-runs the run (`201`, `{"run_id": ...}`). Needs write. | |
| 306 | 307 | | `POST /api/v3/repos/{owner}/{repo}/actions/runs/{run_id}/rerun-failed-jobs` | Same as `rerun` (see below). | |
| 307 | 308 | |
| 308 | -Run objects use GitHub's field names (`id`, `node_id`, `run_number`, `run_attempt`, `name`, `display_title`, `path`, `workflow_id`, `event`, `head_branch`, `head_sha`, `sha`, `ref`, `status`, `conclusion`, `actor` and `triggering_actor` as user objects, `created_at`, `updated_at`, `run_started_at`, `url`, `html_url`, `jobs_url`, `logs_url`, `artifacts_url`, `cancel_url`, `rerun_url`, `workflow_url`, `previous_attempt_url` and `pull_requests`) plus `pull_request_number`. Job objects carry GitHub's fields (`run_id`, `run_attempt`, `head_sha`, `head_branch`, `html_url`, `started_at`, `completed_at`, `workflow_name`, `steps` with `number`) next to Diffinery's own camelCase fields, which the CLI reads. `head_sha` is the commit checks are reported on (the pull request head for pull request runs) and `sha` is the commit the run checked out. Run and job IDs are 32-character hexadecimal strings, not numbers. While a job is running, one logs request returns at most 5,000 lines; page with `after`. The runs, jobs and artifacts lists send a `Link` header with `next`, `prev`, `first` and `last` URLs, so Octokit's `paginate()` works. Errors have a `message` and a `documentation_url`. Every endpoint is listed in the [API reference](/docs/api). | |
| 309 | +Run objects use GitHub's field names (`id`, `node_id`, `run_number`, `run_attempt`, `name`, `display_title`, `path`, `workflow_id`, `event`, `head_branch`, `head_sha`, `sha`, `ref`, `status`, `conclusion`, `actor` and `triggering_actor` as user objects, `created_at`, `updated_at`, `run_started_at`, `url`, `html_url`, `jobs_url`, `logs_url`, `artifacts_url`, `cancel_url`, `rerun_url`, `workflow_url`, `previous_attempt_url` and `pull_requests`) plus `pull_request_number`. Job objects carry GitHub's fields (`run_id`, `run_attempt`, `head_sha`, `head_branch`, `html_url`, `started_at`, `completed_at`, `workflow_name`, `steps` with `number`) next to Diffinery's own camelCase fields, which the CLI reads. `head_sha` is the commit checks are reported on (the pull request head for pull request runs) and `sha` is the commit the run checked out. Run and job IDs are 32-character hexadecimal strings. Wherever a path takes `{run_id}`, the run number works too and means its latest attempt; this is what `github.run_id` and `GITHUB_RUN_ID` hold inside a job. A job's `check_run_url` points at `GET /repos/{owner}/{repo}/check-runs/{job_id}`, which returns the job's check run. While a job is running, one logs request returns at most 5,000 lines; page with `after`. The runs, jobs and artifacts lists send a `Link` header with `next`, `prev`, `first` and `last` URLs, so Octokit's `paginate()` works. Errors have a `message` and a `documentation_url`. Every endpoint is listed in the [API reference](/docs/api). | |
| 309 | 310 | |
| 310 | 311 | ### From MCP |
| 311 | 312 | |
| @@ -315,7 +316,7 @@ | ||
| 315 | 316 | |
| 316 | 317 | Click **Cancel run** on the run page, run `diffinery run cancel <run-id>`, call `POST /actions/runs/{run_id}/cancel`, or use the `cancel_workflow` MCP tool. You need write access. |
| 317 | 318 | |
| 318 | -Cancelling stops every unfinished job's container and marks those jobs `cancelled` with the reason `Cancelled by <login>`. Jobs in later levels that have no `if:` condition are skipped; a job with `if: always()` or `if: cancelled()` still runs, as on GitHub. The run's conclusion is `cancelled`. | |
| 319 | +Cancelling stops every unfinished job's container and marks those jobs `cancelled` with the reason `Cancelled by <login>`. Jobs that haven't started yet and have no status function in their `if:` are skipped; a job with `if: always()` or `if: cancelled()` still runs, as on GitHub. The run's conclusion is `cancelled`. | |
| 319 | 320 | |
| 320 | 321 | ## Re-run a run |
| 321 | 322 | |
| @@ -324,12 +325,15 @@ | ||
| 324 | 325 | A re-run is a new run with: |
| 325 | 326 | |
| 326 | 327 | - the same workflow file (read again from the original commit), commit, ref, event, event payload and inputs, |
| 327 | -- the same run number and an attempt number one higher (shown as `attempt 2`, and available as `github.run_attempt`), | |
| 328 | -- a new run ID, and you as the actor. | |
| 328 | +- the same run number and an attempt number one higher (shown as `attempt 2`, and available as `github.run_attempt`), so `github.run_id` stays the same, | |
| 329 | +- a new run ID in the API, and you as the actor, | |
| 330 | +- whether its code came from an agent, kept from the original run whoever re-runs it (see [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions#runs-whose-code-came-from-an-agent)). | |
| 329 | 331 | |
| 332 | +A workflow's own `GITHUB_TOKEN` can't re-run runs: `403 a workflow's GITHUB_TOKEN cannot re-run workflows`. | |
| 333 | + | |
| 330 | 334 | It fails with `422 workflow file no longer valid at this commit` if the file can't be loaded from that commit any more. |
| 331 | 335 | |
| 332 | -> **Note:** Diffinery always re-runs every job. `diffinery run rerun --failed` and `POST .../rerun-failed-jobs` are accepted for compatibility and behave like a full re-run. On GitHub a re-run keeps the same run ID; on Diffinery it gets a new one. | |
| 336 | +> **Note:** Diffinery always re-runs every job. `diffinery run rerun --failed` and `POST .../rerun-failed-jobs` are accepted for compatibility and behave like a full re-run. On GitHub a re-run keeps the same run ID in the API; on Diffinery the API's `id` is new, while `github.run_id` (the run number) stays the same. | |
| 333 | 337 | |
| 334 | 338 | ## Job summaries |
| 335 | 339 | |
| @@ -386,8 +390,32 @@ | ||
| 386 | 390 | - With `cancel-in-progress: true`, creating a run cancels every unfinished run (queued or in progress) in the same group, with the reason `cancelled by a newer run in the same concurrency group`. |
| 387 | 391 | - Without it, a new run waits while another run in the group is in progress, checking once a minute. After 6 hours of waiting it starts anyway. |
| 388 | 392 | |
| 389 | -Differences from GitHub: GitHub keeps at most one pending run per group and cancels older pending ones; Diffinery lets several runs wait and starts them as the group frees up. Job-level `concurrency` (`jobs.<id>.concurrency`) is accepted but not enforced. If the group expression fails to evaluate, the run has no concurrency group. | |
| 393 | +Differences from GitHub: GitHub keeps at most one pending run per group and cancels older pending ones; Diffinery lets several runs wait and starts them as the group frees up. If the group expression fails to evaluate, the run has no concurrency group. | |
| 390 | 394 | |
| 395 | +### Job concurrency | |
| 396 | + | |
| 397 | +`jobs.<id>.concurrency` limits a group to one running job at a time across the repository, whatever run or workflow the jobs belong to: | |
| 398 | + | |
| 399 | +```yaml | |
| 400 | +jobs: | |
| 401 | + deploy: | |
| 402 | + runs-on: ubuntu-latest | |
| 403 | + concurrency: | |
| 404 | + group: deploy-${{ matrix.env }} | |
| 405 | + cancel-in-progress: false | |
| 406 | + strategy: | |
| 407 | + matrix: | |
| 408 | + env: [staging, production] | |
| 409 | + steps: | |
| 410 | + - run: ./deploy.sh ${{ matrix.env }} | |
| 411 | +``` | |
| 412 | + | |
| 413 | +- The group is evaluated for each matrix combination, with the `github`, `needs`, `inputs`, `vars`, `matrix` and `strategy` contexts. | |
| 414 | +- Without `cancel-in-progress`, a job whose group has a running job waits with the status `pending`. Only the newest pending job is kept: an older pending job of the group is cancelled. | |
| 415 | +- With `cancel-in-progress: true`, the new job takes the group and cancels the group's running and pending jobs. | |
| 416 | +- Jobs cancelled this way end as `cancelled` with the message `Canceling since a higher priority waiting request for '<group>' exists`. | |
| 417 | +- When the running job finishes, the oldest pending job of the group starts, from whichever run it belongs to. | |
| 418 | + | |
| 391 | 419 | ## Notifications |
| 392 | 420 | |
| 393 | 421 | When a run completes with `success` or `failure`, Diffinery notifies the person who triggered it (for a run an agent triggered, the agent's owner; for runs started by the scheduler or the merge queue, the repository's watchers). By default they get an email and a web notification when a run fails; in their notification settings they can turn off **Only failed runs** to get emails for successful runs too. Cancelled runs don't notify anyone. See [Notifications](/docs/accounts/notifications). |
| @@ -406,7 +434,9 @@ | ||
| 406 | 434 | | `This runner is being replaced by a new version. The job keeps running for up to 14 minutes before it is cancelled.` | Diffinery is rolling out a new runner version. Jobs that finish within 14 minutes are unaffected; longer ones are cancelled and need a re-run. | |
| 407 | 435 | | `Error evaluating 'if': ...` | The job's `if:` expression failed at runtime. See [Expressions](/docs/actions/expressions). | |
| 408 | 436 | | A step can't reach a secret | Secrets are configured in the repository or organization settings. See [Secrets and variables](/docs/actions/secrets-and-variables). | |
| 409 | -| `git push` from a job fails | The job's `GITHUB_TOKEN` has read access to the repository only. Push with a personal or agent token stored as a secret. | | |
| 437 | +| `git push` from a job fails | The job's `GITHUB_TOKEN` needs `contents: write` (new repositories default to read-only tokens), can't change workflow files, and follows branch protection. See [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions). | | |
| 438 | +| `Resource not accessible by integration (the workflow's GITHUB_TOKEN needs permissions: ...)` | Add the named scope to the job's `permissions:`. See [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions). | | |
| 439 | +| A job is `skipped` though its `if:` is true | A job above it in the `needs` chain failed, was cancelled or was skipped. Add a status function, for example `if: ${{ !cancelled() && !failure() }}`. | | |
| 410 | 440 | | `<branch> is not allowed to deploy to <environment> due to environment protection rules.` | The job sets `environment:` and the environment's branch policy doesn't allow the run's ref. Run it from an allowed branch or tag, or change the policy. See [Environments](/docs/actions/environments). | |
| 411 | 441 | | `invalid environment name '<name>'` | The job's `environment` name is empty, longer than 255 characters, or contains `/`, `\` or control characters. | |
| 412 | 442 | | An `on: release` workflow runs three times | Creating a published release sends `created`, `published` and `released`. Add `types: [published]`. See [release](/docs/actions/events#release). | |
modifiedapps/diffinery/docs/actions/reusable-workflows.md+25−7View file
| @@ -83,7 +83,8 @@ | ||
| 83 | 83 | | `if` | Skip the call (and every job inside it) when false. | |
| 84 | 84 | | `name` | Display name, used as the prefix of the jobs inside the call. | |
| 85 | 85 | | `strategy` | Accepted, but a `matrix` isn't supported yet (see [Matrix on the calling job](#matrix-on-the-calling-job)). | |
| 86 | -| `permissions`, `concurrency` | Accepted but have no effect on a calling job. | | |
| 86 | +| `permissions` | The most the jobs inside the call may have. They inherit these permissions, and anything the called workflow declares is lowered to them. See [Permissions in called workflows](#permissions-in-called-workflows). | | |
| 87 | +| `concurrency` | Accepted but has no effect on a calling job. Jobs inside the called workflow can use their own job-level `concurrency`. | | |
| 87 | 88 | |
| 88 | 89 | Any other key produces a warning (`unknown reusable-workflow job key '...'`) and is ignored. That includes `environment`: as on GitHub, a calling job can't use an environment, so put `environment:` on the jobs inside the called workflow (see [Environments in called workflows](#environments-in-called-workflows)). `steps` or `runs-on` on a calling job is an error. |
| 89 | 90 | |
| @@ -188,10 +189,28 @@ | ||
| 188 | 189 | | `env` | The called workflow's own `env` (workflow and job level). The caller's `env` is not passed. | |
| 189 | 190 | | `needs` | Job ids as written inside the called workflow. | |
| 190 | 191 | |
| 191 | -Because `github.repository` is the caller's, `actions/checkout` with no inputs checks out the **calling** repository, as on GitHub. To check out the repository that holds the workflow, pass `repository:` and a `token:` from a secret: the job's `GITHUB_TOKEN` can only read its own repository. | |
| 192 | +Because `github.repository` is the caller's, `actions/checkout` with no inputs checks out the **calling** repository, as on GitHub. To check out the repository that holds the workflow, pass `repository:`. The job's `GITHUB_TOKEN` can read that repository when it's public; when it's private, also pass a `token:` from a secret. | |
| 192 | 193 | |
| 193 | -`permissions` that control the job token (`id-token` and `packages`) are read from the called workflow itself: its workflow-level `permissions` or the job's own. The calling job's `permissions` isn't consulted. | |
| 194 | +### Permissions in called workflows | |
| 194 | 195 | |
| 196 | +The jobs inside a call get their `GITHUB_TOKEN` permissions like this, as on GitHub: | |
| 197 | + | |
| 198 | +1. Start from the calling job's effective permissions: its own `permissions:`, or else the calling workflow's, or else the repository default. | |
| 199 | +2. If the called workflow or the called job declares `permissions:`, use that instead, but lowered scope by scope to what the calling job has. A called workflow can never get more than its caller. | |
| 200 | +3. If neither declares anything, the called jobs get exactly the calling job's permissions. | |
| 201 | + | |
| 202 | +```yaml | |
| 203 | +# .github/workflows/release.yml in acme/website | |
| 204 | +jobs: | |
| 205 | + deploy: | |
| 206 | + permissions: | |
| 207 | + contents: read | |
| 208 | + id-token: write | |
| 209 | + uses: ./.github/workflows/deploy.yml | |
| 210 | +``` | |
| 211 | + | |
| 212 | +Here the jobs in `deploy.yml` can have at most `contents: read` and `id-token: write`. If `deploy.yml` declares `permissions: write-all`, its jobs still get only those two. If it declares `packages: write`, its jobs get no `packages` access, because the calling job has none. This applies to `id-token` for [OIDC](/docs/actions/oidc), to `packages` for the [container registry](/docs/packages/container-registry), and to every other scope. See [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions#reusable-workflows). | |
| 213 | + | |
| 195 | 214 | ## Environments in called workflows |
| 196 | 215 | |
| 197 | 216 | A job inside a called workflow can deploy to an [environment](/docs/actions/environments) with `environment:`, and the environment's name and `url` can use the called workflow's `inputs`: |
| @@ -212,7 +231,7 @@ | ||
| 212 | 231 | - The environment belongs to the **calling** repository, the one whose run it is, even when the workflow file lives in another repository. Its branch policy is checked against the caller's ref. |
| 213 | 232 | - The job gets the environment's secrets and variables without the caller passing them in `secrets:`, as on GitHub. An environment secret replaces a passed secret with the same name. |
| 214 | 233 | - A secret declared with `required: true` under `on.workflow_call.secrets` must still be passed by the caller, even if the environment has it. Don't declare secrets that come from the environment. |
| 215 | -- The job records a deployment like any other job with `environment:`, and the usual rules for who receives environment secrets apply (pull request refs never do; runs started by agents only from environments set to protected branches only). | |
| 234 | +- The job records a deployment like any other job with `environment:`, and the usual rules for who receives environment secrets apply (pull request refs never do; runs whose code came from an agent only from environments set to protected branches only). | |
| 216 | 235 | |
| 217 | 236 | ## How calls appear in runs and checks |
| 218 | 237 | |
| @@ -222,7 +241,7 @@ | ||
| 222 | 241 | |
| 223 | 242 | - Jobs inside the call that have no `needs` wait for the calling job's `needs`. |
| 224 | 243 | - A job that `needs` the calling job waits for every job inside the call. |
| 225 | -- If the calling job's `if` is false, or its `needs` failed, every job inside the call is reported as skipped. | |
| 244 | +- If the calling job's `if` is false, or (with the default `if`) any job it depends on, directly or through other jobs, failed or was skipped, every job inside the call is reported as skipped. | |
| 226 | 245 | |
| 227 | 246 | ## Nesting |
| 228 | 247 | |
| @@ -363,8 +382,7 @@ | ||
| 363 | 382 | | Where workflows come from | Only repositories on the same Diffinery instance. | |
| 364 | 383 | | Private repositories | Callable only from repositories with the same owner. | |
| 365 | 384 | | Matrix on the calling job | Not supported yet. | |
| 366 | -| `permissions` on the calling job | Ignored. The called workflow's own `permissions` control `id-token` and `packages`. | | |
| 367 | -| `concurrency` | Only the top-level workflow's `concurrency` applies. A called workflow's `concurrency` and a calling job's `concurrency` are ignored. | | |
| 385 | +| `concurrency` | The top-level workflow's `concurrency` and job-level `concurrency` on the jobs inside a call apply. A called workflow's top-level `concurrency` and a calling job's `concurrency` are ignored. | | |
| 368 | 386 | | Number of calls per run | At most 20. | |
| 369 | 387 | | OIDC `job_workflow_ref` claim | The top-level workflow's reference, not the called workflow's. See [OpenID Connect (OIDC)](/docs/actions/oidc). | |
| 370 | 388 | | Branch references | Re-resolved whenever a job of the run is planned or started. | |
modifiedapps/diffinery/docs/actions/runners.md+25−5View file
| @@ -30,7 +30,8 @@ | ||
| 30 | 30 | |
| 31 | 31 | - Every `ubuntu-*` label gets the same Ubuntu 24.04 x64 image, including `ubuntu-22.04` and labels that ask for Arm, such as `ubuntu-24.04-arm`. |
| 32 | 32 | - `runs-on` can be a string, a list, or a mapping with `labels`. A `group` in the mapping form is ignored; only `labels` count. |
| 33 | -- `runs-on` is evaluated once per job, before the matrix is expanded, so it can use `github`, `inputs`, `vars` and `needs` but **not** `matrix`. `runs-on: ${{ matrix.os }}` evaluates to `null` and fails with "No Diffinery runner matches runs-on: null". Use a literal label (hosted runners are all the same Linux image anyway) or split the job. | |
| 33 | +- `runs-on` is evaluated for each matrix combination, so an entry can be an expression using `github`, `inputs`, `vars`, `needs`, `matrix` and `strategy`. An expression can give one label or a list of labels. Empty and `null` values are dropped. | |
| 34 | +- If evaluating `runs-on` fails, only that matrix combination fails, with `Error evaluating the job for this matrix combination: <message>`. If no labels are left, the job fails with `No Diffinery runner matches runs-on: (no labels). ...`. | |
| 34 | 35 | |
| 35 | 36 | ```yaml |
| 36 | 37 | jobs: |
| @@ -38,6 +39,15 @@ | ||
| 38 | 39 | runs-on: ubuntu-latest # hosted |
| 39 | 40 | steps: |
| 40 | 41 | - run: echo "hosted" |
| 42 | + build: | |
| 43 | + strategy: | |
| 44 | + matrix: | |
| 45 | + target: [linux, macos] | |
| 46 | + # The linux leg runs on a hosted runner; the macos leg waits for a | |
| 47 | + # self-hosted runner with the macos label. | |
| 48 | + runs-on: ${{ matrix.target == 'macos' && fromJSON('["self-hosted","macos"]') || 'ubuntu-latest' }} | |
| 49 | + steps: | |
| 50 | + - run: uname -a | |
| 41 | 51 | gpu: |
| 42 | 52 | runs-on: [self-hosted, linux, gpu] # waits for a runner started with --labels linux,gpu |
| 43 | 53 | steps: |
| @@ -69,7 +79,8 @@ | ||
| 69 | 79 | |
| 70 | 80 | | Group | Installed | |
| 71 | 81 | |---|---| |
| 72 | -| Base tools | `ca-certificates`, `curl`, `git`, `git-lfs`, `jq`, `zip`, `unzip`, `xz-utils`, `tar`, `gzip`, `pkg-config`, `openssh-client`, `gnupg` | | |
| 82 | +| Base tools | `ca-certificates`, `curl`, `git`, `git-lfs`, `jq`, `zip`, `unzip`, `xz-utils`, `tar`, `gzip`, `zstd`, `pkg-config`, `openssh-client`, `gnupg` | | |
| 83 | +| GitHub CLI | `gh`, from GitHub's apt repository | | |
| 73 | 84 | | Build tools | `build-essential` (gcc, g++, make and the C library headers) | |
| 74 | 85 | | Python | Ubuntu's `python3` (3.12), `python3-pip`, `python3-venv`, `python-is-python3` (so `python` runs `python3`) | |
| 75 | 86 | | Node.js | Node.js 22.22.0 with npm, plus corepack shims activated for pnpm 10.28.0 and yarn 1.22.22 | |
| @@ -80,6 +91,8 @@ | ||
| 80 | 91 | - `git config --system safe.directory '*'` is set, and Git LFS is installed system-wide. |
| 81 | 92 | - `PIP_BREAK_SYSTEM_PACKAGES=1` is set, so `pip install` works without a virtual environment, as on GitHub's hosted runners. |
| 82 | 93 | - `COREPACK_ENABLE_DOWNLOAD_PROMPT=0` is set, so corepack never stops to ask. |
| 94 | +- `ImageOS=ubuntu24` and `ImageVersion=20250101.1` are set, as on GitHub's `ubuntu-24.04` image. `actions/setup-go` and `actions/setup-python` include them in their cache keys. | |
| 95 | +- `zstd` is installed, so `actions/cache` compresses with zstd, as on GitHub. | |
| 83 | 96 | - The apt package lists are removed to keep the image small. Run `apt-get update` before `apt-get install`. |
| 84 | 97 | |
| 85 | 98 | Anything else (Go, Java, Ruby, Rust, .NET, PowerShell, browsers, cloud CLIs) is **not** preinstalled. Install it in a step: |
| @@ -113,6 +126,12 @@ | ||
| 113 | 126 | |
| 114 | 127 | Hosted and self-hosted runners set the GitHub-compatible variables (`CI=true`, `GITHUB_ACTIONS=true`, `GITHUB_REPOSITORY`, `GITHUB_SHA`, `GITHUB_REF`, `GITHUB_WORKSPACE`, `GITHUB_ENV`, `GITHUB_OUTPUT`, and so on) plus `DIFFINERY=true`. `GITHUB_SERVER_URL` is `https://diffinery.com` and `GITHUB_API_URL` is `https://diffinery.com/api/v3`. `RUNNER_NAME` is `diffinery` and `RUNNER_ENVIRONMENT` is `diffinery-hosted` on every runner. Workflows can override `CI` and `DIFFINERY`, but not `GITHUB_*` or `RUNNER_*` variables. |
| 115 | 128 | |
| 129 | +Other variables a job gets: | |
| 130 | + | |
| 131 | +- `ImageOS` and `ImageVersion`, on hosted and self-hosted runners alike (`ubuntu24` and `20250101.1` unless the runner's environment sets them). | |
| 132 | +- `RUNNER_DEBUG=1` when the secret or variable `ACTIONS_STEP_DEBUG` or `ACTIONS_RUNNER_DEBUG` is `true`. See [Expressions and contexts](/docs/actions/expressions#default-environment-variables). | |
| 133 | +- `ACTIONS_RUNTIME_TOKEN`, `ACTIONS_RUNTIME_URL`, `ACTIONS_RESULTS_URL`, `ACTIONS_CACHE_URL` and `ACTIONS_CACHE_SERVICE_V2`, but only in JavaScript actions, Docker container actions and the built-in actions, never in `run:` steps. They let actions such as `actions/cache` and `actions/upload-artifact` use the cache and artifact services. See [Caching and artifacts](/docs/actions/caching-and-artifacts#the-runtime-services). | |
| 134 | + | |
| 116 | 135 | ### Limits and timeouts |
| 117 | 136 | |
| 118 | 137 | | Limit | Value | What happens | |
| @@ -244,11 +263,12 @@ | ||
| 244 | 263 | ### What a job sees on a self-hosted runner |
| 245 | 264 | |
| 246 | 265 | - The job runs **directly on the machine**, as the user that started `diffinery runner`, with that user's files, network and permissions. |
| 247 | -- The job's environment is the runner's environment, minus `DIFFINERY_TOKEN`, `DIFFINERY_URL`, `DIFFINERY_RUNNER_AGENT`, `GH_TOKEN` and `GITHUB_TOKEN`, plus the usual `GITHUB_*` and `RUNNER_*` variables. | |
| 266 | +- The job's environment is the runner's environment, minus `DIFFINERY_TOKEN`, `DIFFINERY_URL`, `DIFFINERY_RUNNER_AGENT`, `GH_TOKEN` and `GITHUB_TOKEN`, plus the usual `GITHUB_*` and `RUNNER_*` variables. `DIFFINERY_GITHUB_TOKEN` (see below) is used by the agent to download actions but is removed from every step's environment. | |
| 248 | 267 | - The workspace is `<work dir>/<repository name>` and is **not** wiped between jobs. `actions/checkout` cleans it by default (`clean: true`). |
| 249 | 268 | - The tool cache (`RUNNER_TOOL_CACHE`) is `<work dir>/_tool` unless the runner's environment sets `RUNNER_TOOL_CACHE`. Tools that `setup-node`, `setup-python` and `setup-go` download stay there for later jobs. |
| 250 | 269 | - `runner.os` reports `Linux` and `RUNNER_ENVIRONMENT` reports `diffinery-hosted` on every machine, including Macs. Use your own labels, or check `uname` in a step, if a workflow needs to know the platform. |
| 251 | -- Remote actions (`uses: owner/repo@ref`) are downloaded from `codeload.github.com`. Set `DIFFINERY_GITHUB_TOKEN` in the runner's environment to authenticate those downloads (useful against GitHub's rate limits), or `DIFFINERY_ACTIONS_DOWNLOAD_URL` to a URL template containing `{owner}`, `{repo}` and `{ref}` to download from a mirror instead. | |
| 270 | +- Remote actions (`uses: owner/repo@ref`) are downloaded from `codeload.github.com`. Set `DIFFINERY_GITHUB_TOKEN` in the runner's environment to authenticate those downloads (useful against GitHub's rate limits), or `DIFFINERY_ACTIONS_DOWNLOAD_URL` to a URL template containing `{owner}`, `{repo}` and `{ref}` to download from a mirror instead. If the download from `codeload.github.com` fails, the agent tries a shallow `git fetch` of the ref from `https://github.com/<owner>/<repo>` before failing the step with `Failed to download action '<owner>/<repo>@<ref>': ...`. There is no `git` fallback when `DIFFINERY_ACTIONS_DOWNLOAD_URL` is set. | |
| 271 | +- The common official actions (`actions/checkout`, `actions/cache`, `actions/upload-artifact`, `actions/download-artifact`, `actions/setup-node`, `actions/setup-python`, `actions/setup-go`) run as built-in implementations when the version and inputs allow it. Set `DIFFINERY_DISABLE_BUILTINS=1` in the runner's environment to always download and run the real actions on that runner. See [Compatibility with GitHub Actions](/docs/actions/compatibility#built-in-actions). | |
| 252 | 272 | - `setup-node`, `setup-python` and `setup-go` download their tools from `nodejs.org/dist`, the `actions/python-versions` manifest and `go.dev`. Override the sources with `DIFFINERY_NODE_MIRROR`, `DIFFINERY_PYTHON_MANIFEST` and `DIFFINERY_GO_INDEX` in the runner's environment if the machine can't reach them. |
| 253 | 273 | |
| 254 | 274 | ### Run as a service with systemd |
| @@ -343,7 +363,7 @@ | ||
| 343 | 363 | | Symptom | Cause and fix | |
| 344 | 364 | |---|---| |
| 345 | 365 | | `No Diffinery runner matches runs-on: ...` | A label isn't hosted and the job doesn't include `self-hosted`. Use an `ubuntu-*` label, or add `self-hosted` and start a runner with matching `--labels`. | |
| 346 | -| `No Diffinery runner matches runs-on: null` | `runs-on` uses `matrix`, which isn't available there. Use a literal label. | | |
| 366 | +| `No Diffinery runner matches runs-on: (no labels)` | Every `runs-on` entry evaluated to an empty value, for example a `matrix` key that the combination doesn't set. Check the expression. | | |
| 347 | 367 | | A self-hosted job stays queued | No runner serving that repository offers every label. Compare the job's `runs-on` with the labels the runner printed at start. The job fails after 6 hours. | |
| 348 | 368 | | `403 admin access required` | The runner's token or account lacks admin on the repository, or the token isn't `admin`-scoped. | |
| 349 | 369 | | `no runner agent given` or `runner agent not found at ...` | Pass `--agent /path/to/agent.mjs` or set `DIFFINERY_RUNNER_AGENT`. Build it with `pnpm --filter @diffinery/runner build`. | |
modifiedapps/diffinery/docs/actions/secrets-and-variables.md+133−35View file
| @@ -12,16 +12,17 @@ | ||
| 12 | 12 | | Repository secret | One repository | Yes | Yes | People with the admin role on the repository | |
| 13 | 13 | | Organization secret | Every repository the organization owns | No | Yes | Organization owners and admins, and site admins (people only) | |
| 14 | 14 | | Repository variable | One repository | Yes | Yes | People with the admin role on the repository | |
| 15 | +| Organization variable | Every repository the organization owns | No | API only | Organization owners and admins, and site admins (people only) | | |
| 15 | 16 | | Environment secret | Jobs of one repository that use the environment | No | API only | People with the admin role on the repository | |
| 16 | 17 | | Environment variable | Jobs of one repository that use the environment | No | API only | People with the admin role on the repository | |
| 17 | 18 | |
| 18 | -There are no organization-level variables. Agents can't create, change or delete secrets or variables anywhere, whatever their role (see [Agents, secrets and variables](#agents-secrets-and-variables)). Environment secrets and variables are described in [Environment secrets and variables](#environment-secrets-and-variables) below and in full in [Deployments and environments](/docs/actions/environments). | |
| 19 | +Agents can't create, change or delete secrets or variables anywhere, whatever their role (see [Agents, secrets and variables](#agents-secrets-and-variables)). Environment secrets and variables are described in [Environment secrets and variables](#environment-secrets-and-variables) below and in full in [Deployments and environments](/docs/actions/environments). | |
| 19 | 20 | |
| 20 | 21 | ## Names |
| 21 | 22 | |
| 22 | -- Names use letters, digits and underscores, and don't start with a digit (`^[A-Za-z_][A-Za-z0-9_]*$`). For repository variables, only the web form checks this; the variables API accepts other names, which expressions may not be able to read, so keep to the same format. | |
| 23 | +- Names use letters, digits and underscores, and don't start with a digit (`^[A-Za-z_][A-Za-z0-9_]*$`). For repository variables, only the web form checks this (the organization variables API checks it too); the variables API accepts other names, which expressions may not be able to read, so keep to the same format. | |
| 23 | 24 | - Names are stored in uppercase: saving `api_key` creates `API_KEY`. In expressions, `secrets.api_key` and `secrets.API_KEY` both find it. |
| 24 | -- Repository and environment secrets, and environment variables, can't start with `GITHUB_`. Saving one fails with `invalid secret name` (or `invalid variable name`). | |
| 25 | +- Repository and environment secrets, and environment and organization variables, can't start with `GITHUB_`. Saving one fails with `invalid secret name` (or `invalid variable name`). | |
| 25 | 26 | - Don't name a secret `DIFFINERY_TOKEN` or `GITHUB_TOKEN`: in a job, both always hold the job's own token, which replaces any secret with the same name. |
| 26 | 27 | |
| 27 | 28 | ## Repository secrets |
| @@ -120,7 +121,7 @@ | ||
| 120 | 121 | | `GET /api/v3/repos/{owner}/{repo}/actions/variables` | write role | Returns `{"total_count": 1, "variables": [{"name": "...", "value": "..."}]}`. Takes `per_page` (default 30, at most 100) and `page`. | |
| 121 | 122 | | `GET /api/v3/repos/{owner}/{repo}/actions/variables/{name}` | write role | Returns `{"name", "value", "created_at": null, "updated_at": null}`, or `404 Not Found` | |
| 122 | 123 | | `POST /api/v3/repos/{owner}/{repo}/actions/variables` with `{"name": "...", "value": "..."}` | admin role, people only | Creates a variable (201). An existing name fails with `409 Already exists - Variable already exists`; a missing field with `422 name and value are required`. | |
| 123 | -| `PATCH /api/v3/repos/{owner}/{repo}/actions/variables/{name}` with `{"value": "..."}` and optionally a new `"name"` | admin role, people only | Updates or renames a variable (204), or `404 Not Found` | | |
| 124 | +| `PATCH /api/v3/repos/{owner}/{repo}/actions/variables/{name}` with `{"value": "..."}` and optionally a new `"name"` | admin role, people only | Updates or renames a variable (204), or `404 Not Found`. Renaming onto a name that already exists fails with `409 Already exists - Variable already exists`. A body with only `name` keeps the value. | | |
| 124 | 125 | | `PUT /api/v3/repos/{owner}/{repo}/actions/variables/{name}` with `{"value": "..."}` | admin role, people only | Creates or replaces a variable (204) | |
| 125 | 126 | | `DELETE /api/v3/repos/{owner}/{repo}/actions/variables/{name}` | admin role, people only | Deletes a variable (204) | |
| 126 | 127 | |
| @@ -135,6 +136,65 @@ | ||
| 135 | 136 | |
| 136 | 137 | > **Note:** Variable names are stored in uppercase, but the API's `DELETE` matches the name exactly. Send the uppercase name (`DEPLOY_REGION`), or the request returns 204 without deleting anything. |
| 137 | 138 | |
| 139 | +## Organization variables | |
| 140 | + | |
| 141 | +Organization variables are available as `${{ vars.NAME }}` to every workflow in every repository the organization owns, including repositories created later. Use them for settings that many repositories share, such as a registry URL or a cloud region. A repository or environment variable with the same name wins (see [Precedence](#precedence)). | |
| 142 | + | |
| 143 | +They're managed with the REST API only: there's no web page for them and no CLI command (`diffinery variable` works on one repository). A [GitHub organization import](/docs/migrate-from-github) also adds the GitHub organization's variables that the Diffinery organization doesn't have yet, and leaves existing ones unchanged. Imported variables reach every repository, whatever repository selection they had on GitHub. | |
| 144 | + | |
| 145 | +### Who can manage organization variables | |
| 146 | + | |
| 147 | +| Action | Who | Token | | |
| 148 | +|---|---|---| | |
| 149 | +| List variables and read their values | Organization owners and admins, and site admins | Any scope | | |
| 150 | +| Create, update or delete a variable | Organization owners and admins, and site admins (people only) | The `admin` scope, not restricted to repositories | | |
| 151 | + | |
| 152 | +Other members get `403 organization admins only`, and accounts outside the organization get `404 not found`. Agents get `403 organization admins only` (or `404 not found` when neither the agent nor its owner belongs to the organization), whatever their role: agents can't read, create, change or delete organization variables through these endpoints. | |
| 153 | + | |
| 154 | +### Organization variables with the API | |
| 155 | + | |
| 156 | +| Request | Does | | |
| 157 | +|---|---| | |
| 158 | +| `GET /api/v3/orgs/{org}/actions/variables` | Returns `{"total_count": 1, "variables": [{"name": "...", "value": "...", "visibility": "all", "created_at": "...", "updated_at": "..."}]}`, sorted by name. The list isn't paged. | | |
| 159 | +| `GET /api/v3/orgs/{org}/actions/variables/{name}` | Returns one variable in the same shape, or `404 variable not found`. The name is matched case-insensitively. | | |
| 160 | +| `POST /api/v3/orgs/{org}/actions/variables` with `{"name": "...", "value": "..."}` | Creates a variable, or replaces the value of an existing one. Returns `201` with an empty body. | | |
| 161 | +| `PATCH /api/v3/orgs/{org}/actions/variables/{name}` with `{"value": "..."}` | Sets the value, creating the variable if it doesn't exist. Returns `204`. | | |
| 162 | +| `DELETE /api/v3/orgs/{org}/actions/variables/{name}` | Deletes the variable (case-insensitive name). Returns `204`, also when there was no such variable. | | |
| 163 | + | |
| 164 | +```sh | |
| 165 | +# Create or replace | |
| 166 | +curl -X POST https://diffinery.com/api/v3/orgs/acme/actions/variables \ | |
| 167 | + -H "Authorization: Bearer dfy_..." \ | |
| 168 | + -H "Content-Type: application/json" \ | |
| 169 | + -d '{"name": "NPM_REGISTRY", "value": "https://registry.npmjs.org"}' | |
| 170 | + | |
| 171 | +# List with values | |
| 172 | +curl https://diffinery.com/api/v3/orgs/acme/actions/variables \ | |
| 173 | + -H "Authorization: Bearer dfy_..." | |
| 174 | + | |
| 175 | +# Delete | |
| 176 | +curl -X DELETE https://diffinery.com/api/v3/orgs/acme/actions/variables/NPM_REGISTRY \ | |
| 177 | + -H "Authorization: Bearer dfy_..." | |
| 178 | +``` | |
| 179 | + | |
| 180 | +Names must match `^[A-Za-z_][A-Za-z0-9_]*$` and can't start with `GITHUB_`; otherwise the request fails with `422 invalid variable name`. Names are stored in uppercase. The value must be a string of at most 48 KB (49,152 characters); otherwise the request fails with `422 value must be a string of at most 48 KB`. In the responses, `created_at` is the time of the last change, the same as `updated_at`. | |
| 181 | + | |
| 182 | +Differences from GitHub: | |
| 183 | + | |
| 184 | +- There is no per-repository selection: `visibility` is always `all`, a `visibility` or `selected_repository_ids` field in the request is ignored, and there are no `/repositories` endpoints for a variable. | |
| 185 | +- `POST` on an existing name replaces the value instead of failing with `409`, and `PATCH` creates a missing variable instead of failing with `404`. | |
| 186 | +- `PATCH` can't rename a variable; a `name` in the body is ignored. Delete it and create it under the new name. | |
| 187 | +- The list isn't paged; `per_page` and `page` are ignored. | |
| 188 | + | |
| 189 | +### Organization and repository variables with the same name | |
| 190 | + | |
| 191 | +A repository's own variable list (the **Variables** section of **Settings → Secrets and variables**, `diffinery variable list`, and `GET /api/v3/repos/{owner}/{repo}/actions/variables`) shows only the repository's own variables, never the organization's. The single-variable repository endpoints do see organization variables: | |
| 192 | + | |
| 193 | +- `GET /api/v3/repos/{owner}/{repo}/actions/variables/{name}` returns the organization variable's value when the repository has no variable of that name. Anyone with the write role on the repository can read it this way. | |
| 194 | +- `POST /api/v3/repos/{owner}/{repo}/actions/variables` fails with `409 Already exists - Variable already exists` for a name the organization already has. To give one repository its own value, use `PUT /api/v3/repos/{owner}/{repo}/actions/variables/{name}`, `diffinery variable set`, or the web form instead. | |
| 195 | +- `PATCH /api/v3/repos/{owner}/{repo}/actions/variables/{name}` for a name only the organization has creates a repository variable with the new value. | |
| 196 | +- `DELETE` on a repository variable never removes an organization variable. | |
| 197 | + | |
| 138 | 198 | ## Environment secrets and variables |
| 139 | 199 | |
| 140 | 200 | An [environment](/docs/actions/environments) such as `production` can have its own secrets and variables. Only jobs that name the environment with `environment:` receive them: |
| @@ -218,8 +278,14 @@ | ||
| 218 | 278 | 3. Repository secrets (in a called reusable workflow: the secrets the caller passed). |
| 219 | 279 | 4. Organization secrets. |
| 220 | 280 | |
| 221 | -For variables, an environment variable replaces a repository variable with the same name in a job that uses the environment. | |
| 281 | +For variables, from highest to lowest: | |
| 222 | 282 | |
| 283 | +1. Environment variables, in a job that uses the environment. | |
| 284 | +2. Repository variables. | |
| 285 | +3. Organization variables (repositories owned by an organization only). | |
| 286 | + | |
| 287 | +In places evaluated before a job starts, such as `jobs.<id>.if` and `runs-on`, `vars` holds only repository and organization variables; environment variables are added when the job starts. A change to an organization variable reaches the organization's repositories right away, so jobs that start afterwards see the new value. | |
| 288 | + | |
| 223 | 289 | ### Which runs receive secrets |
| 224 | 290 | |
| 225 | 291 | Every workflow run in the repository receives its repository and organization secrets, whatever the event: `push`, `pull_request`, `pull_request_target`, `merge_group`, `workflow_dispatch`, `schedule`, `workflow_run`, `release` and `repository_dispatch`. Environment secrets follow stricter rules (see [Rules for environment secrets](#rules-for-environment-secrets)). |
| @@ -257,44 +323,56 @@ | ||
| 257 | 323 | |
| 258 | 324 | ### Debug logging |
| 259 | 325 | |
| 260 | -Set a secret or a variable named `ACTIONS_STEP_DEBUG` to `true` to show `::debug::` messages in the log and set `RUNNER_DEBUG=1` for steps. Turn it off again when you're done. | |
| 326 | +Set a secret or a variable named `ACTIONS_STEP_DEBUG` to `true` to show `::debug::` messages in the log, set `RUNNER_DEBUG=1` for steps and make `runner.debug` `1`. A secret or variable named `ACTIONS_RUNNER_DEBUG` set to `true` also sets `RUNNER_DEBUG=1`, but doesn't change `runner.debug`. Turn them off again when you're done. | |
| 261 | 327 | |
| 262 | 328 | ## GITHUB_TOKEN and the job token |
| 263 | 329 | |
| 264 | -Every job gets its own token, created when the job starts and revoked when it finishes. It's available as `${{ secrets.GITHUB_TOKEN }}`, `${{ secrets.DIFFINERY_TOKEN }}` and `${{ github.token }}`, and `actions/checkout` uses it by default. | |
| 330 | +Every job gets its own token, created when the job starts and revoked when it finishes. It's available as `${{ secrets.GITHUB_TOKEN }}`, `${{ secrets.DIFFINERY_TOKEN }}` and `${{ github.token }}`, and `actions/checkout` uses it by default. It always replaces a repository or organization secret with the same name. | |
| 265 | 331 | |
| 266 | -What it can do: | |
| 332 | +What the token may do follows the job's `permissions:`, as on GitHub: | |
| 267 | 333 | |
| 268 | 334 | | Access | Allowed | |
| 269 | 335 | |---|---| |
| 270 | -| Clone and fetch the repository that runs the workflow (including Git LFS downloads) | Yes | | |
| 271 | -| Read that repository through the REST API | Yes | | |
| 272 | -| Push, or write through the API (comments, labels, pull requests, releases, statuses) | **No**, whatever the workflow's `permissions` say | | |
| 273 | -| Read or write any other repository, even a public one | No | | |
| 274 | -| Use the container registry | Decided by the `packages` permission (see below) | | |
| 275 | -| Request an OIDC token | Only with `permissions: id-token: write` | | |
| 336 | +| Clone and fetch the repository that runs the workflow (including Git LFS downloads) | With `contents: read` or `write`, or always when the repository is public | | |
| 337 | +| Push to the repository | With `contents: write`. Branch protection applies, and changes to files under `.github/workflows/` are always refused. | | |
| 338 | +| Read the repository through the REST API | With the scope each endpoint needs, for example `issues: read` for issues. A public repository can always be read. | | |
| 339 | +| Write through the REST API (comments, labels, pull requests, releases, statuses, deployments) | With the matching scope at `write`, for example `pull-requests: write` to comment on a pull request | | |
| 340 | +| Read another repository | Only if that repository is public | | |
| 341 | +| Write to another repository | No | | |
| 342 | +| Use the container registry | Decided by the `packages` scope: `write` pushes and pulls, `read` pulls, `none` neither. Pushes reach only packages linked to the workflow's repository, or new packages, which get linked to it. | | |
| 343 | +| Request an OIDC token | Only with `id-token: write`. See [OpenID Connect (OIDC)](/docs/actions/oidc). | | |
| 344 | +| Manage secrets, variables, environments or repository settings | No | | |
| 276 | 345 | |
| 277 | -How the workflow's `permissions` apply: | |
| 346 | +A job without any `permissions:` (in the job or the workflow) gets the repository's default: read access to contents and packages for repositories created after this setting was introduced, and write access to everything except `id-token` for older ones. An admin can change it. The full rules, the scope each endpoint needs and the error messages are on [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions). | |
| 278 | 347 | |
| 279 | -| Permission | Effect | | |
| 280 | -|---|---| | |
| 281 | -| `packages` | Controls the registry. No `permissions` block or `write-all`: push and pull. `packages: write`: push and pull. Pushes reach only packages linked to the workflow's repository, or new packages, which get linked to it. `packages: read` or `read-all`: pull. A mapping without `packages`: none. A job's `permissions` replaces the workflow's. | | |
| 282 | -| `id-token: write` | Lets the job request OIDC tokens (in the workflow's or the job's `permissions`, or `write-all`). See [OpenID Connect (OIDC)](/docs/actions/oidc). | | |
| 283 | -| `contents`, `pull-requests`, `issues` and the other scopes | Accepted for compatibility; they don't change what the token can do. | | |
| 348 | +```yaml | |
| 349 | +permissions: | |
| 350 | + contents: write | |
| 351 | + pull-requests: write | |
| 284 | 352 | |
| 285 | -To push commits, comment on pull requests or reach other repositories from a workflow, store a personal access token (created by a person, with the `write` scope and restricted to the repositories it needs) as a secret and use it instead: | |
| 353 | +jobs: | |
| 354 | + bump: | |
| 355 | + runs-on: ubuntu-latest | |
| 356 | + steps: | |
| 357 | + - uses: actions/checkout@v4 | |
| 358 | + - run: | | |
| 359 | + git config user.name "diffinery-actions[bot]" | |
| 360 | + git config user.email "diffinery-actions[bot]@users.noreply.diffinery.com" | |
| 361 | + npm version patch --no-git-tag-version | |
| 362 | + git switch -c bump-version | |
| 363 | + git commit -am "Bump version" | |
| 364 | + git push origin bump-version | |
| 365 | +``` | |
| 286 | 366 | |
| 367 | +Use a personal access token stored as a secret instead when a workflow needs something the job token can't do, such as changing workflow files, writing to another repository, or pushing to a branch whose protection doesn't let the job push: | |
| 368 | + | |
| 287 | 369 | ```yaml |
| 288 | 370 | - uses: actions/checkout@v4 |
| 289 | 371 | with: |
| 290 | 372 | token: ${{ secrets.BOT_TOKEN }} |
| 291 | -- run: | | |
| 292 | - git config user.name "release-bot" | |
| 293 | - git config user.email "release-bot@users.noreply.diffinery.com" | |
| 294 | - git commit -am "Bump version" && git push | |
| 295 | 373 | ``` |
| 296 | 374 | |
| 297 | -Pushes made this way still follow branch protection. See [Personal access tokens](/docs/accounts/tokens). | |
| 375 | +Create the token as a person, with the `write` scope and restricted to the repositories it needs. Pushes made this way still follow branch protection. See [Personal access tokens](/docs/accounts/tokens). | |
| 298 | 376 | |
| 299 | 377 | ## Encryption at rest |
| 300 | 378 | |
| @@ -312,22 +390,36 @@ | ||
| 312 | 390 | - The organization secret endpoints (listing included) return `403 agents cannot manage secrets` for any agent's token. |
| 313 | 391 | - Setting and deleting repository secrets (`PUT` and `DELETE /api/v3/repos/{owner}/{repo}/actions/secrets/{name}`) and environment secrets refuse an agent's token: an agent with the admin role gets `403 agents cannot manage secrets`; with the write role, the request already fails with `403 admin access required`. |
| 314 | 392 | - Creating, changing and deleting repository variables (`POST`, `PATCH`, `PUT` and `DELETE` on `/api/v3/repos/{owner}/{repo}/actions/variables`) and environment variables refuse an agent's token the same way, with `403 agents cannot manage variables` for an agent with the admin role. |
| 393 | +- The organization variable endpoints (`/api/v3/orgs/{org}/actions/variables`, listing included) return `403 organization admins only` for any agent's token, or `404 not found` when neither the agent nor its owner belongs to the organization. A GitHub import started by an agent skips variables, secret names and environments. | |
| 315 | 394 | - Agents can't create, change or delete environments or their branch policies (`403 only repository admins (people, not agents) can manage environments` for an agent with the admin role). |
| 316 | -- Listing repository secret names and environment secret names needs the admin role; listing and reading variables needs the write role, so an agent with write access can read variable values. An agent has the write role unless a person gave it more (its **Maximum role**, or an explicit admin role on the repository). | |
| 395 | +- Listing repository secret names and environment secret names needs the admin role; listing and reading variables needs the write role, so an agent with write access can read variable values, including an organization variable's value through `GET /api/v3/repos/{owner}/{repo}/actions/variables/{name}`. An agent has the write role unless a person gave it more (its **Maximum role**, or an explicit admin role on the repository). | |
| 317 | 396 | |
| 318 | -Agents can still push branches and open pull requests, and the workflows that run for them receive repository and organization secrets (see [Which runs receive secrets](#which-runs-receive-secrets)). Treat write access for an agent as access to those secrets. Environment secrets are different: runs started by agents only get them from environments set to protected branches only, and never on pull request refs. See [Agents](/docs/agents/overview). | |
| 397 | +Agents can still push branches and open pull requests, and the workflows that run for them receive repository and organization secrets (see [Which runs receive secrets](#which-runs-receive-secrets)). Treat write access for an agent as access to those secrets. Environment secrets are different: runs whose code came from an agent only get them from environments set to protected branches only, and never on pull request refs. In those runs the job token also acts as an agent, so it can't push to protected or default branches. See [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions#runs-whose-code-came-from-an-agent) and [Agents](/docs/agents/overview). | |
| 319 | 398 | |
| 320 | 399 | ## Audit log |
| 321 | 400 | |
| 322 | -Changes made on the settings page are recorded in the audit log as `repo.secret.set`, `repo.secret.delete`, `repo.variable.set` and `repo.variable.delete`, with the name but never the value. Environment changes made with the API are recorded as `environment.secret.set`, `environment.secret.delete`, `environment.update`, `environment.delete`, `environment.branch_policy.create` and `environment.branch_policy.delete`. See [Audit log](/docs/security/audit-log). | |
| 401 | +Changes are recorded in the audit log with the name but never the value: | |
| 323 | 402 | |
| 403 | +| Change | Audit action | | |
| 404 | +|---|---| | |
| 405 | +| Set or delete a repository secret | `repo.secret.set`, `repo.secret.delete` | | |
| 406 | +| Create, update, rename or delete a repository variable | `repo.variable.set` (a rename adds `renamed_from` with the old name), `repo.variable.delete` | | |
| 407 | +| Set or delete an organization secret | `org.secret.set`, `org.secret.delete` | | |
| 408 | +| Create, update or delete an organization variable | `org.variable_set`, `org.variable_delete`, with the organization as the target and `name` in the details | | |
| 409 | +| Set or delete an environment secret | `environment.secret.set`, `environment.secret.delete` | | |
| 410 | +| Create, update, rename or delete an environment variable | `environment.variable.set` (a rename adds `renamed_from`), `environment.variable.delete` | | |
| 411 | +| Change an environment or its branch policies | `environment.update`, `environment.delete`, `environment.branch_policy.create`, `environment.branch_policy.delete` | | |
| 412 | + | |
| 413 | +Repository and organization secret changes made with the API or the CLI include `"via": "api"` in the entry's details; changes made on the settings page don't, and neither do organization variable changes. When a GitHub organization import adds organization variables, each one is recorded as `org.variable_set` by the person who started the import, with the GitHub organization as the target and the import's id, the Diffinery organization (`org`) and `name` in the details. See [Audit log](/docs/security/audit-log). | |
| 414 | + | |
| 324 | 415 | ## Limits |
| 325 | 416 | |
| 326 | 417 | | Limit | Value | |
| 327 | 418 | |---|---| |
| 328 | -| Name format | `^[A-Za-z_][A-Za-z0-9_]*$`, stored uppercase; repository and environment secrets and environment variables can't start with `GITHUB_` | | |
| 419 | +| Name format | `^[A-Za-z_][A-Za-z0-9_]*$`, stored uppercase; repository and environment secrets, and environment and organization variables, can't start with `GITHUB_` | | |
| 329 | 420 | | Shortest value that is masked | 3 characters | |
| 330 | -| Organization variables | Not available | | |
| 421 | +| Organization variable value | 48 KB (49,152 characters) | | |
| 422 | +| Number of organization variables | No limit of its own | | |
| 331 | 423 | | Number and size of secrets | Diffinery doesn't enforce a limit of its own; every secret is decrypted for every job, so keep them few and small | |
| 332 | 424 | |
| 333 | 425 | ## Troubleshooting |
| @@ -343,13 +435,19 @@ | ||
| 343 | 435 | | `403 agents cannot manage variables` | Agents can't create, change or delete variables. Use a person's token. | |
| 344 | 436 | | `403 admin access required` | The token's owner isn't a repository admin, or the token lacks the `admin` scope. | |
| 345 | 437 | | `403 this token needs the write scope` | The token has only the `read` scope and can't change anything. Use an `admin` token. | |
| 346 | -| `403 this token needs the admin scope` | Organization secrets need an `admin`-scoped token, even for listing. | | |
| 347 | -| `403 this token is limited to repositories and can't change account settings` | Organization secrets need a token without a repository list. | | |
| 348 | -| `403 organization admins only` | Only organization owners and admins manage organization secrets. | | |
| 438 | +| `403 this token needs the admin scope` | Organization secrets need an `admin`-scoped token, even for listing. Creating, changing and deleting organization variables needs one too. | | |
| 439 | +| `403 this token is limited to repositories and can't change account settings` | Organization secrets, and changes to organization variables, need a token without a repository list. | | |
| 440 | +| `403 organization admins only` | Only organization owners and admins (people) manage organization secrets and organization variables, and only they can read organization variables. | | |
| 441 | +| `422 invalid variable name` | The organization variable's name has characters other than letters, digits and `_`, starts with a digit, or starts with `GITHUB_`. | | |
| 442 | +| `422 value must be a string of at most 48 KB` | The organization variable request has no string `value`, or the value is longer than 49,152 characters. | | |
| 443 | +| `404 variable not found` | The organization has no variable of that name. | | |
| 349 | 444 | | `403 only repository admins (people, not agents) can manage environments` | Environments and their branch policies need a person with the admin role. | |
| 350 | 445 | | `404 environment not found` | Create the environment with `PUT .../environments/{name}` before adding secrets or variables. | |
| 351 | 446 | | An environment secret is empty in the job | The run is on a pull request ref, or an agent started it and the environment isn't set to protected branches only, or the job has no `environment:`. | |
| 352 | 447 | | `<ref> is not allowed to deploy to <env> due to environment protection rules.` | The environment's branch policy doesn't allow the ref. See [Deployments and environments](/docs/actions/environments#deployment-branch-policies). | |
| 353 | 448 | | A secret is empty in the workflow | Check the name, that it's set on this repository, its organization or the job's environment, and, in a reusable workflow, that the caller passes it. | |
| 354 | 449 | | `***` appears in output where you expected text | The text contains a secret's value (or a value shorter than you think matches part of it). Rename or change the secret, or don't print that text. | |
| 355 | -| `git push` with `GITHUB_TOKEN` fails with `403 write access required` | The job token is read-only. Use a personal token stored as a secret. | | |
| 450 | +| `git push` with `GITHUB_TOKEN` fails with `403` | The job has no `contents: write`. Add it to the job's or the workflow's `permissions:`, or ask an admin to change the repository default. See [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions). | | |
| 451 | +| `Resource not accessible by integration (the workflow's GITHUB_TOKEN needs permissions: ...)` | The job's `permissions:` don't include the scope that endpoint needs. Add the scope the message names. | | |
| 452 | +| `409 Already exists - Variable already exists` | A variable with that name already exists in the repository, or (for `POST` on a repository) the organization has a variable with that name. Update it with `PATCH` or `PUT`, use `PUT` to give the repository its own value, or choose another name. | | |
| 453 | +| A variable is empty in the workflow | Check the name and that it's set on this repository, its organization or the job's environment. Environment variables aren't available in `jobs.<id>.if` or `runs-on`. | |
addedapps/diffinery/docs/actions/workflow-permissions.md+343−0View file
| @@ -1,0 +1,343 @@ | ||
| 1 | +--- | |
| 2 | +title: GITHUB_TOKEN permissions | |
| 3 | +description: What a workflow job's GITHUB_TOKEN may do on Diffinery, how permissions are resolved, the repository default, and the rules no permission can lift. | |
| 4 | +--- | |
| 5 | + | |
| 6 | +Every workflow job gets its own token, `GITHUB_TOKEN`, that works against Diffinery's REST API and git server. What the token may do follows GitHub's `permissions:` model: each API endpoint needs a permission scope, and the job's effective `permissions:` decide which scopes the token has. This page explains how those permissions are worked out, how to change the repository's default, and the rules that apply whatever a workflow asks for. | |
| 7 | + | |
| 8 | +## The job token | |
| 9 | + | |
| 10 | +The token is available as `secrets.GITHUB_TOKEN`, `github.token` and, under a Diffinery name, `secrets.DIFFINERY_TOKEN`. It isn't an environment variable unless you set one: | |
| 11 | + | |
| 12 | +```yaml | |
| 13 | +- run: gh api repos/${{ github.repository }} | |
| 14 | + env: | |
| 15 | + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| 16 | +``` | |
| 17 | + | |
| 18 | +- It works only while the job runs. When the job completes, the token stops working. | |
| 19 | +- It acts in the repository as `diffinery-actions[bot]`, so issues, comments and labels it creates show that name. | |
| 20 | +- Its role is `write` when any scope other than `id-token` is `write`, and `read` otherwise. It never has the `maintain` or `admin` role, so [branch protection](/docs/repositories/branch-protection) always applies to it. | |
| 21 | +- Each request is then checked against the scope its endpoint needs (see [What each endpoint needs](#what-each-endpoint-needs)). | |
| 22 | + | |
| 23 | +## How permissions are resolved | |
| 24 | + | |
| 25 | +A job's effective permissions come from the first of these that exists: | |
| 26 | + | |
| 27 | +1. The job's own `permissions:` (`jobs.<id>.permissions`). It replaces the workflow's, it isn't merged with it. | |
| 28 | +2. The workflow's top-level `permissions:`. | |
| 29 | +3. The repository default, set by an admin (see [The repository default](#the-repository-default)). | |
| 30 | + | |
| 31 | +Inside a [reusable workflow](/docs/actions/reusable-workflows), the calling job's permissions take the place of the repository default and also cap everything the called workflow declares. See [Reusable workflows](#reusable-workflows). | |
| 32 | + | |
| 33 | +### Scopes | |
| 34 | + | |
| 35 | +`permissions:` takes these scopes, the same as GitHub's: | |
| 36 | + | |
| 37 | +| Scope | What it covers on Diffinery | | |
| 38 | +| --- | --- | | |
| 39 | +| `actions` | Workflows, runs, jobs, logs, artifacts and caches (`/actions/...`), including starting a `workflow_dispatch` run | | |
| 40 | +| `attestations` | `/attestations` | | |
| 41 | +| `checks` | Check runs and check suites | | |
| 42 | +| `contents` | Git fetch and push, files, commits, branches, tags, releases and release assets, compare, archives, commit comments, `repository_dispatch`, merging pull requests | | |
| 43 | +| `deployments` | Deployments, deployment statuses, and reading environments | | |
| 44 | +| `discussions` | `/discussions` | | |
| 45 | +| `id-token` | Requesting an [OIDC](/docs/actions/oidc) token. Only `write` (or `none`) has a meaning. | | |
| 46 | +| `issues` | Issues, issue comments, labels, milestones and assignees | | |
| 47 | +| `models` | Accepted for compatibility. Only `read` or `none`. | | |
| 48 | +| `packages` | The [container registry](/docs/packages/container-registry) | | |
| 49 | +| `pages` | `/pages` | | |
| 50 | +| `pull-requests` | Pull requests, reviews and review comments, and issue endpoints used on pull requests | | |
| 51 | +| `repository-projects` | `/projects` | | |
| 52 | +| `security-events` | `/code-scanning`, `/secret-scanning`, `/dependabot` | | |
| 53 | +| `statuses` | Commit statuses | | |
| 54 | + | |
| 55 | +Each scope takes `read`, `write` or `none`. `write` includes `read`. An unknown scope gives the warning `unknown permission scope '<scope>'` when the workflow is parsed, and an invalid level fails the file with `permission '<scope>' must be 'read', 'write' or 'none'`. | |
| 56 | + | |
| 57 | +### Shorthand values | |
| 58 | + | |
| 59 | +| Value | Result | | |
| 60 | +| --- | --- | | |
| 61 | +| `read-all` | Every scope `read`, except `id-token`, which gets nothing. | | |
| 62 | +| `write-all` | Every scope `write`, except `models`, which stays `read`. Includes `id-token: write`. | | |
| 63 | +| `{}` | Every scope `none`. The token can still read what anyone may read in a public repository. | | |
| 64 | +| A mapping | The scopes you list get the levels you give. **Every scope you don't list is `none`.** | | |
| 65 | + | |
| 66 | +Two scopes have special rules: | |
| 67 | + | |
| 68 | +- `id-token: read` grants nothing. OIDC needs `id-token: write`. | |
| 69 | +- `models: write` is lowered to `read`. | |
| 70 | + | |
| 71 | +Any other value of `permissions:` fails the file with `permissions must be 'read-all', 'write-all' or a mapping of scope: read|write|none`. | |
| 72 | + | |
| 73 | +### Examples | |
| 74 | + | |
| 75 | +A workflow that only reads code, with one job that comments on pull requests: | |
| 76 | + | |
| 77 | +```yaml | |
| 78 | +permissions: | |
| 79 | + contents: read | |
| 80 | + | |
| 81 | +jobs: | |
| 82 | + test: | |
| 83 | + runs-on: ubuntu-latest | |
| 84 | + steps: | |
| 85 | + - uses: actions/checkout@v4 | |
| 86 | + - run: npm ci && npm test | |
| 87 | + | |
| 88 | + comment: | |
| 89 | + needs: test | |
| 90 | + runs-on: ubuntu-latest | |
| 91 | + permissions: | |
| 92 | + contents: read # the job's map replaces the workflow's, so repeat contents | |
| 93 | + pull-requests: write | |
| 94 | + steps: | |
| 95 | + - uses: actions/github-script@v7 | |
| 96 | + with: | |
| 97 | + script: | | |
| 98 | + await github.rest.issues.createComment({ | |
| 99 | + owner: context.repo.owner, | |
| 100 | + repo: context.repo.repo, | |
| 101 | + issue_number: context.issue.number, | |
| 102 | + body: "Tests passed.", | |
| 103 | + }); | |
| 104 | +``` | |
| 105 | + | |
| 106 | +A release job that pushes a tag and uploads release assets needs `contents: write`. A job that pushes an image needs `packages: write`, and because naming any scope sets the others to `none`, it also needs `contents: read` to check out a private repository: | |
| 107 | + | |
| 108 | +```yaml | |
| 109 | +jobs: | |
| 110 | + image: | |
| 111 | + runs-on: ubuntu-latest | |
| 112 | + permissions: | |
| 113 | + contents: read | |
| 114 | + packages: write | |
| 115 | +``` | |
| 116 | + | |
| 117 | +## The repository default | |
| 118 | + | |
| 119 | +When neither the job nor the workflow sets `permissions:`, the job gets the repository default. It's one of two settings, the same as GitHub's "Workflow permissions" setting: | |
| 120 | + | |
| 121 | +| `default_workflow_permissions` | Token permissions | | |
| 122 | +| --- | --- | | |
| 123 | +| `read` | `contents: read` and `packages: read`. Every other scope `none`. | | |
| 124 | +| `write` | Every scope `write` (`models` `read`), except `id-token`, which gets nothing. | | |
| 125 | + | |
| 126 | +| Repository | Default | | |
| 127 | +| --- | --- | | |
| 128 | +| Created after this setting was added | `read`, and pull request creation and approval off | | |
| 129 | +| Created before | `write`, and pull request creation and approval off | | |
| 130 | + | |
| 131 | +A second setting, `can_approve_pull_request_reviews`, decides whether job tokens may open pull requests and approve or request changes on them (see [Creating and approving pull requests](#creating-and-approving-pull-requests)). It's `false` by default. | |
| 132 | + | |
| 133 | +### Read and change the setting | |
| 134 | + | |
| 135 | +In the web UI, open the repository's **Settings → Actions** tab (`/{owner}/{repo}/settings/actions`). Under **Workflow permissions**, pick **Read repository contents and packages permissions** or **Read and write permissions**, check or clear **Allow GitHub Actions to create and approve pull requests**, and click **Save**. You see "Actions settings saved." The tab needs the **admin** role on the repository. | |
| 136 | + | |
| 137 | +You can also read and change it with the REST API or the CLI. Both need the **admin** role on the repository, and only people can change it: an agent's token gets `403 only people can change what workflows are allowed to do`. | |
| 138 | + | |
| 139 | +Read the current setting: | |
| 140 | + | |
| 141 | +```http | |
| 142 | +GET /api/v3/repos/acme/website/actions/permissions/workflow | |
| 143 | +Authorization: Bearer dfy_... | |
| 144 | +``` | |
| 145 | + | |
| 146 | +```json | |
| 147 | +{ | |
| 148 | + "default_workflow_permissions": "read", | |
| 149 | + "can_approve_pull_request_reviews": false | |
| 150 | +} | |
| 151 | +``` | |
| 152 | + | |
| 153 | +Change it with `PUT`. Send one or both fields; a field you leave out keeps its value. The response is `204 No Content`: | |
| 154 | + | |
| 155 | +```sh | |
| 156 | +curl -fsS -X PUT https://diffinery.com/api/v3/repos/acme/website/actions/permissions/workflow \ | |
| 157 | + -H "Authorization: Bearer dfy_..." \ | |
| 158 | + -H "Content-Type: application/json" \ | |
| 159 | + -d '{"default_workflow_permissions": "write", "can_approve_pull_request_reviews": true}' | |
| 160 | +``` | |
| 161 | + | |
| 162 | +With the CLI (install it with `curl -fsSL https://diffinery.com/-/cli/diffinery.mjs -o ~/.local/bin/diffinery && chmod +x ~/.local/bin/diffinery`; it needs Node.js 22 or later): | |
| 163 | + | |
| 164 | +```sh | |
| 165 | +diffinery api GET /repos/acme/website/actions/permissions/workflow | |
| 166 | +diffinery api PUT /repos/acme/website/actions/permissions/workflow -f default_workflow_permissions=write | |
| 167 | +``` | |
| 168 | + | |
| 169 | +`-f` sends string values only. To change `can_approve_pull_request_reviews`, which is a boolean, send a JSON body with `--input` (a file, or `-` for stdin): | |
| 170 | + | |
| 171 | +```sh | |
| 172 | +echo '{"can_approve_pull_request_reviews": true}' \ | |
| 173 | + | diffinery api PUT /repos/acme/website/actions/permissions/workflow --input - | |
| 174 | +``` | |
| 175 | + | |
| 176 | +Every change is recorded in the [audit log](/docs/security/audit-log) as `repo.actions_workflow_settings`, with the new values. | |
| 177 | + | |
| 178 | +| Status and message | Cause | | |
| 179 | +| --- | --- | | |
| 180 | +| `403 admin access required` | Your role on the repository is below admin, or your token's scope doesn't reach admin. | | |
| 181 | +| `403 only people can change what workflows are allowed to do` | The request used an agent's token. | | |
| 182 | +| `422 a JSON body is required` | The body is missing or isn't JSON. | | |
| 183 | +| `422 default_workflow_permissions must be read or write` | Another value was sent. | | |
| 184 | +| `422 can_approve_pull_request_reviews must be a boolean` | A string such as `"true"` was sent instead of `true`. | | |
| 185 | + | |
| 186 | +> **Note:** The setting is read when a job starts. Jobs that are already running keep the permissions they started with. | |
| 187 | + | |
| 188 | +## What each endpoint needs | |
| 189 | + | |
| 190 | +Each request the token makes to `/api/v3/repos/{owner}/{repo}/...`, to the git server, or to a repository's web pages is matched to a scope by the first path segment after the repository. `GET`, `HEAD` and `OPTIONS` need `read`; other methods need `write`. Git fetches (`git-upload-pack`) and Git LFS downloads are reads even though they use `POST`. | |
| 191 | + | |
| 192 | +| Path after `/repos/{owner}/{repo}` | Scope | | |
| 193 | +| --- | --- | | |
| 194 | +| `/issues` (list or create issues) | `issues` | | |
| 195 | +| `/issues/...`, `/labels`, `/milestones`, `/assignees` | `issues` or `pull-requests` (either one is enough) | | |
| 196 | +| `/pulls`, `/pulls/...` | `pull-requests` | | |
| 197 | +| `/pulls/{number}/merge`, `/pulls/{number}/update-branch`, `/pulls/{number}/auto-merge` | `contents` | | |
| 198 | +| `/statuses/...`, `/commits/{sha}/status`, `/commits/{sha}/statuses` | `statuses` | | |
| 199 | +| `/check-runs`, `/check-suites`, `/commits/{sha}/check-runs`, `/commits/{sha}/check-suites` | `checks` | | |
| 200 | +| `/deployments`, reading `/environments` | `deployments` | | |
| 201 | +| `/actions/...` | `actions` | | |
| 202 | +| `/pages` | `pages` | | |
| 203 | +| `/discussions` | `discussions` | | |
| 204 | +| `/code-scanning`, `/secret-scanning`, `/dependabot` | `security-events` | | |
| 205 | +| `/packages` | `packages` | | |
| 206 | +| `/projects` | `repository-projects` | | |
| 207 | +| `/attestations` | `attestations` | | |
| 208 | +| `/contents`, `/commits`, `/compare`, `/git`, `/branches`, `/tags`, `/releases`, `/merges`, `/tarball`, `/zipball`, `/readme`, `/dispatches`, `/comments`, `/merge-upstream`, `/languages`, `/contributors`, `/stats` | `contents` | | |
| 209 | +| Git over HTTPS (`/{owner}/{repo}.git/...`), including push | `contents` | | |
| 210 | +| Release asset uploads (`/api/uploads/repos/{owner}/{repo}/releases/{id}/assets`) | `contents` | | |
| 211 | +| `GET /repos/{owner}/{repo}` and other reads not listed here | None: repository metadata is always readable | | |
| 212 | + | |
| 213 | +### Endpoints a job token can never use | |
| 214 | + | |
| 215 | +Some endpoints are refused whatever the permissions, even with `write-all`: | |
| 216 | + | |
| 217 | +- `/hooks`, `/collaborators`, `/keys`, `/invitations`, `/settings`, `/rulesets`, `/teams`, `/transfer`, `/autolinks`, `/vulnerability-alerts` | |
| 218 | +- `/actions/secrets`, `/actions/variables`, `/actions/runners`, `/actions/permissions`, `/actions/oidc` | |
| 219 | +- Creating, changing or deleting environments, and environment secrets and variables | |
| 220 | +- Branch protection (`/branches/{branch}/protection`) | |
| 221 | +- Changing or deleting the repository itself (`PATCH` or `DELETE /repos/{owner}/{repo}`) | |
| 222 | +- Any write to a path not in the table above | |
| 223 | + | |
| 224 | +Most of these endpoints need the admin role, which a job token never has, so the response is `403 admin access required`. The others answer `403 this endpoint is not available to a workflow's GITHUB_TOKEN`. | |
| 225 | + | |
| 226 | +A job token can't re-run workflows either: `POST /repos/{owner}/{repo}/actions/runs/{run_id}/rerun` (and `/rerun-failed-jobs`) fails with `403 a workflow's GITHUB_TOKEN cannot re-run workflows`. | |
| 227 | + | |
| 228 | +### The 403 response | |
| 229 | + | |
| 230 | +A request whose scope the token lacks gets GitHub's message, with the scope it needed: | |
| 231 | + | |
| 232 | +```json | |
| 233 | +{ | |
| 234 | + "message": "Resource not accessible by integration (the workflow's GITHUB_TOKEN needs permissions: issues: write)", | |
| 235 | + "documentation_url": "https://docs.github.com/rest" | |
| 236 | +} | |
| 237 | +``` | |
| 238 | + | |
| 239 | +When either of two scopes is enough, the message lists both, for example `needs permissions: issues or pull-requests: write`. Add the scope to the job's `permissions:` and run the job again. | |
| 240 | + | |
| 241 | +## Reading public repositories | |
| 242 | + | |
| 243 | +What anyone may read in a public repository, a job token may read too, whatever its permissions: | |
| 244 | + | |
| 245 | +- In its own public repository, a read is allowed even with `permissions: {}` or `contents: none`. | |
| 246 | +- It can read **other** public repositories on Diffinery, for example to check out a public dependency or clone a public submodule, but only read: any write is refused. | |
| 247 | +- It can't see other private repositories at all, even ones with the same owner. A request gets `404`. To check out another private repository, pass a personal or agent token from a secret as `token:`. | |
| 248 | + | |
| 249 | +## Workflow files | |
| 250 | + | |
| 251 | +A job token never creates, changes or deletes files under `.github/workflows/` or `.diffinery/workflows/`, whatever its permissions. This is GitHub's `workflows` permission, which a `GITHUB_TOKEN` can't have. The refusal reads: | |
| 252 | + | |
| 253 | +```text | |
| 254 | +refusing to allow a GitHub App to create or update workflow `.github/workflows/ci.yml` without `workflows` permission | |
| 255 | +``` | |
| 256 | + | |
| 257 | +It applies to: | |
| 258 | + | |
| 259 | +- `git push`: the ref update is rejected with that reason. | |
| 260 | +- Commits through the API, such as the contents API. | |
| 261 | +- Merging a pull request: the message appears as a merge blocker when the pull request changes a workflow file. | |
| 262 | + | |
| 263 | +A push that creates a new branch is compared with where the branch left the default branch, so a new branch that only carries workflow changes already on the default branch is allowed. To change workflow files from automation, use a personal or agent token from a secret. | |
| 264 | + | |
| 265 | +## Creating and approving pull requests | |
| 266 | + | |
| 267 | +By default a job token can't open pull requests, approve them or request changes on them. The request fails with: | |
| 268 | + | |
| 269 | +```text | |
| 270 | +GitHub Actions is not permitted to create pull requests. An admin can allow it in the repository's Actions settings ("Allow GitHub Actions to create and approve pull requests"). | |
| 271 | +``` | |
| 272 | + | |
| 273 | +For reviews the message says `approve` instead of `create`. Commenting reviews are always allowed. To allow the rest, set `can_approve_pull_request_reviews` to `true` (see [Read and change the setting](#read-and-change-the-setting)); the job also needs `pull-requests: write`. | |
| 274 | + | |
| 275 | +Even when allowed, an approval from a job token **never counts** toward a branch's required approvals. Only approvals by people count. | |
| 276 | + | |
| 277 | +## Pull requests from forks | |
| 278 | + | |
| 279 | +A `pull_request` run for a pull request whose head is in another repository gets read access only: `read-all` at most, whatever the workflow asks for, and never `id-token: write`. Diffinery doesn't have forks yet, so every pull request comes from a branch of the same repository and this rule doesn't come into play today. | |
| 280 | + | |
| 281 | +## Reusable workflows | |
| 282 | + | |
| 283 | +A [reusable workflow](/docs/actions/reusable-workflows) never gets more than its caller: | |
| 284 | + | |
| 285 | +- A job in the called workflow without `permissions:` (and a called workflow without top-level `permissions:`) inherits the calling job's permissions. | |
| 286 | +- Whatever the called workflow or its jobs declare is lowered, scope by scope, to what the calling job has. | |
| 287 | +- The calling job's own permissions follow the usual order: its `permissions:`, then its workflow's, then the repository default. | |
| 288 | + | |
| 289 | +```yaml | |
| 290 | +# caller | |
| 291 | +jobs: | |
| 292 | + deploy: | |
| 293 | + uses: ./.github/workflows/deploy.yml | |
| 294 | + permissions: | |
| 295 | + contents: read | |
| 296 | + id-token: write | |
| 297 | +``` | |
| 298 | + | |
| 299 | +Here the jobs in `deploy.yml` can have at most `contents: read` and `id-token: write`, even if `deploy.yml` asks for `write-all`. | |
| 300 | + | |
| 301 | +## Runs whose code came from an agent | |
| 302 | + | |
| 303 | +A run is treated as agent code when: | |
| 304 | + | |
| 305 | +- an agent triggered it, | |
| 306 | +- an agent last pushed the branch it runs on, or | |
| 307 | +- for pull request and merge queue events, an agent opened the pull request or last pushed its head branch. | |
| 308 | + | |
| 309 | +A re-run of such a run keeps this, whoever starts the re-run, and so does a `workflow_run` run that follows such a run, since it acts on what that run produced (its artifacts and outputs). The jobs of these runs act as an agent, so the [agent guardrails](/docs/agents/overview) hold for code an agent wrote, even when a person triggered the run: | |
| 310 | + | |
| 311 | +- Pushes to the default branch or a protected branch are refused with `agents may not push to protected or default branches; open a pull request`, unless the branch's protection rule has **Allow agents to push directly** on. | |
| 312 | +- Merges into those branches are blocked with `agents may not merge into this branch; a human must merge`, unless the rule has **Allow agents to merge pull requests** on. The token can still turn on [auto-merge](/docs/collaboration/pull-requests#auto-merge) where the rule has **Allow agents to land pull requests through auto-merge** and a required check; that rule holds for every job token on those branches, agent code or not. | |
| 313 | +- A pull request the job opens counts as agent-authored, so it needs at least one approval from a person. | |
| 314 | +- A deployment the job records counts as the agent's, so [environment secret rules for agents](/docs/actions/environments#who-receives-environment-secrets) apply. | |
| 315 | + | |
| 316 | +## Runs started by the token | |
| 317 | + | |
| 318 | +Like on GitHub, what a job does with its token starts no new workflow runs, so a workflow can't trigger itself in a loop. Pushes, pull requests, releases and merges made with a job token (including a later merge by an auto-merge the token turned on) don't start runs. The two exceptions, as on GitHub, are `workflow_dispatch` (a job with `actions: write` can start another workflow with `POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches`) and `repository_dispatch` (`POST /repos/{owner}/{repo}/dispatches`, which needs `contents: write`). | |
| 319 | + | |
| 320 | +## Differences from GitHub | |
| 321 | + | |
| 322 | +| Area | GitHub | Diffinery | | |
| 323 | +| --- | --- | --- | | |
| 324 | +| Changing the default | **Settings** > **Actions** > **General** > **Workflow permissions** | **Settings** > **Actions** > **Workflow permissions**, or `PUT /repos/{owner}/{repo}/actions/permissions/workflow` | | |
| 325 | +| Organization and enterprise defaults | Can limit repositories | Not available; each repository has its own setting | | |
| 326 | +| Agent code | Not a concept | Runs whose code came from an agent are held to the agent rules | | |
| 327 | +| Name in the repository | `github-actions[bot]` | `diffinery-actions[bot]` | | |
| 328 | +| Extra name | None | `secrets.DIFFINERY_TOKEN` | | |
| 329 | + | |
| 330 | +## Troubleshooting | |
| 331 | + | |
| 332 | +| Problem | Fix | | |
| 333 | +| --- | --- | | |
| 334 | +| `Resource not accessible by integration (the workflow's GITHUB_TOKEN needs permissions: <scope>: write)` | Add that scope to the job's `permissions:`, or to the workflow's when the job has none. In a reusable workflow, the calling job needs it too. | | |
| 335 | +| A job that used to write now gets 403 | The repository's default is `read` (the default for new repositories), or the workflow added a `permissions:` map that leaves the scope out. Add the scope, or have an admin set `default_workflow_permissions` to `write`. | | |
| 336 | +| `actions/checkout` fails on a private repository after adding `permissions:` | Naming any scope sets the others to `none`. Add `contents: read`. | | |
| 337 | +| `this endpoint is not available to a workflow's GITHUB_TOKEN`, or `admin access required` from a job | The endpoint manages settings, secrets, variables, hooks, runners, environments or branch protection. Use a personal or agent token from a secret. | | |
| 338 | +| `refusing to allow a GitHub App to create or update workflow ...` | Job tokens never change workflow files. Use a personal or agent token. | | |
| 339 | +| `GitHub Actions is not permitted to create pull requests. ...` | An admin sets `can_approve_pull_request_reviews` to `true`, and the job needs `pull-requests: write`. | | |
| 340 | +| A job token's approval doesn't satisfy required reviews | Only people's approvals count. | | |
| 341 | +| `agents may not push to protected or default branches; open a pull request` from a job | The run's code came from an agent. Open a pull request for a person to review and merge. | | |
| 342 | +| A push from a job doesn't start the expected workflow | Events caused by a job token start no runs. Use `workflow_dispatch` with `actions: write` or `repository_dispatch` with `contents: write`, or a personal or agent token. | | |
| 343 | +| `Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable` | Add `id-token: write`. `id-token: read` grants nothing. | |
modifiedapps/diffinery/docs/actions/workflow-syntax.md+80−30View file
| @@ -117,7 +117,7 @@ | ||
| 117 | 117 | | `name` | Supported | Display name of the workflow. Default: the file's path. | |
| 118 | 118 | | `run-name` | Supported | Title of each run. Evaluated with the `github`, `inputs` and `vars` contexts. | |
| 119 | 119 | | `on` | Supported | The events that start the workflow. Required. See [Events that trigger workflows](/docs/actions/events). | |
| 120 | -| `permissions` | Partly | Parsed and validated. Only `id-token` and `packages` change what the job token can do. See [Permissions](#permissions). | | |
| 120 | +| `permissions` | Supported | What the job token (`GITHUB_TOKEN`) may do, for jobs without their own `permissions`. See [permissions](#permissions). | | |
| 121 | 121 | | `env` | Supported | Environment variables for every job. | |
| 122 | 122 | | `defaults` | Supported | `defaults.run.shell` and `defaults.run.working-directory` for every `run` step. | |
| 123 | 123 | | `concurrency` | Supported | Groups runs and optionally cancels older ones. See [Concurrency](#concurrency). | |
| @@ -198,15 +198,15 @@ | ||
| 198 | 198 | |
| 199 | 199 | `permissions` may be `read-all`, `write-all`, an empty mapping (`{}`), or a mapping of scopes to `read`, `write` or `none`. Recognised scopes: `actions`, `attestations`, `checks`, `contents`, `deployments`, `discussions`, `id-token`, `issues`, `models`, `packages`, `pages`, `pull-requests`, `repository-projects`, `security-events` and `statuses`. Another scope gives a warning; another level fails with `permission '<scope>' must be 'read', 'write' or 'none'`. |
| 200 | 200 | |
| 201 | -What the settings change on Diffinery: | |
| 201 | +They work as on GitHub: | |
| 202 | 202 | |
| 203 | -| Scope | Effect | | |
| 204 | -| --- | --- | | |
| 205 | -| `id-token` | `write` (or `write-all`) at the workflow level or on the job lets the job request OIDC tokens. See [OIDC](/docs/actions/oidc). | | |
| 206 | -| `packages` | Controls what `GITHUB_TOKEN` can do in the [container registry](/docs/packages/container-registry). The job's `permissions` replace the workflow's. With no `permissions` anywhere, the token gets `write`; with a mapping that leaves `packages` out, it gets none. `read-all` gives read and `write-all` gives write. | | |
| 207 | -| Every other scope | No effect. | | |
| 203 | +- A job's `permissions` replace the workflow's. A job with neither gets the repository's default: read-only (`contents` and `packages` read) for repositories created since the setting was added, read and write for older ones. | |
| 204 | +- `read-all` gives every scope `read`; `write-all` gives every scope `write`, including `id-token`; `{}` gives nothing. | |
| 205 | +- In a mapping, every scope you don't list is `none`. A job that pushes images and checks out a private repository needs both `packages: write` and `contents: read`. | |
| 206 | +- Each REST endpoint and git operation needs its scope: `contents` for git fetch and push, files, releases and merges, `issues` for issues and labels, `pull-requests` for pull requests and reviews, `statuses`, `checks`, `deployments`, `actions` and so on. A request without it fails with `Resource not accessible by integration (the workflow's GITHUB_TOKEN needs permissions: <scope>: <level>)`. | |
| 207 | +- `id-token: write` lets the job request OIDC tokens (see [OIDC](/docs/actions/oidc)); `id-token: read` grants nothing. `packages` decides what the token can do in the [container registry](/docs/packages/container-registry). | |
| 208 | 208 | |
| 209 | -> **Important:** On Diffinery the job's `GITHUB_TOKEN` always has read access to its own repository and nothing more, whatever `permissions` says. It can clone and read through the API, but it can't push, comment, label, create releases or send `repository_dispatch` events. Use a personal or agent token stored as a secret for that. See [Secrets and variables](/docs/actions/secrets-and-variables). | |
| 209 | +Whatever `permissions` says, the token never administers the repository, never reads or changes secrets and variables, never creates or changes workflow files, and follows branch protection. [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions) has the full model, the scope each endpoint needs, and how an admin changes the repository default. | |
| 210 | 210 | |
| 211 | 211 | ### concurrency |
| 212 | 212 | |
| @@ -234,10 +234,10 @@ | ||
| 234 | 234 | |
| 235 | 235 | | Key | Support | Notes | |
| 236 | 236 | | --- | --- | --- | |
| 237 | -| `name` | Supported | Display name. May use `matrix`. | | |
| 237 | +| `name` | Supported | Display name. May use `matrix`; evaluated for each matrix combination. | | |
| 238 | 238 | | `needs` | Supported | Jobs that must finish first. | |
| 239 | 239 | | `if` | Supported | Condition evaluated before the job starts. | |
| 240 | -| `runs-on` | Supported | Hosted Linux labels or self-hosted labels. Can't use `matrix`. `group` is ignored. | | |
| 240 | +| `runs-on` | Supported | Hosted Linux labels or self-hosted labels. May use `matrix`; evaluated for each matrix combination. `group` is ignored. | | |
| 241 | 241 | | `environment` | Supported | The deployment environment: its branch policy, secrets and variables apply, and a deployment is recorded. Required reviewers and wait timers aren't supported. See [environment](#environment). | |
| 242 | 242 | | `outputs` | Supported | Values passed to dependent jobs through `needs`. | |
| 243 | 243 | | `env` | Supported | Environment variables for every step of the job. | |
| @@ -245,14 +245,14 @@ | ||
| 245 | 245 | | `steps` | Supported | Required for jobs that don't call a reusable workflow. | |
| 246 | 246 | | `timeout-minutes` | Supported | Default and maximum 360. | |
| 247 | 247 | | `strategy` | Supported | `matrix` with `include` and `exclude`, `fail-fast`, `max-parallel`. | |
| 248 | -| `continue-on-error` | Accepted, no effect | A failing job still fails the run. Use step-level `continue-on-error`. | | |
| 248 | +| `continue-on-error` | Supported | A failing job doesn't fail the run. See [continue-on-error (job)](#continue-on-error-job). | | |
| 249 | 249 | | `container` | Supported | Runs the steps in a Docker container. | |
| 250 | 250 | | `services` | Supported | Starts service containers next to the job. | |
| 251 | 251 | | `uses` | Supported | Calls a reusable workflow. | |
| 252 | 252 | | `with` | Supported | Inputs for a reusable workflow. | |
| 253 | 253 | | `secrets` | Supported | Secrets for a reusable workflow, or `inherit`. | |
| 254 | -| `permissions` | Partly | See [Permissions](#permissions). | | |
| 255 | -| `concurrency` | Accepted, ignored | Job-level concurrency isn't enforced. | | |
| 254 | +| `permissions` | Supported | See [permissions (job)](#permissions-job). | | |
| 255 | +| `concurrency` | Supported | One job of a group runs at a time. See [concurrency (job)](#concurrency-job). | | |
| 256 | 256 | | `snapshot` | Accepted, ignored | | |
| 257 | 257 | |
| 258 | 258 | Any other key gives the warning `unknown job key '<key>'`. |
| @@ -289,9 +289,9 @@ | ||
| 289 | 289 | - `Job '<id>' depends on unknown job '<other>'` |
| 290 | 290 | - `Dependency cycle between jobs: a -> b -> a` |
| 291 | 291 | |
| 292 | -Diffinery groups jobs into levels: level 1 holds the jobs that need nothing, level 2 the jobs that need only level 1 jobs, and so on. Every job of a level starts together, and the next level starts when every job of the current level has finished. A job therefore can start a little later than on GitHub, where it starts as soon as its own `needs` finish. | |
| 292 | +A job starts as soon as every job it needs has finished, as on GitHub. It doesn't wait for unrelated jobs. | |
| 293 | 293 | |
| 294 | -By default a job runs only when every job it needs succeeded or was skipped. Use a status function in `if` to change that; see [Status check functions](/docs/actions/expressions#status-check-functions). | |
| 294 | +By default a job runs only when every job above it in the `needs` chain succeeded (a job with `continue-on-error: true` that failed counts as a success). If any of them failed, was cancelled or was skipped, the job is skipped too, and so are the jobs that need it. `needs.<id>.result` is `skipped` for a skipped job. Use a status function in `if` to change that, for example `if: ${{ !cancelled() && !failure() }}` to run after a skipped job; see [Status check functions](/docs/actions/expressions#status-check-functions). | |
| 295 | 295 | |
| 296 | 296 | ### if (job) |
| 297 | 297 | |
| @@ -326,8 +326,19 @@ | ||
| 326 | 326 | | Any list that includes `self-hosted` | A self-hosted runner whose labels include every label in the list. See [Runners](/docs/actions/runners). | |
| 327 | 327 | | Anything else (for example `windows-latest` or `macos-14` without `self-hosted`) | Nowhere. The job fails with `No Diffinery runner matches runs-on: <labels>. Linux (ubuntu-*) runners are hosted; anything else needs runs-on: [self-hosted, ...] and a registered runner.` | |
| 328 | 328 | |
| 329 | -Labels may be expressions, evaluated with the `github`, `needs`, `inputs`, `vars` and `env` contexts. Unlike GitHub, `runs-on` can't use the `matrix` context: `runs-on: ${{ matrix.os }}` evaluates to `null` and the job fails with the message above. Use separate jobs for different operating systems. | |
| 329 | +Labels may be expressions, evaluated for each matrix combination with the `github`, `needs`, `inputs`, `vars`, `matrix` and `strategy` contexts. An expression can give one label or a list of labels: | |
| 330 | 330 | |
| 331 | +```yaml | |
| 332 | +jobs: | |
| 333 | + test: | |
| 334 | + strategy: | |
| 335 | + matrix: | |
| 336 | + runner: [ubuntu-latest, [self-hosted, arm64]] | |
| 337 | + runs-on: ${{ matrix.runner }} | |
| 338 | +``` | |
| 339 | + | |
| 340 | +If an expression fails, only that combination fails, with `Error evaluating the job for this matrix combination: <message>`. If it gives no labels, the job fails with `No Diffinery runner matches runs-on: (no labels). ...`. | |
| 341 | + | |
| 331 | 342 | ### environment |
| 332 | 343 | |
| 333 | 344 | ```yaml |
| @@ -359,26 +370,26 @@ | ||
| 359 | 370 | |
| 360 | 371 | What happens when a job with `environment` starts: |
| 361 | 372 | |
| 362 | -1. `name` and `url` are evaluated with the `github`, `inputs`, `vars` (repository variables) and `matrix` contexts. A `url` that refers to `steps.` is ignored, because step outputs don't exist yet when the job starts. | |
| 373 | +1. `name` and `url` are evaluated with the `github`, `needs`, `inputs`, `vars` (repository variables), `matrix` and `strategy` contexts. A `url` that refers to `steps.` is ignored, because step outputs don't exist yet when the job starts. | |
| 363 | 374 | 2. The name is checked: it must be 1 to 255 characters without `/`, `\` or control characters, otherwise the job fails with `invalid environment name '<name>'`. Names are case-insensitive. An environment that doesn't exist yet is created, with no branch policy. |
| 364 | 375 | 3. The environment's deployment branch policy is checked against the run's ref (`GITHUB_REF`). If the ref isn't allowed, the job fails before any step runs with `<branch or tag> is not allowed to deploy to <name> due to environment protection rules.` |
| 365 | -4. A deployment is recorded for the run's commit and ref, with the task `deploy`, marked as a production deployment when the environment is named `production`, and a deployment status `in_progress` whose log URL is the job's log. When `url` is an `http://` or `https://` URL, it's stored as the status's environment URL. | |
| 366 | -5. The environment's secrets are added to the `secrets` context and its variables to the `vars` context. They win over repository and organization secrets and repository variables with the same names. `GITHUB_TOKEN` and `DIFFINERY_TOKEN` can't be overridden. | |
| 376 | +4. A deployment is recorded for the run's commit and ref, with the task `deploy`, marked as a production deployment when the environment is named `production` (in any case), and a deployment status `in_progress` whose log URL is the job's log. When `url` is an `http://` or `https://` URL, it's stored as the status's environment URL. | |
| 377 | +5. The environment's secrets are added to the `secrets` context and its variables to the `vars` context. They win over repository and organization secrets and variables with the same names. `GITHUB_TOKEN` and `DIFFINERY_TOKEN` can't be overridden. | |
| 367 | 378 | |
| 368 | -When the job finishes, the deployment gets a final status: `success` when the job succeeded, `inactive` when it was cancelled, and `failure` otherwise (including timeouts). Each attempt of a re-run records its own deployment. Read deployments with `GET /api/v3/repos/{owner}/{repo}/deployments`; webhooks subscribed to `deployment_status` get each status. | |
| 379 | +When the job finishes, the deployment gets a final status: `success` when the job succeeded, `inactive` when it was cancelled, and `failure` otherwise (including timeouts). Each attempt of a re-run records its own deployment. When the job succeeds, older successful deployments to the same environment become `inactive` (newer ones are left alone, and production deployments are never made inactive this way). Read deployments with `GET /api/v3/repos/{owner}/{repo}/deployments`; webhooks subscribed to `deployment` get a `created` event for each deployment, and those subscribed to `deployment_status` get each status. | |
| 369 | 380 | |
| 370 | 381 | Environment secrets aren't always handed out, even when the branch policy allows the ref: |
| 371 | 382 | |
| 372 | 383 | - Runs on pull request refs (`refs/pull/<n>/merge`, used by `pull_request` runs) never get environment secrets, because their code is whatever the pull request says. |
| 373 | -- Runs started by agents get environment secrets only when the environment's policy is "protected branches only". | |
| 384 | +- Runs whose code came from an agent (an agent started the run, pushed the branch last, or opened the pull request, and re-runs and `workflow_run` followers of such runs) get environment secrets only when the environment's policy is "protected branches only". | |
| 374 | 385 | |
| 375 | 386 | In those cases the job still runs, with the environment's variables but without its secrets (an unset secret is `''`). |
| 376 | 387 | |
| 377 | 388 | Differences from GitHub: |
| 378 | 389 | |
| 379 | 390 | - Required reviewers, wait timers and other protection rules aren't supported, so a job never waits for approval. Creating an environment with them through the API fails with `required reviewers and wait timers are not supported yet`. |
| 380 | -- `name` and `url` can't use the `needs` or `strategy` contexts, and `url` can't use step outputs. | |
| 381 | -- The job's `GITHUB_TOKEN` stays read-only; the environment doesn't change its permissions. | |
| 391 | +- `url` can't use step outputs. | |
| 392 | +- The environment doesn't change the job token's permissions. To create deployment statuses yourself, give the job `deployments: write`. | |
| 382 | 393 | |
| 383 | 394 | A job that calls a reusable workflow (`uses`) can't set `environment` (you get the warning `unknown reusable-workflow job key 'environment'` and it's ignored). Set it on the jobs inside the called workflow instead. |
| 384 | 395 | |
| @@ -422,7 +433,7 @@ | ||
| 422 | 433 | timeout-minutes: 20 |
| 423 | 434 | ``` |
| 424 | 435 | |
| 425 | -A number or an expression (which may use `matrix`). Default: 360. Values above 360 are capped at 360, the longest a job can run on Diffinery. When the time is up, the runner logs `The job running on runner has exceeded the maximum execution time of <n> minutes.`, stops the job's processes, marks the remaining steps cancelled, and the job fails with `The job exceeded the maximum execution time of <n> minutes.` | |
| 436 | +A number or an expression (which may use `matrix`; evaluated for each matrix combination). Default: 360. Values above 360 are capped at 360, the longest a job can run on Diffinery. When the time is up, the runner logs `The job running on runner has exceeded the maximum execution time of <n> minutes.`, stops the job's processes, marks the remaining steps cancelled, and the job fails with `The job exceeded the maximum execution time of <n> minutes.` | |
| 426 | 437 | |
| 427 | 438 | ### strategy |
| 428 | 439 | |
| @@ -462,7 +473,7 @@ | ||
| 462 | 473 | |
| 463 | 474 | #### fail-fast |
| 464 | 475 | |
| 465 | -`true` (the default), `false` or an expression. With `true`, when one job of the matrix fails, its unfinished sibling jobs are stopped and marked `cancelled` with the reason `Cancelled because <job name> failed (fail-fast).` | |
| 476 | +`true` (the default), `false` or an expression. With `true`, when one job of the matrix fails or times out (and doesn't have `continue-on-error: true`), its unfinished sibling jobs are stopped and marked `cancelled` with the reason `Cancelled because <job name> failed (fail-fast).` | |
| 466 | 477 | |
| 467 | 478 | #### max-parallel |
| 468 | 479 | |
| @@ -470,8 +481,31 @@ | ||
| 470 | 481 | |
| 471 | 482 | ### continue-on-error (job) |
| 472 | 483 | |
| 473 | -Accepted (`true`, `false` or an expression) but has no effect on Diffinery: a failing job fails the run and counts as a failure in `needs`. Put `continue-on-error: true` on the steps that may fail instead, or give dependent jobs `if: always()`. | |
| 484 | +```yaml | |
| 485 | +jobs: | |
| 486 | + test: | |
| 487 | + runs-on: ubuntu-latest | |
| 488 | + continue-on-error: ${{ matrix.experimental }} | |
| 489 | + strategy: | |
| 490 | + matrix: | |
| 491 | + node: [20, 22] | |
| 492 | + experimental: [false] | |
| 493 | + include: | |
| 494 | + - node: 24 | |
| 495 | + experimental: true | |
| 496 | + steps: | |
| 497 | + - uses: actions/setup-node@v4 | |
| 498 | + with: | |
| 499 | + node-version: ${{ matrix.node }} | |
| 500 | + - run: npm ci && npm test | |
| 501 | +``` | |
| 474 | 502 | |
| 503 | +`true`, `false` or an expression, evaluated for each matrix combination (it may use `matrix` and `strategy`). When a job with `continue-on-error: true` fails or times out: | |
| 504 | + | |
| 505 | +- The job itself still shows as failed, but the run's conclusion isn't `failure` because of it. | |
| 506 | +- It doesn't trigger `fail-fast`, so its matrix siblings keep running. | |
| 507 | +- It counts as a success in `needs.<id>.result` and for the `success()` checks of the jobs that need it. | |
| 508 | + | |
| 475 | 509 | ### container |
| 476 | 510 | |
| 477 | 511 | ```yaml |
| @@ -527,12 +561,28 @@ | ||
| 527 | 561 | |
| 528 | 562 | ### permissions (job) |
| 529 | 563 | |
| 530 | -Same syntax as the [top-level `permissions`](#permissions). For `packages`, the job's value replaces the workflow's. For `id-token`, either level can grant `write`. | |
| 564 | +Same syntax as the [top-level `permissions`](#permissions). The job's value replaces the workflow's completely, as on GitHub: scopes the workflow grants but the job's mapping leaves out are `none` for this job. In a [reusable workflow](/docs/actions/reusable-workflows), a job's permissions can't exceed the calling job's. See [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions). | |
| 531 | 565 | |
| 532 | 566 | ### concurrency (job) |
| 533 | 567 | |
| 534 | -Accepted with the same syntax as the top-level `concurrency`, but not enforced: jobs don't wait for or cancel each other. | |
| 568 | +```yaml | |
| 569 | +jobs: | |
| 570 | + deploy: | |
| 571 | + runs-on: ubuntu-latest | |
| 572 | + concurrency: | |
| 573 | + group: deploy-${{ github.ref }} | |
| 574 | + cancel-in-progress: true | |
| 575 | +``` | |
| 535 | 576 | |
| 577 | +Same syntax as the top-level `concurrency`. At most one job of a group runs at a time across the whole repository, whichever run or workflow it belongs to. `group` and `cancel-in-progress` are evaluated for each matrix combination, with the `github`, `needs`, `inputs`, `vars`, `matrix` and `strategy` contexts. | |
| 578 | + | |
| 579 | +- Without `cancel-in-progress`, a job waits with the status `pending` while another job of its group runs. Only the newest pending job is kept: a new pending job cancels the older one. | |
| 580 | +- With `cancel-in-progress: true`, the new job starts and the group's running and pending jobs are cancelled. | |
| 581 | +- A job cancelled by its group ends with the message `Canceling since a higher priority waiting request for '<group>' exists`. | |
| 582 | +- When a running job finishes, the oldest pending job of the group starts. | |
| 583 | + | |
| 584 | +The concurrency of a job that calls a reusable workflow is ignored; set it on the jobs inside the called workflow. | |
| 585 | + | |
| 536 | 586 | ## Steps |
| 537 | 587 | |
| 538 | 588 | Each item in `steps` is a mapping. A step runs either a shell script (`run`) or an action (`uses`), never both. |
| @@ -588,11 +638,11 @@ | ||
| 588 | 638 | args: -c "echo hello" |
| 589 | 639 | ``` |
| 590 | 640 | |
| 591 | -- `actions/checkout`, `actions/cache` (and `cache/restore`, `cache/save`), `actions/upload-artifact`, `actions/download-artifact`, `actions/setup-node`, `actions/setup-python` and `actions/setup-go` are built into the runner and work for any version tag. | |
| 641 | +- `actions/checkout`, `actions/cache` (and `cache/restore`, `cache/save`), `actions/upload-artifact`, `actions/download-artifact`, `actions/setup-node`, `actions/setup-python` and `actions/setup-go` are built into the runner for their current major versions. A commit SHA, a branch, a newer major version, an input the built-in doesn't know, or `DIFFINERY_BUILTINS: false` in `env` runs the real action instead (see [Built-in actions](/docs/actions/compatibility#built-in-actions)). | |
| 592 | 642 | - Other `owner/repo@ref` actions are downloaded from GitHub and run with their `action.yml`: JavaScript (`node*`), composite and Docker actions are supported. JavaScript actions run with the runner's Node.js 22 whatever `runs.using` says. |
| 593 | 643 | - `./path` actions are read from the workspace, so check out the repository first. Otherwise the step fails with `Can't find 'action.yml' or 'action.yaml' under '<dir>'. Did you forget to run actions/checkout before running your local action?` |
| 594 | 644 | - `docker://image` runs a container; `with.args` and `with.entrypoint` set its arguments and entrypoint. |
| 595 | -- `actions/github-script` isn't supported yet and fails with ``actions/github-script is not supported on Diffinery runners yet. Use a `run:` step with `node` or `curl` against $GITHUB_API_URL instead.`` | |
| 645 | +- `actions/github-script` and other actions that call the API work with the job token, within its `permissions`. | |
| 596 | 646 | - A reference without `@ref` fails with `Expected format {owner}/{repo}[/path]@ref. Actual '<value>'`. An unknown action or version fails with `Unable to resolve action '<owner>/<repo>@<ref>', repository or version not found`. |
| 597 | 647 | |
| 598 | 648 | What each built-in action supports is in [GitHub Actions compatibility](/docs/actions/compatibility) and [Caching and artifacts](/docs/actions/caching-and-artifacts). |
modifiedapps/diffinery/docs/admin/site-administration.md+25−5View file
| @@ -1,13 +1,13 @@ | ||
| 1 | 1 | --- |
| 2 | 2 | title: Site administration |
| 3 | -description: What site administrators do: manage people and access requests, invite people, issue sign-in links, read the audit log and check email delivery. | |
| 3 | +description: What site administrators do: manage people and access, invite people, issue sign-in links, oversee GitHub imports, read the audit log and check email. | |
| 4 | 4 | --- |
| 5 | 5 | |
| 6 | 6 | Site administrators run a Diffinery instance. They decide who gets in, help people who lost their passkeys, and check the audit log and email delivery. This page covers who the site administrators are and each page in the **Site administration** group of **Settings**. |
| 7 | 7 | |
| 8 | 8 | ## Site administrators |
| 9 | 9 | |
| 10 | -Site administrators have admin on every repository, can act as an owner of every organization (including its **Settings** tab, where the organization's name and picture are changed, and the **Add people** form on its **People** tab), and see an extra **Site administration** group in the **Settings** sidebar with three pages: | |
| 10 | +Site administrators have admin on every repository, can act as an owner of every organization (including its **Settings** tab, where the organization's name, picture and base permission are changed, and the **Add people** form on its **People** tab), and see an extra **Site administration** group in the **Settings** sidebar with three pages: | |
| 11 | 11 | |
| 12 | 12 | | Page | Path | What it is for | |
| 13 | 13 | | --- | --- | --- | |
| @@ -106,9 +106,27 @@ | ||
| 106 | 106 | |
| 107 | 107 | Anyone with the link can add a passkey to the account, so send it only to that person. The link also works for a first sign-in, for an account that exists but has no passkey yet. |
| 108 | 108 | |
| 109 | +## GitHub imports and linked accounts | |
| 110 | + | |
| 111 | +Site administrators have extra reach over [imports from GitHub](/docs/migrate-from-github): | |
| 112 | + | |
| 113 | +- **Import into any account.** A site administrator can start an import into any user's account or any organization, not only their own (agent accounts can't own imports). | |
| 114 | +- **See and cancel any import.** Open `https://diffinery.com/imports/<id>`, or call `GET /api/v3/diffinery/imports/{id}`, for any import, and stop one with **Cancel import** or `POST /api/v3/diffinery/imports/{id}/cancel` (audited as `import.cancel`, with you in `by`). The **Imports** page and `GET /api/v3/diffinery/imports` still list only the imports you started yourself. | |
| 115 | +- **Retrying stays with the starter.** Only the person or agent who started an import can retry it, site administrators included. | |
| 116 | + | |
| 117 | +### Clear a GitHub link | |
| 118 | + | |
| 119 | +An import attributes a GitHub user's issues, pull requests, comments and reviews to the Diffinery account linked to that GitHub user. If a link is wrong, for example after a Diffinery account changed hands, a site administrator can remove it. There is no page for this; use the API with an `admin`-scoped token that isn't limited to repositories (or a browser session): | |
| 120 | + | |
| 121 | +```sh | |
| 122 | +diffinery api DELETE /diffinery/admin/linked-accounts/<link id> | |
| 123 | +``` | |
| 124 | + | |
| 125 | +The link id comes from `GET /api/v3/diffinery/user/linked-accounts`, called by the account that owns the link; Diffinery has no endpoint that lists other people's links. The request answers `204`, or `404 link not found`, and is audited as `account.link_cleared` with the link (`github.com/diffy`) as the target and the Diffinery account's id in the detail. Imports that already ran keep their attribution; later imports treat that GitHub user as a mannequin until someone links the account again. People unlink their own accounts on **Settings → Linked accounts** (`account.unlink_github`). | |
| 126 | + | |
| 109 | 127 | ## Audit log |
| 110 | 128 | |
| 111 | -**Settings → Audit log** (`/settings/audit`) shows the 200 most recent security-relevant events: sign-ins, passkeys, tokens, invites, account status changes, repository and access changes, pushes, merges and push protection. Each row has **When**, **Actor** (and who an agent acted for), **Action**, **Target** and **Detail**. See [Audit log](/docs/security/audit-log) for every action and how to use it. | |
| 129 | +**Settings → Audit log** (`/settings/audit`) shows the 200 most recent security-relevant events: sign-ins, passkeys, tokens, invites, account status changes, repository and access changes, pushes, merges and push protection. Repository and organization settings changed through the REST API or the CLI are recorded too: repository settings, archiving, collaborators, branch protection, Actions secrets and variables (repository, environment and organization), webhooks, a repository's workflow token settings, and deleted Actions artifacts and caches. Imports from GitHub (`import.create`, `import.push`, `import.finish` and the rest) and linked GitHub accounts are recorded too. Settings changes made through the API have `via: api` in their **Detail**. Each row has **When**, **Actor** (and who an agent acted for), **Action**, **Target** and **Detail**. See [Audit log](/docs/security/audit-log) for every action and how to use it. | |
| 112 | 130 | |
| 113 | 131 | |
| 114 | 132 | |
| @@ -118,7 +136,7 @@ | ||
| 118 | 136 | | --- | --- | |
| 119 | 137 | | **Sending** | "Cloudflare Email Sending is bound", or "not configured (no EMAIL binding): emails are logged, not sent" | |
| 120 | 138 | | **Queue** | `diffinery-mail`, or "not bound: emails are dropped" | |
| 121 | -| **From** | The sender address set in `MAIL_FROM` (see [Configuration](/docs/self-hosting/configuration)) | | |
| 139 | +| **From** | The address emails are sent from: `MAIL_FROM`, or `notifications@<your hostname>` when it is empty (see [Configuration](/docs/self-hosting/configuration)) | | |
| 122 | 140 | | **Last 24 hours** | How many emails were sent, failed and skipped | |
| 123 | 141 | |
| 124 | 142 | Buttons: |
| @@ -136,7 +154,7 @@ | ||
| 136 | 154 | |
| 137 | 155 | | Method and path | Description | Needs | |
| 138 | 156 | | --- | --- | --- | |
| 139 | -| `GET /api/v3/diffinery/admin/email` | `sending` and `queue` (whether they are configured), `from` (`MAIL_FROM`, or `null` when unset), `last_24h` counts (`sent`, `failed`, `skipped`) and the `last` email logged | site administrator, any token scope | | |
| 157 | +| `GET /api/v3/diffinery/admin/email` | `sending` and `queue` (whether they are configured), `from` (the sender address: `MAIL_FROM`, or `notifications@<your hostname>` when it is empty), `last_24h` counts (`sent`, `failed`, `skipped`) and the `last` email logged | site administrator, any token scope | | |
| 140 | 158 | | `POST /api/v3/diffinery/admin/emails/samples` | Send every template to yourself now, or only some with `{"only": ["signin", "invite-org"]}`. Returns `to`, `sent`, `total` and each result. | site administrator, with an `admin` token for all repositories or a browser session | |
| 141 | 159 | | `GET /api/v3/diffinery/emails?all=1` | Everyone's recent emails (without `all=1`, only yours). Also takes `kind`, `since` and `per_page` (up to 500). | site administrator for `all=1` | |
| 142 | 160 | |
| @@ -175,6 +193,8 @@ | ||
| 175 | 193 | | Message | Cause and fix | |
| 176 | 194 | | --- | --- | |
| 177 | 195 | | `403 site administrators only` | Your account isn't a site administrator. | |
| 196 | +| `403 site admin only` from `DELETE /api/v3/diffinery/admin/linked-accounts/{id}` | Your account isn't a site administrator. | | |
| 197 | +| `404 link not found` | No GitHub link has that id. Ask the account's owner for the id from `GET /api/v3/diffinery/user/linked-accounts`. | | |
| 178 | 198 | | "you cannot change your own status" | Ask another site administrator. | |
| 179 | 199 | | "enable this account first" | Sign-in links can't be created for disabled accounts. Set the status to `active`, then create the link. | |
| 180 | 200 | | "enter a valid email address" | The invite form needs an address like `friend@example.com`. | |
modifiedapps/diffinery/docs/agents/mcp.md+57−7View file
| @@ -3,7 +3,7 @@ | ||
| 3 | 3 | description: Connect AI agents to Diffinery over the Model Context Protocol at https://diffinery.com/mcp, with client setup, the tool list, permissions and example calls. |
| 4 | 4 | --- |
| 5 | 5 | |
| 6 | -Diffinery runs a Model Context Protocol (MCP) server at `https://diffinery.com/mcp`. An MCP client such as Claude Code, Cursor or VS Code can use it to read code, commit changes, open and review pull requests, follow CI, publish releases, run commands in sandboxes and look things up in these docs, all without a git client. Every tool goes through the same permission checks, branch protection and agent guardrails as git and the REST API. | |
| 6 | +Diffinery runs a Model Context Protocol (MCP) server at `https://diffinery.com/mcp`. An MCP client such as Claude Code, Cursor or VS Code can use it to read code, commit changes, open and review pull requests, follow CI, publish releases, run commands in sandboxes, import repositories from GitHub and look things up in these docs, all without a git client. Every tool goes through the same permission checks, branch protection and agent guardrails as git and the REST API. | |
| 7 | 7 | |
| 8 | 8 | ## Endpoint and transport |
| 9 | 9 | |
| @@ -123,7 +123,7 @@ | ||
| 123 | 123 | |
| 124 | 124 | ## Tools |
| 125 | 125 | |
| 126 | -Every tool that works on a repository takes `owner` and `repo`. Tools marked read-only have the MCP annotation `readOnlyHint: true`, so clients can run them without asking. Every tool marked **No** needs a token with the `write` or `admin` scope, whatever its minimum role. The full input schema of each tool is on [MCP tools reference](/docs/reference/mcp-tools), and `tools/list` returns it at runtime. | |
| 126 | +Every tool that works on a repository takes `owner` and `repo`. A repository that was transferred or renamed is also found under its old owner and name, for anyone who can read it, so an agent configured with the old name keeps working. Tools marked read-only have the MCP annotation `readOnlyHint: true`, so clients can run them without asking. Every tool marked **No** needs a token with the `write` or `admin` scope, whatever its minimum role. The full input schema of each tool is on [MCP tools reference](/docs/reference/mcp-tools), and `tools/list` returns it at runtime. | |
| 127 | 127 | |
| 128 | 128 | ### Account and repositories |
| 129 | 129 | |
| @@ -160,7 +160,9 @@ | ||
| 160 | 160 | | `create_pull_request` | No | Open a pull request: `title`, `head`, optional `base`, `body`, `draft`, `intent`, `reviewers` | write | |
| 161 | 161 | | `comment` | No | Comment on an issue or pull request | triage | |
| 162 | 162 | | `review_pull_request` | No | Submit an `APPROVE`, `REQUEST_CHANGES` or `COMMENT` review, with optional line `comments` | read (write to approve or request changes) | |
| 163 | -| `merge_pull_request` | No | Merge with `method`, `expected_head_sha`, `delete_branch`; queues the pull request when the base branch uses a merge queue | write | | |
| 163 | +| `merge_pull_request` | No | Merge with `method`, `expected_head_sha`, `delete_branch`; queues the pull request when the base branch uses a merge queue. Agents can't merge into the default branch or protected branches this way: they use `enable_auto_merge` | write | | |
| 164 | +| `enable_auto_merge` | No | Turn on auto-merge, optionally with `merge_method`, `commit_title` and `commit_message`: Diffinery merges the pull request (or adds it to the merge queue) once every branch rule is met. Returns `state` (`enabled`, `queued` or `merged`), `autoMerge` and, once merged, `mergeCommit`. See [Auto-merge](/docs/collaboration/pull-requests#auto-merge) | write | | |
| 165 | +| `disable_auto_merge` | No | Turn auto-merge off; returns `{"auto_merge": null}` | write | | |
| 164 | 166 | | `dequeue_pull_request` | No | Remove a pull request from the merge queue | write | |
| 165 | 167 | |
| 166 | 168 | ### Issues |
| @@ -169,7 +171,7 @@ | ||
| 169 | 171 | | --- | --- | --- | --- | |
| 170 | 172 | | `list_issues` | Yes | Up to 100 issues by `state`, search text `q` and `label` | read | |
| 171 | 173 | | `get_issue` | Yes | An issue (or pull request) with its comments and reviews | read | |
| 172 | -| `create_issue` | No | Open an issue with `title`, optional `body` and `labels` | read | | |
| 174 | +| `create_issue` | No | Open an issue with `title`, optional `body` and `labels` (`labels` from an account below triage are dropped, as on GitHub) | read | | |
| 173 | 175 | | `update_issue` | No | Change the `title`, `body`, `state` (`open`, `closed`) or `labels` of an issue or pull request | triage | |
| 174 | 176 | |
| 175 | 177 | ### Workflows |
| @@ -215,6 +217,14 @@ | ||
| 215 | 217 | |
| 216 | 218 | See [Sandboxes](/docs/agents/sandboxes) for what is inside a sandbox and its limits. |
| 217 | 219 | |
| 220 | +### Imports from GitHub | |
| 221 | + | |
| 222 | +| Tool | Read-only | What it does | Minimum role | | |
| 223 | +| --- | --- | --- | --- | | |
| 224 | +| `start_github_import` | No | Start a server-side import of one GitHub repository: `source` (`owner/name` or a GitHub URL) and `owner` (the Diffinery organization or user to import into), optional `name`, `visibility` (`keep`, `private`, `public`), `github_token` and `github_api_url`. Returns the import, with its `id`. | See [Imports over MCP](#imports-over-mcp) | | |
| 225 | +| `get_import` | Yes | An import by `id`: `status`, per-step progress, `git` transfer progress, `summary`, `warnings`, `error` and, for an organization import, its repositories | You started it; a person who administers its target, or a site administrator, can also read it | | |
| 226 | +| `list_imports` | Yes | The imports you started, newest first | Any token | | |
| 227 | + | |
| 218 | 228 | ## Commit without git |
| 219 | 229 | |
| 220 | 230 | `commit_files` creates a single commit from file contents: |
| @@ -230,6 +240,8 @@ | ||
| 230 | 240 | |
| 231 | 241 | It returns `{ "sha": "...", "branch": "..." }`. At least one file or delete is required (`nothing to commit`). Committing to a branch that doesn't exist without `create_branch_from` fails with `branch fix-pricing-link not found`. Commits made by agents are stamped with the agent and its owner; see [Agents](/docs/agents/overview#how-agent-activity-is-recorded). |
| 232 | 242 | |
| 243 | +`commit_files` runs [push protection](/docs/security/push-protection) before it writes anything, like `git push`. If a file under 1 MiB looks like a credential (an AWS key, a GitHub or Diffinery token, a private key and so on), nothing is committed and the tool fails with, for example, `push protection: possible secret detected (AWS access key) in config/deploy.env. Remove it, or rotate it and ask an admin.` The attempt is recorded in the audit log as `api.blocked_secret` against the token's account. `commit_files` has no bypass. For a test fixture with fake keys, put the text `diffinery:allow-secret` anywhere in the file. | |
| 244 | + | |
| 233 | 245 | ## Releases over MCP |
| 234 | 246 | |
| 235 | 247 | Release tools return a compact release object: |
| @@ -258,6 +270,33 @@ | ||
| 258 | 270 | - `generate_release_notes` only drafts text; pass its `body` to `create_release`, or set `generate_release_notes: true` there. |
| 259 | 271 | - Publishing a release for a tag that doesn't exist creates the tag at `target_commitish`, and that follows the same rules as any ref update. A draft creates its tag only when it is published. |
| 260 | 272 | |
| 273 | +## Imports over MCP | |
| 274 | + | |
| 275 | +`start_github_import` starts the same server-side importer as the web UI, `diffinery migrate github` and `POST /api/v3/diffinery/imports`. It imports one repository with every part turned on: Git history and Git LFS, issues and pull requests with their numbers, comments, reviews and review comments, labels, milestones, branch protection, releases, and (for people) Actions variables, environments and secret names. Secret values are never read from GitHub. The tool returns at once; the import runs on the server. | |
| 276 | + | |
| 277 | +```sh | |
| 278 | +curl -s https://diffinery.com/mcp \ | |
| 279 | + -H "Authorization: Bearer $DIFFINERY_TOKEN" -H "Content-Type: application/json" \ | |
| 280 | + -d '{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"start_github_import","arguments":{"source":"acme/website","owner":"acme","github_token":"ghp_..."}}}' | |
| 281 | +``` | |
| 282 | + | |
| 283 | +The result is the import object (`id`, `html_url`, `status`, `source`, `target`, `steps`, `git`, `summary`, `warnings`, `error`). Call `get_import` with the `id` every minute or so until `status` is `succeeded`, `failed` or `cancelled`. While the import runs, every push to the new repository is refused with `repository is locked by an import in progress`. | |
| 284 | + | |
| 285 | +Who can start one: | |
| 286 | + | |
| 287 | +- The token needs the `write` or `admin` scope (`starting an import needs a token with write or admin scope`). | |
| 288 | +- A person can import into their own account or an organization they own or administer. | |
| 289 | +- An agent can import **only into an organization**: the one that owns it, or one where its owner is an owner or admin (`agents can import only into organizations they or their owner administer`). Its maximum role must be at least `write` (`this agent's policy does not allow writing; an import needs at least write`). | |
| 290 | + | |
| 291 | +Rules for imports an agent starts: | |
| 292 | + | |
| 293 | +- **Push protection is never bypassed.** If old history contains something that looks like a credential, the import stops and fails with `Push protection found possible secrets in the history (...)`. A person who administers the target starts a new import from the web or API to import it with an audited bypass. | |
| 294 | +- **Variables, secret names and environments are skipped** (`skipped (agents cannot manage variables)`); a person sets them up afterwards. Agents never read or set secret values. | |
| 295 | +- **No retry once the repository exists.** Retrying is limited to whoever started the import, through the web or REST API, and an agent's retry is refused after the repository was created. | |
| 296 | +- The agent sees only the imports it started (`get_import`, `list_imports`). | |
| 297 | + | |
| 298 | +An import uses the GitHub token only for that import, deletes it when the import ends, and never returns it. For GitHub Enterprise, pass `github_api_url`, for example `https://ghe.example.com/api/v3`; when `source` is an Enterprise URL and you pass a token, `github_api_url` must be on the same host. See [Migrate from GitHub](/docs/migrate-from-github) for what is copied, limits (at most 3 repository imports at a time per account and per target) and errors. | |
| 299 | + | |
| 261 | 300 | ## Permissions |
| 262 | 301 | |
| 263 | 302 | The token's account (person or agent), its role on the repository and the token's scope decide what each tool may do, exactly as for the REST API. The minimum role for each tool is in the tables above. |
| @@ -272,10 +311,14 @@ | ||
| 272 | 311 | |
| 273 | 312 | On top of the role and scope: |
| 274 | 313 | |
| 275 | -- Agents can't push or commit to the default branch or protected branches, and can't merge into them, unless the branch's rule allows it (`agentsMayPush`, `agentsMayMerge`). | |
| 314 | +- Agents can't push or commit to the default branch or protected branches, and can't merge into them, unless the branch's rule allows it (`agentsMayPush`, `agentsMayMerge`). Where the rule has `allowAgentAutoMerge` and a required check, an agent lands its pull request there with `enable_auto_merge` instead, and Diffinery merges once the rules are met (see [Agents landing changes](/docs/repositories/branch-protection#agents-landing-changes)). | |
| 276 | 315 | - Pull requests opened by agents need a human approval, and approvals submitted by agents with `review_pull_request` never count toward it. |
| 277 | 316 | - Private repositories you can't see return `repository not found`, the same as a repository that doesn't exist. |
| 317 | +- Archived repositories refuse every tool that changes something (`repository is archived (read-only)`), whatever the role. Read-only tools keep working. | |
| 318 | +- `commit_files` refuses files that look like credentials (see [Commit without git](#commit-without-git)). | |
| 319 | +- `create_issue` drops `labels` when the account's role is below triage, as GitHub does for issues opened by people without triage access. | |
| 278 | 320 | - Release tools follow the same rules as the REST API: anyone who can read the repository can list published releases, writers create, edit and publish them and see drafts, and a release that creates a tag is a ref update, so agent branch and tag rules apply. |
| 321 | +- `start_github_import` follows the agent import rules in [Imports over MCP](#imports-over-mcp). | |
| 279 | 322 | - `search_docs` and `read_docs` read the public documentation. They work with any token, read-only included, and don't touch a repository. |
| 280 | 323 | |
| 281 | 324 | See [Agents](/docs/agents/overview#guardrails) and [Permissions](/docs/accounts/permissions). |
| @@ -405,7 +448,7 @@ | ||
| 405 | 448 | 5. `sandbox_create` and `sandbox_exec` to build and run tests when needed. |
| 406 | 449 | 6. `create_pull_request` with an `intent` that states the goal. |
| 407 | 450 | 7. `list_workflow_runs`, `get_workflow_run` and `get_job_logs` to follow CI, fixing failures with more commits. |
| 408 | -8. `get_pull_request` to read reviews and the remaining merge blockers. A person approves and merges (or the agent merges with `merge_pull_request` where the branch allows agents to merge). | |
| 451 | +8. `get_pull_request` to read reviews and the remaining merge blockers. A person approves and merges. Or the agent calls `enable_auto_merge` where the branch allows it (`allowAgentAutoMerge`), and Diffinery merges once the checks pass and a person has approved, or merges with `merge_pull_request` where the branch allows agents to merge. | |
| 409 | 452 | |
| 410 | 453 | ## Limitations |
| 411 | 454 | |
| @@ -419,13 +462,20 @@ | ||
| 419 | 462 | | --- | --- | |
| 420 | 463 | | The client says it can't connect or gets `401` | The `Authorization` header is missing or the token is wrong. Check the header name and the `Bearer ` prefix, and that the token hasn't expired. | |
| 421 | 464 | | `repository not found` | Wrong `owner` or `repo`, or the account (or, for an agent, its owner) can't see the repository. | |
| 422 | -| `this token needs the write scope` | The tool isn't read-only and the token has only the `read` scope. Create a `write` token (for an agent, in **Settings → Agents** or with `POST /api/v3/diffinery/agents/{login}/tokens`). | | |
| 465 | +| `this token needs the write scope` | The tool isn't read-only and the token has only the `read` scope. Create a `write` token (for an agent, in **Settings → Agents** while signed in). | | |
| 423 | 466 | | `write access required` (or `triage access required`) | The account's role, or the token's scope, is too low for the tool. A `read` token can only use read-only tools. | |
| 424 | 467 | | `agents may not push to protected or default branches; open a pull request` | Commit to a new branch with `create_branch_from` and open a pull request. | |
| 468 | +| `agents may not merge into this branch; a human must merge` | `merge_pull_request` into the default branch or a protected branch. Use `enable_auto_merge` where the branch allows it, or ask a person to merge. | | |
| 469 | +| `agents may not have pull requests merged into main automatically unless its branch protection allows it ...` | `enable_auto_merge` on a branch whose rule doesn't set `allowAgentAutoMerge`. A person merges, or an admin allows it. | | |
| 470 | +| `Auto-merge was turned off: ...` (a comment on the pull request) | Auto-merge stopped, for example because someone else pushed to the branch. Read the reason and call `enable_auto_merge` again. See [When auto-merge turns itself off](/docs/collaboration/pull-requests#when-auto-merge-turns-itself-off). | | |
| 425 | 471 | | `branch moved; refresh and retry` | Someone else committed to the branch. Read the new head and retry with the new `expected_head_sha`. | |
| 472 | +| `push protection: possible secret detected (...)` | A file passed to `commit_files` looks like a credential. Remove it and commit again, or add `diffinery:allow-secret` to a file whose keys are fake. | | |
| 473 | +| `repository is archived (read-only)` | The repository is archived. Only read-only tools work until a repository admin unarchives it. | | |
| 426 | 474 | | `method not found: resources/list` | The server only offers tools; the client can ignore this. | |
| 427 | 475 | | `MCP uploads are limited to 25 MB; use the upload_url of the REST API for larger files` | `upload_release_asset` takes the file inline. Upload larger files (up to 100 MB) to the release's `upload_url` with the [REST API](/docs/reference/rest-api#release-assets). | |
| 428 | 476 | | `pass exactly one of content or content_base64` | `upload_release_asset` needs the file as either text or base64, not both and not neither. | |
| 429 | 477 | | `make_latest must be true, false or legacy` | Pass `make_latest` as one of those strings, not as a boolean. | |
| 430 | 478 | | `query is required` | `search_docs` was called with an empty `query`. | |
| 479 | +| `agents can import only into organizations they or their owner administer` | `start_github_import` from an agent needs an organization the agent or its owner administers as `owner`, never a user account. | | |
| 480 | +| `too many repository imports are already running (at most 3 at a time); wait for one to finish` | Wait for one of the running imports to finish (check with `list_imports`). | | |
| 431 | 481 | | `no docs page "..."; pages: ...` | `read_docs` didn't recognize the slug. Pick one from the list in the message, or call `read_docs` with `index`. | |
modifiedapps/diffinery/docs/agents/overview.md+46−17View file
| @@ -24,7 +24,7 @@ | ||
| 24 | 24 | 1. Go to **Settings → Agents** (`https://diffinery.com/settings/agents`). |
| 25 | 25 | 2. Under **Create an agent**, fill in: |
| 26 | 26 | - **Agent username**, for example `review-bot`. |
| 27 | - - **Display name** (optional), for example `Review bot`. | |
| 27 | + - **Display name** (optional), for example `Review bot`. Without one, the agent's username is shown. | |
| 28 | 28 | - **Owner**: yourself, or an organization where you are an owner or admin. |
| 29 | 29 | - **Maximum role**: the highest role the agent can have on any repository (default `write`). |
| 30 | 30 | 3. Click **Create agent**. You see `Agent created. Create a token for it below.` |
| @@ -60,23 +60,27 @@ | ||
| 60 | 60 | |
| 61 | 61 | ### With the REST API |
| 62 | 62 | |
| 63 | -```sh | |
| 64 | -curl -X POST https://diffinery.com/api/v3/diffinery/agents \ | |
| 65 | - -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \ | |
| 66 | - -d '{ | |
| 67 | - "login": "review-bot", | |
| 68 | - "name": "Review bot", | |
| 69 | - "org": "acme", | |
| 70 | - "policy": { "maxRole": "write" }, | |
| 71 | - "token": { "scopes": ["write"], "expires_in_days": 90 } | |
| 72 | - }' | |
| 63 | +Creating an agent mints a token, so it needs a browser session: a token gets `403 creating tokens needs a browser session: sign in at https://diffinery.com/settings/tokens`. Send it from a page on diffinery.com while signed in, for example from the browser console: | |
| 64 | + | |
| 65 | +```js | |
| 66 | +await fetch("/api/v3/diffinery/agents", { | |
| 67 | + method: "POST", | |
| 68 | + headers: { "Content-Type": "application/json" }, | |
| 69 | + body: JSON.stringify({ | |
| 70 | + login: "review-bot", | |
| 71 | + name: "Review bot", | |
| 72 | + org: "acme", | |
| 73 | + policy: { maxRole: "write" }, | |
| 74 | + token: { scopes: ["write"], expires_in_days: 90 }, | |
| 75 | + }), | |
| 76 | +}).then((r) => r.json()); | |
| 73 | 77 | ``` |
| 74 | 78 | |
| 75 | 79 | The response (`201`) contains the agent (`id`, `login`, `name`, `type: "Bot"`, `html_url`) and its first token (`token`, `id`, `expiresAt`). Leave out `org` to own the agent yourself, `policy.maxRole` for the default `write`, and `token` for a `write` token that lasts 90 days. |
| 76 | 80 | |
| 77 | 81 | `token.scopes` (and `scopes` when you create another token) must be a non-empty list of `read`, `write` and `admin`; leaving it out gives `["write"]`. An empty list or an unknown name returns `422 scopes must be a non-empty list of: read, write, admin`. |
| 78 | 82 | |
| 79 | -The `POST` endpoints below create tokens, so they need a browser session: every token gets `403 creating tokens needs a browser session: sign in at https://diffinery.com/settings/tokens`. `DELETE` also accepts a personal token with the `admin` scope that isn't restricted to repositories. | |
| 83 | +The `POST` endpoints below create tokens, so they need a browser session. `DELETE` also accepts a personal token with the `admin` scope that isn't restricted to repositories. | |
| 80 | 84 | |
| 81 | 85 | | Endpoint | Purpose | Who | |
| 82 | 86 | | --- | --- | --- | |
| @@ -118,21 +122,27 @@ | ||
| 118 | 122 | | Rule | Details | How to relax it | |
| 119 | 123 | | --- | --- | --- | |
| 120 | 124 | | No pushes to the default branch or protected branches | Pushes, `commit_files`, the contents API, the git data refs API, the merges API, update-branch and branch updates are refused with `agents may not push to protected or default branches; open a pull request`. | Tick **Allow agents to push directly** (`agentsMayPush`) on that branch's protection rule. | |
| 121 | -| No merges into the default branch or protected branches | The merge blocker `agents may not merge into this branch; a human must merge`. Agents can't add pull requests to a merge queue or turn on auto-merge there either. | Tick **Allow agents to merge pull requests** (`agentsMayMerge`) on the rule, or, so that CI decides instead of the agent, **Allow agents to land pull requests through auto-merge** (`allowAgentAutoMerge`, see [Agents landing changes](/docs/repositories/branch-protection#agents-landing-changes)). | | |
| 122 | -| Pull requests opened by agents need a human approval | At least one approval from a person, even on a branch with no protection rule. | None. | | |
| 125 | +| No merges into the default branch or protected branches | The merge blocker `agents may not merge into this branch; a human must merge`. Agents can't add pull requests to a merge queue or turn on auto-merge there either. | Tick **Allow agents to merge pull requests** (`agentsMayMerge`) on the rule, or, so that CI decides instead of the agent, **Allow agents to land pull requests through auto-merge** (`allowAgentAutoMerge`, see [Agents landing changes](/docs/repositories/branch-protection#agents-landing-changes)); the agent then turns on [auto-merge](/docs/collaboration/pull-requests#auto-merge-for-agents-and-workflow-tokens) with `enable_auto_merge` or `PUT /repos/{owner}/{repo}/pulls/{number}/auto-merge`. | | |
| 126 | +| Pull requests opened by agents need a human approval | At least one approval from a person, even on a branch with no protection rule. When an agent has Diffinery land its pull request on the default branch or a protected branch through auto-merge or the merge queue, the approval counts only for the commit it approved. | Only for landings an agent asked for: **Let agents' auto-merges land without an approving review** (`agentAutoMergeWithoutApproval`), which lets green required checks stand in for the approval. See [Waiving approvals for agents](/docs/repositories/branch-protection#waiving-approvals-for-agents). | | |
| 123 | 127 | | Agent approvals don't count | An agent's Approve review is shown but never counts toward required approvals. An agent's Request changes review still blocks. | None. | |
| 124 | 128 | | No branch protection changes | `agents cannot change branch protection` | None. | |
| 125 | 129 | | No secret management | Setting or deleting repository or environment secrets fails with `agents cannot manage secrets`, and agents can't use the organization secrets endpoints, whatever their role. | None. | |
| 126 | 130 | | No variable changes | Creating, changing or deleting repository or environment Actions variables fails with `agents cannot manage variables`, whatever the agent's role. Agents with write or higher can still list and read variables. | None. | |
| 127 | 131 | | No environment changes | Creating, changing or deleting an environment or its deployment branch policies fails with `only repository admins (people, not agents) can manage environments`. | None. | |
| 128 | 132 | | Agent reports never satisfy required checks | Commit statuses and check runs an agent reports are shown on the commit, but never count toward a required status check, and aren't counted when a deployment checks the commit's statuses. A failure reported by the real source still fails. | None. Report required checks from workflows or from a person's token. | |
| 129 | -| No environment secrets for agent-started runs on open environments | A job with `environment:` in a run an agent started gets the environment's secrets only when the environment is limited to protected branches. Pull request runs never get environment secrets. | Limit the environment to **protected branches**. | | |
| 133 | +| Workflow runs of agent code act as the agent | A run counts as coming from an agent when an agent triggered it, an agent pushed the branch it runs on last, or (for pull request events) an agent opened the pull request or last pushed its head branch. Re-runs of such a run keep this, whoever starts them, and so do `workflow_run` runs that follow it, since they act on what it produced. The run's jobs act as an agent: their `GITHUB_TOKEN` gets the guardrails in this table, so it can't push or merge to the default or protected branches even with `permissions: contents: write`, and their deployments count as an agent's. Agent-written code can't reach a protected branch through a workflow. | The same per-branch settings, **Allow agents to push directly** and **Allow agents to merge pull requests**. | | |
| 134 | +| No environment secrets for agent runs on open environments | A job with `environment:` in a run whose code came from an agent (see the row above) gets the environment's secrets only when the environment is limited to protected branches. Pull request runs never get environment secrets. | Limit the environment to **protected branches**. | | |
| 130 | 135 | | Agent tags don't match environment tag rules | A tag last pushed or moved by an agent never matches an environment's tag pattern, so it can't deploy to that environment. | Have a person push the tag. | |
| 131 | 136 | | No review dismissal | Dismissing a review fails with `agents can't dismiss reviews`, whatever the agent's role. | None. | |
| 132 | 137 | | No repository deletion | `agents cannot delete repositories` | None. | |
| 133 | -| No push protection bypass | Only repository admins who are people can push past secret scanning with `git push -o allow-secrets=<reason>`. | None. | | |
| 138 | +| No repository transfers | `agents and workflow tokens cannot transfer repositories`. Only people move repositories to another owner. | None. | | |
| 139 | +| No push protection bypass | Only repository admins who are people can commit past secret scanning, with `git push -o allow-secrets=<reason>` or `allow_secrets` on `POST /repos/{owner}/{repo}/diffinery/commits`. An agent's push or commit that contains a credential is always refused (and `commit_files` has no bypass at all); see [Push protection](/docs/security/push-protection). | None. | | |
| 134 | 140 | | No admin bypass of merge requirements | Merging past unmet branch rules (`"bypass_rules": true`) or past the merge queue (`"bypass_merge_queue": true`) is for repository admins who are people. An agent's merge with these flags is still refused with the usual blockers. | None. | |
| 135 | 141 | | No creating agents | `only people can create agents` | None. | |
| 142 | +| Imports from GitHub only into organizations | An agent can start a GitHub import only into the organization that owns it, or one where its owner is an owner or admin, never into a user account (`agents can import only into organizations they or their owner administer`). Its maximum role must be at least `write`. | None. | | |
| 143 | +| No push protection bypass in imports | An agent's import always stops when old history contains something that looks like a credential, whatever it asks for. | A person who administers the target starts the import. | | |
| 144 | +| No variables, secret names or environments in imports | The import's variables steps are skipped (`skipped (agents cannot manage variables)`). | A person sets them up after the import. | | |
| 145 | +| No import retry once the repository exists | `an agent cannot retry an import after the repository was created; a person must retry it` | A person starts a new import. | | |
| 136 | 146 | |
| 137 | 147 | The default branch is protected from agents even if it has no protection rule. Agents can still push freely to other branches, open pull requests, comment, review, run workflows, report commit statuses and check runs, create releases and start sandboxes, within their role. |
| 138 | 148 | |
| @@ -178,6 +188,19 @@ | ||
| 178 | 188 | - **Audit log:** entries for agent actions (pushes, merges and other audited actions) record the owner in `on_behalf_of`. Creating, disabling and enabling agents, and creating agent tokens, are audited (`agent.create`, `agent.disable`, `agent.enable`, `agent.token.create`). See [Audit log](/docs/security/audit-log). |
| 179 | 189 | - **Email:** the person who creates an agent gets a security email, `A new agent was added: review-bot`, which can't be turned off. |
| 180 | 190 | |
| 191 | +## Imports from GitHub | |
| 192 | + | |
| 193 | +Agents can import a GitHub repository with the `start_github_import` MCP tool (or `POST /api/v3/diffinery/imports`, or `diffinery migrate github` with the agent's token), and follow it with `get_import` and `list_imports`. The import runs on the server and copies the Git history, issues and pull requests with their numbers, comments, reviews, labels, milestones, branch protection and releases. The agent rules: | |
| 194 | + | |
| 195 | +- The target must be an organization: the one that owns the agent, or one where the agent's owner is an owner or admin. | |
| 196 | +- The agent's maximum role must be at least `write`, and its token needs the `write` scope. | |
| 197 | +- Push protection is never bypassed: the `secrets` option is always `stop`, so history with something that looks like a credential fails the import instead of being imported. | |
| 198 | +- Actions variables, secret names, environments and organization variables are skipped. Agents never read or set secret values. | |
| 199 | +- Once the repository exists, the agent can't retry a failed import; a person starts a new one. | |
| 200 | +- The agent sees only the imports it started. | |
| 201 | + | |
| 202 | +Everything the import writes fires no workflows, webhooks or notifications, and the repository is locked against other pushes until the import ends. Imported reviews never count as approvals, so an imported open pull request still needs a fresh human approval. See [Migrate from GitHub](/docs/migrate-from-github#agents-and-imports) and [Imports over MCP](/docs/agents/mcp#imports-over-mcp). | |
| 203 | + | |
| 181 | 204 | ## Disable an agent |
| 182 | 205 | |
| 183 | 206 | Disabling an agent revokes all its tokens at once. |
| @@ -247,8 +270,9 @@ | ||
| 247 | 270 | ## Differences from GitHub |
| 248 | 271 | |
| 249 | 272 | - Agents are accounts owned by a person or an organization, rather than GitHub Apps with installations, and authenticate with ordinary `dfy_` tokens. |
| 250 | -- Agents are held to built-in guardrails (no pushes or merges to default and protected branches, human approval for their pull requests, agent approvals not counted) that you relax per branch with `agentsMayPush` and `agentsMayMerge`. | |
| 273 | +- Agents are held to built-in guardrails (no pushes or merges to default and protected branches, human approval for their pull requests, agent approvals not counted) that you relax per branch with `agentsMayPush`, `agentsMayMerge`, `allowAgentAutoMerge` and `agentAutoMergeWithoutApproval`. The guardrails also hold for workflow runs of code an agent wrote, so an agent can't use a workflow's `GITHUB_TOKEN` to get around them. | |
| 251 | 274 | - Commit statuses and check runs reported by agents never satisfy required checks, and agents can't manage environments, secrets or Actions variables, or dismiss reviews. |
| 275 | +- Agents can run GitHub imports, within the import rules above. | |
| 252 | 276 | - Commits Diffinery writes for agents carry a `diffinery-agent ... on-behalf-of ...` header. |
| 253 | 277 | |
| 254 | 278 | ## Troubleshooting |
| @@ -261,6 +285,8 @@ | ||
| 261 | 285 | | `only people can create agents` | You called the agents API with an agent's token. Use your own. | |
| 262 | 286 | | `agents cannot manage secrets` | A person with admin access must set or delete repository, environment and organization secrets. | |
| 263 | 287 | | `agents cannot manage variables` | A person with admin access must create, change or delete repository and environment variables. | |
| 288 | +| A workflow on the agent's branch fails to push to `main` with `agents may not push to protected or default branches; open a pull request` | The run's code came from an agent, so its job token acts as the agent. Open a pull request from the branch and have a person merge it. | | |
| 289 | +| `push protection: possible secret detected (...)` from `commit_files` or the commits API | A file the agent committed looks like a credential. Remove it, or mark a test fixture with `diffinery:allow-secret`. Agents can't bypass push protection. | | |
| 264 | 290 | | `this token needs the write scope` | The agent's token has only the `read` scope. Create a `write` token for it. | |
| 265 | 291 | | `this token needs the admin scope` | Creating agents or agent tokens, or disabling an agent, through the API or CLI needs your personal `admin` token. | |
| 266 | 292 | | `this token is limited to repositories and can't change account settings` | Managing agents needs a personal token without a repository list. | |
| @@ -273,4 +299,7 @@ | ||
| 273 | 299 | | `name already taken` | Pick another login; agents share the namespace with people and organizations. | |
| 274 | 300 | | `not your agent` | Only the owner, or an owner or admin of the owning organization, can manage it. | |
| 275 | 301 | | `repository not found` for a repository you can see | The agent's owner (or, for an organization agent, the organization) has no access to it. | |
| 302 | +| `agents can import only into organizations they or their owner administer` | Import into an organization that owns the agent or that the agent's owner administers. Agents can't import into user accounts. | | |
| 303 | +| `this agent's policy does not allow writing; an import needs at least write` | Raise the agent's **Maximum role** to `write` or higher. | | |
| 304 | +| `skipped (agents cannot manage variables)` on an import | Expected for an agent's import. A person sets variables, environments and secrets afterwards. | | |
| 276 | 305 | | `invalid, expired or revoked token` | The token expired or was revoked, or the agent is disabled. Create a new token. | |
modifiedapps/diffinery/docs/agents/sandboxes.md+2−1View file
| @@ -27,8 +27,9 @@ | ||
| 27 | 27 | - Creating and using a sandbox needs **write** access to the repository. A `read`-scoped token can't create one. |
| 28 | 28 | - Each call on a sandbox is allowed for the account that created it and for repository admins; anyone else gets `not your sandbox`. |
| 29 | 29 | - Inside the sandbox, git authenticates as the creator, with the role the creator had when the sandbox was created, capped at write (even if the creator is an admin) and at the creator's current role. The sandbox's credentials stop working 24 hours after it starts, when it is closed, or as soon as the creator's account is disabled or loses access to the repository. |
| 30 | +- The sandbox's git credential (a `dst_` token) also lets git clone and fetch other public repositories, for example public submodules, but no other private repository. Use a token of your own inside the sandbox if you need one. | |
| 30 | 31 | - Job and sandbox tokens can't create sandboxes (`403 job and sandbox tokens can't start sandboxes`). Pushes from the sandbox follow the same rules as any push: branch protection, push protection (secret scanning) and, for agents, the [agent guardrails](/docs/agents/overview#guardrails). An agent can't push to the default branch from a sandbox either. |
| 31 | -- Archived repositories can't have sandboxes. | |
| 32 | +- Archived repositories can't have sandboxes: starting one, and every call on an existing one (running commands, reading or writing files, closing it), gets `403 repository is archived (read-only)`. A sandbox left running when its repository is archived stops on its own after 30 idle minutes. | |
| 32 | 33 | |
| 33 | 34 | ## What's inside |
| 34 | 35 |
modifiedapps/diffinery/docs/collaboration/checks-and-statuses.md+27−5View file
| @@ -31,10 +31,30 @@ | ||
| 31 | 31 | |
| 32 | 32 | Re-running a job replaces the check of the same name on the same commit. Check runs from Actions can't be changed over the REST API: a `PATCH` fails with `check runs from Diffinery Actions can't be changed over the API`. |
| 33 | 33 | |
| 34 | +A job's `check_run_url` (in `GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs` and `GET /repos/{owner}/{repo}/actions/jobs/{job_id}`) is `https://diffinery.com/api/v3/repos/acme/website/check-runs/{job_id}`. `GET` on it returns the check run that job reports (its latest one, after a re-run), so tools that follow `check_run_url` from a job to its check run work as on GitHub. | |
| 35 | + | |
| 34 | 36 | > **Note:** GitHub names an Actions check run after the job only. In Diffinery the name includes the workflow name, so required check names copied from GitHub usually need the `<workflow> / ` prefix. |
| 35 | 37 | |
| 36 | -The job's own `GITHUB_TOKEN` is read-only on the REST API, so a step can't post extra statuses or check runs with it. If a workflow needs to report something beyond its job checks, store a personal token with the `write` scope as a secret and use that. | |
| 38 | +A step can also report with the job's own `GITHUB_TOKEN`, within the job's `permissions:`: | |
| 37 | 39 | |
| 40 | +- **Commit statuses** need `statuses: write`. They are reported by `diffinery-actions[bot]` and count toward required checks like any other non-agent report (in a run whose code came from an agent, the token acts as an agent, and its statuses don't count). | |
| 41 | +- **Check runs** can't be created with the job token: `POST .../check-runs` fails with `this account can't report check runs`. Report a commit status instead, or use a personal token with the `write` scope stored as a secret. | |
| 42 | + | |
| 43 | +```yaml | |
| 44 | +permissions: | |
| 45 | + contents: read | |
| 46 | + statuses: write | |
| 47 | +steps: | |
| 48 | + - run: | | |
| 49 | + curl -sS -X POST -H "Authorization: Bearer $GITHUB_TOKEN" -H "Content-Type: application/json" \ | |
| 50 | + "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" \ | |
| 51 | + -d '{"state": "success", "context": "lighthouse", "description": "Score 98"}' | |
| 52 | + env: | |
| 53 | + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| 54 | +``` | |
| 55 | + | |
| 56 | +Without `statuses: write`, the request fails with `403` and `Resource not accessible by integration (the workflow's GITHUB_TOKEN needs permissions: statuses: write)`. | |
| 57 | + | |
| 38 | 58 | ## Report results from external CI |
| 39 | 59 | |
| 40 | 60 | An external CI system (or any script or bot) reports results with a token for an account that has the **write** role on the repository. A personal access token with the `write` scope works. For results that should satisfy required checks, use a regular account, not an agent: reports from agents never satisfy required checks (see [Agents and checks](#agents-and-checks)). |
| @@ -304,7 +324,7 @@ | ||
| 304 | 324 | - Binding only affects names listed in `requiredChecks`. |
| 305 | 325 | - Binding a check to an agent's login means it can never pass, because agent reports are ignored. |
| 306 | 326 | |
| 307 | -> **Important:** `requiredCheckSources` can only be set through the API, and the **Branch protection** settings page does not show it. Saving the rule from that page writes the rule without `requiredCheckSources`, which removes the bindings. Set them again with the API after editing the rule in the web UI. | |
| 327 | +`requiredCheckSources` can only be set through the API, and the **Branch protection** settings page does not show it. Saving the rule from that page keeps the bindings of every check that is still listed in **Required status checks** (also when you rename the rule's pattern). A binding for a check you removed from the list is dropped; add the check back with the API, binding included. | |
| 308 | 328 | |
| 309 | 329 | ### Required checks and the merge queue |
| 310 | 330 | |
| @@ -351,7 +371,7 @@ | ||
| 351 | 371 | | Check suites can be created and re-requested | Check suites are derived from check runs and read-only. | |
| 352 | 372 | | Status descriptions are limited to 140 characters | Up to 1,024 characters are kept. | |
| 353 | 373 | | `required_status_checks` with an app id restricts who can satisfy a check | `requiredCheckSources` binds a check name to a login or to `diffinery-actions`; when Actions reports a name, it alone decides that check even without a binding. | |
| 354 | -| Statuses and check runs can be posted with the workflow's `GITHUB_TOKEN` | The job token is read-only on the REST API. | | |
| 374 | +| Statuses and check runs can be posted with the workflow's `GITHUB_TOKEN` | Statuses can (with `statuses: write`); check runs can't. | | |
| 355 | 375 | | `check_run` and `check_suite` webhook events | Not sent. Commit statuses send `status` events. | |
| 356 | 376 | | Agents don't exist | Reports from agents never decide a required check. | |
| 357 | 377 | |
| @@ -361,8 +381,10 @@ | ||
| 361 | 381 | |---|---| |
| 362 | 382 | | `required check '<name>' has not run` | No report with exactly that name on the head commit, or only reports that don't count (from agents, from Actions-overridden reporters, or from someone other than the bound source). Compare the name with the **Checks** tab, including the `<workflow> / ` prefix for Actions jobs. | |
| 363 | 383 | | A passing external status is ignored | Actions also reports a check with that name, and Actions wins; or `requiredCheckSources` binds the name to a different login; or the reporter is an agent. Rename your context or fix the binding. | |
| 364 | -| Bindings disappeared | The rule was saved from the **Branch protection** settings page, which drops `requiredCheckSources`. Set them again with the API. | | |
| 365 | -| `write access required` | The token's account lacks the write role, or the request used a workflow's `GITHUB_TOKEN`, which is always read-only here. | | |
| 384 | +| Bindings disappeared | The check was removed from **Required status checks** when the rule was saved from the **Branch protection** settings page, which drops the binding of a check that is no longer required. Set it again with the API. | | |
| 385 | +| `write access required` | The token's account lacks the write role, or a workflow's `GITHUB_TOKEN` has no write permission at all (for example `permissions: read-all`). | | |
| 386 | +| `Resource not accessible by integration (...)` | A workflow's `GITHUB_TOKEN` lacks the permission named in the message. Add `statuses: write` to the job's `permissions:` to post statuses. | | |
| 387 | +| `this account can't report check runs` | Check runs can't be created with a workflow's `GITHUB_TOKEN`. Post a commit status, or use a personal token. | | |
| 366 | 388 | | `this token needs the write scope` | The token has only the `read` scope. Use a token with the `write` scope. | |
| 367 | 389 | | `only the app that created a check run can update it` | Update the check run with the same account (any of its tokens) that created it. | |
| 368 | 390 | | `check runs from Diffinery Actions can't be changed over the API` | Re-run the job instead. | |
modifiedapps/diffinery/docs/collaboration/issues.md+87−6View file
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | --- |
| 2 | 2 | title: Issues |
| 3 | -description: Open, discuss, label, assign, lock, react to, close and search issues in the web UI, the CLI, the REST API and over MCP. | |
| 3 | +description: Open, discuss, label, assign, lock, react to, close and search issues, and group them in milestones, in the web UI, the CLI, the REST API and over MCP. | |
| 4 | 4 | --- |
| 5 | 5 | |
| 6 | 6 | Issues track bugs, tasks and ideas for a repository. Each issue has a title, a Markdown description, a conversation, labels, assignees and an open or closed state, and you can work with them from the web UI, the `diffinery` CLI, the REST API or an MCP client. |
| @@ -34,16 +34,19 @@ | ||
| 34 | 34 | | Set labels and assignees with `PATCH .../issues/{number}` | The issue's author, or triage or higher | |
| 35 | 35 | | Add or remove labels and assignees with the label and assignee endpoints | Triage or higher | |
| 36 | 36 | | Create, rename, recolor and delete labels | Triage or higher | |
| 37 | +| Create, edit and close milestones, and set an issue's milestone | Triage or higher | | |
| 37 | 38 | | Edit or delete a comment | The comment's author, or maintain or higher | |
| 38 | 39 | | Lock and unlock a conversation | Triage or higher | |
| 39 | 40 | | Comment or react on a locked issue | Triage or higher | |
| 40 | 41 | | Add a reaction | Anyone signed in who can read the repository (with a `write` or `admin` token through the API) | |
| 41 | 42 | | Delete a reaction | The person who added it, or admin | |
| 42 | 43 | |
| 43 | -When you open an issue through the API or MCP, the `labels` and `assignees` you send are applied whatever your role; the web form only offers labels to triage or higher. | |
| 44 | +When you open an issue, the `labels` and `assignees` you send are applied only if you have triage access or higher. Without it they are silently dropped and the issue is created without them, as on GitHub: the request still succeeds with `201`. This applies to the REST API, the CLI (`--label`, `--assignee`) and the MCP `create_issue` tool; the web form only offers labels to triage or higher. Editing works the same way: when the issue's author (or anyone else) without triage access sends `labels` or `assignees` with `PATCH .../issues/{number}`, they are dropped too, and the rest of the edit is applied. | |
| 44 | 45 | |
| 45 | -In an archived repository the web UI shows issues read-only and has no forms to change them, and the label, assignee, lock, reaction and comment deletion endpoints refuse changes with `403` and `repository is archived (read-only)`. The REST endpoints that open and edit issues and add or edit comments (`POST` and `PATCH .../issues`, `POST .../issues/{number}/comments`, `PATCH .../issues/comments/{id}`) and the MCP `create_issue` tool don't check the archived state and still accept changes. | |
| 46 | +In an archived repository every change is refused, whatever your role: opening and editing issues, comments, labels, assignees, locks and reactions, through the web UI, the REST API, the CLI and MCP, all fail with `403` and `repository is archived (read-only)`. The web UI shows issues read-only. Subscribing to an issue and watching the repository still work. | |
| 46 | 47 | |
| 48 | +A workflow's `GITHUB_TOKEN` can work with issues within its `permissions:`: opening an issue (`POST .../issues`) needs `issues: write`, and comments, labels, assignees and reactions on an issue accept `issues: write` or `pull-requests: write`. Without them the request fails with `403` and `Resource not accessible by integration (the workflow's GITHUB_TOKEN needs permissions: issues: write)`. A job token acts with at most the write role, so it can set labels and assignees when it opens an issue. | |
| 49 | + | |
| 47 | 50 | ## Open an issue |
| 48 | 51 | |
| 49 | 52 | ### Open an issue in the web UI |
| @@ -209,6 +212,73 @@ | ||
| 209 | 212 | |
| 210 | 213 | Logins are not checked when you assign them, so check the spelling. `GET /repos/{owner}/{repo}/assignees` lists the accounts GitHub clients offer as assignees: the repository's direct collaborators with triage or higher, plus the owner of a personal repository. `GET /repos/{owner}/{repo}/assignees/{login}` answers `204` for those accounts and `404` otherwise; it doesn't stop you from assigning anyone else. |
| 211 | 214 | |
| 215 | +## Milestones | |
| 216 | + | |
| 217 | +A milestone groups issues and pull requests that belong to one goal, such as a release, with an optional due date. Each issue or pull request has at most one milestone. Milestones have their own numbers in each repository, starting at 1; milestones brought in by a [GitHub import](/docs/migrate-from-github) keep their GitHub numbers, and new ones are numbered after them. | |
| 218 | + | |
| 219 | +You create and edit milestones with the REST API (there's no web page, CLI command or MCP tool for them yet) and set an issue's milestone in the web UI or with the REST API. Creating, editing and setting milestones needs triage or higher and a token with the `write` scope; anyone who can read the repository can read them. | |
| 220 | + | |
| 221 | +### Milestone fields | |
| 222 | + | |
| 223 | +| Field | Description | | |
| 224 | +| --- | --- | | |
| 225 | +| `number` (also `id`) | The milestone's number in the repository | | |
| 226 | +| `title` | 1 to 255 characters; required when you create a milestone | | |
| 227 | +| `description` | Text up to 65,536 characters; empty by default | | |
| 228 | +| `state` | `open` or `closed`. New milestones are always `open`. | | |
| 229 | +| `due_on` | An ISO 8601 timestamp, or `null` for no due date. It's stored in UTC. | | |
| 230 | +| `creator` | `{"login": "..."}` of the account that created it | | |
| 231 | +| `open_issues`, `closed_issues` | How many open and closed issues and pull requests have this milestone (merged pull requests count as closed) | | |
| 232 | +| `created_at`, `updated_at`, `closed_at` | Timestamps; `closed_at` is set when the milestone is closed and cleared when it's reopened | | |
| 233 | + | |
| 234 | +### Create and edit milestones with the REST API | |
| 235 | + | |
| 236 | +| Request | Needs | Does | | |
| 237 | +| --- | --- | --- | | |
| 238 | +| `GET /repos/{owner}/{repo}/milestones` | read | Lists milestones. `state` is `open` (default), `closed` or `all`. Sorted by due date (milestones without one last), then by number. The list isn't paged. | | |
| 239 | +| `GET /repos/{owner}/{repo}/milestones/{number}` | read | One milestone, or `404 milestone not found` | | |
| 240 | +| `POST /repos/{owner}/{repo}/milestones` | triage | Creates a milestone from `title`, `description` and `due_on`; returns `201` with the milestone. A `state` in the body is ignored. | | |
| 241 | +| `PATCH /repos/{owner}/{repo}/milestones/{number}` | triage | Changes any of `title`, `description`, `state` and `due_on` (`null` removes the due date); returns the milestone | | |
| 242 | + | |
| 243 | +```sh | |
| 244 | +# Create a milestone | |
| 245 | +curl -X POST https://diffinery.com/api/v3/repos/acme/website/milestones \ | |
| 246 | + -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \ | |
| 247 | + -d '{"title": "v2.0", "description": "Pricing page redesign", "due_on": "2026-12-01T00:00:00Z"}' | |
| 248 | + | |
| 249 | +# Close it | |
| 250 | +curl -X PATCH https://diffinery.com/api/v3/repos/acme/website/milestones/1 \ | |
| 251 | + -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \ | |
| 252 | + -d '{"state": "closed"}' | |
| 253 | + | |
| 254 | +# List every milestone, open and closed | |
| 255 | +curl "https://diffinery.com/api/v3/repos/acme/website/milestones?state=all" \ | |
| 256 | + -H "Authorization: Bearer dfy_..." | |
| 257 | +``` | |
| 258 | + | |
| 259 | +Milestones can't be deleted; close one you no longer use. To clear a description, send `"description": ""` (`null` leaves it unchanged). | |
| 260 | + | |
| 261 | +### Set the milestone of an issue or pull request | |
| 262 | + | |
| 263 | +- **Web UI (triage or higher):** the sidebar of an issue or pull request has a **Milestone** section showing the milestone's title, a `closed` badge if it's closed and its due date, or **No milestone**. Click **Set milestone**, choose a milestone in the **Milestone** list (it offers the open milestones and **No milestone**), and click **Save**. **Set milestone** appears only when the repository has an open milestone or the issue already has one. | |
| 264 | +- **REST API (triage or higher):** send `milestone` with the milestone's number, or `null` to remove it, on `PATCH /repos/{owner}/{repo}/issues/{number}`. This works for pull request numbers too, and also accepts closed milestones. | |
| 265 | + | |
| 266 | +```sh | |
| 267 | +curl -X PATCH https://diffinery.com/api/v3/repos/acme/website/issues/12 \ | |
| 268 | + -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \ | |
| 269 | + -d '{"milestone": 1}' | |
| 270 | +``` | |
| 271 | + | |
| 272 | +Issues and pull requests returned by the REST API carry a `milestone` object (`number`, `id`, `title`, `state`, `due_on`, `url`; `description` is always `null` there), or `null`. Things to know about setting it with `PATCH`: | |
| 273 | + | |
| 274 | +- The response shows the issue as it was before the milestone changed. Read the issue again (`GET .../issues/{number}`) to see the new milestone. | |
| 275 | +- The other fields in the same request are saved first. If you're the issue's author without triage access, or the milestone doesn't exist, the title, body, state, labels and assignees you sent are saved and the request then fails with `403 triage access required` or `404 milestone not found`. Send `milestone` in a request of its own. | |
| 276 | +- A value that isn't a whole number or `null` fails with `422 milestone must be a milestone number or null`, and nothing is saved. | |
| 277 | + | |
| 278 | +A workflow's `GITHUB_TOKEN` needs `issues: write` or `pull-requests: write` to create, edit or set milestones. | |
| 279 | + | |
| 280 | +Setting or changing a milestone doesn't create an event in the timeline, doesn't send a webhook or a notification, and there are no `milestone` webhook events. You can't filter issue lists or searches by milestone yet: `milestone` on `GET .../issues` is ignored, and `milestone:` in a search is searched as text. | |
| 281 | + | |
| 212 | 282 | ## Comments |
| 213 | 283 | |
| 214 | 284 | ### Add a comment |
| @@ -330,7 +400,7 @@ | ||
| 330 | 400 | | `review_requested`, `review_request_removed` | `requested_reviewer`, `review_requester` | |
| 331 | 401 | | `review_dismissed` | `dismissed_review` (`state`, `review_id`, `dismissal_message`) | |
| 332 | 402 | | `merged` | `commit_id` | |
| 333 | -| `ready_for_review`, `converted_to_draft` | none; pull requests only (see [Pull requests](/docs/collaboration/pull-requests)) | | |
| 403 | +| `ready_for_review`, `convert_to_draft` | none; pull requests only (see [Pull requests](/docs/collaboration/pull-requests#drafts)). The matching webhook actions are `ready_for_review` and `converted_to_draft`. | | |
| 334 | 404 | |
| 335 | 405 | Every event also has `id`, `actor`, `event`, `created_at`, `commit_id` and `commit_url`. |
| 336 | 406 | |
| @@ -499,7 +569,8 @@ | ||
| 499 | 569 | |
| 500 | 570 | - An issue's `id` is its number; issues are addressed by number only. |
| 501 | 571 | - `/search/issues` covers at most 50 repositories per search and ignores qualifiers it doesn't know (they are searched as text). `review-requested:`, `mentions:`, `commenter:`, `milestone:`, `linked:` and `reactions:` are not supported. |
| 502 | -- There are no milestones, issue types, issue templates, transfers or pinned issues. | |
| 572 | +- There are no issue types, issue templates, transfers or pinned issues. | |
| 573 | +- Milestones are created and edited through the REST API only, can't be deleted, and have no events, webhooks or list filters. See [Milestones](#milestones). | |
| 503 | 574 | - Deleting a label in the web UI keeps its name on issues; deleting it through the API removes it from them. |
| 504 | 575 | - Locking, reactions, events and comment deletion are available through the REST API only, not in the web UI. |
| 505 | 576 | - `GET /repos/{owner}/{repo}/assignees` lists direct collaborators only, not organization members. |
| @@ -512,11 +583,21 @@ | ||
| 512 | 583 | | `title is required` | The title was empty. Give the issue a title. | |
| 513 | 584 | | `body is required` | You posted an empty comment. | |
| 514 | 585 | | `permission denied` | You tried to edit, close or reopen someone else's issue without triage access, or edit someone else's comment without maintain access. | |
| 515 | -| `triage access required` | Creating, changing or deleting labels, setting labels and assignees in the web UI, and the issue label, assignee and lock endpoints need triage or higher. | | |
| 586 | +| `triage access required` | Creating, changing or deleting labels, setting labels and assignees in the web UI, the issue label, assignee and lock endpoints, and creating, editing and setting milestones need triage or higher. | | |
| 516 | 587 | | `#12 not found` | There is no issue or pull request with that number. | |
| 517 | 588 | | `conversation is locked` | The issue is locked. Only triage or higher can comment or react. | |
| 518 | 589 | | `a label with this name already exists` | Use `PATCH /labels/{name}` to change an existing label. | |
| 590 | +| `milestone not found` | There is no milestone with that number. List them with `GET .../milestones?state=all`. | | |
| 591 | +| `title is required`, `title must be 1 to 255 characters` | A new milestone needs a title, and a milestone title can't be blank or longer than 255 characters. | | |
| 592 | +| `due_on must be an ISO 8601 timestamp` | Send a date such as `2026-12-01T00:00:00Z`, or `null`. | | |
| 593 | +| `state must be open or closed`, `state must be open, closed or all` | Use one of the listed values for a milestone's `state`, or for the `state` filter when listing milestones. | | |
| 594 | +| `description must be a string` | A milestone description must be text of at most 65,536 characters. | | |
| 595 | +| `milestone must be a milestone number or null` | `PATCH .../issues/{number}` takes the milestone's number, not its title. | | |
| 596 | +| The milestone didn't change in the `PATCH` response | The response shows the issue before the milestone changed. Read the issue again. | | |
| 519 | 597 | | `incomplete_results: true` in a search | You can read more than 50 repositories. Add `repo:`, `user:` or `org:` to the query. | |
| 520 | 598 | | `repository not found` | The repository doesn't exist, or your account or token can't see it (private repositories return 404). | |
| 521 | 599 | | `authentication required` | You called the API without a token, either on a private repository or to change something (open an issue, comment, react) on a public one. Send `Authorization: Bearer dfy_...`. | |
| 522 | 600 | | `this token needs the write scope` | Your token has only the `read` scope, which can't open issues, comment or react. Create a token with the `write` scope. | |
| 601 | +| The new issue has no labels or assignees | You don't have triage access, so the `labels` and `assignees` you sent were dropped. Ask someone with triage access to add them. | | |
| 602 | +| `repository is archived (read-only)` | The repository is archived. An admin must unarchive it before issues can change. | | |
| 603 | +| `Resource not accessible by integration (...)` | A workflow's `GITHUB_TOKEN` lacks the permission named in the message. Add `issues: write` to the job's `permissions:`. | |
modifiedapps/diffinery/docs/collaboration/merge-queue.md+17−6View file
| @@ -119,7 +119,7 @@ | ||
| 119 | 119 | |
| 120 | 120 | ## Add a pull request to the queue |
| 121 | 121 | |
| 122 | -The pull request must have no merge blockers: approvals, required checks on its head, no conflicts, not a draft (mark a draft ready first with **Ready for review** or `diffinery pr ready 12`; see [Drafts](/docs/collaboration/pull-requests#drafts)). You need write access, and through the API a token with the `write` scope. Agents (and workflow tokens) can queue a pull request into the default branch or a protected one only when its rule has **Allow agents to land pull requests through auto-merge** and at least one required check, or, for agents, **Allow agents to merge pull requests**. An agent can also turn on auto-merge, which adds the pull request to the queue once it is ready (see [Agents landing changes](/docs/repositories/branch-protection#agents-landing-changes)). | |
| 122 | +The pull request must have no merge blockers: approvals, required checks on its head, no conflicts, not a draft (mark a draft ready first with **Ready for review** or `diffinery pr ready 12`; see [Drafts](/docs/collaboration/pull-requests#drafts)). You need write access, and through the API a token with the `write` scope. Agents (and workflow tokens) can queue a pull request into the default branch or a protected one only when its rule has **Allow agents to land pull requests through auto-merge** and at least one required check, or, for agents, **Allow agents to merge pull requests**. To queue a pull request that isn't ready yet, turn on [auto-merge](/docs/collaboration/pull-requests#auto-merge): it adds the pull request to the queue by itself once it has no blockers, with the auto-merge method (by default the queue's). Agents use it the same way (see [Agents landing changes](/docs/repositories/branch-protection#agents-landing-changes)). | |
| 123 | 123 | |
| 124 | 124 | ### Queue in the web UI |
| 125 | 125 | |
| @@ -276,14 +276,24 @@ | ||
| 276 | 276 | |
| 277 | 277 | ## Bypass the queue |
| 278 | 278 | |
| 279 | -A repository admin who is a person can merge a pull request directly, skipping the queue, when it has no conflicts, isn't a draft, and the rule doesn't have **Do not allow administrators to bypass these rules**. Through a token, that needs the `admin` scope. The bypass must be asked for explicitly: | |
| 279 | +A repository admin who is a person can merge a pull request directly, skipping the queue, when it has no conflicts, isn't a draft, and the rule doesn't have **Do not allow administrators to bypass these rules**. Through a token, that needs the `admin` scope. The bypass must be asked for explicitly, and it skips only the queue: | |
| 280 | 280 | |
| 281 | -- **Web UI:** **Merge now (bypass the queue)**, which asks `Merge directly, bypassing the merge queue?` first. It merges with the queue's merge method. When the pull request doesn't meet the rules, the button reads **Merge now (bypass the queue and the unmet requirements)** instead and also sends `bypass_rules`; the bypass is audited as `pull_request.merge_bypassed`. | |
| 282 | -- **CLI:** `diffinery pr merge 12 --admin`. It sends `bypass_merge_queue` and `bypass_rules`, so the CLI doesn't fall back to queueing the pull request. | |
| 283 | -- **REST API:** `PUT /repos/{owner}/{repo}/pulls/{number}/merge` with `"bypass_merge_queue": true`. `"bypass_rules": true` alone doesn't skip the queue. | |
| 281 | +| Request | Effect | | |
| 282 | +| --- | --- | | |
| 283 | +| `"bypass_merge_queue": true` | Merges directly instead of queueing. The pull request must still meet its branch rules (approvals, required checks on its head, linear history); otherwise the merge fails with `405` and `pull request is not mergeable: ...`. | | |
| 284 | +| `"bypass_rules": true` | Skips unmet branch rules, but not the queue: alone, it still gets `main requires a merge queue: add the pull request to the queue`. | | |
| 285 | +| Both | Merges directly even with unmet branch rules. | | |
| 284 | 286 | |
| 285 | -Skipping the queue also skips the pull request's unmet branch rules (missing approvals, required checks, linear history), as with an [admin bypass](/docs/collaboration/pull-requests#admin-bypass). When the pull request had unmet blockers, the merge is recorded in the [audit log](/docs/security/audit-log#git-and-pull-requests) as `pull_request.merge_bypassed`. | |
| 287 | +How to ask for it: | |
| 286 | 288 | |
| 289 | +- **Web UI:** **Merge now (bypass the queue)** under **Merge when ready**, which asks `Merge directly, bypassing the merge queue?` first. It merges with the queue's merge method. When the pull request doesn't meet the rules, the button reads **Merge now (bypass the queue and the unmet requirements)** instead and also sends `bypass_rules`. The button isn't shown when the rule has **Do not allow administrators to bypass these rules**. | |
| 290 | +- **CLI:** `diffinery pr merge 12 --admin`. It sends `bypass_merge_queue` and `bypass_rules`, so it skips both the queue and unmet rules, and the CLI doesn't fall back to queueing the pull request. | |
| 291 | +- **REST API:** `PUT /repos/{owner}/{repo}/pulls/{number}/merge` with `"bypass_merge_queue": true`, plus `"bypass_rules": true` when the pull request has unmet rules. | |
| 292 | + | |
| 293 | +A direct merge with the `merge` method on a branch that requires linear history also needs `bypass_rules`; otherwise it fails with `main requires linear history; use squash or rebase`. | |
| 294 | + | |
| 295 | +Every merge that skipped the queue is recorded in the [audit log](/docs/security/audit-log#git-and-pull-requests) as `pull_request.merge_bypassed`, with `merge queue` in the detail's `blockers`, together with any unmet rules skipped with `bypass_rules` (and `linear history required` for a merge commit on a linear-history branch). See [Admin bypass](/docs/collaboration/pull-requests#admin-bypass). | |
| 296 | + | |
| 287 | 297 | Everyone else gets `main requires a merge queue: add the pull request to the queue` (`405`) from a direct merge; an admin who could bypass sees `(or, as an admin, bypass the queue with bypass_merge_queue)` added to it. A bypass moves the base branch, so the queued groups are rebuilt. |
| 288 | 298 | |
| 289 | 299 | ## Differences from GitHub |
| @@ -302,6 +312,7 @@ | ||
| 302 | 312 | | `main does not use a merge queue; merge the pull request directly` | The base branch's rule doesn't have **Require a merge queue**. Merge normally. | |
| 303 | 313 | | `pull request cannot be added to the merge queue: ...` | The pull request has merge blockers, listed after the colon. Fix them first. | |
| 304 | 314 | | `main requires linear history; use squash or rebase` | Pick `squash` or `rebase`, or change the queue's method. | |
| 315 | +| **Merge now (bypass the queue)** fails with `pull request is not mergeable: ... (as an admin you can bypass with bypass_rules)` | Skipping the queue doesn't skip unmet rules. Meet them, or merge with `diffinery pr merge 12 --admin` (or the API with both `bypass_merge_queue` and `bypass_rules`). | | |
| 305 | 316 | | `the pull request left the merge queue: ...` (web UI) | It joined and was removed in the same step, for example because it conflicts with the groups ahead. | |
| 306 | 317 | | `merge queue branches are read-only` | You pushed to a `gh-readonly-queue/` branch. Push to the pull request's branch instead. | |
| 307 | 318 | | The group waits until the timeout | The required check names don't match what the workflow reports, or the workflow has no `merge_group` trigger. Compare **Required status checks** with the check names on the merge group commit. | |
modifiedapps/diffinery/docs/collaboration/pull-requests.md+260−24View file
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | --- |
| 2 | 2 | title: Pull requests |
| 3 | -description: Open, review, check, update and merge pull requests in the web UI, the CLI, the REST API and over MCP, including intent, drafts and merge blockers. | |
| 3 | +description: Open, review, check, update and merge pull requests in the web UI, the CLI, the REST API and over MCP, including intent, drafts, blockers and auto-merge. | |
| 4 | 4 | --- |
| 5 | 5 | |
| 6 | 6 | A pull request proposes merging the commits on one branch (the head) into another branch of the same repository (the base). Use it to get changes reviewed, run CI on them, and merge them under the base branch's protection rules, whether the author is a person or an AI agent. |
| @@ -10,8 +10,9 @@ | ||
| 10 | 10 | - **Same repository.** The head and base are branches of the same repository. In the API, `head` may be written `owner:branch`; the part before the colon is ignored. |
| 11 | 11 | - **Shared numbering.** Pull requests and issues share one number sequence per repository, like on GitHub. `/acme/website/pull/12` and `/acme/website/issues/12` redirect to each other depending on what `#12` is, and `/acme/website/pulls/12` redirects to `/acme/website/pull/12`. |
| 12 | 12 | - **One open pull request per branch pair.** You can't open a second pull request for the same head and base while one is open. |
| 13 | -- **The head moves with the branch.** Pushing to the head branch updates the pull request (a `synchronize` event), re-runs its `pull_request` workflows, and, if the base branch's rule has **Dismiss stale approvals when new commits are pushed**, dismisses earlier approvals. | |
| 13 | +- **The head moves with the branch.** Pushing to the head branch updates the pull request (a `synchronize` event), re-runs its `pull_request` workflows, and, if the base branch's rule has **Dismiss stale approvals when new commits are pushed**, dismisses earlier approvals. A push by anyone other than the person or agent who turned on [auto-merge](#auto-merge) turns it off. | |
| 14 | 14 | - **Intent.** Each pull request can carry an `intent`: a short statement of what the change is meant to do, shown above the conversation. See [Intent](#intent). |
| 15 | +- **Auto-merge.** Instead of waiting to click merge, you can turn on auto-merge: Diffinery merges the pull request by itself once every rule of its base branch is met. See [Auto-merge](#auto-merge). | |
| 15 | 16 | - **Agents.** Pull requests opened by agents always need at least one human approval. See [Pull requests opened by agents](#pull-requests-opened-by-agents). |
| 16 | 17 | |
| 17 | 18 | ## Permissions |
| @@ -31,8 +32,9 @@ | ||
| 31 | 32 | | Dismiss a review | Maintain or higher, not the pull request's author, and never an agent | |
| 32 | 33 | | Update the branch with its base (`update-branch`) | Write or higher, subject to the head branch's protection | |
| 33 | 34 | | Merge, or add to and remove from the merge queue | Write or higher, subject to [merge blockers](#merge-blockers) | |
| 35 | +| Turn [auto-merge](#auto-merge) on or off | Write or higher; agents and workflow tokens only where the base branch allows it | | |
| 34 | 36 | |
| 35 | -A token's scope can lower your role: a `read` token acts with at most read, a `write` token with at most maintain, and only an `admin` token keeps admin. Every change (opening, commenting, reviewing, editing, marking ready, merging) needs a signed-in caller: without credentials the API answers `401` with `authentication required`, even on a public repository, and a `read`-scoped token gets `403` with `this token needs the write scope`. See [Permissions](/docs/accounts/permissions) and [Tokens](/docs/accounts/tokens). | |
| 37 | +A token's scope can lower your role: a `read` token acts with at most read, a `write` token with at most maintain, and only an `admin` token keeps admin. Every change (opening, commenting, reviewing, editing, marking ready, merging) needs a signed-in caller: without credentials the API answers `401` with `authentication required`, even on a public repository, and a `read`-scoped token gets `403` with `this token needs the write scope`. On an archived repository every change is refused with `403` and `repository is archived (read-only)`. A workflow's `GITHUB_TOKEN` has extra limits: see [Pull requests and workflow tokens](#pull-requests-and-workflow-tokens). See [Permissions](/docs/accounts/permissions) and [Tokens](/docs/accounts/tokens). | |
| 36 | 38 | |
| 37 | 39 | ## Open a pull request |
| 38 | 40 | |
| @@ -126,6 +128,7 @@ | ||
| 126 | 128 | | `a pull request already exists for x: #12` | Use the open pull request, or close it first. The response also includes `number`. | |
| 127 | 129 | | `title is required` | Give it a title. | |
| 128 | 130 | | `write access required` | Opening a pull request needs write access. | |
| 131 | +| `GitHub Actions is not permitted to create pull requests. ...` (403) | A workflow's `GITHUB_TOKEN` tried to open a pull request in a repository that doesn't allow it. See [Pull requests and workflow tokens](#pull-requests-and-workflow-tokens). | | |
| 129 | 132 | |
| 130 | 133 | ## Intent |
| 131 | 134 | |
| @@ -166,8 +169,9 @@ | ||
| 166 | 169 | |
| 167 | 170 | ### What changing the draft state does |
| 168 | 171 | |
| 169 | -- The pull request's events get `ready_for_review` or `converted_to_draft`, and webhooks get a `pull_request` event with the same action. See [Webhook events](/docs/reference/webhook-events#pull_request). | |
| 172 | +- The pull request's timeline gets a `ready_for_review` or `convert_to_draft` event (GitHub's timeline names), and webhooks get a `pull_request` event with the action `ready_for_review` or `converted_to_draft` (GitHub's webhook names). See [Webhook events](/docs/reference/webhook-events#pull_request). | |
| 170 | 173 | - `pull_request` and `pull_request_target` workflows whose `types` include `ready_for_review` or `converted_to_draft` run. The default types don't include them. See [Events](/docs/actions/events#pull_request). |
| 174 | +- Converting a pull request to a draft turns off [auto-merge](#auto-merge), with the comment `Auto-merge was turned off: the pull request was converted to a draft.` Marking it ready again doesn't turn auto-merge back on. | |
| 171 | 175 | - Asking for the state the pull request already has changes nothing and sends no event. |
| 172 | 176 | - `PATCH` can change `draft` together with `title`, `body` and `state` in one request. |
| 173 | 177 | |
| @@ -184,22 +188,24 @@ | ||
| 184 | 188 | |
| 185 | 189 | | Tab | Shows | |
| 186 | 190 | | --- | --- | |
| 187 | -| **Conversation** | The description, the intent, comments, reviews with their line comments, the merge box, **Review changes**, the comment form, and a sidebar with **Reviewers**, **Labels**, **Assignees**, **Notifications** and **Participants** | | |
| 191 | +| **Conversation** | The description, the intent, comments, reviews with their line comments, the merge box, **Review changes**, the comment form, and a sidebar with **Reviewers**, **Labels**, **Milestone**, **Assignees**, **Notifications** and **Participants** | | |
| 188 | 192 | | **Commits** | The commits on the head branch that aren't on the base (up to 250) | |
| 189 | 193 | | **Checks** | Check runs and commit statuses reported on the head commit, and the workflow runs for this pull request | |
| 190 | 194 | | **Files changed** | The diff from the merge base to the head (up to 300 files), where you can comment on lines | |
| 191 | 195 | |
| 192 | 196 | The header says `wants to merge into main from fix-pricing-link` (or `merged into` once merged). Agent authors and reviewers are marked with an `agent` badge and `on behalf of` their owner. |
| 193 | 197 | |
| 194 | -Labels, assignees, comments, mentions, editing the title and description, and subscribing work exactly as on issues. See [Issues](/docs/collaboration/issues). | |
| 198 | +Labels, assignees, milestones, comments, mentions, editing the title and description, and subscribing work exactly as on issues. See [Issues](/docs/collaboration/issues). | |
| 195 | 199 | |
| 200 | +To put a pull request in a milestone, someone with triage access or higher uses **Set milestone** in the sidebar, or sends `{"milestone": 1}` (or `null`) with `PATCH /repos/{owner}/{repo}/issues/{number}` using the pull request's number. Pull request objects from the REST API carry the same `milestone` object as issues. Milestones themselves are created and edited with the REST API; see [Milestones](/docs/collaboration/issues#milestones). | |
| 201 | + | |
| 196 | 202 | ## Reviews |
| 197 | 203 | |
| 198 | 204 | A review is one of three kinds: |
| 199 | 205 | |
| 200 | 206 | | Kind | Web UI label | API and MCP `event` | Effect | |
| 201 | 207 | | --- | --- | --- | --- | |
| 202 | -| Approve | **Approve** | `APPROVE` | Counts toward required approvals (people only, not agents) | | |
| 208 | +| Approve | **Approve** | `APPROVE` | Counts toward required approvals (people only, not agents or workflow tokens) | | |
| 203 | 209 | | Request changes | **Request changes** | `REQUEST_CHANGES` | Blocks merging until that reviewer approves | |
| 204 | 210 | | Comment | **Comment** | `COMMENT` | Feedback only; needs a body or line comments | |
| 205 | 211 | |
| @@ -209,7 +215,8 @@ | ||
| 209 | 215 | - Approving and requesting changes need write access. Anyone signed in who can read the repository can leave a Comment review. |
| 210 | 216 | - Only open pull requests can be reviewed. |
| 211 | 217 | - For each reviewer, their latest Approve or Request changes review is the one that counts. A later Comment review doesn't change it. To withdraw a change request, submit an Approve review, or ask a maintainer to [dismiss it](#dismiss-a-review). |
| 212 | -- Approvals from agents are shown but never count toward required approvals. An agent's Request changes review does block merging. | |
| 218 | +- Approvals from agents and from a workflow's `GITHUB_TOKEN` are shown but never count toward required approvals. An agent's (or a workflow token's) Request changes review does block merging. | |
| 219 | +- A workflow's `GITHUB_TOKEN` can approve or request changes only when the repository allows it; see [Pull requests and workflow tokens](#pull-requests-and-workflow-tokens). | |
| 213 | 220 | - If the base branch's rule has **Dismiss stale approvals when new commits are pushed**, pushing to the head marks earlier approvals as dismissed (shown as `reviewed (dismissed)`), and only approvals of the current head commit count. |
| 214 | 221 | - Submitting any review removes you from the pull request's requested reviewers. |
| 215 | 222 | |
| @@ -440,20 +447,27 @@ | ||
| 440 | 447 | |
| 441 | 448 | Commits Diffinery writes use the email `<login>@users.noreply.diffinery.com`. Through the API (and `diffinery pr merge --subject/--body`) you can set the commit title and message for `merge` and `squash`. |
| 442 | 449 | |
| 443 | -When no method is given, the API uses `squash` if the base branch requires linear history, and `merge` otherwise. The web UI, the CLI and MCP default to `merge`. | |
| 450 | +Default method when you don't pick one: | |
| 444 | 451 | |
| 445 | -On a branch whose rule has **Require linear history**, a merge with the `merge` method is refused with `405` and `main requires linear history; use squash or rebase` (the merge queue refuses it with `422` and the same message), so choose `squash` or `rebase` there. Only an explicit [admin bypass](#admin-bypass) can create a merge commit on such a branch. | |
| 452 | +| Where | Base branch without linear history | Base branch with **Require linear history** | | |
| 453 | +| --- | --- | --- | | |
| 454 | +| REST API (`merge_method` left out) | `merge` | `squash` | | |
| 455 | +| Web UI merge form | **Create a merge commit** | **Squash and merge**; **Create a merge commit** isn't offered | | |
| 456 | +| CLI (`diffinery pr merge` without `--merge`, `--squash` or `--rebase`) | `merge` | `merge`, which is refused: pass `--squash` or `--rebase` | | |
| 457 | +| MCP `merge_pull_request` (no `method`) | `merge` | `merge`, which is refused: pass `method` | | |
| 446 | 458 | |
| 459 | +On a branch whose rule has **Require linear history**, a merge with the `merge` method is refused with `405` and `main requires linear history; use squash or rebase` (the merge queue refuses it with `422` and the same message), so choose `squash` or `rebase` there. Only an explicit [admin bypass](#admin-bypass) with `bypass_rules` can create a merge commit on such a branch, and that is recorded in the audit log. | |
| 460 | + | |
| 447 | 461 | In a [merge queue](/docs/collaboration/merge-queue), `rebase` replays each commit onto the queue instead of fast-forwarding. |
| 448 | 462 | |
| 449 | 463 | ## Merge a pull request |
| 450 | 464 | |
| 451 | -Merging needs write access and a pull request with no [merge blockers](#merge-blockers). If the base branch uses a merge queue, merging adds the pull request to the queue instead; see [Merge when ready and the merge queue](#merge-when-ready-and-the-merge-queue). | |
| 465 | +Merging needs write access and a pull request with no [merge blockers](#merge-blockers). If the base branch uses a merge queue, merging adds the pull request to the queue instead; see [Merge when ready and the merge queue](#merge-when-ready-and-the-merge-queue). If the pull request isn't ready yet, turn on [auto-merge](#auto-merge) and Diffinery merges it once it is. | |
| 452 | 466 | |
| 453 | 467 | ### Merge in the web UI |
| 454 | 468 | |
| 455 | 469 | 1. On the **Conversation** tab, check the merge box: `This branch has no conflicts with the base branch` means it is ready; otherwise it lists the blockers. |
| 456 | -2. Pick the **Merge method**. | |
| 470 | +2. Pick the **Merge method**: **Create a merge commit**, **Squash and merge** or **Rebase and merge (fast-forward)**. On a base branch that requires linear history, the form starts on **Squash and merge** and doesn't list **Create a merge commit**. | |
| 457 | 471 | 3. Leave **Delete fix-pricing-link after merge** ticked to delete the head branch, or untick it. |
| 458 | 472 | 4. Click **Merge pull request**. |
| 459 | 473 | |
| @@ -477,7 +491,7 @@ | ||
| 477 | 491 | | `-b, --body TEXT`, `--body-file FILE` | Commit message body | |
| 478 | 492 | | `--match-head-commit SHA` | Only merge if the head is still at this commit | |
| 479 | 493 | | `--admin` | Merge now, bypassing unmet branch rules and any merge queue (repository admins; audited). Sends `bypass_rules` and `bypass_merge_queue`. See [Admin bypass](#admin-bypass). | |
| 480 | -| `--disable-auto` | Remove the pull request from the merge queue | | |
| 494 | +| `--disable-auto` | Remove the pull request from the merge queue. It doesn't turn off [auto-merge](#auto-merge). | | |
| 481 | 495 | |
| 482 | 496 | ### Merge with the REST API |
| 483 | 497 | |
| @@ -493,8 +507,8 @@ | ||
| 493 | 507 | | `commit_title`, `commit_message` | Override the commit title and body (`merge` and `squash`) | |
| 494 | 508 | | `sha` | Only merge if the head is still at this commit; otherwise `409 head branch was modified; review and try again` | |
| 495 | 509 | | `delete_branch` | Delete the head branch after merging (default `false`) | |
| 496 | -| `bypass_rules` | Admins only: merge even though the pull request has unmet branch rules. See [Admin bypass](#admin-bypass). | | |
| 497 | -| `bypass_merge_queue` | Admins only: merge directly on a branch that uses a merge queue. It also bypasses unmet branch rules, like `bypass_rules`. | | |
| 510 | +| `bypass_rules` | Admins only: merge even though the pull request has unmet branch rules, or create a merge commit on a branch that requires linear history. It doesn't skip a merge queue. See [Admin bypass](#admin-bypass). | | |
| 511 | +| `bypass_merge_queue` | Admins only: merge directly on a branch that uses a merge queue. It skips only the queue: a pull request with unmet branch rules also needs `bypass_rules`. | | |
| 498 | 512 | |
| 499 | 513 | The response is `{"merged": true, "sha": "...", "method": "squash", "message": "Pull Request successfully merged"}`. |
| 500 | 514 | |
| @@ -514,7 +528,7 @@ | ||
| 514 | 528 | |
| 515 | 529 | ### Merge over MCP |
| 516 | 530 | |
| 517 | -`merge_pull_request` takes `owner`, `repo`, `number`, and optionally `method`, `expected_head_sha` and `delete_branch`. On a branch with a merge queue it queues the pull request instead. | |
| 531 | +`merge_pull_request` takes `owner`, `repo`, `number`, and optionally `method`, `expected_head_sha` and `delete_branch`. On a branch with a merge queue it queues the pull request instead. Agents can't merge into the default branch or a protected branch themselves; they use `enable_auto_merge` there (see [Auto-merge](#auto-merge)). | |
| 518 | 532 | |
| 519 | 533 | ### After the merge |
| 520 | 534 | |
| @@ -533,11 +547,18 @@ | ||
| 533 | 547 | - the base branch's rule doesn't have **Do not allow administrators to bypass these rules**; |
| 534 | 548 | - the pull request has no merge conflicts and isn't a draft. |
| 535 | 549 | |
| 536 | -What it bypasses: missing approvals, change requests, required checks that failed or haven't run, a branch that is behind its base under linear history, and the refusal of merge commits under linear history. With `bypass_merge_queue` it also skips a merge queue. | |
| 550 | +Each bypass flag covers only what it names: | |
| 537 | 551 | |
| 552 | +| Flag | Skips | Doesn't skip | | |
| 553 | +| --- | --- | --- | | |
| 554 | +| `bypass_rules` | Missing approvals, change requests, required checks that failed or haven't run, a branch that is behind its base under linear history, and the refusal of merge commits under linear history | A merge queue | | |
| 555 | +| `bypass_merge_queue` | The merge queue: the pull request merges directly | Any unmet branch rule (approvals, checks, linear history) | | |
| 556 | + | |
| 557 | +To merge past both a merge queue and unmet rules, send both flags. Conflicts, a draft state and the agent rules are never bypassed. | |
| 558 | + | |
| 538 | 559 | How to ask for it: |
| 539 | 560 | |
| 540 | -- **Web UI:** when you are an admin and the pull request has blockers but no conflicts and isn't a draft, the button reads **Merge without waiting for requirements (bypass)** and asks `Merge without meeting the branch requirements?` before merging. | |
| 561 | +- **Web UI:** when you are an admin and the pull request has blockers but no conflicts and isn't a draft, the button reads **Merge without waiting for requirements (bypass)** and asks `Merge without meeting the branch requirements?` before merging. It sends `bypass_rules`. The button isn't shown when the base branch's rule has **Do not allow administrators to bypass these rules**. On a branch with a merge queue, the merge box instead offers **Merge now (bypass the queue)**, which sends only `bypass_merge_queue`; see [Merge queue](/docs/collaboration/merge-queue#bypass-the-queue). | |
| 541 | 562 | - **CLI:** `diffinery pr merge 12 --squash --admin`. The CLI sends both `bypass_rules` and `bypass_merge_queue`. |
| 542 | 563 | - **REST API:** add `"bypass_rules": true` to `PUT /repos/{owner}/{repo}/pulls/{number}/merge`: |
| 543 | 564 | |
| @@ -549,7 +570,7 @@ | ||
| 549 | 570 | |
| 550 | 571 | When you could bypass but didn't ask, the `405` message ends with `(as an admin you can bypass with bypass_rules)`, for example `pull request is not mergeable: needs 1 more approving review(s) (as an admin you can bypass with bypass_rules)`. MCP's `merge_pull_request` has no bypass option. |
| 551 | 572 | |
| 552 | -A merge that bypassed blockers is recorded in the [audit log](/docs/security/audit-log#git-and-pull-requests) as `pull_request.merge_bypassed`, with the pull request as the target and the unmet `blockers` and the `base` branch in the detail, in addition to the usual `pull.merge` entry. | |
| 573 | +A merge that bypassed anything is recorded in the [audit log](/docs/security/audit-log#git-and-pull-requests) as `pull_request.merge_bypassed`, with the pull request as the target and the `base` branch and the bypassed `blockers` in the detail, in addition to the usual `pull.merge` entry. `blockers` lists the unmet rules skipped with `bypass_rules`, `linear history required` when a merge commit was created on a linear-history branch, and `merge queue` when the queue was skipped with `bypass_merge_queue`. | |
| 553 | 574 | |
| 554 | 575 | ## Merge blockers |
| 555 | 576 | |
| @@ -569,7 +590,8 @@ | ||
| 569 | 590 | | `required check 'CI / test' failure` (or another conclusion such as `cancelled`, `timed_out` or `action_required`) | The check didn't pass. Fix the code or re-run the workflow. | |
| 570 | 591 | | `branch is behind its base; update it (linear history required)` | The base requires linear history and the head doesn't contain the latest base. Rebase onto the base and push. | |
| 571 | 592 | | `write access is required to merge` | You need write access. | |
| 572 | -| `agents may not merge into this branch; a human must merge` | An agent tried to merge into the default branch or a branch with a protection rule that lacks **Allow agents to merge pull requests**. | | |
| 593 | +| `agents may not merge into this branch; a human must merge` | An agent tried to merge into the default branch or a branch with a protection rule that lacks **Allow agents to merge pull requests**. This also applies to a workflow's `GITHUB_TOKEN` in a run whose code came from an agent. Where the rule allows it, the agent can [turn on auto-merge](#auto-merge-for-agents-and-workflow-tokens) instead. | | |
| 594 | +| ``refusing to allow a GitHub App to create or update workflow `.github/workflows/ci.yml` without `workflows` permission`` | A workflow's `GITHUB_TOKEN` tried to merge a pull request that adds, changes or deletes a workflow file. A person must merge it. | | |
| 573 | 595 | |
| 574 | 596 | Required approvals come from the branch protection rule's **Required approvals** (0 to 10). Pull requests opened by agents need at least one even without a rule. |
| 575 | 597 | |
| @@ -577,6 +599,189 @@ | ||
| 577 | 599 | |
| 578 | 600 | Merge blockers apply to admins too, unless they ask for an [admin bypass](#admin-bypass). |
| 579 | 601 | |
| 602 | +When Diffinery lands a pull request for an agent or a workflow token, through [auto-merge](#auto-merge) or the merge queue, the agent blocker is replaced by the rules in [Auto-merge for agents and workflow tokens](#auto-merge-for-agents-and-workflow-tokens), and these blockers can appear: `needs N more approving review(s) of its latest commit`, ``the pull request changes workflow file `<path>`; a person must merge it``, and the refusals `agents may not have pull requests merged into main automatically ...` and `main has no required status checks: ...`. | |
| 603 | + | |
| 604 | +## Auto-merge | |
| 605 | + | |
| 606 | +Auto-merge lets you approve the outcome now and have Diffinery do the merge later: you turn it on for a pull request that isn't ready yet (required checks still running, an approval still missing, the branch behind its base), and Diffinery merges it by itself as soon as every rule of its base branch is met. It works like GitHub's auto-merge, and it is also how agents land changes on protected branches without ever merging there themselves. | |
| 607 | + | |
| 608 | +### How auto-merge works | |
| 609 | + | |
| 610 | +- **It checks right away.** When you turn it on, Diffinery looks at the pull request at once. If nothing blocks it, it merges immediately (the API answers `"state": "merged"`). Otherwise auto-merge stays on and waits. | |
| 611 | +- **It looks again when something changes.** Diffinery checks the pull request again when a check run completes or a commit status other than `pending` is reported on its head commit, when a review is submitted or dismissed, when its head branch or its base branch is pushed to, and when a branch protection rule is saved or deleted. | |
| 612 | +- **It merges only when nothing blocks.** The pull request must have no [merge blockers](#merge-blockers): every required check passed on the head commit, enough approvals from people, no changes requested, no conflicts, and an up-to-date branch where the base requires linear history. Auto-merge never bypasses anything, even when an admin turned it on. Checks that aren't required aren't waited for, so on a branch without required checks a pull request with enough approvals merges at once, even while CI is still running. | |
| 613 | +- **Into a merge queue, it queues.** When the base branch has **Require a merge queue**, auto-merge adds the pull request to the [merge queue](/docs/collaboration/merge-queue) once it is ready, with the auto-merge method, and then turns itself off: the queue takes over and merges it when its merge group passes. The API answers `"state": "queued"` when that happens right away. | |
| 614 | +- **It merges as the one who turned it on.** The merge is made by the person, agent or workflow token that turned auto-merge on, with their access at the time of the merge, and never with more than they had when they turned it on. They appear as `merged_by`, and the commit is written as if they had merged by hand (see [Merge methods](#merge-methods)). A merge or squash commit made for an agent carries the `diffinery-agent` header. | |
| 615 | +- **It keeps the head branch.** Auto-merge doesn't delete the head branch after merging. Delete it yourself afterwards. | |
| 616 | +- **Turning it on again changes it.** Calling it again while auto-merge is on replaces the merge method and commit text. | |
| 617 | + | |
| 618 | +Closing the pull request, or merging it some other way, ends auto-merge quietly. Reopening a closed pull request doesn't turn it back on. | |
| 619 | + | |
| 620 | +### Who can turn on auto-merge | |
| 621 | + | |
| 622 | +- You need write access to the repository, and through the API a token with the `write` (or `admin`) scope. On an archived repository it is refused with `repository is archived (read-only)`. | |
| 623 | +- The pull request must be open, not a draft, not merged and not already in the merge queue. | |
| 624 | +- **People** can turn it on for any base branch. | |
| 625 | +- **Agents and workflow tokens** (`GITHUB_TOKEN`) can turn it on for a base branch that is neither the default branch nor protected by a rule. On the default branch or a protected branch, the branch's rule must have **Allow agents to land pull requests through auto-merge** (`allowAgentAutoMerge`) and at least one required status check; an agent may also use it where the rule has **Allow agents to merge pull requests** (`agentsMayMerge`). See [Auto-merge for agents and workflow tokens](#auto-merge-for-agents-and-workflow-tokens). | |
| 626 | +- A workflow's `GITHUB_TOKEN` needs `contents: write` in the job's `permissions:`, as for merging. | |
| 627 | + | |
| 628 | +Anyone with write access can turn it off, whoever turned it on. | |
| 629 | + | |
| 630 | +### Auto-merge method | |
| 631 | + | |
| 632 | +Auto-merge uses one of the three [merge methods](#merge-methods). When you don't pick one: | |
| 633 | + | |
| 634 | +| Base branch | Default auto-merge method | | |
| 635 | +| --- | --- | | |
| 636 | +| Uses a merge queue | The queue's method | | |
| 637 | +| Requires linear history (no merge queue) | `squash` | | |
| 638 | +| Anything else | `merge` | | |
| 639 | + | |
| 640 | +On a branch that requires linear history, `merge` is refused with `main requires linear history; use squash or rebase` (`422`). Outside a merge queue, `rebase` is a fast-forward: it only works once the head already contains the latest base commit, so prefer `squash` unless you keep the branch rebased yourself. Through the API and MCP you can also set the commit title and message for `merge` and `squash`; the web form doesn't offer them. | |
| 641 | + | |
| 642 | +### Turn on auto-merge in the web UI | |
| 643 | + | |
| 644 | +1. Open the pull request's **Conversation** tab. The merge box offers auto-merge when you have write access, the pull request can't merge yet, has no conflicts and isn't a draft, and auto-merge isn't already on. | |
| 645 | +2. Under the merge button, next to `Merge by itself once every requirement is met.` (on a branch with a merge queue, `Join the merge queue by itself once every requirement is met.`), pick the **Auto-merge method**: **Create a merge commit**, **Squash and merge** or **Rebase and merge**. **Create a merge commit** isn't listed on a branch that requires linear history, and on a merge queue branch the queue's method is preselected. | |
| 646 | +3. Click **Enable auto-merge**. | |
| 647 | + | |
| 648 | +You see `Auto-merge is on: it merges once every requirement is met.`, or `Pull request merged.` or `Added to the merge queue.` when the pull request turned out to be ready. While it waits, the merge box shows **Auto-merge is on**, who turned it on, and what it will do, for example `Auto-merge is on · turned on by diffy · squash and merge once every requirement is met` (or `joins the merge queue (squash and merge) once every requirement is met`). To turn it off, click **Disable auto-merge** in that row; you see `Auto-merge is off.` | |
| 649 | + | |
| 650 | +The button isn't offered while the pull request can already merge: merge it directly instead. | |
| 651 | + | |
| 652 | +### Turn on auto-merge with the REST API | |
| 653 | + | |
| 654 | +GitHub offers auto-merge only through GraphQL (`enablePullRequestAutoMerge` and `disablePullRequestAutoMerge`); Diffinery has REST endpoints for it: | |
| 655 | + | |
| 656 | +```sh | |
| 657 | +curl -X PUT https://diffinery.com/api/v3/repos/acme/website/pulls/12/auto-merge \ | |
| 658 | + -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \ | |
| 659 | + -d '{"merge_method": "squash", "commit_title": "Fix the Contact sales link (#12)"}' | |
| 660 | +``` | |
| 661 | + | |
| 662 | +| Field | Meaning | | |
| 663 | +| --- | --- | | |
| 664 | +| `merge_method` | `merge`, `squash` or `rebase`. Default: see [Auto-merge method](#auto-merge-method). | | |
| 665 | +| `commit_title`, `commit_message` | Title and body of the merge or squash commit. Default: the same as a merge by hand. | | |
| 666 | + | |
| 667 | +All fields are optional; an empty body works. The response (`200`) says what happened: | |
| 668 | + | |
| 669 | +```json | |
| 670 | +{ | |
| 671 | + "state": "enabled", | |
| 672 | + "auto_merge": { | |
| 673 | + "enabled_by": { "login": "diffy", "type": "User" }, | |
| 674 | + "merge_method": "squash", | |
| 675 | + "commit_title": "Fix the Contact sales link (#12)", | |
| 676 | + "commit_message": null | |
| 677 | + }, | |
| 678 | + "merge_commit_sha": null | |
| 679 | +} | |
| 680 | +``` | |
| 681 | + | |
| 682 | +| `state` | Meaning | | |
| 683 | +| --- | --- | | |
| 684 | +| `enabled` | Auto-merge is on and waiting. `auto_merge` shows its settings (`enabled_by` is a full user object). | | |
| 685 | +| `merged` | The pull request was ready and merged right away. `auto_merge` is `null` and `merge_commit_sha` is the new commit. | | |
| 686 | +| `queued` | The base branch uses a merge queue and the pull request was ready, so it joined the queue. `auto_merge` is `null`. | | |
| 687 | + | |
| 688 | +While auto-merge is on, the pull request object (`GET /repos/{owner}/{repo}/pulls/{number}` and the list) carries the same `auto_merge` object, as on GitHub; otherwise `auto_merge` is `null`. | |
| 689 | + | |
| 690 | +Turn it off with: | |
| 691 | + | |
| 692 | +```sh | |
| 693 | +curl -X DELETE https://diffinery.com/api/v3/repos/acme/website/pulls/12/auto-merge \ | |
| 694 | + -H "Authorization: Bearer dfy_..." | |
| 695 | +``` | |
| 696 | + | |
| 697 | +It returns `204`, also when auto-merge was already off. Both endpoints need write access and a `write` token. | |
| 698 | + | |
| 699 | +### Turn on auto-merge with the CLI | |
| 700 | + | |
| 701 | +The CLI has no auto-merge command or flag yet (`diffinery pr merge --disable-auto` removes a pull request from the merge queue; it doesn't touch auto-merge). Call the REST endpoints through `diffinery api`: | |
| 702 | + | |
| 703 | +```sh | |
| 704 | +diffinery api PUT /repos/acme/website/pulls/12/auto-merge -f merge_method=squash | |
| 705 | +diffinery api DELETE /repos/acme/website/pulls/12/auto-merge | |
| 706 | +``` | |
| 707 | + | |
| 708 | +`diffinery pr view 12` lists the merge blockers auto-merge is waiting on. | |
| 709 | + | |
| 710 | +### Turn on auto-merge over MCP | |
| 711 | + | |
| 712 | +`enable_auto_merge` takes `owner`, `repo`, `number`, and optionally `merge_method`, `commit_title` and `commit_message`. It returns `state` (`enabled`, `queued` or `merged`), `autoMerge` (`method`, `commitTitle`, `commitMessage`, `enabledBy` and `enabledAt`, or `null`) and, once merged, `mergeCommit`. `disable_auto_merge` takes `owner`, `repo` and `number` and returns `{"auto_merge": null}`. `get_pull_request` shows the current setting as `issue.pull.autoMerge`. Both tools need a `write` token. See [MCP server](/docs/agents/mcp). | |
| 713 | + | |
| 714 | +### When auto-merge turns itself off | |
| 715 | + | |
| 716 | +Auto-merge turns itself off when the pull request can never go through as set up, and posts a comment on the pull request saying why, as `diffinery-auto-merge[bot]`: `Auto-merge was turned off: <reason>.` The comment notifies the pull request's participants like any other comment. | |
| 717 | + | |
| 718 | +| Reason in the comment | What happened and what to do | | |
| 719 | +| --- | --- | | |
| 720 | +| `diffy pushed new commits to the branch` | Someone other than the one who turned auto-merge on pushed to the head branch, as on GitHub. Review the new commits and turn it on again. Pushes by the same person or agent keep it on. | | |
| 721 | +| `the pull request was converted to a draft` | Mark it ready for review and turn auto-merge on again. | | |
| 722 | +| `diffy no longer has write access` | The account that turned it on lost write access to the repository. Someone with write access turns it on again. | | |
| 723 | +| `agents may not have pull requests merged into main automatically unless its branch protection allows it ...` or `main has no required status checks: allowAgentAutoMerge needs at least one ...` | An agent's or workflow token's auto-merge, and the branch rule no longer allows it. See [Auto-merge for agents and workflow tokens](#auto-merge-for-agents-and-workflow-tokens). | | |
| 724 | +| `main requires linear history, and auto-merge was set to create a merge commit` | The rule started requiring linear history. Turn auto-merge on again with `squash` or `rebase`. | | |
| 725 | +| `rebase merge requires the branch to be up to date with its base (fast-forward only for now)` | A `rebase` auto-merge found the head behind the base. Rebase the branch and push, or use `squash`. | | |
| 726 | +| Another merge error, or `the merge failed` | The merge was attempted and refused for a reason that won't fix itself. Read the message, fix the cause, and turn auto-merge on again. | | |
| 727 | + | |
| 728 | +When the head or base branch moves in the middle of the merge, auto-merge stays on and tries again on the next change. Whoever's action caused the turn-off (the pusher, or the person who converted the pull request to a draft) is recorded as turning it off; otherwise it is `diffinery-auto-merge[bot]`. | |
| 729 | + | |
| 730 | +### Auto-merge for agents and workflow tokens | |
| 731 | + | |
| 732 | +Agents never merge into the default branch or a protected branch themselves (`agents may not merge into this branch; a human must merge`). Auto-merge is how they land work there anyway, with CI and people still in control: the agent turns on auto-merge, and Diffinery merges once the branch's rules are met. A repository admin decides per branch rule whether this is allowed; see [Agents landing changes](/docs/repositories/branch-protection#agents-landing-changes) for the full rules. In short: | |
| 733 | + | |
| 734 | +- The rule needs **Allow agents to land pull requests through auto-merge** (`allowAgentAutoMerge`) and at least one required status check. The same applies to agents adding a pull request to a merge queue on such a branch. | |
| 735 | +- Approvals are required as for any merge (the rule's **Required approvals**, and at least one from a person for a pull request an agent opened), but an approval counts only for the commit it approved: if the agent pushes again, a person approves again. The blocker then reads `needs 1 more approving review(s) of its latest commit`. An admin can waive the approval on green required checks with **Let agents' auto-merges land without an approving review** (`agentAutoMergeWithoutApproval`); see [Waiving approvals for agents](/docs/repositories/branch-protection#waiving-approvals-for-agents). Requested changes always block. | |
| 736 | +- A pull request that adds, changes or deletes a workflow file can't land this way; a person must merge it. | |
| 737 | +- A workflow's `GITHUB_TOKEN` is held to the same rule, even when the run's code didn't come from an agent. | |
| 738 | +- On branches that are neither the default branch nor protected, agents and workflow tokens can use auto-merge without any rule. | |
| 739 | + | |
| 740 | +A typical agent flow over MCP: | |
| 741 | + | |
| 742 | +1. `commit_files` with `create_branch_from` to put the change on a new branch. | |
| 743 | +2. `create_pull_request` with an `intent`. | |
| 744 | +3. `enable_auto_merge` with `merge_method: "squash"`. If the answer is `isError: true` with `agents may not have pull requests merged into main automatically ...`, the branch doesn't allow it: ask a person to merge, or an admin to set `allowAgentAutoMerge`. | |
| 745 | +4. Follow CI with `list_workflow_runs`, and fix failures with more commits. The agent's own pushes keep auto-merge on. | |
| 746 | +5. `get_pull_request` shows `issue.pull.autoMerge` while it waits, and `issue.pull.merged` once Diffinery merged it. | |
| 747 | + | |
| 748 | +Over the REST API the same flow uses `PUT /repos/{owner}/{repo}/pulls/{number}/auto-merge` with the agent's token. | |
| 749 | + | |
| 750 | +### Auto-merge events and audit log | |
| 751 | + | |
| 752 | +- **Timeline:** turning auto-merge on or off adds an `auto_merge_enabled` or `auto_merge_disabled` event to the pull request (GitHub's names), returned by `GET /repos/{owner}/{repo}/issues/{number}/events` and the timeline. | |
| 753 | +- **Webhooks:** a `pull_request` webhook with the action `auto_merge_enabled` or `auto_merge_disabled` is sent. The sender is who turned it on or off; when auto-merge turned itself off, it is the pusher, the person who converted the pull request to a draft, or `diffinery-auto-merge[bot]`. See [Webhook events](/docs/reference/webhook-events#pull_request). | |
| 754 | +- **Workflows and email:** these actions don't start `pull_request` workflows and send no notification emails. The merge itself does everything a merge by hand does (see [After the merge](#after-the-merge)). | |
| 755 | +- **Audit log:** each step is recorded with the pull request (`acme/website#12`) as the target: | |
| 756 | + | |
| 757 | +| Action | When | Detail | | |
| 758 | +| --- | --- | --- | | |
| 759 | +| `pull_request.auto_merge_enabled` | Auto-merge is turned on (or changed) | `base`, `method`, `viaAgentRule` (`true` when it relies on `allowAgentAutoMerge`) | | |
| 760 | +| `pull_request.auto_merge_disabled` | Auto-merge is turned off, by someone or by itself | `enabledBy`, and `reason` when it turned itself off | | |
| 761 | +| `pull_request.auto_merged` | Auto-merge merged the pull request; a `pull.merge` entry is written too | `base`, `method`, `sha` | | |
| 762 | +| `pull_request.auto_merge_enqueued` | Auto-merge added the pull request to the merge queue | `base`, `state` | | |
| 763 | + | |
| 764 | +See [Audit log](/docs/security/audit-log#git-and-pull-requests). | |
| 765 | + | |
| 766 | +### Auto-merge errors | |
| 767 | + | |
| 768 | +| Message | Cause and fix | | |
| 769 | +| --- | --- | | |
| 770 | +| `pull request is a draft; mark it ready for review first` (422) | Mark it ready, then turn on auto-merge. | | |
| 771 | +| `pull request is closed` (422) or `pull request is already merged` (422) | Only open pull requests can auto-merge. | | |
| 772 | +| `not a pull request` (404) | The number belongs to an issue. | | |
| 773 | +| `pull request is already in the merge queue` (422) | It is queued already; the queue merges it. | | |
| 774 | +| `merge_method must be merge, squash or rebase` (422) | Fix `merge_method`. | | |
| 775 | +| `main requires linear history; use squash or rebase` (422) | Pick `squash` or `rebase`. | | |
| 776 | +| `write access required` (403) | You need write access to the repository. | | |
| 777 | +| `this token needs the write scope` (403) | Use a `write` or `admin` token. | | |
| 778 | +| `agents may not have pull requests merged into main automatically unless its branch protection allows it (allowAgentAutoMerge, set by a repository admin); a person must merge` (403) | The base branch's rule doesn't allow agents (or workflow tokens) to land changes. A person merges, or an admin turns on `allowAgentAutoMerge`. For a workflow token the message says `workflow tokens` instead of `agents`. | | |
| 779 | +| `main has no required status checks: allowAgentAutoMerge needs at least one, so that CI gates what agents land` (422) | Add a required status check to the rule. | | |
| 780 | +| ``the pull request changes workflow file `.github/workflows/ci.yml`; CI can't gate a change to itself, so a person must merge it`` (422) | An agent or workflow token relies on `allowAgentAutoMerge` for a pull request that changes a workflow. A person must merge it. | | |
| 781 | +| `Resource not accessible by integration (...)` (403) | A workflow's `GITHUB_TOKEN` lacks `contents: write`. | | |
| 782 | +| Auto-merge is on but nothing happens | A blocker remains. Check the merge box, `diffinery pr view 12` or `merge_blockers` in the API: usually a required check that hasn't run (check the check name), a missing approval, or a change request. | | |
| 783 | +| Auto-merge merged before CI finished | Only required checks are waited for. Add the checks to **Required status checks** in the branch's rule. | | |
| 784 | + | |
| 580 | 785 | ## Pull requests opened by agents |
| 581 | 786 | |
| 582 | 787 | When the author is an [agent](/docs/agents/overview): |
| @@ -584,13 +789,37 @@ | ||
| 584 | 789 | - the pull request needs at least one approval from a person, even on a branch without protection rules, and approvals from agents never count; |
| 585 | 790 | - the agent's owner can approve it, because the owner is a different account from the agent; |
| 586 | 791 | - agents can't merge into the default branch or any protected branch unless its rule has **Allow agents to merge pull requests**, and even then the human approval is still required; |
| 792 | +- where the rule has **Allow agents to land pull requests through auto-merge** and a required check, the agent can turn on [auto-merge](#auto-merge-for-agents-and-workflow-tokens) and Diffinery merges once the rules are met; an admin can also waive the human approval for those landings with **Let agents' auto-merges land without an approving review**; | |
| 587 | 793 | - the author is shown with an `agent` badge and `on behalf of` its owner, and the API returns `"type": "Bot"` for it. |
| 588 | 794 | |
| 795 | +## Pull requests and workflow tokens | |
| 796 | + | |
| 797 | +A workflow job's `GITHUB_TOKEN` can work with pull requests, within its `permissions:` and these limits, as on GitHub: | |
| 798 | + | |
| 799 | +- **Permissions.** Opening, reviewing and editing pull requests (the `/pulls` endpoints) need `pull-requests: write`; conversation comments, labels and assignees (the `/issues/{number}/...` endpoints) accept `issues: write` or `pull-requests: write`. Merging needs `contents: write`. A request outside the token's permissions gets `403` with `Resource not accessible by integration (the workflow's GITHUB_TOKEN needs permissions: pull-requests: write)`. | |
| 800 | +- **Opening and approving is off by default.** A job token can't open a pull request, approve one or request changes unless a repository admin allows it. Otherwise the request fails with `403` and `GitHub Actions is not permitted to create pull requests.` (or `... to approve pull requests.`), followed by `An admin can allow it in the repository's Actions settings ("Allow GitHub Actions to create and approve pull requests").` Comment reviews are always allowed. | |
| 801 | +- **Its approvals never count.** Even where approving is allowed, an approval from a job token doesn't count toward required approvals: only people's approvals do. Its Request changes review still blocks merging. | |
| 802 | +- **No admin bypass.** A job token acts with at most the write role, so branch protection always applies to its merges. | |
| 803 | +- **No workflow file changes.** A job token can't merge a pull request that adds, changes or deletes files under `.github/workflows/` or `.diffinery/workflows/` (see [Merge blockers](#merge-blockers)). | |
| 804 | +- **Auto-merge.** A job token can turn on [auto-merge](#auto-merge) with `contents: write`. On the default branch or a protected branch it is held to the agent rule (**Allow agents to land pull requests through auto-merge** and a required check), even when the run's code didn't come from an agent. The later merge keeps the token's permissions from when it was turned on. | |
| 805 | +- **Code from agents.** In a run whose code came from an agent (an agent opened the pull request or pushed its branch last, or started the run, and re-runs of such a run and `workflow_run` runs that follow it), the job token acts as an agent: it can't merge into the default branch or a protected branch unless the rule has **Allow agents to merge pull requests**. | |
| 806 | +- **No new runs.** Pull requests a job token opens, pushes or merges don't start `pull_request` workflows, so workflows can't trigger each other in a loop. | |
| 807 | + | |
| 808 | +An admin allows workflows to open and approve pull requests on the repository's **Settings → Actions** tab (check **Allow GitHub Actions to create and approve pull requests** and click **Save**), or with the REST API: | |
| 809 | + | |
| 810 | +```sh | |
| 811 | +curl -X PUT https://diffinery.com/api/v3/repos/acme/website/actions/permissions/workflow \ | |
| 812 | + -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \ | |
| 813 | + -d '{"can_approve_pull_request_reviews": true}' | |
| 814 | +``` | |
| 815 | + | |
| 816 | +`GET` on the same path returns `default_workflow_permissions` (`read` or `write`) and `can_approve_pull_request_reviews` (`false` by default). Changing it needs a person with the admin role (a token with the `admin` scope), and each change is recorded in the audit log as `repo.actions_workflow_settings`. See [Workflow syntax](/docs/actions/workflow-syntax) for `permissions:`. | |
| 817 | + | |
| 589 | 818 | ## Merge when ready and the merge queue |
| 590 | 819 | |
| 591 | 820 | If the base branch's protection rule has **Require a merge queue**, the merge box shows **Merge when ready** instead of **Merge pull request**. Clicking it (or `diffinery pr merge 12`, `PUT /repos/{owner}/{repo}/pulls/{number}/merge-queue`, or the `merge_pull_request` MCP tool) adds the pull request to the queue. Diffinery tests it together with the pull requests ahead of it on a merge group branch and merges it once the required checks pass there. The merge box then shows `Queued to merge`, its position, and a **Remove from queue** button. |
| 592 | 821 | |
| 593 | -The pull request must already have no blockers to join the queue. Pushing to its branch removes it from the queue. See [Merge queue](/docs/collaboration/merge-queue) for the full behaviour. | |
| 822 | +The pull request must already have no blockers to join the queue. Pushing to its branch removes it from the queue. To have it join the queue by itself once it has none, turn on [auto-merge](#auto-merge). See [Merge queue](/docs/collaboration/merge-queue) for the full behaviour. | |
| 594 | 823 | |
| 595 | 824 | ## Lock the conversation |
| 596 | 825 | |
| @@ -611,7 +840,7 @@ | ||
| 611 | 840 | |
| 612 | 841 | ## Events and timeline |
| 613 | 842 | |
| 614 | -Changes to a pull request are recorded as events: `labeled`, `unlabeled`, `assigned`, `unassigned`, `renamed`, `closed`, `reopened`, `locked`, `unlocked`, `review_requested`, `review_request_removed`, `review_dismissed`, `ready_for_review`, `converted_to_draft` and `merged`. (GitHub names the draft event `convert_to_draft`.) Read them with `GET /repos/{owner}/{repo}/issues/{number}/events`, or read events, comments and reviews in time order with `GET /repos/{owner}/{repo}/issues/{number}/timeline`. See [Issues](/docs/collaboration/issues#events-and-timeline). | |
| 843 | +Changes to a pull request are recorded as events: `labeled`, `unlabeled`, `assigned`, `unassigned`, `renamed`, `closed`, `reopened`, `locked`, `unlocked`, `review_requested`, `review_request_removed`, `review_dismissed`, `ready_for_review`, `convert_to_draft`, `auto_merge_enabled`, `auto_merge_disabled` and `merged`, with GitHub's names. (The `pull_request` webhook action for converting to a draft is `converted_to_draft`, as on GitHub.) Read them with `GET /repos/{owner}/{repo}/issues/{number}/events`, or read events, comments and reviews in time order with `GET /repos/{owner}/{repo}/issues/{number}/timeline`. See [Issues](/docs/collaboration/issues#events-and-timeline). | |
| 615 | 844 | |
| 616 | 845 | ## Close and reopen |
| 617 | 846 | |
| @@ -679,8 +908,8 @@ | ||
| 679 | 908 | |
| 680 | 909 | - Pull requests are between branches of the same repository; there are no forks. |
| 681 | 910 | - The intent can't be edited after opening. |
| 682 | -- Draft state changes through `PATCH /pulls/{number}` with `draft` (GitHub offers this only in GraphQL), and the timeline event for converting to a draft is `converted_to_draft` (GitHub: `convert_to_draft`). | |
| 683 | -- An admin's merge past unmet branch rules must be asked for with `bypass_rules` (or `bypass_merge_queue`); without it an admin is refused like anyone else. A merge commit is refused on a branch that requires linear history. | |
| 911 | +- Draft state changes through `PATCH /pulls/{number}` with `draft` (GitHub offers this only in GraphQL). | |
| 912 | +- An admin's merge past unmet branch rules must be asked for with `bypass_rules`, and skipping a merge queue with `bypass_merge_queue`; without them an admin is refused like anyone else. A merge commit is refused on a branch that requires linear history unless `bypass_rules` is sent. | |
| 684 | 913 | - Reviews are submitted immediately; there are no pending reviews, suggested changes, or thread resolution yet. |
| 685 | 914 | - `rebase` outside the merge queue is a fast-forward only. |
| 686 | 915 | - `update-branch` exists in the REST API only (merge method only, no rebase); there is no **Update branch** button and no web conflict editor. |
| @@ -690,6 +919,8 @@ | ||
| 690 | 919 | - `mergeable_state` never reports `behind`, `unstable` or `has_hooks`. |
| 691 | 920 | - Requested reviewers are accounts only (no teams). |
| 692 | 921 | - Locking, reactions, review dismissal and repository-wide comment lists have no web UI yet. |
| 922 | +- Auto-merge is available in every repository, with no repository setting to allow it first, and has REST endpoints (`PUT` and `DELETE /pulls/{number}/auto-merge`) where GitHub uses GraphQL. It waits only for required checks, never deletes the head branch, and its `auto_merge_enabled` and `auto_merge_disabled` actions don't start `pull_request` workflows. The CLI has no `--auto` flag. | |
| 923 | +- Agents land changes on protected branches through auto-merge only where a branch rule allows it (`allowAgentAutoMerge`), which has no GitHub equivalent. | |
| 693 | 924 | - See also [Merge queue](/docs/collaboration/merge-queue#differences-from-github). |
| 694 | 925 | |
| 695 | 926 | ## Troubleshooting |
| @@ -699,6 +930,9 @@ | ||
| 699 | 930 | | `pull request is not mergeable: ...` | The merge has blockers, listed after the colon. See [Merge blockers](#merge-blockers). If it ends with `(as an admin you can bypass with bypass_rules)`, you can merge anyway with an [admin bypass](#admin-bypass). | |
| 700 | 931 | | `main requires linear history; use squash or rebase` (405) | You asked for a merge commit on a branch that requires linear history. Use `squash` or `rebase` (`diffinery pr merge 12 --squash`). | |
| 701 | 932 | | `only the author or someone with write access can change draft status` (403) | Only the author or someone with write access can mark a draft ready or convert it back. | |
| 933 | +| `GitHub Actions is not permitted to approve pull requests. ...` (403) | A workflow's `GITHUB_TOKEN` tried to approve or request changes. An admin can allow it; see [Pull requests and workflow tokens](#pull-requests-and-workflow-tokens). | | |
| 934 | +| `Resource not accessible by integration (...)` (403) | A workflow's `GITHUB_TOKEN` lacks the permission named in the message. Add it to the job's `permissions:`. | | |
| 935 | +| `repository is archived (read-only)` (403) | The repository is archived. An admin must unarchive it first. | | |
| 702 | 936 | | `authentication required` (401) | You sent no credentials. Every change needs a signed-in caller, even on a public repository. | |
| 703 | 937 | | `this token needs the write scope` (403) | Your token has the `read` scope. Create one with `write` (or `admin`). | |
| 704 | 938 | | `head branch was modified; review and try again` (409) | Someone pushed after you loaded the page or read the head SHA. Review the new commits and merge again. | |
| @@ -714,4 +948,6 @@ | ||
| 714 | 948 | | `path and line are required` | A new line comment needs `path` and `line` (or `in_reply_to` for a reply). | |
| 715 | 949 | | `team reviewers are not supported; request people by login` | Request reviewers by login in `reviewers`. | |
| 716 | 950 | | `main requires a merge queue: add the pull request to the queue` | Use **Merge when ready**, `diffinery pr merge`, or the merge queue API. | |
| 951 | +| `Auto-merge was turned off: ...` (a comment) | Auto-merge can't go through as set up. See [When auto-merge turns itself off](#when-auto-merge-turns-itself-off). | | |
| 952 | +| `agents may not have pull requests merged into main automatically ...` | An agent or workflow token turned on auto-merge for a branch that doesn't allow it. See [Auto-merge errors](#auto-merge-errors). | | |
| 717 | 953 | | No `pull_request` runs appear | The pull request conflicts with its base (no test merge commit), or the workflow's `on.pull_request` filters exclude it. | |
modifiedapps/diffinery/docs/collaboration/releases.md+9−6View file
| @@ -29,7 +29,7 @@ | ||
| 29 | 29 | | React to a release | read (signed in) | |
| 30 | 30 | |
| 31 | 31 | - With a personal access token, the token needs the `write` scope (or `admin`) to create or change releases. A `read` token can list and download. |
| 32 | -- A workflow's `GITHUB_TOKEN` (the job token) is read-only on the REST API, whatever the workflow's `permissions` say, so it can read releases but not create them or upload assets. Store a personal or agent token with the `write` scope as an Actions secret and use that instead. | |
| 32 | +- A workflow's `GITHUB_TOKEN` (the job token) can create, edit and delete releases and upload assets when the job has `contents: write`, as on GitHub. With only `contents: read` (the default for new repositories) it can read releases, and a write fails with `403` and `Resource not accessible by integration (the workflow's GITHUB_TOKEN needs permissions: contents: write)`. Releases a job token creates or changes don't start `on: release` workflows (see [Workflows triggered by releases](#workflows-triggered-by-releases)). | |
| 33 | 33 | - On an archived repository, every release change (including assets) fails with `repository is archived (read-only)`. |
| 34 | 34 | - Deleting a release never deletes its tag. Moving or deleting an existing tag needs the maintain role. |
| 35 | 35 | |
| @@ -420,6 +420,8 @@ | ||
| 420 | 420 | on: |
| 421 | 421 | release: |
| 422 | 422 | types: [published] |
| 423 | +permissions: | |
| 424 | + contents: write | |
| 423 | 425 | jobs: |
| 424 | 426 | build: |
| 425 | 427 | runs-on: ubuntu-latest |
| @@ -428,7 +430,7 @@ | ||
| 428 | 430 | - run: ./build.sh "${{ github.event.release.tag_name }}" |
| 429 | 431 | - name: Attach the build to the release |
| 430 | 432 | env: |
| 431 | - TOKEN: ${{ secrets.RELEASE_TOKEN }} | |
| 433 | + TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| 432 | 434 | run: | |
| 433 | 435 | curl -fsS -X POST \ |
| 434 | 436 | -H "Authorization: Bearer $TOKEN" \ |
| @@ -437,7 +439,7 @@ | ||
| 437 | 439 | "https://diffinery.com/api/uploads/repos/${{ github.repository }}/releases/${{ github.event.release.id }}/assets?name=website.tar.gz" |
| 438 | 440 | ``` |
| 439 | 441 | |
| 440 | -`RELEASE_TOKEN` is a personal or agent token with the `write` scope, stored as an Actions secret: the job's own `GITHUB_TOKEN` can't upload assets. | |
| 442 | +The job's own `GITHUB_TOKEN` uploads the asset, which needs `contents: write` (set above for the whole workflow). Uploads go to the release's `upload_url`, under `/api/uploads/repos/...` (GitHub's `uploads.github.com`). To use a personal or agent token instead, store it as an Actions secret and put it in `TOKEN`. | |
| 441 | 443 | |
| 442 | 444 | ### Activity types |
| 443 | 445 | |
| @@ -456,7 +458,7 @@ | ||
| 456 | 458 | - Without `types`, every activity type starts the workflow. Creating a published release sends `created`, `published` and `released` (or `prereleased`), so a workflow without `types` runs three times. Use `types: [published]` for "run once per new release". |
| 457 | 459 | - Changes to drafts (creating, editing or deleting a draft) never start workflows. They do send webhooks. |
| 458 | 460 | - The workflow only runs when the release's tag exists. |
| 459 | -- Releases created or changed with a job's `GITHUB_TOKEN` don't start workflows (the job token can't change releases anyway). Releases made with a personal or agent token do. | |
| 461 | +- Releases created or changed with a job's `GITHUB_TOKEN` don't start workflows, so a workflow can't trigger itself in a loop. Releases made with a personal or agent token do. | |
| 460 | 462 | |
| 461 | 463 | ### The event payload |
| 462 | 464 | |
| @@ -466,7 +468,7 @@ | ||
| 466 | 468 | |---|---| |
| 467 | 469 | | `action` | The activity type, for example `published` | |
| 468 | 470 | | `release` | The release, in the same shape as the REST API (`id`, `tag_name`, `name`, `body`, `draft`, `prerelease`, `assets`, `upload_url`, `html_url`, `author`, ...) | |
| 469 | -| `repository` | `id`, `node_id`, `name`, `full_name`, `owner.login`, `default_branch`, `html_url`, `clone_url` | | |
| 471 | +| `repository` | GitHub's full repository object, as `GET /repos/{owner}/{repo}` returns it: `id`, `name`, `full_name`, `owner`, `private` and `visibility` (the repository's real visibility), `default_branch`, `html_url`, `clone_url`, the API URLs and so on | | |
| 470 | 472 | | `sender` | `login` and `type` (`User` or `Bot`) of who caused the event | |
| 471 | 473 | |
| 472 | 474 | ## Release webhooks |
| @@ -502,7 +504,8 @@ | ||
| 502 | 504 | | `'<tag>' is not a valid tag name` | The tag isn't a valid git ref name (for example it contains spaces, `..` or ends with `.lock`). | |
| 503 | 505 | | `target_commitish '<x>' does not exist` | The branch, tag or SHA for the new tag doesn't exist in the repository. `generate-notes` reports it as `target_commitish does not exist`. | |
| 504 | 506 | | `tag was created concurrently; retry` | Someone created the tag at the same moment. Send the request again. | |
| 505 | -| `write access required` | Your role is below write, or the request used a workflow's `GITHUB_TOKEN`, which is always read-only here. Use a stored personal or agent token. | | |
| 507 | +| `write access required` | Your role is below write, or the request used a workflow's `GITHUB_TOKEN` without any write permission. | | |
| 508 | +| `Resource not accessible by integration (...)` | A workflow's `GITHUB_TOKEN` lacks `contents: write`. Add it to the job's or workflow's `permissions:`. | | |
| 506 | 509 | | `this token needs the write scope` | Your token has only the `read` scope. | |
| 507 | 510 | | `release not found` | Wrong id, or the release is a draft and you can't write to the repository. | |
| 508 | 511 | | `no published releases` | `/releases/latest` with no published full release. Publish one, or mark one as latest. | |
modifiedapps/diffinery/docs/concepts.md+24−16View file
| @@ -23,11 +23,11 @@ | ||
| 23 | 23 | |
| 24 | 24 | | Organization role | Role on the organization's repositories | |
| 25 | 25 | | --- | --- | |
| 26 | -| member | write | | |
| 26 | +| member | The organization's base permission: `none`, `read`, `write` or `admin` | | |
| 27 | 27 | | admin | admin | |
| 28 | 28 | | owner | admin, plus managing owners and renaming the organization | |
| 29 | 29 | |
| 30 | -Unlike GitHub, there are no teams and no custom roles. To give someone access to a single repository, add them as a collaborator on it instead. See [Organizations](/docs/accounts/organizations). | |
| 30 | +The base permission is a setting of the organization, as on GitHub: new organizations start with `read`, and owners change it on the organization's **Settings** tab. A member who creates a repository in the organization gets `admin` on it. Unlike GitHub, there are no teams and no custom roles. To give someone more access to a single repository, add them as a collaborator on it. See [Organizations](/docs/accounts/organizations) and [Base permission](/docs/accounts/organizations#base-permission). | |
| 31 | 31 | |
| 32 | 32 | ### Agents |
| 33 | 33 | |
| @@ -37,22 +37,30 @@ | ||
| 37 | 37 | |
| 38 | 38 | - An agent never has more access than its owner. An organization's agent can reach only that organization's repositories. |
| 39 | 39 | - Its maximum role is `write` unless its owner chooses otherwise. |
| 40 | -- It can't push to, or merge into, the default branch or a protected branch, unless a branch protection rule explicitly allows it. | |
| 40 | +- It can't push to, or merge into, the default branch or a protected branch, unless a branch protection rule explicitly allows it. A rule can instead let it turn on auto-merge, so Diffinery merges its pull request once the required checks pass and a person approves (or, if an admin waives that, on green checks alone). | |
| 41 | 41 | - A pull request it opens needs at least one approval from a person, and its own approvals never count. |
| 42 | -- It can't change branch protection, add, change or delete secrets or variables (repository, organization or environment), create, change or delete environments, dismiss reviews, delete repositories or bypass push protection. | |
| 42 | +- It can't change branch protection, add, change or delete secrets or variables (repository, organization or environment), create, change or delete environments, dismiss reviews, delete or transfer repositories, or bypass push protection. | |
| 43 | 43 | - Commit statuses and check runs it reports never satisfy a required check, and tags it pushes never match an environment's tag rules. |
| 44 | 44 | - Commits it makes carry a `diffinery-agent` header naming the agent and the person it acts for. |
| 45 | +- A workflow run whose code came from an agent (the agent triggered it, pushed the branch last, or opened the pull request, and re-runs and `workflow_run` followers of such a run) acts as the agent, so a workflow can't carry agent-written code past these rules. | |
| 46 | +- It can import from GitHub only into an organization it or its owner administers, its imports never bypass push protection, and they skip variables, secret names and environments. | |
| 45 | 47 | |
| 46 | 48 | Agents work through the [MCP server](/docs/agents/mcp), the REST API, the CLI and Git. See [Agents](/docs/agents/overview). |
| 47 | 49 | |
| 50 | +### People imported from GitHub | |
| 51 | + | |
| 52 | +When you [import from GitHub](/docs/migrate-from-github), no accounts are created. A GitHub user whose GitHub account is linked to a Diffinery account (under **Settings → Linked accounts**, matched by GitHub's numeric user id) is credited to that account. Everyone else becomes a **mannequin**: a placeholder that carries their issues, comments and reviews under their GitHub login, with a **GitHub** badge in the web UI and `"type": "Mannequin"` and a `github:<login>` login in the API. A mannequin isn't an account: it can't sign in or act. Imported reviews, whoever wrote them, are history and never count as approvals. See [People: linked accounts and mannequins](/docs/migrate-from-github#people-linked-accounts-and-mannequins). | |
| 53 | + | |
| 48 | 54 | ## Repositories |
| 49 | 55 | |
| 50 | 56 | A repository holds a project's Git history plus its issues, pull requests, reviews, checks, releases, deployment environments, workflow runs, webhooks, secrets and variables. It lives at `https://diffinery.com/<owner>/<name>` (for example `https://diffinery.com/acme/website`) and is cloned from the same URL with `.git` appended. |
| 51 | 57 | |
| 52 | 58 | New repositories are private and use `main` as the default branch unless you choose otherwise. |
| 53 | 59 | |
| 54 | -To move a repository from GitHub, `diffinery migrate github` copies its branches, tags, default branch and variables, and optionally its labels, issues, comments and open pull requests. See [Migrate from GitHub](/docs/migrate-from-github). | |
| 60 | +A repository can be renamed, or transferred to your own account or to an organization you own or administer. Its old URLs keep working, as on GitHub: web pages and the REST API redirect, and Git, MCP and container images are served under the old name, for anyone who can still read it. | |
| 55 | 61 | |
| 62 | +To move from GitHub, the server-side importer copies a repository, or every repository of an organization, with its history, issues and pull requests (keeping their numbers), comments, reviews, labels, milestones, releases, branch protection, variables and environments. Secret values are never copied, only their names. Start it from **Settings → Imports**, `diffinery migrate github`, the REST API or MCP. See [Migrate from GitHub](/docs/migrate-from-github). | |
| 63 | + | |
| 56 | 64 | There is no forking. Pull requests are always between two branches of the same repository, so contributors need write access to push a branch. |
| 57 | 65 | |
| 58 | 66 | Repositories can carry topics, and any branch, tag or commit can be downloaded as a `.zip` or `.tar.gz` source archive. Code is shown with syntax highlighting. |
| @@ -65,10 +73,10 @@ | ||
| 65 | 73 | |
| 66 | 74 | | Visibility | Who can see it | |
| 67 | 75 | | --- | --- | |
| 68 | -| Private | The owner, organization members (for organization repositories), collaborators and site administrators | | |
| 76 | +| Private | The owner, organization members (for organization repositories, unless the organization's base permission is `none`), collaborators and site administrators | | |
| 69 | 77 | | Public | Everyone, including people who aren't signed in. Signed-in users get `read`. | |
| 70 | 78 | |
| 71 | -Someone without access to a private repository gets `404 repository not found`, never `403`, so its name isn't revealed. Writing to a public repository still needs a signed-in account (or a token) with a role that allows it. People who aren't signed in can only read: opening an issue, commenting, reacting or pushing without an account fails with `401 authentication required`. See [Visibility](/docs/repositories/overview#visibility). | |
| 79 | +Someone without access to a private repository gets `404 repository not found`, never `403`, so its name isn't revealed. Writing to a public repository still needs a signed-in account (or a token) with a role that allows it. People who aren't signed in can only read: opening an issue, commenting, reacting or pushing without an account fails with `401 authentication required` (for a push, Git then asks for your token). See [Visibility](/docs/repositories/overview#visibility). | |
| 72 | 80 | |
| 73 | 81 | ## Roles |
| 74 | 82 | |
| @@ -82,7 +90,7 @@ | ||
| 82 | 90 | | `maintain` | Also move or delete tags, edit other people's comments and dismiss reviews | |
| 83 | 91 | | `admin` | Settings, access, branch protection, secrets and webhooks | |
| 84 | 92 | |
| 85 | -Your role on a repository comes from owning it, your organization role, a collaborator grant or the repository being public, whichever is highest. Tokens and agent limits can then only lower it. See [Permissions](/docs/accounts/permissions). | |
| 93 | +Your role on a repository comes from owning it, your organization role (members get the organization's base permission), a collaborator grant or the repository being public, whichever is highest. Tokens and agent limits can then only lower it. See [Permissions](/docs/accounts/permissions). | |
| 86 | 94 | |
| 87 | 95 | ## Tokens |
| 88 | 96 | |
| @@ -106,15 +114,15 @@ | ||
| 106 | 114 | | Create an organization, send invitations, manage organization secrets | `admin` | |
| 107 | 115 | | Create or revoke tokens, create or disable agents, create agent tokens | `admin` | |
| 108 | 116 | |
| 109 | -A token can also be restricted to a list of repositories; every other repository then answers `404` as if you had no access, and the token can't create repositories or organizations, send invitations or manage agents. Browser sessions aren't capped, so in the web UI you always act with your full role. Unlike GitHub's fine-grained tokens, there are no per-permission settings: the scope and the repository list are the whole model. | |
| 117 | +A token can also be restricted to a list of repositories; every other repository then answers `404` as if you had no access, and the token can't create repositories or organizations, send invitations, manage agents or create other tokens. A token can only be restricted to repositories your account can see. Browser sessions aren't capped, so in the web UI you always act with your full role. Unlike GitHub's fine-grained tokens, there are no per-permission settings: the scope and the repository list are the whole model. | |
| 110 | 118 | |
| 111 | 119 | Use a token as the Git password (with any username), as `Authorization: Bearer dfy_...` for the API and MCP, or let `diffinery login` and `diffinery auth setup-git` store it. See [Personal access tokens](/docs/accounts/tokens). |
| 112 | 120 | |
| 113 | 121 | ## Job tokens |
| 114 | 122 | |
| 115 | -Each workflow job gets a short-lived job token as `GITHUB_TOKEN` (also `DIFFINERY_TOKEN`). It starts with `djt_`, acts as `diffinery-actions[bot]`, works only for the repository the job runs in and stops working when the job completes. | |
| 123 | +Each workflow job gets a short-lived job token as `GITHUB_TOKEN` (also `DIFFINERY_TOKEN`). It starts with `djt_`, acts as `diffinery-actions[bot]` on the repository the job runs in (it can also read other public repositories) and stops working when the job completes. | |
| 116 | 124 | |
| 117 | -The job token has `read` on the repository: the checkout step uses it to clone. With the job's `permissions: packages`, it can pull its owner's container images and push to images linked to its own repository. This differs from GitHub, where `GITHUB_TOKEN` can usually push. To push commits or call write endpoints from a workflow, store a personal or agent token as a secret and use that. | |
| 125 | +As on GitHub, the token can do what the workflow's `permissions:` grant (`contents`, `issues`, `pull-requests`, `statuses`, `packages` and so on), and an endpoint it lacks the permission for answers `403 Resource not accessible by integration`. Without `permissions:`, a job gets the repository's default, which for new repositories is GitHub's restricted one (`contents` and `packages` read); a repository admin can change it (see [Workflow permissions](/docs/actions/workflow-permissions)). The token never acts above `write`, so branch protection applies; it never changes repository settings, secrets or workflow files; it can't open or approve pull requests unless the repository allows it, and its approvals never count; and a run whose code came from an agent acts as that agent. | |
| 118 | 126 | |
| 119 | 127 | Sandboxes get a similar token starting with `dst_` (see [Sandboxes](#sandboxes)). See [Job tokens and sandbox tokens](/docs/accounts/permissions#job-tokens-and-sandbox-tokens) and [Secrets and variables](/docs/actions/secrets-and-variables). |
| 120 | 128 | |
| @@ -126,7 +134,7 @@ | ||
| 126 | 134 | |
| 127 | 135 | Some protections apply even without any rule: the default branch can't be deleted, and agents can't push to it or merge into it. When several rules match a branch, the rule that names the branch exactly wins over patterns; otherwise the oldest matching rule applies. |
| 128 | 136 | |
| 129 | -Repository admins can bypass the rules unless the rule sets **Do not allow administrators to bypass these rules** (`enforceAdmins`). A push by an admin bypasses them on its own. A merge doesn't: an admin has to ask for it explicitly (the **Merge without waiting for requirements (bypass)** button, `"bypass_rules": true` in the API, or `diffinery pr merge --admin`), and the bypass is recorded in the audit log as `pull_request.merge_bypassed`. Bypassing merge requirements is for people only, never agents, and never works for a draft or a pull request with conflicts. | |
| 137 | +Repository admins can bypass the rules unless the rule sets **Do not allow administrators to bypass these rules** (`enforceAdmins`). A push by an admin bypasses them on its own. A merge doesn't: an admin has to ask for it explicitly (the **Merge without waiting for requirements (bypass)** button, `"bypass_rules": true` in the API, or `diffinery pr merge --admin`), and the bypass is recorded in the audit log as `pull_request.merge_bypassed`. `"bypass_rules": true` skips unmet rules, and `"bypass_merge_queue": true` skips only a required merge queue. Bypassing merge requirements is for people only, never agents, and never works for a draft or a pull request with conflicts. | |
| 130 | 138 | |
| 131 | 139 | See [Branch protection](/docs/repositories/branch-protection) and [Merge queue](/docs/collaboration/merge-queue). |
| 132 | 140 | |
| @@ -138,7 +146,7 @@ | ||
| 138 | 146 | |
| 139 | 147 | A pull request can be opened as a **draft**: a draft can't be merged until it is marked ready for review (the **Ready for review** button, `diffinery pr ready`, or `PATCH` with `"draft": false`), and it can be turned back into a draft later. |
| 140 | 148 | |
| 141 | -Pull requests are reviewed with **Approve**, **Request changes** or comments, checked by workflow runs and other CI, and merged with a merge commit, a squash or a fast-forward rebase once nothing blocks them. See [Pull requests](/docs/collaboration/pull-requests) and [Issues](/docs/collaboration/issues). | |
| 149 | +Pull requests are reviewed with **Approve**, **Request changes** or comments, checked by workflow runs and other CI, and merged with a merge commit, a squash or a fast-forward rebase once nothing blocks them. With **auto-merge** turned on, Diffinery merges a pull request by itself as soon as nothing blocks it. See [Pull requests](/docs/collaboration/pull-requests) and [Issues](/docs/collaboration/issues). | |
| 142 | 150 | |
| 143 | 151 | ## Actions |
| 144 | 152 | |
| @@ -187,11 +195,11 @@ | ||
| 187 | 195 | |
| 188 | 196 | ## Push protection |
| 189 | 197 | |
| 190 | -Every `git push` is scanned for leaked credentials such as API keys and private keys. A push that contains one is rejected and nothing from it is stored. A repository admin who is a person can push anyway with `git push -o allow-secrets=<reason>`, which is recorded and emailed. See [Push protection](/docs/security/push-protection) and [Security overview](/docs/security/overview). | |
| 198 | +Every `git push` is scanned for leaked credentials such as API keys and private keys, and so is every commit made without Git: in the web editor, through the REST API (`POST /repos/{owner}/{repo}/diffinery/commits` and the contents and git data APIs) and with the MCP `commit_files` tool. A push or commit that contains one is rejected and nothing from it is stored. A repository admin who is a person can push anyway with `git push -o allow-secrets=<reason>`, or commit through the commits API with `allow_secrets`, which is recorded and emailed. See [Push protection](/docs/security/push-protection) and [Security overview](/docs/security/overview). | |
| 191 | 199 | |
| 192 | 200 | ## Audit log |
| 193 | 201 | |
| 194 | -The audit log is an append-only record of security-relevant actions: sign-ins, passkey and token changes, repository settings, pushes, force pushes, merges, push protection decisions and package changes. When an agent acts, the entry names both the agent and the account it acts for. | |
| 202 | +The audit log is an append-only record of security-relevant actions: sign-ins, passkey and token changes, repository settings (whether changed on the web or through the API), pushes, force pushes, merges, push protection decisions and package changes. When an agent acts, the entry names both the agent and the account it acts for. | |
| 195 | 203 | |
| 196 | 204 | Only site administrators can view it, under **Settings → Audit log**, which shows the 200 most recent entries. See [Audit log](/docs/security/audit-log). |
| 197 | 205 | |
| @@ -205,6 +213,6 @@ | ||
| 205 | 213 | |
| 206 | 214 | - [Quickstart](/docs/quickstart): try these concepts end to end in about 15 minutes. |
| 207 | 215 | - [Releases](/docs/collaboration/releases), [Environments](/docs/actions/environments) and [Checks and statuses](/docs/collaboration/checks-and-statuses). |
| 208 | -- [Migrate from GitHub](/docs/migrate-from-github): import repositories, variables, issues and open pull requests with `diffinery migrate github`, and set secrets again. | |
| 216 | +- [Migrate from GitHub](/docs/migrate-from-github): import repositories or whole organizations, link your GitHub account, and set secrets again. | |
| 209 | 217 | - [REST API](/docs/reference/rest-api), the [CLI reference](/docs/reference/cli) and the [MCP tool reference](/docs/reference/mcp-tools). |
| 210 | 218 | - [Limits](/docs/reference/limits): sizes, timeouts and quotas in one place. |
modifiedapps/diffinery/docs/index.md+2−2View file
| @@ -3,7 +3,7 @@ | ||
| 3 | 3 | description: What Diffinery is, how it fits together, and where to start, whether you are a person, a team moving from GitHub or an AI agent. |
| 4 | 4 | --- |
| 5 | 5 | |
| 6 | -Diffinery is a git forge for teams of people and AI agents, running entirely on Cloudflare. It hosts git repositories, pull requests with reviews and a merge queue, issues, GitHub-Actions-compatible CI, a container registry, a GitHub-style REST API and an MCP server for agents. Your existing `.github/workflows/*.yml` files run as they are, and `diffinery migrate github` imports a GitHub repository in one command (see [Migrate from GitHub](/docs/migrate-from-github)). | |
| 6 | +Diffinery is a git forge for teams of people and AI agents, running entirely on Cloudflare. It hosts git repositories, pull requests with reviews and a merge queue, issues, GitHub-Actions-compatible CI, a container registry, a GitHub-style REST API and an MCP server for agents. Your existing `.github/workflows/*.yml` files run as they are, and a server-side importer brings over a GitHub repository or a whole organization, with its history, issues, pull requests, reviews and releases, from the web, the CLI (`diffinery migrate github`), the REST API or MCP (see [Migrate from GitHub](/docs/migrate-from-github)). | |
| 7 | 7 | |
| 8 | 8 | ## Start here |
| 9 | 9 | |
| @@ -11,7 +11,7 @@ | ||
| 11 | 11 | | --- | --- | |
| 12 | 12 | | Get set up and push your first repository | [Quickstart](/docs/quickstart) | |
| 13 | 13 | | Understand accounts, roles, agents and runs | [Concepts](/docs/concepts) | |
| 14 | -| Move a project from GitHub (`diffinery migrate github`) | [Migrate from GitHub](/docs/migrate-from-github) | | |
| 14 | +| Move a project or an organization from GitHub (**Settings → Imports**, or `diffinery migrate github`) | [Migrate from GitHub](/docs/migrate-from-github) | | |
| 15 | 15 | | Run CI | [Actions overview](/docs/actions/overview) and [Workflow syntax](/docs/actions/workflow-syntax) | |
| 16 | 16 | | Connect an AI agent | [Agents](/docs/agents/overview) and [MCP](/docs/agents/mcp) | |
| 17 | 17 | | Script against Diffinery | [REST API guide](/docs/reference/rest-api), [REST API reference](/docs/api) and [CLI reference](/docs/reference/cli) | |
modifiedapps/diffinery/docs/migrate-from-github.md+678−153View file
| @@ -1,168 +1,643 @@ | ||
| 1 | 1 | --- |
| 2 | 2 | title: Migrate from GitHub |
| 3 | -description: Move repositories from GitHub to Diffinery with diffinery migrate or plain Git, copy secrets, variables and releases, update remotes, or mirror from GitHub. | |
| 3 | +description: Import GitHub repositories or whole organizations with the server-side importer (web, CLI, API or MCP), then set secrets, update remotes and check workflows. | |
| 4 | 4 | --- |
| 5 | 5 | |
| 6 | -This page shows how to move a project from GitHub to Diffinery: with the `diffinery migrate github` command, with plain Git, or by keeping GitHub as the upstream and mirroring every push to Diffinery. It also covers secrets, variables and environments, releases, Git LFS files, remotes, workflows, scripts that call GitHub's API, and a checklist. | |
| 6 | +Diffinery imports a GitHub repository, or every repository of a GitHub organization or user, on the server: it copies the Git history in a container and everything else through GitHub's REST API, keeping issue and pull request numbers, comments, reviews and their authors. You start an import from the web, the `diffinery` CLI, the REST API or MCP, and nothing runs on your machine. This page covers the importer in full, the older client-side and plain Git paths, mirroring from GitHub, and what to change after you move. | |
| 7 | 7 | |
| 8 | 8 | ## Choose a method |
| 9 | 9 | |
| 10 | 10 | | Method | Copies | Use it when | |
| 11 | 11 | | --- | --- | --- | |
| 12 | -| [`diffinery migrate github`](#import-with-diffinery-migrate) | Branches, tags, default branch, Actions variables, and the names of Actions secrets. With `--issues`, also labels, issues, comments and open pull requests. | You want one command that does the whole move. | | |
| 13 | -| [Plain Git](#move-with-plain-git) | Branches and tags. | You only need the code, or the CLI can't run where you are. | | |
| 12 | +| [The server-side importer](#how-the-importer-works) (web, `diffinery migrate github`, REST API, MCP) | Branches, tags, pull request heads, Git LFS objects, issues and pull requests with their numbers, comments, reviews and review comments, labels, milestones, releases and assets, branch protection, Actions variables, deployment environments with their variables, and the names (never the values) of secrets. | You are moving a repository or an organization to Diffinery. This is the default. | | |
| 13 | +| [`diffinery migrate github --local`](#the-local-client-side-import) | Branches, tags, default branch, repository variables and secret names. With `--issues`, also labels, issues, comments and open pull requests, created anew. | You want the older client-side path that clones on your machine. | | |
| 14 | +| [Plain Git](#move-with-plain-git) | Branches and tags. | You only need the code. | | |
| 14 | 15 | | [Mirror from GitHub](#keep-github-as-upstream-and-mirror-to-diffinery) | Every branch and tag on every push, plus the secrets you list. | You want to try Diffinery (or run its Actions) while your team keeps working on GitHub. | |
| 15 | 16 | |
| 16 | -Whatever you choose, Diffinery runs your existing `.github/workflows/*.yml` files on the first push. See [Workflows](#workflows). | |
| 17 | +Whatever you choose, Diffinery runs your existing `.github/workflows/*.yml` files. See [Workflows](#workflows). | |
| 17 | 18 | |
| 19 | +## How the importer works | |
| 20 | + | |
| 21 | +An import is a record on the server that runs through fixed steps, each retried on failure and resumed where it stopped: | |
| 22 | + | |
| 23 | +1. **Create the repository** under the Diffinery user or organization you chose, with GitHub's description, homepage, topics (up to 20) and default branch. | |
| 24 | +2. **Copy branches, tags and history.** A container clones the GitHub repository with `git clone --mirror` and pushes it to Diffinery in chunks of about 32 MB, then copies Git LFS objects. | |
| 25 | +3. **Copy issues, pull requests, comments, reviews, review comments, labels, milestones, branch protection, variables, secret names, environments and releases** through GitHub's REST API, page by page. | |
| 26 | +4. **Finish:** create `refs/pull/<n>/head` for every imported pull request, set the default branch, archive the repository if it is archived on GitHub, and write the summary. | |
| 27 | + | |
| 28 | +Things to know: | |
| 29 | + | |
| 30 | +- **Nothing the import writes fires events.** No workflow runs start, no webhooks are delivered and nobody is notified about imported issues, comments or pushes. Your first push after the import starts workflows as usual. | |
| 31 | +- **The repository is locked while the import runs.** Every push from anyone else is refused with `repository is locked by an import in progress` until the import finishes, fails or is cancelled. | |
| 32 | +- **Every write is keyed by GitHub's ids**, so a retried step or a retried import never duplicates issues, pull requests, comments, reviews, labels, milestones or branch protection rules. | |
| 33 | +- **The import keeps running if you close the page or stop the CLI.** You get an entry in your notifications list when it succeeds (`Imported acme/website into acme/website`) or fails, linking to its page. | |
| 34 | +- **Every import is audited:** `import.create`, `import.push`, `import.finish`, `import.fail`, `import.cancel` and `import.retry` appear in the audit log. See [Audit log](/docs/security/audit-log). | |
| 35 | + | |
| 18 | 36 | ## Before you start |
| 19 | 37 | |
| 20 | 38 | You need: |
| 21 | 39 | |
| 22 | 40 | - **A Diffinery account.** Diffinery is invite-only; ask a site administrator or an owner or admin of your organization for an invite. See [Invitations](/docs/accounts/invitations). |
| 23 | -- **An organization** if the repository should belong to one, for example `acme`. Create it under **Settings → Organizations**. See [Organizations](/docs/accounts/organizations). | |
| 24 | -- **A Diffinery token with the `admin` scope.** Create it under **Settings → Access tokens** (`https://diffinery.com/settings/tokens`). The import sets the default branch and Actions variables, which need the admin role, and a `write` token acts as at most maintain. Don't restrict the token to some repositories: the import creates the repository, and a restricted token gets `this token is limited to repositories and can't change account settings`. Use your own account's token, not an agent's: agents can't push to the default branch or set Actions variables. See [Personal access tokens](/docs/accounts/tokens). | |
| 25 | -- **Git**, version 2.31 or newer for `diffinery migrate` (2.26 or newer for plain Git). | |
| 26 | -- **The `diffinery` CLI** for the import command. It needs Node.js 22 or later: | |
| 41 | +- **An organization** if the repositories should belong to one, for example `acme`, where you are an owner or admin. Create it under **Settings → Organizations**. See [Organizations](/docs/accounts/organizations). | |
| 42 | +- **For the CLI, API or MCP, a Diffinery token with the `write` or `admin` scope.** Use `admin` if old history may contain credentials and you want them imported with a bypass (see [Secrets in old history](#secrets-in-old-history)). The web UI needs no token. See [Personal access tokens](/docs/accounts/tokens). | |
| 43 | +- **A GitHub token** for a private repository, and for Git LFS, variables, secret names, environments and branch protection. See [GitHub token](#github-token). | |
| 44 | +- **Optionally, a linked GitHub account**, so your own past issues, comments and reviews are attributed to your Diffinery account. Link it before you import. See [Link your GitHub account](#link-your-github-account). | |
| 27 | 45 | |
| 28 | - ```sh | |
| 29 | - curl -fsSL https://diffinery.com/-/cli/diffinery.mjs -o ~/.local/bin/diffinery && chmod +x ~/.local/bin/diffinery | |
| 30 | - diffinery login # paste the dfy_ token | |
| 31 | - diffinery auth setup-git # let plain git use the same token | |
| 32 | - ``` | |
| 46 | +### Who can import where | |
| 33 | 47 | |
| 34 | -## Import with diffinery migrate | |
| 48 | +| Who | Can import into | | |
| 49 | +| --- | --- | | |
| 50 | +| A person | Their own account, and organizations where they are an owner or admin | | |
| 51 | +| A site administrator | Any user or organization | | |
| 52 | +| An agent | Only organizations: the organization that owns the agent, or one where the agent's owner is an owner or admin. Never a user account. The agent's maximum role must be at least `write`. | | |
| 53 | +| An organization | Nothing: `organizations cannot start imports` | | |
| 35 | 54 | |
| 36 | -`diffinery migrate github` copies a GitHub repository into Diffinery in one go: | |
| 55 | +Organization members who aren't owners or admins can't import into the organization (`you can import only into your own account or organizations you administer, not acme`). The check runs again when the import starts; if you lost the right in the meantime the import fails with `the person or agent who started this import can no longer import into this account`. | |
| 37 | 56 | |
| 57 | +### GitHub token | |
| 58 | + | |
| 59 | +Without a GitHub token, the importer reads only public data, and GitHub's limit for anonymous requests (60 an hour) makes a large import slow. The importer waits whenever GitHub asks it to, so it still finishes. With a token: | |
| 60 | + | |
| 61 | +| To import | The GitHub token needs | | |
| 62 | +| --- | --- | | |
| 63 | +| A private repository, its Git data, issues, pull requests, comments, reviews, labels, milestones and releases | Classic token: the `repo` scope. Fine-grained token: read access to **Contents**, **Metadata**, **Issues** and **Pull requests**. | | |
| 64 | +| Branch protection | Admin access to the repository on GitHub | | |
| 65 | +| Actions variables, secret names and environments | Admin access to the repository, or for a fine-grained token **Variables** and **Secrets** read access | | |
| 66 | +| Organization variables and secret names (organization imports) | Admin access to the GitHub organization | | |
| 67 | +| Private repositories of a GitHub organization or user (organization imports) | Access to those repositories; the list includes private ones only when the token can see them | | |
| 68 | + | |
| 69 | +When the token can't read something optional, the import carries on and records a warning, for example `Branch protection of main could not be read (the GitHub token needs admin access to the repository); set it up under Settings → Branches.` (on Diffinery the page is **Settings → Branch protection**). | |
| 70 | + | |
| 71 | +How the token is handled: | |
| 72 | + | |
| 73 | +- It is used only for this import, encrypted at rest, and deleted when the import finishes, fails or is cancelled. A retry needs it again. | |
| 74 | +- It is never returned by the API, MCP or the web UI, and never written to logs. In the container, Git gets it from per-host configuration, never on its command line. | |
| 75 | +- It is sent only to the one GitHub host you import from. Redirects to another host are not followed, the clone URL must be on that host (`GitHub returned a clone URL on another host`), and release assets get the token only when they are on the API's own origin. | |
| 76 | +- The CLI sends `GH_TOKEN` (or `GITHUB_TOKEN`) to Diffinery over HTTPS so the server can use it. | |
| 77 | + | |
| 78 | +### GitHub Enterprise Server | |
| 79 | + | |
| 80 | +To import from GitHub Enterprise Server, name its REST API base explicitly: the **GitHub Enterprise API URL** field in the web form, `github_api_url` in the API and MCP, or `GITHUB_API_URL` for the CLI. The rules: | |
| 81 | + | |
| 82 | +| Rule | Details | | |
| 83 | +| --- | --- | | |
| 84 | +| Default | `https://api.github.com`. `github.com`, `www.github.com` and `api.github.com` all mean github.com. | | |
| 85 | +| Form | `https://ghe.example.com/api/v3`. A bare host such as `https://ghe.example.com` gets `/api/v3` added. | | |
| 86 | +| Must be https on a public host | Otherwise `github_api_url must use https`, `github_api_url must be a public host` (localhost, single-label names, `.local`, `.internal` and private or loopback addresses are refused), `github_api_url must not contain credentials` or `github_api_url is not a valid URL`. | | |
| 87 | +| A token with an Enterprise source URL | If `source` is a URL on an Enterprise host and you give a token, `github_api_url` is required (`github_api_url is required when a github_token is given and the source names a GitHub Enterprise host`) and must be on the same host (`github_api_url must be on the same host as the source repository`). This keeps a token from going to a host taken from the source URL. | | |
| 88 | +| No token | A source URL on an Enterprise host implies `https://<host>/api/v3`. | | |
| 89 | + | |
| 90 | +Release assets are downloaded only from github.com's asset hosts and from the GitHub Enterprise host the server administrator configured (`IMPORT_GHE_HOST`). On other Enterprise hosts, assets are skipped with a warning and the releases themselves are still imported. | |
| 91 | + | |
| 92 | +## Import from the web | |
| 93 | + | |
| 94 | +### One repository | |
| 95 | + | |
| 96 | +1. Open the import form in any of these ways: | |
| 97 | + - **Settings → Imports**, then **Import from GitHub**. | |
| 98 | + - On **Create a new repository** (`https://diffinery.com/new`), the **Import a repository** link. | |
| 99 | + - On an organization's page, **Import from GitHub** next to **New repository** (shown to its owners and admins; it opens the form for an organization import into that organization). | |
| 100 | + - Directly: `https://diffinery.com/new/import`. | |
| 101 | +2. Under **What to import**, choose **One repository**. | |
| 102 | +3. In **GitHub repository, or organization/user**, enter `acme/website` or `https://github.com/acme/website`. | |
| 103 | +4. In **Import into**, choose yourself or an organization you administer. | |
| 104 | +5. Optionally set **New repository name** (defaults to the GitHub name). | |
| 105 | +6. Choose **Visibility**: **Keep** (a public GitHub repository stays public, any other becomes private), **Private** or **Public**. | |
| 106 | +7. Paste a **GitHub token** if the repository is private or you want LFS, variables and protection. | |
| 107 | +8. Optionally open **What to copy, and GitHub Enterprise** to turn parts off, choose what happens to [secrets in old history](#secrets-in-old-history), or set **GitHub Enterprise API URL**. See [Import options](#import-options). | |
| 108 | +9. Click **Start import**. The import's page opens and follows its progress. | |
| 109 | + | |
| 110 | +### An organization or user | |
| 111 | + | |
| 112 | +1. Open the import form as above. The **Import from GitHub** button on an organization's page preselects it. | |
| 113 | +2. Under **What to import**, choose **A whole organization or user**. | |
| 114 | +3. In **GitHub repository, or organization/user**, enter the GitHub organization or user, for example `acme`. | |
| 115 | +4. Choose **Import into**, **Visibility**, the **GitHub token** and the options, and set **Repositories at a time** (1 to 8, default 3). | |
| 116 | +5. Click **Start import**. The **Choose repositories** page lists the account's repositories with `private`, `fork` and `archived` badges. Everything except forks and archived repositories is ticked. | |
| 117 | +6. Use the filter box with **Filter**, **Select all** (or **Select all shown** when filtered) and **Clear** to adjust the selection, then click **Import N repositories**. | |
| 118 | + | |
| 119 | +Each repository becomes its own import under the organization import, keeps its GitHub name, and is imported in full. A repository whose name already exists in the target fails straight away with `acme/website already exists`; the others go ahead. | |
| 120 | + | |
| 121 | +An organization import waiting on **Choose repositories** is a draft. Drafts left for seven days are cancelled (`draft expired`) and their stored GitHub token is erased. | |
| 122 | + | |
| 123 | +### Import options | |
| 124 | + | |
| 125 | +| Web label | API field (`options.`) | Default | What it controls | | |
| 126 | +| --- | --- | --- | --- | | |
| 127 | +| **Issues and pull requests** | `issues` | on | Issues and pull requests with comments, reviews and review comments | | |
| 128 | +| **Labels and milestones** | `labels` | on | Labels and milestones, even without issues | | |
| 129 | +| **Branch protection** | `branch_protection` | on | Classic branch protection rules | | |
| 130 | +| **Actions variables and secret names** | `variables` | on | Repository variables, secret names, and deployment environments with their variables and secret names; for organization imports also organization variables and secret names | | |
| 131 | +| **Releases** | `releases` | on | Releases and their assets | | |
| 132 | +| **Git LFS objects** | `lfs` | on | Git LFS files | | |
| 133 | +| **Secrets found in old history (push protection)**: **Import with an audited bypass** or **Stop the import** | `secrets`: `"bypass"` or `"stop"` | `bypass` | What happens when push protection finds a credential in old commits. Agents always get `stop`. | | |
| 134 | + | |
| 135 | +Branches, tags and history are always copied. A step whose option is off shows as skipped with `Not selected`. | |
| 136 | + | |
| 137 | +### Follow progress | |
| 138 | + | |
| 139 | +The import's page (`https://diffinery.com/imports/<id>`) refreshes itself every few seconds while the import runs ("This page refreshes automatically.") and shows: | |
| 140 | + | |
| 141 | +- The source, the target and a status badge: `queued`, `running`, `succeeded`, `failed` or `cancelled` (and `draft` for an organization import that waits for its selection). | |
| 142 | +- Each step with its state and a short message, such as `12 branches, 30 tags, 118 pull request heads, 4 LFS objects`. | |
| 143 | +- For an organization import, every repository with its status and an **Open** button. | |
| 144 | +- After it finishes: **Imported** (counts), **Secret names** with a **Set secret values** button, **Possible secrets in history (rotate these)** when a bypass was used, and **Warnings**. | |
| 145 | +- **Cancel import** while it runs, and **Retry this import** when it failed or was cancelled. | |
| 146 | + | |
| 147 | +**Settings → Imports** (`https://diffinery.com/imports`) lists the imports you started, newest first. Admins of the target account can also open an import's page by its link. | |
| 148 | + | |
| 149 | +| Step | Title on the page | | |
| 150 | +| --- | --- | | |
| 151 | +| `repository` | Create the repository | | |
| 152 | +| `git` | Copy branches, tags and history | | |
| 153 | +| `labels` | Labels and milestones | | |
| 154 | +| `issues` | Issues and pull requests | | |
| 155 | +| `comments` | Comments | | |
| 156 | +| `reviews` | Reviews | | |
| 157 | +| `review_comments` | Review comments | | |
| 158 | +| `protection` | Branch protection | | |
| 159 | +| `variables` | Variables and secret names | | |
| 160 | +| `releases` | Releases | | |
| 161 | +| `finalize` | Finish | | |
| 162 | + | |
| 163 | +An organization import has three steps: `organization` (**Organization variables and secret names**), `repositories` (**Repositories**, with `3 of 10 finished (2 imported, 1 failed)`) and `finalize` (**Finish**). Each step is `pending`, `running`, `done`, `skipped` or `failed`. | |
| 164 | + | |
| 165 | +## Import with the CLI | |
| 166 | + | |
| 167 | +Install the CLI (it needs Node.js 22 or later) and log in with a `write` or `admin` token: | |
| 168 | + | |
| 38 | 169 | ```sh |
| 39 | -export GH_TOKEN=ghp_... # a GitHub token; needed for private repositories | |
| 40 | -diffinery migrate github acme/website --to acme/website --issues | |
| 170 | +curl -fsSL https://diffinery.com/-/cli/diffinery.mjs -o ~/.local/bin/diffinery && chmod +x ~/.local/bin/diffinery | |
| 171 | +diffinery login # paste the dfy_ token | |
| 41 | 172 | ``` |
| 42 | 173 | |
| 43 | -### What it copies | |
| 174 | +### diffinery migrate github | |
| 44 | 175 | |
| 45 | -| Data | Copied | How | | |
| 176 | +Starts a server-side import of one repository and follows it until it finishes: | |
| 177 | + | |
| 178 | +```sh | |
| 179 | +export GH_TOKEN=ghp_... # needed for private repositories, LFS, variables and protection | |
| 180 | +diffinery migrate github acme/website --to acme/website | |
| 181 | +``` | |
| 182 | + | |
| 183 | +```text | |
| 184 | +diffinery migrate github <GH-OWNER/GH-REPO> [options] | |
| 185 | +``` | |
| 186 | + | |
| 187 | +The argument can also be a URL such as `https://github.com/acme/website`; the CLI keeps only `owner/name` from it, so for GitHub Enterprise set `GITHUB_API_URL`. | |
| 188 | + | |
| 189 | +| Option | Default | Meaning | | |
| 46 | 190 | | --- | --- | --- | |
| 47 | -| Repository | Created if it doesn't exist | With GitHub's description and visibility (a public repository stays public; any other becomes private), or private with `--private`. If the target exists, the command uses it. | | |
| 48 | -| Branches and tags | Yes | Every `refs/heads/*` and `refs/tags/*`. Pull request refs (`refs/pull/*`) and other refs are not copied. | | |
| 49 | -| Default branch | Yes | Set to GitHub's default branch, if that branch was pushed. | | |
| 50 | -| Actions variables | Yes | Repository variables, with their values. | | |
| 51 | -| Actions secrets | Names only | GitHub never reveals secret values. The command prints a `diffinery secret set` line for each one. | | |
| 52 | -| Labels | With `--issues` | Name, color and description. | | |
| 53 | -| Issues | With `--issues` | Title, body, labels, and closed state with its reason (completed or not planned). | | |
| 54 | -| Issue and pull request comments | With `--issues` | The conversation comments, in order. | | |
| 55 | -| Open pull requests | With `--issues` | Recreated when both branches were copied and the head branch is in the same repository (not a fork), with title, body and draft state. | | |
| 191 | +| `-t, --to OWNER/NAME` | `<you>/<github repo name>` | The Diffinery repository to create. It must not exist yet. | | |
| 192 | +| `-p, --private` | off | Create the repository as private, whatever its visibility on GitHub (otherwise visibility is kept) | | |
| 193 | +| `-l, --local` | off | Run the older client-side import instead. See [The local client-side import](#the-local-client-side-import). | | |
| 194 | +| `-i, --issues` | off | With `--local` only: also copy labels, issues, comments and open pull requests | | |
| 195 | +| `--json` | off | Print the final import object as JSON instead of progress lines | | |
| 56 | 196 | |
| 57 | -Everything created on Diffinery is created by you (the token's account), with new timestamps. Each issue, pull request and comment starts with an attribution line that names the original author and date and links back to GitHub: | |
| 197 | +| Environment variable | Meaning | | |
| 198 | +| --- | --- | | |
| 199 | +| `GH_TOKEN` or `GITHUB_TOKEN` | GitHub token, sent to Diffinery for the import (`GH_TOKEN` wins if both are set) | | |
| 200 | +| `GITHUB_API_URL` | GitHub Enterprise API base, for example `https://ghe.example.com/api/v3` | | |
| 201 | +| `DIFFINERY_TOKEN`, `DIFFINERY_URL` | Override the token and server saved by `diffinery login` | | |
| 58 | 202 | |
| 203 | +The CLI imports with every option on and `secrets` set to `bypass`. To turn parts off or stop on secrets, use the web form or the API. | |
| 204 | + | |
| 205 | +While it runs, the command prints each step and Git phase as it changes: | |
| 206 | + | |
| 59 | 207 | ```text |
| 60 | -> Imported from GitHub issue [#12](https://github.com/acme/website/issues/12) by @diffy on 2026-03-01 | |
| 208 | +Starting server import of acme/website … | |
| 209 | + … Create the repository | |
| 210 | + ✓ Create the repository (acme/website) | |
| 211 | + … Copy branches, tags and history (Starting the import container) | |
| 212 | + … git: Cloning acme/website | |
| 213 | + … git: Pushing history of main: chunk 1 (31.6 MB) | |
| 214 | + ✓ Copy branches, tags and history (42) | |
| 215 | + ✓ Labels and milestones (14) | |
| 216 | + ✓ Issues and pull requests (220) | |
| 217 | + ... | |
| 218 | + ✓ Finish (180 issues, 40 pull requests) | |
| 61 | 219 | |
| 62 | -The original description follows here. | |
| 220 | +Imported acme/website → acme/website | |
| 221 | + | |
| 222 | +GitHub does not reveal secret values. Set these 2 secrets by hand: | |
| 223 | + diffinery secret set NPM_TOKEN -R acme/website | |
| 224 | + diffinery secret set SENTRY_DSN -R acme/website | |
| 225 | + | |
| 226 | +https://diffinery.com/acme/website | |
| 63 | 227 | ``` |
| 64 | 228 | |
| 65 | -Comments start with `> Imported from GitHub: comment by @diffy on 2026-03-02`. | |
| 229 | +Warnings are printed to standard error. Environment secret names are listed as `<environment>/<NAME>`, for example `production/DEPLOY_KEY`; set those with the environments API, not `diffinery secret set` (see [Environments and their secrets](#environments-and-their-secrets)). | |
| 66 | 230 | |
| 67 | -### What it doesn't copy | |
| 231 | +Stopping the command (Ctrl-C) stops following, not the import. Check on it at `https://diffinery.com/imports` or with `GET /api/v3/diffinery/imports`. | |
| 68 | 232 | |
| 69 | -- Closed and merged pull requests, pull requests from forks, and pull requests whose branch is missing. | |
| 70 | -- Pull request reviews and review (line) comments. | |
| 71 | -- Assignees, milestones, reactions, locked state and the original authors' accounts (only the attribution line). | |
| 72 | -- Original issue and pull request numbers. Diffinery numbers them in order as it creates them, so they differ from GitHub's whenever something was skipped. The output lists each mapping (`GitHub #12 → #9`). | |
| 73 | -- Secret values, environments with their secrets and variables, organization secrets and organization variables (see [Move secrets and variables](#move-secrets-and-variables)). | |
| 74 | -- Git LFS files (see [Move Git LFS files](#move-git-lfs-files)). | |
| 75 | -- Releases and their assets (see [Move releases](#move-releases)). | |
| 76 | -- Topics, wikis, projects, discussions, GitHub Pages, packages, webhooks, deploy keys, branch protection rules, collaborators and teams, and Actions run history. | |
| 233 | +### diffinery migrate github-org | |
| 77 | 234 | |
| 78 | -Set up branch protection, collaborators, webhooks and topics on Diffinery yourself afterwards (see [Checklist](#checklist)). | |
| 235 | +Starts a server-side import of many repositories of a GitHub organization or user and follows it until every repository has finished: | |
| 79 | 236 | |
| 80 | -### Options | |
| 237 | +```sh | |
| 238 | +export GH_TOKEN=ghp_... | |
| 239 | +diffinery migrate github-org acme --to acme --exclude-forks --exclude-archived | |
| 240 | +``` | |
| 81 | 241 | |
| 82 | 242 | ```text |
| 83 | -diffinery migrate github <GH-OWNER/GH-REPO> [options] | |
| 243 | +diffinery migrate github-org <GH-ORG-OR-USER> [options] | |
| 84 | 244 | ``` |
| 85 | 245 | |
| 86 | -The argument can also be a URL such as `https://github.com/acme/website`. | |
| 87 | - | |
| 88 | 246 | | Option | Default | Meaning | |
| 89 | 247 | | --- | --- | --- | |
| 90 | -| `-t, --to OWNER/NAME` | `<you>/<github repo name>` | The Diffinery repository to create or push into. Use `--to acme/website` for an organization. | | |
| 91 | -| `-i, --issues` | off | Also import labels, issues, comments and open pull requests. | | |
| 92 | -| `-p, --private` | off | Create the repository as private, whatever its visibility on GitHub. | | |
| 93 | -| `--json` | off | Print a JSON summary instead of the text report. | | |
| 248 | +| `-t, --to OWNER` | you | The Diffinery user or organization to import into | | |
| 249 | +| `--repos A,B,C` | all | Comma-separated names of the repositories to import | | |
| 250 | +| `-p, --private` | off | Create every repository as private | | |
| 251 | +| `--exclude-forks` | off | Skip forks | | |
| 252 | +| `--exclude-archived` | off | Skip archived repositories | | |
| 253 | +| `-c, --concurrency N` | 3 | Repositories imported at the same time, 1 to 8 | | |
| 254 | +| `--json` | off | Print the final import object as JSON | | |
| 94 | 255 | |
| 95 | -Environment variables: | |
| 256 | +Unlike the web form, the CLI has no selection step: it starts importing the repositories straight away. It prints a line per repository as its state changes, then a summary such as `Bulk import of acme: 9/10 repositories imported, 1 failed` and the import's URL. | |
| 96 | 257 | |
| 97 | -| Variable | Meaning | | |
| 258 | +### JSON output and exit status | |
| 259 | + | |
| 260 | +With `--json`, both commands print the final [import object](#the-import-object). Both exit with status `0` when the import succeeded and `1` otherwise (failed or cancelled). An organization import counts as failed when any of its repositories wasn't imported. | |
| 261 | + | |
| 262 | +### The local client-side import | |
| 263 | + | |
| 264 | +`diffinery migrate github --local` runs the older import on your machine. It needs Git 2.31 or newer and a Diffinery token with the `admin` scope that isn't restricted to repositories, from a person, not an agent (it creates the repository, sets the default branch and Actions variables). Run `diffinery auth setup-git` once so Git can use the token. | |
| 265 | + | |
| 266 | +```sh | |
| 267 | +export GH_TOKEN=ghp_... | |
| 268 | +diffinery migrate github acme/website --to acme/website --local --issues | |
| 269 | +``` | |
| 270 | + | |
| 271 | +What it does: | |
| 272 | + | |
| 273 | +1. Creates the target repository with GitHub's description and visibility (or private with `--private`), or uses it if it exists (`Using existing repository acme/website`). | |
| 274 | +2. Clones with `git clone --mirror` into a temporary folder and pushes every `refs/heads/*` and `refs/tags/*` in one `git push`. If that fails, it prints `warning: bulk push failed (...); pushing refs one at a time` and pushes each ref separately. | |
| 275 | +3. Sets the default branch, copies repository variables and lists secret names with a `diffinery secret set` line for each. | |
| 276 | +4. With `--issues`: copies labels, then creates issues, recreates open pull requests whose branches are in the same repository, copies conversation comments and closes closed issues. Everything is created by you, now, with new numbers (the output lists `GitHub #12 → #9`), and each item starts with an attribution line such as `> Imported from GitHub issue [#12](https://github.com/acme/website/issues/12) by @diffy on 2026-03-01`. | |
| 277 | + | |
| 278 | +It doesn't copy closed or merged pull requests, reviews, review comments, milestones, assignees, releases, Git LFS files, environments or branch protection, and pushes go through push protection like any other push (and start workflows). `--issues` isn't idempotent: running it twice creates every issue again. It exits with status 1 if any branch or tag failed to push. With `--json` it prints `repo`, `created`, `branches`, `tags`, `pushFailures`, `defaultBranch`, `labels`, `issues` and `pulls` (`{from, to}` pairs), `skippedPulls`, `comments`, `variables`, `secrets` and `warnings`. | |
| 279 | + | |
| 280 | +Prefer the server-side import: it keeps numbers, authors, dates, reviews and merged pull requests, and doesn't depend on your connection. | |
| 281 | + | |
| 282 | +## Import with the REST API | |
| 283 | + | |
| 284 | +The importer is a Diffinery extension of the REST API under `/api/v3/diffinery/imports`. Every endpoint needs a token (or a browser session). Starting an import needs the `write` or `admin` scope (`starting an import needs a token with write or admin scope`); cancelling, retrying and choosing repositories need the `write` scope on a token that isn't restricted to repositories. | |
| 285 | + | |
| 286 | +### Start an import | |
| 287 | + | |
| 288 | +`POST /api/v3/diffinery/imports` with a JSON body. One repository: | |
| 289 | + | |
| 290 | +```sh | |
| 291 | +curl -X POST https://diffinery.com/api/v3/diffinery/imports \ | |
| 292 | + -H "Authorization: Bearer dfy_..." -H "content-type: application/json" \ | |
| 293 | + -d '{ | |
| 294 | + "source": "acme/website", | |
| 295 | + "owner": "acme", | |
| 296 | + "name": "website", | |
| 297 | + "visibility": "keep", | |
| 298 | + "github_token": "ghp_...", | |
| 299 | + "options": { "releases": true, "secrets": "bypass" } | |
| 300 | + }' | |
| 301 | +``` | |
| 302 | + | |
| 303 | +Many repositories from an organization or user: | |
| 304 | + | |
| 305 | +```sh | |
| 306 | +curl -X POST https://diffinery.com/api/v3/diffinery/imports \ | |
| 307 | + -H "Authorization: Bearer dfy_..." -H "content-type: application/json" \ | |
| 308 | + -d '{ | |
| 309 | + "source_owner": "acme", | |
| 310 | + "owner": "acme", | |
| 311 | + "exclude_forks": true, | |
| 312 | + "exclude_archived": true, | |
| 313 | + "concurrency": 4, | |
| 314 | + "github_token": "ghp_..." | |
| 315 | + }' | |
| 316 | +``` | |
| 317 | + | |
| 318 | +| Field | Applies to | Default | Meaning | | |
| 319 | +| --- | --- | --- | --- | | |
| 320 | +| `source` | One repository | | `owner/name`, a github.com or Enterprise URL, or an SSH remote such as `git@github.com:acme/website.git`. Give either `source` or `source_owner`. | | |
| 321 | +| `source_owner` | Many | | A GitHub organization or user login | | |
| 322 | +| `owner` | Both | required | The Diffinery user or organization to import into | | |
| 323 | +| `name` | One repository | the GitHub name | The new repository's name: letters, digits, `.`, `-` and `_`. It must not exist yet. | | |
| 324 | +| `repositories` | Many | all | Names of the repositories to import, at most 1,000. Unknown names fail the request (`not found in acme: a, b`). | | |
| 325 | +| `exclude_forks` | Many | `false` | Skip forks (ignored when `repositories` is given) | | |
| 326 | +| `exclude_archived` | Many | `false` | Skip archived repositories (ignored when `repositories` is given) | | |
| 327 | +| `concurrency` | Many | `3` | Repositories imported at the same time, 1 to 8 | | |
| 328 | +| `visibility` | Both | `keep` | `keep`, `private` or `public`. With `keep`, a public GitHub repository stays public and any other becomes private. | | |
| 329 | +| `options` | Both | everything on, `secrets: "bypass"` | An object with `issues`, `labels`, `branch_protection`, `variables`, `releases`, `lfs` (booleans) and `secrets` (`"bypass"` or `"stop"`). Unknown keys fail with `unknown option "..."`. See [Import options](#import-options). | | |
| 330 | +| `github_token` | Both | none | GitHub token; at most 400 characters, no spaces | | |
| 331 | +| `github_api_url` | Both | `https://api.github.com` | GitHub Enterprise API base. See [GitHub Enterprise Server](#github-enterprise-server). | | |
| 332 | + | |
| 333 | +Before it records anything, the server reads the source on GitHub, so mistakes fail right away: `GitHub repository acme/website was not found or the token cannot read it`, `GitHub rejected the token (expired or revoked?)`, `no GitHub organization or user named acme`, `acme has no repositories to import`, or `acme/website already exists; choose another name`. A single repository larger than the size limit is refused here too (see [Limits](#limits)). | |
| 334 | + | |
| 335 | +The response is `201 Created` with the [import object](#the-import-object). An organization import created through the API starts at once with the repositories you named (or all of them); the draft and selection step exists only in the web form. | |
| 336 | + | |
| 337 | +### The import object | |
| 338 | + | |
| 339 | +The same object is returned by the REST API, by the MCP tools and by `--json`. It never contains a credential. | |
| 340 | + | |
| 341 | +```json | |
| 342 | +{ | |
| 343 | + "id": "01JA7Q3V9X2M8K4T6R5N0B1C2D", | |
| 344 | + "url": "https://diffinery.com/api/v3/diffinery/imports/01JA7Q3V9X2M8K4T6R5N0B1C2D", | |
| 345 | + "html_url": "https://diffinery.com/imports/01JA7Q3V9X2M8K4T6R5N0B1C2D", | |
| 346 | + "kind": "repository", | |
| 347 | + "parent_id": null, | |
| 348 | + "status": "running", | |
| 349 | + "attempt": 1, | |
| 350 | + "source": { "full_name": "acme/website", "html_url": "https://github.com/acme/website", "api_url": "https://api.github.com" }, | |
| 351 | + "target": { "owner": "acme", "name": "website", "full_name": "acme/website", "html_url": "https://diffinery.com/acme/website" }, | |
| 352 | + "visibility": "keep", | |
| 353 | + "options": { "issues": true, "releases": true, "labels": true, "branch_protection": true, "variables": true, "lfs": true, "secrets": "bypass" }, | |
| 354 | + "created_by": { "login": "diffy", "type": "User" }, | |
| 355 | + "steps": [ | |
| 356 | + { "name": "repository", "title": "Create the repository", "status": "done", "message": "acme/website", "count": null, "started_at": "2026-10-13T09:00:01.000Z", "finished_at": "2026-10-13T09:00:03.000Z" }, | |
| 357 | + { "name": "git", "title": "Copy branches, tags and history", "status": "running", "message": "Starting the import container", "count": null, "started_at": "2026-10-13T09:00:03.000Z", "finished_at": null } | |
| 358 | + ], | |
| 359 | + "git": { "phase": "push", "message": "Pushing history of main: chunk 3 (31.9 MB)", "chunks": 2, "bytes": 66846720, "tries": 1 }, | |
| 360 | + "summary": {}, | |
| 361 | + "warnings": [], | |
| 362 | + "error": null, | |
| 363 | + "created_at": "2026-10-13T09:00:00.000Z", | |
| 364 | + "updated_at": "2026-10-13T09:04:12.000Z", | |
| 365 | + "started_at": "2026-10-13T09:00:01.000Z", | |
| 366 | + "finished_at": null | |
| 367 | +} | |
| 368 | +``` | |
| 369 | + | |
| 370 | +| Field | Meaning | | |
| 98 | 371 | | --- | --- | |
| 99 | -| `GH_TOKEN` or `GITHUB_TOKEN` | GitHub token (`GH_TOKEN` wins if both are set). Without one, only public repositories can be read, and GitHub's low rate limit for anonymous requests applies. | | |
| 100 | -| `GITHUB_API_URL` | GitHub Enterprise Server API URL, for example `https://github.example.com/api/v3`. Defaults to `https://api.github.com`. | | |
| 101 | -| `DIFFINERY_TOKEN`, `DIFFINERY_URL` | Override the token and server saved by `diffinery login`. | | |
| 372 | +| `kind` | `repository` or `organization` | | |
| 373 | +| `parent_id` | For a repository of an organization import, the organization import's id | | |
| 374 | +| `status` | `draft`, `queued`, `running`, `succeeded`, `failed` or `cancelled` | | |
| 375 | +| `attempt` | 1, plus one per retry | | |
| 376 | +| `target.html_url` | Set once the repository exists | | |
| 377 | +| `steps` | Every step in order, with `status` (`pending`, `running`, `done`, `skipped`, `failed`), `message` and `count` | | |
| 378 | +| `git` | The Git transfer: `phase` (`starting`, `clone`, `lfs-fetch`, `push`, `refs`, `lfs-push`, `cleanup`, `done` or `failed`), `message`, history `chunks` and `bytes` pushed, and `tries` | | |
| 379 | +| `summary` | Filled in as steps finish: `counts` (`issues`, `pulls`, `comments`, `reviews`, `labels`, `milestones`), `git` (`branches`, `tags`, `pullRefs`, `lfsObjects`, `lfsBytes`, `defaultBranch` and more), `pullRefs.missing`, `protections`, `variables`, `secretNames`, `environments` (`imported`, `variables`, `secretNames`), `releases` (`imported`, `assets`, `skippedAssets`), `findings` (`kind`, `blob`, `path`), `people` (`linked`, `imported`), and for organization imports `orgVariables`, `orgSecretNames` and `children` counts | | |
| 380 | +| `warnings` | Things that didn't come across, in words | | |
| 381 | +| `error` | Why the import failed or who cancelled it | | |
| 382 | +| `concurrency`, `candidates`, `children` | Organization imports only: the concurrency, the repositories to choose from (drafts), and each repository import (`id`, `html_url`, `source`, `target`, `status`, the running `step`, `error`) | | |
| 102 | 383 | |
| 103 | -### GitHub token permissions | |
| 384 | +### Endpoints | |
| 104 | 385 | |
| 105 | -The GitHub token is used to clone the repository and read its metadata through GitHub's REST API. It is passed to Git as a one-off HTTP header and is never written to disk. | |
| 386 | +| Endpoint | Purpose | Response | | |
| 387 | +| --- | --- | --- | | |
| 388 | +| `POST /api/v3/diffinery/imports` | Start an import | `201` with the import | | |
| 389 | +| `GET /api/v3/diffinery/imports` | The imports you started (not the repositories of an organization import), newest first, up to 100 | `200` with a list | | |
| 390 | +| `GET /api/v3/diffinery/imports/{id}` | One import, with `children` for an organization import | `200`, or `404 import not found` | | |
| 391 | +| `POST /api/v3/diffinery/imports/{id}/cancel` | Cancel a draft, queued or running import (an organization import cancels its queued and running repositories) | `200` with the import | | |
| 392 | +| `POST /api/v3/diffinery/imports/{id}/retry` | Run a failed or cancelled import again; body `{"github_token": "ghp_..."}` (optional) | `202` with the import | | |
| 393 | +| `POST /api/v3/diffinery/imports/{id}/selection` | Choose the repositories of a draft organization import (created in the web form) and start it; body `{"repositories": ["website", "docs"]}` | `202` with the import | | |
| 106 | 394 | |
| 107 | -- **Classic token**: the `repo` scope. | |
| 108 | -- **Fine-grained token** for the repository, read-only access to: **Contents**, **Metadata**, **Issues** and **Pull requests** (for `--issues`), **Variables**, and **Secrets** (only names are read). | |
| 395 | +You can read an import if you started it, if you are a person who administers the account it imports into, or if you are a site administrator. The same people can cancel it. Only the person or agent who started it can retry it (see [Retries, cancelling and the repository lock](#retries-cancelling-and-the-repository-lock)). | |
| 109 | 396 | |
| 110 | -If the token can't read something optional (labels, issues, pull requests, comments, variables or secret names), the command prints a warning such as `could not read Actions variables from GitHub (403; the token may lack access)` and carries on without it. | |
| 397 | +Poll an import until it finishes: | |
| 111 | 398 | |
| 112 | -### What happens when you run it | |
| 399 | +```sh | |
| 400 | +while :; do | |
| 401 | + s=$(curl -fsS https://diffinery.com/api/v3/diffinery/imports/$ID -H "Authorization: Bearer dfy_..." | jq -r .status) | |
| 402 | + echo "$s"; case "$s" in succeeded|failed|cancelled) break;; esac; sleep 10 | |
| 403 | +done | |
| 404 | +``` | |
| 113 | 405 | |
| 114 | -1. Checks your Diffinery login (`GET /api/v3/user`) and reads the GitHub repository. | |
| 115 | -2. Creates the Diffinery repository, or prints `Using existing repository acme/website`. | |
| 116 | -3. Clones the GitHub repository with `git clone --mirror` into a temporary folder, which it deletes at the end. | |
| 117 | -4. Pushes all branches and tags in one `git push`. If that fails (for example because the push is over 100 MB or a ref is rejected), it prints a warning and pushes each ref separately, collecting the refs that still fail. | |
| 118 | -5. Sets the default branch, copies Actions variables and reads secret names. | |
| 119 | -6. With `--issues`: copies labels, then walks through every issue and pull request in number order, creating issues, recreating eligible open pull requests, copying comments and closing issues that were closed. | |
| 120 | -7. Prints a summary. | |
| 406 | +See the [REST API reference](/docs/api) for every endpoint. | |
| 121 | 407 | |
| 122 | -GitHub rate limits are handled: when GitHub asks it to wait, the command prints `GitHub rate limit reached; waiting 42s…` and retries. If the reset is more than an hour away, it stops with `GitHub rate limit: reset is N minutes away; try again later`. | |
| 408 | +## Import over MCP | |
| 123 | 409 | |
| 124 | -Pushes go through Diffinery's [push protection](/docs/security/push-protection) like any other push. Opening the recreated pull requests starts their `pull_request` workflows, and pushing the default branch starts its `push` workflows. | |
| 410 | +The [MCP server](/docs/agents/mcp) has three import tools: | |
| 125 | 411 | |
| 126 | -### Example output | |
| 412 | +| Tool | Read-only | What it does | | |
| 413 | +| --- | --- | --- | | |
| 414 | +| `start_github_import` | No | Start a single-repository import. Arguments: `source` and `owner` (required), `name`, `visibility` (`keep`, `private`, `public`), `github_token`, `github_api_url`. Returns the [import object](#the-import-object). | | |
| 415 | +| `get_import` | Yes | An import by `id`: status, steps, warnings, error and, for an organization import, its repositories | | |
| 416 | +| `list_imports` | Yes | The imports you started, newest first | | |
| 127 | 417 | |
| 128 | -```text | |
| 129 | -✓ Created acme/website (private) | |
| 130 | -Cloning https://github.com/acme/website … | |
| 131 | -Pushing 14 branches and 31 tags to acme/website … | |
| 132 | -✓ Copied 9 labels | |
| 133 | -Importing 120 issues and pull requests … | |
| 134 | - GitHub #1 → #1 Set up the build | |
| 135 | - GitHub #2 → #2 Add a contact form | |
| 136 | - ... | |
| 418 | +`start_github_import` imports with every option on. An agent's import never bypasses push protection and skips variables, secret names and environments (see [Agents and imports](#agents-and-imports)). `start_github_import` needs a `write` or `admin` token, like every tool that isn't read-only. | |
| 137 | 419 | |
| 138 | -Imported acme/website → acme/website | |
| 139 | - git: 14 branches, 31 tags | |
| 140 | - default: main | |
| 141 | - variables: 3 | |
| 142 | - labels: 9 | |
| 143 | - issues: 97 (412 comments) | |
| 144 | - pulls: 4 recreated, 2 skipped | |
| 145 | - #118: head branch is in a fork | |
| 146 | - #119: branch spike/search missing | |
| 420 | +```sh | |
| 421 | +curl -s https://diffinery.com/mcp \ | |
| 422 | + -H "Authorization: Bearer $DIFFINERY_TOKEN" -H "Content-Type: application/json" \ | |
| 423 | + -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"start_github_import","arguments":{"source":"acme/website","owner":"acme"}}}' | |
| 424 | +``` | |
| 147 | 425 | |
| 148 | -GitHub does not reveal secret values. Set these 2 secrets: | |
| 149 | - diffinery secret set NPM_TOKEN -R acme/website | |
| 150 | - diffinery secret set SENTRY_DSN -R acme/website | |
| 426 | +Then call `get_import` with the returned `id` every minute or so until `status` is `succeeded`, `failed` or `cancelled`. | |
| 151 | 427 | |
| 152 | -https://diffinery.com/acme/website | |
| 428 | +## What comes across | |
| 429 | + | |
| 430 | +| Data | What is imported | | |
| 431 | +| --- | --- | | |
| 432 | +| Repository | Description, homepage, topics (up to 20), default branch, archived state, and visibility as you chose | | |
| 433 | +| Branches and tags | Every branch and tag. GitHub merge queue branches (`gh-readonly-queue/*`), `refs/pull/<n>/merge`, notes and other refs are skipped. | | |
| 434 | +| Pull request heads | Each pull request's head commit, kept as `refs/pull/<n>/head`, also for merged and closed pull requests and pull requests from forks | | |
| 435 | +| Git LFS | Every LFS object GitHub serves for the repository | | |
| 436 | +| Issues | Number, title, body, author, assignees, labels, milestone, open or closed with its reason (`completed`, `not_planned`, `reopened`, `duplicate`), locked state and original dates | | |
| 437 | +| Pull requests | Number, title, body, author, base and head branch, head commit, draft state, open, closed or merged, merge commit, who merged it, requested reviewers (open pull requests), labels, milestone and original dates | | |
| 438 | +| Comments | Conversation comments on issues and pull requests, with author and dates | | |
| 439 | +| Reviews | Approve, request changes, comment and dismissed reviews with body, commit and date (pending reviews are skipped). They never count as approvals. | | |
| 440 | +| Review comments | Line comments with path, line, side, commit, review and reply threading | | |
| 441 | +| Labels | Name, color and description | | |
| 442 | +| Milestones | Number, title, description, state, due date, creator and dates | | |
| 443 | +| Branch protection | Classic rules (see [Branch protection rules](#branch-protection-rules)) | | |
| 444 | +| Actions variables | Repository variables with their values | | |
| 445 | +| Secrets | Names only: repository secrets and environment secrets (organization secrets for organization imports) | | |
| 446 | +| Environments | Name, deployment branch policy, custom branch patterns, and environment variables | | |
| 447 | +| Organization variables | For an organization imported into a Diffinery organization: variables the Diffinery organization doesn't have yet | | |
| 448 | +| Releases | Tag, target, name, notes, draft and pre-release state, author, dates, and assets up to 2 GiB each with their download counts | | |
| 449 | + | |
| 450 | +Issue and pull request numbers are kept, gaps and all. New issues and pull requests on Diffinery continue after the highest imported number, so links like `#123` in old text still point at the right place. | |
| 451 | + | |
| 452 | +### Branch protection rules | |
| 453 | + | |
| 454 | +Each protected branch's classic rule becomes a Diffinery rule for the same branch name: **Require a pull request before merging**, required approvals, dismissing stale approvals, required status checks, force pushes and deletions allowed, linear history, and whether rules apply to admins. Required checks are renamed to the names Diffinery's workflows report (`<workflow name> / <job name>`), matched by job, so a rule that required `build` on GitHub requires `CI / build` here. Check them under **Settings → Branch protection** after the import. Repository rulesets aren't imported. | |
| 455 | + | |
| 456 | +### Variables, secret names and environments | |
| 457 | + | |
| 458 | +- Repository variables are imported with their values. Names that start with `GITHUB_` or aren't valid variable names are skipped. | |
| 459 | +- Secret **values are never read and never copied**: GitHub doesn't reveal them, and the importer doesn't try. Only names are listed, in the summary and on the import's page, for an admin to set by hand. Environment secret names appear as `<environment>/<NAME>`. | |
| 460 | +- Environments are created with their branch policy and variables; their secrets are listed by name only. | |
| 461 | +- In an organization import into a Diffinery organization, the GitHub organization's variables are added to the Diffinery organization when it has no variable of that name. Existing ones are left alone (`Organization variable NPM_REGISTRY already exists and was left unchanged.`). Organization variables are shared by every repository of the organization; a repository variable of the same name wins. | |
| 462 | + | |
| 463 | +### Releases and assets | |
| 464 | + | |
| 465 | +Releases keep their tag, notes, dates and author. Importing a release creates no tag (the tags came with the Git data), starts no `release` workflows and sends no webhook. Assets are copied with their names, labels, content types and download counts; an asset with an unsafe file name, over 2 GiB, or on a host the server may not download from is skipped with a warning such as `Release v1.0.0: could not import asset "app.zip": ...`. | |
| 466 | + | |
| 467 | +## What doesn't come across | |
| 468 | + | |
| 469 | +- **Secret values** of any kind. Set them again yourself (see [Set secret values](#set-secret-values)). | |
| 470 | +- **Accounts.** No Diffinery accounts are created for GitHub users. See [People: linked accounts and mannequins](#people-linked-accounts-and-mannequins). | |
| 471 | +- **Reactions**, wikis, projects, discussions, GitHub Pages, packages, webhooks, deploy keys, collaborators and teams, repository rulesets, Actions run history, and GitHub Apps. | |
| 472 | +- **Required reviewers and wait timers** on environments. | |
| 473 | +- **Auto-merge** on open pull requests. Turn it on again on Diffinery (see [Auto-merge](/docs/collaboration/pull-requests#auto-merge)). | |
| 474 | +- **Pull request heads whose fork was deleted on GitHub.** The pull request is imported but has no head commit (`3 pull requests have no head commit (their fork was deleted on GitHub): #12, #15, #40`). | |
| 475 | +- **Anything a GitHub token couldn't read**, listed under **Warnings**. | |
| 476 | + | |
| 477 | +## People: linked accounts and mannequins | |
| 478 | + | |
| 479 | +No accounts are created during an import. Each GitHub user who appears in the imported data (authors, commenters, reviewers, assignees, requested reviewers, who merged) is either mapped to a Diffinery account or shown as a mannequin. | |
| 480 | + | |
| 481 | +### Mannequins | |
| 482 | + | |
| 483 | +A mannequin is a placeholder for a GitHub user who has no linked Diffinery account. It carries the history but isn't an account: it can't sign in or do anything on Diffinery, and its imported reviews never count as votes. | |
| 484 | + | |
| 485 | +| Where | How a mannequin appears | | |
| 486 | +| --- | --- | | |
| 487 | +| Web UI | The GitHub login with its picture and a **GitHub** badge, not linked, with the tooltip "Imported from GitHub; not linked to a Diffinery account" | | |
| 488 | +| REST API | A user object with `login` `github:<login>`, `type` `"Mannequin"`, and `html_url` and `url` pointing at the GitHub profile | | |
| 489 | +| Assignees, requested reviewers, merged by | Stored as `github:<login>` | | |
| 490 | +| Issue search | `author:github:diffy` finds the mannequin's issues; `author:diffy` finds only the Diffinery account `diffy` | | |
| 491 | +| Squash merges | A squash commit for a pull request a mannequin opened is authored as `diffy <diffy@users.noreply.github.com>`, never as a Diffinery address | | |
| 492 | + | |
| 493 | +A deleted GitHub user appears as `ghost`. | |
| 494 | + | |
| 495 | +### Link your GitHub account | |
| 496 | + | |
| 497 | +Linking proves that you control a GitHub account, so imports attribute that account's work to you. Link it **before** you import: links are applied as an import runs, and work already imported stays with the mannequin. | |
| 498 | + | |
| 499 | +In the web UI, go to **Settings → Linked accounts** (`https://diffinery.com/settings/linked-accounts`), the **Linked GitHub accounts** page. | |
| 500 | + | |
| 501 | +With a gist (no token needed): | |
| 502 | + | |
| 503 | +1. Under **Step 1 · get a code**, leave **GitHub Enterprise API URL** blank for github.com (or enter your Enterprise API URL) and click **Get my code**. The code looks like `diffinery-link-3f9a...`. It is the same every time for your account and that host. | |
| 504 | +2. On GitHub, create a **public** gist whose description or one of whose files contains the code. | |
| 505 | +3. Under **Step 2 · verify your gist**, enter **Your GitHub username on github.com** and click **Verify and link**. You see "GitHub account linked." | |
| 506 | + | |
| 507 | +Diffinery reads your public profile and public gists without any token, from the API host you named, and links the account only when it finds the code. It looks at up to 100 of your gists (and reads the contents of up to 20). You can delete the gist afterwards. | |
| 508 | + | |
| 509 | +With GitHub OAuth, when the server administrator configured a GitHub OAuth app (`GITHUB_OAUTH_CLIENT_ID` and `GITHUB_OAUTH_CLIENT_SECRET`): the page shows **Link with GitHub**. Click **Continue to GitHub**, authorize read-only access to your profile (`read:user`), and you come back linked. The OAuth flow links github.com accounts only; its state is bound to your account, so a callback can't be replayed for someone else. | |
| 510 | + | |
| 511 | +To remove a link, click **Unlink** next to it and confirm. Only people can link accounts (`only people can link GitHub accounts`); agents can't. | |
| 512 | + | |
| 513 | +With the REST API (a token with the `admin` scope that isn't restricted to repositories): | |
| 514 | + | |
| 515 | +```sh | |
| 516 | +# Step 1: get the code (api_url is optional; default github.com) | |
| 517 | +curl -X POST https://diffinery.com/api/v3/diffinery/user/linked-accounts/github/challenge \ | |
| 518 | + -H "Authorization: Bearer dfy_..." -H "content-type: application/json" -d '{}' | |
| 519 | +# {"nonce": "diffinery-link-...", "apiBase": "https://api.github.com", "host": "github.com", "gistsUrl": "https://api.github.com/gists"} | |
| 520 | + | |
| 521 | +# Step 2: after creating the public gist | |
| 522 | +curl -X POST https://diffinery.com/api/v3/diffinery/user/linked-accounts/github \ | |
| 523 | + -H "Authorization: Bearer dfy_..." -H "content-type: application/json" -d '{"login": "diffy"}' | |
| 524 | +# 201 {"id": "...", "host": "github.com", "login": "diffy"} | |
| 153 | 525 | ``` |
| 154 | 526 | |
| 155 | -The command exits with status 1 if any branch or tag failed to push, and 0 otherwise. With `--json` it prints an object with `repo`, `created`, `branches`, `tags`, `pushFailures`, `defaultBranch`, `labels`, `issues` and `pulls` (lists of `{from, to}` number pairs), `skippedPulls` (`{number, reason}`), `comments`, `variables`, `secrets` and `warnings`. | |
| 527 | +| Endpoint | Purpose | | |
| 528 | +| --- | --- | | |
| 529 | +| `GET /api/v3/diffinery/user/linked-accounts` | Your links: `id`, `host`, `apiBase`, `login`, `externalId` (GitHub's numeric user id), `createdAt` | | |
| 530 | +| `POST /api/v3/diffinery/user/linked-accounts/github/challenge` | Your code for a host; body `{"api_url": "..."}` (optional) | | |
| 531 | +| `POST /api/v3/diffinery/user/linked-accounts/github` | Verify the gist and link; body `{"login": "diffy", "api_url": "..."}` | | |
| 532 | +| `DELETE /api/v3/diffinery/user/linked-accounts/{id}` | Remove one of your links (`204`) | | |
| 533 | +| `DELETE /api/v3/diffinery/admin/linked-accounts/{id}` | Site administrators: remove anyone's link (`204`), recorded in the audit log as `account.link_cleared` | | |
| 156 | 534 | |
| 157 | -### Running it again | |
| 535 | +Linking and unlinking are audited as `account.link_github` and `account.unlink_github`. | |
| 158 | 536 | |
| 159 | -Running the command again for the same target pushes new commits and tags. Pushes are not forced, so a branch that changed on both sides fails to push and is listed in the summary. Variables are overwritten with GitHub's values. | |
| 537 | +### How links are matched | |
| 160 | 538 | |
| 161 | -> **Important:** `--issues` is not idempotent. A second run creates every issue again (labels are only updated). Import issues once, after you have checked the Git import. | |
| 539 | +- A link stores GitHub's **numeric user id**, not only the login, and imports match by that id. Renaming a GitHub account, or someone else taking an old login, can't move work to the wrong person. | |
| 540 | +- Links are kept per GitHub host (the full API base), so a github.com link and an Enterprise link are separate even if the logins match. | |
| 541 | +- One GitHub account can be linked to only one Diffinery account (`GitHub user diffy is linked to another Diffinery account`). One Diffinery account can link several GitHub accounts. | |
| 542 | +- Only links of active accounts are used. | |
| 162 | 543 | |
| 544 | +The summary lists which GitHub users were mapped (`people.linked`) and which became mannequins (`people.imported`). | |
| 545 | + | |
| 546 | +### Imported reviews never count as approvals | |
| 547 | + | |
| 548 | +Imported reviews are history, not votes. A review imported from GitHub never counts toward required approvals and never blocks a merge, even when its author is a linked account and even if it was an approval. A pull request still open after the import needs a fresh review on Diffinery to be approved. | |
| 549 | + | |
| 550 | +## Secrets in old history | |
| 551 | + | |
| 552 | +Imported history goes through [push protection](/docs/security/push-protection) like any push. When old commits contain something that looks like a credential, the `secrets` option decides: | |
| 553 | + | |
| 554 | +| Option | What happens | | |
| 555 | +| --- | --- | | |
| 556 | +| **Import with an audited bypass** (`bypass`, the default) | The history is imported anyway, because it is already on GitHub. The push carries the reason `Imported from GitHub acme/website`, is audited as `push.secret_bypassed` with the import and the blobs, and the import's page lists each finding under **Possible secrets in history (rotate these)** (`summary.findings`, with the file path where one was found). Rotate those credentials. | | |
| 557 | +| **Stop the import** (`stop`) | The import fails at the Git step so you can clean the history first. | | |
| 558 | + | |
| 559 | +A bypass works only for a person who is an admin of the target repository and started the import with an `admin`-scoped token or from the web. Otherwise (an agent, or a `write` token) the import fails with `Push protection found possible secrets in the history (AWS access key). Only a person who administers the target can import them with an audited bypass (agents and write-scoped tokens cannot); start a new import as such a person.` | |
| 560 | + | |
| 561 | +**Agents never bypass push protection.** An import started by an agent always uses `stop`, whatever it asks for. | |
| 562 | + | |
| 563 | +A retry keeps the original options, so it can't switch from `stop` to `bypass`. After a stop, rotate the credentials, then start a new import with `bypass` as an admin (into a new name, or after deleting the partly imported repository), or clean the history on GitHub first. | |
| 564 | + | |
| 565 | +## Limits | |
| 566 | + | |
| 567 | +| Limit | Value | | |
| 568 | +| --- | --- | | |
| 569 | +| Imports running at once | Per account that starts them and per target account: at most 3 single-repository imports (`too many repository imports are already running (at most 3 at a time); wait for one to finish`) and 1 organization import, drafts included (`an organization import is already in progress (at most 1 at a time); wait for it to finish or cancel it`). Repositories inside an organization import don't count toward the 3. | | |
| 570 | +| Repository size | 10 GB by default, as GitHub reports it; a single-repository import over it is refused up front with `the repository is about 12 GB, over the 10 GB import limit; ask an administrator to raise IMPORT_MAX_REPO_KB or split the repository`. Self-hosters set `IMPORT_MAX_REPO_KB`. | | |
| 571 | +| Git transfer | 6 hours. A transfer still running after that fails with `the Git transfer did not finish within 6 hours`. A failed transfer is tried a second time automatically. | | |
| 572 | +| Single Git object | 100 MB (the push limit). A larger blob can't be imported. | | |
| 573 | +| Repositories per organization import | 1,000 | | |
| 574 | +| Concurrency of an organization import | 1 to 8, default 3 | | |
| 575 | +| Organization import duration | The organization import waits up to a week for its repositories | | |
| 576 | +| Draft organization imports | Cancelled after 7 days (`draft expired`), and the GitHub token erased | | |
| 577 | +| Release asset | 2 GiB each | | |
| 578 | +| Warnings kept per import | 500 | | |
| 579 | + | |
| 580 | +## Retries, cancelling and the repository lock | |
| 581 | + | |
| 582 | +**Cancel** an import with **Cancel import** on its page (confirm "Cancel this import?"), or `POST /api/v3/diffinery/imports/{id}/cancel`. Its container is stopped, its credentials are revoked and the repository lock is released. Cancelling one repository of an organization import starts the next queued one. Cancelling a finished import fails with `the import already succeeded` (or `failed`, `cancelled`). | |
| 583 | + | |
| 584 | +**Retry** a failed or cancelled import with **Retry this import** on its page, or `POST /api/v3/diffinery/imports/{id}/retry`: | |
| 585 | + | |
| 586 | +- Only the person or agent who started the import can retry it. Others get `404 import not found`, even admins of the target. | |
| 587 | +- Give the GitHub token again for a private repository (or for LFS, variables and protection): the first run's token was deleted when it ended. Leave it out for a public repository. | |
| 588 | +- Retry the whole organization import, not one of its repositories (`retry the organization import this repository belongs to`). Its failed and cancelled repositories are retried; those that failed because the name already existed are not. | |
| 589 | +- The options can't change on a retry. | |
| 590 | +- Finished work isn't repeated or duplicated: steps write by GitHub id, and the Git transfer only sends what the repository doesn't have yet. | |
| 591 | +- An agent can't retry once the repository was created (`an agent cannot retry an import after the repository was created; a person must retry it`). Because retry is limited to whoever started the import, a person then starts a new import instead. | |
| 592 | + | |
| 593 | +**The repository lock.** From the moment the import creates the repository until it finishes, fails or is cancelled, only the import may push to it; any other push gets `repository is locked by an import in progress`. The import itself may only create refs or move refs it wrote. A retry claims the repository again only when it holds nothing the import didn't write. If anyone pushed a branch, opened an issue, added a label or a protection rule in the meantime, the retry fails with `repository already holds content not written by this import; start a new import`. A failed or cancelled import leaves the partly imported repository in place; delete it before importing again under the same name. | |
| 594 | + | |
| 595 | +## Agents and imports | |
| 596 | + | |
| 597 | +Agents can start imports over MCP, the REST API or the CLI, with these rules: | |
| 598 | + | |
| 599 | +- **Only into organizations**: the organization that owns the agent, or one where the agent's owner is an owner or admin. Otherwise `agents can import only into organizations they or their owner administer`. | |
| 600 | +- **The agent's maximum role must be at least `write`** (`this agent's policy does not allow writing; an import needs at least write`), and its token needs the `write` scope. | |
| 601 | +- **Never a push protection bypass.** The `secrets` option is forced to `stop`. | |
| 602 | +- **No variables, secret names or environments.** The **Variables and secret names** step, and an organization import's **Organization variables and secret names** step, are skipped with `skipped (agents cannot manage variables)`. A person imports or sets those afterwards. For people, the right to manage variables is checked again when that step runs. | |
| 603 | +- **No retry after the repository exists.** | |
| 604 | +- **No secret values**, ever. Agents can't set secrets anywhere; a person with admin access sets them. | |
| 605 | +- An agent sees and lists only the imports it started. | |
| 606 | + | |
| 607 | +Everything else (Git data, issues, pull requests, reviews, labels, milestones, branch protection, releases) is imported as for a person. See [Agents](/docs/agents/overview). | |
| 608 | + | |
| 609 | +## After the import | |
| 610 | + | |
| 611 | +### Set secret values | |
| 612 | + | |
| 613 | +The import lists secret names; set each value from where you keep it (a password manager, your cloud provider's console). Repository admins who are people can: | |
| 614 | + | |
| 615 | +- Click **Set secret values** on the import's page. It lists each repository secret name with a `set` or `empty` badge, a **Value** field and **Save**. | |
| 616 | +- Use the CLI, which prompts for the value without echoing it: | |
| 617 | + | |
| 618 | + ```sh | |
| 619 | + diffinery secret set NPM_TOKEN -R acme/website | |
| 620 | + printf '%s' "$NPM_TOKEN" | diffinery secret set NPM_TOKEN -R acme/website | |
| 621 | + diffinery secret set SENTRY_DSN --org acme # organization secret | |
| 622 | + ``` | |
| 623 | + | |
| 624 | +- Use the API (see [Move secrets and variables](#move-secrets-and-variables)). | |
| 625 | + | |
| 626 | +Environment secrets (listed as `production/DEPLOY_KEY`) are set with the environments API, as in [Environments and their secrets](#environments-and-their-secrets). | |
| 627 | + | |
| 628 | +### Check the rest | |
| 629 | + | |
| 630 | +1. Under **Settings → General**, check the default branch, description and visibility. | |
| 631 | +2. Under **Settings → Branch protection**, check the imported rules and their required check names. | |
| 632 | +3. Under **Settings → Collaborators**, add people, and invite your team to the organization. | |
| 633 | +4. Recreate webhooks under **Settings → Webhooks**. | |
| 634 | +5. Push a commit and watch the first workflow runs. The import itself started none. | |
| 635 | +6. If the repository landed under the wrong owner or name, [transfer](/docs/repositories/overview#transfer-a-repository) or [rename](/docs/repositories/overview#rename-a-repository) it once the import has finished (a transfer is refused while the import runs). The old Diffinery URL keeps working. | |
| 636 | +7. Agent settings in branch protection (**Allow agents to push directly**, **Allow agents to merge pull requests**, **Allow agents to land pull requests through auto-merge** and its approval waiver) never come across with an import; an admin sets them here. | |
| 637 | + | |
| 163 | 638 | ## Move with plain Git |
| 164 | 639 | |
| 165 | -If you only need the code, copy it with Git. Create an empty repository on Diffinery first (web: `https://diffinery.com/new`, or CLI), using the same default branch name as on GitHub: | |
| 640 | +If you only need the code, copy it with Git (2.26 or newer). Create an empty repository on Diffinery first (web: `https://diffinery.com/new`, or CLI), using the same default branch name as on GitHub: | |
| 166 | 641 | |
| 167 | 642 | ```sh |
| 168 | 643 | diffinery repo create website --org acme --private --default-branch main |
| @@ -198,7 +673,7 @@ | ||
| 198 | 673 | |
| 199 | 674 | ## Move releases |
| 200 | 675 | |
| 201 | -`diffinery migrate github` doesn't copy releases. Recreate them with the GitHub-compatible releases API once the tags are on Diffinery (after the Git import). This script uses GitHub's `gh` CLI to read each release and `curl` to recreate it, oldest first so the newest ends up as the latest release, and copies its assets: | |
| 676 | +The importer copies releases and their assets. If you moved the code with plain Git or `--local`, recreate releases with the GitHub-compatible releases API once the tags are on Diffinery. This script uses GitHub's `gh` CLI to read each release and `curl` to recreate it, oldest first so the newest ends up as the latest release, and copies its assets: | |
| 202 | 677 | |
| 203 | 678 | ```sh |
| 204 | 679 | export DIFFINERY_TOKEN=dfy_... # write scope or higher |
| @@ -229,16 +704,16 @@ | ||
| 229 | 704 | Things to know: |
| 230 | 705 | |
| 231 | 706 | - Push the tags first. If a release's tag is missing on Diffinery, publishing the release creates the tag on the default branch, which is probably not the commit you want. |
| 232 | -- Each published release starts the repository's `on: release` workflows, and webhooks get a `release` event. Disable or guard those workflows during the copy if they publish or deploy. | |
| 707 | +- Unlike imported releases, each release published this way starts the repository's `on: release` workflows, and webhooks get a `release` event. Disable or guard those workflows during the copy if they publish or deploy. | |
| 233 | 708 | - Release notes, titles, draft and pre-release state are copied. Authors and dates are not: the releases are created by you, now. |
| 234 | -- An asset can be up to 100 MB. Larger GitHub assets can't be copied. | |
| 709 | +- An asset uploaded through the API can be up to 100 MB. | |
| 235 | 710 | - Source code archives (**Source code (zip)** and **Source code (tar.gz)**) are not assets: Diffinery builds them from the tag. See [Source code archives](/docs/repositories/overview#source-code-archives). |
| 236 | 711 | |
| 237 | 712 | See [Releases](/docs/collaboration/releases) for the API in full. |
| 238 | 713 | |
| 239 | 714 | ## Move Git LFS files |
| 240 | 715 | |
| 241 | -Neither `git clone --mirror` nor `diffinery migrate github` copies Git LFS files, only the pointers to them. If the repository uses LFS, copy the files from a clone that can fetch them: | |
| 716 | +The importer copies Git LFS objects. Neither `git clone --mirror` nor `diffinery migrate github --local` does: they copy only the pointers. If you moved the code that way and the repository uses LFS, copy the files from a clone that can fetch them: | |
| 242 | 717 | |
| 243 | 718 | ```sh |
| 244 | 719 | git clone https://github.com/acme/website.git website-lfs |
| @@ -253,22 +728,9 @@ | ||
| 253 | 728 | |
| 254 | 729 | ### Secrets |
| 255 | 730 | |
| 256 | -GitHub never shows secret values, so you set them again on Diffinery from where you keep them (a password manager, your cloud provider's console). Get the list of names with `diffinery migrate github` (it prints them) or with GitHub's CLI (`gh secret list -R acme/website`). Then: | |
| 731 | +GitHub never shows secret values, so you set them again on Diffinery. Get the list of names from the import's summary, from `diffinery migrate github`'s output, or with GitHub's CLI (`gh secret list -R acme/website`). Then set them as in [Set secret values](#set-secret-values), or with the API (an admin role on the repository, and not an agent): | |
| 257 | 732 | |
| 258 | 733 | ```sh |
| 259 | -# Prompts for the value without echoing it | |
| 260 | -diffinery secret set NPM_TOKEN -R acme/website | |
| 261 | - | |
| 262 | -# Or pipe it in | |
| 263 | -printf '%s' "$NPM_TOKEN" | diffinery secret set NPM_TOKEN -R acme/website | |
| 264 | - | |
| 265 | -# Organization secret, available to every repository in acme | |
| 266 | -diffinery secret set SENTRY_DSN --org acme | |
| 267 | -``` | |
| 268 | - | |
| 269 | -Or with the API (an admin role on the repository, and not an agent): | |
| 270 | - | |
| 271 | -```sh | |
| 272 | 734 | curl -X PUT https://diffinery.com/api/v3/repos/acme/website/actions/secrets/NPM_TOKEN \ |
| 273 | 735 | -H "Authorization: Bearer dfy_..." -H "content-type: application/json" \ |
| 274 | 736 | -d '{"value": "npm_..."}' |
| @@ -280,10 +742,10 @@ | ||
| 280 | 742 | |
| 281 | 743 | ### Environments and their secrets |
| 282 | 744 | |
| 283 | -Diffinery has deployment environments with their own secrets, variables and branch policies, like GitHub. They have no page in the web UI yet: create them with the REST API, as a repository admin who is a person (agents can't manage environments, secrets or variables). | |
| 745 | +Diffinery has deployment environments with their own secrets, variables and branch policies, like GitHub. The importer creates them with their branch policies and variables. They have no page in the web UI yet: change them, and set their secrets, with the REST API, as a repository admin who is a person (agents can't manage environments, secrets or variables). | |
| 284 | 746 | |
| 285 | 747 | ```sh |
| 286 | -# Create the environment | |
| 748 | +# Create the environment (the importer already did if it came from GitHub) | |
| 287 | 749 | curl -X PUT https://diffinery.com/api/v3/repos/acme/website/environments/production \ |
| 288 | 750 | -H "Authorization: Bearer dfy_..." |
| 289 | 751 | |
| @@ -302,7 +764,7 @@ | ||
| 302 | 764 | |
| 303 | 765 | ### Variables |
| 304 | 766 | |
| 305 | -`diffinery migrate github` copies repository variables. To copy them yourself: | |
| 767 | +The importer and `diffinery migrate github --local` copy repository variables. To copy them yourself: | |
| 306 | 768 | |
| 307 | 769 | ```sh |
| 308 | 770 | gh variable list -R acme/website --json name,value --jq '.[] | [.name, .value] | @tsv' | |
| @@ -311,8 +773,16 @@ | ||
| 311 | 773 | done |
| 312 | 774 | ``` |
| 313 | 775 | |
| 314 | -Diffinery has repository and environment variables (see [Environments and their secrets](#environments-and-their-secrets)) but no organization variables: copy organization variables into each repository that uses them. See [Secrets and variables](/docs/actions/secrets-and-variables). | |
| 776 | +Organization variables are shared by every repository of a Diffinery organization; a repository variable of the same name wins. An organization import copies them. Owners and admins of the organization (people, with an `admin`-scoped token for changes) manage them with `GET` and `POST /api/v3/orgs/{org}/actions/variables` and `GET`, `PATCH` and `DELETE /api/v3/orgs/{org}/actions/variables/{name}`: | |
| 315 | 777 | |
| 778 | +```sh | |
| 779 | +curl -X POST https://diffinery.com/api/v3/orgs/acme/actions/variables \ | |
| 780 | + -H "Authorization: Bearer dfy_..." -H "content-type: application/json" \ | |
| 781 | + -d '{"name": "NPM_REGISTRY", "value": "https://registry.npmjs.org"}' | |
| 782 | +``` | |
| 783 | + | |
| 784 | +See [Secrets and variables](/docs/actions/secrets-and-variables). | |
| 785 | + | |
| 316 | 786 | ## Update remotes and local clones |
| 317 | 787 | |
| 318 | 788 | Everyone with a clone points it at Diffinery: |
| @@ -337,18 +807,20 @@ | ||
| 337 | 807 | |
| 338 | 808 | ## Workflows |
| 339 | 809 | |
| 340 | -Diffinery runs GitHub Actions workflows from `.github/workflows/` as they are: the same triggers, expressions, matrices, `needs`, job outputs, caches, artifacts, secrets, OIDC, `actions/checkout`, `actions/setup-node`, `setup-python`, `setup-go`, and remote JavaScript and composite actions. `runs-on: ubuntu-latest` runs on Diffinery's hosted runners. | |
| 810 | +Diffinery runs GitHub Actions workflows from `.github/workflows/` as they are: the same triggers, expressions, matrices, `needs`, job outputs, caches, artifacts, secrets, `GITHUB_TOKEN` permissions, OIDC, `actions/checkout`, `actions/setup-node`, `setup-python`, `setup-go`, `actions/github-script`, and remote JavaScript and composite actions. `runs-on: ubuntu-latest` runs on Diffinery's hosted runners. An import starts no workflow runs; the first push after it does. | |
| 341 | 811 | |
| 342 | 812 | Things to check after the move: |
| 343 | 813 | |
| 344 | 814 | | If a workflow... | Do this on Diffinery | |
| 345 | 815 | | --- | --- | |
| 346 | 816 | | Uses `runs-on: windows-*` or `macos-*` | Windows and macOS hosted runners are not supported yet. Run the job on a self-hosted runner (`diffinery runner`). See [Runners](/docs/actions/runners). | |
| 347 | -| Uses `actions/github-script` | Not supported yet. Replace it with a `run:` step that calls the REST API with `curl`. | | |
| 348 | -| Pushes commits or tags using `GITHUB_TOKEN` | The job token can read the repository but not push to it. Store a personal or agent token as a secret and use that. | | |
| 349 | -| Uses environments (`environment: production`) | Create the environment and its secrets and variables with the API (see [Environments and their secrets](#environments-and-their-secrets)). Required reviewers and wait timers are not supported yet. | | |
| 817 | +| Uses `actions/github-script` | Works with the REST API (`github.rest.*`) and the job's token. `github.graphql` doesn't: Diffinery has no GraphQL API yet. | | |
| 818 | +| Relies on `GITHUB_TOKEN` writing (comments, labels, statuses, releases, pushes) without a `permissions:` block | Repositories created on Diffinery, imported ones included, give the job token GitHub's restricted default, `contents` and `packages` read. Add a `permissions:` block (for example `contents: write`, `issues: write`, `pull-requests: write`) to the workflow or job, or have an admin set the repository default to write. Each endpoint needs its permission, as on GitHub; a missing one gets `403 Resource not accessible by integration`. See [Workflow permissions](/docs/actions/workflow-permissions). | | |
| 819 | +| Pushes commits or tags using `GITHUB_TOKEN` | Works with `permissions: contents: write`, as on GitHub, within branch protection. The token can't change files under `.github/workflows/`, and a push made with it starts no new `push` workflows. | | |
| 820 | +| Opens or approves pull requests using `GITHUB_TOKEN` | Refused until a repository admin allows it (`can_approve_pull_request_reviews` in the workflow permissions setting, off by default, as GitHub's **Allow GitHub Actions to create and approve pull requests**). Approvals from the token never count toward required approvals. | | |
| 821 | +| Uses environments (`environment: production`) | The importer creates the environments and their variables; set their secrets with the API (see [Environments and their secrets](#environments-and-their-secrets)). Required reviewers and wait timers are not supported yet. | | |
| 350 | 822 | | Runs on `release` or `repository_dispatch` | Works: publishing a release on Diffinery starts `on: release` workflows, and `POST /api/v3/repos/acme/website/dispatches` starts `on: repository_dispatch` workflows. | |
| 351 | -| Checks out Git LFS files (`lfs: true`) | Add a `git lfs pull` step after `actions/checkout`. See [Git LFS](/docs/repositories/git-lfs). | | |
| 823 | +| Checks out Git LFS files (`lfs: true`) | Works as on GitHub. See [Git LFS](/docs/repositories/git-lfs). | | |
| 352 | 824 | | Publishes images to `ghcr.io` | Use the registry at `diffinery.com` instead. See [Container registry](/docs/packages/container-registry). | |
| 353 | 825 | | Hard-codes `github.com` or `api.github.com` | Use `${{ github.server_url }}` (`https://diffinery.com`) and `${{ github.api_url }}` (`https://diffinery.com/api/v3`). | |
| 354 | 826 | | Should run only on one forge | Guard it with `if: github.server_url == 'https://github.com'` (or `'https://diffinery.com'`). | |
| @@ -366,7 +838,7 @@ | ||
| 366 | 838 | const { data } = await octokit.rest.repos.listReleases({ owner: "acme", repo: "website" }); |
| 367 | 839 | ``` |
| 368 | 840 | |
| 369 | -Inside workflows, `${{ github.api_url }}` is already `https://diffinery.com/api/v3`. Differences to check, including endpoints and features that are not supported yet (such as GraphQL), are listed in the [REST API reference](/docs/reference/rest-api). | |
| 841 | +Inside workflows, `${{ github.api_url }}` is already `https://diffinery.com/api/v3`. Imported people without a Diffinery account come back as user objects with `"type": "Mannequin"` and a `github:` login, so tools that look up users by login should expect them. Differences to check, including endpoints and features that are not supported yet (such as GraphQL), are listed in the [REST API reference](/docs/reference/rest-api). | |
| 370 | 842 | |
| 371 | 843 | ## Keep GitHub as upstream and mirror to Diffinery |
| 372 | 844 | |
| @@ -439,7 +911,7 @@ | ||
| 439 | 911 | |
| 440 | 912 | - The checkout fetches the full history, then every GitHub branch is pushed to the branch of the same name on Diffinery, with a leading `+` so GitHub always wins. Changes made directly on Diffinery to mirrored branches are overwritten. |
| 441 | 913 | - Branches deleted on GitHub are not deleted on Diffinery. Delete them there by hand (`git push https://diffinery.com/acme/website.git --delete old-branch`). |
| 442 | -- The secrets step sends each value straight from GitHub's secret store to Diffinery's over HTTPS; values are never printed. List every secret your workflows use. Variables can be copied the same way with `vars.NAME` and the `actions/variables` endpoint. | |
| 914 | +- The secrets step sends each value from GitHub's secret store, inside your own workflow, straight to Diffinery over HTTPS; values are never printed. List every secret your workflows use. Variables can be copied the same way with `vars.NAME` and the `actions/variables` endpoint. | |
| 443 | 915 | - The `if:` guard keeps the job from running on Diffinery, which also runs `.github/workflows/` files once they are mirrored. |
| 444 | 916 | - If push protection blocks a mirrored push, a repository admin can let it through with `-o allow-secrets=<reason>` on the `git push` line, for example from a `workflow_dispatch` input. Leave it empty normally. |
| 445 | 917 | |
| @@ -469,36 +941,89 @@ | ||
| 469 | 941 | |
| 470 | 942 | ## Checklist |
| 471 | 943 | |
| 472 | -1. Get a Diffinery account, create the organization, and create an `admin`-scoped token. | |
| 473 | -2. Import the repository: `diffinery migrate github acme/website --to acme/website` (add `--issues` once you are happy with the Git import), or push with plain Git. | |
| 474 | -3. Copy Git LFS files if the repository uses LFS. | |
| 475 | -4. Check the default branch, description and visibility under **Settings → General**. | |
| 476 | -5. Set every secret printed by the import (`diffinery secret set NAME -R acme/website`), and organization secrets with `--org`. | |
| 477 | -6. Check the variables under **Settings → Secrets and variables**, and recreate environments with their secrets and variables (see [Environments and their secrets](#environments-and-their-secrets)). | |
| 478 | -7. Recreate branch protection rules under **Settings → Branch protection**, using the check names Diffinery shows on pull requests (`<workflow name> / <job name>`). As on GitHub, a rule that names a branch exactly wins over wildcard rules, and otherwise the oldest matching rule applies. See [Branch protection](/docs/repositories/branch-protection). | |
| 944 | +1. Get a Diffinery account and create the organization. | |
| 945 | +2. Link your GitHub account under **Settings → Linked accounts**, and ask your team to link theirs, so their work is attributed to them. | |
| 946 | +3. Create a GitHub token that can read the repositories (admin access for branch protection, variables and secret names). | |
| 947 | +4. Import: **Settings → Imports → Import from GitHub**, `diffinery migrate github acme/website --to acme/website`, or `diffinery migrate github-org acme --to acme`. | |
| 948 | +5. Read the import's **Warnings** and rotate anything under **Possible secrets in history (rotate these)**. | |
| 949 | +6. Set every secret listed (**Set secret values**), environment secrets with the API, and organization secrets with `diffinery secret set NAME --org acme`. | |
| 950 | +7. Check the default branch, description and visibility under **Settings → General**, and the imported rules under **Settings → Branch protection** (required check names are `<workflow name> / <job name>`; see [Branch protection](/docs/repositories/branch-protection)). | |
| 479 | 951 | 8. Add collaborators under **Settings → Collaborators**, and invite your team to the organization. |
| 480 | 952 | 9. Recreate webhooks under **Settings → Webhooks**. Deliveries carry GitHub's `X-GitHub-Event` header and are signed with `X-Hub-Signature-256`, but check the payload fields your receiver uses. Failed deliveries (network errors, `408`, `429` and `5xx`) are retried up to five times with the same `X-GitHub-Delivery` ID, so make your receiver ignore a delivery ID it has already handled. See [Webhooks](/docs/repositories/webhooks). |
| 481 | -10. Copy releases (see [Move releases](#move-releases)) and set topics with `PUT /api/v3/repos/acme/website/topics` (see [Topics](/docs/repositories/overview#topics)). | |
| 482 | -11. If an outside CI system reported commit statuses or checks to GitHub, point it at `https://diffinery.com/api/v3` with a Diffinery token. Its status contexts and check names can be required checks in branch protection, as on GitHub. See [Checks and statuses](/docs/collaboration/checks-and-statuses). | |
| 483 | -12. Push a commit and watch the first workflow runs in the **Actions** tab (or `diffinery run list -R acme/website`). Fix anything from [Workflows](#workflows). | |
| 484 | -13. Create agent accounts for AI tools and connect them over MCP. See [Agents](/docs/agents/overview). | |
| 485 | -14. Update remotes on every clone, and URLs in scripts, manifests and READMEs. | |
| 486 | -15. When the team has moved, archive the GitHub repository or remove the mirror workflow. | |
| 953 | +10. If an outside CI system reported commit statuses or checks to GitHub, point it at `https://diffinery.com/api/v3` with a Diffinery token. Its status contexts and check names can be required checks in branch protection, as on GitHub. See [Checks and statuses](/docs/collaboration/checks-and-statuses). | |
| 954 | +11. Push a commit and watch the first workflow runs in the **Actions** tab (or `diffinery run list -R acme/website`). Fix anything from [Workflows](#workflows). | |
| 955 | +12. Create agent accounts for AI tools and connect them over MCP. See [Agents](/docs/agents/overview). | |
| 956 | +13. Update remotes on every clone, and URLs in scripts, manifests and READMEs. | |
| 957 | +14. When the team has moved, archive the GitHub repository or remove the mirror workflow. | |
| 487 | 958 | |
| 488 | 959 | ## Troubleshooting |
| 489 | 960 | |
| 961 | +### Starting an import | |
| 962 | + | |
| 490 | 963 | | Message | Cause and fix | |
| 491 | 964 | | --- | --- | |
| 492 | -| `not logged in: run ...` (naming `diffinery login` and `DIFFINERY_TOKEN`) | Log in to Diffinery first with `diffinery login`, or set `DIFFINERY_TOKEN`. | | |
| 493 | -| `GitHub /repos/acme/website: 404 Not Found` | The GitHub repository name is wrong, or it is private and `GH_TOKEN` is missing or can't see it. | | |
| 494 | -| `git clone --mirror failed: ...` | Git couldn't clone from GitHub. Check `GH_TOKEN` (it needs Contents read access) and that Git is installed. | | |
| 495 | -| `this token is limited to repositories and can't change account settings` | The Diffinery token is restricted to some repositories, so it can't create the target repository. Create an unrestricted `admin` token. | | |
| 496 | -| `agents cannot manage variables` or `agents may not push to protected or default branches` | You ran the import with an agent's token. Use a token of your own account. | | |
| 497 | -| `admin access required` | The Diffinery token is not `admin`-scoped, or you are not an admin of the target repository (organization members get write only). Use an `admin` token and ask an organization owner to make you an admin. | | |
| 965 | +| `GitHub repository acme/website was not found or the token cannot read it` | The name is wrong, or the repository is private and the GitHub token is missing or can't see it. | | |
| 966 | +| `GitHub rejected the token (expired or revoked?)` | Create a new GitHub token. | | |
| 967 | +| `no GitHub organization or user named acme` | Check the spelling, and `github_api_url` for Enterprise. | | |
| 968 | +| `acme has no repositories to import` | The account has none the token can see, or the filters excluded all of them. | | |
| 969 | +| `acme/website already exists; choose another name` | Pick another `name` (or **New repository name**), or delete the existing repository. | | |
| 970 | +| `you can import only into your own account or organizations you administer, not acme` | Ask an owner of `acme` to make you an admin, or to run the import. | | |
| 971 | +| `agents can import only into organizations they or their owner administer` | An agent can't import into a user account, or into an organization its owner doesn't administer. | | |
| 972 | +| `starting an import needs a token with write or admin scope` | Use a `write` or `admin` token. | | |
| 973 | +| `this token needs the write scope` or `this token is limited to repositories and can't change account settings` | Cancelling, retrying and choosing repositories need a `write` token that isn't restricted to repositories. | | |
| 974 | +| `too many repository imports are already running (at most 3 at a time); wait for one to finish` | Wait for one to finish, or cancel one. | | |
| 975 | +| `an organization import is already in progress (at most 1 at a time); wait for it to finish or cancel it` | Includes drafts waiting on **Choose repositories**. Finish or cancel it. | | |
| 976 | +| `the repository is about N GB, over the 10 GB import limit; ...` | Split the repository, or on your own instance raise `IMPORT_MAX_REPO_KB`. | | |
| 977 | +| `give either source (one repository) or source_owner (an organization or user)` | Send exactly one of them. | | |
| 978 | +| `github_api_url is required when a github_token is given and the source names a GitHub Enterprise host` | Add `github_api_url` for that host, or pass `owner/name` as the source. | | |
| 979 | +| `github_api_url must be on the same host as the source repository` | The API URL and the source URL name different hosts. | | |
| 980 | +| `GitHub rate limit reached; continuing in N seconds` | Shown on a step while the importer waits; it continues by itself. At start time it comes back as `429`; try again later or use a GitHub token. | | |
| 981 | + | |
| 982 | +### While an import runs | |
| 983 | + | |
| 984 | +| Message | Cause and fix | | |
| 985 | +| --- | --- | | |
| 986 | +| `Push protection found possible secrets in the history (...)` | See [Secrets in old history](#secrets-in-old-history). | | |
| 987 | +| `a single Git object is N MB, larger than the 100 MB push limit, so it cannot be imported. ...` | Remove the oversized file from history on GitHub (for example with `git filter-repo`), or move it to Git LFS, and import again. | | |
| 988 | +| `the Git transfer did not finish within 6 hours` | The repository is too large or GitHub was too slow. Retry, or split the repository. | | |
| 989 | +| `the import container stopped before finishing (exit code ...)` | The transfer is retried once automatically; after that, retry the import. | | |
| 990 | +| `The Git transfer failed and is being retried: ...` (warning) | The first transfer failed and a second one started. Nothing to do. | | |
| 991 | +| `repository is locked by an import in progress` | You pushed to a repository an import is still filling. Wait for the import to finish. | | |
| 992 | +| `repository already holds content not written by this import; start a new import` | Someone changed the repository between attempts. Start a new import under another name, or delete the repository first. | | |
| 993 | +| `skipped (agents cannot manage variables)` | An agent started the import. A person sets variables and secrets afterwards. | | |
| 994 | +| `skipped (you no longer administer the target)` | You lost admin rights on the target before the variables step ran. | | |
| 995 | +| `Branch protection of main could not be read ...` | The GitHub token lacks admin access. Set the rule up under **Settings → Branch protection**. | | |
| 996 | +| `Actions variables could not be read ...` (or `secrets`, `environments`) | Give the token admin access, or the fine-grained **Variables** and **Secrets** read permissions, and set the rest by hand. | | |
| 997 | +| `N pull requests have no head commit (their fork was deleted on GitHub): ...` | Those pull requests are imported without their commits. Nothing to do. | | |
| 998 | +| `the person or agent who started this import can no longer import into this account` | The starter lost the right to import there, or their account was disabled. | | |
| 999 | + | |
| 1000 | +### Retrying and cancelling | |
| 1001 | + | |
| 1002 | +| Message | Cause and fix | | |
| 1003 | +| --- | --- | | |
| 1004 | +| `import not found` | You didn't start the import (only the starter can retry), or you can't see it. | | |
| 1005 | +| `only failed or cancelled imports can be retried` | The import is still running or succeeded. | | |
| 1006 | +| `retry the organization import this repository belongs to` | Retry the parent import. | | |
| 1007 | +| `an agent cannot retry an import after the repository was created; a person must retry it` | A person starts a new import instead. | | |
| 1008 | +| `the import already succeeded` (or `failed`, `cancelled`) | Only drafts and queued or running imports can be cancelled. | | |
| 1009 | + | |
| 1010 | +### Linking a GitHub account | |
| 1011 | + | |
| 1012 | +| Message | Cause and fix | | |
| 1013 | +| --- | --- | | |
| 1014 | +| `create a PUBLIC gist whose description or a file contains diffinery-link-..., then try again` | The gist is missing, secret, or doesn't contain the exact code. | | |
| 1015 | +| `GitHub user diffy was not found` | Check the username and the host. | | |
| 1016 | +| `GitHub user diffy is linked to another Diffinery account` | Unlink it there first, or ask a site administrator to clear the link. | | |
| 1017 | +| `that is not a valid GitHub login` | Use the username, not an email address or URL. | | |
| 1018 | +| `invalid or expired OAuth state` | Start **Continue to GitHub** again from your own settings page. | | |
| 1019 | + | |
| 1020 | +### Errors from the local import | |
| 1021 | + | |
| 1022 | +| Message | Cause and fix | | |
| 1023 | +| --- | --- | | |
| 1024 | +| `git clone --mirror failed: ...` | Git couldn't clone from GitHub. Check `GH_TOKEN` and that Git is installed. | | |
| 498 | 1025 | | `warning: bulk push failed (...); pushing refs one at a time` | One push was too big or a ref was rejected. Check the refs listed as failed in the summary. | |
| 499 | -| A ref fails with `secret detected (push protection)` | Old commits contain something that looks like a credential. Rotate it, then push that ref with plain Git and `-o allow-secrets=<reason>` as a repository admin. See [Move with plain Git](#move-with-plain-git). | | |
| 500 | -| A ref fails with `push exceeds 100 MB` | Push that branch's history in steps, oldest first, as shown in [Move with plain Git](#move-with-plain-git). | | |
| 501 | -| A ref fails with `fetch first` or a non-fast-forward error | The branch already exists on Diffinery with other commits. Delete it there, or push it with `+` (force) using plain Git. | | |
| 502 | -| `could not read ... from GitHub (403; the token may lack access)` | The GitHub token can't read that data. Give it the permissions in [GitHub token permissions](#github-token-permissions), or copy that data by hand. | | |
| 503 | -| `GitHub rate limit: reset is N minutes away; try again later` (plus `or set GH_TOKEN` when no GitHub token is set) | Wait for the reset, or set `GH_TOKEN` to get a higher limit. | | |
| 504 | -| Workflows don't start after the import | Check the **Actions** tab for runs that failed to parse, and run `diffinery workflow validate` in the repository. | | |
| 1026 | +| A ref fails with `secret detected (push protection)` | Rotate the credential, then push that ref with plain Git and `-o allow-secrets=<reason>` as a repository admin, or use the server-side importer. | | |
| 1027 | +| A ref fails with `push exceeds 100 MB` | Push that branch's history in steps, as in [Move with plain Git](#move-with-plain-git), or use the server-side importer. | | |
| 1028 | +| `could not read ... from GitHub (403; the token may lack access)` | The GitHub token can't read that data. | | |
| 1029 | +| `agents cannot manage variables` or `agents may not push to protected or default branches` | `--local` needs a token of your own account, not an agent's. | |
modifiedapps/diffinery/docs/packages/container-registry.md+6−4View file
| @@ -160,6 +160,8 @@ | ||
| 160 | 160 | |
| 161 | 161 | When a linked repository is [transferred](/docs/repositories/overview#transfer-a-repository) to another owner, its packages move with it and keep following its access: `diffinery.com/acme/website` becomes `diffinery.com/acme-labs/website`. The old name keeps working for pulls and pushes, for anyone the moved package allows (everyone else gets `NAME_UNKNOWN`), until the old owner gets a repository of that name again. Images under a renamed owner's old login resolve the same way. Unlinked packages stay with their owner. |
| 162 | 162 | |
| 163 | +An image keeps its own name when it moves: transferring the repository under a new name, or renaming it, doesn't rename its images. Workflows that push to `diffinery.com/${{ github.repository }}` push to the new owner and name from then on, which creates a new package there on the first push. A redirect for an image name ends when the old owner creates a repository whose name is that image name (or its first part, for an image such as `acme/website/api`). A transfer is refused when the new owner already has an image with the name of one of the repository's images. | |
| 164 | + | |
| 163 | 165 | ## Permissions |
| 164 | 166 | |
| 165 | 167 | Each caller gets one of four roles on a package: |
| @@ -210,16 +212,16 @@ | ||
| 210 | 212 | |
| 211 | 213 | ### Workflows |
| 212 | 214 | |
| 213 | -A workflow's `GITHUB_TOKEN` follows the job's `packages` permission: | |
| 215 | +A workflow's `GITHUB_TOKEN` follows the `packages` permission of the job's effective permissions, the same ones the rest of the API checks (see [Workflow permissions](/docs/actions/workflow-permissions)): | |
| 214 | 216 | |
| 215 | 217 | | `permissions` in the workflow or job | `packages` permission | |
| 216 | 218 | | --- | --- | |
| 217 | -| No `permissions` anywhere | `write` | | |
| 219 | +| No `permissions` anywhere | The repository's default: `read` when the default is read-only (the default for new repositories), `write` when it is read and write | | |
| 218 | 220 | | `write-all` or `packages: write` | `write` | |
| 219 | 221 | | `read-all` or `packages: read` | `read` | |
| 220 | 222 | | A mapping without `packages`, or `packages: none` | none | |
| 221 | 223 | |
| 222 | -The job's `permissions` replace the workflow's. With that permission, the token gets these roles: | |
| 224 | +The job's `permissions` replace the workflow's. A job in a reusable workflow never gets more than the job that calls it, and a run for a pull request from another repository gets `read` at most. With that permission, the token gets these roles: | |
| 223 | 225 | |
| 224 | 226 | | Package | `read` | `write` | |
| 225 | 227 | | --- | --- | --- | |
| @@ -576,7 +578,7 @@ | ||
| 576 | 578 | | `denied: pushing to acme/tools is not allowed` | You can pull but not push. For an organization member, this happens on an unlinked package another member created: ask its creator or an organization owner to link it to a repository you can write to. | |
| 577 | 579 | | `denied: deleting from acme/website is not allowed` | Deleting needs the admin role, and an `admin`-scoped token. | |
| 578 | 580 | | `... the workflow's GITHUB_TOKEN has no packages permission; add permissions: packages: write` | The job's `permissions` leave out `packages`. Add `packages: write` (or `read` to pull). | |
| 579 | -| `... the workflow's GITHUB_TOKEN has packages: read; add permissions: packages: write to push` | Change the permission to `write`. | | |
| 581 | +| `... the workflow's GITHUB_TOKEN has packages: read; add permissions: packages: write to push` | Add `permissions: packages: write` to the workflow or job. Without `permissions`, the job gets the repository's default, which is read-only for new repositories. | | |
| 580 | 582 | | `... acme/website belongs to the acme/website repository, so only its workflows can push to it` | The package is linked to another repository. Push from that repository's workflows, or change the linked repository. | |
| 581 | 583 | | `... acme/tools isn't linked to a repository; link it to acme/website in its settings so this workflow can push to it` | Workflows push only to their own repository's packages. Link the package to the workflow's repository on the package page. | |
| 582 | 584 | | `... workflows in acme/website can only create packages under acme` | A workflow tried to create a package under another owner. Use a personal or agent token from a secret. | |
modifiedapps/diffinery/docs/quickstart.md+13−3View file
| @@ -89,7 +89,7 @@ | ||
| 89 | 89 | |
| 90 | 90 | ## 5. Push your code |
| 91 | 91 | |
| 92 | -To bring over a repository that lives on GitHub, with its branches, tags, variables and optionally its issues and open pull requests, use `diffinery migrate github` instead; see [Migrate from GitHub](/docs/migrate-from-github). | |
| 92 | +To bring over a repository that lives on GitHub, import it instead of pushing: **Settings → Imports → Import from GitHub** in the web UI, or `diffinery migrate github acme/website --to acme/website`. The import runs on the server and copies the history, issues and pull requests with their numbers, reviews, labels, releases, branch protection and variables (secret names only, never values); see [Migrate from GitHub](/docs/migrate-from-github). | |
| 93 | 93 | |
| 94 | 94 | From an existing project: |
| 95 | 95 | |
| @@ -113,7 +113,7 @@ | ||
| 113 | 113 | |
| 114 | 114 | Open `https://diffinery.com/acme/website` to see the files (with syntax highlighting) and the rendered README. |
| 115 | 115 | |
| 116 | -Every push is checked for leaked credentials. If you accidentally commit an API key or private key, the push is rejected with `Push blocked: possible secrets detected.` and nothing from it is stored: remove the secret from your commits and push again. See [Push protection](/docs/security/push-protection) and [Git over HTTPS](/docs/repositories/git). | |
| 116 | +Every push is checked for leaked credentials, and so is every commit made in the web editor, through the REST API or over MCP. If you accidentally commit an API key or private key, the push is rejected with `Push blocked: possible secrets detected.` and nothing from it is stored: remove the secret from your commits and push again. See [Push protection](/docs/security/push-protection) and [Git over HTTPS](/docs/repositories/git). | |
| 117 | 117 | |
| 118 | 118 | ## 6. Add a workflow and watch it run |
| 119 | 119 | |
| @@ -160,6 +160,16 @@ | ||
| 160 | 160 | |
| 161 | 161 | `runs-on: ubuntu-latest` runs in a fresh Ubuntu 24.04 container on Diffinery's hosted runners. The job reports a check named `CI / test` (workflow name, then job name) on the commit, which you can require in branch protection later. If you also run another CI system, it can report its results on the same commits as commit statuses or check runs through the GitHub-compatible REST API; see [Checks and statuses](/docs/collaboration/checks-and-statuses). If a workflow file has a syntax error, its run fails and shows the errors; `diffinery workflow validate` checks files before you push. See [Actions](/docs/actions/overview). |
| 162 | 162 | |
| 163 | +Each job gets a `GITHUB_TOKEN` that follows GitHub's `permissions:` model. In a new repository it can only read the code and packages (`contents: read`, `packages: read`), which is all the checkout above needs. A workflow that comments on pull requests, pushes or publishes releases has to ask for more, for example: | |
| 164 | + | |
| 165 | +```yaml | |
| 166 | +permissions: | |
| 167 | + contents: write | |
| 168 | + pull-requests: write | |
| 169 | +``` | |
| 170 | + | |
| 171 | +A repository admin can also change the default. See [Workflow permissions](/docs/actions/workflow-permissions). | |
| 172 | + | |
| 163 | 173 | ## 7. Open a pull request |
| 164 | 174 | |
| 165 | 175 | Make a change on a branch and push it: |
| @@ -248,7 +258,7 @@ | ||
| 248 | 258 | ## Next steps |
| 249 | 259 | |
| 250 | 260 | - [Concepts](/docs/concepts): accounts, roles, tokens, visibility, Actions and agents in one page. |
| 251 | -- [Migrate from GitHub](/docs/migrate-from-github): import a GitHub repository with `diffinery migrate github` (branches, tags, variables and, optionally, issues and open pull requests), then set its secrets again. | |
| 261 | +- [Migrate from GitHub](/docs/migrate-from-github): import a GitHub repository or organization from the web, the CLI, the API or MCP, then set its secrets again. | |
| 252 | 262 | - [Branch protection](/docs/repositories/branch-protection): require pull requests, reviews and checks, and [Checks and statuses](/docs/collaboration/checks-and-statuses) to report results from other CI systems. |
| 253 | 263 | - [Releases](/docs/collaboration/releases) and [Environments](/docs/actions/environments): ship versions and deploy them. |
| 254 | 264 | - [Organizations](/docs/accounts/organizations) and [Invitations](/docs/accounts/invitations): bring your team. |
modifiedapps/diffinery/docs/reference/limits.md+48−9View file
| @@ -1,11 +1,11 @@ | ||
| 1 | 1 | --- |
| 2 | 2 | title: Limits |
| 3 | -description: Every hard limit in Diffinery and where it applies: sign-in, tokens, API, search, archives, releases, git, pull requests, Actions, sandboxes, packages, email. | |
| 3 | +description: Every hard limit in Diffinery and where it applies: sign-in, tokens, API, search, archives, releases, git, Actions, sandboxes, packages, imports, email. | |
| 4 | 4 | --- |
| 5 | 5 | |
| 6 | 6 | This page lists the fixed limits built into Diffinery: sizes, counts, timeouts and lifetimes. Use it when a push, a workflow, an upload or an API call fails with a size or count error, or when you plan something large. Values come from the code of the running version. Where a limit comes from Cloudflare rather than from Diffinery, the table says so. |
| 7 | 7 | |
| 8 | -Units: 1 KiB is 1,024 bytes, 1 MiB is 1,048,576 bytes, and 1 GB in the cache row is 1,073,741,824 bytes. Sizes Diffinery checks itself and writes as "MB" (pushes, release assets, archives, profile pictures, forms) are in MiB: "100 MB" for pushes means 104,857,600 bytes. | |
| 8 | +Units: 1 KiB is 1,024 bytes, 1 MiB is 1,048,576 bytes, and 1 GB in the cache and artifact rows is 1,073,741,824 bytes. Sizes Diffinery checks itself and writes as "MB" (pushes, release assets, archives, profile pictures, forms) are in MiB: "100 MB" for pushes means 104,857,600 bytes. | |
| 9 | 9 | |
| 10 | 10 | ## Sign-in and sessions |
| 11 | 11 | |
| @@ -31,7 +31,7 @@ | ||
| 31 | 31 | | An agent's first token | 90 days, `write` scope | The token created together with an agent, unless you pass `token.expires_in_days` or `token.scopes`. | |
| 32 | 32 | | Expiry reminder email | 7 days before expiry | Sent once per token, and only for tokens that were valid for more than 8 days. | |
| 33 | 33 | | "Last used" accuracy | 1 hour | A token's last-used time is updated at most once an hour. | |
| 34 | -| Creating tokens through the API | A browser session | No token can create a token or an agent: `POST /api/v3/diffinery/tokens` and the agent endpoints return `403 creating tokens needs a browser session: sign in at https://diffinery.com/settings/tokens` for every token. | | |
| 34 | +| Creating tokens through the API | A browser session | No token can create a token or an agent: `POST /api/v3/diffinery/tokens` and the agent endpoints return `403 creating tokens needs a browser session: sign in at https://diffinery.com/settings/tokens` for every token. Each name in `repositories` must be a repository you can see, or the request fails with `422 unknown repository acme/website`. | | |
| 35 | 35 | | Sandbox token lifetime | 24 hours | A sandbox's git and API credentials stop working 24 hours after it starts, or earlier when it is closed or its creator loses access. | |
| 36 | 36 | | Token scopes | `read`, `write` or `admin`; default `["write"]` | A new token (personal or agent) without `scopes` gets `write`. An empty list or any other name returns `422 scopes must be a non-empty list of: read, write, admin`. Repeated names are stored once. | |
| 37 | 37 | | Writes with a `read` token | Refused | Every change returns `403 this token needs the write scope`. Git fetch and Git LFS downloads still work. | |
| @@ -172,16 +172,29 @@ | ||
| 172 | 172 | | Service container health check | 15 minutes | How long a job waits for a `services:` container's health check to pass. | |
| 173 | 173 | | Log line length | 16,384 characters | Longer lines are cut and end with `…`. | |
| 174 | 174 | | Log shown in the web UI | 20,000 lines | The job page shows the first 20,000 lines and links to the full log. | |
| 175 | +| Job log retention | 90 days | A finished job's log is stored for 90 days, then deleted. The run and its jobs stay listed. | | |
| 175 | 176 | | OIDC token lifetime | 10 minutes | ID tokens from `permissions: id-token: write`. | |
| 176 | 177 | |
| 177 | 178 | ## Caches and artifacts |
| 178 | 179 | |
| 180 | +These apply to `actions/cache` and `actions/upload-artifact` (and the matching built-ins) in hosted and self-hosted jobs. See [Artifacts and caches](/docs/reference/rest-api#actions) in the REST API reference for listing and deleting them. | |
| 181 | + | |
| 179 | 182 | | Limit | Value | Applies to | |
| 180 | 183 | | --- | --- | --- | |
| 181 | -| Cache storage per repository | 10 GB | When saving a cache pushes the total over 10 GB, the least recently used entries are deleted until it fits. | | |
| 182 | -| Cache key length | 512 characters | Longer keys fail with `cache key too long`. | | |
| 183 | -| Saving an existing cache key | Not allowed | Cache entries are immutable; saving a key that exists is skipped (`409 cache entry already exists`). | | |
| 184 | -| Cache entry and artifact size | 100 MB compressed | Each cache entry and each artifact is uploaded as one compressed archive in one request, and Cloudflare refuses request bodies over 100 MB. | | |
| 184 | +| Cache storage per repository | 10 GB | Every cache entry of the repository together, on all branches. When saving a cache would go over 10 GB, the least recently used entries are deleted until it fits. | | |
| 185 | +| Unused caches | Deleted after 7 days | An entry that no job restored for 7 days is deleted the next time a job saves a cache in the repository. | | |
| 186 | +| Cache entry size | 10 GB | A larger entry fails with `413 a cache entry may be at most 10737418240 bytes`. If caches still being uploaded by other jobs fill the budget, saving fails with `413 uploads in progress use the repository's whole cache budget`. | | |
| 187 | +| Cache key | 512 characters, no commas | Longer keys or keys with `,` fail with `400 invalid cache key`. Up to 20 restore keys per lookup. | | |
| 188 | +| Saving an existing cache key | Skipped | A key and version that already exist for the branch are not replaced; the save is skipped with a warning. | | |
| 189 | +| Which caches a job can restore | Its own branch, then the base branch, then the default branch | A pull request job also restores caches of its base branch, a merge queue job those of the target branch. | | |
| 190 | +| Artifact storage per repository | 10 GB by default | Artifacts that haven't expired plus uploads in progress. Past the quota, an upload fails with `413 the repository's artifact storage quota (N bytes) is used up; delete artifacts or wait for them to expire`. Operators change it with `ARTIFACT_QUOTA_BYTES` (see [Configuration](/docs/self-hosting/configuration#worker-variables)). | | |
| 191 | +| Artifact retention | 1 to 90 days, default 90 | `retention-days`. Values outside the range are clamped. | | |
| 192 | +| Expired artifacts | Listed for 90 more days | After its retention the content is deleted. The artifact stays in the API with `expired: true`, and downloading it returns `410 Artifact has expired`. | | |
| 193 | +| Artifact name | 256 characters, unique per run | Names can't contain `"`, `:`, `<`, `>`, `|`, `*`, `?`, `\`, `/` or line breaks. A second artifact with the same name in a run fails with `409 an artifact with this name already exists on the workflow run`. | | |
| 194 | +| Upload request size | 256 MiB, and Cloudflare's 100 MB request limit | `actions/cache` and `actions/upload-artifact` upload in blocks, so cache entries and artifacts can be larger than one request. On plans where Cloudflare refuses request bodies over 100 MB, that is the effective block size. | | |
| 195 | +| Blocks per upload | 50,000 | More fails with `Too many blocks.` | | |
| 196 | +| Download links | 1 hour, or 10 minutes from the REST API | The signed links a job gets to restore a cache or download an artifact, and the redirect from `GET .../actions/artifacts/{id}/zip`. | | |
| 197 | +| Runtime token | While the job runs | `ACTIONS_RUNTIME_TOKEN` works only while its job is in progress, and at most until 30 minutes after the job's timeout. | | |
| 185 | 198 | |
| 186 | 199 | ## Agent sandboxes |
| 187 | 200 | |
| @@ -206,6 +219,31 @@ | ||
| 206 | 219 | | Tags per tag-list page | 10,000 | `GET /v2/<owner>/<name>/tags/list?n=`. | |
| 207 | 220 | | Packages listed per owner | 1,000 | `GET /api/v3/users/{owner}/packages`, `/orgs/{owner}/packages`, `/user/packages`. | |
| 208 | 221 | |
| 222 | +## Imports | |
| 223 | + | |
| 224 | +Limits of [imports from GitHub](/docs/migrate-from-github), from the web form, `diffinery migrate github`, `POST /api/v3/diffinery/imports` and the MCP tool `start_github_import`. | |
| 225 | + | |
| 226 | +| Limit | Value | Applies to | | |
| 227 | +| --- | --- | --- | | |
| 228 | +| Imports at a time | 1 organization import and 3 repository imports | Counted per person or agent who starts them and per account imported into. Organization imports count while `draft`, `queued` or `running`; repository imports while `queued` or `running`, not counting the repositories inside an organization import. Over the limit: `429 an organization import is already in progress (at most 1 at a time); wait for it to finish or cancel it` or `429 too many repository imports are already running (at most 3 at a time); wait for one to finish`. | | |
| 229 | +| Repository size | 10 GB by default | GitHub's reported size of the repository in a one-repository import, checked before the import starts: `422 the repository is about N GB, over the M GB import limit; ask an administrator to raise IMPORT_MAX_REPO_KB or split the repository`. Operators change it with `IMPORT_MAX_REPO_KB` (see [Configuration](/docs/self-hosting/configuration#worker-variables)). The repositories of an organization import are not checked against it. | | |
| 230 | +| Repositories per organization import | 1,000 | The `repositories` list (`422 at most 1000 repositories per import`), and the repositories read from the GitHub organization or user. | | |
| 231 | +| Repositories imported at once | 1 to 8, default 3 | One organization import's `concurrency` (**Repositories at a time** in the form). `422 concurrency must be between 1 and 8`. | | |
| 232 | +| Git transfer | 6 hours | Copying the Git data in the import container. After that the import fails with `the Git transfer did not finish within 6 hours`. A transfer that fails for another reason is tried once more, except when push protection stopped it. | | |
| 233 | +| Size of each push in the transfer | 32 MiB | The import agent pushes history in chunks of at most this size, below the 100 MB push limit, and smaller ones if a push is still refused as too large. | | |
| 234 | +| Retries of each import step | 5, 15 seconds apart at first and backing off exponentially | Each step of the import's durable workflow, which may run for at most 15 minutes per call. | | |
| 235 | +| Calls per paged step | 2,000 | Steps that page through GitHub (issues, comments, reviews and the rest). Each call reads up to 10 pages of 100 items (30 pull requests for reviews). More fails the import with `the <step> step did not finish within 2000 pages`. | | |
| 236 | +| GitHub rate limits | Waits 5 seconds to 1 hour at a time | When GitHub answers with a rate limit, the import sleeps until it resets and continues; it doesn't fail. | | |
| 237 | +| GitHub responses | 8 MiB each, 30 seconds per request | A larger response fails with `GitHub response is too large` or `GitHub response exceeded the size limit`. Redirects are followed only on the same host. | | |
| 238 | +| Organization import duration | 1 week | An organization import waits up to 168 hours for its repositories. Any still unfinished then count as not imported, and the organization import fails. | | |
| 239 | +| Draft organization imports | 7 days | An organization import started in the web form whose repositories were never chosen is cancelled (`draft expired`) and its GitHub token erased. | | |
| 240 | +| Release assets | 2 GiB each | Larger assets, and assets that can't be downloaded, are skipped with a warning. | | |
| 241 | +| Warnings | 500 per import, 1,000 characters each | Further warnings are not recorded. | | |
| 242 | +| Imports listed | 100 | The most recent imports you started, in `GET /api/v3/diffinery/imports`, `list_imports` and the **Imports** page. | | |
| 243 | +| GitHub token | 400 characters, no spaces | `422 github_token is not a valid token`. Deleted when the import ends. | | |
| 244 | +| Push token of the transfer | 1 day, one repository | The token the import container pushes with. It works only for Git, Git LFS and the import's own agent endpoints, and is revoked when the transfer ends. | | |
| 245 | +| Copied repository details | 20 topics, 1,000 characters of description, 500 of homepage | Taken from the GitHub repository. | | |
| 246 | + | |
| 209 | 247 | ## Webhooks and events |
| 210 | 248 | |
| 211 | 249 | | Limit | Value | Applies to | |
| @@ -239,7 +277,8 @@ | ||
| 239 | 277 | | Worker CPU time | 300,000 ms per request | `limits.cpu_ms`. | |
| 240 | 278 | | Runner containers | `standard-2`, up to 100 instances | `containers[RunnerContainer]`. | |
| 241 | 279 | | Sandbox containers | `standard-1`, up to 50 instances | `containers[SandboxContainer]`. | |
| 242 | -| Container rollout grace period | 3,600 seconds | `rollout_active_grace_period` for both container classes. | | |
| 280 | +| Import containers | `standard-4`, up to 20 instances | `containers[ImportContainer]`: one container per Git transfer of an import. | | |
| 281 | +| Container rollout grace period | 3,600 seconds | `rollout_active_grace_period` for every container class. | | |
| 243 | 282 | | Event queue | Batches of up to 50 messages (2 seconds), 8 retries | `diffinery-events`, dead-letter queue `diffinery-events-dlq`. | |
| 244 | 283 | | Mail queue | Batches of up to 20 messages (2 seconds), 8 retries | `diffinery-mail`, dead-letter queue `diffinery-mail-dlq`. The consumer itself gives up after 6 attempts. | |
| 245 | -| Cron trigger | Every 5 minutes | `*/5 * * * *`: scheduled workflows, token expiry reminders, email log pruning and container registry cleanup. | | |
| 284 | +| Cron trigger | Every 5 minutes | `*/5 * * * *`: scheduled workflows, token expiry reminders, email log pruning, container registry cleanup and cancelling draft imports older than 7 days. | |
modifiedapps/diffinery/docs/reference/rest-api.md+121−27View file
| @@ -83,7 +83,8 @@ | ||
| 83 | 83 | | Users, organizations, agents, repositories | A positive integer derived from the account's or repository's own ID in Diffinery. It is stable, but nothing looks an account or repository up by it: use the login or `owner/repo` in paths. | |
| 84 | 84 | | Issues and pull requests | The `id` equals the `number` (issues and pull requests share one number sequence per repository). | |
| 85 | 85 | | Issue comments, review comments, reviews, check runs, releases, release assets, commit statuses, deployments, deployment statuses, reactions, issue events | Integers, as on GitHub. Use them in paths such as `/issues/comments/{id}` and `/check-runs/{id}`. | |
| 86 | -| Workflow runs and jobs | 32-character hexadecimal strings. Use them as they are in `/actions/runs/{id}` and `/actions/jobs/{id}`. | | |
| 86 | +| Workflow runs and jobs | 32-character hexadecimal strings. Use them as they are in `/actions/runs/{id}` and `/actions/jobs/{id}`. Getting a run, its jobs, logs and attempts, re-running it and listing its artifacts also accept the run's number (`run_number`, which is `github.run_id` inside a workflow): it picks the latest attempt, and for artifacts it covers every attempt. | | |
| 87 | +| Artifacts and cache entries | Integers, as on GitHub: `/actions/artifacts/{id}`, `/actions/caches/{id}`. An artifact's `workflow_run.id` is the run number. | | |
| 87 | 88 | | Workflows | The workflow's path, such as `.github/workflows/ci.yml`. `/actions/workflows/{id}` also accepts the file name, `ci.yml`. | |
| 88 | 89 | | Webhooks and tokens | Strings. | |
| 89 | 90 | |
| @@ -96,13 +97,26 @@ | ||
| 96 | 97 | | Pull requests | Mergeability spread over several calls | `GET /pulls/{n}` also returns `merge_blockers`, `approvals`, `required_approvals` and `checks`, so one call tells you why a pull request can't merge yet. `GET /pulls/{n}/merge` answers GitHub clients (an `Accept: application/vnd.github...` header) with `204` or `404`, and other clients with Diffinery's full mergeability report. | |
| 97 | 98 | | Draft pull requests | Marking ready for review or converting to draft is GraphQL only | `PATCH /repos/{owner}/{repo}/pulls/{n}` accepts `"draft": false` (ready for review) and `"draft": true` (back to draft). | |
| 98 | 99 | | Merge queue | GraphQL only | REST: `PUT` and `DELETE /repos/{owner}/{repo}/pulls/{n}/merge-queue`, `GET /repos/{owner}/{repo}/merge-queue/{branch}`. See [Merge queue](/docs/collaboration/merge-queue). | |
| 100 | +| Auto-merge | GraphQL only (`enablePullRequestAutoMerge`) | REST: `PUT` and `DELETE /repos/{owner}/{repo}/pulls/{n}/auto-merge`. The pull request's `auto_merge` field is filled in while it is on. See [Auto-merge](/docs/collaboration/pull-requests#auto-merge). | | |
| 99 | 101 | | Text filter on issue lists | Not on the list endpoints | A `q` parameter on `GET /repos/{owner}/{repo}/issues` and `/pulls` matches text in the title and body. | |
| 100 | 102 | | Webhook secret | You always choose it | If you create a webhook without `config.secret`, Diffinery generates one and returns it once as `secret` in the response. | |
| 101 | 103 | | Release asset upload host | `uploads.github.com` | The same host as the API, under `/api/uploads`. Use the release's `upload_url` and clients find it. | |
| 102 | 104 | | Required checks | Decided by GitHub Apps and statuses | When Diffinery Actions reports a check, it alone decides it; other reporters can be bound per check. See [Commit statuses, checks and required checks](#commit-statuses-checks-and-required-checks). | |
| 103 | 105 | | Rate limits | Enforced | Not enforced; a constant budget is reported. See [Rate limits](#rate-limits). | |
| 104 | 106 | | CORS | Allowed for some endpoints | No CORS headers. Call the API from a server, a script, a CI job or the CLI, not from JavaScript on another website. | |
| 107 | +| People imported from GitHub | Every author is a GitHub account | In a repository [imported from GitHub](/docs/migrate-from-github), people without a linked Diffinery account are mannequins: a user object with `login` `github:<login>`, `type` `Mannequin`, and `html_url` and `url` pointing at their GitHub profile. They can't sign in, and their imported reviews never count as approvals. Search for them with `author:github:<login>`. | | |
| 105 | 108 | |
| 109 | +### Transferred and renamed repositories | |
| 110 | + | |
| 111 | +After a repository is [transferred](/docs/repositories/overview#transfer-a-repository) or renamed, requests that name it by its old owner and name in the path keep working, as on GitHub, for anyone who can read it: | |
| 112 | + | |
| 113 | +- `GET` and `HEAD` get `301 Moved Permanently`; every other method gets `307 Temporary Redirect`, so a client that follows it repeats the method and body. | |
| 114 | +- `Location` holds the same URL with the new owner and name (the query string is kept), and the body is `{"message": "Moved Permanently", "url": "<the new URL>", "documentation_url": "..."}`. | |
| 115 | +- Callers who can't read the repository get `404` (or `401` without credentials), the same answer as for the repository under its new name. | |
| 116 | +- Old names that appear elsewhere than in the path (for example `repositories` when creating a token restricted to some repositories) are accepted as the repository they now lead to. | |
| 117 | + | |
| 118 | +`curl` follows redirects with `-L`, and Octokit follows them by itself. The redirect lasts until a repository is created at the old owner and name. Update stored owner and repository names when you see one, and use the repository's numeric `id`, which never changes, to recognize it. | |
| 119 | + | |
| 106 | 120 | ## Authentication |
| 107 | 121 | |
| 108 | 122 | Most endpoints need you to be signed in. Scripts, CI jobs, agents and the CLI authenticate with a personal access token or an agent token. Tokens start with `dfy_`. Create one under **Settings → Access tokens** (`/settings/tokens`). No token can create another token, so `POST /api/v3/diffinery/tokens` needs a browser session (see [Personal access tokens](/docs/accounts/tokens)). |
| @@ -122,7 +136,7 @@ | ||
| 122 | 136 | curl -u diffy:dfy_... https://diffinery.com/api/v3/user |
| 123 | 137 | ``` |
| 124 | 138 | |
| 125 | -Inside a workflow job, the job's `GITHUB_TOKEN` can read its own repository through the API, with the read role. | |
| 139 | +Inside a workflow job, the job's `GITHUB_TOKEN` works with the API too, with the permissions of the job's `permissions:` rather than token scopes. See [Workflow job tokens](#workflow-job-tokens). | |
| 126 | 140 | |
| 127 | 141 | Without an `Authorization` header and without a browser session, you are anonymous. Anonymous callers can read public repositories (and `GET /api/v3`, `/api/v3/meta`, `/rate_limit` and search over public repositories); most other endpoints return `401`. As on GitHub, anonymous callers never change anything: every repository request other than `GET` or `HEAD` (opening an issue, commenting, reacting, watching and so on) gets `401 authentication required`, even on a public repository. Anonymous `git clone` and `git fetch` of public repositories and Git LFS downloads keep working, because they only read. |
| 128 | 142 | |
| @@ -133,12 +147,15 @@ | ||
| 133 | 147 | | `401` | `invalid, expired or revoked token` | The `dfy_` token is unknown, past its expiry date, revoked, or its account is disabled or not yet active. Create a new token. | |
| 134 | 148 | | `401` | `unrecognised token; create one under Settings → Access tokens` | The credential does not start with `dfy_`. GitHub tokens (`ghp_...`) do not work here. | |
| 135 | 149 | | `401` | `authentication required` | You called a private resource anonymously, or tried to change something in a public repository without a token. Add the `Authorization` header. | |
| 150 | +| `401` | `malformed basic credentials` | The `Basic` value isn't valid base64. Encode `user:token` with base64. | | |
| 136 | 151 | | `401` | `sign in required` | The endpoint needs an account (for example `GET /user`) and you sent no credentials. | |
| 137 | 152 | | `403` | `your account is waiting for an invite` | Your account exists but no one has invited you yet. Ask an administrator for an invite. | |
| 138 | 153 | | `403` | `your account is disabled` | A site administrator disabled the account. | |
| 139 | 154 | |
| 140 | 155 | Every `401` response carries `WWW-Authenticate: Basic realm="Diffinery", charset="UTF-8"`, which makes git and other HTTP clients prompt for credentials. |
| 141 | 156 | |
| 157 | +If a request carries the `Authorization` header twice (git-lfs does this when `http.extraheader` is set), Diffinery uses the first one. | |
| 158 | + | |
| 142 | 159 | ### Token scopes |
| 143 | 160 | |
| 144 | 161 | A token has one or more scopes. The scope caps what the token can do in each repository, on top of the role its account has there: |
| @@ -147,13 +164,13 @@ | ||
| 147 | 164 | | --- | --- | --- | |
| 148 | 165 | | `read` | read | Clone, fetch, read issues, pull requests, releases and runs. Never changes anything. | |
| 149 | 166 | | `write` (the default) | maintain | Push, open and merge pull requests, comment, create releases, report statuses and check runs, rerun workflows. | |
| 150 | -| `admin` | admin | Everything the account can do, including repository settings, secrets, environments, webhooks, branch protection and creating more tokens. | | |
| 167 | +| `admin` | admin | Everything the account can do, including repository settings, secrets, environments, webhooks, branch protection and revoking tokens. No token can create tokens or agents. | | |
| 151 | 168 | |
| 152 | 169 | The effective role is the lower of the account's role and the token's cap. A `write` token for someone who only has read access to a repository still only reads; an `admin` token for a repository writer still only writes. See [Permissions](/docs/accounts/permissions) for what each role allows. |
| 153 | 170 | |
| 154 | -A `read` token never writes, even where the `read` role could (opening issues, commenting, reacting): every repository request other than `GET` or `HEAD` gets `403 this token needs the write scope`. `git fetch` and Git LFS downloads, which use `POST` but only read, are the exceptions. MCP tools that aren't read-only also need a `write` token. | |
| 171 | +A `read` token never writes, even where the `read` role could (opening issues, commenting, reacting): every repository request other than `GET` or `HEAD` gets `403 this token needs the write scope`. `git fetch` and Git LFS downloads, which use `POST` but only read, are the exceptions. MCP tools that aren't read-only also need a `write` token. A write to a private repository the token can't see is `404 repository not found`, not a scope error, so a `read` token can't find out that the repository exists. | |
| 155 | 172 | |
| 156 | -A token can also be restricted to a list of repositories. Requests for any other repository then behave as if the repository does not exist (`404`). | |
| 173 | +A token can also be restricted to a list of repositories. Requests for any other repository then behave as if the repository does not exist (`404`). The list may only name repositories the account can see; any other name is refused with `422 unknown repository owner/name`, whether or not it exists. | |
| 157 | 174 | |
| 158 | 175 | When you create a token (`POST /api/v3/diffinery/tokens` or `POST /api/v3/diffinery/agents/{login}/tokens`) or an agent with a first token (`token.scopes` on `POST /api/v3/diffinery/agents`), `scopes` must be a non-empty list of `read`, `write` and `admin`. Leave it out for `["write"]`. An empty list, a value that isn't a list or an unknown name is refused with `422 scopes must be a non-empty list of: read, write, admin`. |
| 159 | 176 | |
| @@ -167,12 +184,17 @@ | ||
| 167 | 184 | | `PUT` and `DELETE /api/v3/user/avatar` and `/api/v3/orgs/{org}/avatar` | A `write` or `admin` token that is not limited to repositories (`403 changing a profile picture needs a write or admin token that is not limited to repositories`). | |
| 168 | 185 | | `POST /api/v3/diffinery/tokens` | A browser session. Every token gets `403 creating tokens needs a browser session: sign in at https://diffinery.com/settings/tokens`. | |
| 169 | 186 | | `DELETE /api/v3/diffinery/tokens/{id}` | An `admin` token not restricted to repositories. | |
| 170 | -| `POST /api/v3/user/orgs` | An `admin` token not restricted to repositories. | | |
| 187 | +| `POST /api/v3/user/orgs`, `PATCH /api/v3/orgs/{org}` | An `admin` token not restricted to repositories. | | |
| 171 | 188 | | `POST /api/v3/orgs/{org}/invitations`, `POST /api/v3/invitations` | An `admin` token not restricted to repositories. | |
| 172 | 189 | | `GET`, `PUT` and `DELETE /api/v3/orgs/{org}/actions/secrets[/{name}]` | An `admin` token not restricted to repositories. | |
| 173 | 190 | | `POST /api/v3/diffinery/agents`, `POST /api/v3/diffinery/agents/{login}/tokens` | A browser session (they mint tokens). Every token gets `403 creating tokens needs a browser session: sign in at https://diffinery.com/settings/tokens`. | |
| 174 | 191 | | `DELETE /api/v3/diffinery/agents/{login}` | An `admin` token not restricted to repositories. | |
| 175 | 192 | | `POST /api/v3/diffinery/admin/emails/samples` | A site administrator with an `admin` token not restricted to repositories. | |
| 193 | +| `POST`, `PATCH` and `DELETE /api/v3/orgs/{org}/actions/variables[/{name}]` | An `admin` token not restricted to repositories (reading them needs only an organization owner or admin). | | |
| 194 | +| `POST /api/v3/diffinery/imports` | A `write` or `admin` token (`403 starting an import needs a token with write or admin scope`). An import that should bypass push protection for old secrets needs a person's `admin` token or browser session. | | |
| 195 | +| `POST /api/v3/diffinery/imports/{id}/cancel`, `/retry` and `/selection` | A `write` or `admin` token not restricted to repositories. | | |
| 196 | +| `POST /api/v3/diffinery/user/linked-accounts/github/challenge`, `POST /api/v3/diffinery/user/linked-accounts/github`, `DELETE /api/v3/diffinery/user/linked-accounts/{id}` | An `admin` token not restricted to repositories. | | |
| 197 | +| `DELETE /api/v3/diffinery/admin/linked-accounts/{id}` | A site administrator with an `admin` token not restricted to repositories. | | |
| 176 | 198 | |
| 177 | 199 | A token whose scope is too low for one of these gets `403 this token needs the write scope` or `403 this token needs the admin scope`. |
| 178 | 200 | |
| @@ -193,6 +215,21 @@ | ||
| 193 | 215 | |
| 194 | 216 | Machines should always use tokens, never browser cookies. Passkeys and browser sessions can only be managed in the web UI; tokens can't add or remove passkeys, and they can't create tokens or agents. Web forms (`/settings/...`, repository settings and the other pages' buttons) refuse requests that carry a token with `403 web forms need a browser session; use the API with a token`, so a token's scope and repository limits always apply. |
| 195 | 217 | |
| 218 | +### Workflow job tokens | |
| 219 | + | |
| 220 | +A workflow job's `GITHUB_TOKEN` (`secrets.GITHUB_TOKEN`, or `github.token`) authenticates to the REST API at `github.api_url`, to git and to the container registry while the job runs, and stops working when the job ends. It follows GitHub's permission model, not the scopes of personal access tokens: | |
| 221 | + | |
| 222 | +- **Its permissions** come from the job's `permissions:`, else the workflow's, else the repository default. The default is a repository setting (`default_workflow_permissions` in `GET` and `PUT /repos/{owner}/{repo}/actions/permissions/workflow`, admins only): `read` gives `contents` and `packages` read and nothing else, `write` gives read and write on every scope except `id-token`. New repositories start with `read`; repositories created before the setting existed keep `write`. A reusable workflow never gets more than its caller, and a run for a pull request from another repository only reads. | |
| 223 | +- **Each endpoint needs its permission**: `issues` for issues and labels, `pull-requests` for pull requests and reviews, `contents` for code, commits, branches, releases and merges, `statuses`, `checks`, `deployments` (also environments), `actions` for runs, artifacts and caches, and so on. Without it: `403 Resource not accessible by integration (the workflow's GITHUB_TOKEN needs permissions: issues or pull-requests: write)`. Reading a public repository works whatever the permissions. | |
| 224 | +- **Out of reach:** repository settings, collaborators, webhooks, secrets, variables, runners, branch protection, environment changes and the Actions permissions setting (`403 this endpoint is not available to a workflow's GITHUB_TOKEN`). The token's role never goes above write, so branch protection always applies to it. | |
| 225 | +- **Workflow files:** a job token never creates, changes or deletes files under `.github/workflows/` or `.diffinery/workflows/`, by push, API commit or merge: `refusing to allow a GitHub App to create or update workflow <path> without workflows permission`. | |
| 226 | +- **Pull requests:** it can open pull requests and approve or request changes only when the repository allows it (`can_approve_pull_request_reviews`); otherwise `403 GitHub Actions is not permitted to create pull requests` (or `approve`). Its approvals never count toward required approvals. It can't re-run workflows. | |
| 227 | +- **Other repositories:** it reads public repositories (to clone a public submodule or download a public repository's artifacts) and gets `404` for other private ones. | |
| 228 | +- **Agents:** a run whose code came from an agent (an agent started it, pushed the branch last or opened the pull request, and re-runs of such runs and `workflow_run` runs that follow them) acts as an agent, so the agent guardrails hold for its token. | |
| 229 | +- **No loops:** what a job does with its token starts no new workflow runs, except `workflow_dispatch` and `repository_dispatch`. | |
| 230 | + | |
| 231 | +See [Workflow permissions](/docs/actions/workflow-permissions) for the full model and examples. | |
| 232 | + | |
| 196 | 233 | ## Read-your-writes and the x-d1-bookmark header |
| 197 | 234 | |
| 198 | 235 | Account-level data (accounts, organizations, memberships, tokens, the list of repositories and who can access them, invitations, notifications, the audit log, the package index and email settings) can be served from a copy that briefly lags behind the latest write. Every response that read or wrote this data carries a bookmark header that marks how new the data it saw was: |
| @@ -246,6 +283,7 @@ | ||
| 246 | 283 | | `GET /repos/{owner}/{repo}/zipball/{ref}`, `/tarball/{ref}` | `application/zip` or `application/gzip`, as a download (see [Archives](#archives)) | |
| 247 | 284 | | `GET /repos/{owner}/{repo}/releases/assets/{id}` with `Accept: application/octet-stream` | The asset's bytes, as a download | |
| 248 | 285 | | `GET /repos/{owner}/{repo}/actions/runs/{id}/logs` | `application/zip`, one text file per job | |
| 286 | +| `GET /repos/{owner}/{repo}/actions/artifacts/{id}/zip` | A `302` redirect to a signed URL, valid for 10 minutes, that serves the artifact (a zip, or the file itself for an artifact uploaded unzipped) | | |
| 249 | 287 | | `GET /repos/{owner}/{repo}/actions/jobs/{id}/logs` | `text/plain`, one line per log line prefixed with its timestamp. Add `?format=json` for an array of `{t, step, text}` objects. | |
| 250 | 288 | | `DELETE` endpoints and some `PUT` endpoints | No body (`204`) | |
| 251 | 289 | |
| @@ -288,22 +326,26 @@ | ||
| 288 | 326 | | --- | --- | --- | |
| 289 | 327 | | `200 OK` | Success with a body. Also returned when you add a reaction you had already added. | | |
| 290 | 328 | | `201 Created` | Something was created: a repository, issue, comment, pull request, review, release, asset, status, check run, deployment, token, webhook, workflow run, sandbox, commit, ref. Also returned by `PUT .../merge-queue` when the pull request joined the queue, and by `PUT .../environments/{env}/secrets/{name}` for a new secret. | | |
| 291 | -| `202 Accepted` | Work started in the background: `POST .../actions/runs/{id}/cancel`, `PUT .../pulls/{n}/update-branch`. | | | |
| 329 | +| `202 Accepted` | Work started in the background: `POST .../actions/runs/{id}/cancel`, `PUT .../pulls/{n}/update-branch`. Also returned by `POST /repos/{owner}/{repo}/transfer` and `DELETE /orgs/{org}`, which have finished when they answer. | | | |
| 292 | 330 | | `204 No Content` | Success without a body (deletes, settings changes, `POST .../dispatches`). `POST .../merges` also returns `204` when there was nothing to merge, and `POST .../actions/runners/claim` when no job is waiting. | | |
| 331 | +| `301 Moved Permanently` | A `GET` or `HEAD` named a repository by an old owner or name, after a [transfer or rename](#transferred-and-renamed-repositories). `Location` (and `url` in the body) is the same request under the new name. | `Moved Permanently` | | |
| 332 | +| `302 Found` | `GET .../actions/artifacts/{id}/zip` redirects to a signed download URL. | | | |
| 333 | +| `307 Temporary Redirect` | Any other method on a transferred or renamed repository's old name. Repeat the request, with its method and body, at `Location`. | `Moved Permanently` | | |
| 293 | 334 | | `205 Reset Content` | `PUT /api/v3/notifications`: every notification marked as read. | | |
| 294 | 335 | | `400 Bad Request` | The body is not valid JSON, or the URL has a malformed percent-encoding. | `request body must be JSON`, `Problems parsing JSON`, `malformed percent-encoding in the URL` | |
| 295 | 336 | | `401 Unauthorized` | No credentials where they are needed, or a bad token. | See [Authentication](#authentication). | |
| 296 | -| `403 Forbidden` | You are signed in but your role, token scope or account kind doesn't allow this. | `write access required`, `admin access required`, `this token needs the write scope`, `this token is limited to repositories and can't change account settings`, `repository is archived (read-only)`, `agents cannot change branch protection`, `agents cannot manage secrets`, `agents cannot manage variables`, `agents can't dismiss reviews`, `only the app that created a check run can update it`, `cross-site request refused` | | |
| 337 | +| `403 Forbidden` | You are signed in but your role, token scope or account kind doesn't allow this. | `write access required`, `admin access required`, `this token needs the write scope`, `this token is limited to repositories and can't change account settings`, `repository is archived (read-only)`, `agents cannot change branch protection`, `agents cannot manage secrets`, `agents cannot manage variables`, `agents can't dismiss reviews`, `only the app that created a check run can update it`, `cross-site request refused`, `Resource not accessible by integration (...)` (a workflow's `GITHUB_TOKEN`) | | |
| 297 | 338 | | `404 Not Found` | The resource doesn't exist, or it is private and you can't see it. Diffinery doesn't reveal whether a private repository exists, and draft releases are hidden from readers the same way. | `repository not found`, `Branch not found`, `Not Found` | |
| 298 | 339 | | `405 Method Not Allowed` | A pull request merge was refused by policy. | `pull request is not mergeable: ...` (followed by the blockers), `main requires a merge queue: add the pull request to the queue`, `main requires linear history; use squash or rebase` | |
| 299 | 340 | | `409 Conflict` | Someone else changed the branch, ref or file first, or a merge conflicts. Fetch the latest state and retry. | `branch moved; refresh and retry`, `head branch was modified; review and try again`, `README.md does not match <sha>`, `Merge conflict`, `Git Repository is empty.`, `Already exists - Variable already exists` | |
| 341 | +| `410 Gone` | The artifact has expired; it is still listed, but its content is gone. | `Artifact has expired` | | |
| 300 | 342 | | `413 Payload Too Large` | An upload or a generated archive is over a limit. | `release assets are limited to 100 MB`, `repository is too large for an on-the-fly archive; clone it with git instead` | |
| 301 | 343 | | `422 Unprocessable Entity` | The request is understood but invalid. | `title is required`, `invalid secret name`, `webhook URL must use https`, `state must be one of error, failure, pending, success`, `The search is longer than 256 characters.`, `push protection: possible secret detected (...)`, `scopes must be a non-empty list of: read, write, admin` | |
| 302 | 344 | | `429 Too Many Requests` | A per-repository budget ran out (only new source archives have one). | `too many archives were built for this repository in the last hour; try again later or clone it with git` | |
| 303 | 345 | | `500 Internal Server Error` | A bug or an outage. | `internal error` | |
| 304 | 346 | | `503 Service Unavailable` | A sandbox container didn't start. | `the sandbox container did not start; try again` | |
| 305 | 347 | |
| 306 | -Most writes to an archived repository return `403 repository is archived (read-only)`. Repository administration (collaborators, branch protection, webhooks, secrets, variables) still works, so an admin can unarchive it with `PATCH /repos/{owner}/{repo}` and `{"archived": false}`. Opening and editing issues, editing pull requests, adding and editing comments, submitting reviews and requesting reviewers don't check the archived state yet; see [Archive a repository](/docs/repositories/overview#archive-a-repository). | |
| 348 | +Every write to an archived repository returns `403 repository is archived (read-only)`, whatever role it needs: pushes, issues, comments, reviews, labels, branch protection, collaborators, secrets, variables, webhooks and MCP tools alike. Only a few calls still work: unarchiving it with `PATCH /repos/{owner}/{repo}` and `{"archived": false}` (admins), deleting it, watching or unwatching it, and linking a package to it. See [Archive a repository](/docs/repositories/overview#archive-a-repository). | |
| 307 | 349 | |
| 308 | 350 | ## Pagination |
| 309 | 351 | |
| @@ -325,6 +367,7 @@ | ||
| 325 | 367 | | `GET /repos/{owner}/{repo}/issues` and `/pulls` | 30 | Also sends `x-total-count` with the number of matching items. | |
| 326 | 368 | | `GET /repos/{owner}/{repo}/actions/runs` | 25 | `total_count` in the body. | |
| 327 | 369 | | `GET /repos/{owner}/{repo}/actions/workflows/{id}/runs` | 30 | `total_count` in the body. No `Link` header: page with `page` until you have `total_count` runs. | |
| 370 | +| `GET .../actions/artifacts`, `.../actions/runs/{id}/artifacts`, `.../actions/caches` | 30 | `total_count` in the body. No `Link` header. | | |
| 328 | 371 | | `GET /repos/{owner}/{repo}/compare/{base}...{head}` (its `commits`), `GET .../pulls/{n}/commits` | 100 | At most 250 commits in total, as on GitHub. | |
| 329 | 372 | | `GET .../git/refs`, `.../git/matching-refs/{ref}` | 100 | | |
| 330 | 373 | | `GET .../commits/{ref}/status` (its `statuses`) | 100 | | |
| @@ -381,14 +424,15 @@ | ||
| 381 | 424 | - Branches (with `protected=true|false`), one branch, tags, README (`GET /readme` and `/readme/{dir}`), commits, one commit with its files and stats, compare (`base...head`), and `GET /commits/{sha}/pulls`. |
| 382 | 425 | - `POST /merges` merges a branch or commit into a branch (`201` with the merge commit, `204` when there was nothing to merge, `409 Merge conflict`). |
| 383 | 426 | - `POST /dispatches` sends a `repository_dispatch` event: `event_type` (up to 100 characters) and an optional `client_payload` object (at most 10 top-level properties, 64 KiB). It returns `204` and starts workflows with `on: repository_dispatch`. |
| 384 | -- Archived repositories are read-only through the API, for everyone. See [Errors](#errors). | |
| 427 | +- `POST /transfer` moves the repository to `new_owner` (your own account, or an organization you own or administer), optionally under `new_name`, and returns `202` with the repository. People only, with an `admin` token that isn't limited to repositories. See [Transfer a repository](/docs/repositories/overview#transfer-a-repository). | |
| 428 | +- Archived repositories are read-only through the API, for everyone, except unarchiving, deleting, transferring and watching. See [Errors](#errors). `PATCH` and the collaborator endpoints are recorded in the audit log, like changes in the web settings pages. | |
| 385 | 429 | |
| 386 | 430 | ### Contents |
| 387 | 431 | |
| 388 | 432 | - `GET /contents/{path}` returns a file (base64, with `sha`), a directory listing, or a symlink (with `target`). `ref` picks the branch, tag or commit (default: the default branch). `Accept: application/vnd.github.raw` returns the bytes. |
| 389 | 433 | - `PUT /contents/{path}` creates or updates one file with a commit: `message`, base64 `content`, optional `branch`, `author` and `committer`. Updating an existing file needs its current blob `sha`, as on GitHub: without it you get `422 "sha" wasn't supplied`, and a stale one gets `409 <path> does not match <sha>`. It returns `201` for a new file and `200` for an update. |
| 390 | 434 | - `DELETE /contents/{path}` removes a file with a commit; `sha` is required. |
| 391 | -- Every write refuses unsafe tree entry names: `.`, `..`, `.git` in any case and spellings that some systems treat as `.git` (such as `.git.` or `git~1`), and names with `\` or NUL. Push protection scans new file content as it does for `git push`. | |
| 435 | +- Every write refuses unsafe tree entry names: `.`, `..`, `.git` in any case and spellings that some systems treat as `.git` (such as `.git.` or `git~1`), and names with `\` or NUL. Push protection scans new file content as it does for `git push`, here and in `POST /repos/{owner}/{repo}/diffinery/commits`: content that looks like a credential is refused with `422 push protection: possible secret detected (...)`. Only the multi-file commit endpoint takes a bypass: a repository admin who is a person sends `"allow_secrets": "<reason>"`, which is audited as `api.secret_bypassed` and emailed to the repository's admins. | |
| 392 | 436 | |
| 393 | 437 | ### Git data |
| 394 | 438 | |
| @@ -446,12 +490,14 @@ | ||
| 446 | 490 | |
| 447 | 491 | ### Issues and pull requests |
| 448 | 492 | |
| 449 | -- Issues: list, create, get, update (including `state_reason`), labels (create, get, update, delete; add, set, remove on an issue), assignees (add, remove, `GET /assignees`, `GET /assignees/{login}`), lock and unlock (`PUT` and `DELETE /issues/{n}/lock`, triage), comments (per issue and repository-wide `GET /issues/comments`; get, edit and delete one), events (`GET /issues/{n}/events`, `/issues/events`, `/issues/events/{id}`) and the timeline (`GET /issues/{n}/timeline`). | |
| 450 | -- Pull requests: list (with the filters in [Pagination](#pagination)), create, get, update, files, commits, merge (`PUT /pulls/{n}/merge` with `merge_method`, `commit_title`, `commit_message`, `sha`), reviews (list, get, submit with line comments), review comments (list per pull request and repository-wide, get, edit, delete, create, and replies with `POST /pulls/{n}/comments/{id}/replies` or `in_reply_to`), requested reviewers (list, add, remove), review dismissal and `update-branch`. | |
| 493 | +- Issues: list, create (`labels` and `assignees` from someone without triage access are dropped without an error, as on GitHub), get, update (including `state_reason`; `labels` and `assignees` from someone without triage access are dropped here too), labels (create, get, update, delete; add, set, remove on an issue), assignees (add, remove, `GET /assignees`, `GET /assignees/{login}`), lock and unlock (`PUT` and `DELETE /issues/{n}/lock`, triage), comments (per issue and repository-wide `GET /issues/comments`; get, edit and delete one), events (`GET /issues/{n}/events`, `/issues/events`, `/issues/events/{id}`) and the timeline (`GET /issues/{n}/timeline`). | |
| 494 | +- Pull requests: list (with the filters in [Pagination](#pagination)), create, get, update, files, commits, merge (`PUT /pulls/{n}/merge` with `merge_method`, `commit_title`, `commit_message`, `sha`), auto-merge (`PUT` and `DELETE /pulls/{n}/auto-merge`), reviews (list, get, submit with line comments), review comments (list per pull request and repository-wide, get, edit, delete, create, and replies with `POST /pulls/{n}/comments/{id}/replies` or `in_reply_to`), requested reviewers (list, add, remove), review dismissal and `update-branch`. | |
| 451 | 495 | - `GET /pulls/{n}` reports `mergeable`, `mergeable_state` (`clean`, `blocked`, `dirty`, `unknown` and so on) and `rebaseable`, plus Diffinery's `merge_blockers`. |
| 452 | -- **Drafts:** `PATCH /pulls/{n}` with `"draft": false` marks a draft ready for review, and `"draft": true` converts it back (a Diffinery addition; GitHub only offers this through GraphQL). The pull request's author or anyone with write access can do it, only while it is open (`422 the pull request is not open`; others get `403 only the author or someone with write access can change draft status`). It records a `ready_for_review` or `converted_to_draft` event on the timeline and sends a `pull_request` webhook with the action `ready_for_review` or `converted_to_draft`. | |
| 453 | -- **Merging past branch rules:** a merge that branch protection blocks fails with `405 pull request is not mergeable: ...`. A repository admin who is a person can merge anyway by sending `"bypass_rules": true` with `PUT /pulls/{n}/merge`, unless the rule turns on **Do not allow administrators to bypass these rules** (`enforceAdmins`), and only when the pull request has no conflicts and isn't a draft. Without the flag, admins are blocked like everyone else, and the error ends with `(as an admin you can bypass with bypass_rules)`. A bypassed merge is recorded in the audit log as `pull_request.merge_bypassed`. `"bypass_merge_queue": true` merges past a merge queue and also bypasses unmet rules. `diffinery pr merge --admin` sends both. | |
| 454 | -- **Linear history:** when the base branch's rule requires linear history, `merge_method: "merge"` is refused with `405 <branch> requires linear history; use squash or rebase` (unless an admin bypasses), and a merge without `merge_method` uses `squash`. | |
| 496 | +- **Auto-merge:** GitHub turns auto-merge on and off only through GraphQL; Diffinery has `PUT /pulls/{n}/auto-merge` (optional `merge_method`, `commit_title`, `commit_message`) and `DELETE /pulls/{n}/auto-merge`. `PUT` answers `{"state": "enabled" | "queued" | "merged", "auto_merge": {...}, "merge_commit_sha": ...}`, and pull request objects carry GitHub's `auto_merge` object (`enabled_by`, `merge_method`, `commit_title`, `commit_message`) while it is on. Agents and workflow tokens can use it on the default branch or a protected branch only where the rule sets `allowAgentAutoMerge`. See [Auto-merge](/docs/collaboration/pull-requests#auto-merge). | |
| 497 | +- **Drafts:** `PATCH /pulls/{n}` with `"draft": false` marks a draft ready for review, and `"draft": true` converts it back (a Diffinery addition; GitHub only offers this through GraphQL). The pull request's author or anyone with write access can do it, only while it is open (`422 the pull request is not open`; others get `403 only the author or someone with write access can change draft status`). It records a `ready_for_review` or `convert_to_draft` event on the timeline (GitHub's names) and sends a `pull_request` webhook with the action `ready_for_review` or `converted_to_draft`. The web UI's **Ready for review** button and `diffinery pr ready` do the same. | |
| 498 | +- **Merging past branch rules:** a merge that branch protection blocks fails with `405 pull request is not mergeable: ...`. A repository admin who is a person can merge anyway by sending `"bypass_rules": true` with `PUT /pulls/{n}/merge`, unless the rule turns on **Do not allow administrators to bypass these rules** (`enforceAdmins`), and only when the pull request has no conflicts and isn't a draft. Without the flag, admins are blocked like everyone else, and the error ends with `(as an admin you can bypass with bypass_rules)`. Each flag covers only what it names: `"bypass_rules": true` skips unmet rules (and linear history), and `"bypass_merge_queue": true` merges past a required merge queue but not past unmet rules. A merge that bypassed rules, linear history or the queue is recorded in the audit log as `pull_request.merge_bypassed`. `diffinery pr merge --admin` sends both flags. Agents and workflow tokens can't bypass. | |
| 499 | +- **Linear history:** when the base branch's rule requires linear history, `merge_method: "merge"` is refused with `405 <branch> requires linear history; use squash or rebase` (unless an admin sends `bypass_rules`), and a merge without `merge_method` uses `squash`. | |
| 500 | +- **Workflow tokens and reviews:** a workflow's `GITHUB_TOKEN` can open pull requests and approve or request changes only when the repository allows it (see [Workflow job tokens](#workflow-job-tokens)), and only people's approvals count toward required approvals. | |
| 455 | 501 | - Changing an issue's or pull request's title or body sends an `issues` or `pull_request` webhook with the action `edited`. |
| 456 | 502 | - `PUT /pulls/{n}/update-branch` merges the base branch into the head branch (optional `expected_head_sha`) and returns `202`. |
| 457 | 503 | - `PUT /pulls/{n}/reviews/{id}/dismissals` with a `message` dismisses an approving or change-requesting review. It takes maintain access; the pull request's author can't dismiss reviews on it, and agents can't dismiss reviews at all. |
| @@ -459,6 +505,7 @@ | ||
| 459 | 505 | - **Reactions** (`+1`, `-1`, `laugh`, `confused`, `heart`, `hooray`, `rocket`, `eyes`) on issues and pull requests, issue comments, review comments and releases: list (filter with `content`), add (`201`, or `200` if you had already reacted) and delete by id. |
| 460 | 506 | - Comment, review, review comment, event and reaction IDs are numbers. |
| 461 | 507 | - Team reviewers (`team_reviewers`) are refused with `422 team reviewers are not supported; request people by login`. |
| 508 | +- **Milestones:** `GET /milestones` (`state` `open`, `closed` or `all`), `GET /milestones/{n}`, `POST /milestones` and `PATCH /milestones/{n}` (triage). There is no `DELETE`. Set an issue's or pull request's milestone with `PATCH /issues/{n}` and `"milestone": <number>` or `null` (triage); issues and pull requests carry a `milestone` object. | |
| 462 | 509 | |
| 463 | 510 | ### Deployments and environments |
| 464 | 511 | |
| @@ -471,20 +518,25 @@ | ||
| 471 | 518 | |
| 472 | 519 | ### Actions |
| 473 | 520 | |
| 474 | -- Workflows (list, get by path or file name, `POST /actions/workflows/{id}/dispatches`), runs (list, per workflow, get, attempts `GET /actions/runs/{id}/attempts/{n}`, cancel, `rerun` and `rerun-failed-jobs`), jobs (per run, get), logs (a zip per run, or text per job), artifacts per run. | |
| 475 | -- Repository secrets (list names, set with `value`, delete; admin, people only), organization secrets (organization owners and admins, people only), repository variables (`GET`, `POST`, `PATCH`, `PUT`, `DELETE`; reading needs write access, changing needs admin), and environment secrets and variables. | |
| 521 | +- Workflows (list, get by path or file name, `POST /actions/workflows/{id}/dispatches`), runs (list, per workflow, get, attempts `GET /actions/runs/{id}/attempts/{n}`, cancel, `rerun` and `rerun-failed-jobs`), jobs (per run, get), logs (a zip per run, or text per job). | |
| 522 | +- **Artifacts:** `GET /actions/artifacts` (every artifact, newest first, `name` filter), `GET /actions/runs/{id}/artifacts` (one run; a run number covers all its attempts), `GET` and `DELETE /actions/artifacts/{id}`, and `GET /actions/artifacts/{id}/zip`, which redirects (`302`) to a signed URL valid for 10 minutes. Artifacts have numeric IDs, `digest`, `expires_at` and `expired`. An artifact past its retention (1 to 90 days, from `retention-days`) loses its content but stays listed with `expired: true` for 90 more days; downloading it gets `410 Artifact has expired`. Deleting needs write access and is audited as `actions.artifact.delete`. | |
| 523 | +- **Caches:** `GET /actions/caches` (filter with `key` prefix and `ref`, sort by `last_accessed_at`, `created_at` or `size_in_bytes`), `DELETE /actions/caches?key=<key>[&ref=<ref>]` (returns the deleted entries), `DELETE /actions/caches/{id}`, and `GET /actions/cache/usage` (`active_caches_size_in_bytes`, `active_caches_count`). Each entry has its `ref` (like `refs/heads/main`), `key` and `version`. Deleting needs write access and is audited as `actions.cache.delete`. See [Caching and artifacts](/docs/actions/caching-and-artifacts) and [Limits](/docs/reference/limits#caches-and-artifacts). | |
| 524 | +- **Workflow permissions:** `GET` and `PUT /actions/permissions/workflow` read and change `default_workflow_permissions` (`read` or `write`) and `can_approve_pull_request_reviews`. Admins only, people only; each change is audited as `repo.actions_workflow_settings`. | |
| 525 | +- Repository secrets (list names, set with `value`, delete; admin, people only), organization secrets (organization owners and admins, people only), repository variables (`GET`, `POST`, `PATCH`, `PUT`, `DELETE`; reading needs write access, changing needs admin), organization variables (`GET`, `POST`, `PATCH`, `DELETE /orgs/{org}/actions/variables`; organization owners and admins, people only), and environment secrets and variables. | |
| 526 | +- Organization variables reach every repository of the organization as `vars.NAME`; a repository variable with the same name wins. `GET /repos/{owner}/{repo}/actions/variables` lists only the repository's own. Unlike GitHub, `POST` on an existing organization variable replaces its value instead of answering `409`, `PATCH` can't rename, and `visibility` is always `all`. Changes are audited as `org.variable_set` and `org.variable_delete`. | |
| 527 | +- Changing repository secrets and variables, environment variables and organization secrets is recorded in the audit log (`repo.secret.set`, `repo.variable.set`, `environment.variable.set`, `org.secret.set` and the matching `.delete` actions). Renaming a variable onto a name that exists fails with `409 Already exists - Variable already exists`. | |
| 476 | 528 | - Secrets are sent as plain `value` over HTTPS and encrypted at rest. GitHub's sealed-box `encrypted_value` is not supported yet: `422 encrypted_value (sealed box) is not supported yet: send the secret as value over HTTPS; it is encrypted at rest`. |
| 477 | 529 | |
| 478 | 530 | ### Webhooks |
| 479 | 531 | |
| 480 | -List, create (`config.url` must use `https`, `events` defaults to all) and delete repository webhooks (admin). Webhooks get the `release`, `status`, `deployment`, `deployment_status` and `workflow_dispatch` events as well as pushes, pull requests, issues and the rest, including the `edited`, `ready_for_review` and `converted_to_draft` actions. A delivery that fails with a network error or timeout, `408`, `429` or a `5xx` status is retried up to five times (after 1 minute, 5 minutes, 30 minutes, 1 hour and 3 hours) with the same `X-GitHub-Delivery` ID. See [Webhooks](/docs/repositories/webhooks) and [Webhook events](/docs/reference/webhook-events). | |
| 532 | +List, create (`config.url` must use `https`, `events` defaults to all) and delete repository webhooks (admin; creating and deleting are audited as `repo.webhook.create` and `repo.webhook.delete`). Webhooks get the `release`, `status`, `deployment`, `deployment_status` and `workflow_dispatch` events as well as pushes, pull requests, issues and the rest, including the `edited`, `ready_for_review` and `converted_to_draft` actions. A delivery that fails with a network error or timeout, `408`, `429` or a `5xx` status is retried up to five times (after 1 minute, 5 minutes, 30 minutes, 1 hour and 3 hours) with the same `X-GitHub-Delivery` ID. See [Webhooks](/docs/repositories/webhooks) and [Webhook events](/docs/reference/webhook-events). | |
| 481 | 533 | |
| 482 | 534 | ### Workflows started by API calls |
| 483 | 535 | |
| 484 | 536 | - A release starts `on: release` workflows (with `types:` matching the action, such as `published`). Drafts start nothing until they are published. |
| 485 | 537 | - `POST /repos/{owner}/{repo}/dispatches` starts `on: repository_dispatch` workflows (with `types:` matching `event_type`), with `client_payload` in `github.event.client_payload`. |
| 486 | 538 | - `POST /actions/workflows/{id}/dispatches` starts a `workflow_dispatch` run. |
| 487 | -- As on GitHub, events caused by a job's `GITHUB_TOKEN` don't start new workflow runs, except `repository_dispatch`. | |
| 539 | +- As on GitHub, events caused by a job's `GITHUB_TOKEN` don't start new workflow runs, except `workflow_dispatch` and `repository_dispatch`. | |
| 488 | 540 | |
| 489 | 541 | See [Events that trigger workflows](/docs/actions/events). |
| 490 | 542 | |
| @@ -568,22 +620,25 @@ | ||
| 568 | 620 | |
| 569 | 621 | | Endpoint | What it does | |
| 570 | 622 | | --- | --- | |
| 571 | -| `GET`, `POST /diffinery/tokens`, `DELETE /diffinery/tokens/{id}` | List, create and revoke your tokens. `POST` takes `name`, `scopes` (a non-empty list of `read`, `write`, `admin`; default `["write"]`), `repositories` (`["acme/website"]`) and `expires_in_days`, and returns the token once. Creating and revoking need an `admin` token. | | |
| 572 | -| `GET`, `POST /diffinery/agents` | List the agents you or your organizations own, and create an agent (it comes with a first `write` token valid for 90 days unless you pass `token`). Creating needs an `admin` token not restricted to repositories. | | |
| 573 | -| `POST /diffinery/agents/{login}/tokens` | Create another token for an agent you own. Needs an `admin` token not restricted to repositories. | | |
| 623 | +| `GET`, `POST /diffinery/tokens`, `DELETE /diffinery/tokens/{id}` | List, create and revoke your tokens. `POST` takes `name`, `scopes` (a non-empty list of `read`, `write`, `admin`; default `["write"]`), `repositories` (`["acme/website"]`, only repositories you can see) and `expires_in_days`, and returns the token once. Creating needs a browser session; revoking needs an `admin` token not restricted to repositories. | | |
| 624 | +| `GET`, `POST /diffinery/agents` | List the agents you or your organizations own, and create an agent (it comes with a first `write` token valid for 90 days unless you pass `token`). Creating needs a browser session. | | |
| 625 | +| `POST /diffinery/agents/{login}/tokens` | Create another token for an agent you own. Needs a browser session. | | |
| 574 | 626 | | `DELETE /diffinery/agents/{login}` | Disable an agent and revoke its tokens. Needs an `admin` token not restricted to repositories. | |
| 575 | -| `POST /repos/{owner}/{repo}/diffinery/commits` | Commit several files at once without a git client: `branch`, `message`, optional `base` (creates the branch from it) and `expected_head_sha` (fails with `409` if the branch moved), and `files` (each with `path`, `content`, `encoding`, `mode` or `delete`). Needs write access. | | |
| 627 | +| `POST /repos/{owner}/{repo}/diffinery/commits` | Commit several files at once without a git client: `branch`, `message`, optional `base` (creates the branch from it) and `expected_head_sha` (fails with `409` if the branch moved), and `files` (each with `path`, `content`, `encoding`, `mode` or `delete`). Push protection applies; a repository admin who is a person can pass `allow_secrets` with a reason. Needs write access. | | |
| 576 | 628 | | `POST /repos/{owner}/{repo}/diffinery/sandboxes` | Start a sandbox container with the repository checked out. See [Sandboxes](/docs/agents/sandboxes). | |
| 577 | 629 | | `POST /repos/{owner}/{repo}/diffinery/sandboxes/{id}/exec`, `/read`, `/write`, `/close` | Run a command, read or write a file, or stop the sandbox. | |
| 578 | 630 | | `GET`, `PUT /diffinery/notification-settings` | Your notification settings. `PUT` is a partial update: send only the groups you change. | |
| 579 | 631 | | `GET /diffinery/emails` | Emails Diffinery sent you recently (`per_page`, `kind`, `since`). Site administrators can add `all=1`. | |
| 580 | 632 | | `POST /diffinery/emails/test` | Send yourself a test email now. | |
| 581 | 633 | | `GET /diffinery/admin/email`, `POST /diffinery/admin/emails/samples` | Site administrators: email delivery status, and send every email template to yourself. | |
| 634 | +| `GET`, `POST /diffinery/imports`, `GET /diffinery/imports/{id}`, `POST /diffinery/imports/{id}/cancel`, `/retry`, `/selection` | Import repositories from GitHub, follow and manage the imports. See [Imports](#imports). | | |
| 635 | +| `GET /diffinery/user/linked-accounts`, `POST /diffinery/user/linked-accounts/github/challenge`, `POST /diffinery/user/linked-accounts/github`, `DELETE /diffinery/user/linked-accounts/{id}` | Link your GitHub account (gist proof, no token), so imports attribute your GitHub work to you. | | |
| 636 | +| `DELETE /diffinery/admin/linked-accounts/{id}` | Site administrators: clear anyone's GitHub link. | | |
| 582 | 637 | |
| 583 | 638 | Other Diffinery additions sit next to the GitHub-shaped endpoints: |
| 584 | 639 | |
| 585 | -- `POST /api/v3/user/orgs` creates an organization; `POST /api/v3/invitations` (site administrators) and `POST /api/v3/orgs/{org}/invitations` invite people by email. | |
| 586 | -- `PUT /api/v3/user/avatar` sets your profile picture from the raw image in the request body (JPEG, PNG or WebP, up to 5 MB and 3072×3072 pixels), and `DELETE` goes back to generated initials. `PUT` and `DELETE /api/v3/orgs/{org}/avatar` do the same for an organization (owners and admins). Both return `{"avatar_url": ...}`. `GET /api/avatars/{login}?s=64|128|256` serves the picture to anyone, as WebP, or initials as SVG; it is the `avatar_url` of every user object. | |
| 640 | +- `POST /api/v3/user/orgs` creates an organization; `POST /api/v3/invitations` (site administrators) and `POST /api/v3/orgs/{org}/invitations` invite people by email. `GET /api/v3/orgs/{org}` and `PATCH /api/v3/orgs/{org}` (owners) follow GitHub: `default_repository_permission` is the organization's base permission (`none`, `read`, `write` or `admin`), shown to members and site administrators, and `PATCH` also takes the display `name`. See [Base permission](/docs/accounts/organizations#base-permission). | |
| 641 | +- `PUT /api/v3/user/avatar` sets your profile picture from the raw image in the request body (JPEG, PNG or WebP, up to 5 MB and 3072×3072 pixels), and `DELETE` goes back to generated initials. `PUT` and `DELETE /api/v3/orgs/{org}/avatar` do the same for an organization (owners only). Both return `{"avatar_url": ...}`. `GET /api/avatars/{login}?s=64|128|256` serves the picture to anyone, as WebP, or initials as SVG; it is the `avatar_url` of every user object. | |
| 587 | 642 | - `PUT`, `DELETE /repos/{owner}/{repo}/pulls/{n}/merge-queue` and `GET /repos/{owner}/{repo}/merge-queue/{branch}` drive the merge queue. |
| 588 | 643 | - `POST /repos/{owner}/{repo}/actions/runners/claim` is how self-hosted runners pick up jobs (see [Runners](/docs/actions/runners)). |
| 589 | 644 | - `GET /repos/{owner}/{repo}/raw/{ref}/{path}` returns a file's raw bytes. |
| @@ -596,6 +651,38 @@ | ||
| 596 | 651 | |
| 597 | 652 | The [API reference](/docs/api) lists every endpoint with its request and response fields. |
| 598 | 653 | |
| 654 | +## Imports | |
| 655 | + | |
| 656 | +Diffinery imports repositories from GitHub.com or GitHub Enterprise Server on the server: Git history with LFS objects, issues and pull requests with their original numbers, comments, reviews, labels, milestones, branch protection, Actions variables, deployment environments and releases. You start an import with one request and poll it; nothing goes through your machine. The web form (**Import from GitHub**), `diffinery migrate github` and the MCP tool `start_github_import` use the same service. For the whole procedure, what is and isn't copied, and how people are attributed, read [Migrate from GitHub](/docs/migrate-from-github). | |
| 657 | + | |
| 658 | +```sh | |
| 659 | +# Import one repository into the acme organization | |
| 660 | +curl -sS -X POST https://diffinery.com/api/v3/diffinery/imports \ | |
| 661 | + -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \ | |
| 662 | + -d '{"source": "diffy/hello-world", "owner": "acme", "name": "website", "github_token": "ghp_..."}' | |
| 663 | + | |
| 664 | +# Follow it until status is succeeded, failed or cancelled | |
| 665 | +curl -sS https://diffinery.com/api/v3/diffinery/imports/<id> -H "Authorization: Bearer dfy_..." | |
| 666 | +``` | |
| 667 | + | |
| 668 | +| Endpoint | What it does | | |
| 669 | +| --- | --- | | |
| 670 | +| `POST /diffinery/imports` | Start an import: `source` (one repository) or `source_owner` (a GitHub organization or user, with optional `repositories`, `exclude_forks`, `exclude_archived` and `concurrency`), `owner`, and optional `name`, `visibility` (`keep`, `private` or `public`), `github_token`, `github_api_url` and `options`. Returns `201` with the import. | | |
| 671 | +| `GET /diffinery/imports`, `GET /diffinery/imports/{id}` | Your imports, and one import with its `status`, per-step progress (`steps`), the Git transfer (`git`), `summary` (counts, secret names to set by hand, push protection findings), `warnings`, `error` and, for an organization import, `children`. | | |
| 672 | +| `POST /diffinery/imports/{id}/cancel` | Stop an import that hasn't finished. | | |
| 673 | +| `POST /diffinery/imports/{id}/retry` | Run a failed or cancelled import again (`202`), optionally with a new `github_token`. Only whoever started it. | | |
| 674 | +| `POST /diffinery/imports/{id}/selection` | Start an organization import that was drafted in the web form, with the chosen `repositories`. | | |
| 675 | + | |
| 676 | +Things to know when you script imports: | |
| 677 | + | |
| 678 | +- **Token:** starting one needs a `write` or `admin` token (or a browser session). Old history with credentials in it is imported with an audited push protection bypass only when a person who administers the target started the import with an `admin` token or the web UI; otherwise the import fails and names the findings. See [Push protection](/docs/security/push-protection). | |
| 679 | +- **Agents** can import only into an organization that owns them or that their owner owns or administers. Their imports never bypass push protection and skip Actions variables and environments. | |
| 680 | +- **Secret values are never read or copied.** Only secret names are listed in `summary`, for a repository admin to set by hand. | |
| 681 | +- **Limits:** one organization import and three repository imports at a time per account and per target (`429`), and more. See [Limits](/docs/reference/limits#imports). | |
| 682 | +- **The GitHub token** is encrypted, used only by that import and deleted when it ends. It is never returned. | |
| 683 | + | |
| 684 | +Every endpoint, with its fields and errors, is in the [API reference](/docs/api). | |
| 685 | + | |
| 599 | 686 | ## Using the API |
| 600 | 687 | |
| 601 | 688 | The examples below list open pull requests in `acme/website` and open a new issue. Replace `dfy_...` with your token. |
| @@ -740,11 +827,18 @@ | ||
| 740 | 827 | | `401 authentication required` when opening an issue or commenting on a public repository | Anonymous callers can only read. Send a token. | |
| 741 | 828 | | `403 this token needs the write scope` | The token has only the `read` scope. Use a `write` token. | |
| 742 | 829 | | `403 this token needs the admin scope` or `403 this token is limited to repositories and can't change account settings` | Account-level endpoints (organizations, invitations, agents, organization secrets, revoking tokens) need an `admin` token without a repository list. See [Token scopes](#token-scopes). | |
| 743 | -| `405 pull request is not mergeable: ... (as an admin you can bypass with bypass_rules)` | Meet the requirements, or, as an admin, send `"bypass_rules": true`. The bypass is audited. | | |
| 830 | +| `405 pull request is not mergeable: ... (as an admin you can bypass with bypass_rules)` | Meet the requirements, or, as an admin, send `"bypass_rules": true`. `bypass_merge_queue` alone doesn't skip rules. The bypass is audited. | | |
| 831 | +| `403 Resource not accessible by integration (...)` from a workflow | The job's `permissions:` don't grant what the endpoint needs. Add the permission the message names, for example `issues: write`. See [Workflow job tokens](#workflow-job-tokens). | | |
| 832 | +| `403 GitHub Actions is not permitted to create pull requests` (or `approve`) | An admin allows it with `PUT /repos/{owner}/{repo}/actions/permissions/workflow` and `{"can_approve_pull_request_reviews": true}`. | | |
| 833 | +| `refusing to allow a GitHub App to create or update workflow ...` | A workflow's token can't change workflow files. Push that change with a personal or agent token. | | |
| 834 | +| `410 Artifact has expired` | The artifact passed its retention. Upload it again with a longer `retention-days` (at most 90). | | |
| 744 | 835 | | `403 repository is archived (read-only)` | Unarchive the repository first (admin): `PATCH /repos/{owner}/{repo}` with `{"archived": false}`. | |
| 836 | +| `301` or `307` with `{"message": "Moved Permanently", "url": ...}` | The repository was transferred or renamed. Follow `Location` (`curl -L`, Octokit follows it), and update the owner and name your client uses. See [Transferred and renamed repositories](#transferred-and-renamed-repositories). | | |
| 745 | 837 | | `409 <path> does not match <sha>` from the contents API | The file changed since you read it. Read it again and send the new `sha`. | |
| 746 | 838 | | `422 push protection: possible secret detected (...)` | The content looks like a credential. Remove it; see [Push protection](/docs/security/push-protection). | |
| 747 | 839 | | `413` or `429` from `zipball` or `tarball` | The repository is too big for an archive, or too many new archives were built this hour. Clone it with git. | |
| 748 | 840 | | `413 release assets are limited to 100 MB` | Split the file, or host it elsewhere and link to it from the release notes. | |
| 749 | 841 | | `search` returns `incomplete_results: true` | More than 50 repositories matched. Add `repo:` or `org:` to the query. | |
| 750 | 842 | | A list stops after one page | The client doesn't follow `Link`. Use `octokit.paginate()` or loop on `rel="next"`. | |
| 843 | +| `429 too many repository imports are already running (at most 3 at a time); wait for one to finish` | You, or the account you import into, already run three repository imports (or one organization import: `an organization import is already in progress`). Wait, or cancel one with `POST /diffinery/imports/{id}/cancel`. | | |
| 844 | +| `422 github_api_url is required when a github_token is given and the source names a GitHub Enterprise host` | Send `github_api_url` (such as `https://ghe.example.com/api/v3`) on the same host as the source. Diffinery never sends a token to a host taken only from the source URL. | |
modifiedapps/diffinery/docs/reference/webhook-events.md+118−42View file
| @@ -35,28 +35,107 @@ | ||
| 35 | 35 | |
| 36 | 36 | ### The repository object |
| 37 | 37 | |
| 38 | +`repository` is GitHub's full repository object, the same shape `GET /api/v3/repos/{owner}/{repo}` returns, so receivers that read `repository.private`, `repository.visibility`, `repository.owner` or `repository.default_branch` work unchanged: | |
| 39 | + | |
| 38 | 40 | ```json |
| 39 | 41 | { |
| 40 | 42 | "id": 6628874657374920, |
| 41 | 43 | "node_id": "UmVwb3NpdG9yeTo1YzBlOGI3ZjJkMWE0ZTNiOWY2YTdjOGQ5ZTBmMWEyYg", |
| 42 | 44 | "name": "website", |
| 43 | 45 | "full_name": "acme/website", |
| 44 | - "owner": { "login": "acme" }, | |
| 46 | + "private": false, | |
| 47 | + "owner": { | |
| 48 | + "login": "acme", | |
| 49 | + "id": 3915577218345164, | |
| 50 | + "node_id": "T3JnYW5pemF0aW9uOjdlNmQ1YzRiM2EyZjFlMGQ5YzhiN2E2ZjVlNGQzYzJi", | |
| 51 | + "avatar_url": "https://diffinery.com/api/avatars/acme", | |
| 52 | + "gravatar_id": "", | |
| 53 | + "url": "https://diffinery.com/api/v3/users/acme", | |
| 54 | + "html_url": "https://diffinery.com/acme", | |
| 55 | + "repos_url": "https://diffinery.com/api/v3/users/acme/repos", | |
| 56 | + "organizations_url": "https://diffinery.com/api/v3/users/acme/orgs", | |
| 57 | + "type": "Organization", | |
| 58 | + "user_view_type": "public", | |
| 59 | + "site_admin": false | |
| 60 | + }, | |
| 45 | 61 | "html_url": "https://diffinery.com/acme/website", |
| 46 | - "clone_url": "https://diffinery.com/acme/website.git" | |
| 62 | + "description": "The acme.example website", | |
| 63 | + "fork": false, | |
| 64 | + "url": "https://diffinery.com/api/v3/repos/acme/website", | |
| 65 | + "archive_url": "https://diffinery.com/api/v3/repos/acme/website/{archive_format}{/ref}", | |
| 66 | + "branches_url": "https://diffinery.com/api/v3/repos/acme/website/branches{/branch}", | |
| 67 | + "commits_url": "https://diffinery.com/api/v3/repos/acme/website/commits{/sha}", | |
| 68 | + "compare_url": "https://diffinery.com/api/v3/repos/acme/website/compare/{base}...{head}", | |
| 69 | + "contents_url": "https://diffinery.com/api/v3/repos/acme/website/contents/{+path}", | |
| 70 | + "deployments_url": "https://diffinery.com/api/v3/repos/acme/website/deployments", | |
| 71 | + "git_refs_url": "https://diffinery.com/api/v3/repos/acme/website/git/refs{/sha}", | |
| 72 | + "hooks_url": "https://diffinery.com/api/v3/repos/acme/website/hooks", | |
| 73 | + "issues_url": "https://diffinery.com/api/v3/repos/acme/website/issues{/number}", | |
| 74 | + "labels_url": "https://diffinery.com/api/v3/repos/acme/website/labels{/name}", | |
| 75 | + "languages_url": "https://diffinery.com/api/v3/repos/acme/website/languages", | |
| 76 | + "merges_url": "https://diffinery.com/api/v3/repos/acme/website/merges", | |
| 77 | + "pulls_url": "https://diffinery.com/api/v3/repos/acme/website/pulls{/number}", | |
| 78 | + "releases_url": "https://diffinery.com/api/v3/repos/acme/website/releases{/id}", | |
| 79 | + "statuses_url": "https://diffinery.com/api/v3/repos/acme/website/statuses/{sha}", | |
| 80 | + "tags_url": "https://diffinery.com/api/v3/repos/acme/website/tags", | |
| 81 | + "git_url": "https://diffinery.com/acme/website.git", | |
| 82 | + "ssh_url": "https://diffinery.com/acme/website.git", | |
| 83 | + "clone_url": "https://diffinery.com/acme/website.git", | |
| 84 | + "svn_url": "https://diffinery.com/acme/website", | |
| 85 | + "homepage": null, | |
| 86 | + "size": 0, | |
| 87 | + "stargazers_count": 0, | |
| 88 | + "watchers_count": 0, | |
| 89 | + "language": null, | |
| 90 | + "has_issues": true, | |
| 91 | + "has_projects": false, | |
| 92 | + "has_downloads": true, | |
| 93 | + "has_wiki": false, | |
| 94 | + "has_pages": false, | |
| 95 | + "has_discussions": false, | |
| 96 | + "forks_count": 0, | |
| 97 | + "mirror_url": null, | |
| 98 | + "archived": false, | |
| 99 | + "disabled": false, | |
| 100 | + "open_issues_count": 0, | |
| 101 | + "license": null, | |
| 102 | + "allow_forking": false, | |
| 103 | + "is_template": false, | |
| 104 | + "web_commit_signoff_required": false, | |
| 105 | + "topics": [ | |
| 106 | + "website" | |
| 107 | + ], | |
| 108 | + "visibility": "public", | |
| 109 | + "forks": 0, | |
| 110 | + "open_issues": 0, | |
| 111 | + "watchers": 0, | |
| 112 | + "default_branch": "main", | |
| 113 | + "created_at": "2026-03-02T14:20:11Z", | |
| 114 | + "updated_at": "2026-10-12T16:03:40Z", | |
| 115 | + "pushed_at": "2026-10-13T09:30:00Z" | |
| 47 | 116 | } |
| 48 | 117 | ``` |
| 49 | 118 | |
| 50 | 119 | | Field | Description | |
| 51 | 120 | |---|---| |
| 52 | -| `id` | The repository's numeric id, the same number the REST API returns. It does not change when the repository is renamed. | | |
| 121 | +| `id` | The repository's numeric id, the same number the REST API returns. It does not change when the repository is renamed or transferred, so use it rather than `full_name` to recognize a repository. | | |
| 53 | 122 | | `node_id` | An opaque string id for the repository, as in the REST API | |
| 54 | -| `name` | Repository name | | |
| 55 | -| `full_name` | `owner/name` | | |
| 56 | -| `owner.login` | Login of the owning user or organization | | |
| 57 | -| `html_url` | Web address of the repository | | |
| 58 | -| `clone_url` | HTTPS clone URL | | |
| 123 | +| `name`, `full_name` | Repository name, and `owner/name` | | |
| 124 | +| `private` | `true` for a private repository, `false` for a public one | | |
| 125 | +| `visibility` | `public` or `private`: the repository's real visibility when the event is delivered | | |
| 126 | +| `owner` | The owning account as a user object: `login`, numeric `id`, `node_id`, `type` (`User` or `Organization`), `html_url`, `avatar_url` and API URLs | | |
| 127 | +| `description` | The repository's description, or `null` | | |
| 128 | +| `default_branch` | The default branch, for example `main` | | |
| 129 | +| `archived` | `true` when the repository is archived | | |
| 130 | +| `topics` | The repository's topics | | |
| 131 | +| `html_url`, `clone_url`, `git_url`, `ssh_url`, `svn_url` | Web address and clone URLs (all HTTPS: Diffinery has no SSH or git protocol access) | | |
| 132 | +| `url` and the `*_url` templates | REST API addresses, as on GitHub | | |
| 133 | +| `created_at`, `updated_at`, `pushed_at` | ISO 8601 times | | |
| 59 | 134 | |
| 135 | +Fields GitHub fills with counts (`size`, `stargazers_count`, `watchers_count`, `forks_count`, `open_issues_count` and their short forms) are always `0` in webhook payloads, `fork` and `allow_forking` are `false`, and there is no `permissions` object. If the repository was deleted before the delivery was sent, `repository` falls back to a minimal object (`id`, `node_id`, `name`, `full_name`, `owner.login`, `html_url`, `clone_url`) with `"private": true` and `"visibility": "private"`. | |
| 136 | + | |
| 137 | +To keep them short, the examples on this page show `repository` with only a few of its fields. | |
| 138 | + | |
| 60 | 139 | ### The sender object |
| 61 | 140 | |
| 62 | 141 | A person: |
| @@ -78,7 +157,7 @@ | ||
| 78 | 157 | | `type` | `User` for people, `Bot` for agents and for Diffinery's own actors | |
| 79 | 158 | | `on_behalf_of` | Only for agents: the login of the agent's owner | |
| 80 | 159 | |
| 81 | -Diffinery's own actors appear as `Bot` senders: `diffinery-merge-queue[bot]` (merge queue), `diffinery-scheduler` (scheduled workflow runs). | |
| 160 | +Diffinery's own actors appear as `Bot` senders: `diffinery-merge-queue[bot]` (merge queue), `diffinery-scheduler` (scheduled workflow runs), and `diffinery-actions[bot]` with an id such as `job:2f1e0d9c8b7a69584736251403f2e1d0` for what a workflow job does with its `GITHUB_TOKEN` (opening an issue, posting a status, creating a release, ...). | |
| 82 | 161 | |
| 83 | 162 | ## push |
| 84 | 163 | |
| @@ -105,15 +184,7 @@ | ||
| 105 | 184 | "created": false, |
| 106 | 185 | "deleted": false, |
| 107 | 186 | "at": "2026-10-13T09:30:00.000Z", |
| 108 | - "repository": { | |
| 109 | - "id": 6628874657374920, | |
| 110 | - "node_id": "UmVwb3NpdG9yeTo1YzBlOGI3ZjJkMWE0ZTNiOWY2YTdjOGQ5ZTBmMWEyYg", | |
| 111 | - "name": "website", | |
| 112 | - "full_name": "acme/website", | |
| 113 | - "owner": { "login": "acme" }, | |
| 114 | - "html_url": "https://diffinery.com/acme/website", | |
| 115 | - "clone_url": "https://diffinery.com/acme/website.git" | |
| 116 | - }, | |
| 187 | + "repository": { "id": 6628874657374920, "name": "website", "full_name": "acme/website", "private": false, "visibility": "public", "html_url": "https://diffinery.com/acme/website" }, | |
| 117 | 188 | "sender": { "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90", "login": "diffy", "type": "User" } |
| 118 | 189 | } |
| 119 | 190 | ``` |
| @@ -129,7 +200,7 @@ | ||
| 129 | 200 | "created": true, |
| 130 | 201 | "deleted": false, |
| 131 | 202 | "at": "2026-10-13T09:31:12.000Z", |
| 132 | - "repository": { "id": 6628874657374920, "node_id": "UmVwb3NpdG9yeTo1YzBlOGI3ZjJkMWE0ZTNiOWY2YTdjOGQ5ZTBmMWEyYg", "name": "website", "full_name": "acme/website", "owner": { "login": "acme" }, "html_url": "https://diffinery.com/acme/website", "clone_url": "https://diffinery.com/acme/website.git" }, | |
| 203 | + "repository": { "id": 6628874657374920, "name": "website", "full_name": "acme/website", "private": false, "visibility": "public", "html_url": "https://diffinery.com/acme/website" }, | |
| 133 | 204 | "sender": { "id": "f0e1d2c3b4a5968778695a4b3c2d1e0f", "login": "review-bot", "type": "Bot", "on_behalf_of": "diffy" } |
| 134 | 205 | } |
| 135 | 206 | ``` |
| @@ -144,13 +215,15 @@ | ||
| 144 | 215 | | `synchronize` | The head branch moved to a new commit | The pusher | |
| 145 | 216 | | `edited` | The title or description changed (web UI, `PATCH /pulls/{number}`, or the `update_issue` MCP tool). Not sent when the new text equals the old, or for label changes. | Who edited it | |
| 146 | 217 | | `ready_for_review` | A draft is marked ready for review (**Ready for review**, `diffinery pr ready`, or `PATCH` with `"draft": false`) | Who marked it | |
| 147 | -| `converted_to_draft` | An open pull request is turned back into a draft (`diffinery pr ready --undo`, or `PATCH` with `"draft": true`) | Who converted it | | |
| 218 | +| `converted_to_draft` | An open pull request is turned back into a draft (`diffinery pr ready --undo`, or `PATCH` with `"draft": true`). The pull request's timeline records this as `convert_to_draft`, GitHub's timeline name; the webhook action is `converted_to_draft`, as on GitHub. | Who converted it | | |
| 148 | 219 | | `closed` | The pull request is closed without merging (`pull_request.merged` is `false`), or is merged (`pull_request.merged` is `true`) | Who closed or merged it; for a merge by the merge queue, who added it to the queue | |
| 149 | 220 | | `reopened` | A closed pull request is reopened | Who reopened it | |
| 150 | 221 | | `assigned` | One or more people are newly assigned | Who assigned them | |
| 151 | 222 | | `review_requested` | One or more reviewers are newly requested | Who requested the review | |
| 152 | 223 | | `enqueued` | The pull request was added to a merge queue | Who added it | |
| 153 | 224 | | `dequeued` | The pull request was removed from the merge queue without merging: by a person, or because its merge group failed, conflicted or timed out | Who removed it, or `diffinery-merge-queue[bot]` | |
| 225 | +| `auto_merge_enabled` | [Auto-merge](/docs/collaboration/pull-requests#auto-merge) was turned on, or its method changed | Who turned it on | | |
| 226 | +| `auto_merge_disabled` | Auto-merge was turned off: by someone, by a push from someone else, by converting the pull request to a draft, or by itself because it can't go through | Who turned it off, the pusher, or `diffinery-auto-merge[bot]` | | |
| 154 | 227 | |
| 155 | 228 | | Field | Type | Description | |
| 156 | 229 | |---|---|---| |
| @@ -181,7 +254,7 @@ | ||
| 181 | 254 | "headSha": "2b7e9d4c1a3f5e7d9b0c2a4e6f8d0b1c3e5a7f9d", |
| 182 | 255 | "baseRef": "main", |
| 183 | 256 | "at": "2026-10-13T09:32:40.000Z", |
| 184 | - "repository": { "id": 6628874657374920, "node_id": "UmVwb3NpdG9yeTo1YzBlOGI3ZjJkMWE0ZTNiOWY2YTdjOGQ5ZTBmMWEyYg", "name": "website", "full_name": "acme/website", "owner": { "login": "acme" }, "html_url": "https://diffinery.com/acme/website", "clone_url": "https://diffinery.com/acme/website.git" }, | |
| 257 | + "repository": { "id": 6628874657374920, "name": "website", "full_name": "acme/website", "private": false, "visibility": "public", "html_url": "https://diffinery.com/acme/website" }, | |
| 185 | 258 | "sender": { "id": "f0e1d2c3b4a5968778695a4b3c2d1e0f", "login": "review-bot", "type": "Bot", "on_behalf_of": "diffy" }, |
| 186 | 259 | "pull_request": { |
| 187 | 260 | "number": 42, |
| @@ -203,7 +276,7 @@ | ||
| 203 | 276 | "headSha": "2b7e9d4c1a3f5e7d9b0c2a4e6f8d0b1c3e5a7f9d", |
| 204 | 277 | "baseRef": "main", |
| 205 | 278 | "at": "2026-10-13T10:05:11.000Z", |
| 206 | - "repository": { "id": 6628874657374920, "node_id": "UmVwb3NpdG9yeTo1YzBlOGI3ZjJkMWE0ZTNiOWY2YTdjOGQ5ZTBmMWEyYg", "name": "website", "full_name": "acme/website", "owner": { "login": "acme" }, "html_url": "https://diffinery.com/acme/website", "clone_url": "https://diffinery.com/acme/website.git" }, | |
| 279 | + "repository": { "id": 6628874657374920, "name": "website", "full_name": "acme/website", "private": false, "visibility": "public", "html_url": "https://diffinery.com/acme/website" }, | |
| 207 | 280 | "sender": { "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90", "login": "diffy", "type": "User" }, |
| 208 | 281 | "pull_request": { |
| 209 | 282 | "number": 42, |
| @@ -228,7 +301,7 @@ | ||
| 228 | 301 | "baseRef": "main", |
| 229 | 302 | "at": "2026-10-13T09:33:05.000Z", |
| 230 | 303 | "reviewers": ["diffy"], |
| 231 | - "repository": { "id": 6628874657374920, "node_id": "UmVwb3NpdG9yeTo1YzBlOGI3ZjJkMWE0ZTNiOWY2YTdjOGQ5ZTBmMWEyYg", "name": "website", "full_name": "acme/website", "owner": { "login": "acme" }, "html_url": "https://diffinery.com/acme/website", "clone_url": "https://diffinery.com/acme/website.git" }, | |
| 304 | + "repository": { "id": 6628874657374920, "name": "website", "full_name": "acme/website", "private": false, "visibility": "public", "html_url": "https://diffinery.com/acme/website" }, | |
| 232 | 305 | "sender": { "id": "f0e1d2c3b4a5968778695a4b3c2d1e0f", "login": "review-bot", "type": "Bot", "on_behalf_of": "diffy" }, |
| 233 | 306 | "pull_request": { |
| 234 | 307 | "number": 42, |
| @@ -252,7 +325,7 @@ | ||
| 252 | 325 | "headSha": "2b7e9d4c1a3f5e7d9b0c2a4e6f8d0b1c3e5a7f9d", |
| 253 | 326 | "baseRef": "main", |
| 254 | 327 | "at": "2026-10-13T09:45:10.000Z", |
| 255 | - "repository": { "id": 6628874657374920, "node_id": "UmVwb3NpdG9yeTo1YzBlOGI3ZjJkMWE0ZTNiOWY2YTdjOGQ5ZTBmMWEyYg", "name": "website", "full_name": "acme/website", "owner": { "login": "acme" }, "html_url": "https://diffinery.com/acme/website", "clone_url": "https://diffinery.com/acme/website.git" }, | |
| 328 | + "repository": { "id": 6628874657374920, "name": "website", "full_name": "acme/website", "private": false, "visibility": "public", "html_url": "https://diffinery.com/acme/website" }, | |
| 256 | 329 | "sender": { "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90", "login": "diffy", "type": "User" }, |
| 257 | 330 | "pull_request": { |
| 258 | 331 | "number": 42, |
| @@ -287,7 +360,7 @@ | ||
| 287 | 360 | "number": 42, |
| 288 | 361 | "state": "APPROVED", |
| 289 | 362 | "at": "2026-10-13T09:50:00.000Z", |
| 290 | - "repository": { "id": 6628874657374920, "node_id": "UmVwb3NpdG9yeTo1YzBlOGI3ZjJkMWE0ZTNiOWY2YTdjOGQ5ZTBmMWEyYg", "name": "website", "full_name": "acme/website", "owner": { "login": "acme" }, "html_url": "https://diffinery.com/acme/website", "clone_url": "https://diffinery.com/acme/website.git" }, | |
| 363 | + "repository": { "id": 6628874657374920, "name": "website", "full_name": "acme/website", "private": false, "visibility": "public", "html_url": "https://diffinery.com/acme/website" }, | |
| 291 | 364 | "sender": { "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90", "login": "diffy", "type": "User" } |
| 292 | 365 | } |
| 293 | 366 | ``` |
| @@ -320,7 +393,7 @@ | ||
| 320 | 393 | "action": "opened", |
| 321 | 394 | "number": 12, |
| 322 | 395 | "at": "2026-10-13T08:15:00.000Z", |
| 323 | - "repository": { "id": 6628874657374920, "node_id": "UmVwb3NpdG9yeTo1YzBlOGI3ZjJkMWE0ZTNiOWY2YTdjOGQ5ZTBmMWEyYg", "name": "website", "full_name": "acme/website", "owner": { "login": "acme" }, "html_url": "https://diffinery.com/acme/website", "clone_url": "https://diffinery.com/acme/website.git" }, | |
| 396 | + "repository": { "id": 6628874657374920, "name": "website", "full_name": "acme/website", "private": false, "visibility": "public", "html_url": "https://diffinery.com/acme/website" }, | |
| 324 | 397 | "sender": { "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90", "login": "diffy", "type": "User" }, |
| 325 | 398 | "issue": { "number": 12, "html_url": "https://diffinery.com/acme/website/issues/12" } |
| 326 | 399 | } |
| @@ -332,7 +405,7 @@ | ||
| 332 | 405 | "number": 12, |
| 333 | 406 | "at": "2026-10-13T08:20:00.000Z", |
| 334 | 407 | "assignees": ["review-bot"], |
| 335 | - "repository": { "id": 6628874657374920, "node_id": "UmVwb3NpdG9yeTo1YzBlOGI3ZjJkMWE0ZTNiOWY2YTdjOGQ5ZTBmMWEyYg", "name": "website", "full_name": "acme/website", "owner": { "login": "acme" }, "html_url": "https://diffinery.com/acme/website", "clone_url": "https://diffinery.com/acme/website.git" }, | |
| 408 | + "repository": { "id": 6628874657374920, "name": "website", "full_name": "acme/website", "private": false, "visibility": "public", "html_url": "https://diffinery.com/acme/website" }, | |
| 336 | 409 | "sender": { "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90", "login": "diffy", "type": "User" }, |
| 337 | 410 | "issue": { "number": 12, "html_url": "https://diffinery.com/acme/website/issues/12" } |
| 338 | 411 | } |
| @@ -363,7 +436,7 @@ | ||
| 363 | 436 | "number": 42, |
| 364 | 437 | "commentId": "3e2d1c0b9a8f4e7d6c5b4a3f2e1d0c9b", |
| 365 | 438 | "at": "2026-10-13T09:40:00.000Z", |
| 366 | - "repository": { "id": 6628874657374920, "node_id": "UmVwb3NpdG9yeTo1YzBlOGI3ZjJkMWE0ZTNiOWY2YTdjOGQ5ZTBmMWEyYg", "name": "website", "full_name": "acme/website", "owner": { "login": "acme" }, "html_url": "https://diffinery.com/acme/website", "clone_url": "https://diffinery.com/acme/website.git" }, | |
| 439 | + "repository": { "id": 6628874657374920, "name": "website", "full_name": "acme/website", "private": false, "visibility": "public", "html_url": "https://diffinery.com/acme/website" }, | |
| 367 | 440 | "sender": { "id": "f0e1d2c3b4a5968778695a4b3c2d1e0f", "login": "review-bot", "type": "Bot", "on_behalf_of": "diffy" }, |
| 368 | 441 | "issue": { "number": 42, "html_url": "https://diffinery.com/acme/website/issues/42" } |
| 369 | 442 | } |
| @@ -393,7 +466,7 @@ | ||
| 393 | 466 | "runId": "8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a", |
| 394 | 467 | "conclusion": "success", |
| 395 | 468 | "at": "2026-10-13T09:36:21.000Z", |
| 396 | - "repository": { "id": 6628874657374920, "node_id": "UmVwb3NpdG9yeTo1YzBlOGI3ZjJkMWE0ZTNiOWY2YTdjOGQ5ZTBmMWEyYg", "name": "website", "full_name": "acme/website", "owner": { "login": "acme" }, "html_url": "https://diffinery.com/acme/website", "clone_url": "https://diffinery.com/acme/website.git" }, | |
| 469 | + "repository": { "id": 6628874657374920, "name": "website", "full_name": "acme/website", "private": false, "visibility": "public", "html_url": "https://diffinery.com/acme/website" }, | |
| 397 | 470 | "sender": { "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90", "login": "diffy", "type": "User" } |
| 398 | 471 | } |
| 399 | 472 | ``` |
| @@ -463,7 +536,7 @@ | ||
| 463 | 536 | "reactions": { "url": "https://diffinery.com/api/v3/repos/acme/website/releases/7/reactions", "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "confused": 0, "heart": 0, "hooray": 0, "rocket": 0, "eyes": 0 }, |
| 464 | 537 | "latest": true |
| 465 | 538 | }, |
| 466 | - "repository": { "id": 6628874657374920, "node_id": "UmVwb3NpdG9yeTo1YzBlOGI3ZjJkMWE0ZTNiOWY2YTdjOGQ5ZTBmMWEyYg", "name": "website", "full_name": "acme/website", "owner": { "login": "acme" }, "html_url": "https://diffinery.com/acme/website", "clone_url": "https://diffinery.com/acme/website.git" }, | |
| 539 | + "repository": { "id": 6628874657374920, "name": "website", "full_name": "acme/website", "private": false, "visibility": "public", "html_url": "https://diffinery.com/acme/website" }, | |
| 467 | 540 | "sender": { "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90", "login": "diffy", "type": "User" } |
| 468 | 541 | } |
| 469 | 542 | ``` |
| @@ -498,7 +571,7 @@ | ||
| 498 | 571 | "commit": { "sha": "9c4d8e2f1a0b3c5d7e9f2a4b6c8d0e1f3a5b7c9d" }, |
| 499 | 572 | "created_at": "2026-10-13T09:40:00.000Z", |
| 500 | 573 | "updated_at": "2026-10-13T09:40:00.000Z", |
| 501 | - "repository": { "id": 6628874657374920, "node_id": "UmVwb3NpdG9yeTo1YzBlOGI3ZjJkMWE0ZTNiOWY2YTdjOGQ5ZTBmMWEyYg", "name": "website", "full_name": "acme/website", "owner": { "login": "acme" }, "html_url": "https://diffinery.com/acme/website", "clone_url": "https://diffinery.com/acme/website.git" }, | |
| 574 | + "repository": { "id": 6628874657374920, "name": "website", "full_name": "acme/website", "private": false, "visibility": "public", "html_url": "https://diffinery.com/acme/website" }, | |
| 502 | 575 | "sender": { "id": "c1b2a3d4e5f60718293a4b5c6d7e8f9a", "login": "ci-bot", "type": "User" } |
| 503 | 576 | } |
| 504 | 577 | ``` |
| @@ -507,8 +580,11 @@ | ||
| 507 | 580 | |
| 508 | 581 | ## deployment |
| 509 | 582 | |
| 510 | -Sent when a deployment is created with `POST /repos/{owner}/{repo}/deployments`. | |
| 583 | +Sent when a deployment is created: | |
| 511 | 584 | |
| 585 | +- with `POST /repos/{owner}/{repo}/deployments` (sender: who created it); | |
| 586 | +- when a workflow job with `environment:` starts (sender: who started the run), as on GitHub. The `deployment` event comes first, then a `deployment_status` event with `in_progress`. Such a deployment has the task `deploy`, `description` `null`, `payload` `{}`, and `ref` set to the branch or tag name the run is for. | |
| 587 | + | |
| 512 | 588 | | Action | Sent when | |
| 513 | 589 | |---|---| |
| 514 | 590 | | `created` | A deployment is created | |
| @@ -520,13 +596,11 @@ | ||
| 520 | 596 | | `deployment.sha` | string | The commit being deployed | |
| 521 | 597 | | `deployment.ref` | string | The ref as it was given (branch, tag or SHA) | |
| 522 | 598 | | `deployment.task` | string | The task, `deploy` by default | |
| 523 | -| `deployment.environment` | string | The environment, `production` by default | | |
| 599 | +| `deployment.environment` | string | The environment, `production` by default. Environment names are case-insensitive: a deployment to `Production` when a `production` environment exists reports `production`. | | |
| 524 | 600 | | `deployment.description` | string or null | The description | |
| 525 | 601 | | `deployment.payload` | any | The `payload` sent when creating the deployment: the JSON value, or the string when it wasn't JSON. `{}` by default. | |
| 526 | 602 | | `deployment.created_at` | string | Creation time (ISO 8601 with milliseconds) | |
| 527 | 603 | |
| 528 | -Workflow jobs with `environment:` record a deployment too, but they don't send a `deployment` event: watch `deployment_status` for those. | |
| 529 | - | |
| 530 | 604 | ```json |
| 531 | 605 | { |
| 532 | 606 | "action": "created", |
| @@ -540,7 +614,7 @@ | ||
| 540 | 614 | "payload": { "version": "1.2.0" }, |
| 541 | 615 | "created_at": "2026-10-13T10:05:00.000Z" |
| 542 | 616 | }, |
| 543 | - "repository": { "id": 6628874657374920, "node_id": "UmVwb3NpdG9yeTo1YzBlOGI3ZjJkMWE0ZTNiOWY2YTdjOGQ5ZTBmMWEyYg", "name": "website", "full_name": "acme/website", "owner": { "login": "acme" }, "html_url": "https://diffinery.com/acme/website", "clone_url": "https://diffinery.com/acme/website.git" }, | |
| 617 | + "repository": { "id": 6628874657374920, "name": "website", "full_name": "acme/website", "private": false, "visibility": "public", "html_url": "https://diffinery.com/acme/website" }, | |
| 544 | 618 | "sender": { "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90", "login": "diffy", "type": "User" } |
| 545 | 619 | } |
| 546 | 620 | ``` |
| @@ -553,7 +627,7 @@ | ||
| 553 | 627 | - when a workflow job with `environment:` starts (`in_progress`, sender: who started the run); |
| 554 | 628 | - when that job finishes (`success`; `inactive` when it was cancelled or skipped; `failure` otherwise; sender: who started the run). |
| 555 | 629 | |
| 556 | -When a `success` status makes older deployments to the same environment `inactive`, those `inactive` statuses don't send events. | |
| 630 | +When a `success` status makes older deployments (created earlier) to the same environment `inactive`, those `inactive` statuses don't send events. Deployments created later than the one that succeeded are left as they are. | |
| 557 | 631 | |
| 558 | 632 | | Action | Sent when | |
| 559 | 633 | |---|---| |
| @@ -590,7 +664,7 @@ | ||
| 590 | 664 | "task": "deploy", |
| 591 | 665 | "environment": "production" |
| 592 | 666 | }, |
| 593 | - "repository": { "id": 6628874657374920, "node_id": "UmVwb3NpdG9yeTo1YzBlOGI3ZjJkMWE0ZTNiOWY2YTdjOGQ5ZTBmMWEyYg", "name": "website", "full_name": "acme/website", "owner": { "login": "acme" }, "html_url": "https://diffinery.com/acme/website", "clone_url": "https://diffinery.com/acme/website.git" }, | |
| 667 | + "repository": { "id": 6628874657374920, "name": "website", "full_name": "acme/website", "private": false, "visibility": "public", "html_url": "https://diffinery.com/acme/website" }, | |
| 594 | 668 | "sender": { "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90", "login": "diffy", "type": "User" } |
| 595 | 669 | } |
| 596 | 670 | ``` |
| @@ -616,7 +690,7 @@ | ||
| 616 | 690 | "action": "deploy-docs", |
| 617 | 691 | "branch": "main", |
| 618 | 692 | "client_payload": { "version": "1.2.0" }, |
| 619 | - "repository": { "id": 6628874657374920, "node_id": "UmVwb3NpdG9yeTo1YzBlOGI3ZjJkMWE0ZTNiOWY2YTdjOGQ5ZTBmMWEyYg", "name": "website", "full_name": "acme/website", "owner": { "login": "acme" }, "html_url": "https://diffinery.com/acme/website", "clone_url": "https://diffinery.com/acme/website.git" }, | |
| 693 | + "repository": { "id": 6628874657374920, "name": "website", "full_name": "acme/website", "private": false, "visibility": "public", "html_url": "https://diffinery.com/acme/website" }, | |
| 620 | 694 | "sender": { "id": "f0e1d2c3b4a5968778695a4b3c2d1e0f", "login": "review-bot", "type": "Bot", "on_behalf_of": "diffy" } |
| 621 | 695 | } |
| 622 | 696 | ``` |
| @@ -638,7 +712,7 @@ | ||
| 638 | 712 | "workflow": ".github/workflows/deploy.yml", |
| 639 | 713 | "inputs": { "environment": "staging", "version": "1.2.0", "dry-run": false }, |
| 640 | 714 | "at": "2026-10-13T10:15:00.000Z", |
| 641 | - "repository": { "id": 6628874657374920, "node_id": "UmVwb3NpdG9yeTo1YzBlOGI3ZjJkMWE0ZTNiOWY2YTdjOGQ5ZTBmMWEyYg", "name": "website", "full_name": "acme/website", "owner": { "login": "acme" }, "html_url": "https://diffinery.com/acme/website", "clone_url": "https://diffinery.com/acme/website.git" }, | |
| 715 | + "repository": { "id": 6628874657374920, "name": "website", "full_name": "acme/website", "private": false, "visibility": "public", "html_url": "https://diffinery.com/acme/website" }, | |
| 642 | 716 | "sender": { "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90", "login": "diffy", "type": "User" } |
| 643 | 717 | } |
| 644 | 718 | ``` |
| @@ -647,8 +721,10 @@ | ||
| 647 | 721 | |
| 648 | 722 | ## Events that are never delivered |
| 649 | 723 | |
| 650 | -Audit entries (sign-ins, push protection decisions, settings changes) are not sent to webhooks. They are in the [audit log](/docs/security/audit-log). Check runs and check suites (GitHub's `check_run` and `check_suite`) send no events either; commit statuses send `status`. | |
| 724 | +Audit entries (sign-ins, push protection decisions, settings changes) are not sent to webhooks. They are in the [audit log](/docs/security/audit-log). Check runs and check suites (GitHub's `check_run` and `check_suite`) send no events either; commit statuses send `status`. Transferring or renaming a repository sends no event (GitHub sends a `repository` event); a webhook keeps receiving deliveries afterwards, with the new `full_name` and `html_url`. | |
| 651 | 725 | |
| 726 | +An [import from GitHub](/docs/migrate-from-github) sends no events while it fills a repository: the branches and tags it pushes send no `push`, and the issues, pull requests, comments, reviews and releases it copies send no `issues`, `pull_request`, `issue_comment`, `pull_request_review` or `release`, so a webhook added during an import isn't flooded with old history and no workflows start. Changes that people, agents and workflows make send events as usual. A repository imported with a homepage on GitHub has it as `homepage` in the repository object; otherwise `homepage` is `null`. | |
| 727 | + | |
| 652 | 728 | ## Comparing with GitHub payloads |
| 653 | 729 | |
| 654 | 730 | | Event | Fields GitHub sends that Diffinery does not | |
| @@ -666,4 +742,4 @@ | ||
| 666 | 742 | | `repository_dispatch` | Nothing: the same `action`, `branch` and `client_payload` | |
| 667 | 743 | | `workflow_dispatch` | Nothing: the same `inputs`, `ref` and `workflow` (inputs keep their types) | |
| 668 | 744 | |
| 669 | -All events also lack GitHub's `installation` and `organization` objects, and the `repository` object has only the fields shown above. Use the [REST API](/docs/api) to fetch any detail you need, keyed by `number`, `commentId` or `runId`. See [Webhooks](/docs/repositories/webhooks) for delivery behavior and signature verification. | |
| 745 | +All events also lack GitHub's `installation` and `organization` objects. The `repository` object is complete apart from counters (always `0`) and `permissions`. Use the [REST API](/docs/api) to fetch any detail you need, keyed by `number`, `commentId` or `runId`. See [Webhooks](/docs/repositories/webhooks) for delivery behavior and signature verification. |
modifiedapps/diffinery/docs/repositories/branch-protection.md+26−12View file
| @@ -109,15 +109,13 @@ | ||
| 109 | 109 | - An entry only has an effect for a name that is also in `requiredChecks`. |
| 110 | 110 | - Binding a check to an agent's login makes it impossible to satisfy, because agents' reports never count. |
| 111 | 111 | |
| 112 | -`requiredCheckSources` can only be set through the API. The **Branch protection** page does not show it. | |
| 112 | +`requiredCheckSources` can only be set through the API. The **Branch protection** page does not show it, but saving a rule there keeps the bindings of every check that is still listed in **Required status checks**, also when you change the rule's pattern. A binding for a check you remove from **Required status checks** is dropped with it; to require and bind that check again, send the rule with `PUT .../protection`. | |
| 113 | 113 | |
| 114 | -> **Important:** Saving a rule from the **Branch protection** page in the web UI replaces the whole rule with the form's fields, which removes `requiredCheckSources`. After editing a rule in the web UI, send the binding again with `PUT .../protection`. | |
| 115 | - | |
| 116 | 114 | ### Approvals |
| 117 | 115 | |
| 118 | 116 | Diffinery counts each reviewer's most recent approving or "request changes" review: |
| 119 | 117 | |
| 120 | -- Only approvals from people count. Agent approvals are advisory and never count. | |
| 118 | +- Only approvals from people count. Approvals from agents and from a workflow's `GITHUB_TOKEN` are advisory and never count. (A job token can approve at all only when the repository allows workflows to approve pull requests; see [Pull requests and workflow tokens](/docs/collaboration/pull-requests#pull-requests-and-workflow-tokens).) | |
| 121 | 119 | - You cannot approve your own pull request, and approving requires the write role. |
| 122 | 120 | - With `dismissStaleReviews`, only approvals given on the pull request's current head commit count. |
| 123 | 121 | - If a pull request was opened by an agent, it needs at least one approval from a person even when `requiredApprovals` is `0`. |
| @@ -145,7 +143,7 @@ | ||
| 145 | 143 | - A workflow's `GITHUB_TOKEN` is held to the same rule as agents (and needs `contents: write`), even when the run's code didn't come from an agent. |
| 146 | 144 | - The agent's access is checked again when the merge happens, and auto-merge turns itself off, with a comment saying why, if the rule no longer allows it, or if someone else pushes to the pull request's branch. |
| 147 | 145 | |
| 148 | -Turning on auto-merge, turning it off and the merge itself are audited (`pull_request.auto_merge_enabled`, `pull_request.auto_merge_disabled`, `pull_request.auto_merged`). | |
| 146 | +Turning on auto-merge, turning it off and the merge itself are audited (`pull_request.auto_merge_enabled`, `pull_request.auto_merge_disabled`, `pull_request.auto_merged`, or `pull_request.auto_merge_enqueued` when it joins a merge queue). How to turn auto-merge on and off in the web UI, the API and MCP, and when it turns itself off, is in [Auto-merge](/docs/collaboration/pull-requests#auto-merge). | |
| 149 | 147 | |
| 150 | 148 | #### Waiving approvals for agents |
| 151 | 149 | |
| @@ -156,6 +154,16 @@ | ||
| 156 | 154 | - A person's requested changes still block the merge, and a pull request that changes a workflow file still needs a person to merge it. |
| 157 | 155 | - It is set per branch rule, only by people with the admin role, and never comes in with an import. Turning `allowAgentAutoMerge` off turns this off too. Every change is audited as `repo.protection.agent_approval_waiver`. |
| 158 | 156 | |
| 157 | +### Workflow tokens | |
| 158 | + | |
| 159 | +A workflow job's `GITHUB_TOKEN` is held to branch protection like a person with the write role: | |
| 160 | + | |
| 161 | +- It never has more than the write role, whatever its `permissions:` say, so it can't bypass any rule as an admin, and it can't read or change branch protection rules (`this endpoint is not available to a workflow's GITHUB_TOKEN`). | |
| 162 | +- Pushes and merges with it need `contents: write`. | |
| 163 | +- It can't create, change or delete files under `.github/workflows/` or `.diffinery/workflows/`, by push, API commit or merge: the update is rejected with ``refusing to allow a GitHub App to create or update workflow `<path>` without `workflows` permission``. | |
| 164 | +- In a run whose code came from an agent (an agent started the run, pushed the branch last, or opened the pull request, and re-runs of such runs and `workflow_run` runs that follow them), the token acts as an agent: the agent rules above apply to it. | |
| 165 | +- Its approvals never count toward `requiredApprovals`. | |
| 166 | + | |
| 159 | 167 | ### Merge queue settings |
| 160 | 168 | |
| 161 | 169 | `mergeQueue` turns on a merge queue for matching branches. Pull requests are then added to the queue instead of being merged directly, and each is tested on top of the base branch and the pull requests ahead of it. See [Merge queue](/docs/collaboration/merge-queue) for how the queue works. |
| @@ -177,13 +185,13 @@ | ||
| 177 | 185 | | Action | Admin bypass (when `enforceAdmins` is off) | |
| 178 | 186 | |---|---| |
| 179 | 187 | | Direct push, force push or deletion on a protected branch | Automatic for admins. An agent with the admin role gets this only on branches where `agentsMayPush` is on. | |
| 180 | -| Merging a pull request that has blockers | Only on request, and only for admins who are people, when the pull request has no merge conflicts and is not a draft. In the web UI the button reads **Merge without waiting for requirements (bypass)** and asks for confirmation. Through the API send `"bypass_rules": true` on `PUT /repos/{owner}/{repo}/pulls/{number}/merge`; with the CLI use `diffinery pr merge 12 --admin`. Without the flag the merge fails with `405`, and the message ends with `(as an admin you can bypass with bypass_rules)`. | | |
| 181 | -| A merge commit on a branch that requires linear history | Refused with `405` and `main requires linear history; use squash or rebase`, unless the admin asks for a bypass as above. | | |
| 188 | +| Merging a pull request that has blockers | Only on request, and only for admins who are people, when the pull request has no merge conflicts and is not a draft. In the web UI the button reads **Merge without waiting for requirements (bypass)** and asks for confirmation; it isn't shown when the rule sets `enforceAdmins`. Through the API send `"bypass_rules": true` on `PUT /repos/{owner}/{repo}/pulls/{number}/merge`; with the CLI use `diffinery pr merge 12 --admin`. Without the flag the merge fails with `405`, and the message ends with `(as an admin you can bypass with bypass_rules)`. | | |
| 189 | +| A merge commit on a branch that requires linear history | Refused with `405` and `main requires linear history; use squash or rebase`, unless the admin sends `bypass_rules`. The web merge form doesn't offer **Create a merge commit** on such a branch at all. | | |
| 182 | 190 | | Merging directly when a merge queue is required | Only on request: **Merge now (bypass the queue)** in the web UI, `"bypass_merge_queue": true` on `PUT /repos/{owner}/{repo}/pulls/{number}/merge`, or `diffinery pr merge 12 --admin`. This skips only the queue: a pull request that doesn't meet the rules also needs `"bypass_rules": true`. In the web UI the button then reads **Merge now (bypass the queue and the unmet requirements)** and sends both. | |
| 183 | 191 | | Deleting the default branch | Never allowed. | |
| 184 | 192 | | Agent rules | Never bypassed by admin status for merges. | |
| 185 | 193 | |
| 186 | -A merge that bypassed unmet blockers is recorded in the [audit log](/docs/security/audit-log#git-and-pull-requests) as `pull_request.merge_bypassed`, with the pull request (`acme/website#42`) as the target and the `blockers` and `base` branch in the detail. See [Admin bypass](/docs/collaboration/pull-requests#admin-bypass). | |
| 194 | +A merge that bypassed anything is recorded in the [audit log](/docs/security/audit-log#git-and-pull-requests) as `pull_request.merge_bypassed`, with the pull request (`acme/website#42`) as the target and the `base` branch and bypassed `blockers` in the detail: the unmet rules, `linear history required` for a merge commit on a linear-history branch, and `merge queue` for a skipped queue. See [Admin bypass](/docs/collaboration/pull-requests#admin-bypass). | |
| 187 | 195 | |
| 188 | 196 | With `enforceAdmins` on, admins follow the same rules as everyone else. |
| 189 | 197 | |
| @@ -198,8 +206,10 @@ | ||
| 198 | 206 | 5. To use a merge queue, check **Require a merge queue** and choose the **Merge method**, **Build concurrency** and **Check timeout (minutes)**. |
| 199 | 207 | 6. Select **Create rule**. |
| 200 | 208 | |
| 201 | -Existing rules are listed above the form with a summary such as `PR required · 1 approval · 2 required checks`. Click a rule to expand and edit it, then select **Save rule**. If you change the pattern of an existing rule, the old pattern's rule is replaced, and the rule counts as newly created when several rules match a branch (see [Which rule applies](#which-rule-applies)). Select **Delete** next to a rule to remove it. | |
| 209 | +Existing rules are listed above the form with a summary such as `PR required · 1 approval · 2 required checks`. Click a rule to expand and edit it, then select **Save rule**. You see `Branch protection saved.` Saving keeps the rule's `requiredCheckSources` bindings for the checks that are still required. If you change the pattern of an existing rule, the old pattern's rule is replaced, and the rule counts as newly created when several rules match a branch (see [Which rule applies](#which-rule-applies)). Select **Delete** next to a rule to remove it. | |
| 202 | 210 | |
| 211 | +Changes are recorded in the [audit log](/docs/security/audit-log) as `repo.protection.set` and `repo.protection.delete`, whether they come from the web UI or the API (API changes carry `"via": "api"` in the detail). On an archived repository, rules can't be changed (`repository is archived (read-only)`). | |
| 212 | + | |
| 203 | 213 | The repository's branches list marks protected branches with a **protected** badge, and `GET /api/v3/repos/{owner}/{repo}/branches` returns `"protected": true` for them. |
| 204 | 214 | |
| 205 | 215 | ## Managing rules with the API |
| @@ -321,6 +331,8 @@ | ||
| 321 | 331 | 9. A non-fast-forward update needs `allowForcePush`. |
| 322 | 332 | 10. With `requireLinearHistory`, new commits must not include merge commits (the first 1,000 new commits are checked). |
| 323 | 333 | |
| 334 | +A workflow's `GITHUB_TOKEN` is also refused, right after step 2, when the update creates, changes or deletes a file under `.github/workflows/` or `.diffinery/workflows/`. A new branch is compared with where it branched off the default branch. | |
| 335 | + | |
| 324 | 336 | A rejected ref shows the reason in git's output: |
| 325 | 337 | |
| 326 | 338 | ```text |
| @@ -342,6 +354,7 @@ | ||
| 342 | 354 | | `merge queue branches are read-only` | `gh-readonly-queue/...` branches belong to the merge queue. | |
| 343 | 355 | | `refs/pull/ is managed by Diffinery for pull requests` | Pull request refs cannot be pushed. | |
| 344 | 356 | | `missing objects` | The update points at a commit the server doesn't have. Push the commits too, or fetch the full history first. | |
| 357 | +| ``refusing to allow a GitHub App to create or update workflow `<path>` without `workflows` permission`` | A workflow's `GITHUB_TOKEN` tried to add, change or delete a workflow file. Push it as a person or with a personal token. | | |
| 345 | 358 | |
| 346 | 359 | ## Merge blockers |
| 347 | 360 | |
| @@ -361,8 +374,9 @@ | ||
| 361 | 374 | | `required check '<name>' <conclusion>` | The check finished with a conclusion other than success, skipped or neutral, for example `required check 'ci / test' failure`. A commit status in the `failure` or `error` state shows as `failure`. | |
| 362 | 375 | | `branch is behind its base; update it (linear history required)` | With `requireLinearHistory`, the head branch must contain the latest base commit. | |
| 363 | 376 | | `write access is required to merge` | Your role is below write. | |
| 364 | -| `agents may not merge into this branch; a human must merge` | An agent tried to merge into the default branch or a protected branch without `agentsMayMerge`. Agents can [land changes through auto-merge](#agents-landing-changes) instead. | | |
| 377 | +| `agents may not merge into this branch; a human must merge` | An agent (or a workflow token in a run whose code came from an agent) tried to merge into the default branch or a protected branch without `agentsMayMerge`. Agents can [land changes through auto-merge](#agents-landing-changes) instead. | | |
| 365 | 378 | | `the pull request changes workflow file <path>; a person must merge it` | An agent's auto-merge or merge queue entry relies on `allowAgentAutoMerge`, and the pull request changes a workflow. | |
| 379 | +| ``refusing to allow a GitHub App to create or update workflow `<path>` without `workflows` permission`` | A workflow's `GITHUB_TOKEN` tried to merge a pull request that changes a workflow file. | | |
| 366 | 380 | |
| 367 | 381 | Example response from `GET /api/v3/repos/acme/website/pulls/42`, trimmed: |
| 368 | 382 | |
| @@ -418,7 +432,7 @@ | ||
| 418 | 432 | - Required Actions check names include the workflow name: `ci / test`. |
| 419 | 433 | - Rules have no options for restricting pushes to specific users or teams, requiring signed commits, requiring conversation resolution or requiring code owner reviews. |
| 420 | 434 | - A "changes requested" review blocks merging even on branches without a rule. |
| 421 | -- An admin's merge past unmet rules must be asked for with `bypass_rules` (or `bypass_merge_queue`) and is written to the audit log as `pull_request.merge_bypassed`. | |
| 435 | +- An admin's merge past unmet rules must be asked for with `bypass_rules`, and skipping a merge queue with `bypass_merge_queue`; each covers only what it names, and both are written to the audit log as `pull_request.merge_bypassed`. | |
| 422 | 436 | - When several rules match, the choice follows GitHub: a rule naming the branch exactly wins, then the oldest rule. |
| 423 | 437 | |
| 424 | 438 | ## Troubleshooting |
| @@ -429,7 +443,7 @@ | ||
| 429 | 443 | | A required check never turns green | The name does not match. Compare it with the checks list on the pull request, including the `workflow / ` prefix. | |
| 430 | 444 | | A check shows as passed on the pull request but the merge says `has not run` | The passing report doesn't count: it came from an agent, or from an account other than the one bound in `requiredCheckSources`, or Diffinery Actions also reported a check with the same name. Report the check from the bound account, or change the binding. | |
| 431 | 445 | | `invalid source for required check '<name>'` | A value in `requiredCheckSources` is not a valid login. Use the reporter's login or `diffinery-actions`. | |
| 432 | -| `requiredCheckSources` disappeared | The rule was saved from the web UI, which doesn't keep it. Send it again with `PUT .../protection`. | | |
| 446 | +| A `requiredCheckSources` binding disappeared | The check was removed from **Required status checks** when the rule was saved in the web UI, which drops the bindings of checks that are no longer required. Send the rule again with `PUT .../protection`. | | |
| 433 | 447 | | An agent's pull request needs an approval even with `requiredApprovals: 0` | Agent-authored pull requests always need one approval from a person. | |
| 434 | 448 | | An approval disappeared after a push | `dismissStaleReviews` is on. Ask the reviewer to approve the new commits. | |
| 435 | 449 | | Your admin push was rejected | The rule sets `enforceAdmins`, or your token has the `write` scope instead of `admin`. | |
modifiedapps/diffinery/docs/repositories/browsing-code.md+5−1View file
| @@ -175,6 +175,8 @@ | ||
| 175 | 175 | | `enter a valid file path` | The path is empty or contains `..` or `.git`. | |
| 176 | 176 | | `invalid path "<path>": ...` | A part of the path is a name Git reserves (such as `.GIT` or `git~1`), contains `\`, or is longer than 255 characters. Choose another name. | |
| 177 | 177 | | `a commit message is required` | Fill in the commit message. | |
| 178 | +| `push protection: possible secret detected (<kind>) in <path>. Remove it, or rotate it and ask an admin.` | The new content looks like a credential, such as an API key or private key. Remove it and commit again. See [Push protection](/docs/security/push-protection). | | |
| 179 | +| `repository is archived (read-only)` | The repository is archived; nothing can be committed until an admin unarchives it. | | |
| 178 | 180 | |
| 179 | 181 | Without a browser or a Git client, you can change files with the REST API: |
| 180 | 182 | |
| @@ -182,8 +184,10 @@ | ||
| 182 | 184 | - `DELETE /api/v3/repos/{owner}/{repo}/contents/{path}` deletes one file, with `message` and the file's `sha`. |
| 183 | 185 | - `POST /api/v3/repos/{owner}/{repo}/diffinery/commits` (or the MCP `commit_files` tool) changes several files in one commit. |
| 184 | 186 | |
| 185 | -Branch protection and agent guardrails apply to all of them, as for a `git push`. `PUT .../contents` (and `POST .../git/blobs`) also scan the new content for credentials and refuse it with `push protection: possible secret detected (...)` (HTTP 422). See [REST API](/docs/reference/rest-api) and [MCP](/docs/agents/mcp). | |
| 187 | +Branch protection and agent guardrails apply to all of them, as for a `git push`. Push protection does too: the web editor, `PUT .../contents`, `POST .../diffinery/commits`, MCP `commit_files` (and `POST .../git/blobs`) scan the new content for credentials before anything is written, and refuse it with HTTP 422 and `push protection: possible secret detected (...)`. A repository admin who is a person can commit a flagged file anyway through `POST .../diffinery/commits` by adding `"allow_secrets": "<reason>"`; the bypass is recorded in the audit log and emailed to the repository's admins. See [Push protection](/docs/security/push-protection), [REST API](/docs/reference/rest-api) and [MCP](/docs/agents/mcp). | |
| 186 | 188 | |
| 189 | +A workflow's `GITHUB_TOKEN` can use these endpoints when the job has `contents: write`, but it can't create, change or delete files under `.github/workflows/` or `.diffinery/workflows/`: that fails with ``refusing to allow a GitHub App to create or update workflow `<path>` without `workflows` permission``. | |
| 190 | + | |
| 187 | 191 | ## Commits |
| 188 | 192 | |
| 189 | 193 | `/acme/website/commits/<ref>` lists the commits reachable from a branch, tag or commit, 30 per page, grouped by day ("Commits on October 13, 2026"), with **Newer** and **Older** buttons. Add `?path=src/app.ts` (the **History** button does this) to see only commits that changed that file or folder; the file history looks at the most recent 2,000 commits. |
modifiedapps/diffinery/docs/repositories/git-lfs.md+15−10View file
| @@ -37,19 +37,22 @@ | ||
| 37 | 37 | |
| 38 | 38 | LFS uses the same credentials as Git: a personal access token as the password, through your credential helper or `diffinery auth setup-git` (see [Git over HTTPS](/docs/repositories/git#authentication)). |
| 39 | 39 | |
| 40 | +If you also pass the token with `http.extraHeader` (as `actions/checkout` does when it keeps credentials), `git-lfs` sends that header and the batch response's `Authorization` header together on each transfer. Diffinery accepts the repeated header and uses the first value, so this works. A `Basic` credential that isn't valid base64 gets `401 malformed basic credentials`. | |
| 41 | + | |
| 40 | 42 | | Operation | Needs | |
| 41 | 43 | | --- | --- | |
| 42 | 44 | | Download from a public repository | Nothing | |
| 43 | 45 | | Download from a private repository | A token with at least the `read` scope, for an account with read access | |
| 44 | 46 | | Upload | A token with the `write` or `admin` scope, for an account with write access. The repository must not be archived. | |
| 45 | 47 | |
| 46 | -The batch request is always a `POST`, but a batch request for `download` counts as a read, so anonymous callers (on public repositories), `read`-scoped tokens and job tokens can download LFS files. A batch request for `upload` and the `PUT` that sends the file are writes: | |
| 48 | +The batch request is always a `POST`, but a batch request for `download` counts as a read, so anonymous callers (on public repositories), `read`-scoped tokens and job tokens with `contents: read` can download LFS files. A batch request for `upload` and the `PUT` that sends the file are writes: | |
| 47 | 49 | |
| 48 | 50 | | Caller | Upload refused with | |
| 49 | 51 | | --- | --- | |
| 50 | 52 | | No credentials | `401 authentication required` (the `git-lfs` client then asks for credentials) | |
| 51 | 53 | | A `read`-scoped token | `403 this token needs the write scope` | |
| 52 | -| A token whose account has less than write access, or a job token | `403 write access required` | | |
| 54 | +| A token whose account has less than write access | `403 write access required` | | |
| 55 | +| A workflow's job token without `contents: write` | `403 Resource not accessible by integration (the workflow's GITHUB_TOKEN needs permissions: contents: write)` | | |
| 53 | 56 | | Anyone, on an archived repository | `403 repository is archived (read-only)` | |
| 54 | 57 | |
| 55 | 58 | Agents can upload LFS objects like any account with write access. Branch protection applies to the commits that reference the objects, not to the LFS upload itself. |
| @@ -129,26 +132,26 @@ | ||
| 129 | 132 | |
| 130 | 133 | ## Use LFS files in workflows |
| 131 | 134 | |
| 132 | -The `actions/checkout` step in Diffinery workflows doesn't download LFS files: with `lfs: true` it prints the warning `Git LFS is not supported by the Diffinery checkout built-in; LFS pointers will not be resolved.` and leaves pointer files in the workspace. | |
| 135 | +As on GitHub, `actions/checkout` downloads LFS files when you set `lfs: true` (the default is `false`, which leaves pointer files in the workspace). With `submodules: true` or `recursive`, it also pulls the submodules' LFS files. Hosted runners have `git-lfs` installed: | |
| 133 | 136 | |
| 134 | -Download them in a step after the checkout. Hosted runners have `git-lfs` installed, and the checkout leaves the job's token configured for the repository's URL (unless you set `persist-credentials: false`), so this works without extra credentials: | |
| 135 | - | |
| 136 | 137 | ```yaml |
| 137 | 138 | name: Build |
| 138 | 139 | on: push |
| 139 | 140 | jobs: |
| 140 | 141 | build: |
| 141 | 142 | runs-on: ubuntu-latest |
| 143 | + permissions: | |
| 144 | + contents: read | |
| 142 | 145 | steps: |
| 143 | 146 | - uses: actions/checkout@v4 |
| 144 | - - name: Download LFS files | |
| 145 | - run: git lfs pull | |
| 147 | + with: | |
| 148 | + lfs: true | |
| 146 | 149 | - run: ls -lh assets/ |
| 147 | 150 | ``` |
| 148 | 151 | |
| 149 | -To download only some files, use `git lfs pull --include="assets/video/**"`. The job token can read the repository but not upload LFS objects; use a secret holding a personal or agent token to push from a workflow. On self-hosted runners, install `git-lfs` on the machine first. | |
| 152 | +You can also download them in a later step. The checkout leaves the job's token configured for the repository's URL (unless you set `persist-credentials: false`), so `git lfs pull` works without extra credentials. To download only some files, use `git lfs pull --include="assets/video/**"`. | |
| 150 | 153 | |
| 151 | -Other differences in the checkout step: `sparse-checkout` is also ignored with a warning (a full checkout is done). See [Compatibility with GitHub Actions](/docs/actions/compatibility). | |
| 154 | +The job token reads LFS files with `contents: read` (the default for new repositories) and uploads them with `contents: write`, as on GitHub. On self-hosted runners, install `git-lfs` on the machine first. See [Compatibility with GitHub Actions](/docs/actions/compatibility). | |
| 152 | 155 | |
| 153 | 156 | ## Moving LFS files from GitHub |
| 154 | 157 | |
| @@ -184,5 +187,7 @@ | ||
| 184 | 187 | | Upload fails with HTTP 403 | Your token is `read`-scoped (`this token needs the write scope`), your role is below write (`write access required`), or the repository is archived (`repository is archived (read-only)`). Use a token with the `write` scope for an account with write access. | |
| 185 | 188 | | Upload fails with HTTP 401 | The upload had no credentials (`authentication required`). Configure a credential helper or `diffinery auth setup-git`, as in [Endpoint and authentication](#endpoint-and-authentication). | |
| 186 | 189 | | Upload fails for a very large file | Files over 100 MB can't be uploaded. Split the file, or keep it outside the repository. | |
| 187 | -| Workflow sees small text files instead of the real files | The checkout step doesn't fetch LFS files. Add a `git lfs pull` step. | | |
| 190 | +| Workflow sees small text files instead of the real files | The checkout step ran without `lfs: true`. Add `with: lfs: true` to `actions/checkout`, or a `git lfs pull` step after it. | | |
| 191 | +| Upload from a workflow fails with HTTP 403 `Resource not accessible by integration` | The job's token has no `contents: write`. Add `permissions: contents: write` to the job. | | |
| 192 | +| Upload fails with HTTP 401 `malformed basic credentials` | The `Authorization: Basic` value isn't valid base64. Check how the credential is built, or send `Authorization: Bearer dfy_...`. | | |
| 188 | 193 | | `content does not match oid` | The uploaded bytes didn't match the object ID, usually a corrupted local object. Run `git lfs fsck` and push again. | |
modifiedapps/diffinery/docs/repositories/git.md+30−7View file
| @@ -72,7 +72,7 @@ | ||
| 72 | 72 | |
| 73 | 73 | The empty entry resets the helper list for this host, so a generic helper such as your OS keychain doesn't answer first. The helper answers only for the configured Diffinery host and protocol, with username `x-token` and the token from the config file or the `DIFFINERY_TOKEN` environment variable. It never stores anything (`store` and `erase` are no-ops), so revoking or replacing the token takes effect at once. The `diffinery` command must be on your `PATH` for Git to run it. |
| 74 | 74 | |
| 75 | -> **Note:** Git only asks a credential helper (or you) for a token when the server answers `401`. Private repositories do that. When you push to a **public** repository without credentials, the server answers `403` (`write access required`) instead, so Git never asks and the push fails with `The requested URL returned error: 403`. Pass the token with the push itself, for example `git -c http.extraHeader="Authorization: Bearer $DIFFINERY_TOKEN" push`, as shown in [Other ways to pass the token](#other-ways-to-pass-the-token). | |
| 75 | +Git only asks a credential helper (or you) for a token when the server answers `401`. Diffinery answers `401 authentication required` with a `WWW-Authenticate: Basic realm="Diffinery"` challenge to every anonymous push, to public repositories too, and to every anonymous clone or fetch of a private repository. So you can clone a public repository without credentials and push to it later: Git asks the helper for the token at the first push. | |
| 76 | 76 | |
| 77 | 77 | `diffinery repo clone acme/website` clones with the token passed as a one-off HTTP header that is never written to `.git/config`. |
| 78 | 78 | |
| @@ -84,7 +84,7 @@ | ||
| 84 | 84 | | Header for one command | `git -c http.extraHeader="Authorization: Bearer $DIFFINERY_TOKEN" clone https://diffinery.com/acme/website.git` | Good for CI. Nothing is saved. | |
| 85 | 85 | | Token in the URL | `https://x-token:dfy_...@diffinery.com/acme/website.git` | Works, but the token ends up in `.git/config` and shell history. Avoid outside throwaway jobs. | |
| 86 | 86 | |
| 87 | -Inside Diffinery Actions, `actions/checkout` authenticates with the job's own token automatically. That token can read the repository but not push to it (see [Job tokens](#job-tokens)). | |
| 87 | +Inside Diffinery Actions, `actions/checkout` authenticates with the job's own token automatically. Whether that token can push depends on the workflow's `permissions:` (see [Job tokens](#job-tokens)). | |
| 88 | 88 | |
| 89 | 89 | ### Token scopes and roles |
| 90 | 90 | |
| @@ -100,8 +100,29 @@ | ||
| 100 | 100 | |
| 101 | 101 | ### Job tokens |
| 102 | 102 | |
| 103 | -Each Actions job gets a token (`GITHUB_TOKEN`, starting with `djt_`). It works for Git and the REST API on the job's own repository only, with **read** access, and stops working when the job completes. A workflow can clone and fetch its repository but can't push to it with this token. To push from a workflow, store a personal or agent token as a secret and use that. This differs from GitHub, where `GITHUB_TOKEN` can push when the workflow has `contents: write`. | |
| 103 | +Each Actions job gets a token (`GITHUB_TOKEN`, starting with `djt_`) that works for Git and the REST API and stops working when the job completes. As on GitHub, what it may do over Git follows the workflow's `permissions:`: | |
| 104 | 104 | |
| 105 | +| `contents` permission | Git with the job token | | |
| 106 | +| --- | --- | | |
| 107 | +| `read` | Clone and fetch the job's repository | | |
| 108 | +| `write` | Also push to it | | |
| 109 | +| `none` | Nothing on a private repository; a public one can still be cloned, as anyone can | | |
| 110 | + | |
| 111 | +Without `permissions:` in the workflow, the job gets the repository's default: `contents: read` for repositories created now, write for repositories created before the setting existed. A repository admin changes the default with `PUT /api/v3/repos/{owner}/{repo}/actions/permissions/workflow`; see [Workflow permissions](/docs/actions/workflow-permissions). To push from a workflow, ask for it: | |
| 112 | + | |
| 113 | +```yaml | |
| 114 | +permissions: | |
| 115 | + contents: write | |
| 116 | +``` | |
| 117 | + | |
| 118 | +Pushes with a job token have these limits: | |
| 119 | + | |
| 120 | +- The token acts with at most the **write** role, so branch protection always applies to it. | |
| 121 | +- It can never create, change or delete files under `.github/workflows/` or `.diffinery/workflows/`. Git rejects the ref with the reason: refusing to allow a GitHub App to create or update workflow `<path>` without `workflows` permission. Use a personal token for that. | |
| 122 | +- If the run's code came from an agent (an agent triggered the run, pushed the branch last, or opened the pull request, or the run re-runs or follows through `workflow_run` such a run), the token acts as an agent: it can't push to the default branch or a protected branch (`agents may not push to protected or default branches; open a pull request`). | |
| 123 | +- A push made with a job token starts no new `push` workflows, as on GitHub. | |
| 124 | +- The token can also clone other public repositories (for example public submodules), but no other private repository. | |
| 125 | + | |
| 105 | 126 | ## Protocol versions |
| 106 | 127 | |
| 107 | 128 | | Operation | Protocol | Notes | |
| @@ -194,7 +215,7 @@ | ||
| 194 | 215 | |
| 195 | 216 | Every push goes through these steps, in this order. If an earlier step fails, nothing after it happens. |
| 196 | 217 | |
| 197 | -1. **Authorization.** You must send a token: pushes without credentials are refused. Your account needs write access, your token the `write` or `admin` scope, and the repository must not be archived. Otherwise the server answers HTTP 401 (`authentication required`, when a private repository gets no credentials, which makes Git ask for them) or HTTP 403 (`write access required` when your role or a `read`-scoped token only allows reading, or `repository is archived (read-only)`). Git shows these as `Authentication failed` or `The requested URL returned error: 403`. | |
| 218 | +1. **Authorization.** You must send a token: a push without credentials gets HTTP 401 (`authentication required`) with a `WWW-Authenticate` challenge, for public and private repositories alike, which makes Git ask for credentials and retry. Your account needs write access, your token the `write` or `admin` scope, and the repository must not be archived. Otherwise the server answers HTTP 403 (`write access required` when your role only allows reading, `this token needs the write scope` for a `read`-scoped token, or `repository is archived (read-only)`), or HTTP 404 if the account can't see the repository at all. Git shows these as `Authentication failed` or `The requested URL returned error: 403`. | |
| 198 | 219 | 2. **Size check.** A push larger than 100 MB is refused. Git shows `remote unpack failed: push exceeds 100 MB; split it into smaller pushes`, or an HTTP error if the request is stopped before it reaches Diffinery. |
| 199 | 220 | 3. **Unpacking.** The pack is parsed and every object checked. |
| 200 | 221 | 4. **Push protection.** New text files under 1 MB are scanned for credentials such as AWS keys, GitHub and Diffinery tokens, Slack and Stripe keys, Google, Anthropic and OpenAI API keys, and private keys. If any are found, the whole push is rejected, nothing from it is stored, and the output lists each finding: |
| @@ -294,7 +315,7 @@ | ||
| 294 | 315 | | `--deepen` | Behaves like `--depth`. | |
| 295 | 316 | | Packs sent to clients | Sent without deltas, so clones and fetches download more bytes than from GitHub. | |
| 296 | 317 | | Signed pushes | Not supported. Signed commits push normally. | |
| 297 | -| `GITHUB_TOKEN` in workflows | Read-only; can't push. | | |
| 318 | +| `GITHUB_TOKEN` in workflows | Follows `permissions:` as on GitHub (`contents: write` to push), but new repositories default to read-only, it never changes workflow files, and code from an agent is held to the agent rules. | | |
| 298 | 319 | | Repository rename or transfer | The old URL keeps working, as on GitHub: clones, fetches and pushes are served under the old name, and a push prints `This repository moved. Please use the new location:` with the new URL. Update remotes anyway. | |
| 299 | 320 | | `refs/pull/*` | Same as GitHub: fetchable, not pushable, hidden from `push --mirror`. | |
| 300 | 321 | | Ref names | Must start with `refs/` and follow Git's ref name rules, otherwise `invalid ref name`. | |
| @@ -308,8 +329,10 @@ | ||
| 308 | 329 | | `The requested URL returned error: 400` on clone or fetch | Git is older than 2.26 or protocol v2 is turned off. The server's message is `Diffinery requires Git protocol v2 (git 2.26 or newer, or run: git config --global protocol.version 2)`. | Update Git, or run `git config --global protocol.version 2`. | |
| 309 | 330 | | `Authentication failed` after entering a password | You entered a password instead of a token, the token expired or was revoked (`invalid, expired or revoked token`), or it isn't a `dfy_` token (`unrecognised token; create one under Settings → Access tokens`). | Create a token under **Settings → Access tokens** and use it as the password. Clear the old one from your credential helper (for example `git credential reject`, or your keychain). | |
| 310 | 331 | | `repository '...' not found` for a repository you can see in the browser | The token belongs to another account, is restricted to other repositories, or Git sent stale credentials. | Check with `diffinery whoami` and `diffinery token list`. | |
| 311 | -| `The requested URL returned error: 403` on push | Your role or your token's scope only allows reading (`write access required`), or the repository is archived (`repository is archived (read-only)`). | Ask an admin for write access, use a `write` token, or unarchive the repository. | | |
| 312 | -| `The requested URL returned error: 403` when pushing to a public repository, and Git never asked for a token | Git sent no credentials. For a public repository the server answers an anonymous push with `403` (`write access required`), not `401`, so neither Git nor your credential helper offers a token. | Send the token with the push: `git -c http.extraHeader="Authorization: Bearer $DIFFINERY_TOKEN" push origin main`. | | |
| 332 | +| `The requested URL returned error: 403` on push | Your role or your token's scope only allows reading (`write access required`, `this token needs the write scope`), or the repository is archived (`repository is archived (read-only)`). | Ask an admin for write access, use a `write` token, or unarchive the repository. | | |
| 333 | +| Git asks for a username and password when you push to a public repository you cloned without credentials | Expected: anonymous pushes get `401`, so Git asks for credentials. | Enter any username and your token as the password, or set up a credential helper with `diffinery auth setup-git`. | | |
| 334 | +| `The requested URL returned error: 403` on push from a workflow | The job's token has no `contents: write` (`Resource not accessible by integration`). | Add `permissions: contents: write` to the workflow or job, or have an admin change the repository's default workflow permissions. | | |
| 335 | +| `[remote rejected] ... (refusing to allow a GitHub App to create or update workflow ...)` | A workflow's job token tried to push a change to a workflow file. | Push workflow changes with a personal or agent token instead. | | |
| 313 | 336 | | `remote unpack failed: push exceeds 100 MB; split it into smaller pushes` | The pack for this push is larger than 100 MB. | Push history in steps, oldest first, for example `git push origin <older-commit>:refs/heads/main`, then later commits. Move large binaries to Git LFS. | |
| 314 | 337 | | `[remote rejected] ... (secret detected (push protection))` | A pushed file contains something that looks like a credential. | Remove it from every commit in the push (`git commit --amend`, or `git rebase -i`), rotate the credential, and push again. See [Push protection](/docs/security/push-protection). | |
| 315 | 338 | | `[remote rejected] ... (fetch first (stale info))` | The branch moved since you fetched. | `git pull --rebase` (or fetch and merge), then push. | |
modifiedapps/diffinery/docs/repositories/overview.md+137−29View file
| @@ -1,9 +1,9 @@ | ||
| 1 | 1 | --- |
| 2 | 2 | title: Repositories |
| 3 | -description: Create, configure, share, archive, rename and delete Diffinery repositories, and use topics, READMEs, languages and source archives. | |
| 3 | +description: Create, configure, share, archive, rename, transfer and delete Diffinery repositories, and use topics, READMEs, languages and source archives. | |
| 4 | 4 | --- |
| 5 | 5 | |
| 6 | -A repository holds a project's Git history, issues, pull requests, workflows, secrets and webhooks. This page covers creating repositories, naming and visibility, the settings pages, collaborators and roles, archiving, renaming, deleting and watching, plus topics, the README and languages endpoints, and source code archives (zipball and tarball). For pushing and cloning, see [Git over HTTPS](/docs/repositories/git). | |
| 6 | +A repository holds a project's Git history, issues, pull requests, workflows, secrets and webhooks. This page covers creating repositories, naming and visibility, the settings pages, collaborators and roles, archiving, renaming, transferring, deleting and watching, plus topics, the README and languages endpoints, and source code archives (zipball and tarball). For pushing and cloning, see [Git over HTTPS](/docs/repositories/git). | |
| 7 | 7 | |
| 8 | 8 | Every repository lives at `https://diffinery.com/<owner>/<name>`, for example `https://diffinery.com/acme/website`, and clones from `https://diffinery.com/acme/website.git`. The owner is either a person (a user) or an organization. |
| 9 | 9 | |
| @@ -12,9 +12,9 @@ | ||
| 12 | 12 | | Owner | Who can create repositories there | Who gets access automatically | |
| 13 | 13 | | --- | --- | --- | |
| 14 | 14 | | Your user account (`diffy/notes`) | Only you (and site administrators) | You are **admin**. Nobody else, unless you add collaborators or make it public. | |
| 15 | -| An organization (`acme/website`) | Any member of the organization | Organization **members** get **write**. Organization **owners** and **admins** get **admin**. | | |
| 15 | +| An organization (`acme/website`) | Any member of the organization | Organization **members** get the organization's base permission (**read** for new organizations; owners can set **none**, **read**, **write** or **admin**). Organization **owners** and **admins** get **admin**. A member who creates the repository gets **admin** on it. | | |
| 16 | 16 | |
| 17 | -You can add collaborators to either kind for extra or narrower access (see [Collaborators and roles](#collaborators-and-roles)). Organizations have no teams: access in an organization comes from the member's organization role plus any collaborator role on the repository. See [Organizations](/docs/accounts/organizations). | |
| 17 | +You can add collaborators to either kind for extra access (see [Collaborators and roles](#collaborators-and-roles)). Organizations have no teams: access in an organization comes from the member's organization role plus any collaborator role on the repository, and a collaborator role can raise a member above the base permission but not lower it. See [Base permission](/docs/accounts/organizations#base-permission). | |
| 18 | 18 | |
| 19 | 19 | An agent that creates a repository with `POST /api/v3/user/repos` creates it for the account that owns the agent. See [Agents](/docs/agents/overview). |
| 20 | 20 | |
| @@ -90,7 +90,7 @@ | ||
| 90 | 90 | | --- | --- | --- | --- | |
| 91 | 91 | | `name` | string | (required) | See [Repository names](#repository-names). | |
| 92 | 92 | | `description` | string | `""` | | |
| 93 | -| `visibility` | `"public"` or `"private"` | `"private"` | A misspelled value fails with `visibility must be public or private` (HTTP 422). | | |
| 93 | +| `visibility` | `"public"` or `"private"` | `"private"` | Any other value fails with `visibility must be public or private` (HTTP 422), with `errors` naming the `visibility` field. | | |
| 94 | 94 | | `private` | boolean | | As on GitHub: `false` creates a public repository, `true` a private one. When you send both, `visibility` wins. | |
| 95 | 95 | | `default_branch` | string | `"main"` | | |
| 96 | 96 | |
| @@ -111,14 +111,14 @@ | ||
| 111 | 111 | |
| 112 | 112 | | Visibility | Who can see and clone it | |
| 113 | 113 | | --- | --- | |
| 114 | -| **Private** (default) | The owner, the organization's members (for organization repositories), collaborators, and site administrators. | | |
| 114 | +| **Private** (default) | The owner, the organization's members (for organization repositories, unless the organization's base permission is **none**), collaborators, and site administrators. | | |
| 115 | 115 | | **Public** | Anyone, including people who are not signed in. Signed-out visitors can browse the code, and `git clone` works without credentials. Every signed-in user gets **read** access. | |
| 116 | 116 | |
| 117 | -Writing to a public repository still needs a signed-in account (or a token) with a role that allows it. People who aren't signed in can read a public repository but can't change anything in it: opening an issue, commenting, reacting, watching and pushing all need an account, and the API answers such requests with `401 authentication required`. A `read`-scoped token can read too, but every write with it gets `403 this token needs the write scope`. Only public repositories are indexed by search engines; private pages carry a `noindex` tag. | |
| 117 | +Writing to a public repository still needs a signed-in account (or a token) with a role that allows it. People who aren't signed in can read a public repository but can't change anything in it: opening an issue, commenting, reacting, watching and pushing all need an account, and the API answers such requests with `401 authentication required`. An anonymous `git push` gets a `401` with a `WWW-Authenticate` challenge, so Git asks for your credentials (or uses its credential helper) and tries again with them. A `read`-scoped token can read too, but every write with it gets `403 this token needs the write scope`. Only public repositories are indexed by search engines; private pages carry a `noindex` tag. | |
| 118 | 118 | |
| 119 | 119 | For people without access, a private repository looks like it does not exist: the web UI, Git and the API answer `404` (`repository not found`) rather than `403`, so the name is not revealed. Signed-out requests get `401` (`authentication required`) instead, so Git knows to ask for credentials. |
| 120 | 120 | |
| 121 | -Change visibility on the **General** settings page or with `PATCH /api/v3/repos/{owner}/{repo}` and `{"visibility": "public"}`. As on GitHub, `{"private": false}` also makes a repository public and `{"private": true}` makes it private. | |
| 121 | +Change visibility on the **General** settings page or with `PATCH /api/v3/repos/{owner}/{repo}` and `{"visibility": "public"}`. As on GitHub, `{"private": false}` also makes a repository public and `{"private": true}` makes it private. `visibility` accepts only `public` and `private` here too: any other value gets the same `422 visibility must be public or private` as when creating a repository. | |
| 122 | 122 | |
| 123 | 123 | ## Repository settings |
| 124 | 124 | |
| @@ -126,19 +126,20 @@ | ||
| 126 | 126 | |
| 127 | 127 | | Page | Path | What it controls | |
| 128 | 128 | | --- | --- | --- | |
| 129 | -| **General** | `/acme/website/settings` | Name, description, default branch, visibility, archive, delete. | | |
| 129 | +| **General** | `/acme/website/settings` | Name, description, default branch, visibility, archive, transfer, delete. | | |
| 130 | 130 | | **Collaborators** | `/acme/website/settings/access` | Direct access for people and agents, and email invitations, from one **Add people** form. | |
| 131 | 131 | | **Branch protection** | `/acme/website/settings/branches` | Rules for matching branches. See [Branch protection](/docs/repositories/branch-protection). | |
| 132 | 132 | | **Secrets and variables** | `/acme/website/settings/secrets` | Actions secrets and variables. See [Secrets and variables](/docs/actions/secrets-and-variables). | |
| 133 | 133 | | **Webhooks** | `/acme/website/settings/webhooks` | HTTPS endpoints that receive events. See [Webhooks](/docs/repositories/webhooks). | |
| 134 | 134 | |
| 135 | -Every change on these pages is written to the audit log (for example `repo.update`, `repo.collaborator.set`, `repo.protection.set`, `repo.secret.set`, `repo.webhook.create`). | |
| 135 | +Every change on these pages is written to the audit log (for example `repo.update`, `repo.collaborator.set`, `repo.protection.set`, `repo.secret.set`, `repo.webhook.create`). The same changes made through the REST API are recorded too, with `via: "api"` in the entry's detail. See [Audit log](/docs/security/audit-log#repositories). | |
| 136 | 136 | |
| 137 | 137 | Some repository features have no settings page: |
| 138 | 138 | |
| 139 | 139 | - **Releases** live at `/acme/website/releases`, and anyone with write access manages them there. See [Releases](/docs/collaboration/releases). |
| 140 | 140 | - **Deployment environments** (with their own secrets, variables and branch policies) are managed with the REST API. See [Environments](/docs/actions/environments). |
| 141 | 141 | - **Topics** are set with the REST API. See [Topics](#topics). |
| 142 | +- **Workflow permissions** (what a workflow's `GITHUB_TOKEN` may do by default, and whether workflows may create and approve pull requests) are set on the **Actions** tab or with the REST API. See [Workflow permissions](#workflow-permissions). | |
| 142 | 143 | |
| 143 | 144 | ### General |
| 144 | 145 | |
| @@ -149,7 +150,7 @@ | ||
| 149 | 150 | - **Default branch**: a list of the repository's branches (a text field while the repository is empty). The default branch is what the repository page shows, what clones check out, the base for new pull requests, and it is always protected from agents. |
| 150 | 151 | - **Visibility**: **Private** or **Public**. |
| 151 | 152 | |
| 152 | -Below it, the **Danger zone** has **Archive this repository** and **Delete this repository**. | |
| 153 | +Below it, the **Danger zone** has **Archive this repository**, **Transfer ownership** (see [Transfer a repository](#transfer-a-repository)) and **Delete this repository**. | |
| 153 | 154 | |
| 154 | 155 | ### Secrets and variables |
| 155 | 156 | |
| @@ -162,6 +163,32 @@ | ||
| 162 | 163 | |
| 163 | 164 | Organization secrets, available to every repository in the organization, are managed with the API or `diffinery secret set NAME --org acme`. Details are in [Secrets and variables](/docs/actions/secrets-and-variables). |
| 164 | 165 | |
| 166 | +### Workflow permissions | |
| 167 | + | |
| 168 | +Each repository has GitHub's **Workflow permissions** setting for Actions. It decides the permissions a job's `GITHUB_TOKEN` gets when the workflow doesn't set `permissions:`, and whether workflows may open pull requests and approve or request changes on them. | |
| 169 | + | |
| 170 | +| Field | Values | Default | | |
| 171 | +| --- | --- | --- | | |
| 172 | +| `default_workflow_permissions` | `read`: `contents` and `packages` read only. `write`: read and write on every permission except `id-token`. | `read` for repositories created now; `write` for repositories created before the setting existed | | |
| 173 | +| `can_approve_pull_request_reviews` | `true` or `false` | `false` | | |
| 174 | + | |
| 175 | +In the web UI, open the repository's **Settings → Actions** tab (`/{owner}/{repo}/settings/actions`). Under **Workflow permissions**, pick **Read repository contents and packages permissions** or **Read and write permissions**, check or clear **Allow GitHub Actions to create and approve pull requests**, and click **Save**. You see "Actions settings saved." The tab needs the **admin** role on the repository. | |
| 176 | + | |
| 177 | +With the REST API (admin role, so an `admin`-scoped token): | |
| 178 | + | |
| 179 | +```sh | |
| 180 | +# Read the setting | |
| 181 | +curl https://diffinery.com/api/v3/repos/acme/website/actions/permissions/workflow \ | |
| 182 | + -H "Authorization: Bearer dfy_..." | |
| 183 | + | |
| 184 | +# Give workflows read and write by default, and let them create and approve pull requests | |
| 185 | +curl -X PUT https://diffinery.com/api/v3/repos/acme/website/actions/permissions/workflow \ | |
| 186 | + -H "Authorization: Bearer dfy_..." -H "content-type: application/json" \ | |
| 187 | + -d '{"default_workflow_permissions": "write", "can_approve_pull_request_reviews": true}' | |
| 188 | +``` | |
| 189 | + | |
| 190 | +`GET` returns `{"default_workflow_permissions": "read", "can_approve_pull_request_reviews": false}`, and `PUT` returns `204`. Fields you leave out keep their value. Only people can change it (agents get `403 only people can change what workflows are allowed to do`), an invalid value gets `422 default_workflow_permissions must be read or write` or `422 can_approve_pull_request_reviews must be a boolean`, and each change is recorded in the audit log as `repo.actions_workflow_settings`. Even when workflows may approve, their approvals never count toward required approvals. See [Workflow permissions](/docs/actions/workflow-permissions) for how the token's permissions are worked out. | |
| 191 | + | |
| 165 | 192 | ### Webhooks |
| 166 | 193 | |
| 167 | 194 | Add a webhook with a **Payload URL** (must start with `https://`), the **Events** to send (**Everything**, or any of `push`, `pull_request`, `pull_request_review`, `issues`, `issue_comment`, `workflow_run`, `workflow_dispatch`; **Everything** also covers `release`, `status`, `deployment`, `deployment_status` and `repository_dispatch`) and an optional **Secret** (generated when left empty, and shown once). Diffinery signs each delivery with an HMAC-SHA256 of the body in `X-Hub-Signature-256`, like GitHub. A delivery that fails with a network error, a timeout, `408`, `429` or a `5xx` status is retried up to five times (1 minute after the first failure, then 5 minutes, 30 minutes, 1 hour and 3 hours after each further failure), with the same `X-GitHub-Delivery` ID. The list shows each webhook's last delivery status. See [Webhooks](/docs/repositories/webhooks). |
| @@ -178,7 +205,7 @@ | ||
| 178 | 205 | | **maintain** | Also move and delete tags. | |
| 179 | 206 | | **admin** | Also change settings, secrets, branch protection, webhooks and access, archive and delete the repository. | |
| 180 | 207 | |
| 181 | -A person's role is the highest of: owning the repository (admin), their organization role (members write, owners and admins admin), a collaborator role, and **read** on public repositories. Site administrators are admin everywhere. A token can lower this further (a `read` token acts as read and can't make any change, a `write` token acts as at most maintain). Agents have their own limits on top: for example, whatever their role, they can't add, change or delete secrets or variables, change branch protection or delete the repository. See [Permissions](/docs/accounts/permissions) for the full matrix. | |
| 208 | +A person's role is the highest of: owning the repository (admin), their organization role (members get the organization's base permission, owners and admins admin), a collaborator role, and **read** on public repositories. Site administrators are admin everywhere. A token can lower this further (a `read` token acts as read and can't make any change, a `write` token acts as at most maintain). Agents have their own limits on top: for example, whatever their role, they can't add, change or delete secrets or variables, change branch protection or delete the repository. See [Permissions](/docs/accounts/permissions) for the full matrix. | |
| 182 | 209 | |
| 183 | 210 | ### Add a collaborator in the web UI |
| 184 | 211 | |
| @@ -204,10 +231,11 @@ | ||
| 204 | 231 | | `<name> is an organization; add its people instead` | Organizations can't be collaborators. Add the people one by one. | |
| 205 | 232 | | `<name>'s account is disabled` | A site administrator disabled that account. | |
| 206 | 233 | | `enter a valid email address` | The text contains `@` but isn't a valid address. | |
| 234 | +| `<email> already has a pending invitation; revoke it first to send a new one` | That address already has an open invitation to this repository (letter case doesn't matter). Repository invitations can't be revoked: send the person the first link again, or wait until it is used or expires after 14 days. | | |
| 207 | 235 | |
| 208 | 236 | The table lists direct collaborators. Change someone's role with the role list and **Save**, or remove them with **Remove**. People get an email when their access to a repository is granted or changed. |
| 209 | 237 | |
| 210 | -Organization members don't appear in this table: they get their access from the organization. | |
| 238 | +Organization members don't appear in this table: they get their access from the organization's base permission. The exception is a member who created the repository, who is listed with **Admin**. | |
| 211 | 239 | |
| 212 | 240 | ### Manage collaborators with the API |
| 213 | 241 | |
| @@ -230,13 +258,25 @@ | ||
| 230 | 258 | |
| 231 | 259 | ## Archive a repository |
| 232 | 260 | |
| 233 | -Archiving makes a repository read-only: nobody can push, open or change issues and pull requests, create or edit releases, or start runs, and scheduled workflows stop. Everything stays readable, cloneable and downloadable as an archive. Admins can still change settings and unarchive it. | |
| 261 | +Archiving makes a repository read-only, as on GitHub: nobody can push, open or change issues and pull requests, comment, review, create or edit releases, start runs or change settings, and scheduled workflows stop. Everything stays readable, cloneable and downloadable as an archive. | |
| 234 | 262 | |
| 235 | 263 | - **Web UI**: **Settings → General → Danger zone → Archive**, then confirm. **Unarchive** reverses it. |
| 236 | 264 | - **API**: `PATCH /api/v3/repos/acme/website` with `{"archived": true}` (or `false`). |
| 237 | 265 | |
| 238 | -Archived repositories show an **archived** badge. Requests that would write to one fail with `repository is archived (read-only)` (HTTP 403), and a `git push` fails with that message. Most write endpoints of the REST API (for example topics, releases, statuses, deployments and environments) refuse every write to an archived repository, even from admins; unarchive it first. Exceptions: the admin-only endpoints for collaborators, branch protection, webhooks, secrets and variables keep working, and the REST endpoints that open and edit issues, edit pull requests, add and edit comments, submit reviews and request reviewers don't check the archived state yet (see [Issues](/docs/collaboration/issues#permissions)). | |
| 266 | +Archived repositories show an **archived** badge. Every request that would write to one fails with `repository is archived (read-only)` (HTTP 403), whatever role it needs and whoever sends it, admins included: pushes, issues, comments, reactions, reviews, pull requests, releases, statuses, deployments, topics, collaborators, branch protection, secrets, variables, environments, webhooks and MCP tools that change something. A `git push` fails with the same message. Unarchive the repository first. | |
| 239 | 267 | |
| 268 | +Only these still work on an archived repository: | |
| 269 | + | |
| 270 | +| Action | How | | |
| 271 | +| --- | --- | | |
| 272 | +| Unarchive it | **Settings → General → Unarchive**, or `PATCH /api/v3/repos/acme/website` with `{"archived": false}` | | |
| 273 | +| Delete it | **Settings → General → Delete this repository**, or `DELETE /api/v3/repos/acme/website` | | |
| 274 | +| Transfer it | **Settings → General → Transfer ownership**, or `POST /api/v3/repos/acme/website/transfer`. See [Transfer a repository](#transfer-a-repository). | | |
| 275 | +| Watch or unwatch it | The **Watch** menu, or `PUT`/`DELETE /api/v3/repos/acme/website/subscription` | | |
| 276 | +| Link a container image to it | The package's settings, or the packages API. See [Container registry](/docs/packages/container-registry). | | |
| 277 | + | |
| 278 | +Archiving and unarchiving are recorded in the audit log as `repo.archive` and `repo.unarchive`, from the web UI and the API alike. | |
| 279 | + | |
| 240 | 280 | ## Rename a repository |
| 241 | 281 | |
| 242 | 282 | Admins can rename a repository on **Settings → General** (change **Repository name** and click **Save changes**) or with: |
| @@ -247,38 +287,104 @@ | ||
| 247 | 287 | -d '{"name": "site"}' |
| 248 | 288 | ``` |
| 249 | 289 | |
| 250 | -Renaming changes the repository's URL and clone URL. The old name keeps redirecting, as after a [transfer](#transfer-a-repository), until a repository is created with that name. Update your remotes anyway: `git remote set-url origin https://diffinery.com/acme/site.git`, and anything else that uses the old name, such as CI scripts and webhook consumers. A name the owner already uses is refused (`a repository with this name already exists`, HTTP 422). | |
| 290 | +Renaming changes the repository's URL and clone URL. The old name keeps working, exactly as after a transfer (see [Old URLs after a transfer or rename](#old-urls-after-a-transfer-or-rename)), until a repository is created with that name. Update your remotes anyway: `git remote set-url origin https://diffinery.com/acme/site.git`, and anything else that uses the old name, such as CI scripts and webhook consumers. A name the owner already uses is refused (`a repository with this name already exists`, HTTP 422). | |
| 251 | 291 | |
| 252 | 292 | ## Transfer a repository |
| 253 | 293 | |
| 254 | -A transfer moves a repository to another owner, like GitHub's: your own account, or an organization where you are an owner or admin. You need admin access to the repository; a personal repository can only be transferred by its owner. Agents, workflow tokens (`GITHUB_TOKEN`) and sandbox tokens can't transfer repositories. | |
| 294 | +A transfer moves a repository to another owner, like GitHub's transfer: from your account to an organization, from an organization to your account, or between organizations. Everything in the repository moves with it, and its old URLs keep working. | |
| 255 | 295 | |
| 256 | -- **Web UI**: **Settings → General → Danger zone → Transfer ownership**. Pick the new owner, optionally a new name, type the full name (for example `acme/website`) and click **Transfer**. | |
| 257 | -- **API**: `POST /api/v3/repos/acme/website/transfer` with `{"new_owner": "acme-labs"}` (and `"new_name"` to rename it too) returns `202` and the repository. It needs an `admin` token that isn't limited to repositories. | |
| 296 | +### Who can transfer | |
| 258 | 297 | |
| 298 | +- You must be a person with the **admin** role on the repository. Agents, workflow tokens (`GITHUB_TOKEN`) and sandbox tokens can't transfer repositories. | |
| 299 | +- A personal repository can only be transferred by its owner (or a site administrator). | |
| 300 | +- The new owner must be your own account, or an organization where you are an **owner** or **admin**. You can't transfer a repository to another person's account, or to an organization where you are only a member. Because of this, there is no acceptance step: the transfer happens at once. | |
| 301 | +- Archived repositories can be transferred without unarchiving them first. | |
| 302 | +- Through the API, you need a token with the `admin` scope that isn't limited to repositories. | |
| 303 | + | |
| 304 | +### Transfer in the web UI | |
| 305 | + | |
| 306 | +1. Open the repository's **Settings** tab (the **General** page) and scroll to the **Danger zone**. | |
| 307 | +2. Under **Transfer ownership**, pick the **New owner**: your own account (shown as `diffy (your account)`) or one of the organizations you own or administer. The current owner isn't listed. | |
| 308 | +3. Optionally type a **New name**. Leave it empty to keep the current name. | |
| 309 | +4. Type the repository's full name (for example `acme/website`) in the confirmation box, as `Type acme/website to confirm.` asks, and click **Transfer**. | |
| 310 | + | |
| 311 | +You land on the repository's settings at its new address, with `Transferred acme/website to acme-labs/website.` When you can't transfer it, the section says why instead: `Only diffy can transfer this repository.` for someone else's personal repository, or `You don't own or administer another account or organization to transfer it to.` | |
| 312 | + | |
| 313 | +### Transfer with the API | |
| 314 | + | |
| 259 | 315 | ```sh |
| 260 | 316 | curl -X POST https://diffinery.com/api/v3/repos/acme/website/transfer \ |
| 261 | - -H "Authorization: Bearer dfy_..." -H "content-type: application/json" \ | |
| 262 | - -d '{"new_owner": "acme-labs"}' | |
| 317 | + -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \ | |
| 318 | + -d '{"new_owner": "acme-labs", "new_name": "site"}' | |
| 263 | 319 | ``` |
| 264 | 320 | |
| 265 | -The repository keeps everything: Git history, issues, pull requests, releases, Actions runs, caches, secrets, variables, environments and webhooks. Container images linked to it move with it and keep following its access. Direct collaborators keep their roles; grants that came from the old organization (its members', including the admin grant of whoever created the repository, their agents' and the organization's own agents') are removed, and the new owner's base permission, organization secrets and organization variables apply instead. A transfer is refused (HTTP 422) when the new owner already has a repository with that name, or a container image with the name of one of the repository's images. | |
| 321 | +| Field | Meaning | | |
| 322 | +| --- | --- | | |
| 323 | +| `new_owner` | Required. The login of your own account or of an organization you own or administer. | | |
| 324 | +| `new_name` | Optional. A new name for the repository; it follows the [naming rules](#repository-names). Default: keep the current name. | | |
| 266 | 325 | |
| 267 | -The old owner/name keeps working for anyone who can still read the repository; everyone else gets a 404, so a redirect never shows that a private repository exists: | |
| 326 | +The response is `202 Accepted` with the repository object at its new owner and name. The transfer is complete when the response arrives. GitHub's `team_ids` field isn't used, since Diffinery has no teams. | |
| 268 | 327 | |
| 328 | +### What moves with the repository | |
| 329 | + | |
| 330 | +The repository keeps its id, so everything stored in it moves: Git history and Git LFS objects, issues, pull requests, reviews, releases, the merge queue, Actions runs, logs, artifacts and caches, repository secrets and variables, environments, webhooks, branch protection rules and watchers. Its numeric id in the API and in webhook payloads doesn't change. | |
| 331 | + | |
| 332 | +- **Access.** Direct collaborators keep their roles. Access that came from the old organization is removed: its members' roles (including the admin grant of the member who created the repository), their agents' and the organization's own agents'. The new owner's access applies instead: for an organization, its owners and admins get admin and its members get its base permission; for your own account, you are admin. | |
| 333 | +- **Organization secrets and variables.** The old organization's secrets and variables no longer reach the repository's workflows; the new organization's do. Repository and environment secrets stay. | |
| 334 | +- **Container images.** Images linked to the repository move to the new owner with it and keep following its access (see [Container registry](/docs/packages/container-registry#when-the-repository-moves)). Images that aren't linked to it stay with the old owner. | |
| 335 | +- **Source archives.** Zipballs and tarballs are built again with the new owner and name in their folder name. | |
| 336 | +- **Webhooks.** They keep firing, with the new `full_name` and URLs in their payloads. No `repository` event is sent for the transfer itself. | |
| 337 | +- **Tokens.** Personal access tokens limited to the repository keep working, since they refer to it by id. | |
| 338 | + | |
| 339 | +### Old URLs after a transfer or rename | |
| 340 | + | |
| 341 | +The old owner/name keeps working for anyone who can still read the repository, as on GitHub. Everyone else gets the same answer as for the repository under its new name (`404`, or `401` without credentials), so a redirect never shows that a private repository exists: | |
| 342 | + | |
| 269 | 343 | | Where | What happens under the old name | |
| 270 | 344 | | --- | --- | |
| 271 | 345 | | Web pages | `301` to the same page under the new name, query string kept | |
| 272 | -| REST API | `301` with the new URL in `Location` (and `url` in the body) for `GET` and `HEAD`, `307` for other methods, like GitHub | | |
| 273 | -| Git | Clones, fetches and pushes are served under the old name; a push prints `This repository moved. Please use the new location:` and the new URL | | |
| 346 | +| REST API | `301` with the new URL in `Location` (and `url` in the body) for `GET` and `HEAD`, `307` for other methods so the method and body are kept, like GitHub | | |
| 347 | +| Git | Clones, fetches, pushes and Git LFS are served under the old name, with no redirect; a push prints `This repository moved. Please use the new location:` and the new URL | | |
| 274 | 348 | | MCP tools | Served as if you had named the new owner and name | |
| 275 | -| Container images | `diffinery.com/<old owner>/<image>` resolves to the moved image | | |
| 349 | +| Reusable workflows | `uses: acme/website/.github/workflows/build.yml@main` still finds the moved repository's workflow | | |
| 350 | +| Container images | `diffinery.com/<old owner>/<image>` resolves to the moved image, for pulls and pushes | | |
| 276 | 351 | |
| 277 | -Creating a repository with the old name ends the redirect for good. Transfers are recorded in the audit log as `repo.transfer` with `from` and `to`, and the admins of both owners get an email, unless they transferred it themselves. | |
| 352 | +Git is served rather than redirected because Git follows redirects only on its first request and rebuilds its credentials from the new URL, which would drop a token embedded in the remote URL. Update remotes when you can: | |
| 278 | 353 | |
| 354 | +```sh | |
| 355 | +git remote set-url origin https://diffinery.com/acme-labs/site.git | |
| 356 | +``` | |
| 357 | + | |
| 358 | +The redirect ends for good when a repository is created at the old owner and name (by anyone who can create one there), and when the moved repository is deleted. A renamed repository leaves the same redirect, and so does every repository of a renamed user or organization (see [Rename an organization](/docs/accounts/organizations#rename-an-organization)). | |
| 359 | + | |
| 360 | +### After a transfer | |
| 361 | + | |
| 362 | +- The transfer is recorded in the audit log as `repo.transfer`, with the new name as the target and `from` and `to` in the detail. | |
| 363 | +- The admins of both owners (for a personal account, the person; for an organization, its owners and admins) get an email such as `[acme-labs/website] Repository transferred from acme/website`, with the `git remote set-url` command to run, except whoever made the transfer. See [Notifications](/docs/accounts/notifications). | |
| 364 | + | |
| 365 | +### Transfer errors | |
| 366 | + | |
| 367 | +| Message | Cause and fix | | |
| 368 | +| --- | --- | | |
| 369 | +| `new_owner is required` (422) | Send `new_owner`. | | |
| 370 | +| `no user or organization named acme-labs` (422) | Check the login. | | |
| 371 | +| `you can transfer repositories only to your own account or to an organization you own or administer` (403) | Ask an owner of the target organization to make you an admin, or to do the transfer. | | |
| 372 | +| `only the owner can transfer a personal repository` (403) | Only the account that owns a personal repository can transfer it. | | |
| 373 | +| `repositories can only be transferred to a person or an organization` (422) | The target is an agent or a disabled account. | | |
| 374 | +| `acme/website already belongs to acme` (422) | The repository already has that owner. To change only its name, [rename it](#rename-a-repository). | | |
| 375 | +| `acme-labs already has a repository named website` (422) | Pick another `new_name`, or rename or delete the other repository first. | | |
| 376 | +| `acme-labs already has a container image named website; this repository's image of that name would replace it` (422) | Delete or rename the target's image, or unlink the image from this repository first. | | |
| 377 | +| `invalid repository name: use letters, numbers, '.', '-' and '_'` (422) | Fix `new_name`. | | |
| 378 | +| `repository is locked by an import in progress; transfer it once the import finishes` (423) | Wait for the [GitHub import](/docs/migrate-from-github) filling the repository to finish. | | |
| 379 | +| `agents and workflow tokens cannot transfer repositories` (403) | Transfer as a person, with your own token or the web UI. | | |
| 380 | +| `admin access required` (403) | You need the admin role. A `write` token caps you at maintain: use an `admin` token. | | |
| 381 | +| `this token is limited to repositories and can't change account settings` (403) | Use an `admin` token that isn't restricted to repositories. | | |
| 382 | +| `type acme/website to confirm the transfer` | The web confirmation box didn't match the repository's full name. | | |
| 383 | +| `choose the new owner` | Pick a **New owner** in the web form. | | |
| 384 | + | |
| 279 | 385 | ## Delete a repository |
| 280 | 386 | |
| 281 | -Deleting permanently removes the repository with its history, issues, pull requests, releases, runs, secrets, environments and webhooks. It can't be undone. | |
| 387 | +Deleting permanently removes the repository with its history, issues, pull requests, releases, runs, secrets, environments and webhooks, and its Actions caches, artifacts, job logs and cached source archives. It can't be undone. An archived repository can be deleted without unarchiving it first. | |
| 282 | 388 | |
| 283 | 389 | - **Web UI**: **Settings → General → Danger zone → Delete this repository**. Type the full name (for example `acme/website`) and click **Delete this repository**. |
| 284 | 390 | - **API**: `DELETE /api/v3/repos/acme/website` returns `204 No Content`. It needs an admin role. Agents can't delete repositories (`agents cannot delete repositories`, HTTP 403). |
| @@ -412,7 +518,7 @@ | ||
| 412 | 518 | - Git LFS files appear as their small pointer files, not their content. |
| 413 | 519 | - Entries with names Git refuses to check out (such as `.git` in any spelling) are left out. |
| 414 | 520 | |
| 415 | -Unlike GitHub, the API answers with the file itself (`200`) rather than redirecting to another host; `curl -L` works either way. Diffinery builds the archive the first time a commit is requested and serves the same file afterwards; it can be built again later. | |
| 521 | +Unlike GitHub, the API answers with the file itself (`200`) rather than redirecting to another host; `curl -L` works either way. Diffinery builds the archive the first time a commit is requested and serves the same file afterwards, for up to 30 days; after that it is built again the next time someone asks for it. | |
| 416 | 522 | |
| 417 | 523 | Limits: |
| 418 | 524 | |
| @@ -442,6 +548,8 @@ | ||
| 442 | 548 | | `repository is archived (read-only)` | Unarchive the repository first. | |
| 443 | 549 | | `404 repository not found` for a repository that exists | You (or the token) have no access to this private repository, or the token is restricted to other repositories. | |
| 444 | 550 | | `visibility must be public or private` | Send `"public"` or `"private"` (or use the `private` boolean). | |
| 551 | +| `remote: This repository moved. Please use the new location:` on push | The repository was transferred or renamed. The push went through; run `git remote set-url origin <new URL>`. | | |
| 552 | +| An API call answers `301` or `307` with `Moved Permanently` | The repository was transferred or renamed. Follow `Location` and update the owner and name in your scripts. See [Old URLs after a transfer or rename](#old-urls-after-a-transfer-or-rename). | | |
| 445 | 553 | | `no one has the username <name>; to invite someone new, enter their email address` | The **Add people** form found no account with that username. Enter an email address to send an invitation. | |
| 446 | 554 | | `<name> is not a valid topic...` or `a repository can have at most 20 topics` | Follow the [topic rules](#topics). | |
| 447 | 555 | | `repository is too large for an on-the-fly archive; clone it with git instead` | The [source archive](#source-code-archives) would hold more than 24 MB. Clone the repository instead. | |
modifiedapps/diffinery/docs/repositories/webhooks.md+9−7View file
| @@ -19,14 +19,14 @@ | ||
| 19 | 19 | | Event | Sent when | Actions | |
| 20 | 20 | |---|---|---| |
| 21 | 21 | | `push` | A branch or tag is created, updated or deleted, by `git push`, the web editor, the contents or Git data API, MCP, a merge, the merge queue, or a release that creates its tag | none | |
| 22 | -| `pull_request` | A pull request is opened, updated, edited, closed, merged, reopened, assigned, gets a review request, is marked ready for review or converted to a draft, or enters or leaves the merge queue | `opened`, `synchronize`, `edited`, `closed`, `reopened`, `assigned`, `review_requested`, `ready_for_review`, `converted_to_draft`, `enqueued`, `dequeued` | | |
| 22 | +| `pull_request` | A pull request is opened, updated, edited, closed, merged, reopened, assigned, gets a review request, is marked ready for review or converted to a draft, enters or leaves the merge queue, or has auto-merge turned on or off | `opened`, `synchronize`, `edited`, `closed`, `reopened`, `assigned`, `review_requested`, `ready_for_review`, `converted_to_draft`, `enqueued`, `dequeued`, `auto_merge_enabled`, `auto_merge_disabled` | | |
| 23 | 23 | | `pull_request_review` | A review is submitted | `submitted` | |
| 24 | 24 | | `issues` | An issue is opened, edited (title or body), closed, reopened or assigned | `opened`, `edited`, `closed`, `reopened`, `assigned` | |
| 25 | 25 | | `issue_comment` | A comment is added to an issue or a pull request | `created` | |
| 26 | 26 | | `workflow_run` | A workflow run is created or finishes | `requested`, `completed` | |
| 27 | 27 | | `release` | A release is created, published, edited, unpublished or deleted, including drafts | `created`, `published`, `released`, `prereleased`, `edited`, `unpublished`, `deleted` | |
| 28 | 28 | | `status` | A commit status is posted | none | |
| 29 | -| `deployment` | A deployment is created with the REST API | `created` | | |
| 29 | +| `deployment` | A deployment is created, with the REST API or by a workflow job with `environment:` | `created` | | |
| 30 | 30 | | `deployment_status` | A deployment gets a new status, from the REST API or from a workflow job with `environment:` | `created` | |
| 31 | 31 | | `repository_dispatch` | Someone calls `POST /repos/{owner}/{repo}/dispatches` | the `event_type` that was sent | |
| 32 | 32 | | `workflow_dispatch` | Someone starts a workflow by hand (web UI, CLI, REST API or MCP) | none | |
| @@ -170,7 +170,7 @@ | ||
| 170 | 170 | | `X-GitHub-Delivery` | A unique id for this delivery: 32 hexadecimal characters | |
| 171 | 171 | | `X-Hub-Signature-256` | `sha256=` followed by the hex HMAC-SHA256 of the raw body, keyed with the webhook's secret | |
| 172 | 172 | |
| 173 | -The body is compact JSON (no extra whitespace). Every payload has `repository` and `sender` objects; `repository.id` is a number, like GitHub's. See [Webhook events](/docs/reference/webhook-events). | |
| 173 | +The body is compact JSON (no extra whitespace). Every payload has `repository` and `sender` objects. `repository` is GitHub's full repository object, the same one `GET /api/v3/repos/{owner}/{repo}` returns: its `id` is a number, and `private` and `visibility` give the repository's real visibility (`false` and `public` for a public repository). See [Webhook events](/docs/reference/webhook-events#the-repository-object). | |
| 174 | 174 | |
| 175 | 175 | ### Timeouts, redirects and retries |
| 176 | 176 | |
| @@ -180,7 +180,7 @@ | ||
| 180 | 180 | | Redirects | Not followed. A `3xx` response is recorded as that status and counts as failed. Use the final URL. | |
| 181 | 181 | | Success | Any `2xx` status. The response body is ignored. | |
| 182 | 182 | | Failed deliveries | Recorded (the status, or `0` for a timeout, DNS or connection error). Network errors, timeouts, `408`, `429` and `5xx` are retried; see [Retries](#retries). Other statuses are not. | |
| 183 | -| Duplicates | A retry repeats the same delivery with the same `X-GitHub-Delivery` id, so your endpoint can see a delivery again if it processed it but answered too slowly or with an error. Separately, if Diffinery's own processing of an event fails, the event is processed again (up to 8 times, with a growing delay of up to 5 minutes), which delivers it again with a new `X-GitHub-Delivery` id. Make your handler idempotent, for example by remembering delivery ids. | | |
| 183 | +| Duplicates | A retry repeats the same delivery with the same `X-GitHub-Delivery` id, so your endpoint can see a delivery again if it processed it but answered too slowly or with an error. Diffinery's own failures don't cause new deliveries: webhooks and notifications for an event are handled separately, and a step that fails before any webhook was called (for example while loading the repository's webhooks) is tried again on its own after 30 seconds, 2 minutes, 10 minutes, 30 minutes and 1 hour. Still, make your handler idempotent, for example by remembering delivery ids. | | |
| 184 | 184 | | Order | Deliveries are not guaranteed to arrive in the order the events happened. Use the `at` timestamp in the payload, or the `created_at` and `updated_at` fields of the GitHub-shaped events. | |
| 185 | 185 | |
| 186 | 186 | Respond quickly with a `2xx` and do slow work afterwards, so that deliveries don't time out. |
| @@ -301,7 +301,7 @@ | ||
| 301 | 301 | - The same headers: `X-GitHub-Event`, `X-GitHub-Delivery` and `X-Hub-Signature-256`, plus `X-Diffinery-Event`. |
| 302 | 302 | - The same signature scheme (HMAC-SHA256, `sha256=` prefix, hex). |
| 303 | 303 | - The same event names and the same `action` values for the actions Diffinery sends. |
| 304 | -- Payloads have GitHub-style `repository` (with a numeric `id` and a `node_id`), `sender`, `pull_request` and `issue` objects, and `pull_request.merged` on `closed`. | |
| 304 | +- Payloads have GitHub's full `repository` object (numeric `id`, `node_id`, `owner`, `private`, `visibility`, `default_branch`, URLs and so on), a `sender`, GitHub-style `pull_request` and `issue` objects, and `pull_request.merged` on `closed`. | |
| 305 | 305 | - `release`, `status`, `deployment`, `deployment_status` and `repository_dispatch` payloads have GitHub's structure, with the release, status and deployment fields GitHub receivers read. `workflow_dispatch` has GitHub's `inputs`, `ref` and `workflow` fields. |
| 306 | 306 | - The hooks API returns GitHub's hook object (`type`, `name`, `config`, `last_response`, ...). |
| 307 | 307 | |
| @@ -328,7 +328,9 @@ | ||
| 328 | 328 | - Payload URLs must use HTTPS. |
| 329 | 329 | - The secret is stored with the webhook and is shown only once in the web UI (and returned once by the create API). Use a long random value, and keep it in your receiver's secret store. |
| 330 | 330 | - Verify the signature on every request, and reject requests without one. |
| 331 | -- Creating a webhook from the web UI is recorded in the [audit log](/docs/security/audit-log) as `repo.webhook.create`, and deleting one as `repo.webhook.delete`. | |
| 331 | +- Creating a webhook is recorded in the [audit log](/docs/security/audit-log) as `repo.webhook.create`, and deleting one as `repo.webhook.delete`, both from the web UI and from the API (API changes carry `"via": "api"` in the detail). | |
| 332 | +- On an archived repository, webhooks can't be created or deleted: the request fails with `403` and `repository is archived (read-only)`. | |
| 333 | +- Payloads from a private repository say so (`"private": true`, `"visibility": "private"`), so a receiver can decide what it may show or forward. | |
| 332 | 334 | - Payloads never contain Actions secrets, tokens or file contents. |
| 333 | 335 | |
| 334 | 336 | ## Troubleshooting |
| @@ -344,6 +346,6 @@ | ||
| 344 | 346 | | No `edited` event after changing labels | `edited` is sent only when the title or body changes. Label changes send no webhook event. | |
| 345 | 347 | | No `release`, `status` or `deployment` deliveries | The webhook was created in the web UI with individual events checked, which can't include these. Recreate it with **Everything** checked, or with the API listing the events. | |
| 346 | 348 | | Three `release` deliveries for one new release | Publishing a release sends `created`, `published` and `released` (or `prereleased`), like GitHub. Act on the one you need, usually `published`. | |
| 347 | -| No `deployment` event for a workflow job with `environment:` | Jobs record their deployment without a `deployment` event; they send `deployment_status` events (`in_progress` when the job starts, then `success`, `failure` or `inactive`). | | |
| 349 | +| A `deployment` and a `deployment_status` delivery for one workflow job | A job with `environment:` creates a deployment when it starts (`deployment` with `created`) and reports its progress as `deployment_status` events (`in_progress` when the job starts, then `success`, `failure` or `inactive`), as on GitHub. | | |
| 348 | 350 | | `config.url is required` | Send the payload URL as `config.url`, not as a top-level `url`. | |
| 349 | 351 | | A webhook stopped receiving events | Check that it still exists on the **Webhooks** page and that the events it subscribes to are the ones you expect. | |
modifiedapps/diffinery/docs/security/audit-log.md+48−24View file
| @@ -3,7 +3,7 @@ | ||
| 3 | 3 | description: What Diffinery records in its audit log, the fields of each entry, who can view it, and which entries also send security emails. |
| 4 | 4 | --- |
| 5 | 5 | |
| 6 | -The audit log is an append-only record of security-relevant actions on a Diffinery instance: sign-ins, passkey and token changes, profile pictures, repository and environment settings, pushes, merges, admin merges that bypass branch protection, push protection decisions and package changes. Site administrators use it to answer "who did what, when, and on whose behalf". | |
| 6 | +The audit log is an append-only record of security-relevant actions on a Diffinery instance: sign-ins, passkey and token changes, profile pictures, repository and environment settings (from the web UI and the API alike), pushes, merges, admin merges that bypass branch protection, push protection decisions, package changes, imports from GitHub and linked GitHub accounts. Site administrators use it to answer "who did what, when, and on whose behalf". | |
| 7 | 7 | |
| 8 | 8 | ## Viewing the audit log |
| 9 | 9 | |
| @@ -18,7 +18,7 @@ | ||
| 18 | 18 | | Column | Content | |
| 19 | 19 | |---|---| |
| 20 | 20 | | **When** | The time of the action | |
| 21 | -| **Actor** | The login of the account that acted, followed by `for <owner>` when an agent acted on behalf of a person or organization. Shows `system` when no account is attached, and `api` for `api.blocked_secret`. | | |
| 21 | +| **Actor** | The login of the account that acted, followed by `for <owner>` when an agent acted on behalf of a person or organization. Shows `system` when no account is attached. | | |
| 22 | 22 | | **Action** | The action name, such as `git.push` | |
| 23 | 23 | | **Target** | What the action applied to, such as `acme/website` or `acme/website:refs/heads/main` | |
| 24 | 24 | | **Detail** | Extra data as JSON (hover to see it all) | |
| @@ -56,6 +56,9 @@ | ||
| 56 | 56 | | `token.revoke` | A token is revoked on **Access tokens** | the token id | none | |
| 57 | 57 | | `signin_link.create` | A site admin creates a one-time sign-in link for someone | that user's login | none | |
| 58 | 58 | | `user.active`, `user.pending`, `user.disabled` | A site admin changes a user's status | that user's login | none | |
| 59 | +| `account.link_github` | You link a GitHub account on **Settings → Linked accounts** (gist code or GitHub sign-in) or with `POST /api/v3/diffinery/user/linked-accounts/github` | `<host>/<GitHub login>`, such as `github.com/diffy` | none | | |
| 60 | +| `account.unlink_github` | You unlink a GitHub account (**Unlink**, or `DELETE /api/v3/diffinery/user/linked-accounts/{id}`) | the link's id | none | | |
| 61 | +| `account.link_cleared` | A site admin clears someone's GitHub link with `DELETE /api/v3/diffinery/admin/linked-accounts/{id}` | `<host>/<GitHub login>` | `account` (the id of the Diffinery account the link belonged to) | | |
| 59 | 62 | | `avatar.update`, `avatar.remove` | A profile picture is uploaded or removed, for yourself or for an organization you administer (web UI, `PUT`/`DELETE /api/v3/user/avatar` or `PUT`/`DELETE /api/v3/orgs/{org}/avatar`) | the login of the person or organization | `kind` (`user` or `org`) and `via` (`web` or `api`) | |
| 60 | 63 | |
| 61 | 64 | ### Agents |
| @@ -71,8 +74,10 @@ | ||
| 71 | 74 | | Action | Recorded when | Target | Detail | |
| 72 | 75 | |---|---|---|---| |
| 73 | 76 | | `org.create` | An organization is created from the web UI | the organization's login | none | |
| 74 | -| `org.update` | An organization's settings are changed | the organization's login | `login` | | |
| 77 | +| `org.update` | An organization's settings are changed on its **Settings** tab, or with `PATCH /api/v3/orgs/{org}` | the organization's login | `login` and `base_role` (web UI), or `name` and `base_role` (API) | | |
| 75 | 78 | | `org.delete` | An organization is deleted (web or `DELETE /api/v3/orgs/{org}`) | the organization's login | none | |
| 79 | +| `org.secret.set`, `org.secret.delete` | An organization secret is saved or deleted with `PUT`/`DELETE /api/v3/orgs/{org}/actions/secrets/{name}` (or `diffinery secret ... --org`) | the organization's login | `name` (never the value), `via: "api"` | | |
| 80 | +| `org.variable_set`, `org.variable_delete` | An organization Actions variable is created, changed or deleted with `POST`, `PATCH` or `DELETE /api/v3/orgs/{org}/actions/variables[/{name}]`, or added by a GitHub import of an organization | the organization's login (API), or the GitHub organization (import) | `name` (upper-cased); an import adds `import` (its id) and `org` | | |
| 76 | 81 | | `org.invite` | Someone without an account is invited to an organization: an email address entered in **Add people** on the organization's **People** tab, or `POST /api/v3/orgs/{org}/invitations` | the organization's login | `email`, `role` | |
| 77 | 82 | | `org.invite.revoke` | A pending organization invitation is revoked with **Revoke** | the organization's login | `id` (the invitation id) | |
| 78 | 83 | | `org.member.set`, `org.member.remove` | Someone with an account is added with **Add people**, a member's role is changed, or a member is removed | the organization's login | `user`, `role` | |
| @@ -81,27 +86,28 @@ | ||
| 81 | 86 | |
| 82 | 87 | ### Repositories |
| 83 | 88 | |
| 84 | -All actions on the repository settings pages use the repository (`acme/website`) as the target. | |
| 89 | +All repository entries use the repository (`acme/website`) as the target. Settings changes are recorded the same way whether they are made on the web settings pages or through the REST API (and the CLI, which uses it). Entries written for an API call add `via: "api"` to the detail. | |
| 85 | 90 | |
| 86 | 91 | | Action | Recorded when | Detail | |
| 87 | 92 | |---|---|---| |
| 88 | 93 | | `repo.create` | A repository is created from the web UI | none (target `acme/website`) | |
| 89 | -| `repo.update` | **General** settings are saved | `name`, `visibility`, `defaultBranch` | | |
| 90 | -| `repo.archive`, `repo.unarchive` | The repository is archived or unarchived | none | | |
| 94 | +| `repo.update` | **General** settings are saved, or `PATCH /api/v3/repos/{owner}/{repo}` changes the name, description, visibility or default branch | Web: `name`, `visibility`, `defaultBranch`. API: the fields that were sent (`name`, `description`, `visibility`, `default_branch`) | | |
| 95 | +| `repo.archive`, `repo.unarchive` | The repository is archived or unarchived, on **General** or with `PATCH /api/v3/repos/{owner}/{repo}` and `{"archived": true}` (or `false`) | none | | |
| 91 | 96 | | `repo.delete` | The repository is deleted (web or `DELETE /api/v3/repos/{owner}/{repo}`) | none | |
| 92 | 97 | | `repo.transfer` | The repository is transferred to another owner (web or `POST /api/v3/repos/{owner}/{repo}/transfer`); the target is the new name | `from`, `to` | |
| 93 | -| `repo.collaborator.set`, `repo.collaborator.remove` | On **Collaborators**: a person or agent with an account is added with **Add people**, or a collaborator's role is changed, or a collaborator is removed | `login`, `role` | | |
| 98 | +| `repo.collaborator.set`, `repo.collaborator.remove` | On **Collaborators**: a person or agent with an account is added with **Add people**, or a collaborator's role is changed, or a collaborator is removed. Through the API: `PUT` or `DELETE /api/v3/repos/{owner}/{repo}/collaborators/{username}` | `login`, `role` | | |
| 94 | 99 | | `repo.invite` | An email address without an account is entered in **Add people**, which sends an invitation | `email`, `role` | |
| 95 | 100 | | `repo.topics` | The topics are replaced with `PUT /api/v3/repos/{owner}/{repo}/topics` | `names` | |
| 96 | -| `repo.protection.set`, `repo.protection.delete` | A branch protection rule is saved or deleted on **Branch protection** | `pattern`, and `rules` when saved | | |
| 101 | +| `repo.protection.set`, `repo.protection.delete` | A branch protection rule is saved or deleted on **Branch protection**, or with `PUT`/`DELETE /api/v3/repos/{owner}/{repo}/branches/{branch}/protection` | `pattern`, and on the web `rules` when saved | | |
| 97 | 102 | | `repo.protection.agent_auto_merge` | **Allow agents to land pull requests through auto-merge** (`allowAgentAutoMerge`) is turned on or off for a rule, from the web UI or the API | `pattern`, `enabled`, `requiredChecks` | |
| 98 | 103 | | `repo.protection.agent_approval_waiver` | **Let agents' auto-merges land without an approving review** (`agentAutoMergeWithoutApproval`) is turned on or off for a rule, from the web UI or the API | `pattern`, `enabled`, `requiredChecks` | |
| 99 | -| `repo.secret.set`, `repo.secret.delete` | An Actions secret is saved or deleted on **Secrets and variables** | `name` (never the value) | | |
| 100 | -| `repo.variable.set`, `repo.variable.delete` | An Actions variable is saved or deleted | `name` | | |
| 101 | -| `repo.webhook.create`, `repo.webhook.delete` | A webhook is added or deleted on **Webhooks** | `url` and `events`, or `id` | | |
| 104 | +| `repo.secret.set`, `repo.secret.delete` | An Actions secret is saved or deleted on **Secrets and variables**, or with `PUT`/`DELETE /api/v3/repos/{owner}/{repo}/actions/secrets/{name}` | `name` (never the value) | | |
| 105 | +| `repo.variable.set`, `repo.variable.delete` | An Actions variable is saved or deleted on **Secrets and variables**, or created, updated, renamed or deleted through `/api/v3/repos/{owner}/{repo}/actions/variables` | `name`, and `renamed_from` when a `PATCH` renamed it | | |
| 106 | +| `repo.webhook.create`, `repo.webhook.delete` | A webhook is added or deleted on **Webhooks**, or with `POST /api/v3/repos/{owner}/{repo}/hooks` or `DELETE /api/v3/repos/{owner}/{repo}/hooks/{id}` | Web: `url` and `events`, or `id`. API: `id` and `url`, or `id` | | |
| 107 | +| `repo.actions_workflow_settings` | An admin changes the repository's workflow permissions on the **Actions** settings tab or with `PUT /api/v3/repos/{owner}/{repo}/actions/permissions/workflow` (see [Workflow permissions](/docs/actions/workflow-permissions)) | the new settings: `defaultPermissions` (`read` or `write`) and `canApprovePullRequests` | | |
| 108 | +| `actions.cache.delete` | Actions caches are deleted with `DELETE /api/v3/repos/{owner}/{repo}/actions/caches?key=...` or `DELETE /api/v3/repos/{owner}/{repo}/actions/caches/{id}` | `key`, `ids` | | |
| 109 | +| `actions.artifact.delete` | A workflow artifact is deleted with `DELETE /api/v3/repos/{owner}/{repo}/actions/artifacts/{id}` | `id`, `name` | | |
| 102 | 110 | |
| 103 | -> **Note:** The collaborator, branch protection, secret, variable and webhook entries above are written by the repository settings pages in the web UI. The same changes made through the REST API (for example `PUT /api/v3/repos/{owner}/{repo}/branches/{branch}/protection` or `PUT /api/v3/repos/{owner}/{repo}/actions/secrets/{name}`) are not recorded on this version. `repo.delete`, `repo.topics` and the environment entries below are recorded for API calls. | |
| 104 | - | |
| 105 | 111 | ### Environments |
| 106 | 112 | |
| 107 | 113 | Changes to [environments](/docs/actions/environments) are made through the REST API by repository admins who are people (agents are refused). Each entry has the repository (`acme/website`) as the target and names the environment in `environment`. |
| @@ -113,9 +119,8 @@ | ||
| 113 | 119 | | `environment.branch_policy.create` | A deployment branch or tag pattern is added | `environment`, `pattern`, `type` (`branch` or `tag`) | |
| 114 | 120 | | `environment.branch_policy.delete` | A deployment branch or tag pattern is removed | `environment`, `id` | |
| 115 | 121 | | `environment.secret.set`, `environment.secret.delete` | An environment secret is saved or deleted | `environment`, `name` (never the value) | |
| 122 | +| `environment.variable.set`, `environment.variable.delete` | An environment variable is created, updated, renamed or deleted | `environment`, `name`, and `renamed_from` when a `PATCH` renamed it | | |
| 116 | 123 | |
| 117 | -Environment variables are not recorded. | |
| 118 | - | |
| 119 | 124 | ### Git and pull requests |
| 120 | 125 | |
| 121 | 126 | | Action | Recorded when | Target | Detail | |
| @@ -123,12 +128,32 @@ | ||
| 123 | 128 | | `git.push` | A branch or tag moves forward or is created, by any means: `git push`, the web editor, the API, MCP, a merge or the merge queue | `acme/website:refs/heads/main` | `before`, `after` (commit ids) | |
| 124 | 129 | | `git.force_push` | A ref moves to a commit that does not contain the old one | as above | `before`, `after` | |
| 125 | 130 | | `git.delete_ref` | A branch or tag is deleted | as above | `before`, `after` | |
| 126 | -| `pull.merge` | A pull request is merged, directly or by the merge queue | `acme/website#42` | none | | |
| 127 | -| `pull_request.merge_bypassed` | A repository admin merges a pull request past unmet branch rules, by asking for a bypass (`"bypass_rules": true` or `"bypass_merge_queue": true` in the API, `diffinery pr merge --admin`, or **Merge without waiting for requirements (bypass)** or **Merge now (bypass the queue)** in the web UI). Recorded when the pull request had at least one merge blocker; a `pull.merge` entry follows. | `acme/website#42` | `blockers` (the unmet blockers, such as `needs 1 more approving review(s)`), `base` (the base branch) | | |
| 131 | +| `pull.merge` | A pull request is merged, directly, by the merge queue or by auto-merge | `acme/website#42` | none | | |
| 132 | +| `pull_request.merge_bypassed` | A repository admin merges a pull request past branch rules by asking for a bypass: with `"bypass_rules": true` (or **Merge without waiting for requirements (bypass)** in the web UI) past unmet blockers or a linear-history rule (a merge commit), or with `"bypass_merge_queue": true` (or **Merge now (bypass the queue)**) past a required merge queue. `diffinery pr merge --admin` sends both. A `pull.merge` entry follows. | `acme/website#42` | `blockers` (what was bypassed: the unmet blockers, such as `needs 1 more approving review(s)`, plus `linear history required` or `merge queue` when those were bypassed), `base` (the base branch) | | |
| 133 | +| `pull_request.auto_merge_enabled` | Someone turns on [auto-merge](/docs/collaboration/pull-requests#auto-merge) for a pull request, or changes its method (web UI, `PUT /api/v3/repos/{owner}/{repo}/pulls/{number}/auto-merge` or the `enable_auto_merge` MCP tool) | `acme/website#42` | `base`, `method`, `viaAgentRule` (`true` when an agent or workflow token relies on `allowAgentAutoMerge`) | | |
| 134 | +| `pull_request.auto_merge_disabled` | Auto-merge is turned off: by someone (**Disable auto-merge**, `DELETE .../auto-merge` or `disable_auto_merge`), by a push from someone else, by converting the pull request to a draft, or by itself when it can't go through. The actor is who turned it off, the pusher, or `diffinery-auto-merge[bot]`. | `acme/website#42` | `enabledBy` (login), and `reason` when it turned itself off | | |
| 135 | +| `pull_request.auto_merged` | Auto-merge merges the pull request, as the account that turned it on. A `pull.merge` entry is written too. | `acme/website#42` | `base`, `method`, `sha` | | |
| 136 | +| `pull_request.auto_merge_enqueued` | Auto-merge adds the pull request to the base branch's merge queue | `acme/website#42` | `base`, `state` | | |
| 128 | 137 | | `push.blocked_secret` | [Push protection](/docs/security/push-protection) blocks a push | `acme/website` | `kinds` (credential types found), `refs` | |
| 129 | -| `push.secret_bypassed` | A repository admin pushes past push protection with `git push -o allow-secrets=<reason>` | `acme/website` | `kinds`, `reason`, `refs` | | |
| 130 | -| `api.blocked_secret` | [Push protection](/docs/security/push-protection#writes-through-the-rest-api) refuses a file written with the contents API or the git data API. The actor is recorded as `api`, not as the caller. | `acme/website` | `kinds` | | |
| 138 | +| `push.secret_bypassed` | A repository admin pushes past push protection with `git push -o allow-secrets=<reason>`, or a GitHub import imports old history with an audited bypass (see [Imported history](/docs/security/push-protection#imported-history)) | `acme/website` | `kinds`, `reason`, `refs`; for an import also `import` (its id) and `blobs` (up to 50 blob ids), and the reason is `Imported from GitHub <owner>/<name>` | | |
| 139 | +| `api.blocked_secret` | [Push protection](/docs/security/push-protection#commits-and-writes-without-git) refuses a commit made with the web editor, `POST /repos/{owner}/{repo}/diffinery/commits`, the contents API or the MCP `commit_files` tool, or a file written with the git data API. The actor is the person or agent who made the request. | `acme/website` | `kinds`, and `paths` (up to 10), or `blob` for the git data API | | |
| 140 | +| `api.secret_bypassed` | A repository admin who is a person commits past push protection with `allow_secrets` on `POST /repos/{owner}/{repo}/diffinery/commits` | `acme/website` | `kinds`, `paths`, `reason` | | |
| 131 | 141 | |
| 142 | +### GitHub imports | |
| 143 | + | |
| 144 | +[Imports from GitHub](/docs/migrate-from-github) are recorded with the person or agent who started the import as the actor. Apart from `import.push` and the push protection entries, they don't fill in `on_behalf_of` for an agent. Entries written by the import service have the GitHub source (`diffy/hello-world`, or `diffy` for an organization import) as the target and the import's id as `import` in the detail. | |
| 145 | + | |
| 146 | +| Action | Recorded when | Target | Detail | | |
| 147 | +|---|---|---|---| | |
| 148 | +| `import.create` | An import is started (web form, `POST /api/v3/diffinery/imports`, `diffinery migrate github`, MCP `start_github_import`), or a draft organization import starts after its repositories are chosen | the GitHub source | `import`, `kind` (`repo` or `bulk`), `owner` (the Diffinery account imported into), and `name` and `options`, or `repositories` (how many were chosen) | | |
| 149 | +| `import.push` | The import's Git transfer pushes history into the new repository. Replaces the `git.push` entries such pushes would otherwise write. | `acme/website` | `import`, `refs` (how many refs the push created or moved) | | |
| 150 | +| `import.cancel` | Someone cancels the import (**Cancel import**, or `POST /api/v3/diffinery/imports/{id}/cancel`) | the GitHub source | `import`, `by` (who cancelled it) | | |
| 151 | +| `import.retry` | Whoever started it retries a failed or cancelled import | the GitHub source | `import`, `by`, `attempt` | | |
| 152 | +| `import.fail` | An import fails after its retries | the GitHub source | `import`, `error` | | |
| 153 | +| `import.finish` | An import finishes. An organization import with repositories that weren't imported is recorded as `import.finish` too, and its status is `failed`. | the GitHub source | `import`, and `repo`, `counts` (issues, pull requests, comments, reviews, labels, milestones) and `findings` (how many push protection findings were bypassed), or `children` (counts of the organization import's repositories) | | |
| 154 | + | |
| 155 | +Old history imported past push protection writes `push.secret_bypassed`, and a push blocked during an import writes `push.blocked_secret` (see [Git and pull requests](#git-and-pull-requests)). Organization variables an import adds write `org.variable_set`. Nothing an import writes into the repository (issues, comments, reviews, labels, milestones, releases, branch protection, variables) writes other entries. | |
| 156 | + | |
| 132 | 157 | ### Container packages |
| 133 | 158 | |
| 134 | 159 | | Action | Recorded when | Target | Detail | |
| @@ -141,7 +166,7 @@ | ||
| 141 | 166 | |
| 142 | 167 | ## Agents and on-behalf-of |
| 143 | 168 | |
| 144 | -When an agent acts, the entry records the agent as the actor and the agent's owner in `on_behalf_of`, so the page reads, for example, `review-bot for diffy`. This applies to pushes, merges, push protection decisions and settings changes made with the agent's token. (Agents can never bypass branch protection on a merge, so `pull_request.merge_bypassed` always names a person.) See [Agents](/docs/agents/overview). | |
| 169 | +When an agent acts, the entry records the agent as the actor and the agent's owner in `on_behalf_of`, so the page reads, for example, `review-bot for diffy`. This applies to pushes, merges, push protection decisions (including `api.blocked_secret`) and settings changes made with the agent's token, on the web or through the API. (Agents can never bypass branch protection on a merge, so `pull_request.merge_bypassed` always names a person.) See [Agents](/docs/agents/overview). | |
| 145 | 170 | |
| 146 | 171 | ## Security emails |
| 147 | 172 | |
| @@ -153,8 +178,8 @@ | ||
| 153 | 178 | | `passkey.add` | "A passkey was added to your account" | |
| 154 | 179 | | `passkey.remove` | "A passkey was removed from your account" | |
| 155 | 180 | | `agent.create` | "A new agent was added" | |
| 156 | -| `push.blocked_secret` | "Your push was blocked: possible secrets", to the pusher (or the agent's owner) | | |
| 157 | -| `push.secret_bypassed` | "Push protection was bypassed", to the pusher and the repository's admins | | |
| 181 | +| `push.blocked_secret` | "Your push was blocked: possible secrets", to the pusher (or the agent's owner); for a GitHub import, to whoever started it | | |
| 182 | +| `push.secret_bypassed`, `api.secret_bypassed` | "Push protection was bypassed", to the pusher (or committer) and the repository's admins | | |
| 158 | 183 | |
| 159 | 184 | Deleting a repository (`repo.delete`) also emails the repository owner, or the organization's owners and admins, except the person who deleted it. Token creation emails are sent when the token is created. See [Notifications](/docs/accounts/notifications). |
| 160 | 185 | |
| @@ -163,6 +188,5 @@ | ||
| 163 | 188 | | Symptom | Cause | |
| 164 | 189 | |---|---| |
| 165 | 190 | | You don't see **Audit log** in the settings sidebar | Only site administrators can view it. Ask a site admin; see [Site administration](/docs/admin/site-administration). | |
| 166 | -| A push or merge appears a few seconds late | `git.push`, `git.force_push`, `git.delete_ref`, `pull.merge`, `pull_request.merge_bypassed` and push protection entries (including `api.blocked_secret`) are written by the event queue shortly after the action, not in the request itself. | | |
| 167 | -| A settings change made through the API is missing | Collaborator, branch protection, secret, variable and webhook changes are recorded only when made in the web UI. See the note under [Repositories](#repositories). | | |
| 191 | +| A push or merge appears a few seconds late | `git.push`, `git.force_push`, `git.delete_ref`, `pull.merge`, `pull_request.merge_bypassed`, the `pull_request.auto_merge_*` and `pull_request.auto_merged` entries, `repo.protection.agent_auto_merge`, `repo.protection.agent_approval_waiver`, `repo.actions_workflow_settings` and push protection entries (including `api.blocked_secret` and `api.secret_bypassed`) are written by the event queue shortly after the action, not in the request itself. | | |
| 168 | 192 | | An older entry is missing from the page | The page shows only the 200 most recent entries. | |
modifiedapps/diffinery/docs/security/overview.md+26−18View file
| @@ -18,12 +18,12 @@ | ||
| 18 | 18 | | Agents | Accounts owned by a person or organization, with server-enforced limits | |
| 19 | 19 | | Branches | Branch protection rules, enforced for git, the API, MCP and the web UI | |
| 20 | 20 | | Required checks | Checks reported by agents never satisfy a required check; a check can be bound to one reporter | |
| 21 | -| Pushes | Credential scanning on every push and on file writes through the REST API; blocked pushes leave nothing behind | | |
| 21 | +| Pushes and commits | Credential scanning on every push and on every commit made without Git (web editor, REST API, MCP); blocked writes leave nothing behind | | |
| 22 | 22 | | File names | Writes through the API, MCP and the web editor refuse `.git`, `..` and other unsafe tree entry names | |
| 23 | -| Archived repositories | Read-only | | |
| 23 | +| Archived repositories | Every write refused, except unarchiving, deleting, watching and linking packages | | |
| 24 | 24 | | Actions secrets | Envelope encryption with a per-repository, per-environment or per-organization key derived from `MASTER_KEY` | |
| 25 | 25 | | Workflow logs | Secret values are replaced with `***` | |
| 26 | -| Workflow credentials | A job token valid for one job of one repository, and short-lived OIDC id tokens | | |
| 26 | +| Workflow credentials | A job token valid for one job, limited by the workflow's `permissions:` (read-only by default for new repositories), and short-lived OIDC id tokens | | |
| 27 | 27 | | Webhooks | HTTPS only, signed with HMAC-SHA256, failed deliveries retried | |
| 28 | 28 | | Accountability | Append-only audit log and security emails | |
| 29 | 29 | | Isolation | One Durable Object per repository | |
| @@ -118,24 +118,26 @@ | ||
| 118 | 118 | |
| 119 | 119 | A `read` token can't change anything: every repository request other than `GET` or `HEAD` gets `403 this token needs the write scope`, even for changes the `read` role allows, such as opening an issue or commenting. `git fetch` and Git LFS downloads still work with it. |
| 120 | 120 | |
| 121 | -Account-level changes check the scope directly and refuse tokens restricted to repositories (`403 this token is limited to repositories and can't change account settings`). Creating repositories needs `write`; creating organizations, inviting people, managing organization secrets, managing agents and their tokens, and revoking tokens need `admin`. Creating a token through the API needs an `admin`-scoped token or a browser session. Scopes sent to the API are checked: an empty list or an unknown name is refused with `422 scopes must be a non-empty list of: read, write, admin`, and leaving them out gives `["write"]`. Every new token triggers a security email, and for tokens valid longer than 8 days you get a reminder about 7 days before the token expires. See [Tokens](/docs/accounts/tokens) and [Permissions](/docs/accounts/permissions). | |
| 121 | +The scope is checked only after Diffinery has found a repository the caller can see, so a `read` token's write to a private repository it can't see gets `404 repository not found`, not a scope error that would reveal the repository. | |
| 122 | 122 | |
| 123 | +Account-level changes check the scope directly and refuse tokens restricted to repositories (`403 this token is limited to repositories and can't change account settings`). Creating repositories needs `write`; creating organizations, inviting people, managing organization secrets, disabling agents and revoking tokens need `admin`. No token can create a token or an agent: those need a browser session, so a leaked token can't mint another one. Job and sandbox tokens can't start sandboxes either, and a sandbox's credentials last 24 hours at most, and a new token can only be restricted to repositories the account can see (any other name gets `422 unknown repository <name>`). Scopes sent to the API are checked: an empty list or an unknown name is refused with `422 scopes must be a non-empty list of: read, write, admin`, and leaving them out gives `["write"]`. Every new token triggers a security email, and for tokens valid longer than 8 days you get a reminder about 7 days before the token expires. See [Tokens](/docs/accounts/tokens) and [Permissions](/docs/accounts/permissions). | |
| 124 | + | |
| 123 | 125 | ## Repository access |
| 124 | 126 | |
| 125 | 127 | Each request to a repository resolves the caller's role (read, triage, write, maintain or admin) from ownership, organization membership and collaborator grants, then applies the token's scope and repository restriction. Roles are computed on every request, so removing someone from an organization or a repository takes effect immediately, including for tokens they already have. Private repositories return `404 repository not found` to anyone without access, so their existence is not revealed. See [Permissions](/docs/accounts/permissions). |
| 126 | 128 | |
| 127 | 129 | ### Anonymous callers only read |
| 128 | 130 | |
| 129 | -Without a token or a session, a caller gets the `read` role on public repositories and nothing on private ones. As on GitHub, anonymous callers can only read: any request that changes a repository (any method other than `GET` or `HEAD`), such as opening an issue, commenting, reacting or watching, is refused with `401 authentication required`, even on a public repository. Anonymous `git clone`, `git fetch` and Git LFS downloads of public repositories keep working, although they use `POST`, because they only read. Unknown paths under `/api/` answer with a JSON `404` (`{"message": "Not Found", "documentation_url": "..."}`), not the web UI's error page. | |
| 131 | +Without a token or a session, a caller gets the `read` role on public repositories and nothing on private ones. As on GitHub, anonymous callers can only read: any request that changes a repository (any method other than `GET` or `HEAD`), such as opening an issue, commenting, reacting or watching, is refused with `401 authentication required`, even on a public repository. Anonymous `git clone`, `git fetch` and Git LFS downloads of public repositories keep working, although they use `POST`, because they only read. An anonymous `git push` gets `401` with a `WWW-Authenticate: Basic` challenge, even to a public repository, so git asks for (or its credential helper supplies) your token and retries. Unknown paths under `/api/` answer with a JSON `404` (`{"message": "Not Found", "documentation_url": "..."}`), not the web UI's error page. | |
| 130 | 132 | |
| 131 | 133 | ### Archived repositories |
| 132 | 134 | |
| 133 | -An archived repository is read-only for its content: | |
| 135 | +An archived repository is read-only, for everyone including admins, as on GitHub: | |
| 134 | 136 | |
| 135 | 137 | - `git push` is refused, and scheduled workflows (`on: schedule`) stop running. |
| 136 | 138 | - The web UI shows code, issues and pull requests without any forms to change them. A conversation shows `This repository is archived; the conversation is read-only.` |
| 137 | -- Most REST API and MCP writes, such as file commits, new pull requests, merges, labels, assignees, locks, reactions, releases, commit statuses and check runs, deployments and git data, are refused with `403` and `repository is archived (read-only)`, even for admins. Reading still works. | |
| 138 | -- Exceptions: the admin endpoints for collaborators, branch protection, webhooks, secrets and variables keep working. The REST endpoints that open and edit issues, edit pull requests, add and edit comments, submit reviews and request reviewers, and the MCP `create_issue` and `review_pull_request` tools, don't check the archived state yet and still accept changes. See [Archive a repository](/docs/repositories/overview#archive-a-repository). | |
| 139 | +- Every write through the web UI, the REST API and MCP is refused with `403` and `repository is archived (read-only)`, whatever role the endpoint needs: issues, comments, reviews, pull requests, merges, labels, reactions, releases, statuses, check runs, deployments, git data, and the admin settings (collaborators, branch protection, webhooks, secrets and variables). Reading still works. | |
| 140 | +- Only these still work: unarchiving, deleting the repository, watching or unwatching it, and linking a container image to it. See [Archive a repository](/docs/repositories/overview#archive-a-repository). | |
| 139 | 141 | |
| 140 | 142 | A repository admin archives or unarchives a repository under **Settings**, **General**, with the **Archive** (or **Unarchive**) button and a confirmation. Both are recorded in the [audit log](/docs/security/audit-log) as `repo.archive` and `repo.unarchive`. |
| 141 | 143 | |
| @@ -146,7 +148,7 @@ | ||
| 146 | 148 | - An agent's role is capped by its **Maximum role** (default write) and never exceeds its owner's role. |
| 147 | 149 | - Agents cannot push to the default branch or to protected branches, unless a branch protection rule allows it (`agentsMayPush`). |
| 148 | 150 | - Agents cannot merge pull requests into the default branch or protected branches, unless a rule allows it (`agentsMayMerge`). |
| 149 | -- Pull requests opened by agents need at least one approval from a person, and agents' approvals never count toward required approvals. | |
| 151 | +- Pull requests opened by agents need at least one approval from a person. Only people's approvals count toward required approvals; approvals from agents and workflow tokens never do. | |
| 150 | 152 | - Agents cannot change branch protection, delete repositories, create agents, or bypass push protection. |
| 151 | 153 | - Whatever their role, agents cannot create, update or delete Actions secrets (repository, environment or organization), and cannot create, change or delete Actions variables (repository or environment). They get `403 agents cannot manage secrets` or `403 agents cannot manage variables`. |
| 152 | 154 | - Agents never bypass branch protection when merging, even with the admin role. |
| @@ -154,6 +156,7 @@ | ||
| 154 | 156 | - Agents cannot dismiss pull request reviews (`agents can't dismiss reviews`). |
| 155 | 157 | - Commit statuses and check runs that agents report are shown, but never satisfy a required check in branch protection or in a merge queue. A deployment created without `required_contexts` also ignores contexts reported by agents. See [Checks and statuses](/docs/collaboration/checks-and-statuses). |
| 156 | 158 | - Workflow runs started by agents receive an environment's secrets only when the environment is limited to protected branches. Tags created or moved by agents never match an environment's tag patterns. |
| 159 | +- A workflow run whose code came from an agent (an agent triggered it, pushed the branch last, or opened the pull request, every re-run of such a run, whoever starts it, and every `workflow_run` run that follows it) acts as an agent: its job token is held to these same rules, so it can't push or merge agent-written code into the default branch or a protected branch. | |
| 157 | 160 | - Commits that agents create through the API or MCP (including commits made with the git data API and the merge commit of `update-branch`) carry a `diffinery-agent` header naming the agent and who it acts for. |
| 158 | 161 | - The audit log records who an agent acted for, and security emails about an agent (blocked pushes, new tokens) go to its owner. |
| 159 | 162 | |
| @@ -163,7 +166,7 @@ | ||
| 163 | 166 | |
| 164 | 167 | Branch protection rules decide how matching branches may change: required pull requests, approvals and checks, stale approval dismissal, linear history, force pushes, deletion, agent permissions and merge queues. When several rules match a branch, the rule that names the branch exactly wins; otherwise the oldest matching pattern applies. With linear history required, a merge commit is refused (`405`); squash or rebase instead. The default branch can never be deleted. |
| 165 | 168 | |
| 166 | -Repository admins can bypass rules unless the rule sets `enforceAdmins`. A merge never bypasses silently: an admin who is a person has to ask for it (**Merge without waiting for requirements (bypass)** in the web UI, `"bypass_rules": true` on `PUT /repos/{owner}/{repo}/pulls/{number}/merge`, or `diffinery pr merge --admin`), only when the pull request has no conflicts and isn't a draft, and each bypassed merge is recorded in the audit log as `pull_request.merge_bypassed` with the unmet requirements. See [Branch protection](/docs/repositories/branch-protection). | |
| 169 | +Repository admins can bypass rules unless the rule sets `enforceAdmins`. A merge never bypasses silently: an admin who is a person has to ask for it (**Merge without waiting for requirements (bypass)** in the web UI, `"bypass_rules": true` on `PUT /repos/{owner}/{repo}/pulls/{number}/merge`, or `diffinery pr merge --admin`), only when the pull request has no conflicts and isn't a draft, and each bypassed merge is recorded in the audit log as `pull_request.merge_bypassed` with the unmet requirements. Each bypass covers only what it names: `"bypass_rules": true` skips unmet rules (including a merge commit on a linear-history branch), and `"bypass_merge_queue": true` skips only a required merge queue; bypassing linear history or the queue is audited too. See [Branch protection](/docs/repositories/branch-protection). | |
| 167 | 170 | |
| 168 | 171 | Required checks are guarded against spoofing. A required check matches check runs and commit statuses by name, and Diffinery decides which reports count: |
| 169 | 172 | |
| @@ -174,8 +177,10 @@ | ||
| 174 | 177 | |
| 175 | 178 | ## Push protection |
| 176 | 179 | |
| 177 | -Every `git push` is scanned for credentials (AWS, GitHub, Diffinery, Slack, Stripe live, Google, Anthropic and OpenAI keys, and private keys) before anything is stored. A push that contains one is rejected as a whole and nothing from it is kept. A file can opt out with a `diffinery:allow-secret` marker, and a repository admin who is a person can push anyway with `git push -o allow-secrets=<reason>`, which is audited and emailed to the repository's admins. The same scan runs on files written with the contents API (`PUT /repos/{owner}/{repo}/contents/{path}`) and the git data API (blobs, and trees with inline `content`); there a match is refused with `422` and no bypass exists. See [Push protection](/docs/security/push-protection). | |
| 180 | +Every `git push` is scanned for credentials (AWS, GitHub, Diffinery, Slack, Stripe live, Google, Anthropic and OpenAI keys, and private keys) before anything is stored. A push that contains one is rejected as a whole and nothing from it is kept. A file can opt out with a `diffinery:allow-secret` marker, and a repository admin who is a person can push anyway with `git push -o allow-secrets=<reason>`, which is audited and emailed to the repository's admins. | |
| 178 | 181 | |
| 182 | +The same scan runs on every commit made without a Git client: the web editor, `POST /repos/{owner}/{repo}/diffinery/commits`, the contents API (`PUT /repos/{owner}/{repo}/contents/{path}`) and the MCP `commit_files` tool, and on the git data API (blobs, and trees with inline `content`). A match is refused with `422` and recorded as `api.blocked_secret` against the caller. Only the commits API has a bypass: a repository admin who is a person sends a reason in `allow_secrets`, which is recorded as `api.secret_bypassed` and emailed to the repository's admins. See [Push protection](/docs/security/push-protection). | |
| 183 | + | |
| 179 | 184 | ## Tree entry names |
| 180 | 185 | |
| 181 | 186 | Every write that builds a tree on the server refuses file and directory names that could be dangerous in a checkout. This covers commits made through the web editor, the contents API, `POST /repos/{owner}/{repo}/diffinery/commits`, the MCP `commit_files` tool and `POST /repos/{owner}/{repo}/git/trees`. A path is refused when any of its segments: |
| @@ -222,12 +227,15 @@ | ||
| 222 | 227 | |
| 223 | 228 | Each workflow job gets its own token, available as `secrets.GITHUB_TOKEN` and `DIFFINERY_TOKEN`: |
| 224 | 229 | |
| 225 | -- It is valid only for the repository and the job it was issued for. Diffinery stores only its hash. | |
| 226 | -- It stops working as soon as the job completes. | |
| 227 | -- It acts as `diffinery-actions[bot]` with read access to its own repository. | |
| 230 | +- It is issued for one job of one repository and stops working as soon as the job completes. Diffinery stores only its hash. | |
| 231 | +- It acts as `diffinery-actions[bot]` with the job's effective `permissions:` (the job's, else the workflow's, else the repository default). New repositories default to GitHub's restricted set, `contents` and `packages` read; repositories created before the setting existed default to write. Only a repository admin who is a person can change the default, with `PUT /repos/{owner}/{repo}/actions/permissions/workflow`, and each change is audited as `repo.actions_workflow_settings`. See [Workflow permissions](/docs/actions/workflow-permissions). | |
| 232 | +- Each API endpoint needs its permission; without it the request gets `403 Resource not accessible by integration`. Reads of public repositories are always allowed, and the token can read other public repositories but no other private one. | |
| 233 | +- Its role never exceeds write, so branch protection applies. It can't change repository settings, secrets, variables, webhooks, runners or branch protection, and it never creates, changes or deletes workflow files (`.github/workflows/`, `.diffinery/workflows/`), by push, API commit or merge. | |
| 234 | +- It can't open, approve or request changes on pull requests unless an admin allows it (off by default), and its approvals never count toward required approvals. | |
| 235 | +- A run whose code came from an agent acts as an agent (see [Agent guardrails](#agent-guardrails)). | |
| 228 | 236 | - For the [container registry](/docs/packages/container-registry), it can pull and push packages of its own repository according to the workflow's `packages` permission. |
| 229 | 237 | |
| 230 | -[Sandboxes](/docs/agents/sandboxes) get a similar token bound to one sandbox, which acts as the person or agent that created it with at most the write role, and stops working when the sandbox is closed. | |
| 238 | +[Sandboxes](/docs/agents/sandboxes) get a similar token bound to one sandbox, which acts as the person or agent that created it with at most the write role, can also read public repositories, and stops working when the sandbox is closed. | |
| 231 | 239 | |
| 232 | 240 | ## OIDC id tokens |
| 233 | 241 | |
| @@ -239,7 +247,7 @@ | ||
| 239 | 247 | |
| 240 | 248 | ## Audit log |
| 241 | 249 | |
| 242 | -Security-relevant actions go to an append-only audit log: sign-ins, passkey and token changes, agent and organization changes, profile pictures, repository settings (collaborators, branch protection, secrets, webhooks, topics), environment and environment secret changes, pushes, force pushes and ref deletions, merges, admin merges that bypass branch protection, push protection decisions, and package changes. Each entry records the actor, who an agent acted for, the action, the target and details. Site admins view it under **Settings**, **Audit log**. See [Audit log](/docs/security/audit-log). | |
| 250 | +Security-relevant actions go to an append-only audit log: sign-ins, passkey and token changes, agent and organization changes, profile pictures, repository settings (collaborators, branch protection, secrets, variables, webhooks, topics, workflow permissions, whether changed on the web or through the API), organization secrets, environment, environment secret and environment variable changes, cache and artifact deletions, pushes, force pushes and ref deletions, merges, admin merges that bypass branch protection or the merge queue, push protection decisions (for pushes and for commits made without Git), and package changes. Each entry records the actor, who an agent acted for, the action, the target and details. Site admins view it under **Settings**, **Audit log**. See [Audit log](/docs/security/audit-log). | |
| 243 | 251 | |
| 244 | 252 | ## Security emails |
| 245 | 253 | |
| @@ -253,7 +261,7 @@ | ||
| 253 | 261 | | Your token expires on ... | About 7 days before a token expires | |
| 254 | 262 | | A new agent was added | You create an agent | |
| 255 | 263 | | Your push was blocked: possible secrets | Push protection blocked your push (or your agent's) | |
| 256 | -| Push protection was bypassed | An admin pushed past push protection in a repository you administer | | |
| 264 | +| Push protection was bypassed | An admin pushed or committed past push protection in a repository you administer | | |
| 257 | 265 | | Your account was disabled / enabled | A site admin changes your account's status | |
| 258 | 266 | |
| 259 | 267 | See [Notifications](/docs/accounts/notifications). |
| @@ -262,7 +270,7 @@ | ||
| 262 | 270 | |
| 263 | 271 | - **One Durable Object per repository.** Each repository's refs, Git objects, issues, pull requests, reviews, checks, workflow runs, Actions secrets, webhooks and protection rules live in that repository's own Durable Object with its own SQLite storage. It is the only writer for that repository, which also makes ref updates and merges strongly consistent. |
| 264 | 272 | - **Authorization before routing.** The Worker resolves the caller and checks their role before a request reaches a repository's Durable Object. |
| 265 | -- **Scoped credentials.** Job and sandbox tokens name their repository and are verified by that repository's Durable Object, so they cannot be used on another repository. Secret encryption keys are derived per repository and per organization. | |
| 273 | +- **Scoped credentials.** Job and sandbox tokens name their repository and are verified by that repository's Durable Object, so they cannot change another repository or read another private one (like anyone, they can read public repositories). Secret encryption keys are derived per repository and per organization. | |
| 266 | 274 | - **Global data** (accounts, organizations, organization secrets, tokens, invites, notifications and the audit log) lives in a separate D1 database. |
| 267 | 275 | |
| 268 | 276 | See [Architecture](/docs/self-hosting/architecture). |
modifiedapps/diffinery/docs/security/push-protection.md+85−21View file
| @@ -1,9 +1,9 @@ | ||
| 1 | 1 | --- |
| 2 | 2 | title: Push protection |
| 3 | -description: How Diffinery scans git pushes and API file writes for leaked credentials, what a blocked push looks like, and how to opt out or bypass the check. | |
| 3 | +description: How Diffinery scans git pushes, imported history, web editor commits and API and MCP writes for leaked credentials, and how to opt out or bypass. | |
| 4 | 4 | --- |
| 5 | 5 | |
| 6 | -Push protection scans the files in every `git push` for credentials such as cloud keys, API tokens and private keys, and rejects the whole push if it finds one. It also scans files written through the REST API's contents and git data endpoints. It is always on for every repository: there is nothing to enable, and nothing from a blocked push or write is stored. | |
| 6 | +Push protection scans the files in every `git push` for credentials such as cloud keys, API tokens and private keys, and rejects the whole push if it finds one. The same scan runs on every commit made without a Git client: the web editor, `POST /repos/{owner}/{repo}/diffinery/commits`, the contents API and the MCP `commit_files` tool, and on files written through the git data API. It is always on for every repository: there is nothing to enable, and nothing from a blocked push or write is stored. History brought in by an [import from GitHub](/docs/migrate-from-github) is scanned too (see [Imported history](#imported-history)). | |
| 7 | 7 | |
| 8 | 8 | ## How it works |
| 9 | 9 | |
| @@ -45,17 +45,17 @@ | ||
| 45 | 45 | | Scanned | Not scanned | |
| 46 | 46 | |---|---| |
| 47 | 47 | | Blobs (file contents) contained in the packfile of a `git push` | Commits, trees and tags themselves (commit messages are not scanned) | |
| 48 | -| In a push, text files smaller than 1 MiB (1,048,576 bytes) | In a push, blobs of 1 MiB or more | | |
| 48 | +| In a push, text files smaller than 1 MiB (1,048,576 bytes) | In a push or a commit made without Git, files of 1 MiB or more | | |
| 49 | 49 | | Every ref in the push: branches and tags | Binary files: any blob that contains a NUL byte anywhere | |
| 50 | -| Files written with `PUT /repos/{owner}/{repo}/contents/{path}` | Files that contain the text `diffinery:allow-secret` (see [Opting a file out](#opting-a-file-out)) | | |
| 51 | -| Blobs created with `POST /repos/{owner}/{repo}/git/blobs`, and inline `content` in `POST /repos/{owner}/{repo}/git/trees` | Objects the server already has, which git does not send again | | |
| 52 | -| | Git LFS objects, which are uploaded separately from the push | | |
| 53 | -| | Release assets | | |
| 54 | -| | The web editor, `POST /repos/{owner}/{repo}/diffinery/commits` and the MCP `commit_files` tool | | |
| 50 | +| Files you add or change in the web editor | Files that contain the text `diffinery:allow-secret` (see [Opting a file out](#opting-a-file-out)) | | |
| 51 | +| Files in a commit made with `POST /repos/{owner}/{repo}/diffinery/commits` or the MCP `commit_files` tool | Objects the server already has, which git does not send again | | |
| 52 | +| Files written with `PUT /repos/{owner}/{repo}/contents/{path}` | Git LFS objects, which are uploaded separately from the push | | |
| 53 | +| Blobs created with `POST /repos/{owner}/{repo}/git/blobs`, and inline `content` in `POST /repos/{owner}/{repo}/git/trees` | Release assets | | |
| 54 | +| | Deleted files | | |
| 55 | 55 | |
| 56 | 56 | Pushes larger than 100 MB are refused before scanning with `push exceeds 100 MB; split it into smaller pushes`. |
| 57 | 57 | |
| 58 | -> **Important:** Push protection is a safety net, not a guarantee. It finds only the formats listed above, and changes made through the web editor, `POST /repos/{owner}/{repo}/diffinery/commits` or MCP are not scanned. Never rely on it as your only defense: keep secrets in [Actions secrets](/docs/actions/secrets-and-variables) and out of your repository. | |
| 58 | +> **Important:** Push protection is a safety net, not a guarantee. It finds only the formats listed above, in text files under 1 MiB. Never rely on it as your only defense: keep secrets in [Actions secrets](/docs/actions/secrets-and-variables) and out of your repository. | |
| 59 | 59 | |
| 60 | 60 | ## When a push is blocked |
| 61 | 61 | |
| @@ -114,29 +114,58 @@ | ||
| 114 | 114 | |
| 115 | 115 | 4. Push again. |
| 116 | 116 | |
| 117 | -## Writes through the REST API | |
| 117 | +## Commits and writes without Git | |
| 118 | 118 | |
| 119 | -The GitHub-compatible file endpoints run the same scan on each file they write: | |
| 119 | +Commits made without a Git client run the same scan before anything is written. This covers: | |
| 120 | 120 | |
| 121 | -| Endpoint | What is scanned | | |
| 121 | +| Where | What is scanned | | |
| 122 | 122 | |---|---| |
| 123 | +| The web editor (**Commit changes** on a new or edited file) | The file's new content | | |
| 124 | +| `POST /repos/{owner}/{repo}/diffinery/commits` | Every file in `files` that isn't a delete | | |
| 125 | +| The MCP `commit_files` tool | Every file in `files` | | |
| 123 | 126 | | `PUT /repos/{owner}/{repo}/contents/{path}` | The new file content | |
| 124 | 127 | | `POST /repos/{owner}/{repo}/git/blobs` | The blob | |
| 125 | 128 | | `POST /repos/{owner}/{repo}/git/trees` | Each tree entry given with inline `content`; entries given by `sha` refer to blobs already stored and are not scanned again | |
| 126 | 129 | |
| 127 | -The patterns, the binary-file rule and the `diffinery:allow-secret` opt-out are the same as for pushes. The 1 MiB limit does not apply: every text file is scanned, whatever its size. | |
| 130 | +The patterns, the binary-file rule and the `diffinery:allow-secret` opt-out are the same as for pushes. Commits (the first four rows) skip files of 1 MiB or more, as pushes do. The git data endpoints (`git/blobs` and `git/trees`) scan every text file, whatever its size. | |
| 128 | 131 | |
| 129 | -When a file matches, the request fails with `422` and nothing is stored: | |
| 132 | +### When a commit is refused | |
| 130 | 133 | |
| 134 | +A commit that contains a credential fails with `422` and nothing is stored. For the web editor, the commits API, the contents API and MCP, the message names the types and up to 10 paths: | |
| 135 | + | |
| 131 | 136 | ```json |
| 132 | 137 | { |
| 133 | - "message": "push protection: possible secret detected (AWS access key)", | |
| 138 | + "message": "push protection: possible secret detected (AWS access key) in config/deploy.env. Remove it, or rotate it and ask an admin.", | |
| 134 | 139 | "documentation_url": "https://docs.github.com/rest" |
| 135 | 140 | } |
| 136 | 141 | ``` |
| 137 | 142 | |
| 138 | -There is no bypass for API writes, not even for repository admins. If the file really must contain the value, add the `diffinery:allow-secret` marker to it, or push it with git and use [the bypass](#bypassing-push-protection). Each refused write is recorded in the [audit log](/docs/security/audit-log) as `api.blocked_secret` with the repository as the target and the detected types as detail. No email is sent for it. | |
| 143 | +The web editor shows the same message above the form and keeps your edit, so you can remove the value and commit again. If you sent `allow_secrets` but aren't allowed to bypass, the message ends with `Only repository admins (people, not agents) can bypass push protection.` | |
| 139 | 144 | |
| 145 | +The git data endpoints answer `422` with the shorter `push protection: possible secret detected (AWS access key)`. | |
| 146 | + | |
| 147 | +Each refused write is recorded in the [audit log](/docs/security/audit-log) as `api.blocked_secret`, against the person or agent who made the request, with the repository as the target and the detected types and paths (or, for the git data API, the blob id) as detail. No email is sent for it. | |
| 148 | + | |
| 149 | +### Bypassing with the commits API | |
| 150 | + | |
| 151 | +A repository admin who is a person can commit a flagged file anyway through `POST /repos/{owner}/{repo}/diffinery/commits` by giving a reason in `allow_secrets`, the API counterpart of `git push -o allow-secrets=`: | |
| 152 | + | |
| 153 | +```sh | |
| 154 | +curl -X POST https://diffinery.com/api/v3/repos/acme/website/diffinery/commits \ | |
| 155 | + -H "Authorization: Bearer dfy_..." \ | |
| 156 | + -H "Content-Type: application/json" \ | |
| 157 | + -d '{ | |
| 158 | + "branch": "main", | |
| 159 | + "message": "Add migration test fixture", | |
| 160 | + "allow_secrets": "revoked key kept in a migration test", | |
| 161 | + "files": [{ "path": "test/fixtures/old-key.txt", "content": "AKIA..." }] | |
| 162 | + }' | |
| 163 | +``` | |
| 164 | + | |
| 165 | +The same [requirements](#who-can-bypass) apply as for a push: the admin role on the repository, a person rather than an agent, a token with the `admin` scope, and a non-empty reason (trimmed and cut to 200 characters). A bypassed commit is recorded in the audit log as `api.secret_bypassed`, with the detected types, the paths and your reason, and the `[acme/website] Push protection was bypassed` email goes to you and to the repository's admins. | |
| 166 | + | |
| 167 | +The web editor, the contents API, the git data API and MCP have no bypass. If a file really must contain the value, add the `diffinery:allow-secret` marker to it, commit it with `allow_secrets` through the commits API, or push it with git and use [the bypass](#bypassing-push-protection). | |
| 168 | + | |
| 140 | 169 | ## Opting a file out |
| 141 | 170 | |
| 142 | 171 | Test fixtures sometimes need values that look like real keys. To let such a file through, put the text `diffinery:allow-secret` anywhere in it, usually in a comment: |
| @@ -152,7 +181,7 @@ | ||
| 152 | 181 | |
| 153 | 182 | ## Bypassing push protection |
| 154 | 183 | |
| 155 | -A repository admin who is a person can push a flagged change anyway by giving a reason with a git push option: | |
| 184 | +A repository admin who is a person can push a flagged change anyway by giving a reason with a git push option (for commits made through the API, see [Bypassing with the commits API](#bypassing-with-the-commits-api)): | |
| 156 | 185 | |
| 157 | 186 | ```sh |
| 158 | 187 | git push -o allow-secrets="revoked key kept in a migration test" origin main |
| @@ -182,6 +211,39 @@ | ||
| 182 | 211 | |
| 183 | 212 | Bypassing does not make a real credential safe: anyone who can read the repository can read it. Rotate real credentials even after a bypass. |
| 184 | 213 | |
| 214 | +## Imported history | |
| 215 | + | |
| 216 | +An [import from GitHub](/docs/migrate-from-github) copies a repository's Git history by pushing it into Diffinery in chunks, and push protection scans every one of those pushes like any other: each new blob under 1 MiB, with the same patterns and the same `diffinery:allow-secret` opt-out. Git LFS objects and release assets are not scanned, as for any push. Old history often holds credentials that were committed and removed later, so every import has a setting for what happens when push protection finds one: | |
| 217 | + | |
| 218 | +| Web form choice (under **Secrets found in old history (push protection)**) | API, CLI and MCP value | What happens | | |
| 219 | +|---|---|---| | |
| 220 | +| **Import with an audited bypass** (the default) | `"options": {"secrets": "bypass"}` | The import pushes with a bypass whose reason is `Imported from GitHub <owner>/<name>`. It works only when the import may bypass (see below); otherwise the import fails. | | |
| 221 | +| **Stop the import** | `"options": {"secrets": "stop"}` | The import fails at the first push with a finding, so you can rewrite the history on GitHub first. | | |
| 222 | + | |
| 223 | +### Who can import past a finding | |
| 224 | + | |
| 225 | +An import pushes as the person or agent who started it, with the token scope they started it with (a browser session counts as `admin`). So the bypass follows the same rules as a `git push -o allow-secrets=...`: | |
| 226 | + | |
| 227 | +- The import was started by a person, not an agent. **Agents never bypass push protection**: an import an agent starts always uses `stop`, whatever it asks for. | |
| 228 | +- That person has the admin role on the new repository: it is in their own account, or in an organization they own or administer (site admins have it everywhere). | |
| 229 | +- They started the import in the web UI or with an `admin`-scoped token. With a `write` token the bypass is refused. | |
| 230 | + | |
| 231 | +When a finding stops an import, the import fails with one of these errors, and the import page and `GET /api/v3/diffinery/imports/{id}` show it: | |
| 232 | + | |
| 233 | +| Error | Why | | |
| 234 | +|---|---| | |
| 235 | +| `Push protection found possible secrets in the history (<types>). The import was set to stop on secrets. Rotate them, then start a NEW import with the secrets option set to bypass (as a person who administers the target); a retry cannot change the options.` | The import used `stop`, or an agent started it. | | |
| 236 | +| `Push protection found possible secrets in the history (<types>). Only a person who administers the target can import them with an audited bypass (agents and write-scoped tokens cannot); start a new import as such a person.` | The import used `bypass`, but whoever started it can't bypass. | | |
| 237 | + | |
| 238 | +The Git transfer isn't tried again after such a failure, and retrying the import doesn't help, because a retry keeps the options and runs as the same person or agent. Rotate the credentials, then start a new import (as a person who administers the target, with `bypass`), or rewrite the history on GitHub and import again. The failed import's repository stays, so delete it or give the new import another `name`. | |
| 239 | + | |
| 240 | +### What an imported bypass records | |
| 241 | + | |
| 242 | +- Each bypassed push writes a `push.secret_bypassed` entry to the [audit log](/docs/security/audit-log), with the person who started the import as the actor, the reason `Imported from GitHub <owner>/<name>`, and in its detail the import's id and up to 50 blob ids. | |
| 243 | +- The person who started the import and the repository's admins get the `[acme/website] Push protection was bypassed` email. | |
| 244 | +- The import's summary lists every finding with its type, the file path when the import agent saw one, and the blob id: on the import page under **Possible secrets in history (rotate these)**, and as `summary.findings` in the API. Rotate these credentials. They are readable by anyone who can read the repository, and were already readable on GitHub. | |
| 245 | +- A push that push protection blocks during an import is recorded as `push.blocked_secret`, and the person who started the import (or the agent's owner) gets the `Your push was blocked: possible secrets` email. | |
| 246 | + | |
| 185 | 247 | ## Differences from GitHub push protection |
| 186 | 248 | |
| 187 | 249 | Diffinery's push protection follows the same idea as GitHub's: scan at push time, block before anything is stored, and allow an audited bypass. The differences: |
| @@ -190,10 +252,10 @@ | ||
| 190 | 252 | |---|---|---| |
| 191 | 253 | | Patterns | A large catalog of provider patterns, plus custom patterns | The nine types listed above; no custom patterns | |
| 192 | 254 | | Enabling | A repository or organization setting | Always on for every repository; cannot be turned off | |
| 193 | -| Bypass | A link in the push output where you pick a reason, then push again | `git push -o allow-secrets=<reason>` in the same push; repository admins who are people only | | |
| 255 | +| Bypass | A link in the push output where you pick a reason, then push again | `git push -o allow-secrets=<reason>` in the same push, or `allow_secrets` on `POST /repos/{owner}/{repo}/diffinery/commits`; repository admins who are people only | | |
| 194 | 256 | | Excluding files | Paths in `.github/secret_scanning.yml` | A `diffinery:allow-secret` marker inside each file | |
| 195 | 257 | | Notification | Secret scanning alerts in the repository | Audit log entries and email to the pusher (and to admins on a bypass) | |
| 196 | -| Existing history | Secret scanning can also scan existing content | Only new blobs in each push (and each file written through the contents and git data APIs) are scanned | | |
| 258 | +| Existing history | Secret scanning can also scan existing content | Only new blobs in each push, and each file written through the web editor, the commits, contents and git data APIs and MCP, are scanned. History imported from GitHub is scanned as it is pushed in. | | |
| 197 | 259 | |
| 198 | 260 | ## Troubleshooting |
| 199 | 261 | |
| @@ -203,6 +265,8 @@ | ||
| 203 | 265 | | The push is still blocked after you deleted the file in a new commit | The secret is still in an earlier commit you are pushing. Rewrite the commit that added it instead of adding a commit on top. | |
| 204 | 266 | | `Only repository admins (people, not agents) can bypass push protection.` | You used `-o allow-secrets=...` without being allowed. Check your role, that you are not using an agent's token, and that your token has the `admin` scope. | |
| 205 | 267 | | `-o allow-secrets=` seems to be ignored | The reason is empty. Give a non-empty reason, for example `-o allow-secrets="test fixture"`. | |
| 206 | -| A real key was not caught | It is not one of the formats above, it is in a binary file or in a pushed file of 1 MiB or more, the file contains `diffinery:allow-secret`, or the change was made with the web editor, `diffinery/commits` or MCP. Rotate the key and remove it. | | |
| 207 | -| `push protection: possible secret detected (...)` (422) from the API | A file you wrote with the contents or git data API matched. Remove the value, or mark a test fixture with `diffinery:allow-secret`. API writes have no bypass. | | |
| 268 | +| A real key was not caught | It is not one of the formats above, it is in a binary file or in a pushed or committed file of 1 MiB or more, or the file contains `diffinery:allow-secret`. Rotate the key and remove it. | | |
| 269 | +| `push protection: possible secret detected (...)` (422) from the API, MCP or the web editor | A file you committed or wrote matched. Remove the value, or mark a test fixture with `diffinery:allow-secret`. A repository admin who is a person can commit it anyway with `allow_secrets` on `POST /repos/{owner}/{repo}/diffinery/commits`; the other endpoints have no bypass. | | |
| 270 | +| `... Only repository admins (people, not agents) can bypass push protection.` (422) from the commits API | You sent `allow_secrets` without being allowed. Check your role, that you are not using an agent's token, and that your token has the `admin` scope. | | |
| 271 | +| An import from GitHub failed with `Push protection found possible secrets in the history (...)` | The source's history contains credentials and the import couldn't bypass. See [Imported history](#imported-history). | | |
| 208 | 272 | | `push exceeds 100 MB; split it into smaller pushes` | The pack is over the push size limit. Push fewer commits at a time, or move large files to [Git LFS](/docs/repositories/git-lfs). | |
modifiedapps/diffinery/docs/self-hosting/architecture.md+96−34View file
| @@ -9,16 +9,18 @@ | ||
| 9 | 9 | |
| 10 | 10 | | Piece of Diffinery | Cloudflare product | Binding | |
| 11 | 11 | | --- | --- | --- | |
| 12 | -| Web UI and public docs, REST API, git smart HTTP, Git LFS, MCP server, container registry, runner API, release asset uploads and downloads, source archives, profile pictures | Workers, with the Hono router | The Worker itself (`src/index.ts`) | | |
| 12 | +| Web UI and public docs, REST API, git smart HTTP, Git LFS, MCP server, container registry, runner API, Actions cache and artifact services, release asset uploads and downloads, source archives, profile pictures | Workers, with the Hono router | The Worker itself (`src/index.ts`) | | |
| 13 | 13 | | One repository: refs, git objects, issues, pull requests, reviews, reactions, checks and commit statuses, releases, deployments and environments, workflow runs, secrets, webhooks, merge queue | Durable Object with SQLite storage (`RepoDO`), one per repository | `REPO` | |
| 14 | -| Accounts (with profile picture version and appearance setting), organizations, tokens, repository list, topics and access, invitations, notifications, audit log, schedules, package index, email settings and log | D1, read through the Sessions API | `DB` | | |
| 14 | +| Accounts (with profile picture version and appearance setting), organizations, tokens, repository list, topics and access, invitations, notifications, audit log, schedules, package index, email settings and log, organization variables, GitHub imports and linked GitHub accounts | D1, read through the Sessions API | `DB` | | |
| 15 | 15 | | Large git objects, LFS objects, archived job logs, caches, artifacts, container image layers and manifests, the event archive, release assets, cached source archives, profile pictures | R2 | `BUCKET` | |
| 16 | 16 | | Webhooks (and retries of failed deliveries), notifications, audit fan-out | Queues (`diffinery-events`, dead-letter queue `diffinery-events-dlq`) | `EVENTS` | |
| 17 | 17 | | Email jobs | Queues (`diffinery-mail`, dead-letter queue `diffinery-mail-dlq`) | `MAIL` | |
| 18 | 18 | | Workflow run orchestration (durable across restarts) | Workflows (`diffinery-run`, class `RunWorkflow`) | `RUN_WORKFLOW` | |
| 19 | 19 | | Hosted job runners | Containers (`RunnerContainer`, one container per job) | `RUNNER` | |
| 20 | 20 | | Agent sandboxes | Containers (`SandboxContainer`, one container per sandbox) | `SANDBOX` | |
| 21 | -| Scheduled workflows, token expiry reminders, email log pruning, registry cleanup | Cron Triggers, every 5 minutes | `scheduled` handler | | |
| 21 | +| GitHub import orchestration (durable, with paged and retried steps) | Workflows (`diffinery-import`, class `ImportWorkflow`) | `IMPORT_WORKFLOW` | | |
| 22 | +| The Git transfer of a GitHub import | Containers (`ImportContainer`, one container per transfer) | `IMPORTER` | | |
| 23 | +| Scheduled workflows, token expiry reminders, email log pruning, registry cleanup, expiry of draft imports | Cron Triggers, every 5 minutes | `scheduled` handler | | |
| 22 | 24 | | Live run logs and pull request updates in the browser | Durable Object WebSockets with hibernation | Through `REPO` | |
| 23 | 25 | | Outgoing email | Email Sending | `EMAIL` | |
| 24 | 26 | | Email-based account recovery and first admin sign-in | Cloudflare Access, on `/auth/access` only | `ACCESS_TEAM_DOMAIN`, `ACCESS_AUD` | |
| @@ -37,26 +39,27 @@ | ||
| 37 | 39 | | RepoDO | | Queues | | Containers | |
| 38 | 40 | | (one per | | events | | RunnerContainer | |
| 39 | 41 | | repo, | | mail | | SandboxContainer | |
| 40 | - | SQLite) | +--------+ +------------------+ | |
| 41 | - +------------+ | ^ | |
| 42 | - | ^ v | | |
| 42 | + | SQLite) | +--------+ | ImportContainer | | |
| 43 | + +------------+ | +------------------+ | |
| 44 | + | ^ v ^ | |
| 43 | 45 | | | webhooks, email, | |
| 44 | 46 | v | audit log | |
| 45 | - Workflows (RunWorkflow) ------------+ | |
| 47 | + Workflows (RunWorkflow, | | |
| 48 | + ImportWorkflow) --------+ | |
| 46 | 49 | ``` |
| 47 | 50 | |
| 48 | 51 | ## The Worker |
| 49 | 52 | |
| 50 | -`apps/diffinery/src/index.ts` exports the Worker's `fetch`, `queue` and `scheduled` handlers, and the classes Cloudflare instantiates: `RepoDO`, `RunnerContainer`, `SandboxContainer` and `RunWorkflow`. | |
| 53 | +`apps/diffinery/src/index.ts` exports the Worker's `fetch`, `queue` and `scheduled` handlers, and the classes Cloudflare instantiates: `RepoDO`, `RunnerContainer`, `SandboxContainer`, `RunWorkflow`, `ImportWorkflow` and `ImportContainer`. | |
| 51 | 54 | |
| 52 | 55 | Every request goes through one middleware before it reaches a route: |
| 53 | 56 | |
| 54 | 57 | 1. **Cross-site check.** A request that changes state (anything but `GET`, `HEAD` and `OPTIONS`) and carries no `Authorization` credential is refused with `403 cross-site request refused` if the browser marks it as coming from another site (`Sec-Fetch-Site`, or a foreign `Origin`). Token clients are exempt. The web UI's forms also use a double-submit CSRF token. |
| 55 | 58 | 2. **D1 session.** All D1 queries of the request run in one D1 session (see [D1 and read replication](#d1-and-read-replication)). |
| 56 | -3. **Identity.** `GET` and `HEAD` requests for profile pictures (`/api/avatars/...`) skip this step and run as anonymous, because they are public and appear many times on every page. For everything else, a `dfy_` token in the `Authorization` header (Bearer, `token` or Basic) becomes an account principal with the token's scopes. Job tokens (`djt_`) and sandbox tokens (`dst_`) become a principal for one repository. Otherwise the session cookie from a passkey sign-in is checked. Otherwise the request is anonymous. | |
| 59 | +3. **Identity.** `GET` and `HEAD` requests for profile pictures (`/api/avatars/...`) skip this step and run as anonymous, because they are public and appear many times on every page. Requests to the Actions cache and artifact services (`/twirp/...` and `/_runtime/...`) skip it too: they carry their own signed token or URL (see [Actions cache and artifact services](#actions-cache-and-artifact-services)). For everything else, a `dfy_` token in the `Authorization` header (Bearer, `token` or Basic) becomes an account principal with the token's scopes. The push token of a GitHub import is a `dfy_` token too, but it is accepted only on Git smart HTTP, Git LFS and that import's own agent endpoints (`403 this token can only be used by its GitHub import` elsewhere). Job tokens (`djt_`) and sandbox tokens (`dst_`) become a principal for one repository. Otherwise the session cookie from a passkey sign-in is checked. Otherwise the request is anonymous. | |
| 57 | 60 | 4. After the route runs, the response gets security headers and the `x-d1-bookmark` header. |
| 58 | 61 | |
| 59 | -Routes are mounted in this order, so earlier ones win: profile pictures (`/api/avatars/...`), OIDC discovery (`/.well-known/*`), git smart HTTP and LFS (`/<owner>/<repo>.git/...`), the runner API (`/runner/jobs/...`), the email REST API, the container registry (`/v2/`), the packages REST API, the REST API (`/api/v3`), release asset uploads (`/api/uploads/...`), release asset downloads (`/<owner>/<repo>/releases/download/...`) and source archives (`/<owner>/<repo>/archive/...`), the MCP server (`/mcp`), the live-update WebSocket and artifact downloads, a catch-all for `/api/*`, and finally the server-rendered web UI, which also serves the public docs at `/docs`. The `/api/*` catch-all answers any API path that no route matched with GitHub's JSON 404, `{"message": "Not Found", "documentation_url": "https://docs.github.com/rest"}`, so API clients never get the web UI's HTML error page. | |
| 62 | +Routes are mounted in this order, so earlier ones win: profile pictures (`/api/avatars/...`), OIDC discovery (`/.well-known/*`), git smart HTTP and LFS (`/<owner>/<repo>.git/...`), the runner API (`/runner/jobs/...`), the email REST API, the container registry (`/v2/`), the packages REST API, the Actions cache and artifact services together with the REST API's artifact and cache routes, the REST API (`/api/v3`), release asset uploads (`/api/uploads/...`), release asset downloads (`/<owner>/<repo>/releases/download/...`) and source archives (`/<owner>/<repo>/archive/...`), the MCP server (`/mcp`), the live-update WebSocket and artifact downloads, a catch-all for `/api/*`, and finally the server-rendered web UI, which also serves the public docs at `/docs`. The `/api/*` catch-all answers any API path that no route matched with GitHub's JSON 404, `{"message": "Not Found", "documentation_url": "https://docs.github.com/rest"}`, so API clients never get the web UI's HTML error page. | |
| 60 | 63 | |
| 61 | 64 | Some work happens in the Worker itself: profile pictures are checked, cropped and resized to small WebP files with WebAssembly image codecs, and code in the web UI and the docs is syntax highlighted with Shiki on the server. |
| 62 | 65 | |
| @@ -67,8 +70,9 @@ | ||
| 67 | 70 | - An anonymous caller (no token, no session) gets `401 authentication required` for any write, even on a public repository. |
| 68 | 71 | - A personal or agent token whose scope is only `read` gets `403 this token needs the write scope`. |
| 69 | 72 | - Browser sessions act with full scope, and job and sandbox tokens are limited by their own role instead. |
| 73 | +- A job token is also checked against the job's effective `permissions:`, which the Durable Object records when the job starts. A request outside them gets `403 Resource not accessible by integration`, and some endpoints refuse job tokens altogether. See [Workflow permissions](/docs/actions/workflow-permissions). | |
| 70 | 74 | |
| 71 | -Account-level changes that don't belong to a repository check the token's scope too: creating a repository needs `write`, while creating an organization, sending invitations, managing organization secrets, creating and revoking tokens, and creating and deleting agents need `admin`. A token restricted to some repositories is refused for creating repositories and organizations, invitations, organization secrets, agents and revoking tokens (`403 this token is limited to repositories and can't change account settings`). | |
| 75 | +Account-level changes that don't belong to a repository check the token's scope too: creating a repository needs `write`, while creating an organization, sending invitations, managing organization secrets, creating and revoking tokens, and creating and deleting agents need `admin`. A token restricted to some repositories is refused for creating repositories and organizations, invitations, organization secrets, agents, and creating and revoking tokens (`403 this token is limited to repositories and can't change account settings`). | |
| 72 | 76 | |
| 73 | 77 | ## RepoDO: one Durable Object per repository |
| 74 | 78 | |
| @@ -79,13 +83,15 @@ | ||
| 79 | 83 | - **Git object store.** Objects are stored deflated. Small objects live in SQLite; objects larger than 512 KiB after compression go to R2 (SQLite rows are capped at 2 MB). Recently read objects stay in a 24 MiB in-memory cache. |
| 80 | 84 | - **Git protocol.** Protocol v2 `upload-pack` for clone and fetch (including shallow clones), and `receive-pack` for push, with push protection, branch protection and agent rules checked before refs move. |
| 81 | 85 | - **Collaboration.** Issues, pull requests, comments, reviews, reactions, issue events, labels, check runs and commit statuses, webhooks configuration and mergeability. |
| 82 | -- **Releases and deployments.** Releases and their asset metadata (the files are in R2), deployments and their statuses, and environments with their branch policies, secrets (encrypted with a key derived from `MASTER_KEY`) and variables. The object also remembers who last moved each tag, so tags pushed by agents never match an environment's tag rules. | |
| 86 | +- **Releases and deployments.** Releases and their asset metadata (the files are in R2), deployments and their statuses, and environments with their branch policies, secrets (encrypted with a key derived from `MASTER_KEY`) and variables. The object also remembers who last moved each branch and tag, so tags pushed by agents never match an environment's tag rules and runs on code an agent pushed act as an agent. | |
| 83 | 87 | - **Git data and archives.** The git data API (blobs, trees, commits, tags and refs) and zip and tar.gz source archives, which are built in the object and cached in R2. |
| 84 | -- **Actions.** Parsing workflows from the pushed commit, creating runs and jobs, the runner API's server side, logs while a job runs, caches and artifacts metadata, secrets (encrypted with a key derived from `MASTER_KEY`) and variables, and sandboxes. | |
| 88 | +- **Actions.** Parsing workflows from the pushed commit, creating runs and jobs, the runner API's server side, logs while a job runs, secrets (encrypted with a key derived from `MASTER_KEY`) and variables, and sandboxes. | |
| 89 | +- **Workflow tokens.** The repository's workflow settings (default token permissions, whether workflows may create and approve pull requests), each job's granted role and permissions, and the check that keeps a job token from creating, changing or deleting workflow files. | |
| 90 | +- **Caches and artifacts.** Their records, the per-repository cache budget and artifact quota, eviction of caches and expiry of artifacts. The contents are in R2. | |
| 85 | 91 | - **Merge queue.** One queue per protected branch. The object's single alarm wakes it when a merge group's check timeout runs out. |
| 86 | 92 | - **Live updates.** WebSocket connections, see [Live updates](#live-updates). |
| 87 | 93 | |
| 88 | -Deleting a repository wipes the object's storage. | |
| 94 | +Deleting a repository wipes the object's storage and deletes the repository's caches, artifacts, archived job logs, unfinished uploads and cached source archives from R2. | |
| 89 | 95 | |
| 90 | 96 | ## D1 and read replication |
| 91 | 97 | |
| @@ -106,18 +112,36 @@ | ||
| 106 | 112 | - git objects over 512 KiB compressed, |
| 107 | 113 | - Git LFS objects (verified against their SHA-256 on upload), |
| 108 | 114 | - job logs, archived from the Durable Object when the job completes, |
| 109 | -- Actions caches and artifacts, | |
| 115 | +- Actions caches and artifacts, and their uploads while they are in progress, | |
| 110 | 116 | - container image blobs and manifests, with uploads written as multipart uploads, |
| 111 | 117 | - an append-only archive of every event that goes through the events queue, |
| 112 | 118 | - release assets (up to 100 MB each), |
| 113 | 119 | - source archives (zipball and tarball downloads), cached after they are first built, |
| 114 | 120 | - profile pictures of people and organizations, stored as small WebP files in a few sizes. |
| 115 | 121 | |
| 122 | +Durable and short-lived data have their own top-level prefixes, because R2 lifecycle rules select objects by prefix: | |
| 123 | + | |
| 124 | +| Prefix | What | Removed by | | |
| 125 | +| --- | --- | --- | | |
| 126 | +| `repos/` | Git objects, LFS objects and release assets | Nothing by age. Kept until the repository or the object is deleted. | | |
| 127 | +| `registry/` | Container image blobs and manifests | Nothing by age. The cron trigger removes blobs no image uses. | | |
| 128 | +| `avatars/` | Profile pictures | Nothing by age. Replaced when someone uploads a new picture. | | |
| 129 | +| `actions-cache/` | Actions caches | Nothing by age. Diffinery evicts caches itself (least recently used, and unused for 7 days). | | |
| 130 | +| `artifacts/` | Workflow artifacts | Diffinery at the end of each artifact's retention, and a lifecycle rule after 90 days. | | |
| 131 | +| `logs/` | Archived job logs | A lifecycle rule after 90 days. | | |
| 132 | +| `actions-staging/` | Cache and artifact uploads in progress (blocks and chunks) | Diffinery when the upload finishes, and a lifecycle rule after 1 day. | | |
| 133 | +| `archives/` | Cached source archives | A lifecycle rule after 30 days. They are built again on the next download. | | |
| 134 | +| `events/` | The event archive | A lifecycle rule after 400 days. | | |
| 135 | + | |
| 136 | +A lifecycle rule also aborts multipart uploads left unfinished for a day, under every prefix. `scripts/bootstrap.sh` sets these rules on every deploy and replaces the bucket's lifecycle configuration when it does, so rules added by hand in the Cloudflare dashboard don't survive the next deploy. If it can't set them, the deploy stops with `could not set the R2 lifecycle rules (token has R2: Edit?)`. It then reads the rules back and stops with `an R2 lifecycle rule still deletes objects under repos/ (git objects, LFS); refusing to continue` if any rule would delete objects under `repos/` or in the whole bucket. | |
| 137 | + | |
| 138 | +Instances that stored artifacts, caches and job logs before these prefixes existed kept them under `repos/<repository ID>/`. Diffinery still reads them from there, and the lifecycle rules never touch them. | |
| 139 | + | |
| 116 | 140 | ## Queues |
| 117 | 141 | |
| 118 | -**`diffinery-events`** carries every repository event: `push`, `pull_request`, `issues`, `issue_comment`, `pull_request_review`, `workflow_run`, `workflow_dispatch`, `release`, `status`, `deployment`, `deployment_status`, `repository_dispatch` and `audit`. The Durable Object sends events as things happen, so the request that caused them doesn't wait. The consumer takes batches of up to 50 and, for each batch, appends the events to the R2 archive. For each event it writes audit log entries (pushes, force pushes, ref deletions, merges and explicit audit events), delivers webhooks, and creates notifications and email jobs. A failing event is retried with backoff up to 8 times, then moved to `diffinery-events-dlq`. | |
| 142 | +**`diffinery-events`** carries every repository event: `push`, `pull_request`, `issues`, `issue_comment`, `pull_request_review`, `workflow_run`, `workflow_dispatch`, `release`, `status`, `deployment`, `deployment_status`, `repository_dispatch` and `audit`. The Durable Object sends events as things happen, so the request that caused them doesn't wait. The consumer takes batches of up to 50 and, for each batch, appends the events to the R2 archive. For each event it writes audit log entries (pushes, force pushes, ref deletions, merges and explicit audit events), delivers webhooks, and creates notifications and email jobs. Webhooks and notifications run as two separate stages: a stage that fails is put back on the queue alone (a `stage_retry` message) after 30 seconds, 2 minutes, 10 minutes, 30 minutes and 1 hour, then given up and logged, so retrying notifications never sends webhooks again and the other way round. Once a webhook may have been called, errors no longer fail the webhooks stage; that delivery's own retries take over. Any other failure retries the whole event with backoff up to 8 times, then moves it to `diffinery-events-dlq`. | |
| 119 | 143 | |
| 120 | -The same queue carries webhook retries. When a delivery fails with a network error or timeout, `408`, `429` or a `5xx` status, the consumer puts a retry message for that one webhook back on the queue with a delay: 1 minute, then 5 minutes, 30 minutes, 1 hour and 3 hours, so a delivery is tried at most 6 times, the last one about 4 hours and 36 minutes after the first. A retry sends the same body with the same `X-GitHub-Delivery` ID, to the webhook's current URL and signed with its current secret; it is dropped if the webhook was deleted in the meantime. Other statuses, including `3xx` and other `4xx` answers, are not retried. Retry messages are not added to the R2 event archive. | |
| 144 | +The same queue carries webhook retries. When a delivery fails with a network error or timeout, `408`, `429` or a `5xx` status, the consumer puts a retry message for that one webhook back on the queue with a delay: 1 minute, then 5 minutes, 30 minutes, 1 hour and 3 hours, so a delivery is tried at most 6 times, the last one about 4 hours and 36 minutes after the first. A retry sends the same body with the same `X-GitHub-Delivery` ID, to the webhook's current URL and signed with its current secret; it is dropped if the webhook was deleted in the meantime. Other statuses, including `3xx` and other `4xx` answers, are not retried. Webhook and stage retry messages are not added to the R2 event archive. | |
| 121 | 145 | |
| 122 | 146 | **`diffinery-mail`** carries email jobs. It is separate so that sign-in links and invitation links never land in the event archive. The consumer applies the recipient's notification settings, renders the HTML and text versions, and sends through Email Sending. Temporary failures are retried with backoff; after 6 attempts the email is logged as failed. |
| 123 | 147 | |
| @@ -125,20 +149,36 @@ | ||
| 125 | 149 | |
| 126 | 150 | Each workflow run is one instance of the `diffinery-run` Workflow, with the run's ID as the instance ID. `RunWorkflow` makes a run durable: its steps, sleeps and waits survive restarts and deploys. |
| 127 | 151 | |
| 128 | -1. **Plan.** Ask the repository's Durable Object for the job graph, in levels by `needs`. If the run's concurrency group is busy, sleep a minute and plan again, for up to 6 hours. | |
| 129 | -2. **Start each level.** Start every job of the level (each start retried up to 3 times). Matrix jobs are expanded here, and up to `max-parallel` start at once. | |
| 152 | +1. **Plan.** Ask the repository's Durable Object for the job graph: each job with the jobs it `needs` (calls to reusable workflows expanded). If the run's concurrency group is busy, sleep a minute and plan again, for up to 6 hours. | |
| 153 | +2. **Start jobs as their needs finish.** A job starts as soon as every job it needs has finished, without waiting for unrelated jobs (each start retried up to 3 times). Matrix jobs are expanded here, and up to `max-parallel` start at once. | |
| 130 | 154 | 3. **Wait.** For each started job, wait for an event named `job-<id>` that the Durable Object sends when the job completes, with a timeout of 6 hours. On timeout the job is marked `timed_out` and its container stopped. |
| 131 | -4. **Finish.** Compute the run's conclusion, which also triggers `workflow_run` followers. | |
| 155 | +4. **Finish.** Once every job has finished, compute the run's conclusion, which also triggers `workflow_run` followers. | |
| 132 | 156 | |
| 157 | +Runs that were planned by an older version, which grouped jobs in levels, finish level by level. | |
| 158 | + | |
| 159 | +Each attempt of a GitHub import is one instance of the `diffinery-import` Workflow, with `<import id>-<attempt>` as the instance ID. `ImportWorkflow` runs every step with `step.do`, each retried up to 5 times with exponential backoff from 15 seconds and limited to 15 minutes per call, and turns GitHub rate limits into Workflow sleeps. Steps that page through GitHub are called again and again with a cursor until they report that they are done, so no single step runs long. See [Request flow: a GitHub import](#request-flow-a-github-import). | |
| 160 | + | |
| 133 | 161 | ## Containers |
| 134 | 162 | |
| 135 | -Both container classes use the same image, built from `containers/runner/Dockerfile` (Ubuntu 24.04 with Node.js 22, git, git-lfs, Python and build tools, plus the runner agent). | |
| 163 | +All three container classes use the same image, built from `containers/runner/Dockerfile` (Ubuntu 24.04 with Node.js 22, git, git-lfs, Python and build tools, plus the runner agent). | |
| 136 | 164 | |
| 137 | -- **`RunnerContainer`** runs one hosted job (`runs-on: ubuntu-latest` and other `ubuntu-*` or `linux` labels). The Durable Object starts a container named after the job with three environment variables: `DIFFINERY_API_URL`, `DIFFINERY_JOB_ID` and `DIFFINERY_JOB_TOKEN`. The agent fetches its job spec from `/runner/jobs/<id>`, runs the steps, streams logs, uses the cache, artifact and OIDC endpoints, reports completion and exits. If the container stops before the job reports completion, the job fails with "The runner stopped unexpectedly". Instance type `standard-2`, at most 100 at once, internet access on. Docker is started inside the container the first time a job uses it. | |
| 165 | +- **`RunnerContainer`** runs one hosted job (`runs-on: ubuntu-latest` and other `ubuntu-*` or `linux` labels). The Durable Object starts a container named after the job with three environment variables: `DIFFINERY_API_URL`, `DIFFINERY_JOB_ID` and `DIFFINERY_JOB_TOKEN`. The agent fetches its job spec from `/runner/jobs/<id>`, runs the steps, streams logs, uses the cache and artifact services and the OIDC endpoint, reports completion and exits. If the container stops before the job reports completion, the job fails with "The runner stopped unexpectedly". Instance type `standard-2`, at most 100 at once, internet access on. Docker is started inside the container the first time a job uses it. | |
| 138 | 166 | - **`SandboxContainer`** runs one agent sandbox: the repository checked out at a ref, with an HTTP API on port 8080 for `exec`, file reads and file writes. The Worker forwards REST and MCP sandbox calls to it with a per-sandbox secret. Instance type `standard-1`, at most 50 at once, stopped after 30 minutes without requests. |
| 167 | +- **`ImportContainer`** runs the Git transfer of one GitHub import: the runner agent in import mode, started with `DIFFINERY_API_URL`, `DIFFINERY_IMPORT_ID` and `DIFFINERY_IMPORT_TOKEN`. One container per try, named `<import id>-<attempt>-<try>`. If it stops before the agent reports a result (a crash, running out of memory, a platform restart), the transfer counts as failed and the import's Workflow can try it again. Instance type `standard-4` (a mirror clone of a large repository needs disk and memory), at most 20 at once, internet access on, stopped after 7 hours without activity. | |
| 139 | 168 | |
| 140 | 169 | Self-hosted runners (`runs-on: [self-hosted, ...]`) run the same agent outside Cloudflare. They poll `POST /api/v3/repos/{owner}/{repo}/actions/runners/claim` and then talk to the same runner API. See [Runners](/docs/actions/runners). |
| 141 | 170 | |
| 171 | +## Actions cache and artifact services | |
| 172 | + | |
| 173 | +`actions/cache`, `actions/upload-artifact`, `actions/download-artifact` and other tools built on GitHub's Actions toolkit talk to the same services they use on GitHub, which the Worker implements in `src/http/actions-runtime.ts`: | |
| 174 | + | |
| 175 | +- The cache and artifact services at `ACTIONS_RESULTS_URL`, and the older cache API at `ACTIONS_CACHE_URL`. | |
| 176 | +- Signed upload and download URLs under `/_runtime/blob/...`, which accept the parts of the Azure Blob Storage protocol the toolkit uses (blocks, block lists and ranged reads) and store the data in R2. | |
| 177 | + | |
| 178 | +The job spec gives the agent an `ACTIONS_RUNTIME_TOKEN` and these URLs, and the agent passes them to JavaScript and Docker actions and to its built-in cache and artifact steps, not to `run:` steps. The token is an HMAC-signed JWT for one job, with a key derived from `MASTER_KEY`. It is accepted only while its job is in progress and until 30 minutes after the job's timeout, and it can only read caches of the job's own branch, its base branch and the default branch. Without `MASTER_KEY`, jobs get no token: the built-in cache step is skipped with a warning and the built-in artifact steps fail. | |
| 179 | + | |
| 180 | +The Durable Object decides what each request may read or write and keeps the records (`rt_caches` and `rt_artifacts`). It applies the 10 GB cache budget per repository and the artifact quota (`ARTIFACT_QUOTA_BYTES`), evicts caches and expires artifacts. Older runner agents use their own cache and artifact endpoints under `/runner/jobs/<id>/...`, recorded in the `caches` and `artifacts` tables. Limits are listed in [Caches and artifacts](/docs/reference/limits#caches-and-artifacts). | |
| 181 | + | |
| 142 | 182 | ## Cron trigger |
| 143 | 183 | |
| 144 | 184 | The `scheduled` handler runs every 5 minutes (`*/5 * * * *`, GitHub's minimum schedule interval) and: |
| @@ -146,6 +186,7 @@ | ||
| 146 | 186 | 1. Cleans up the container registry: abandoned uploads and blobs no image uses. |
| 147 | 187 | 2. Reads every workflow schedule from D1 (kept in sync from the default branch on each push) for repositories that aren't archived, and starts a run for each cron expression that matched any minute of the last 5. |
| 148 | 188 | 3. Sends token expiry reminders and, once a day, prunes the email log. |
| 189 | +4. Cancels draft organization imports (started in the web form, repositories never chosen) that are more than 7 days old, with the error `draft expired`, and erases their GitHub token. | |
| 149 | 190 | |
| 150 | 191 | ## Live updates |
| 151 | 192 | |
| @@ -163,8 +204,8 @@ | ||
| 163 | 204 | 3. The Durable Object refuses pushes over 100 MB, parses the ref commands and the packfile, and resolves deltas against objects it already has. Nothing is stored yet. |
| 164 | 205 | 4. **Push protection** scans every new blob under 1 MiB for credentials. A finding blocks the whole push unless a human repository admin passed `-o allow-secrets=<reason>`. Both a block and a bypass are recorded as audit events. |
| 165 | 206 | 5. The objects are written (SQLite or R2 by size). |
| 166 | -6. Each ref update is checked: valid ref name, write role, `refs/pull/` and merge queue branches are read-only, objects exist, tags can only be moved by maintainers, agents can't push to the default or protected branches, and branch protection rules (pull request required, force push, deletion, linear history; admins bypass unless the rule is enforced for them). When several rules match the branch, the rule whose pattern is exactly the branch name applies; otherwise the oldest matching rule does. With `atomic`, one failure rejects all. | |
| 167 | -7. Each accepted ref moves with a compare-and-swap. | |
| 207 | +6. Each ref update is checked: valid ref name, write role, a job token can't create, change or delete workflow files, `refs/pull/` and merge queue branches are read-only, objects exist, tags can only be moved by maintainers, agents can't push to the default or protected branches, and branch protection rules (pull request required, force push, deletion, linear history; admins bypass unless the rule is enforced for them). When several rules match the branch, the rule whose pattern is exactly the branch name applies; otherwise the oldest matching rule does. With `atomic`, one failure rejects all. | |
| 208 | +7. Each accepted ref moves with a compare-and-swap, and the object records who moved it and whether that was a person or an agent. | |
| 168 | 209 | 8. After each ref moves, the object emits a `push` event to the events queue and to WebSocket subscribers, updates open pull requests whose head branch moved (new head SHA, stale approvals dismissed when the rule asks for it, a `synchronize` event, new test merge commits and `pull_request` workflows), starts `push` workflows whose filters match, re-syncs schedules when the default branch moved, and re-checks merge queues on that branch. |
| 169 | 210 | 9. The report goes back to git, including messages like "Create a pull request for 'feature':" with a link. |
| 170 | 211 | 10. In the background, the Worker records the push time in D1. The events queue consumer archives the event, writes the audit log entry, delivers webhooks and sends notifications. |
| @@ -173,11 +214,23 @@ | ||
| 173 | 214 | |
| 174 | 215 | 1. A trigger (push, pull request, dispatch, `repository_dispatch`, a release, schedule, `workflow_run`, merge group) reaches the Durable Object, which loads `.github/workflows/*.yml` and `.diffinery/workflows/*.yml` from the commit, parses them with `@diffinery/workflow-yaml`, and matches triggers and filters. |
| 175 | 216 | 2. For each matching workflow it inserts a run, evaluates `run-name` and `concurrency` (cancelling older runs with `cancel-in-progress`), creates a `diffinery-run` Workflow instance and emits a `workflow_run` event. |
| 176 | -3. The Workflow plans the job graph and starts the first level. For each job the Durable Object expands reusable workflows and the matrix, inserts job rows and a queued check run on the commit, creates a job token and starts a `RunnerContainer`. A job with `environment:` is first checked against the environment's branch policy, gets the environment's secrets and variables, and creates a deployment that tracks the job. Self-hosted jobs wait for a runner to claim them instead. | |
| 177 | -4. The runner agent in the container fetches the job spec (steps, contexts, secrets and variables decrypted for this job), checks out the code and runs the steps. Log lines go to the Durable Object, which stores them and broadcasts them on `job:<id>` to open browser tabs. Caches and artifacts go to R2 through the runner API; OIDC tokens are signed with `OIDC_PRIVATE_KEY`. | |
| 217 | +3. The Workflow plans the job graph and starts the jobs that need nothing. For each job the Durable Object expands reusable workflows and the matrix, inserts job rows and a queued check run on the commit, works out the job's effective permissions (from `permissions:`, the repository's default, and the caller of a reusable workflow) and records them, creates a job token and a runtime token, and starts a `RunnerContainer`. A job with `environment:` is first checked against the environment's branch policy, gets the environment's secrets and variables, and creates a deployment that tracks the job. Self-hosted jobs wait for a runner to claim them instead. | |
| 218 | +4. The runner agent in the container fetches the job spec (steps, contexts, secrets and variables decrypted for this job), checks out the code and runs the steps. Log lines go to the Durable Object, which stores them and broadcasts them on `job:<id>` to open browser tabs. Caches and artifacts go to R2 through the [cache and artifact services](#actions-cache-and-artifact-services); OIDC tokens are signed with `OIDC_PRIVATE_KEY`. | |
| 178 | 219 | 5. On completion the agent reports outputs and conclusion. The Durable Object updates the job and its check run, archives the log to R2, cancels matrix siblings on failure with `fail-fast`, starts the next queued matrix job, and sends the `job-<id>` event to the Workflow instance. |
| 179 | -6. The Workflow moves to the next level. When all levels are done, it finishes the run: the Durable Object records the conclusion, emits a `workflow_run` completed event (which drives webhooks and failure emails) and starts any `workflow_run` followers. | |
| 220 | +6. The Workflow starts every job whose needs have now all finished. When every job is done, it finishes the run: the Durable Object records the conclusion, emits a `workflow_run` completed event (which drives webhooks and failure emails) and starts any `workflow_run` followers. | |
| 180 | 221 | |
| 222 | +## Request flow: a GitHub import | |
| 223 | + | |
| 224 | +1. `POST /api/v3/diffinery/imports` (also the web form, `diffinery migrate github` and the MCP tool `start_github_import`) reaches the Worker. It validates the request, checks that the caller may import into the target account and is under the limits of imports at a time, and reads the source repository (or the GitHub organization's repository list) from the GitHub REST API. It encrypts the GitHub token with a key derived from `MASTER_KEY`, records the import in D1 (one `imports` row per repository, plus one for an organization import), writes the `import.create` audit entry and creates the `ImportWorkflow` instance. | |
| 225 | +2. **Repository.** The Workflow checks again that the person or agent who started the import may still import into the target, creates the repository (or reuses the one an earlier attempt of the same import created), copies its description, homepage and topics, and takes the repository's import lock in its Durable Object. While the lock is held, the Durable Object refuses every `git push` that isn't this import's (`repository is locked by an import in progress`). A retry also refuses to continue when the repository holds refs, issues, pull requests, comments, reviews, labels, milestones or protection rules that the import didn't write. | |
| 226 | +3. **Git transfer.** The Worker mints a push token for the transfer (the starter's account and token scope, this repository only, valid for one day) and starts an `ImportContainer`. The agent fetches its spec from `/api/v3/diffinery/imports/<id>/agent/spec`, runs `git clone --mirror` from GitHub and fetches the LFS objects. The GitHub token reaches git only through per-host configuration in the environment, never in a command line or a log, and git never follows redirects. The agent then pushes the history to Diffinery in chunks of at most 32 MiB, then every branch and tag, the head commit of each pull request to a temporary `refs/diffinery-import/pull/<n>` ref, and the LFS objects. Every push carries `-o diffinery-import`, which the Durable Object honors only for that import's current push token: push protection still scans every new blob, refs can only be created or moved if this import wrote them, and no push event, workflow, webhook or notification follows. Each such push writes one `import.push` audit entry. When old history contains credentials and the import was set to bypass, the push also carries `-o allow-secrets=Imported from GitHub <owner>/<name>`, which works only for a person with the admin role and an `admin` token or browser session (see [Push protection](/docs/security/push-protection#imported-history)). | |
| 227 | +4. The agent reports its progress and then its result, which revokes the push token and wakes the Workflow with a `git-done` event. The Workflow also checks every 10 minutes, for up to 6 hours. A transfer that failed for any reason other than push protection is tried once more with a new container and a new push token. | |
| 228 | +5. **Paged steps.** Labels and milestones, issues and pull requests (with their original numbers), issue comments, reviews, review comments, branch protection, Actions variables with secret names and deployment environments, and releases are read from the GitHub REST API, a few pages per Workflow step. Issues, pull requests, comments, reviews, labels and milestones are written by GitHub number or id and marked with the import, so a retried step doesn't duplicate them. People are mapped through linked GitHub accounts; everyone else becomes a `github:<login>` mannequin. Nothing these steps write fires workflows, webhooks or notifications. | |
| 229 | +6. **Finish.** The Durable Object creates `refs/pull/<n>/head` for every imported pull request whose head commit arrived and removes the temporary refs. The Worker sets the default branch and archived state from GitHub, records the counts and push protection findings in the import's summary, releases the lock, revokes every push token, deletes the GitHub token, writes `import.finish` and sends the starter an in-app notification. | |
| 230 | +7. If a step still fails after its retries, the import is marked `failed` with the error: its tokens are revoked, the lock is released, its containers are stopped, an `import.fail` audit entry is written and the starter gets an in-app notification. The repository is kept with what was imported. | |
| 231 | + | |
| 232 | +An organization import runs a Workflow of its own: it copies the organization's Actions variables (only those the target organization doesn't have yet) and secret names when a person imports an organization into an organization, then starts its repository imports, each its own Workflow instance, up to its concurrency. A repository import that ends starts the next queued one and wakes the organization import when none is left. The organization import also checks every hour, for up to a week, restarting any that are stuck in the queue. | |
| 233 | + | |
| 181 | 234 | ## Packages in the repository |
| 182 | 235 | |
| 183 | 236 | | Path | Package | What it contains | |
| @@ -197,9 +250,10 @@ | ||
| 197 | 250 | | --- | --- | |
| 198 | 251 | | `accounts` | Users, organizations and agents: login, kind, email, status (`active`, `pending`, `disabled`), site admin flag, an agent's owner and policy, profile picture version and appearance setting (`light`, `dark`, or empty to follow the system). | |
| 199 | 252 | | `org_members` | Organization membership with role `owner`, `admin` or `member`. | |
| 200 | -| `repos` | Every repository: owner, name, description, visibility (`public` or `private`), topics, default branch, archived flag and timestamps. | | |
| 253 | +| `repos` | Every repository: owner, name, description, homepage, visibility (`public` or `private`), topics, default branch, archived flag and timestamps. | | |
| 254 | +| `repo_redirects`, `login_redirects`, `package_redirects` | Old owner and repository names left by transfers and renames, earlier logins of renamed people and organizations, and old names of container images that moved with their repository, so old URLs keep leading to them. A redirect ends when a repository (or account) takes the old name. | | |
| 201 | 255 | | `repo_access` | Direct repository access for a person or agent, with role `read`, `triage`, `write`, `maintain` or `admin`. | |
| 202 | -| `tokens` | Personal and agent tokens: SHA-256 hash, prefix, scopes, repository restriction, expiry, last use, revocation, expiry reminder. | | |
| 256 | +| `tokens` | Personal and agent tokens: SHA-256 hash, prefix, scopes, repository restriction, expiry, last use, revocation, expiry reminder, and the import a GitHub import's push token belongs to. | | |
| 203 | 257 | | `invites` | Invitations to the site, an organization or a repository, by token hash, with expiry and acceptance. | |
| 204 | 258 | | `access_requests` | Requests from people waiting for an invite. | |
| 205 | 259 | | `passkeys` | WebAuthn credentials: public key, counter, transports, device type, backup state, name. | |
| @@ -210,6 +264,9 @@ | ||
| 210 | 264 | | `audit_log` | The append-only audit log: actor, on-behalf-of, action, target, detail. | |
| 211 | 265 | | `schedules` | Cron expressions of default-branch workflows, read by the cron trigger. | |
| 212 | 266 | | `org_secrets` | Organization Actions secrets, encrypted. | |
| 267 | +| `org_variables` | Organization Actions variables. A copy is kept in each of the organization's repositories. | | |
| 268 | +| `imports` | GitHub imports: kind (one repository or an organization), status, attempt, source, target, options, per-step progress, the Git transfer's progress and result, summary, warnings, error, the encrypted GitHub token while the import runs and the current push token's id. | | |
| 269 | +| `account_links` | Verified links between Diffinery accounts and GitHub accounts, by API base and GitHub's numeric user id. | | |
| 213 | 270 | | `packages`, `package_manifests`, `package_tags`, `package_blobs`, `package_manifest_blobs`, `registry_blobs`, `registry_uploads` | The container registry index: packages, their manifests, tags and blobs, and uploads in progress. The bytes are in R2. | |
| 214 | 271 | | `notification_prefs`, `repo_watches`, `thread_subscriptions` | Notification settings, repository watching and per-thread subscriptions. | |
| 215 | 272 | | `known_devices` | Browsers and passkeys a person has signed in with, so only new ones trigger a security email. | |
| @@ -219,15 +276,18 @@ | ||
| 219 | 276 | |
| 220 | 277 | | Table | Holds | |
| 221 | 278 | | --- | --- | |
| 222 | -| `meta` | The repository's identity: ID, owner, name, default branch, owner kind. | | |
| 279 | +| `meta` | The repository's identity (ID, owner, name, default branch, owner kind), its workflow settings and, while a GitHub import fills it, that import's lock. | | |
| 223 | 280 | | `refs` | Branches, tags, `refs/pull/*` and merge queue refs. | |
| 224 | 281 | | `objects` | Git objects: type, size, deflated data, or a flag that the data is in R2. | |
| 225 | 282 | | `counters` | Number sequences: issue and pull request numbers, run numbers and merge queue order. | |
| 226 | 283 | | `issues` | Issues and pull requests (they share one number sequence), with title, body, state, author, assignees, labels and lock state. | |
| 227 | -| `pulls` | Pull request fields: head and base branches, head SHA, draft, merge state, merge commit, requested reviewers, intent. | | |
| 284 | +| `pulls` | Pull request fields: head and base branches, the head repository, head SHA, draft, merge state, merge commit, requested reviewers, intent, and the auto-merge setting while it is on (method, commit text, who turned it on and with which role). | | |
| 228 | 285 | | `comments` | Issue and pull request comments, and review line comments with path, line, side, commit and resolution. | |
| 229 | 286 | | `reviews` | Pull request reviews with state and commit. | |
| 230 | 287 | | `labels` | The repository's labels. | |
| 288 | +| `milestones`, `issue_milestones` | Milestones, and the milestone of each issue and pull request. | | |
| 289 | +| `import_refs`, `import_findings` | The refs a GitHub import wrote (so a retry only moves its own), and the push protection findings it bypassed. Imported issues, pull requests, comments, reviews, labels, milestones and protection rules carry the import's id in an `imported` column. | | |
| 290 | +| `org_variables` | A copy of the owning organization's Actions variables, for `vars.*`. | | |
| 231 | 291 | | `protections` | Branch protection rules by branch pattern, in the order they were created (the oldest matching pattern applies when no rule names the branch exactly). | |
| 232 | 292 | | `secrets`, `variables` | Repository Actions secrets (encrypted) and variables. | |
| 233 | 293 | | `webhooks` | Webhook URL, secret, events, and the last delivery status and time. | |
| @@ -238,9 +298,11 @@ | ||
| 238 | 298 | | `environments`, `environment_secrets`, `environment_variables` | Deployment environments with their branch policy, encrypted secrets and variables. | |
| 239 | 299 | | `reactions` | Reactions on issues, pull requests and comments. | |
| 240 | 300 | | `issue_events` | The event history of issues and pull requests (for example labeled, assigned, locked). | |
| 241 | -| `ref_actors` | Who last created or moved each tag, and whether that was a person or an agent. | | |
| 242 | -| `runs`, `jobs` | Workflow runs and their jobs: event, refs, SHAs, status, conclusion, matrix, outputs, steps, timeout, labels. | | |
| 301 | +| `ref_actors` | Who last created or moved each branch and tag, and whether that was a person or an agent. | | |
| 302 | +| `runs`, `jobs` | Workflow runs and their jobs: event, refs, SHAs, status, conclusion, whether the run's code came from an agent, matrix, outputs, steps, timeout, `continue-on-error`, concurrency group, labels. | | |
| 243 | 303 | | `logs` | Log lines of running jobs (moved to R2 when the job completes). | |
| 244 | -| `caches`, `artifacts` | Cache and artifact metadata; the archives are in R2. | | |
| 304 | +| `rt_caches`, `rt_artifacts` | Caches and artifacts from the cache and artifact services: key, version and branch of each cache, name, size, retention and expiry of each artifact, and uploads in progress. The contents are in R2. | | |
| 305 | +| `caches`, `artifacts` | Caches and artifacts saved by older runner agents; the archives are in R2. | | |
| 306 | +| `job_grants` | The role and effective permissions of each job's token. | | |
| 245 | 307 | | `merge_queue` | Queued pull requests and their merge groups. | |
| 246 | 308 | | `sandboxes` | Agent sandboxes, who created them and their status. | |
modifiedapps/diffinery/docs/self-hosting/configuration.md+58−11View file
| @@ -11,7 +11,7 @@ | ||
| 11 | 11 | | --- | --- | --- | |
| 12 | 12 | | `apps/diffinery/wrangler.jsonc` | Worker name, route, variables, bindings, container settings, cron, and the `dev` environment. | Yes. This is the source of truth. | |
| 13 | 13 | | `apps/diffinery/wrangler.deploy.jsonc` | `wrangler.jsonc` without the `dev` environment, plus the real D1 database ID, `ACCESS_TEAM_DOMAIN`, `ACCESS_AUD`, and the route, `PUBLIC_URL` and `MAIL_FROM` for your hostname. Written by `scripts/render-config.mjs` on every deploy and ignored by git. | No. It is regenerated on each deploy. | |
| 14 | -| Worker secrets | `MASTER_KEY` and `OIDC_PRIVATE_KEY`. Created by `scripts/ensure-secrets.sh`. | Only to restore them. Never change `MASTER_KEY`. | | |
| 14 | +| Worker secrets | `MASTER_KEY` and `OIDC_PRIVATE_KEY`, created by `scripts/ensure-secrets.sh`, and optionally `GITHUB_OAUTH_CLIENT_SECRET`, which you set yourself. | Only to restore them, or to add the optional one. Never change `MASTER_KEY`. | | |
| 15 | 15 | | Repository secrets and variables | Cloudflare credentials and options for the Deploy workflow, and a token for the live test and sync workflows. | Yes, in your forge's repository settings. | |
| 16 | 16 | | `apps/diffinery/.dev.vars` | Local secrets for `wrangler dev`, such as `MASTER_KEY`. Ignored by git. | Yes, for local development only. | |
| 17 | 17 | |
| @@ -26,7 +26,12 @@ | ||
| 26 | 26 | | `BOOTSTRAP_ADMINS` | A comma-separated list of emails | For the first sign-in | Emails (case-insensitive) that become site administrators. See [BOOTSTRAP_ADMINS](#bootstrap_admins). | |
| 27 | 27 | | `ACCESS_TEAM_DOMAIN` | Empty (filled in on deploy) | For email sign-in | Your Cloudflare Zero Trust team domain, like `example.cloudflareaccess.com`. Used to fetch the Access signing keys and to check the token issuer. | |
| 28 | 28 | | `ACCESS_AUD` | Empty (filled in on deploy) | For email sign-in | The audience tag of the Access application that guards `/auth/access`. | |
| 29 | +| `ARTIFACT_QUOTA_BYTES` | Not set | No | Workflow artifact storage per repository, in bytes: artifacts that haven't expired plus uploads in progress. Default `10737418240` (10 GB). Anything that isn't a positive whole number counts as not set. Add it under `vars` in `wrangler.jsonc`; the deploy keeps it. When a repository reaches it, uploads fail with `413 the repository's artifact storage quota (N bytes) is used up; delete artifacts or wait for them to expire`. See [Caches and artifacts](/docs/reference/limits#caches-and-artifacts). | | |
| 29 | 30 | | `DEV_AUTH` | Not set in production; `dev@diffinery.local` in the `dev` environment | Never in production | Local development only. See [DEV_AUTH](#dev_auth). | |
| 31 | +| `IMPORT_MAX_REPO_KB` | Not set | No | The largest GitHub repository a one-repository import accepts, in KB as GitHub reports a repository's `size`. Default `10485760` (10 GB). An empty value, `0` or text that isn't a number counts as not set. A larger source is refused before the import starts with `422 the repository is about N GB, over the M GB import limit; ask an administrator to raise IMPORT_MAX_REPO_KB or split the repository`. See [GitHub imports](#github-imports). | | |
| 32 | +| `IMPORT_GHE_HOST` | Not set | No | The host name of a GitHub Enterprise Server (like `ghe.example.com`) that imports may download release assets from. Without it, imports download release assets only from github.com and its asset hosts, and assets of an Enterprise import are skipped with a warning. | | |
| 33 | +| `GITHUB_OAUTH_CLIENT_ID` | Not set | No | The client ID of a GitHub OAuth app, for linking GitHub accounts with a GitHub sign-in instead of a gist. Needs `GITHUB_OAUTH_CLIENT_SECRET` too. See [GitHub imports](#github-imports). | | |
| 34 | +| `IMPORT_GIT_AGENT` | Not set in production; `external` in the `dev` environment | Never in production | With `external`, an import's Git transfer doesn't start a container: it waits for an import agent started by hand (see [The dev environment](#the-dev-environment)). | | |
| 30 | 35 | |
| 31 | 36 | When either `ACCESS_TEAM_DOMAIN` or `ACCESS_AUD` is empty, email sign-in at `/auth/access` is off: the page answers "Account recovery by email is not set up on this instance. Ask an administrator for a sign-in link.", and the sign-in page doesn't offer **Recover your account by email**. Passkeys, invitations and admin sign-in links keep working, but the first administrator can't sign in, because email sign-in is how an account in `BOOTSTRAP_ADMINS` is created. |
| 32 | 37 | |
| @@ -61,10 +66,11 @@ | ||
| 61 | 66 | |
| 62 | 67 | | Secret | Format | Created by | What it does | If it is missing | |
| 63 | 68 | | --- | --- | --- | --- | --- | |
| 64 | -| `MASTER_KEY` | 32 random bytes, base64 | `ensure-secrets.sh`, once | Root key for envelope encryption of Actions secrets: each repository, organization and deployment environment gets its own AES-GCM key derived from it with HKDF. It also signs the unsubscribe links in emails. | Repository, organization and environment secrets can't be stored or read (`could not store the secret: ... (is MASTER_KEY configured?)`). Notification emails that carry unsubscribe links can't be built, so they end up in the email log as failed; security and account emails still send. | | |
| 69 | +| `MASTER_KEY` | 32 random bytes, base64 | `ensure-secrets.sh`, once | Root key for envelope encryption of Actions secrets: each repository, organization and deployment environment gets its own AES-GCM key derived from it with HKDF. The GitHub token of a running import is encrypted the same way. It also signs the unsubscribe links in emails, the codes and OAuth state for linking GitHub accounts, and a key derived from it (HKDF) signs each job's `ACTIONS_RUNTIME_TOKEN` and the upload and download URLs of the Actions cache and artifact services. | Repository, organization and environment secrets can't be stored or read (`could not store the secret: ... (is MASTER_KEY configured?)`). Notification emails that carry unsubscribe links can't be built, so they end up in the email log as failed; security and account emails still send. Jobs get no `ACTIONS_RUNTIME_TOKEN`: `actions/cache` and the built-in cache step skip caching (`The cache service is not available to this job; skipping.`), and uploading or downloading artifacts fails (`The artifact service is not available to this job.`). Imports that come with a GitHub token can't be started. | | |
| 65 | 70 | | `OIDC_PRIVATE_KEY` | An ECDSA P-256 private key as a JWK (JSON), with a `kid` | `ensure-secrets.sh`, once | Signs the ID tokens that workflow jobs request with `permissions: id-token: write` (ES256). Its public half is served at `/.well-known/jwks`. | Jobs that request an ID token fail with `OIDC is not configured on this Diffinery instance`, `/.well-known/jwks` returns no keys, and `GET /api/v3/meta` returns `"oidc": false`. | |
| 71 | +| `GITHUB_OAUTH_CLIENT_SECRET` | The client secret of a GitHub OAuth app | You, optionally | With `GITHUB_OAUTH_CLIENT_ID`, turns on the **Link with GitHub** option on **Settings → Linked accounts**. | People link GitHub accounts with the gist code only. | | |
| 66 | 72 | |
| 67 | -Neither secret is rotated automatically. Changing `MASTER_KEY` makes every stored secret undecryptable and invalidates unsubscribe links already sent; keep a copy somewhere safe. To rotate the OIDC key, delete `OIDC_PRIVATE_KEY` and deploy: a new key with a new `kid` (`diffinery-<date>`) is generated, and relying parties pick it up from `/.well-known/jwks`. | |
| 73 | +Neither secret is rotated automatically. Changing `MASTER_KEY` makes every stored secret undecryptable and invalidates unsubscribe links already sent, along with the runtime tokens of running jobs and cache and artifact links already handed out; keep a copy somewhere safe. To rotate the OIDC key, delete `OIDC_PRIVATE_KEY` and deploy: a new key with a new `kid` (`diffinery-<date>`) is generated, and relying parties pick it up from `/.well-known/jwks`. | |
| 68 | 74 | |
| 69 | 75 | To set a secret by hand (for example to restore a backup): |
| 70 | 76 | |
| @@ -79,7 +85,7 @@ | ||
| 79 | 85 | |
| 80 | 86 | | Binding | Kind | Resource (production) | What Diffinery uses it for | |
| 81 | 87 | | --- | --- | --- | --- | |
| 82 | -| `DB` | D1 database | `diffinery` (migrations in `migrations/`) | Accounts (including each account's appearance setting and profile picture version), organizations, tokens, repository list, topics and access, invitations, sessions and passkeys, notifications, audit log, schedules, organization secrets, the registry index, email settings and log. | | |
| 88 | +| `DB` | D1 database | `diffinery` (migrations in `migrations/`) | Accounts (including each account's appearance setting and profile picture version), organizations, tokens, repository list, topics and access, invitations, sessions and passkeys, notifications, audit log, schedules, organization secrets and variables, the registry index, email settings and log, GitHub imports and linked GitHub accounts. | | |
| 83 | 89 | | `BUCKET` | R2 bucket | `diffinery-data` | Large git objects, LFS objects, archived job logs, caches, artifacts, container image layers and manifests, the event archive, release assets, cached source archives (zipball and tarball downloads) and profile pictures. | |
| 84 | 90 | | `EVENTS` | Queue producer, with a consumer on the same Worker | `diffinery-events`, dead-letter queue `diffinery-events-dlq` | Repository events for webhooks, notifications and the audit log. Consumer: batches of up to 50, waiting up to 2 seconds, 8 retries. | |
| 85 | 91 | | `MAIL` | Queue producer, with a consumer on the same Worker | `diffinery-mail`, dead-letter queue `diffinery-mail-dlq` | Email jobs. Consumer: batches of up to 20, waiting up to 2 seconds, 8 retries. Optional: without it emails are dropped with a warning in the logs. | |
| @@ -88,8 +94,10 @@ | ||
| 88 | 94 | | `RUNNER` | Durable Object namespace with a container | Class `RunnerContainer` (SQLite) | One container per hosted workflow job. | |
| 89 | 95 | | `SANDBOX` | Durable Object namespace with a container | Class `SandboxContainer` (SQLite) | One container per agent sandbox. | |
| 90 | 96 | | `RUN_WORKFLOW` | Workflow | `diffinery-run`, class `RunWorkflow` | One durable instance per workflow run. | |
| 97 | +| `IMPORTER` | Durable Object namespace with a container | Class `ImportContainer` (SQLite) | One container per Git transfer of a GitHub import. | | |
| 98 | +| `IMPORT_WORKFLOW` | Workflow | `diffinery-import`, class `ImportWorkflow` | One durable instance per attempt of a GitHub import. | | |
| 91 | 99 | |
| 92 | -The Durable Object classes are declared in one migration with tag `v1` (`new_sqlite_classes`). | |
| 100 | +The Durable Object classes are declared in two migrations (`new_sqlite_classes`): tag `v1` for `RepoDO`, `RunnerContainer` and `SandboxContainer`, and tag `v2` for `ImportContainer`. `wrangler deploy` applies a migration tag the account doesn't have yet. | |
| 93 | 101 | |
| 94 | 102 | ## Other Worker settings |
| 95 | 103 | |
| @@ -104,8 +112,8 @@ | ||
| 104 | 112 | | `observability` | Enabled, `head_sampling_rate: 1` | Every request is logged to Workers Logs. | |
| 105 | 113 | | `upload_source_maps` | `true` | Readable stack traces in the logs. | |
| 106 | 114 | | `limits.cpu_ms` | `300000` | Up to 5 minutes of CPU time per request (large pushes, diffs and merges). | |
| 107 | -| `containers` | `RunnerContainer`: `standard-2`, `max_instances: 100`; `SandboxContainer`: `standard-1`, `max_instances: 50`; both `rollout_active_grace_period: 3600` | Both build `containers/runner/Dockerfile` with `containers/runner` as context. Raise `max_instances` for more concurrent jobs or sandboxes. | | |
| 108 | -| `triggers.crons` | `["*/5 * * * *"]` | Scheduled workflows, token expiry reminders, email log pruning, registry cleanup. | | |
| 115 | +| `containers` | `RunnerContainer`: `standard-2`, `max_instances: 100`; `SandboxContainer`: `standard-1`, `max_instances: 50`; `ImportContainer`: `standard-4`, `max_instances: 20`; all `rollout_active_grace_period: 3600` | All three build `containers/runner/Dockerfile` with `containers/runner` as context. Raise `max_instances` for more concurrent jobs, sandboxes or import transfers. | | |
| 116 | +| `triggers.crons` | `["*/5 * * * *"]` | Scheduled workflows, token expiry reminders, email log pruning, registry cleanup, and cancelling draft imports older than 7 days. | | |
| 109 | 117 | |
| 110 | 118 | ## The dev environment |
| 111 | 119 | |
| @@ -122,11 +130,45 @@ | ||
| 122 | 130 | | Queues | `diffinery-events-dev` and `diffinery-mail-dev`, batches waiting up to 1 second, no dead-letter queues | |
| 123 | 131 | | `EMAIL` | Captured locally by Miniflare, not delivered | |
| 124 | 132 | | `RUN_WORKFLOW` | `diffinery-run-dev` | |
| 125 | -| `containers` | None: hosted runners and sandboxes are unavailable locally; use a self-hosted runner | | |
| 133 | +| `IMPORT_WORKFLOW` | `diffinery-import-dev` | | |
| 134 | +| `IMPORT_GIT_AGENT` | `external` | | |
| 135 | +| `containers` | None: hosted runners, sandboxes and import containers are unavailable locally; use a self-hosted runner, and run an import's Git transfer by hand | | |
| 126 | 136 | | `triggers.crons` | None | |
| 127 | 137 | |
| 138 | +To run an import locally, start it as usual, wait until its `git.phase` is `waiting`, claim the transfer, and run the runner agent's import mode against your instance (build it first with `pnpm -r build`): | |
| 139 | + | |
| 140 | +```sh | |
| 141 | +TOKEN=$(curl -sS -X POST http://localhost:8787/api/v3/diffinery/imports/<id>/claim | jq -r .token) | |
| 142 | +DIFFINERY_API_URL=http://localhost:8787 DIFFINERY_IMPORT_ID=<id> DIFFINERY_IMPORT_TOKEN=$TOKEN \ | |
| 143 | + DIFFINERY_WORK_ROOT=/tmp/diffinery-import node containers/runner/dist/agent.mjs | |
| 144 | +``` | |
| 145 | + | |
| 146 | +Only the account that started the import can claim it. In local development (`DEV_AUTH` set and `ACCESS_AUD` empty), imports also accept plain `http://` and loopback GitHub API URLs, for test doubles. | |
| 147 | + | |
| 128 | 148 | `MASTER_KEY` is not set in `dev`. Add it to `apps/diffinery/.dev.vars` (`MASTER_KEY=<base64 of 32 random bytes>`) or pass `--var MASTER_KEY:<value>` to `wrangler dev` to store Actions secrets locally. The end-to-end tests pass a random one with `--var`, together with `PUBLIC_URL`. |
| 129 | 149 | |
| 150 | +## GitHub imports | |
| 151 | + | |
| 152 | +[Imports from GitHub](/docs/migrate-from-github) need no setup beyond a normal deploy: the `IMPORTER` container, the `IMPORT_WORKFLOW` Workflow and the D1 tables of migration `0010_imports.sql` are created by the deploy. What you can tune: | |
| 153 | + | |
| 154 | +| Setting | Where | Effect | | |
| 155 | +| --- | --- | --- | | |
| 156 | +| `IMPORT_MAX_REPO_KB` | `vars` in `wrangler.jsonc` | The size ceiling for one-repository imports (default 10 GB). The Git transfer makes a full mirror clone (`git clone --mirror`) inside the import container (`standard-4`), so a higher ceiling needs room for that clone. | | |
| 157 | +| `max_instances` of `ImportContainer` | `containers` in `wrangler.jsonc` | How many Git transfers run at once across the instance (default 20). | | |
| 158 | +| `IMPORT_GHE_HOST` | `vars` in `wrangler.jsonc` | Lets imports download release assets from your GitHub Enterprise Server host. | | |
| 159 | +| `GITHUB_OAUTH_CLIENT_ID` and `GITHUB_OAUTH_CLIENT_SECRET` | `vars` (or a secret) and a Worker secret | Adds **Link with GitHub** to **Settings → Linked accounts**. | | |
| 160 | + | |
| 161 | +To offer linking through GitHub sign-in, create an OAuth app on github.com (**Settings → Developer settings → OAuth Apps**) with the authorization callback URL `https://git.example.com/settings/linked/github/callback` (your `PUBLIC_URL` plus `/settings/linked/github/callback`). Then add the client ID under `vars` in `wrangler.jsonc` and store the secret: | |
| 162 | + | |
| 163 | +```sh | |
| 164 | +cd apps/diffinery | |
| 165 | +printf '%s' "$CLIENT_SECRET" | pnpm exec wrangler secret put GITHUB_OAUTH_CLIENT_SECRET -c wrangler.deploy.jsonc | |
| 166 | +``` | |
| 167 | + | |
| 168 | +The OAuth flow asks GitHub for the `read:user` scope only, uses github.com (not an Enterprise host), and binds its state to the signed-in account. Without the two settings, people link accounts with the gist code, which needs no configuration. | |
| 169 | + | |
| 170 | +Imports reach GitHub only on HTTPS hosts with public names; a GitHub API URL on a private address, `localhost`, `.local` or `.internal` is refused with `422 github_api_url must be a public host`. | |
| 171 | + | |
| 130 | 172 | ## Deploy script settings |
| 131 | 173 | |
| 132 | 174 | Environment variables the scripts in `scripts/` read: |
| @@ -162,13 +204,18 @@ | ||
| 162 | 204 | | --- | --- | --- | --- | |
| 163 | 205 | | `CLOUDFLARE_API_TOKEN` | Secret | Deploy | Cloudflare API token with the permissions listed in [Deploy your own Diffinery](/docs/self-hosting/deploy). | |
| 164 | 206 | | `CLOUDFLARE_ACCOUNT_ID` | Secret | Deploy | Your Cloudflare account ID. | |
| 165 | -| `ACCESS_TEAM_DOMAIN` | Variable | Deploy | Optional. Your Zero Trust team domain, for example `example.cloudflareaccess.com`, when the API token can't read it. | | |
| 207 | +| `DIFFINERY_DOMAIN` | Variable | Deploy, Sync to Diffinery | Optional. Your instance's hostname, default `diffinery.com`. Deploy passes it to bootstrap (see [Deploy script settings](#deploy-script-settings)), uses it as the `production` environment's URL and runs its checks against `https://<DIFFINERY_DOMAIN>`. | | |
| 208 | +| `DIFFINERY_ZONE` | Variable | Deploy, Sync to Diffinery | Optional. The Cloudflare zone for the route, when it isn't the hostname's last two labels. | | |
| 209 | +| `MAIL_DOMAIN` | Variable | Deploy, Sync to Diffinery | Optional. The domain email is sent from, when it isn't `DIFFINERY_DOMAIN`. | | |
| 210 | +| `ACCESS_TEAM_DOMAIN` | Variable | Deploy, Sync to Diffinery | Optional. Your Zero Trust team domain, for example `example.cloudflareaccess.com`, when the API token can't read it. | | |
| 166 | 211 | | `DIFFINERY_LIVE_TOKEN` | Secret | Live test, Sync to Diffinery | A Diffinery token with the `admin` scope and access to all repositories, created under **Settings → Access tokens** on the instance. The live test creates and deletes throwaway repositories with it, and uploads a test profile picture to the token's account (putting the previous one back afterwards). When the secret is not set, those steps are skipped with a warning; the check that `/docs`, `/llms.txt` and the CLI download are served without signing in still runs. The sync workflow uses it to create the organization and repository, copy secrets and variables, and push. | |
| 167 | 212 | |
| 168 | -The Deploy workflow in the repository doesn't pass `DIFFINERY_DOMAIN`, `DIFFINERY_ZONE` or `MAIL_DOMAIN` to bootstrap. For your own instance, add them to the job's `env` in `.github/workflows/deploy.yml` (see [Deploy your own Diffinery](/docs/self-hosting/deploy#step-1-point-the-code-at-your-domain)). | |
| 213 | +Deploy reads `DIFFINERY_DOMAIN`, `DIFFINERY_ZONE` and `MAIL_DOMAIN` from the repository's variables and passes them to bootstrap, so you point it at your own hostname without editing the workflow (see [Deploy your own Diffinery](/docs/self-hosting/deploy#step-1-point-the-code-at-your-domain)). Deploy runs on pushes to `main` on any Diffinery instance, and when started by hand on GitHub. | |
| 169 | 214 | |
| 170 | 215 | The Deploy job and the Sync job run in an environment named `production`. The Live test and Sync workflows target `https://diffinery.com` and its `codician-team/diffinery` repository; adapt or delete them for your own instance. |
| 171 | 216 | |
| 217 | +Sync to Diffinery copies the four variables above to the repository on Diffinery (only those that are set), together with the secrets `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID` and `DIFFINERY_LIVE_TOKEN`, so the copy deploys to the same place. Each push copies only the branch or tag it pushed, and runs are queued per ref, so a push to another branch never replaces a waiting sync of `main`. A run started by hand copies every branch and tag; it queues together with `main`, and moves each ref only if it still points where it did when the run read it, so it never puts back an older commit that another run copied in the meantime. Branch deletions aren't copied. Only a sync of `main` checks that `main` on Diffinery matches and waits for its Actions runs. | |
| 218 | + | |
| 172 | 219 | `scripts/diffinery-wait-runs.sh`, which the sync workflow uses to wait for Actions runs on a commit, reads `DIFFINERY` (the instance origin) and `TOKEN` (a Diffinery token) from the environment: |
| 173 | 220 | |
| 174 | 221 | ```sh |
| @@ -177,4 +224,4 @@ | ||
| 177 | 224 | |
| 178 | 225 | ## Settings that are not configuration |
| 179 | 226 | |
| 180 | -Some values are fixed in code and can't be changed without editing it, for example the job timeout ceiling, matrix size, cache quota and session lifetime. They are listed in [Limits](/docs/reference/limits). | |
| 227 | +Some values are fixed in code and can't be changed without editing it, for example the job timeout ceiling, matrix size, the 10 GB cache budget per repository and session lifetime. They are listed in [Limits](/docs/reference/limits). |
modifiedapps/diffinery/docs/self-hosting/deploy.md+29−41View file
| @@ -11,7 +11,7 @@ | ||
| 11 | 11 | |
| 12 | 12 | | Requirement | Why | |
| 13 | 13 | | --- | --- | |
| 14 | -| A Cloudflare account on the Workers Paid plan | Diffinery uses Containers (hosted runners and sandboxes), Durable Objects with SQLite, Workflows, Queues, D1, R2, Cron Triggers and Email Sending. Containers need the paid plan, and the Worker asks for 300,000 ms of CPU time per request. | | |
| 14 | +| A Cloudflare account on the Workers Paid plan | Diffinery uses Containers (hosted runners, sandboxes and the Git transfers of GitHub imports), Durable Objects with SQLite, Workflows, Queues, D1, R2, Cron Triggers and Email Sending. Containers need the paid plan, and the Worker asks for 300,000 ms of CPU time per request. | | |
| 15 | 15 | | R2 enabled on the account | Large git objects, LFS objects, logs, caches, artifacts, container images, release assets, cached source archives and profile pictures are stored in R2. | |
| 16 | 16 | | A domain added to the account as a zone | Diffinery serves `https://<hostname>/*` through a Workers route on that zone. | |
| 17 | 17 | | A Cloudflare Zero Trust organization | Cloudflare Access guards `/auth/access`, the email-based sign-in that the first administrator uses and that lets people recover a lost passkey. Its one-time PIN login works without extra setup. | |
| @@ -26,15 +26,15 @@ | ||
| 26 | 26 | One workflow run does everything, and every step is safe to repeat: |
| 27 | 27 | |
| 28 | 28 | 1. Install dependencies, build the runner agent, the web UI's client bundles and the documentation (the pages under `apps/diffinery/docs`, the CLI reference and the CLI file served at `/-/cli/diffinery.mjs`), typecheck and run the unit tests. |
| 29 | -2. `scripts/bootstrap.sh` creates whatever is missing in your account (D1 database, R2 bucket, queues, the Cloudflare Access application for `/auth/access`), checks Email Sending, writes `apps/diffinery/wrangler.deploy.jsonc` with the real IDs and your hostname's route, `PUBLIC_URL` and email sender, and applies D1 migrations. | |
| 30 | -3. `wrangler deploy -c wrangler.deploy.jsonc` uploads the Worker, the Durable Object classes, the Workflow and the cron trigger, and builds and pushes the runner image for Containers. | |
| 29 | +2. `scripts/bootstrap.sh` creates whatever is missing in your account (D1 database, R2 bucket, queues, the Cloudflare Access application for `/auth/access`), sets the R2 bucket's lifecycle rules, checks Email Sending, writes `apps/diffinery/wrangler.deploy.jsonc` with the real IDs and your hostname's route, `PUBLIC_URL` and email sender, and applies D1 migrations. | |
| 30 | +3. `wrangler deploy -c wrangler.deploy.jsonc` uploads the Worker, the Durable Object classes, the two Workflows (workflow runs and GitHub imports) and the cron trigger, and builds and pushes the runner image for Containers. | |
| 31 | 31 | 4. `scripts/ensure-secrets.sh` creates the Worker's two secrets, `MASTER_KEY` and `OIDC_PRIVATE_KEY`, the first time only. |
| 32 | 32 | 5. On the very first deploy, bootstrap runs again to finish the Access setup, and the Worker is redeployed. |
| 33 | 33 | 6. Smoke tests check `/healthz`, the sign-in pages, static assets and D1 sessions. |
| 34 | 34 | |
| 35 | 35 | ## Step 1: Point the code at your domain |
| 36 | 36 | |
| 37 | -The repository is set up for `diffinery.com`. Your instance's hostname comes from the `DIFFINERY_DOMAIN` environment variable of the deploy: on every deploy, `scripts/bootstrap.sh` passes it to `scripts/render-config.mjs`, which writes the Worker's route, `PUBLIC_URL` and `MAIL_FROM` into the deployed config (see [scripts/render-config.mjs](#scriptsrender-configmjs)). You don't need to change those three values in `wrangler.jsonc`; the deploy replaces them. Before your first deploy, change these places in your copy. | |
| 37 | +The repository is set up for `diffinery.com`. Your instance's hostname comes from the `DIFFINERY_DOMAIN` repository variable (step 3): on every deploy, `scripts/bootstrap.sh` passes it to `scripts/render-config.mjs`, which writes the Worker's route, `PUBLIC_URL` and `MAIL_FROM` into the deployed config (see [scripts/render-config.mjs](#scriptsrender-configmjs)). You don't need to change those three values in `wrangler.jsonc`, or the Deploy workflow; the deploy replaces them. Before your first deploy, change this in your copy. | |
| 38 | 38 | |
| 39 | 39 | **`apps/diffinery/wrangler.jsonc`:** replace the `BOOTSTRAP_ADMINS` list with your own. |
| 40 | 40 | |
| @@ -55,37 +55,17 @@ | ||
| 55 | 55 | - Leave `ACCESS_TEAM_DOMAIN` and `ACCESS_AUD` empty; bootstrap fills them in. |
| 56 | 56 | - `routes`, `PUBLIC_URL` and `MAIL_FROM` can stay as they are. The deployed config gets a route for `<DIFFINERY_DOMAIN>/*`, `PUBLIC_URL` set to `https://<DIFFINERY_DOMAIN>` and `MAIL_FROM` set to `notifications@<MAIL_DOMAIN or DIFFINERY_DOMAIN>`. You can still edit them to match, so that the file reads correctly, but the deploy doesn't use their values. |
| 57 | 57 | |
| 58 | -**`.github/workflows/deploy.yml`:** | |
| 58 | +**`.github/workflows/deploy.yml`** needs no changes. It reads your hostname from repository variables you add in step 3: | |
| 59 | 59 | |
| 60 | -- Add `DIFFINERY_DOMAIN` to the job's `env`. `scripts/bootstrap.sh` uses it for the Worker's route, `PUBLIC_URL`, the email sender, the Access application and the Email Sending check. Without it, the scripts assume `diffinery.com`, and the deployed Worker gets the `diffinery.com/*` route and `PUBLIC_URL` `https://diffinery.com` whatever `wrangler.jsonc` says. | |
| 61 | -- Add `DIFFINERY_ZONE` if the Cloudflare zone that holds your hostname is not its last two labels. For `git.example.com` the default zone is `example.com`, which is right; for `git.example.co.uk` the default would be `co.uk`, so set `DIFFINERY_ZONE: example.co.uk`. | |
| 62 | -- Add `MAIL_DOMAIN` if email should come from another domain than your hostname, for example `MAIL_DOMAIN: example.com` to send from `notifications@example.com`. | |
| 63 | -- Replace `https://diffinery.com` in the smoke test steps and in `environment.url` with your origin. | |
| 64 | -- Change the job's `if:` condition. It deploys only when the workflow runs on `https://diffinery.com` or is started by hand. | |
| 60 | +- `DIFFINERY_DOMAIN`: your hostname, for example `git.example.com`. `scripts/bootstrap.sh` uses it for the Worker's route, `PUBLIC_URL`, the email sender, the Access application and the Email Sending check, and the workflow uses it as the `production` environment's URL and for its checks after the deploy. Without it, everything assumes `diffinery.com`, and the deployed Worker gets the `diffinery.com/*` route and `PUBLIC_URL` `https://diffinery.com` whatever `wrangler.jsonc` says. | |
| 61 | +- `DIFFINERY_ZONE`, if the Cloudflare zone that holds your hostname is not its last two labels. For `git.example.com` the default zone is `example.com`, which is right; for `git.example.co.uk` the default would be `co.uk`, so set `DIFFINERY_ZONE` to `example.co.uk`. | |
| 62 | +- `MAIL_DOMAIN`, if email should come from another domain than your hostname, for example `example.com` to send from `notifications@example.com`. | |
| 65 | 63 | |
| 66 | -```yaml | |
| 67 | -jobs: | |
| 68 | - deploy: | |
| 69 | - name: Deploy to Cloudflare | |
| 70 | - # Deploy on every push to main of your copy. | |
| 71 | - if: ${{ github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' }} | |
| 72 | - runs-on: ubuntu-latest | |
| 73 | - timeout-minutes: 45 | |
| 74 | - environment: | |
| 75 | - name: production | |
| 76 | - url: https://git.example.com | |
| 77 | - env: | |
| 78 | - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| 79 | - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| 80 | - ACCESS_TEAM_DOMAIN: ${{ vars.ACCESS_TEAM_DOMAIN }} | |
| 81 | - DIFFINERY_DOMAIN: git.example.com | |
| 82 | - # DIFFINERY_ZONE: example.com # only if the zone isn't the last two labels | |
| 83 | - # MAIL_DOMAIN: example.com # only to send email from another domain | |
| 84 | -``` | |
| 64 | +The Deploy job runs on every push to `main` when your copy lives on a Diffinery instance, and whenever you start it by hand. On GitHub it doesn't run on pushes; start it by hand there (`workflow_dispatch`). | |
| 85 | 65 | |
| 86 | 66 | `DIFFINERY_DOMAIN` is a bare hostname, without `https://` or a path. Diffinery is always served over HTTPS at the root of that hostname. |
| 87 | 67 | |
| 88 | -**Other workflows.** `.github/workflows/live-test.yml` and `.github/workflows/sync-to-diffinery.yml` test and mirror the public `diffinery.com` instance. Delete them, or change their `DIFFINERY` host and organization names before you enable them. `ci.yml` (typecheck, tests and the end-to-end test) works as is. `dind-probe.yml` is a maintenance probe started by hand; you can delete it. `d1-replication.yml` is optional; see [Architecture](/docs/self-hosting/architecture#d1-and-read-replication). | |
| 68 | +**Other workflows.** `.github/workflows/live-test.yml` and `.github/workflows/sync-to-diffinery.yml` test and mirror the public `diffinery.com` instance. Delete them, or change their `DIFFINERY` host and organization names before you enable them. The sync workflow runs only on GitHub: it copies each pushed branch or tag to the instance, copies the deploy secrets and the `DIFFINERY_DOMAIN`, `DIFFINERY_ZONE`, `MAIL_DOMAIN` and `ACCESS_TEAM_DOMAIN` variables, and for `main` waits for the Actions runs there, so the copy on Diffinery deploys itself (see [Configuration](/docs/self-hosting/configuration#repository-secrets-and-variables-for-the-workflows)). `ci.yml` (typecheck, tests and the end-to-end test) works as is. `dind-probe.yml` is a maintenance probe started by hand; you can delete it. `d1-replication.yml` is optional; see [Architecture](/docs/self-hosting/architecture#d1-and-read-replication). | |
| 89 | 69 | |
| 90 | 70 | > **Note:** The scripts use fixed resource names: a D1 database `diffinery`, an R2 bucket `diffinery-data`, queues `diffinery-events`, `diffinery-events-dlq`, `diffinery-mail` and `diffinery-mail-dlq`, and a Worker `diffinery`. One Cloudflare account holds one instance unless you rename them in `scripts/bootstrap.sh` and `wrangler.jsonc`. |
| 91 | 71 | |
| @@ -97,10 +77,10 @@ | ||
| 97 | 77 | | --- | --- | --- | --- | |
| 98 | 78 | | Account | Workers Scripts | Edit | Deploying the Worker, Durable Objects and secrets | |
| 99 | 79 | | Account | D1 | Edit | Creating the database and applying migrations | |
| 100 | -| Account | R2 (Workers R2 Storage) | Edit | Creating the bucket | | |
| 80 | +| Account | R2 (Workers R2 Storage) | Edit | Creating the bucket and setting its lifecycle rules | | |
| 101 | 81 | | Account | Queues | Edit | Creating the queues and consumers | |
| 102 | -| Account | Workflows | Edit | The `diffinery-run` Workflow | | |
| 103 | -| Account | Containers | Edit | Building and deploying the runner image | | |
| 82 | +| Account | Workflows | Edit | The `diffinery-run` and `diffinery-import` Workflows | | |
| 83 | +| Account | Containers | Edit | Building and deploying the runner image for runners, sandboxes and imports | | |
| 104 | 84 | | Account | Access: Apps and Policies | Edit | The Access application for `/auth/access` | |
| 105 | 85 | | Zone (the zone that holds your hostname, `DIFFINERY_ZONE`) | Workers Routes | Edit | The `git.example.com/*` route | |
| 106 | 86 | | Zone (your zone), optional | Zone Read, DNS Read, Email Sending Read | Read | Letting `scripts/email-sending.sh` check your Email Sending setup through the API. Without them it falls back to public DNS lookups. | |
| @@ -117,6 +97,9 @@ | ||
| 117 | 97 | | --- | --- | --- | |
| 118 | 98 | | `CLOUDFLARE_API_TOKEN` | Secret | The API token from step 2. | |
| 119 | 99 | | `CLOUDFLARE_ACCOUNT_ID` | Secret | Your Cloudflare account ID. | |
| 100 | +| `DIFFINERY_DOMAIN` | Variable | Your hostname, for example `git.example.com`. Without it, the deploy targets `diffinery.com`. | | |
| 101 | +| `DIFFINERY_ZONE` | Variable, optional | The Cloudflare zone that holds the hostname, when it isn't the hostname's last two labels. | | |
| 102 | +| `MAIL_DOMAIN` | Variable, optional | The domain email is sent from, when it isn't `DIFFINERY_DOMAIN`. | | |
| 120 | 103 | | `ACCESS_TEAM_DOMAIN` | Variable, optional | Your Zero Trust team domain, for example `example.cloudflareaccess.com`. Needed only when the token can't read it. | |
| 121 | 104 | |
| 122 | 105 | The Deploy job runs in an environment named `production`, so you can also store the secrets as environment secrets there and add required reviewers to that environment. |
| @@ -139,7 +122,7 @@ | ||
| 139 | 122 | Each section prints a collapsible log group: |
| 140 | 123 | |
| 141 | 124 | 1. **D1 database.** Looks for a database named `diffinery` and creates it if missing. |
| 142 | -2. **R2 bucket.** Creates `diffinery-data` if missing. | |
| 125 | +2. **R2 bucket.** Creates `diffinery-data` if missing, then sets its lifecycle rules, which delete short-lived data (such as expired artifacts, old job logs and unfinished uploads) by age and never touch git objects, LFS objects or release assets. The rules are set on every deploy and replace any lifecycle rules you added in the dashboard. If they can't be set, or a rule would still delete git objects, bootstrap stops the deploy. See [R2](/docs/self-hosting/architecture#r2) for the rules. | |
| 143 | 126 | 3. **Queues.** Creates `diffinery-events`, `diffinery-events-dlq`, `diffinery-mail` and `diffinery-mail-dlq` if missing. |
| 144 | 127 | 4. **Email Sending.** Runs `scripts/email-sending.sh` for `MAIL_DOMAIN` (default `DIFFINERY_DOMAIN`). A failure here is only a warning: the deploy continues and email is skipped until sending works. |
| 145 | 128 | 5. **Cloudflare Access.** Finds your team domain (from `ACCESS_TEAM_DOMAIN`, else from your Zero Trust organization through the API). Then it creates, or updates, a self-hosted Access application named **Diffinery account recovery** that covers only `<domain>/auth/access`, with a 15-minute Access session, an HTTP-only cookie and a policy that lets anyone who proves their email through. Diffinery, not Access, decides what that person may do. It also deletes an application named **Diffinery machine endpoints** left by older versions. If it still has no team domain, it requests `https://<domain>/auth/access` and reads the team domain from the Access redirect. |
| @@ -183,13 +166,13 @@ | ||
| 183 | 166 | |
| 184 | 167 | ### wrangler deploy |
| 185 | 168 | |
| 186 | -`pnpm exec wrangler deploy -c wrangler.deploy.jsonc` (run in `apps/diffinery`) deploys the Worker with all its bindings, and builds the runner image from `containers/runner/Dockerfile` (Ubuntu 24.04 with Node.js 22, git, git-lfs and the runner agent) for both container classes. | |
| 169 | +`pnpm exec wrangler deploy -c wrangler.deploy.jsonc` (run in `apps/diffinery`) deploys the Worker with all its bindings, and builds the runner image from `containers/runner/Dockerfile` (Ubuntu 24.04 with Node.js 22, git, git-lfs and the runner agent) for all three container classes: `RunnerContainer` (hosted jobs), `SandboxContainer` (agent sandboxes) and `ImportContainer` (the Git transfers of GitHub imports). It also applies Durable Object migrations the account doesn't have yet: tag `v1` creates `RepoDO`, `RunnerContainer` and `SandboxContainer`, and tag `v2` creates `ImportContainer`. | |
| 187 | 170 | |
| 188 | 171 | ### scripts/ensure-secrets.sh |
| 189 | 172 | |
| 190 | 173 | Creates the Worker's own secrets once and never rotates them: |
| 191 | 174 | |
| 192 | -- `MASTER_KEY`: 32 random bytes, base64 (`openssl rand -base64 32`). It is the root key for encrypting Actions secrets and signing unsubscribe links. | |
| 175 | +- `MASTER_KEY`: 32 random bytes, base64 (`openssl rand -base64 32`). It is the root key for encrypting Actions secrets, signing unsubscribe links, and signing the tokens and links that workflow jobs use for caches and artifacts. | |
| 193 | 176 | - `OIDC_PRIVATE_KEY`: a new ECDSA P-256 private key as a JWK, with a key ID like `diffinery-2026-10-13`. It signs workflow OIDC tokens. |
| 194 | 177 | |
| 195 | 178 | Values are generated on the runner and piped straight into `wrangler secret put`; they are never printed. The script lists existing secrets first and stops with `unexpected output from wrangler secret list` if it can't read them, rather than risk overwriting `MASTER_KEY`, which would make every stored Actions secret unreadable. |
| @@ -282,11 +265,11 @@ | ||
| 282 | 265 | To update, bring your copy up to date with upstream and run Deploy again (or push to `main` if your workflow deploys on push). Each run: |
| 283 | 266 | |
| 284 | 267 | - Creates nothing that already exists and keeps your D1 data, R2 data, secrets and Access application. |
| 285 | -- Applies new D1 migrations before the new Worker goes live. | |
| 268 | +- Applies new D1 migrations before the new Worker goes live. For example, `0010_imports.sql` adds the tables for GitHub imports, linked GitHub accounts and organization variables. | |
| 286 | 269 | - Rebuilds the documentation from the Markdown files in your copy, so `/docs` matches the code you deploy. |
| 287 | 270 | - Rolls out a new runner image. The first sandbox started on a machine after a deploy pulls the new image first, so it can take a few minutes to start; Diffinery waits up to 3 minutes for it. A hosted job that is running when its container is replaced gets a warning in its log ("This runner is being replaced by a new version. The job keeps running for up to 14 minutes before it is cancelled."). |
| 288 | 271 | |
| 289 | -Keep `MASTER_KEY` safe and never change it: it decrypts every stored Actions secret (repository, organization and environment secrets), and unsubscribe links in emails already sent are signed with it. If you delete `OIDC_PRIVATE_KEY`, the next deploy generates a new key with a new key ID; systems that trust your OIDC tokens fetch the new public key from `/.well-known/jwks`. | |
| 272 | +Keep `MASTER_KEY` safe and never change it: it decrypts every stored Actions secret (repository, organization and environment secrets), unsubscribe links in emails already sent are signed with it, and so are the cache and artifact tokens of jobs that are running. If you delete `OIDC_PRIVATE_KEY`, the next deploy generates a new key with a new key ID; systems that trust your OIDC tokens fetch the new public key from `/.well-known/jwks`. | |
| 290 | 273 | |
| 291 | 274 | ## Local development |
| 292 | 275 | |
| @@ -305,11 +288,11 @@ | ||
| 305 | 288 | | --- | --- | --- | |
| 306 | 289 | | `PUBLIC_URL` | `https://git.example.com` | `http://localhost:8787` | |
| 307 | 290 | | Sign-in | Passkeys, tokens, `/auth/access` | Any request without credentials acts as `dev@diffinery.local` (`DEV_AUTH`), who is a site administrator. Tokens still work. | |
| 308 | -| D1, R2, queues, Workflow | `diffinery`, `diffinery-data`, `diffinery-events`, `diffinery-mail`, `diffinery-run` | `diffinery-dev`, `diffinery-data-dev`, `diffinery-events-dev`, `diffinery-mail-dev`, `diffinery-run-dev`, all local | | |
| 309 | -| Containers | Hosted runners and sandboxes | None. `runs-on: ubuntu-latest` jobs and sandboxes can't start; use a self-hosted runner. | | |
| 291 | +| D1, R2, queues, Workflows | `diffinery`, `diffinery-data`, `diffinery-events`, `diffinery-mail`, `diffinery-run`, `diffinery-import` | `diffinery-dev`, `diffinery-data-dev`, `diffinery-events-dev`, `diffinery-mail-dev`, `diffinery-run-dev`, `diffinery-import-dev`, all local | | |
| 292 | +| Containers | Hosted runners, sandboxes and import transfers | None. `runs-on: ubuntu-latest` jobs and sandboxes can't start; use a self-hosted runner. A GitHub import waits for you to run its Git transfer by hand (`IMPORT_GIT_AGENT=external`; see [The dev environment](/docs/self-hosting/configuration#the-dev-environment)). | | |
| 310 | 293 | | Cron | Every 5 minutes | None | |
| 311 | 294 | | Email | Sent | Captured by Miniflare, not delivered. `GET /api/v3/diffinery/emails` shows each full message. | |
| 312 | -| `MASTER_KEY` | Set by `ensure-secrets.sh` | Not set: storing Actions secrets fails until you add one. | | |
| 295 | +| `MASTER_KEY` | Set by `ensure-secrets.sh` | Not set: storing Actions secrets fails, and jobs can't use caches or artifacts, until you add one. | | |
| 313 | 296 | |
| 314 | 297 | To store secrets locally, put a key in `apps/diffinery/.dev.vars` (ignored by git): |
| 315 | 298 | |
| @@ -343,6 +326,8 @@ | ||
| 343 | 326 | | `set CLOUDFLARE_API_TOKEN` or `set CLOUDFLARE_ACCOUNT_ID` | bootstrap, email check | Add the secrets (step 3), or export them for a manual deploy. | |
| 344 | 327 | | `could not create D1 database (does the token have D1: Edit?)` | bootstrap | Add D1 Edit to the token. | |
| 345 | 328 | | `could not create R2 bucket (R2 enabled? token has R2: Edit?)` | bootstrap | Enable R2 on the account (dashboard, R2) and add R2 Edit to the token. | |
| 329 | +| `could not set the R2 lifecycle rules (token has R2: Edit?): ...` | bootstrap | Add R2 Edit to the token. The text after the colon is Cloudflare's error. The deploy stops here, because the bucket's rules weren't confirmed. | | |
| 330 | +| `an R2 lifecycle rule still deletes objects under repos/ (git objects, LFS); refusing to continue` | bootstrap | The rules were set, but reading them back still shows a rule that would delete git or LFS objects. Check the lifecycle rules of the `diffinery-data` bucket in the Cloudflare dashboard, remove the rule that covers `repos/` or the whole bucket, and rerun the deploy. | | |
| 346 | 331 | | `could not create queue diffinery-events (token has Queues: Edit?)` | bootstrap | Add Queues Edit to the token. | |
| 347 | 332 | | `Could not read Access applications (token needs Access: Apps and Policies Edit). Passkeys work; email recovery stays off.` | bootstrap | Add Access: Apps and Policies Edit. Until then nobody can use `/auth/access`, so the first administrator can't sign in. | |
| 348 | 333 | | `Could not create the Access application; email recovery stays off.` | bootstrap | Check that the account has a Zero Trust organization and that the token has Access: Apps and Policies Edit. | |
| @@ -363,4 +348,7 @@ | ||
| 363 | 348 | | "Waiting for an invite" after the email check | Web UI | Your email isn't in `BOOTSTRAP_ADMINS` and nobody invited you. Fix the list in `wrangler.jsonc`, deploy, and sign in through `/auth/access` again. | |
| 364 | 349 | | `docs: N problem(s):` followed by a list | Build | A Markdown page under `apps/diffinery/docs` has missing or overlong frontmatter, uses an H1, or isn't listed in `docs/nav.json` (or the other way round). Fix the listed pages; each line names the file. | |
| 365 | 350 | | `could not store the secret: ... (is MASTER_KEY configured?)` | Repository settings | `MASTER_KEY` is missing. Run `scripts/ensure-secrets.sh`, or add it to `.dev.vars` locally. | |
| 351 | +| `The cache service is not available to this job; skipping.` or `The artifact service is not available to this job.` | Workflow job logs | `MASTER_KEY` is missing, so jobs get no cache and artifact token. Run `scripts/ensure-secrets.sh`, or add it to `.dev.vars` locally. | | |
| 366 | 352 | | `OIDC is not configured on this Diffinery instance` | Workflow jobs with `id-token: write` | `OIDC_PRIVATE_KEY` is missing. Run `scripts/ensure-secrets.sh`. | |
| 353 | +| `no import container is configured (IMPORTER binding)` | A GitHub import's error | The deployed config has no `IMPORTER` Durable Object binding or `ImportContainer` container. Restore them from `wrangler.jsonc` and deploy again. | | |
| 354 | +| `the Git transfer did not finish within 6 hours` | A GitHub import's error | Copying the repository's Git data took longer than the 6-hour limit. Lower `IMPORT_MAX_REPO_KB` so that repositories this large are refused before they start, and move them another way (for example a mirror push in several steps). See [Configuration](/docs/self-hosting/configuration#github-imports). | |
modifiedapps/diffinery/src/docs/api-meta.ts+314−50View file
| @@ -9,7 +9,7 @@ | ||
| 9 | 9 | */ |
| 10 | 10 | |
| 11 | 11 | /** Who may call an endpoint: a repository role, or an account-level requirement. */ |
| 12 | -export type Access = "public" | "account" | "read" | "triage" | "write" | "maintain" | "admin" | "org-admin" | "site-admin"; | |
| 12 | +export type Access = "public" | "account" | "read" | "triage" | "write" | "maintain" | "admin" | "org-admin" | "org-owner" | "site-admin"; | |
| 13 | 13 | |
| 14 | 14 | /** [type, description, required?] */ |
| 15 | 15 | export type Field = [string, string, boolean?]; |
| @@ -31,11 +31,11 @@ | ||
| 31 | 31 | * read token, anything else a write token). These are account-level changes, |
| 32 | 32 | * so a token limited to repositories is refused too, except where noted. |
| 33 | 33 | */ |
| 34 | -export const ACCOUNT_SCOPES: Record<string, { scope: Scope; anyToken?: boolean }> = { | |
| 34 | +export const ACCOUNT_SCOPES: Record<string, { scope: Scope; anyToken?: boolean; sessionOnly?: boolean }> = { | |
| 35 | 35 | "POST /user/orgs": { scope: "admin" }, |
| 36 | + "PATCH /orgs/{org}": { scope: "admin" }, | |
| 36 | 37 | "POST /orgs/{org}/invitations": { scope: "admin" }, |
| 37 | 38 | "POST /invitations": { scope: "admin" }, |
| 38 | - "PATCH /orgs/{org}": { scope: "admin" }, | |
| 39 | 39 | "DELETE /orgs/{org}": { scope: "admin" }, |
| 40 | 40 | "POST /repos/{owner}/{repo}/transfer": { scope: "admin" }, |
| 41 | 41 | "GET /orgs/{org}/invitations": { scope: "read" }, |
| @@ -47,10 +47,10 @@ | ||
| 47 | 47 | "GET /orgs/{org}/actions/secrets": { scope: "admin" }, |
| 48 | 48 | "PUT /orgs/{org}/actions/secrets/{name}": { scope: "admin" }, |
| 49 | 49 | "DELETE /orgs/{org}/actions/secrets/{name}": { scope: "admin" }, |
| 50 | - "POST /diffinery/tokens": { scope: "admin", anyToken: true }, | |
| 50 | + "POST /diffinery/tokens": { scope: "admin", sessionOnly: true }, | |
| 51 | 51 | "DELETE /diffinery/tokens/{id}": { scope: "admin" }, |
| 52 | - "POST /diffinery/agents": { scope: "admin" }, | |
| 53 | - "POST /diffinery/agents/{login}/tokens": { scope: "admin" }, | |
| 52 | + "POST /diffinery/agents": { scope: "admin", sessionOnly: true }, | |
| 53 | + "POST /diffinery/agents/{login}/tokens": { scope: "admin", sessionOnly: true }, | |
| 54 | 54 | "DELETE /diffinery/agents/{login}": { scope: "admin" }, |
| 55 | 55 | "PUT /notifications": { scope: "write" }, |
| 56 | 56 | "PUT /diffinery/notification-settings": { scope: "write" }, |
| @@ -60,6 +60,16 @@ | ||
| 60 | 60 | "DELETE /user/avatar": { scope: "write" }, |
| 61 | 61 | "PUT /orgs/{org}/avatar": { scope: "write" }, |
| 62 | 62 | "DELETE /orgs/{org}/avatar": { scope: "write" }, |
| 63 | + "POST /orgs/{org}/actions/variables": { scope: "admin" }, | |
| 64 | + "PATCH /orgs/{org}/actions/variables/{name}": { scope: "admin" }, | |
| 65 | + "DELETE /orgs/{org}/actions/variables/{name}": { scope: "admin" }, | |
| 66 | + "POST /diffinery/imports/{id}/cancel": { scope: "write" }, | |
| 67 | + "POST /diffinery/imports/{id}/retry": { scope: "write" }, | |
| 68 | + "POST /diffinery/imports/{id}/selection": { scope: "write" }, | |
| 69 | + "POST /diffinery/user/linked-accounts/github/challenge": { scope: "admin" }, | |
| 70 | + "POST /diffinery/user/linked-accounts/github": { scope: "admin" }, | |
| 71 | + "DELETE /diffinery/user/linked-accounts/{id}": { scope: "admin" }, | |
| 72 | + "DELETE /diffinery/admin/linked-accounts/{id}": { scope: "admin" }, | |
| 63 | 73 | }; |
| 64 | 74 | |
| 65 | 75 | const page: Record<string, Field> = { |
| @@ -90,26 +100,27 @@ | ||
| 90 | 100 | summary: "Create an organization", |
| 91 | 101 | access: "account", |
| 92 | 102 | status: 201, |
| 93 | - body: { login: ["string", "Organization login (letters, digits and dashes).", true], name: ["string", "Display name (default: the login)."] }, | |
| 103 | + body: { login: ["string", "Organization login (letters, digits, `-` and `_`; no dots).", true], name: ["string", "Display name (default: the login)."] }, | |
| 94 | 104 | }, |
| 95 | 105 | "GET /orgs/{org}": { |
| 96 | 106 | summary: "Get an organization", |
| 97 | - description: "Members, the organization's agents, agents of members, and site admins also see `default_repository_permission`: the base permission every member gets on its repositories.", | |
| 107 | + description: "GitHub's organization object. Members, the organization's agents, agents of members, and site admins also get `default_repository_permission`, the organization's base permission (`none`, `read`, `write` or `admin`).", | |
| 98 | 108 | access: "account", |
| 99 | 109 | }, |
| 100 | 110 | "PATCH /orgs/{org}": { |
| 101 | 111 | summary: "Update an organization", |
| 102 | - description: "Owners only (or site admins). Every field is checked before anything is saved, so a bad value changes nothing. Audited.", | |
| 103 | - access: "org-admin", | |
| 112 | + description: | |
| 113 | + "Organization owners (and site admins) only; admins and members get 403. Returns the updated organization. Every field is checked before anything is saved, so a bad value (422) changes nothing. Changing the base permission applies to every member right away; owners and admins always keep admin. Audited.", | |
| 114 | + access: "org-owner", | |
| 104 | 115 | body: { |
| 105 | - name: ["string", "Display name; blank goes back to the login."], | |
| 106 | - default_repository_permission: ["string", "Base permission members get on every repository: `none`, `read`, `write` or `admin`. Owners and admins always get admin, and per-repository grants add to it."], | |
| 116 | + default_repository_permission: ["string", "Base permission every member gets on the organization's repositories: `none`, `read` (the default for new organizations), `write` or `admin`."], | |
| 117 | + name: ["string", "Display name. An empty value clears it, and pages show the login instead."], | |
| 107 | 118 | }, |
| 108 | 119 | }, |
| 109 | 120 | "GET /orgs/{org}/members": { summary: "List organization members", description: "Members only (or site admins). Each entry carries the member's `role`.", access: "account" }, |
| 110 | 121 | "POST /orgs/{org}/invitations": { |
| 111 | 122 | summary: "Invite someone to an organization", |
| 112 | - description: "Emails an invite link. The response holds the link too, so you can share it yourself.", | |
| 123 | + description: "Emails an invite link. The response holds the link too, so you can share it yourself. Refused with 422 when the address already has a pending invitation to the organization (letter case doesn't matter) or belongs to a member.", | |
| 113 | 124 | access: "org-admin", |
| 114 | 125 | status: 201, |
| 115 | 126 | body: { email: ["string", "Email address to invite.", true], role: ["string", "`member` (default), `admin` or `owner`."] }, |
| @@ -131,10 +142,10 @@ | ||
| 131 | 142 | "PUT /orgs/{org}/avatar": { |
| 132 | 143 | summary: "Set an organization's picture", |
| 133 | 144 | description: |
| 134 | - "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.", | |
| 135 | - access: "org-admin", | |
| 145 | + "Diffinery only. Organization owners (and site admins) only. The raw request body is the image (JPEG, PNG or WebP, at most 5 MB), not JSON. Returns the new `avatar_url`. A token needs `write` or `admin` scope and must not be limited to repositories. Audited.", | |
| 146 | + access: "org-owner", | |
| 136 | 147 | }, |
| 137 | - "DELETE /orgs/{org}/avatar": { summary: "Remove an organization's picture", description: "Diffinery only. Goes back to generated initials and returns the `avatar_url`. Audited.", access: "org-admin" }, | |
| 148 | + "DELETE /orgs/{org}/avatar": { summary: "Remove an organization's picture", description: "Diffinery only. Organization owners (and site admins) only. Goes back to generated initials and returns the `avatar_url`. Audited.", access: "org-owner" }, | |
| 138 | 149 | "POST /invitations": { |
| 139 | 150 | summary: "Invite someone to this instance", |
| 140 | 151 | description: "A site-wide invite with no organization. Site admins only.", |
| @@ -202,7 +213,7 @@ | ||
| 202 | 213 | "PATCH /repos/{owner}/{repo}": { |
| 203 | 214 | summary: "Update a repository", |
| 204 | 215 | access: "admin", |
| 205 | - description: "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.", | |
| 216 | + description: "Works on an archived repository too, so an admin can unarchive it with `archived: false`. Audited as `repo.update`, `repo.archive` or `repo.unarchive`. A new `name` renames the repository: its old name keeps redirecting (see the transfer endpoint), until a repository is created there again. 422 when the owner already has a repository with that name.", | |
| 206 | 217 | body: { |
| 207 | 218 | name: ["string", "New name."], |
| 208 | 219 | description: ["string", "New description."], |
| @@ -211,7 +222,7 @@ | ||
| 211 | 222 | archived: ["boolean", "Archive (read-only) or unarchive."], |
| 212 | 223 | }, |
| 213 | 224 | }, |
| 214 | - "DELETE /repos/{owner}/{repo}": { summary: "Delete a repository", description: "People only; agents can't delete repositories. Audited.", access: "admin", status: 204 }, | |
| 225 | + "DELETE /repos/{owner}/{repo}": { summary: "Delete a repository", description: "People only; agents can't delete repositories. Works on an archived repository too. Audited.", access: "admin", status: 204 }, | |
| 215 | 226 | "POST /repos/{owner}/{repo}/transfer": { |
| 216 | 227 | summary: "Transfer a repository", |
| 217 | 228 | description: |
| @@ -223,11 +234,12 @@ | ||
| 223 | 234 | "GET /repos/{owner}/{repo}/collaborators": { summary: "List collaborators", description: "Each entry carries `role_name`.", access: "read" }, |
| 224 | 235 | "PUT /repos/{owner}/{repo}/collaborators/{login}": { |
| 225 | 236 | summary: "Add or change a collaborator", |
| 237 | + description: "Audited as `repo.collaborator.set`.", | |
| 226 | 238 | access: "admin", |
| 227 | 239 | status: 204, |
| 228 | 240 | body: { permission: ["string", "`read`, `triage`, `write` (default), `maintain` or `admin`; GitHub's `pull` and `push` work too."] }, |
| 229 | 241 | }, |
| 230 | - "DELETE /repos/{owner}/{repo}/collaborators/{login}": { summary: "Remove a collaborator", access: "admin", status: 204 }, | |
| 242 | + "DELETE /repos/{owner}/{repo}/collaborators/{login}": { summary: "Remove a collaborator", description: "Audited as `repo.collaborator.remove`.", access: "admin", status: 204 }, | |
| 231 | 243 | "GET /repos/{owner}/{repo}/invitations": { summary: "List pending repository invitations", description: "Invitations sent by email to people without an account. Each has an `id` to revoke it with.", access: "admin" }, |
| 232 | 244 | "DELETE /repos/{owner}/{repo}/invitations/{id}": { |
| 233 | 245 | summary: "Revoke a repository invitation", |
| @@ -332,7 +344,8 @@ | ||
| 332 | 344 | }, |
| 333 | 345 | "POST /repos/{owner}/{repo}/diffinery/commits": { |
| 334 | 346 | summary: "Commit several files at once", |
| 335 | - description: "Diffinery only. Writes and deletes any number of files in one commit, without a git client. Pass `expected_head_sha` to fail instead of racing another writer.", | |
| 347 | + description: | |
| 348 | + "Diffinery only. Writes and deletes any number of files in one commit, without a git client. Pass `expected_head_sha` to fail instead of racing another writer. Push protection scans the new content as it does for `git push`: content that looks like a credential is refused with 422 and audited as `api.blocked_secret`. A repository admin who is a person can commit it anyway with an `allow_secrets` reason, audited as `api.secret_bypassed` and emailed to the repository's admins.", | |
| 336 | 349 | access: "write", |
| 337 | 350 | status: 201, |
| 338 | 351 | body: { |
| @@ -341,6 +354,7 @@ | ||
| 341 | 354 | files: ["array", "Changes: `{ path, content, encoding: \"utf-8\" | \"base64\", mode: \"100644\" | \"100755\" }` or `{ path, delete: true }`.", true], |
| 342 | 355 | base: ["string", "Create `branch` from this ref when it doesn't exist."], |
| 343 | 356 | expected_head_sha: ["string", "Fail with 409 if the branch has moved."], |
| 357 | + allow_secrets: ["string", "Why a detected secret may be committed anyway (repository admins who are people; at most 200 characters are kept)."], | |
| 344 | 358 | }, |
| 345 | 359 | }, |
| 346 | 360 | "GET /repos/{owner}/{repo}/readme": { |
| @@ -506,7 +520,7 @@ | ||
| 506 | 520 | "`branch` is a branch name or pattern. People only: agents and workflow tokens can't change protection. `allowAgentAutoMerge` (boolean) lets agents land pull requests through auto-merge or the merge queue; turning it on or off is audited as `repo.protection.agent_auto_merge`. `agentAutoMergeWithoutApproval` (boolean, needs `allowAgentAutoMerge`) lets those landings go through on green required checks without a person's approval, audited as `repo.protection.agent_approval_waiver`. See the branch protection guide for every field.", |
| 507 | 521 | access: "admin", |
| 508 | 522 | }, |
| 509 | - "DELETE /repos/{owner}/{repo}/branches/{branch}/protection": { summary: "Delete a protection rule", access: "admin", status: 204 }, | |
| 523 | + "DELETE /repos/{owner}/{repo}/branches/{branch}/protection": { summary: "Delete a protection rule", description: "Audited as `repo.protection.delete`.", access: "admin", status: 204 }, | |
| 510 | 524 | |
| 511 | 525 | // ------------------------------------------------------------ issues |
| 512 | 526 | "GET /repos/{owner}/{repo}/issues": { |
| @@ -517,6 +531,7 @@ | ||
| 517 | 531 | }, |
| 518 | 532 | "POST /repos/{owner}/{repo}/issues": { |
| 519 | 533 | summary: "Create an issue", |
| 534 | + description: "Needs a signed-in caller, also on a public repository. As on GitHub, `labels` and `assignees` from a caller without triage access are dropped without an error.", | |
| 520 | 535 | access: "read", |
| 521 | 536 | status: 201, |
| 522 | 537 | body: { title: ["string", "Title.", true], body: ["string", "Markdown body."], labels: ["string[]", "Label names."], assignees: ["string[]", "Logins."] }, |
| @@ -524,6 +539,7 @@ | ||
| 524 | 539 | "GET /repos/{owner}/{repo}/issues/{number}": { summary: "Get an issue", access: "read" }, |
| 525 | 540 | "PATCH /repos/{owner}/{repo}/issues/{number}": { |
| 526 | 541 | summary: "Update an issue", |
| 542 | + description: "The author can edit their own issue or pull request. As on GitHub, `labels` and `assignees` from a caller without triage access are dropped without an error.", | |
| 527 | 543 | access: "read", |
| 528 | 544 | body: { |
| 529 | 545 | title: ["string", "New title."], |
| @@ -532,8 +548,38 @@ | ||
| 532 | 548 | state_reason: ["string", "Why it was closed, like `completed` or `not_planned`."], |
| 533 | 549 | labels: ["string[]", "Replace the labels."], |
| 534 | 550 | assignees: ["string[]", "Replace the assignees."], |
| 551 | + milestone: ["integer", "Milestone number, or `null` to remove the milestone. Needs triage access; applied only when the rest of the update succeeds. 422 when it isn't a whole number or `null`, 404 `milestone not found` for an unknown number."], | |
| 535 | 552 | }, |
| 536 | 553 | }, |
| 554 | + "GET /repos/{owner}/{repo}/milestones": { | |
| 555 | + summary: "List milestones", | |
| 556 | + description: "Every milestone in the state you ask for (no paging), ordered by due date (milestones without one last), then by number. The `id` is the milestone number. Milestones come from GitHub imports or are created with the endpoint below; the web UI sets an issue's milestone but doesn't create milestones.", | |
| 557 | + access: "read", | |
| 558 | + query: { state: ["string", "`open` (default), `closed` or `all`. Anything else is 422."] }, | |
| 559 | + }, | |
| 560 | + "GET /repos/{owner}/{repo}/milestones/{number}": { summary: "Get a milestone", description: "`number` is the milestone number. 404 `milestone not found`.", access: "read" }, | |
| 561 | + "POST /repos/{owner}/{repo}/milestones": { | |
| 562 | + summary: "Create a milestone", | |
| 563 | + description: "New milestones are open; close one with `PATCH`. There is no endpoint to delete a milestone.", | |
| 564 | + access: "triage", | |
| 565 | + status: 201, | |
| 566 | + body: { | |
| 567 | + title: ["string", "1 to 255 characters.", true], | |
| 568 | + description: ["string", "At most 65,536 characters."], | |
| 569 | + due_on: ["string", "ISO 8601 timestamp, or `null`."], | |
| 570 | + }, | |
| 571 | + }, | |
| 572 | + "PATCH /repos/{owner}/{repo}/milestones/{number}": { | |
| 573 | + summary: "Update a milestone", | |
| 574 | + description: "Send only the fields you change. Closing records `closed_at`; reopening clears it.", | |
| 575 | + access: "triage", | |
| 576 | + body: { | |
| 577 | + title: ["string", "1 to 255 characters."], | |
| 578 | + description: ["string", "At most 65,536 characters."], | |
| 579 | + state: ["string", "`open` or `closed`."], | |
| 580 | + due_on: ["string", "ISO 8601 timestamp, or `null` to clear it."], | |
| 581 | + }, | |
| 582 | + }, | |
| 537 | 583 | "GET /repos/{owner}/{repo}/issues/{number}/comments": { |
| 538 | 584 | summary: "List comments", |
| 539 | 585 | description: "Conversation comments on an issue or pull request (not review comments).", |
| @@ -627,6 +673,7 @@ | ||
| 627 | 673 | }, |
| 628 | 674 | "POST /repos/{owner}/{repo}/pulls": { |
| 629 | 675 | summary: "Open a pull request", |
| 676 | + 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.", | |
| 630 | 677 | access: "write", |
| 631 | 678 | status: 201, |
| 632 | 679 | body: { |
| @@ -647,7 +694,7 @@ | ||
| 647 | 694 | "PATCH /repos/{owner}/{repo}/pulls/{number}": { |
| 648 | 695 | summary: "Update a pull request", |
| 649 | 696 | description: |
| 650 | - "The author, or anyone with triage access. `base` isn't supported and is ignored. A merged pull request can't be reopened. A title or body change sends a `pull_request` `edited` event. `draft` is a Diffinery extension (GitHub only offers it through GraphQL): the author or anyone with write access can set it on an open pull request, which sends `ready_for_review` or `converted_to_draft`.", | |
| 697 | + "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`.", | |
| 651 | 698 | access: "read", |
| 652 | 699 | body: { |
| 653 | 700 | title: ["string", "New title."], |
| @@ -670,7 +717,7 @@ | ||
| 670 | 717 | "PUT /repos/{owner}/{repo}/pulls/{number}/merge": { |
| 671 | 718 | summary: "Merge a pull request", |
| 672 | 719 | description: |
| 673 | - "Fails with 405 and the blockers when protection rules aren't met, and with 405 when a merge commit is asked for on a branch that requires linear history. A branch that requires a merge queue answers 405 too: add the pull request to the queue instead. A repository admin (a person, not an agent) can bypass unmet rules with `bypass_rules: true`, or the queue with `bypass_merge_queue: true`, when the rule doesn't include administrators, there are no conflicts and the pull request isn't a draft. A bypass that skipped an unmet rule is audited as `pull_request.merge_bypassed`.", | |
| 720 | + "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.", | |
| 674 | 721 | access: "write", |
| 675 | 722 | body: { |
| 676 | 723 | merge_method: ["string", "`merge`, `squash` or `rebase`."], |
| @@ -678,8 +725,8 @@ | ||
| 678 | 725 | commit_message: ["string", "Body of the merge or squash commit."], |
| 679 | 726 | sha: ["string", "Fail if the head has moved from this commit."], |
| 680 | 727 | delete_branch: ["boolean", "Delete the head branch after merging."], |
| 681 | - bypass_merge_queue: ["boolean", "Merge directly even when a queue is required (admins)."], | |
| 682 | - bypass_rules: ["boolean", "Merge even though protection rules aren't met (admins). Without it, an admin's merge is blocked like anyone else's."], | |
| 728 | + bypass_merge_queue: ["boolean", "Merge directly even when a queue is required (admins). Doesn't skip unmet rules; send `bypass_rules` too for that."], | |
| 729 | + 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."], | |
| 683 | 730 | }, |
| 684 | 731 | }, |
| 685 | 732 | "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" }, |
| @@ -694,7 +741,7 @@ | ||
| 694 | 741 | "PUT /repos/{owner}/{repo}/pulls/{number}/auto-merge": { |
| 695 | 742 | summary: "Enable auto-merge", |
| 696 | 743 | description: |
| 697 | - "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 turns it off. 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`.", | |
| 744 | + "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`.", | |
| 698 | 745 | access: "write", |
| 699 | 746 | body: { |
| 700 | 747 | merge_method: ["string", "`merge`, `squash` or `rebase` (default: the queue's method, `squash` where linear history is required, else `merge`)."], |
| @@ -704,14 +751,15 @@ | ||
| 704 | 751 | }, |
| 705 | 752 | "DELETE /repos/{owner}/{repo}/pulls/{number}/auto-merge": { |
| 706 | 753 | summary: "Disable auto-merge", |
| 707 | - description: "GitHub's `disablePullRequestAutoMerge`. Who turned it on, or anyone with write access, may turn it off. 204 even when it was off. Audited as `pull_request.auto_merge_disabled`.", | |
| 754 | + 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`.", | |
| 708 | 755 | access: "write", |
| 709 | 756 | status: 204, |
| 710 | 757 | }, |
| 711 | 758 | "GET /repos/{owner}/{repo}/pulls/{number}/reviews": { summary: "List reviews", access: "read" }, |
| 712 | 759 | "POST /repos/{owner}/{repo}/pulls/{number}/reviews": { |
| 713 | 760 | summary: "Submit a review", |
| 714 | - description: "Approvals from agents don't count toward required approvals.", | |
| 761 | + description: | |
| 762 | + "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.", | |
| 715 | 763 | access: "read", |
| 716 | 764 | status: 201, |
| 717 | 765 | body: { |
| @@ -810,7 +858,7 @@ | ||
| 810 | 858 | |
| 811 | 859 | // ------------------------------------------------------------ checks |
| 812 | 860 | "GET /repos/{owner}/{repo}/commits/{ref}/check-runs": { summary: "List check runs for a commit", access: "read" }, |
| 813 | - "GET /repos/{owner}/{repo}/check-runs/{id}": { summary: "Get a check run", description: "`id` is the numeric id or Diffinery's own.", access: "read" }, | |
| 861 | + "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" }, | |
| 814 | 862 | "PATCH /repos/{owner}/{repo}/check-runs/{id}": { |
| 815 | 863 | summary: "Update a check run", |
| 816 | 864 | description: |
| @@ -867,7 +915,7 @@ | ||
| 867 | 915 | "POST /repos/{owner}/{repo}/deployments": { |
| 868 | 916 | summary: "Create a deployment", |
| 869 | 917 | description: |
| 870 | - "Unless `required_contexts` is `[]`, every check and status on the commit (or each context you name) must have passed, else 409; checks and statuses reported by agents don't count toward the default set. Creates the environment if needed and sends the `deployment` webhook. `auto_merge` is ignored. See [Environments](/docs/actions/environments).", | |
| 918 | + "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).", | |
| 871 | 919 | access: "write", |
| 872 | 920 | status: 201, |
| 873 | 921 | body: { |
| @@ -964,19 +1012,19 @@ | ||
| 964 | 1012 | "GET /repos/{owner}/{repo}/environments/{env}/variables/{name}": { summary: "Get an environment variable", access: "write" }, |
| 965 | 1013 | "POST /repos/{owner}/{repo}/environments/{env}/variables": { |
| 966 | 1014 | summary: "Create an environment variable", |
| 967 | - description: "409 when it exists. Names are upper-cased, and `GITHUB_` names are refused. People only.", | |
| 1015 | + description: "409 when it exists. Names are upper-cased, and `GITHUB_` names are refused. People only. Audited as `environment.variable.set`.", | |
| 968 | 1016 | access: "admin", |
| 969 | 1017 | status: 201, |
| 970 | 1018 | body: { name: ["string", "Variable name.", true], value: ["string", "Variable value.", true] }, |
| 971 | 1019 | }, |
| 972 | 1020 | "PATCH /repos/{owner}/{repo}/environments/{env}/variables/{name}": { |
| 973 | 1021 | summary: "Update an environment variable", |
| 974 | - description: "People only.", | |
| 1022 | + description: "People only. Renaming onto a name that exists fails with 409. Audited as `environment.variable.set`.", | |
| 975 | 1023 | access: "admin", |
| 976 | 1024 | status: 204, |
| 977 | 1025 | body: { name: ["string", "New name (renames the variable)."], value: ["string", "New value."] }, |
| 978 | 1026 | }, |
| 979 | - "DELETE /repos/{owner}/{repo}/environments/{env}/variables/{name}": { summary: "Delete an environment variable", description: "People only.", access: "admin", status: 204 }, | |
| 1027 | + "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 }, | |
| 980 | 1028 | |
| 981 | 1029 | // ------------------------------------------------------------ actions |
| 982 | 1030 | "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" }, |
| @@ -1023,7 +1071,11 @@ | ||
| 1023 | 1071 | page: ["integer", "Page number, starting at 1."], |
| 1024 | 1072 | }, |
| 1025 | 1073 | }, |
| 1026 | - "GET /repos/{owner}/{repo}/actions/runs/{id}": { summary: "Get a workflow run", description: "The run with its jobs.", access: "read" }, | |
| 1074 | + "GET /repos/{owner}/{repo}/actions/runs/{id}": { | |
| 1075 | + summary: "Get a workflow run", | |
| 1076 | + 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.", | |
| 1077 | + access: "read", | |
| 1078 | + }, | |
| 1027 | 1079 | "GET /repos/{owner}/{repo}/actions/runs/{id}/attempts/{attempt}": { |
| 1028 | 1080 | summary: "Get a run attempt", |
| 1029 | 1081 | description: "Re-runs are stored as separate runs with the same run number; this returns attempt `attempt` of the run's number.", |
| @@ -1036,8 +1088,8 @@ | ||
| 1036 | 1088 | }, |
| 1037 | 1089 | "GET /repos/{owner}/{repo}/actions/runs/{id}/jobs": { summary: "List a run's jobs", access: "read" }, |
| 1038 | 1090 | "POST /repos/{owner}/{repo}/actions/runs/{id}/cancel": { summary: "Cancel a run", access: "write", status: 202 }, |
| 1039 | - "POST /repos/{owner}/{repo}/actions/runs/{id}/rerun": { summary: "Re-run a workflow run", access: "write", status: 201 }, | |
| 1040 | - "POST /repos/{owner}/{repo}/actions/runs/{id}/rerun-failed-jobs": { summary: "Re-run a run's failed jobs", access: "write", status: 201 }, | |
| 1091 | + "POST /repos/{owner}/{repo}/actions/runs/{id}/rerun": { summary: "Re-run a workflow run", description: "A workflow's `GITHUB_TOKEN` can't re-run workflows (403).", access: "write", status: 201 }, | |
| 1092 | + "POST /repos/{owner}/{repo}/actions/runs/{id}/rerun-failed-jobs": { summary: "Re-run a run's failed jobs", description: "A workflow's `GITHUB_TOKEN` can't re-run workflows (403).", access: "write", status: 201 }, | |
| 1041 | 1093 | "GET /repos/{owner}/{repo}/actions/jobs/{id}": { summary: "Get a job", description: "The job with its steps.", access: "read" }, |
| 1042 | 1094 | "GET /repos/{owner}/{repo}/actions/jobs/{id}/logs": { |
| 1043 | 1095 | summary: "Get a job's logs", |
| @@ -1047,23 +1099,98 @@ | ||
| 1047 | 1099 | }, |
| 1048 | 1100 | "GET /repos/{owner}/{repo}/actions/runs/{runId}/artifacts": { |
| 1049 | 1101 | summary: "List a run's artifacts", |
| 1050 | - description: "Each with an `archive_download_url` (a zip). Expired artifacts are listed with `expired: true`; their content is gone.", | |
| 1102 | + description: | |
| 1103 | + "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.", | |
| 1051 | 1104 | access: "read", |
| 1052 | - query: { name: ["string", "Only artifacts with this name."] }, | |
| 1105 | + query: { name: ["string", "Only artifacts with exactly this name."], ...page }, | |
| 1053 | 1106 | }, |
| 1107 | + "GET /repos/{owner}/{repo}/actions/artifacts": { | |
| 1108 | + summary: "List artifacts", | |
| 1109 | + description: | |
| 1110 | + "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 }`.", | |
| 1111 | + access: "read", | |
| 1112 | + query: { name: ["string", "Only artifacts with exactly this name."], ...page }, | |
| 1113 | + }, | |
| 1114 | + "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" }, | |
| 1115 | + "GET /repos/{owner}/{repo}/actions/artifacts/{id}/{format}": { | |
| 1116 | + summary: "Download an artifact", | |
| 1117 | + description: | |
| 1118 | + "`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.", | |
| 1119 | + access: "read", | |
| 1120 | + status: 302, | |
| 1121 | + }, | |
| 1122 | + "DELETE /repos/{owner}/{repo}/actions/artifacts/{id}": { | |
| 1123 | + summary: "Delete an artifact", | |
| 1124 | + 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`.", | |
| 1125 | + access: "write", | |
| 1126 | + status: 204, | |
| 1127 | + }, | |
| 1128 | + "GET /repos/{owner}/{repo}/actions/caches": { | |
| 1129 | + summary: "List caches", | |
| 1130 | + description: | |
| 1131 | + "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 }`.", | |
| 1132 | + access: "read", | |
| 1133 | + query: { | |
| 1134 | + key: ["string", "Only entries whose key starts with this."], | |
| 1135 | + ref: ["string", "Only entries saved for this full ref, like `refs/heads/main`."], | |
| 1136 | + sort: ["string", "`last_accessed_at` (default), `created_at` or `size_in_bytes`."], | |
| 1137 | + direction: ["string", "`desc` (default) or `asc`."], | |
| 1138 | + ...page, | |
| 1139 | + }, | |
| 1140 | + }, | |
| 1141 | + "DELETE /repos/{owner}/{repo}/actions/caches": { | |
| 1142 | + summary: "Delete caches by key", | |
| 1143 | + description: | |
| 1144 | + "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`.", | |
| 1145 | + access: "write", | |
| 1146 | + query: { key: ["string", "The exact cache key.", true], ref: ["string", "Only entries saved for this full ref, like `refs/heads/main`."] }, | |
| 1147 | + }, | |
| 1148 | + "DELETE /repos/{owner}/{repo}/actions/caches/{id}": { | |
| 1149 | + summary: "Delete a cache", | |
| 1150 | + description: "By the entry's numeric id. 404 when there is none. Audited as `actions.cache.delete`.", | |
| 1151 | + access: "write", | |
| 1152 | + status: 204, | |
| 1153 | + }, | |
| 1154 | + "GET /repos/{owner}/{repo}/actions/cache/usage": { | |
| 1155 | + summary: "Get cache usage", | |
| 1156 | + 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).", | |
| 1157 | + access: "read", | |
| 1158 | + }, | |
| 1159 | + "GET /repos/{owner}/{repo}/actions/permissions/workflow": { | |
| 1160 | + summary: "Get workflow token permissions", | |
| 1161 | + description: | |
| 1162 | + "The defaults for the `GITHUB_TOKEN` of the repository's workflows (GitHub's \"Workflow permissions\" setting). `default_workflow_permissions` is what a job without `permissions:` gets: `read` (contents and packages read, nothing else) or `write` (read and write on every scope except `id-token`). `can_approve_pull_request_reviews` says whether job tokens may open pull requests and approve or request changes. New repositories start with `read` and `false`; repositories created before the setting existed have `write` and `false`. See [Workflow permissions](/docs/actions/workflow-permissions).", | |
| 1163 | + access: "admin", | |
| 1164 | + }, | |
| 1165 | + "PUT /repos/{owner}/{repo}/actions/permissions/workflow": { | |
| 1166 | + summary: "Set workflow token permissions", | |
| 1167 | + description: | |
| 1168 | + "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.", | |
| 1169 | + access: "admin", | |
| 1170 | + status: 204, | |
| 1171 | + body: { | |
| 1172 | + default_workflow_permissions: ["string", "`read` or `write`."], | |
| 1173 | + can_approve_pull_request_reviews: ["boolean", "Allow workflows' `GITHUB_TOKEN` to open pull requests and to approve or request changes."], | |
| 1174 | + }, | |
| 1175 | + }, | |
| 1054 | 1176 | "GET /repos/{owner}/{repo}/actions/secrets": { summary: "List repository secrets", description: "Names and update times only; values are never returned.", access: "admin" }, |
| 1055 | 1177 | "PUT /repos/{owner}/{repo}/actions/secrets/{name}": { |
| 1056 | 1178 | summary: "Set a repository secret", |
| 1057 | - description: "People only. The value is encrypted at rest. GitHub's sealed-box `encrypted_value` isn't supported; send `value` over HTTPS.", | |
| 1179 | + 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`.", | |
| 1058 | 1180 | access: "admin", |
| 1059 | 1181 | status: 204, |
| 1060 | 1182 | body: { value: ["string", "Secret value.", true] }, |
| 1061 | 1183 | }, |
| 1062 | - "DELETE /repos/{owner}/{repo}/actions/secrets/{name}": { summary: "Delete a repository secret", description: "People only.", access: "admin", status: 204 }, | |
| 1063 | - "GET /repos/{owner}/{repo}/actions/variables": { summary: "List repository variables", access: "write", query: { ...page } }, | |
| 1184 | + "DELETE /repos/{owner}/{repo}/actions/secrets/{name}": { summary: "Delete a repository secret", description: "People only. Audited as `repo.secret.delete`.", access: "admin", status: 204 }, | |
| 1185 | + "GET /repos/{owner}/{repo}/actions/variables": { | |
| 1186 | + summary: "List repository variables", | |
| 1187 | + 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.", | |
| 1188 | + access: "write", | |
| 1189 | + query: { ...page }, | |
| 1190 | + }, | |
| 1064 | 1191 | "POST /repos/{owner}/{repo}/actions/variables": { |
| 1065 | 1192 | summary: "Create a repository variable", |
| 1066 | - description: "409 when it exists. Names are stored upper-cased. People only.", | |
| 1193 | + description: "409 when it exists. Names are stored upper-cased. People only. Audited as `repo.variable.set`.", | |
| 1067 | 1194 | access: "admin", |
| 1068 | 1195 | status: 201, |
| 1069 | 1196 | body: { name: ["string", "Variable name.", true], value: ["string", "Variable value.", true] }, |
| @@ -1071,43 +1198,67 @@ | ||
| 1071 | 1198 | "GET /repos/{owner}/{repo}/actions/variables/{name}": { summary: "Get a repository variable", description: "`created_at` and `updated_at` are always null.", access: "write" }, |
| 1072 | 1199 | "PATCH /repos/{owner}/{repo}/actions/variables/{name}": { |
| 1073 | 1200 | summary: "Update a repository variable", |
| 1074 | - description: "People only.", | |
| 1201 | + description: "People only. Renaming onto a name that exists fails with 409. Audited as `repo.variable.set`.", | |
| 1075 | 1202 | access: "admin", |
| 1076 | 1203 | status: 204, |
| 1077 | 1204 | body: { name: ["string", "New name (renames the variable)."], value: ["string", "New value."] }, |
| 1078 | 1205 | }, |
| 1079 | 1206 | "PUT /repos/{owner}/{repo}/actions/variables/{name}": { |
| 1080 | 1207 | summary: "Set a repository variable", |
| 1081 | - description: "Diffinery's form: creates the variable or replaces its value. GitHub clients use `POST` and `PATCH`. Names are stored upper-cased. People only.", | |
| 1208 | + 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`.", | |
| 1082 | 1209 | access: "admin", |
| 1083 | 1210 | status: 204, |
| 1084 | 1211 | body: { value: ["string", "Variable value.", true] }, |
| 1085 | 1212 | }, |
| 1086 | 1213 | "DELETE /repos/{owner}/{repo}/actions/variables/{name}": { |
| 1087 | 1214 | summary: "Delete a repository variable", |
| 1088 | - description: "People only. `name` must match the stored name exactly, and names are stored upper-cased, so send it in upper case.", | |
| 1215 | + description: "People only. `name` must match the stored name exactly, and names are stored upper-cased, so send it in upper case. Audited as `repo.variable.delete`.", | |
| 1089 | 1216 | access: "admin", |
| 1090 | 1217 | status: 204, |
| 1091 | 1218 | }, |
| 1092 | 1219 | "GET /orgs/{org}/actions/secrets": { summary: "List organization secrets", description: "Names and update times only.", access: "org-admin" }, |
| 1093 | 1220 | "PUT /orgs/{org}/actions/secrets/{name}": { |
| 1094 | 1221 | summary: "Set an organization secret", |
| 1095 | - description: "Available to every repository in the organization. Names are upper-cased and must match `[A-Z_][A-Z0-9_]*`. People only.", | |
| 1222 | + description: "Available to every repository in the organization. Names are upper-cased and must match `[A-Z_][A-Z0-9_]*`. People only. Audited as `org.secret.set`.", | |
| 1096 | 1223 | access: "org-admin", |
| 1097 | 1224 | status: 204, |
| 1098 | 1225 | body: { value: ["string", "Secret value.", true] }, |
| 1099 | 1226 | }, |
| 1100 | - "DELETE /orgs/{org}/actions/secrets/{name}": { summary: "Delete an organization secret", description: "404 when the organization has no secret with this name. Audited.", access: "org-admin", status: 204 }, | |
| 1227 | + "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 }, | |
| 1228 | + "GET /orgs/{org}/actions/variables": { | |
| 1229 | + summary: "List organization variables", | |
| 1230 | + description: | |
| 1231 | + "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.", | |
| 1232 | + access: "org-admin", | |
| 1233 | + }, | |
| 1234 | + "POST /orgs/{org}/actions/variables": { | |
| 1235 | + summary: "Create an organization variable", | |
| 1236 | + description: | |
| 1237 | + "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`.", | |
| 1238 | + access: "org-admin", | |
| 1239 | + status: 201, | |
| 1240 | + body: { name: ["string", "Variable name.", true], value: ["string", "Variable value, at most 49,152 characters.", true] }, | |
| 1241 | + }, | |
| 1242 | + "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" }, | |
| 1243 | + "PATCH /orgs/{org}/actions/variables/{name}": { | |
| 1244 | + summary: "Update an organization variable", | |
| 1245 | + 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`.", | |
| 1246 | + access: "org-admin", | |
| 1247 | + status: 204, | |
| 1248 | + body: { value: ["string", "New value, at most 49,152 characters.", true] }, | |
| 1249 | + }, | |
| 1250 | + "DELETE /orgs/{org}/actions/variables/{name}": { summary: "Delete an organization variable", description: "Answers 204 whether or not the variable existed. People only. Audited as `org.variable_delete`.", access: "org-admin", status: 204 }, | |
| 1101 | 1251 | |
| 1102 | 1252 | // ------------------------------------------------------------ webhooks |
| 1103 | 1253 | "GET /repos/{owner}/{repo}/hooks": { summary: "List webhooks", description: "Secrets are never returned.", access: "admin" }, |
| 1104 | 1254 | "POST /repos/{owner}/{repo}/hooks": { |
| 1105 | 1255 | summary: "Create a webhook", |
| 1256 | + description: "Audited as `repo.webhook.create`.", | |
| 1106 | 1257 | access: "admin", |
| 1107 | 1258 | status: 201, |
| 1108 | 1259 | body: { config: ["object", "`{ url, secret }`. The secret signs each delivery (`X-Hub-Signature-256`).", true], events: ["string[]", "Event names, or `[\"*\"]` (default) for all."] }, |
| 1109 | 1260 | }, |
| 1110 | - "DELETE /repos/{owner}/{repo}/hooks/{id}": { summary: "Delete a webhook", access: "admin", status: 204 }, | |
| 1261 | + "DELETE /repos/{owner}/{repo}/hooks/{id}": { summary: "Delete a webhook", description: "Audited as `repo.webhook.delete`.", access: "admin", status: 204 }, | |
| 1111 | 1262 | |
| 1112 | 1263 | // ------------------------------------------------------------ notifications and email |
| 1113 | 1264 | "GET /notifications": { summary: "List notifications", access: "account", query: { all: ["boolean", "`true` to include read ones."] } }, |
| @@ -1134,13 +1285,13 @@ | ||
| 1134 | 1285 | "GET /diffinery/tokens": { summary: "List your tokens", access: "account" }, |
| 1135 | 1286 | "POST /diffinery/tokens": { |
| 1136 | 1287 | summary: "Create a token", |
| 1137 | - 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.", | |
| 1288 | + 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`).", | |
| 1138 | 1289 | access: "account", |
| 1139 | 1290 | status: 201, |
| 1140 | 1291 | body: { |
| 1141 | 1292 | name: ["string", "A name to recognise it by.", true], |
| 1142 | 1293 | scopes: ["string[]", "`read`, `write` (default) and/or `admin`."], |
| 1143 | - repositories: ["string[]", "Restrict to these `owner/name` repositories."], | |
| 1294 | + repositories: ["string[]", "Restrict to these `owner/name` repositories (only ones you can see)."], | |
| 1144 | 1295 | expires_in_days: ["integer", "Lifetime in days."], |
| 1145 | 1296 | }, |
| 1146 | 1297 | }, |
| @@ -1180,6 +1331,119 @@ | ||
| 1180 | 1331 | access: "write", |
| 1181 | 1332 | }, |
| 1182 | 1333 | |
| 1334 | + // ------------------------------------------------------------ GitHub imports and linked accounts | |
| 1335 | + "POST /diffinery/imports": { | |
| 1336 | + summary: "Start a GitHub import", | |
| 1337 | + description: | |
| 1338 | + "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 into an organization that owns it or that its owner owns or administers, 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).", | |
| 1339 | + access: "account", | |
| 1340 | + status: 201, | |
| 1341 | + body: { | |
| 1342 | + 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`."], | |
| 1343 | + source_owner: ["string", "A GitHub organization or user: imports its repositories (private ones too when the token can see them)."], | |
| 1344 | + owner: ["string", "The Diffinery user or organization that will own the new repositories.", true], | |
| 1345 | + name: ["string", "`source` only: the new repository's name. Default: the GitHub name. It must not exist yet."], | |
| 1346 | + visibility: ["string", "`keep` (default: public stays public, private and internal become private), `private` or `public`. `internal` is accepted and means `private`."], | |
| 1347 | + 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."], | |
| 1348 | + 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`."], | |
| 1349 | + 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."], | |
| 1350 | + repositories: ["string[]", "`source_owner` only: the repository names to import, at most 1,000. Default: every repository (up to 1,000)."], | |
| 1351 | + exclude_forks: ["boolean", "`source_owner` only: skip forks (default `false`)."], | |
| 1352 | + exclude_archived: ["boolean", "`source_owner` only: skip archived repositories (default `false`)."], | |
| 1353 | + concurrency: ["integer", "`source_owner` only: repositories imported at the same time, 1 to 8 (default 3)."], | |
| 1354 | + }, | |
| 1355 | + }, | |
| 1356 | + "GET /diffinery/imports": { | |
| 1357 | + summary: "List your imports", | |
| 1358 | + 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`.", | |
| 1359 | + access: "account", | |
| 1360 | + }, | |
| 1361 | + "GET /diffinery/imports/{id}": { | |
| 1362 | + summary: "Get an import", | |
| 1363 | + description: | |
| 1364 | + "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`.", | |
| 1365 | + access: "account", | |
| 1366 | + }, | |
| 1367 | + "POST /diffinery/imports/{id}/cancel": { | |
| 1368 | + summary: "Cancel an import", | |
| 1369 | + description: | |
| 1370 | + "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`.", | |
| 1371 | + access: "account", | |
| 1372 | + }, | |
| 1373 | + "POST /diffinery/imports/{id}/retry": { | |
| 1374 | + summary: "Retry an import", | |
| 1375 | + description: | |
| 1376 | + "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). 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). Audited as `import.retry`.", | |
| 1377 | + access: "account", | |
| 1378 | + status: 202, | |
| 1379 | + body: { github_token: ["string", "A GitHub token for this attempt (optional; at most 400 characters)."] }, | |
| 1380 | + }, | |
| 1381 | + "POST /diffinery/imports/{id}/selection": { | |
| 1382 | + summary: "Choose repositories for a draft import", | |
| 1383 | + description: | |
| 1384 | + "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`.", | |
| 1385 | + access: "account", | |
| 1386 | + status: 202, | |
| 1387 | + body: { repositories: ["string[]", "Names of the repositories to import, at most 1,000.", true] }, | |
| 1388 | + }, | |
| 1389 | + "POST /diffinery/imports/{id}/claim": { | |
| 1390 | + summary: "Claim an import's Git transfer (local development)", | |
| 1391 | + description: | |
| 1392 | + "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`.", | |
| 1393 | + access: "account", | |
| 1394 | + }, | |
| 1395 | + "GET /diffinery/imports/{id}/agent/spec": { | |
| 1396 | + summary: "Get a Git transfer spec (import agent)", | |
| 1397 | + description: | |
| 1398 | + "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`).", | |
| 1399 | + access: "account", | |
| 1400 | + }, | |
| 1401 | + "POST /diffinery/imports/{id}/agent/progress": { | |
| 1402 | + summary: "Report Git transfer progress (import agent)", | |
| 1403 | + 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.", | |
| 1404 | + access: "account", | |
| 1405 | + status: 204, | |
| 1406 | + }, | |
| 1407 | + "POST /diffinery/imports/{id}/agent/complete": { | |
| 1408 | + summary: "Report a Git transfer's result (import agent)", | |
| 1409 | + 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.", | |
| 1410 | + access: "account", | |
| 1411 | + status: 204, | |
| 1412 | + }, | |
| 1413 | + "GET /diffinery/user/linked-accounts": { | |
| 1414 | + summary: "List your linked GitHub accounts", | |
| 1415 | + description: | |
| 1416 | + "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.", | |
| 1417 | + access: "account", | |
| 1418 | + }, | |
| 1419 | + "POST /diffinery/user/linked-accounts/github/challenge": { | |
| 1420 | + summary: "Get a GitHub link code", | |
| 1421 | + description: | |
| 1422 | + "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`).", | |
| 1423 | + access: "account", | |
| 1424 | + body: { api_url: ["string", "GitHub Enterprise REST API base, such as `https://ghe.example.com/api/v3`. Default: github.com."] }, | |
| 1425 | + }, | |
| 1426 | + "POST /diffinery/user/linked-accounts/github": { | |
| 1427 | + summary: "Link a GitHub account", | |
| 1428 | + description: | |
| 1429 | + "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`.", | |
| 1430 | + access: "account", | |
| 1431 | + status: 201, | |
| 1432 | + body: { login: ["string", "Your GitHub username.", true], api_url: ["string", "The same API base you used for the challenge. Default: github.com."] }, | |
| 1433 | + }, | |
| 1434 | + "DELETE /diffinery/user/linked-accounts/{id}": { | |
| 1435 | + summary: "Unlink a GitHub account", | |
| 1436 | + 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`.", | |
| 1437 | + access: "account", | |
| 1438 | + status: 204, | |
| 1439 | + }, | |
| 1440 | + "DELETE /diffinery/admin/linked-accounts/{id}": { | |
| 1441 | + summary: "Clear anyone's GitHub link", | |
| 1442 | + 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`.", | |
| 1443 | + access: "site-admin", | |
| 1444 | + status: 204, | |
| 1445 | + }, | |
| 1446 | + | |
| 1183 | 1447 | // ------------------------------------------------------------ packages |
| 1184 | 1448 | "GET /repos/{owner}/{repo}/packages": { summary: "List a repository's packages", description: "Container images linked to the repository.", access: "read" }, |
| 1185 | 1449 | ...packageDocs("/user/packages", "your"), |
modifiedapps/diffinery/src/docs/reference.ts+20−7View file
| @@ -22,6 +22,8 @@ | ||
| 22 | 22 | /** The least token scope that may call it, and whether the token must cover every repository. */ |
| 23 | 23 | scope: Scope; |
| 24 | 24 | accountWide: boolean; |
| 25 | + /** No token may call it: only a browser session. */ | |
| 26 | + sessionOnly: boolean; | |
| 25 | 27 | } |
| 26 | 28 | |
| 27 | 29 | const METHOD_ORDER = ["GET", "POST", "PUT", "PATCH", "DELETE"]; |
| @@ -35,9 +37,9 @@ | ||
| 35 | 37 | const TAGS: Array<[string, RegExp]> = [ |
| 36 | 38 | ["Meta", /^\/(meta|rate_limit)?$/], |
| 37 | 39 | ["Users", /^\/users?(\/\{login\})?$|^\/user\/(orgs|avatar)$/], |
| 38 | - ["Organizations", /^\/orgs\/\{org\}\/(members|invitations|avatar)|^\/invitations$/], | |
| 40 | + ["Organizations", /^\/orgs\/\{org\}(\/(members|invitations(\/\{id\})?|avatar))?$|^\/invitations(\/\{id\})?$/], | |
| 39 | 41 | ["Search", /^\/search\//], |
| 40 | - ["Repositories", /^\/(user|users\/\{login\}|orgs\/\{org\})\/repos$|^\/repos\/\{owner\}\/\{repo\}$|\/(collaborators|assignees|languages|topics|dispatches)(\/|$)/], | |
| 42 | + ["Repositories", /^\/(user|users\/\{login\}|orgs\/\{org\})\/repos$|^\/repos\/\{owner\}\/\{repo\}(\/(collaborators|invitations|assignees|languages|topics|dispatches)(\/|$)|$)/], | |
| 41 | 43 | ["Contents", /\/(contents|raw|readme|tarball|zipball|diffinery\/commits)(\/|$)/], |
| 42 | 44 | ["Releases", /\/releases(\/|$)/], |
| 43 | 45 | ["Git database", /\/git\/(blobs|trees|commits|tags|refs|ref|matching-refs)(\/|$)/], |
| @@ -46,15 +48,17 @@ | ||
| 46 | 48 | ["Checks", /\/check-(runs|suites)/], |
| 47 | 49 | ["Commits", /\/(commits|compare)(\/|$)/], |
| 48 | 50 | ["Branch protection", /\/protection$/], |
| 49 | - ["Issues", /\/(issues|labels)(\/|$)/], | |
| 51 | + ["Issues", /\/(issues|labels|milestones)(\/|$)/], | |
| 50 | 52 | ["Pull requests", /\/pulls(\/|$)(?!.*merge-queue)/], |
| 51 | 53 | ["Merge queue", /\/merge-queue/], |
| 52 | 54 | ["Deployments", /\/deployments(\/|$)/], |
| 53 | 55 | ["Environments", /\/environments(\/|$)/], |
| 54 | 56 | ["Actions secrets and variables", /\/actions\/(secrets|variables)/], |
| 57 | + ["Actions artifacts and caches", /\/actions\/(artifacts|caches?)(\/|$)|\/actions\/runs\/\{runId\}\/artifacts$/], | |
| 55 | 58 | ["Actions", /\/actions\//], |
| 56 | 59 | ["Webhooks", /\/hooks(\/|$)/], |
| 57 | 60 | ["Packages", /\/packages(\/|$)/], |
| 61 | + ["Imports", /^\/diffinery\/(imports|user\/linked-accounts|admin\/linked-accounts)(\/|$)/], | |
| 58 | 62 | ["Notifications and email", /^\/notifications$|\/subscription$|\/diffinery\/(notification-settings|emails|admin)/], |
| 59 | 63 | ["Tokens and agents", /\/diffinery\/(tokens|agents)/], |
| 60 | 64 | ["Sandboxes", /\/diffinery\/sandboxes/], |
| @@ -116,6 +120,7 @@ | ||
| 116 | 120 | // A token's scope also caps its repository role (write → maintain), so admin endpoints need an admin token. |
| 117 | 121 | scope: ACCOUNT_SCOPES[key]?.scope ?? (doc?.access === "admin" ? "admin" : r.method === "GET" ? "read" : "write"), |
| 118 | 122 | accountWide: !!ACCOUNT_SCOPES[key] && !ACCOUNT_SCOPES[key]!.anyToken, |
| 123 | + sessionOnly: !!ACCOUNT_SCOPES[key]?.sessionOnly, | |
| 119 | 124 | }); |
| 120 | 125 | } |
| 121 | 126 | ops.sort((a, b) => TAG_ORDER.indexOf(a.tag) - TAG_ORDER.indexOf(b.tag) || a.path.localeCompare(b.path) || METHOD_ORDER.indexOf(a.method) - METHOD_ORDER.indexOf(b.method)); |
| @@ -133,12 +138,14 @@ | ||
| 133 | 138 | maintain: "Maintain access to the repository.", |
| 134 | 139 | admin: "Admin access to the repository (or to the package).", |
| 135 | 140 | "org-admin": "An owner or admin of the organization (or a site admin).", |
| 141 | + "org-owner": "An owner of the organization (or a site admin).", | |
| 136 | 142 | "site-admin": "A site administrator.", |
| 137 | 143 | }; |
| 138 | 144 | |
| 139 | 145 | /** What a personal access token needs; browser sessions always have full scope. */ |
| 140 | -export function scopeText(op: Pick<ApiOperation, "scope" | "accountWide" | "doc">): string { | |
| 146 | +export function scopeText(op: Pick<ApiOperation, "scope" | "accountWide" | "sessionOnly" | "doc">): string { | |
| 141 | 147 | if (op.doc.access === "public") return ""; |
| 148 | + if (op.sessionOnly) return "No token can call it: it needs a browser session."; | |
| 142 | 149 | return `Tokens need the \`${op.scope}\` scope${op.accountWide ? " and access to all repositories" : ""}.`; |
| 143 | 150 | } |
| 144 | 151 | |
| @@ -156,10 +163,12 @@ | ||
| 156 | 163 | name: "Name.", |
| 157 | 164 | type: "Package type: `container`.", |
| 158 | 165 | workflow: "Workflow file path (URL-encoded) or file name.", |
| 166 | + runId: "Workflow run id, or the run number (`github.run_id`).", | |
| 167 | + format: "Archive format: `zip`.", | |
| 159 | 168 | action: "Action to take.", |
| 160 | 169 | }; |
| 161 | 170 | |
| 162 | -const EXAMPLE: Record<string, string> = { owner: "acme", repo: "website", org: "acme", login: "diffy", number: "42", branch: "main", ref: "main", path: "README.md", spec: "main...feature", type: "container", name: "website", workflow: "ci.yml", action: "exec" }; | |
| 171 | +const EXAMPLE: Record<string, string> = { owner: "acme", repo: "website", org: "acme", login: "diffy", number: "42", branch: "main", ref: "main", path: "README.md", spec: "main...feature", type: "container", name: "website", workflow: "ci.yml", action: "exec", runId: "42", format: "zip" }; | |
| 163 | 172 | |
| 164 | 173 | function curl(origin: string, op: ApiOperation): string { |
| 165 | 174 | const url = `${origin}${API_BASE}${op.path === "/" ? "" : op.path.replace(/\{(\w+)\}/g, (_, p: string) => EXAMPLE[p] ?? (p === "id" ? "1" : p))}`; |
| @@ -200,6 +209,9 @@ | ||
| 200 | 209 | "- Authenticate with a personal access token: `Authorization: Bearer dfy_...` (`token dfy_...` and HTTP Basic with the token as the password work too). Create one under *Settings → Access tokens*.", |
| 201 | 210 | "- Send and receive JSON. Errors are JSON objects with a `message` and a `documentation_url`, unknown paths included (404).", |
| 202 | 211 | "- Reads of public repositories need no token. Every write needs a signed-in caller (401 otherwise) and, from a token, the `write` scope (403 otherwise). A token's scope also caps its role on a repository: `read` reads, `write` acts up to maintain, `admin` acts as admin. Each endpoint below says which scope it needs.", |
| 212 | + "- A private repository you can't see is a 404, whatever the method, so a read token's write to it doesn't reveal that it exists.", | |
| 213 | + "- An archived repository refuses every write with 403 `repository is archived (read-only)`, except unarchiving it (`PATCH` with `archived: false`), deleting it and watching it.", | |
| 214 | + "- A workflow job's `GITHUB_TOKEN` follows the job's `permissions:` instead of token scopes: each endpoint needs its permission (`contents`, `issues`, `pull-requests`, `actions` and so on), and settings, secrets, variables, webhooks and branch protection are out of its reach. See [Workflow permissions](/docs/actions/workflow-permissions).", | |
| 203 | 215 | "- Paths, field names and status codes follow GitHub's REST API wherever Diffinery has the same feature, so most GitHub clients work with `baseUrl` set to the URL above.", |
| 204 | 216 | `- ${ops.length} endpoints in ${byTag.size} groups.`, |
| 205 | 217 | "", |
| @@ -243,7 +255,7 @@ | ||
| 243 | 255 | ["POST /{owner}/{repo}.git/git-receive-pack", "Git smart HTTP: push."], |
| 244 | 256 | ["POST /{owner}/{repo}.git/info/lfs/objects/batch", "Git LFS batch API. See [Git LFS](/docs/repositories/git-lfs)."], |
| 245 | 257 | ["GET, PUT /{owner}/{repo}.git/info/lfs/objects/{oid}", "Git LFS object download and upload."], |
| 246 | - ["GET /{owner}/{repo}/actions/runs/{run_id}/artifacts/{name}", "Download a workflow artifact as `.tar.gz`."], | |
| 258 | + ["GET /{owner}/{repo}/actions/runs/{run_id}/artifacts/{name}", "Download a workflow artifact by name or numeric id, for browsers (the run page links here). Serves a zip, the file itself for an artifact uploaded as a single unzipped file, or a `.tar.gz` for an artifact from an older runner. Expired artifacts aren't served."], | |
| 247 | 259 | ["POST /mcp", "The MCP server (Streamable HTTP, JSON-RPC). See [MCP](/docs/agents/mcp) and the [MCP tool reference](/docs/reference/mcp-tools)."], |
| 248 | 260 | ["/v2/...", "The OCI container registry (Docker Registry HTTP API v2). See [Container registry](/docs/packages/container-registry)."], |
| 249 | 261 | ["GET /.well-known/openid-configuration", "OIDC discovery for workflow ID tokens. See [OIDC](/docs/actions/oidc)."], |
| @@ -308,7 +320,7 @@ | ||
| 308 | 320 | ...(body ? { requestBody: body } : {}), |
| 309 | 321 | ...(access === "public" ? { security: [] } : {}), |
| 310 | 322 | "x-diffinery-access": access, |
| 311 | - "x-diffinery-token-scope": op.scope, | |
| 323 | + "x-diffinery-token-scope": op.sessionOnly ? "none" : op.scope, | |
| 312 | 324 | responses: { |
| 313 | 325 | [ok]: { description: ok === "204" ? "No content" : "Success", ...(ok === "204" || ok === "205" ? {} : { content: { "application/json": { schema: {} } } }) }, |
| 314 | 326 | default: { description: "Error", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } }, |
| @@ -359,6 +371,7 @@ | ||
| 359 | 371 | ["Issues", /issue|comment|label/], |
| 360 | 372 | ["Actions", /workflow|run|job|artifact/], |
| 361 | 373 | ["Releases", /release/], |
| 374 | + ["Imports", /import/], | |
| 362 | 375 | ["Sandboxes", /sandbox/], |
| 363 | 376 | ["Docs", /docs/], |
| 364 | 377 | ]; |