Diffinery

codician/diffinerypublic

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

179 lines · 7.4 KBRaw History
1# Container registry
2
3Diffinery hosts container images at `diffinery.com`, next to the code that builds
4them. It speaks the standard OCI distribution API, so `docker`, `podman`,
5`buildx`, `crane`, `oras`, `skopeo` and the `docker/*` GitHub Actions work
6without changes. Permissions follow ghcr.io: images belong to an owner, are
7private by default, and can be linked to a repository whose collaborators then
8share access to them.
9
10## Image names
11
12```
13diffinery.com/<owner>/<name>[:<tag>|@sha256:<digest>]
14```
15
16`<owner>` is a user or organization. `<name>` may contain slashes
17(`diffinery.com/acme/api/worker`). Names are lowercase; `docker` requires that
18anyway. Blobs and manifests are stored in R2, and identical layers are stored
19once across all packages.
20
21## Signing in
22
23Use any username and a Diffinery token as the password. Create a token under
24**Settings → Tokens**: the `read` scope pulls, `write` pushes, and `admin` also
25deletes. A token limited to some repositories only reaches packages linked to
26those repositories.
27
28```sh
29echo "$DIFFINERY_TOKEN" | docker login diffinery.com -u me --password-stdin
30docker push diffinery.com/acme/api:1.4.0
31docker pull diffinery.com/acme/api:1.4.0
32```
33
34## In workflows
35
36The job's `GITHUB_TOKEN` signs in, exactly as with ghcr.io. Its `packages`
37permission decides what it can do:
38
39| `permissions` | `GITHUB_TOKEN` can |
40|---|---|
41| none set, or `write-all` | push and pull |
42| `packages: write` | push and pull |
43| `packages: read`, or `read-all` | pull |
44| a map without `packages` | nothing |
45
46With write, a workflow pushes to packages linked to its own repository,
47creates new packages under its repository's owner (linked to the repository),
48and deletes versions of its repository's packages. Other packages are
49read-only to it, including ones not linked to any repository. With read, it pulls its
50own repository's packages and any other package of the same owner that it can
51see.
52
53```yaml
54jobs:
55 image:
56 runs-on: ubuntu-latest
57 permissions:
58 contents: read
59 packages: write
60 steps:
61 - uses: actions/checkout@v4
62 - uses: docker/setup-buildx-action@v3
63 - uses: docker/login-action@v3
64 with:
65 registry: diffinery.com
66 username: ${{ github.actor }}
67 password: ${{ secrets.GITHUB_TOKEN }}
68 - uses: docker/build-push-action@v6
69 with:
70 push: true
71 tags: diffinery.com/${{ github.repository }}:${{ github.sha }}
72```
73
74`github.repository` keeps the owner's capitals; use a lowercase name if the
75owner or repository has any (`${GITHUB_REPOSITORY,,}` in a shell step).
76
77Plain `docker login` / `docker build` / `docker push` in `run:` steps works the
78same way. A later job pulls with `docker pull`, as a job `container:`, or as a
79`services:` image, after signing in with `credentials:`:
80
81```yaml
82 container:
83 image: diffinery.com/acme/api:${{ github.sha }}
84 credentials:
85 username: ${{ github.actor }}
86 password: ${{ secrets.GITHUB_TOKEN }}
87```
88
89## Moving from ghcr.io
90
91Replace `ghcr.io` with `diffinery.com` in image names and in `registry:` for
92`docker/login-action`. Nothing else changes: the same `permissions: packages:
93write`, the same `GITHUB_TOKEN`, the same `org.opencontainers.image.source`
94label. To copy existing images across, use a tool that copies all platforms
95(this works for layers under 100 MB; for bigger ones, `docker pull`, `docker
96tag` and `docker push` in a hosted workflow, see [Size limits](#size-limits)):
97
98```sh
99crane auth login diffinery.com -u me -p "$DIFFINERY_TOKEN"
100crane copy ghcr.io/acme/api:1.4.0 diffinery.com/acme/api:1.4.0
101```
102
103## Repositories and visibility
104
105A package linked to a repository shares the repository's roles: readers pull,
106writers push, admins delete versions and change settings. The owner (a user,
107or an organization's owners and admins) always administers its packages.
108Organization members can create packages and pull the organization's packages;
109a package that isn't linked to a repository is administered by the member who
110created it, and other members can't push to it.
111
112A package is linked when it is created:
113
114- by a workflow, to the workflow's repository;
115- by a person pushing `<owner>/<repo>` or `<owner>/<repo>/<anything>` while
116 able to write to that repository;
117- by the `org.opencontainers.image.source` label or annotation on the image,
118 when it names a repository of the same owner (on Diffinery or on github.com)
119 that the pusher can write to. An unlinked package is linked the first time
120 an image with the label is pushed.
121
122The link can also be changed on the package's settings page or through the
123API. Packages are **private** by default; **public** lets anyone pull without
124signing in.
125
126Pages:
127
128- `https://diffinery.com/<owner>/-/packages`: an owner's packages.
129- `https://diffinery.com/<owner>/-/packages/container/<name>`: tags, versions,
130 pull commands and settings.
131- `https://diffinery.com/<owner>/<repo>/packages`: the packages linked to a
132 repository (the **Packages** tab).
133
134## API
135
136The REST API follows GitHub's packages API under `/api/v3`, so existing
137scripts work after changing the base URL. `<type>` is always `container`;
138`<name>` is URL-encoded when it contains slashes (`api%2Fworker`).
139
140| Request | Does |
141|---|---|
142| `GET /user/packages?package_type=container` | your packages |
143| `GET /users/<user>/packages`, `GET /orgs/<org>/packages` | an owner's packages you can see (`?visibility=` filters) |
144| `GET …/packages/<type>/<name>` | one package |
145| `GET …/packages/<type>/<name>/versions` | its versions, newest first, with their tags in `metadata.container.tags` |
146| `GET …/packages/<type>/<name>/versions/<id>` | one version |
147| `DELETE …/packages/<type>/<name>/versions/<id>` | delete a version and its tags |
148| `DELETE …/packages/<type>/<name>` | delete the package |
149| `PATCH …/packages/<type>/<name>` | Diffinery only: `{"visibility": "public"}` or `{"repository": "api"}` (`null` unlinks) |
150| `GET /repos/<owner>/<repo>/packages` | Diffinery only: packages linked to a repository |
151
152The registry itself is at `/v2/`: blob uploads (monolithic, chunked and
153streamed), cross-repository mounts, range requests, image manifests and
154indexes (OCI and Docker v2), tag listing with pagination, `DELETE` of
155manifests and tags, the referrers API (signatures and SBOMs attached with
156`subject`), and `/v2/_catalog` (the repositories you can pull).
157
158## Size limits
159
160Cloudflare limits one request to 100 MB, and `docker push` sends each layer
161in a single request. On Diffinery's hosted runners this doesn't matter: the
162runner's Docker daemon (and BuildKit builders created with
163`docker buildx create` or `docker/setup-buildx-action`) reach the registry
164through a local relay that splits uploads into smaller chunks. Layers of any
165size push from workflows that use `docker` or BuildKit. Other clients run
166directly in a step (`crane`, `skopeo`) don't go through the relay.
167
168From a laptop or a self-hosted runner, a layer over 100 MB fails to push with
169`413` or a reset connection, because the common clients (`docker`, `buildx`,
170`crane`, `skopeo`) send a layer in one request. The registry accepts chunked
171uploads (`PATCH` with `Content-Range`) of any total size, so a client that
172chunks works. Otherwise push from a hosted workflow, or keep layers under
173100 MB by putting large dependencies in their own `RUN`/`COPY` steps. Pulls
174have no size limit.
175
176## Cleanup
177
178Unfinished uploads are discarded after a day. Deleting a version removes its
179tags; layers no other image uses are removed from storage by a scheduled job.