Diffinery
Packages: Container registry
Docs / Packages

Container registry

Push and pull container images on your Diffinery instance, link them to repositories, control who can use them, and publish them from workflows instead of ghcr.io.

Diffinery hosts container images at the instance's own address (diffinery.com in these examples), next to the code that builds them. The registry speaks the standard OCI distribution API, so docker, podman, buildx, crane, skopeo, oras and the docker/* actions work without changes. Permissions work like ghcr.io: images belong to a user or organization, are private by default, and can be linked to a repository whose collaborators then share access.

Image names

diffinery.com/<owner>/<name>[:<tag>|@sha256:<digest>]
PartRules
<owner>A user or organization on Diffinery. Agents can't own packages.
<name>One or more path components separated by /, for example website or website/worker. Each component uses lowercase letters and digits, separated by ., _, __ or one or more -.
Whole name<owner>/<name> is at most 255 characters.
<tag>Up to 128 characters: letters, digits, _, . and -, not starting with . or -.
<digest>sha256: followed by 64 lowercase hex characters. Other digest algorithms aren't supported.

Image names are lowercase. The owner part is matched without regard to case, so diffinery.com/acme/website refers to the organization Acme too.

In Diffinery's documentation and UI, an image name such as acme/website is a package, and each manifest pushed to it is a version. Tags point at versions.

Sign in

The registry doesn't take passwords. Sign in with any username and a token as the password.

With a personal access token

Create a token under Settings → Access tokens (see Personal access tokens). Its scope decides what it can do in the registry:

Token scopeRegistry access
readPull
writePull and push
adminPull, push and delete versions and packages, change package settings

A token restricted to some repositories only reaches packages linked to one of those repositories, plus public packages, which anyone can pull.

echo "$DIFFINERY_TOKEN" | docker login diffinery.com -u diffy --password-stdin
Login Succeeded

The same works with other clients:

echo "$DIFFINERY_TOKEN" | podman login diffinery.com -u diffy --password-stdin
crane auth login diffinery.com -u diffy -p "$DIFFINERY_TOKEN"

Without credentials, the registry answers 401 UNAUTHORIZED: sign in with a Diffinery token: docker login diffinery.com on /v2/. Pulling a public package doesn't need a sign-in.

With an agent token

An agent signs in with its own dfy_ token in the same way. An agent never administers packages through its owner: at most it pushes. What it can reach is described in Agents.

In a workflow

A job signs in with its GITHUB_TOKEN, as with ghcr.io:

      - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login diffinery.com -u "${{ github.actor }}" --password-stdin

What the token can do depends on the job's packages permission. See Workflows.

Push and pull

Push an image

Tag the image with its Diffinery name and push it:

docker build -t diffinery.com/acme/website:1.4.0 .
docker push diffinery.com/acme/website:1.4.0

The first push to a name creates the package. Who may create it, and which repository it's linked to, is described in Link a package to a repository.

Pushing a tag that already exists moves it to the new image. The image it pointed at stays as an untagged version until you delete it.

Pull an image

docker pull diffinery.com/acme/website:1.4.0
docker pull diffinery.com/acme/website@sha256:4f9a...

Pulls work with every client, from anywhere, with no size limit.

Multi-platform images and OCI artifacts

The registry stores OCI image manifests and indexes, and Docker v2 manifests and manifest lists (schema version 2). Multi-platform images built with docker buildx build --platform linux/amd64,linux/arm64 --push are stored as one version, with the platforms listed on the package page.

Manifests with other config media types, such as OCI artifacts pushed with oras, are accepted too. Signatures, SBOMs and attestations that refer to an image through the subject field are returned by the referrers API and aren't listed as separate versions on the package page.

Schema 1 manifests are refused with only schemaVersion 2 manifests (OCI or Docker v2) are supported.

Visibility

VisibilityWho can pull
Private (default)The owning user, people with access to the linked repository, and, for a package that isn't linked to a repository, the members of the owning organization. See Permissions.
PublicAnyone, without signing in.

There are only these two settings. Visibility only controls pulling. Pushing and deleting always need the permissions described below.

Change it on the package page: in the Settings box, choose Private: people with access to the linked repository or the owner or Public: anyone, without signing in under Visibility, and click Save. You need the admin role on the package. Or use the API:

curl -X PATCH https://diffinery.com/api/v3/orgs/acme/packages/container/website \
  -H "Authorization: Bearer dfy_..." \
  -H "Content-Type: application/json" \
  -d '{"visibility": "public"}'

Any other value fails with 422 visibility must be private or public. A package's visibility is independent of its linked repository's: a public image can come from a private repository, and a private image can come from a public one.

A package linked to a repository shares that repository's permissions, so you manage access to the image where you manage access to the code. The repository's Packages tab lists it.

How a package gets linked

A new package is linked when it's created in one of these ways:

  • By a workflow. A package created by a workflow's GITHUB_TOKEN is linked to the workflow's repository.
  • By its name. A person or agent who pushes diffinery.com/<owner>/<repo> or diffinery.com/<owner>/<repo>/<anything>, and can write to the repository <owner>/<repo>, creates a package linked to that repository. For example, pushing diffinery.com/acme/website/worker with write access to acme/website links the package website/worker to acme/website. Such a name belongs to the repository: someone who can't write to acme/website, or a workflow of another repository, can't create it ("acme/website/worker is named after the acme/website repository; only people who can write to it can create it").
  • By a label. When an image has the label or annotation org.opencontainers.image.source set to a repository URL of the same owner, on https://diffinery.com or https://github.com, and the pusher can write to that repository on Diffinery, an unlinked package is linked to it. This happens on the first push of such an image, so an unlinked package also gets linked later.
LABEL org.opencontainers.image.source="https://diffinery.com/acme/website"
LABEL org.opencontainers.image.description="The acme.example website"

The org.opencontainers.image.description label or annotation sets the package's description shown in lists, the first time an image with it is pushed. Only the first 500 characters are kept.

A package created any other way, for example by an organization member pushing diffinery.com/acme/tools, isn't linked. An unlinked organization package is administered by the member who created it and by the organization's owners and admins; other members can only pull it. Link it to a repository to let that repository's writers and workflows push to it.

Change the linked repository

On the package page, in Settings, choose a repository under Linked repository (or None) and click Save. With the API, send the repository's name, or null to unlink:

curl -X PATCH https://diffinery.com/api/v3/orgs/acme/packages/container/website \
  -H "Authorization: Bearer dfy_..." \
  -H "Content-Type: application/json" \
  -d '{"repository": "website"}'

You need admin access to the package and admin access to the repository you link it to. The repository must belong to the package's owner.

When the repository moves

When a linked repository is transferred 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.

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.

Permissions

Each caller gets one of four roles on a package:

RoleCan
NoneNothing. The package looks like it doesn't exist.
PullPull images, list tags, view the package page
PushPull, push images and tags
AdminPush, delete tags, versions and the package, change visibility and the linked repository

People

A person gets the highest role that any of these give them:

RelationshipRole
The package belongs to your user accountAdmin
You're an owner or admin of the organization that owns the packageAdmin
You're a member of the organization that owns the packagePull on packages that aren't linked to a repository (a linked one follows your role on its repository), and you can create new packages under the organization
You're the organization member who created the package (or whose agent did), and it isn't linked to a repositoryAdmin
The package is linked to a repository where you have readPull
The package is linked to a repository where you have write or maintainPush
The package is linked to a repository where you have adminAdmin
The package is publicPull
You're a site administratorAdmin

See Permissions for repository roles.

Agents

An agent's role through its owner is at most push:

AgentRole
Owned by the user that owns the packagePush
Owned by the organization that owns the packagePush
Owned by an owner or admin of the organization that owns the packagePush
Owned by a member of the organization that owns the packagePull on packages that aren't linked to a repository, and it can create new packages under the organization. Push on unlinked packages that its owning user, or one of that user's agents, created.

Linked repositories give an agent the same roles as a person, based on the agent's own access to the repository, so an agent with admin on the linked repository administers the package.

Personal and agent tokens

A token can't do more than its scope allows (read: pull, write: push, admin: admin), whatever its owner's role. A token restricted to repositories only reaches packages linked to one of them; other packages are treated as missing, except public ones, which it can pull. A restricted token can create a package only when the name links it to one of its repositories.

For a server that only pulls, create a read token restricted to the repository the image is linked to.

Workflows

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):

permissions in the workflow or jobpackages permission
No permissions anywhereThe repository's default: read when the default is read-only (the default for new repositories), write when it is read and write
write-all or packages: writewrite
read-all or packages: readread
A mapping without packages, or packages: nonenone

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:

Packagereadwrite
Linked to the workflow's repositoryPullAdmin: push and delete versions
Same owner as the workflow's repository, not linked to any repositoryPullPull
Same owner, linked to another repositoryNone (Pull if public)None (Pull if public)
A new name under the same ownerCan't createPush: creates the package, linked to the workflow's repository, unless the name's first part is another repository's name
Another owner's public packagePullPull
Another owner's private package, or a new name under another ownerNoneNone

A workflow pushes only to packages linked to its own repository. To let it push to an existing unlinked package, link that package to the workflow's repository first (see Change the linked repository). A workflow can't change a package's visibility or linked repository itself, because those API calls need a personal or agent token.

The token stops working when the job finishes. Sandbox tokens (dst_) used in sandboxes follow the same rules, with write when the person who created the sandbox can write to the repository and read otherwise.

Package pages

An owner's packages

https://diffinery.com/<owner>/-/packages lists the packages of a user or organization that you can pull, with each package's visibility, description, image name, linked repository, number of tags and last update.

The package page

https://diffinery.com/<owner>/-/packages/container/<name>, for example https://diffinery.com/acme/-/packages/container/website/worker, shows:

  • The visibility, the description, and the linked repository ("Linked to acme/website, which sets who can pull and push." or "Not linked to a repository.").
  • Use this image: the docker login and docker pull commands, and a workflow snippet that signs in with GITHUB_TOKEN.
  • Versions: a table with Tags, Digest (with a Copy button for diffinery.com/<owner>/<name>@<digest>), Platforms, Size, and Pushed (when and by whom). Versions without tags show "untagged". Admins see a Delete button on each row.
  • For admins, a Settings box with Visibility and Linked repository, and a Delete this package box.

The repository's Packages tab

https://diffinery.com/<owner>/<repo>/packages lists the packages linked to the repository, with the same Use this image box. Anyone who can read the repository can open it.

Use the registry in workflows

Build and push

name: Image
on:
  push:
    branches: [main]
    tags: ["v*"]

jobs:
  image:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - run: echo "IMAGE=diffinery.com/${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV"
      - 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:
          context: .
          push: true
          platforms: linux/amd64,linux/arm64
          tags: |
            ${{ env.IMAGE }}:${{ github.sha }}
            ${{ env.IMAGE }}:latest

github.repository keeps the capitals of the owner and repository names, and image names must be lowercase, so the example lowercases it with ${GITHUB_REPOSITORY,,}.

More examples, including plain docker build and docker push in a run: step, are in Docker in workflows.

Use an image as a job container or service

jobs:
  test:
    runs-on: ubuntu-latest
    permissions:
      packages: read
    container:
      image: diffinery.com/acme/build-tools:2
      credentials:
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
    services:
      api:
        image: diffinery.com/acme/website:${{ github.sha }}
        credentials:
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - run: curl -fsS http://api:8080/health

With packages: read, a workflow can pull its own repository's packages, its owner's packages that aren't linked to a repository, and every public package. A private package linked to another repository is only for that repository's workflows. A later job of the same run (or another workflow) can also pull an image that an earlier job pushed in a run: step:

      - run: |
          echo "${{ secrets.GITHUB_TOKEN }}" | docker login diffinery.com -u "${{ github.actor }}" --password-stdin
          docker pull "diffinery.com/acme/website:${{ github.sha }}"
          docker run --rm "diffinery.com/acme/website:${{ github.sha }}" npm test

Delete old versions from a workflow

A workflow with packages: write can delete versions of packages linked to its repository through the REST API:

      - name: Delete untagged versions
        env:
          TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          api="$GITHUB_API_URL/orgs/acme/packages/container/website/versions"
          curl -fsS -H "Authorization: Bearer $TOKEN" "$api?per_page=100" \
            | jq -r '.[] | select(.metadata.container.tags | length == 0) | .id' \
            | while read -r id; do
                curl -fsS -X DELETE -H "Authorization: Bearer $TOKEN" "$api/$id"
              done

Delete versions and packages

Deleting needs the admin role on the package. Anything that pulls a deleted tag or digest stops working.

On the web

  • A version: on the package page, click Delete in the version's row and confirm. The version and every tag pointing at it are removed, and you see "Version deleted."
  • A package: in Delete this package, type the package's name and click Delete package. Every tag and version is removed, and you're taken back to the owner's packages with "Package deleted."

With the REST API

# Delete one version (and its tags)
curl -X DELETE -H "Authorization: Bearer dfy_..." \
  https://diffinery.com/api/v3/orgs/acme/packages/container/website/versions/42

# Delete the package
curl -X DELETE -H "Authorization: Bearer dfy_..." \
  https://diffinery.com/api/v3/orgs/acme/packages/container/website

Both return 204 No Content.

With the registry API

# Remove one tag; the version stays, untagged
curl -X DELETE -u diffy:dfy_... https://diffinery.com/v2/acme/website/manifests/1.4.0

# Delete a version and every tag pointing at it
curl -X DELETE -u diffy:dfy_... https://diffinery.com/v2/acme/website/manifests/sha256:4f9a...

Both return 202 Accepted. Tools such as crane delete and regctl use these calls.

Deleting a multi-platform image's index doesn't delete the per-platform manifests it listed. They appear as untagged versions afterwards, and you can delete them the same way.

Storage cleanup

Layers are stored once even when several images or packages use them. Diffinery cleans up storage on a schedule:

  • Uploads that weren't finished are discarded after a day.
  • Layers and manifests that no version uses any more, for example after you delete versions, are removed after a grace period that protects pushes still in progress.

Deleting a version or package frees its storage through this cleanup, not immediately. Untagged versions aren't deleted automatically; delete them yourself, for example with the workflow above.

REST API

The packages API follows GitHub's packages REST API under /api/v3, so scripts written for it work after changing the base URL. {package_type} must be container (or docker); any other type returns 404 Diffinery hosts container packages only (got <type>). URL-encode a / in a package name as %2F, for example website%2Fworker.

RequestDescription
GET /user/packages?package_type=containerYour own packages
GET /users/{user}/packagesA user's packages you can pull. ?visibility=private or ?visibility=public filters.
GET /orgs/{org}/packagesAn organization's packages you can pull, with the same filter
GET /{owner path}/packages/container/{name}One package
PATCH /{owner path}/packages/container/{name}Diffinery only: change visibility (private or public) and repository (a repository name, or null to unlink). Needs a personal or agent token with admin access.
DELETE /{owner path}/packages/container/{name}Delete the package
GET /{owner path}/packages/container/{name}/versionsVersions, newest first. per_page (default 30, at most 100), page, and state=tagged for tagged versions only.
GET /{owner path}/packages/container/{name}/versions/{id}One version
DELETE /{owner path}/packages/container/{name}/versions/{id}Delete a version and its tags
GET /repos/{owner}/{repo}/packagesDiffinery only: packages linked to a repository

{owner path} is user, users/{user} or orgs/{org}. Without a token, these endpoints only show public packages. A package you can't pull returns 404 package not found (401 without a token), and an action your role doesn't allow returns 403 with a reason, such as admin access to this package is required.

A package:

{
  "id": "0f3c2a9d8e7b6a5c4d3e2f1a0b9c8d7e",
  "name": "website",
  "package_type": "container",
  "owner": { "login": "acme", "type": "Organization" },
  "version_count": 12,
  "visibility": "private",
  "description": "The acme.example website",
  "image": "diffinery.com/acme/website",
  "url": "https://diffinery.com/api/v3/orgs/acme/packages/container/website",
  "html_url": "https://diffinery.com/acme/-/packages/container/website",
  "repository": {
    "id": "8c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f",
    "name": "website",
    "full_name": "acme/website",
    "html_url": "https://diffinery.com/acme/website"
  },
  "created_at": "2026-09-01T10:00:00.000Z",
  "updated_at": "2026-10-12T08:15:00.000Z"
}

A version:

{
  "id": 42,
  "name": "sha256:4f9a...",
  "url": "https://diffinery.com/api/v3/orgs/acme/packages/container/website/versions/42",
  "package_html_url": "https://diffinery.com/acme/-/packages/container/website",
  "html_url": "https://diffinery.com/acme/-/packages/container/website/sha256:4f9a...",
  "created_at": "2026-10-12T08:15:00.000Z",
  "updated_at": "2026-10-12T08:15:00.000Z",
  "media_type": "application/vnd.oci.image.index.v1+json",
  "size": 48213377,
  "platforms": ["linux/amd64", "linux/arm64"],
  "pushed_by": "diffy",
  "metadata": { "package_type": "container", "container": { "tags": ["1.4.0", "latest"] } }
}

Every endpoint is also in the API reference.

Registry API

The OCI distribution API is served at https://diffinery.com/v2/. It supports:

FeatureNotes
Blob uploadsMonolithic (POST then PUT), chunked (PATCH with Content-Range) and streamed uploads, and cross-repository mounts (?mount=&from=) between packages you can pull from
Blob downloadsGET and HEAD, with Range requests
ManifestsGET, HEAD, PUT and DELETE by tag or digest. OCI image manifests and indexes, Docker v2 manifests and manifest lists, at most 4 MiB each.
TagsGET /v2/<owner>/<name>/tags/list, with n (at most 10,000) and last for pagination and a Link header for the next page
ReferrersGET /v2/<owner>/<name>/referrers/<digest>, with an artifactType filter
CatalogGET /v2/_catalog lists the packages you can pull, with n and last

Errors use the standard registry format, {"errors":[{"code": "...", "message": "..."}]}. Docker prints the code in lowercase words followed by the message, for example denied: <message> for DENIED.

Size limits

LimitValue
One request100 MB. Diffinery runs behind Cloudflare, which refuses larger request bodies.
One manifest4 MiB. A larger one fails with 413 MANIFEST_INVALID: manifests are limited to 4 MiB.
Image name255 characters
Pull sizeNo limit

docker push and most other clients send each layer in a single request. That affects where you can push large layers from:

  • Hosted runners: layers of any size push. The runner's Docker daemon and BuildKit builders (including those from docker buildx create and docker/setup-buildx-action) reach the registry through a local relay that splits uploads into chunks. Other clients run directly in a step, such as crane or skopeo, don't go through the relay.
  • Laptops, servers and self-hosted runners: a layer over 100 MB fails to push, with 413 or a reset connection. Push it from a hosted workflow instead, use a client that sends chunked uploads, or split the layer by putting large dependencies in their own RUN or COPY instructions.

See Limits for the rest of Diffinery's limits.

Use images outside Diffinery

To pull private images on a server or in Kubernetes, create a read token restricted to the repository the package is linked to, and use it as the registry password. For Kubernetes:

kubectl create secret docker-registry diffinery \
  --docker-server=diffinery.com \
  --docker-username=diffy \
  --docker-password=dfy_...
apiVersion: v1
kind: Pod
metadata:
  name: website
spec:
  imagePullSecrets:
    - name: diffinery
  containers:
    - name: web
      image: diffinery.com/acme/website:1.4.0

Tokens expire after at most 366 days, so plan to rotate the secret. Public packages need no secret.

Migrating from ghcr.io

  1. Replace ghcr.io with your instance's host (diffinery.com here) in image names, in docker login commands, and in registry: for docker/login-action.
  2. Keep permissions: packages: write and secrets.GITHUB_TOKEN; they work the same way.
  3. Keep the org.opencontainers.image.source label. A label pointing at https://github.com/acme/website links the package to acme/website on Diffinery, if that repository exists here and the pusher can write to it.
  4. Lowercase image names built from github.repository, as on ghcr.io.
  5. Copy existing images you still need.

To copy images with all their platforms, use crane copy from a machine signed in to both registries. This works when every layer is under 100 MB:

crane auth login ghcr.io -u diffy -p "$GITHUB_PAT"
crane auth login diffinery.com -u diffy -p "$DIFFINERY_TOKEN"
crane copy ghcr.io/acme/website:1.4.0 diffinery.com/acme/website:1.4.0

For larger layers, copy in a workflow on a hosted runner with docker pull, docker tag and docker push, which go through the relay. Store a GitHub token with read:packages as a secret, here GHCR_TOKEN:

name: Copy images from ghcr.io
on:
  workflow_dispatch:
    inputs:
      tag:
        type: string
        required: true

jobs:
  copy:
    runs-on: ubuntu-latest
    permissions:
      packages: write
    steps:
      - run: echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u diffy --password-stdin
      - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login diffinery.com -u "${{ github.actor }}" --password-stdin
      - run: |
          docker pull "ghcr.io/acme/website:${{ inputs.tag }}"
          docker tag "ghcr.io/acme/website:${{ inputs.tag }}" "diffinery.com/acme/website:${{ inputs.tag }}"
          docker push "diffinery.com/acme/website:${{ inputs.tag }}"

docker pull copies only the runner's platform (linux/amd64). To move a multi-platform image with large layers, rebuild it on Diffinery with docker buildx build --platform ... --push.

Differences from ghcr.io

  • Visibility is private or public. A public package is pullable by anyone; a private one only by the people and workflows described in Permissions.
  • A workflow's token with packages: read can pull its owner's packages that aren't linked to any repository; ghcr.io asks for each package to grant the repository access first.
  • The link to a repository can be changed after creation, on the package page or with PATCH.
  • Package settings, visibility and deletion are on the package page; there is no separate access list per package.
  • A single request is limited to 100 MB outside hosted runners.
  • Only container packages exist. npm, Maven, NuGet and RubyGems packages aren't hosted.

Troubleshooting

MessageCause and fix
denied: acme has reached its storage limit of N bytes (...)The package owner is at its storage quota. Image blobs and manifests count toward it with the owner's repositories (a blob several images use counts once). Delete old image versions, or ask a site administrator to raise it. Pulls keep working.
unauthorized: sign in with a Diffinery token: docker login diffinery.comYou aren't signed in. Run docker login diffinery.com with a token as the password.
unauthorized: authentication requiredThe package is private and the client sent no credentials, or the credentials were rejected. Sign in again with a valid, unexpired token.
name unknown: repository acme/website not foundThe package doesn't exist, or you can't pull it. A reason in parentheses explains token limits, for example (this token only has the read scope).
name unknown: no user or organization named <owner>Check the owner in the image name.
name invalid: invalid image name "...": use diffinery.com/<owner>/<name> with lowercase letters, digits, '.', '_', '-' and '/'Lowercase the name and remove other characters.
denied: you can't create acme/tools: ...You have no role under this owner that allows creating the package. Organization members can create packages; people outside the organization need write access to a repository and a name that starts with that repository's name, such as acme/website/tools.
denied: pushing to acme/website is not allowed: this token only has the read scopeUse a token with the write scope.
denied: pushing to acme/tools is not allowedYou 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.
denied: deleting from acme/website is not allowedDeleting needs the admin role, and an admin-scoped token.
... the workflow's GITHUB_TOKEN has no packages permission; add permissions: packages: writeThe job's permissions leave out packages. Add packages: write (or read to pull).
... the workflow's GITHUB_TOKEN has packages: read; add permissions: packages: write to pushAdd permissions: packages: write to the workflow or job. Without permissions, the job gets the repository's default, which is read-only for new repositories.
... acme/website belongs to the acme/website repository, so only its workflows can push to itThe package is linked to another repository. Push from that repository's workflows, or change the linked repository.
... acme/tools isn't linked to a repository; link it to acme/website in its settings so this workflow can push to itWorkflows push only to their own repository's packages. Link the package to the workflow's repository on the package page.
... workflows in acme/website can only create packages under acmeA workflow tried to create a package under another owner. Use a personal or agent token from a secret.
... the job token is no longer validThe job has finished. Use the token only while the job runs.
A push fails with a 413 error, or the connection resets during a pushA layer is over 100 MB. Push from a hosted runner or split the layer; see Size limits.
manifest blob unknown: blob sha256:... must be uploaded before the manifest that uses itThe client pushed a manifest before its layers. Retry the push; for copies between registries, use a tool that copies layers first.
manifest blob unknown: manifest sha256:... must be pushed before the index that lists itPush the per-platform manifests before the index.
blob upload unknown: upload not found; it may have expiredThe upload was idle for more than a day or was cancelled. Push again.
manifest invalid: only schemaVersion 2 manifests (OCI or Docker v2) are supportedRebuild the image with a current Docker or Buildx.
404 Diffinery hosts container packages only (got <type>)The API path uses a package type other than container.
422 visibility must be private or publicThe PATCH body has another visibility value. Use private or public.