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

Container registry

Diffinery hosts container images at diffinery.com, next to the code that builds them. It speaks the standard OCI distribution API, so docker, podman, buildx, crane, oras, skopeo and the docker/* GitHub Actions work without changes. Permissions follow ghcr.io: images belong to an owner, are private by default, and can be linked to a repository whose collaborators then share access to them.

Image names

diffinery.com/<owner>/<name>[:<tag>|@sha256:<digest>]

<owner> is a user or organization. <name> may contain slashes (diffinery.com/acme/api/worker). Names are lowercase; docker requires that anyway. Blobs and manifests are stored in R2, and identical layers are stored once across all packages.

Signing in

Use any username and a Diffinery token as the password. Create a token under Settings → Tokens: the read scope pulls, write pushes, and admin also deletes. A token limited to some repositories only reaches packages linked to those repositories.

echo "$DIFFINERY_TOKEN" | docker login diffinery.com -u me --password-stdin
docker push diffinery.com/acme/api:1.4.0
docker pull diffinery.com/acme/api:1.4.0

In workflows

The job's GITHUB_TOKEN signs in, exactly as with ghcr.io. Its packages permission decides what it can do:

permissionsGITHUB_TOKEN can
none set, or write-allpush and pull
packages: writepush and pull
packages: read, or read-allpull
a map without packagesnothing

With write, a workflow pushes to packages linked to its own repository, creates new packages under its repository's owner (linked to the repository), and deletes versions of its repository's packages. With read, it pulls its own repository's packages and any other package of the same owner that it can see.

jobs:
  image:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: diffinery.com
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          push: true
          tags: diffinery.com/${{ github.repository }}:${{ github.sha }}

github.repository keeps the owner's capitals; use a lowercase name if the owner or repository has any (${GITHUB_REPOSITORY,,} in a shell step).

Plain docker login / docker build / docker push in run: steps works the same way. A later job pulls with docker pull, as a job container:, or as a services: image, after signing in with credentials::

    container:
      image: diffinery.com/acme/api:${{ github.sha }}
      credentials:
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

Moving from ghcr.io

Replace ghcr.io with diffinery.com in image names and in registry: for docker/login-action. Nothing else changes: the same permissions: packages: write, the same GITHUB_TOKEN, the same org.opencontainers.image.source label. To copy existing images across, use a tool that copies all platforms (this works for layers under 100 MB; for bigger ones, docker pull, docker tag and docker push in a hosted workflow, see Size limits):

crane auth login diffinery.com -u me -p "$DIFFINERY_TOKEN"
crane copy ghcr.io/acme/api:1.4.0 diffinery.com/acme/api:1.4.0

Repositories and visibility

A package linked to a repository shares the repository's roles: readers pull, writers push, admins delete versions and change settings. The owner (a user, or an organization's owners and admins) always administers its packages, and organization members can push to packages that aren't linked to a repository.

A package is linked when it is created:

  • by a workflow, to the workflow's repository;
  • by a person pushing <owner>/<repo> or <owner>/<repo>/<anything> while able to write to that repository;
  • by the org.opencontainers.image.source label or annotation on the image, when it names a repository of the same owner (on Diffinery or on github.com) that the pusher can write to. An unlinked package is linked the first time an image with the label is pushed.

The link can also be changed on the package's settings page or through the API. Packages are private by default; public lets anyone pull without signing in.

Pages:

  • https://diffinery.com/<owner>/-/packages: an owner's packages.
  • https://diffinery.com/<owner>/-/packages/container/<name>: tags, versions, pull commands and settings.
  • https://diffinery.com/<owner>/<repo>/packages: the packages linked to a repository (the Packages tab).

API

The REST API follows GitHub's packages API under /api/v3, so existing scripts work after changing the base URL. <type> is always container; <name> is URL-encoded when it contains slashes (api%2Fworker).

RequestDoes
GET /user/packages?package_type=containeryour packages
GET /users/<user>/packages, GET /orgs/<org>/packagesan owner's packages you can see (?visibility= filters)
GET …/packages/<type>/<name>one package
GET …/packages/<type>/<name>/versionsits versions, newest first, with their tags in metadata.container.tags
GET …/packages/<type>/<name>/versions/<id>one version
DELETE …/packages/<type>/<name>/versions/<id>delete a version and its tags
DELETE …/packages/<type>/<name>delete the package
PATCH …/packages/<type>/<name>Diffinery only: {"visibility": "public"} or {"repository": "api"} (null unlinks)
GET /repos/<owner>/<repo>/packagesDiffinery only: packages linked to a repository

The registry itself is at /v2/: blob uploads (monolithic, chunked and streamed), cross-repository mounts, range requests, image manifests and indexes (OCI and Docker v2), tag listing with pagination, DELETE of manifests and tags, the referrers API (signatures and SBOMs attached with subject), and /v2/_catalog (the repositories you can pull).

Size limits

Cloudflare limits one request to 100 MB, and docker push sends each layer in a single request. On Diffinery's hosted runners this doesn't matter: the runner's Docker daemon (and BuildKit builders created with docker buildx create or docker/setup-buildx-action) reach the registry through a local relay that splits uploads into smaller chunks. Layers of any size push from workflows that use docker or BuildKit. Other clients run directly in a step (crane, skopeo) don't go through the relay.

From a laptop or a self-hosted runner, a layer over 100 MB fails to push with 413 or a reset connection, because the common clients (docker, buildx, crane, skopeo) send a layer in one request. The registry accepts chunked uploads (PATCH with Content-Range) of any total size, so a client that chunks works. Otherwise push from a hosted workflow, or keep layers under 100 MB by putting large dependencies in their own RUN/COPY steps. Pulls have no size limit.

Cleanup

Unfinished uploads are discarded after a day. Deleting a version removes its tags; layers no other image uses are removed from storage by a scheduled job.