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.

176 lines · 7.2 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. With read, it pulls its
49own repository's packages and any other package of the same owner that it can
50see.
51
52```yaml
53jobs:
54 image:
55 runs-on: ubuntu-latest
56 permissions:
57 contents: read
58 packages: write
59 steps:
60 - uses: actions/checkout@v4
61 - uses: docker/setup-buildx-action@v3
62 - uses: docker/login-action@v3
63 with:
64 registry: diffinery.com
65 username: ${{ github.actor }}
66 password: ${{ secrets.GITHUB_TOKEN }}
67 - uses: docker/build-push-action@v6
68 with:
69 push: true
70 tags: diffinery.com/${{ github.repository }}:${{ github.sha }}
71```
72
73`github.repository` keeps the owner's capitals; use a lowercase name if the
74owner or repository has any (`${GITHUB_REPOSITORY,,}` in a shell step).
75
76Plain `docker login` / `docker build` / `docker push` in `run:` steps works the
77same way. A later job pulls with `docker pull`, as a job `container:`, or as a
78`services:` image, after signing in with `credentials:`:
79
80```yaml
81 container:
82 image: diffinery.com/acme/api:${{ github.sha }}
83 credentials:
84 username: ${{ github.actor }}
85 password: ${{ secrets.GITHUB_TOKEN }}
86```
87
88## Moving from ghcr.io
89
90Replace `ghcr.io` with `diffinery.com` in image names and in `registry:` for
91`docker/login-action`. Nothing else changes: the same `permissions: packages:
92write`, the same `GITHUB_TOKEN`, the same `org.opencontainers.image.source`
93label. To copy existing images across, use a tool that copies all platforms
94(this works for layers under 100 MB; for bigger ones, `docker pull`, `docker
95tag` and `docker push` in a hosted workflow, see [Size limits](#size-limits)):
96
97```sh
98crane auth login diffinery.com -u me -p "$DIFFINERY_TOKEN"
99crane copy ghcr.io/acme/api:1.4.0 diffinery.com/acme/api:1.4.0
100```
101
102## Repositories and visibility
103
104A package linked to a repository shares the repository's roles: readers pull,
105writers push, admins delete versions and change settings. The owner (a user,
106or an organization's owners and admins) always administers its packages, and
107organization members can push to packages that aren't linked to a repository.
108
109A package is linked when it is created:
110
111- by a workflow, to the workflow's repository;
112- by a person pushing `<owner>/<repo>` or `<owner>/<repo>/<anything>` while
113 able to write to that repository;
114- by the `org.opencontainers.image.source` label or annotation on the image,
115 when it names a repository of the same owner (on Diffinery or on github.com)
116 that the pusher can write to. An unlinked package is linked the first time
117 an image with the label is pushed.
118
119The link can also be changed on the package's settings page or through the
120API. Packages are **private** by default; **public** lets anyone pull without
121signing in.
122
123Pages:
124
125- `https://diffinery.com/<owner>/-/packages`: an owner's packages.
126- `https://diffinery.com/<owner>/-/packages/container/<name>`: tags, versions,
127 pull commands and settings.
128- `https://diffinery.com/<owner>/<repo>/packages`: the packages linked to a
129 repository (the **Packages** tab).
130
131## API
132
133The REST API follows GitHub's packages API under `/api/v3`, so existing
134scripts work after changing the base URL. `<type>` is always `container`;
135`<name>` is URL-encoded when it contains slashes (`api%2Fworker`).
136
137| Request | Does |
138|---|---|
139| `GET /user/packages?package_type=container` | your packages |
140| `GET /users/<user>/packages`, `GET /orgs/<org>/packages` | an owner's packages you can see (`?visibility=` filters) |
141| `GET …/packages/<type>/<name>` | one package |
142| `GET …/packages/<type>/<name>/versions` | its versions, newest first, with their tags in `metadata.container.tags` |
143| `GET …/packages/<type>/<name>/versions/<id>` | one version |
144| `DELETE …/packages/<type>/<name>/versions/<id>` | delete a version and its tags |
145| `DELETE …/packages/<type>/<name>` | delete the package |
146| `PATCH …/packages/<type>/<name>` | Diffinery only: `{"visibility": "public"}` or `{"repository": "api"}` (`null` unlinks) |
147| `GET /repos/<owner>/<repo>/packages` | Diffinery only: packages linked to a repository |
148
149The registry itself is at `/v2/`: blob uploads (monolithic, chunked and
150streamed), cross-repository mounts, range requests, image manifests and
151indexes (OCI and Docker v2), tag listing with pagination, `DELETE` of
152manifests and tags, the referrers API (signatures and SBOMs attached with
153`subject`), and `/v2/_catalog` (the repositories you can pull).
154
155## Size limits
156
157Cloudflare limits one request to 100 MB, and `docker push` sends each layer
158in a single request. On Diffinery's hosted runners this doesn't matter: the
159runner's Docker daemon (and BuildKit builders created with
160`docker buildx create` or `docker/setup-buildx-action`) reach the registry
161through a local relay that splits uploads into smaller chunks. Layers of any
162size push from workflows that use `docker` or BuildKit. Other clients run
163directly in a step (`crane`, `skopeo`) don't go through the relay.
164
165From a laptop or a self-hosted runner, a layer over 100 MB fails to push with
166`413` or a reset connection, because the common clients (`docker`, `buildx`,
167`crane`, `skopeo`) send a layer in one request. The registry accepts chunked
168uploads (`PATCH` with `Content-Range`) of any total size, so a client that
169chunks works. Otherwise push from a hosted workflow, or keep layers under
170100 MB by putting large dependencies in their own `RUN`/`COPY` steps. Pulls
171have no size limit.
172
173## Cleanup
174
175Unfinished uploads are discarded after a day. Deleting a version removes its
176tags; layers no other image uses are removed from storage by a scheduled job.