An action to upload a file to the package registry.
  • JavaScript 100%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Stefan Bethke 8108b351ce
All checks were successful
test / test (push) Successful in 15s
Add delete-deb action for the Debian package registry
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 10:51:52 +02:00
.forgejo/workflows Add upload/delete Forgejo actions with tests and CI workflow 2026-08-19 18:26:49 +02:00
delete Add delete-deb action for the Debian package registry 2026-10-22 10:51:52 +02:00
delete-deb Add delete-deb action for the Debian package registry 2026-10-22 10:51:52 +02:00
docker-login Add delete-deb action for the Debian package registry 2026-10-22 10:51:52 +02:00
src Add delete-deb action for the Debian package registry 2026-10-22 10:51:52 +02:00
tests Add delete-deb action for the Debian package registry 2026-10-22 10:51:52 +02:00
token Add docker-login action that writes and verifies registry credentials 2026-10-18 12:38:36 +02:00
upload Add delete-deb action for the Debian package registry 2026-10-22 10:51:52 +02:00
upload-deb Add delete-deb action for the Debian package registry 2026-10-22 10:51:52 +02:00
.gitignore Add upload/delete Forgejo actions with tests and CI workflow 2026-08-19 18:26:49 +02:00
esbuild.build.js Add delete-deb action for the Debian package registry 2026-10-22 10:51:52 +02:00
package-lock.json Add upload/delete Forgejo actions with tests and CI workflow 2026-08-19 18:26:49 +02:00
package.json Add delete-deb action for the Debian package registry 2026-10-22 10:51:52 +02:00
README.md Add delete-deb action for the Debian package registry 2026-10-22 10:51:52 +02:00

upload-package

Forgejo Actions for uploading to and deleting from a generic package registry, uploading to and deleting from a Debian (APT) package registry, and authenticating with a job-scoped JWT obtained via OIDC (ACTIONS_ID_TOKEN_REQUEST_URL / ACTIONS_ID_TOKEN_REQUEST_TOKEN). See Forgejo Authorized Integrations.

Six actions, referenced independently:

  • upload/ — uploads a local file.
  • delete/ — deletes a remote package file.
  • upload-deb/ — uploads .deb files to the Debian package registry, so they can be installed with apt.
  • delete-deb/ — deletes a package from the Debian package registry.
  • token/ — retrieves the JWT itself, for cases like pushing container images that need the raw token rather than a package upload/delete.
  • docker-login/ — writes the JWT into the Docker config.json, so Docker, kaniko, buildah, crane etc. pick it up without further configuration.

The actions require that the workflow has enable-openid-connect: true set.

Upload

permissions:
  id-token: write

steps:
  - uses: actions/checkout@v4
  - name: Upload theme package
    uses: https://git.hanse.de/hanse/upload-package/upload@v3
    with:
      audience: u:2:50bef203-4429-4694-83b3-a9b4ad683d20
      owner: vvm
      package-name: vvm-theme
      version: latest
      file-path: vvm-theme.zip

Inputs

Input Required Default Description
server-url no GITHUB_SERVER_URL Base URL of the Forgejo/Gitea instance
audience yes Audience passed to the OIDC ID token request, e.g. u:2:<uuid>
owner yes Package owner (user or org)
package-name yes Package name
package-type no generic Package type path segment
version no latest Package version
file-path yes Local file to upload
file-name no basename of file-path Remote file name
content-type no application/octet-stream Content-Type header sent with the upload

Outputs

Output Description
package-url Full URL of the uploaded package file

Delete

permissions:
  id-token: write

steps:
  - name: Delete previous theme package
    uses: https://git.hanse.de/hanse/upload-package/delete@v3
    with:
      audience: u:2:50bef203-4429-4694-83b3-a9b4ad683d20
      owner: vvm
      package-name: vvm-theme
      version: latest
      file-name: vvm-theme.zip

Inputs

Input Required Default Description
server-url no GITHUB_SERVER_URL Base URL of the Forgejo/Gitea instance
audience yes Audience passed to the OIDC ID token request
owner yes Package owner (user or org)
package-name yes Package name
package-type no generic Package type path segment
version no latest Package version
file-name yes Remote file name to delete
ignore-not-found no true Treat a 404 response as success instead of failing the step

Outputs

Output Description
deleted "true" if a file was deleted, "false" if it didn't exist and ignore-not-found applied

Upload Debian package

Uploads one or more .deb files to the owner's Debian package registry. Unlike the generic registry, the package name, version and architecture come from the .deb itself; you only choose the distribution and component it's added to. Forgejo regenerates and signs the repository index after each upload.

The upload-deb action is only available from v4 on.

permissions:
  id-token: write

steps:
  - uses: actions/checkout@v4
  - name: Build packages
    run: dpkg-buildpackage -b -us -uc && mv ../*.deb .
  - name: Upload Debian packages
    uses: https://git.hanse.de/hanse/upload-package/upload-deb@v4
    with:
      audience: u:2:50bef203-4429-4694-83b3-a9b4ad683d20
      owner: vvm
      distribution: bookworm
      file-path: |
        vvm-tools_1.2.0_amd64.deb
        vvm-tools_1.2.0_arm64.deb

The registry rejects a package whose name, version and architecture already exist in the distribution and component with 409 Conflict; bump the package version, or delete the old one first with delete-deb, instead of re-uploading the same version. Every file is checked before the first upload, but an upload failing part way through leaves the earlier files uploaded.

To install from the repository:

sudo curl https://git.hanse.de/api/packages/vvm/debian/repository.key \
  -o /etc/apt/keyrings/forgejo-vvm.asc
echo "deb [signed-by=/etc/apt/keyrings/forgejo-vvm.asc] https://git.hanse.de/api/packages/vvm/debian bookworm main" | \
  sudo tee /etc/apt/sources.list.d/forgejo-vvm.list
sudo apt update

Inputs

Input Required Default Description
server-url no GITHUB_SERVER_URL Base URL of the Forgejo/Gitea instance
audience yes Audience passed to the OIDC ID token request, e.g. u:2:<uuid>
owner yes Package owner (user or org)
distribution yes Distribution to add the packages to, e.g. bookworm
component no main Component to add the packages to
file-path yes Local .deb file to upload; several paths may be given, one per line

Outputs

Output Description
repository-url URL of the Debian repository, e.g. https://git.hanse.de/api/packages/vvm/debian, for an apt sources entry

Delete Debian package

Removes one package (name, version and architecture) from a distribution and component of the Debian package registry. Forgejo regenerates the repository index afterwards.

The delete-deb action is only available from v4 on.

permissions:
  id-token: write

steps:
  - name: Delete the previous build
    uses: https://git.hanse.de/hanse/upload-package/delete-deb@v4
    with:
      audience: u:2:50bef203-4429-4694-83b3-a9b4ad683d20
      owner: vvm
      distribution: bookworm
      package-name: vvm-tools
      version: 1.2.0
      architecture: amd64

version is the full version from the package's control file, including any epoch (e.g. 1:1.2.0-1). Architecture-independent packages use all.

Inputs

Input Required Default Description
server-url no GITHUB_SERVER_URL Base URL of the Forgejo/Gitea instance
audience yes Audience passed to the OIDC ID token request
owner yes Package owner (user or org)
distribution yes Distribution to delete the package from, e.g. bookworm
component no main Component to delete the package from
package-name yes Debian package name
version yes Debian package version
architecture yes Package architecture, e.g. amd64 or all
ignore-not-found no true Treat a 404 response as success instead of failing the step

Outputs

Output Description
deleted "true" if the package was deleted, "false" if it didn't exist and ignore-not-found applied

Token

Use this when you need the raw JWT for something other than the generic package API: to push container images (see below), or to call the Forgejo API directly with Authorization: bearer <token>.

permissions:
  id-token: write

steps:
  - name: Get registry token
    id: registry-token
    uses: https://git.hanse.de/hanse/upload-package/token@v3
    with:
      audience: u:2:50bef203-4429-4694-83b3-a9b4ad683d20
  - name: Call the Forgejo API with the token
    run: |
      curl -sS -H "Authorization: bearer ${{ steps.registry-token.outputs.token }}" \
        https://git.hanse.de/api/v1/user

The token action is only available from v2 on; token@v1 does not exist.

Pushing a container image

The JWT works as the password for Forgejo's container registry. The username is not checked; the JWT alone determines identity and permissions. See forgejo/forgejo#3571 and Forgejo's end-to-end test.

This example builds and pushes with kaniko, so it needs no Docker daemon on the runner. Node.js is installed because actions/checkout and the token action are JavaScript actions:

name: container-image
on:
  push:
    branches: [main]

enable-openid-connect: true

jobs:
  build-image:
    runs-on: alpine-latest
    container:
      image: ghcr.io/osscontainertools/kaniko:alpine
    steps:
      - name: Install Node.js for JavaScript actions
        run: apk add --no-cache nodejs git

      - uses: actions/checkout@v4

      - name: Get registry token
        id: registry-token
        uses: https://git.hanse.de/hanse/upload-package/token@v3
        with:
          audience: u:2:50bef203-4429-4694-83b3-a9b4ad683d20

      - name: Build and push image
        env:
          # kaniko's "env" credential helper reads KANIKO_<HOST>_USER/_PASSWORD,
          # with the registry host upper-cased and dots replaced by underscores.
          KANIKO_GIT_HANSE_DE_USER: ${{ forgejo.actor }}
          KANIKO_GIT_HANSE_DE_PASSWORD: ${{ steps.registry-token.outputs.token }}
        run: >-
          /kaniko/executor
          --context=dir://${{ forgejo.workspace }}
          --dockerfile=${{ forgejo.workspace }}/Dockerfile
          --destination=git.hanse.de/${{ forgejo.repository }}:latest
          --destination=git.hanse.de/${{ forgejo.repository }}:${{ forgejo.sha }}
          --credential-helpers=env

On a runner with a Docker daemon, pass the token to docker login instead (docker/login-action works the same way, with the token as password):

      - name: Log in to the container registry
        run: |
          echo "${{ steps.registry-token.outputs.token }}" | \
            docker login git.hanse.de -u "$GITHUB_ACTOR" --password-stdin

Container image names must be lower case; if the repository path has upper case letters, lower-case it before using it as the destination.

Troubleshooting

A rejected JWT shows up as a generic invalid username/password (docker) or UNAUTHORIZED (kaniko), the same error you get with a wrong password. Check:

  • Audience. It must be the audience of an Authorized Integration that grants write:package on the image's owner. The value starts with the user ID of the integration's owner (u:<user-id>:<uuid>); don't copy the example values above, use the one shown for your own integration.
  • Ref filter. The integration's ref filter must match the run's ref exactly, e.g. refs/heads/main (not refs/head/main). Runs triggered by pull_request have a refs/pull/... ref and are rejected by a branch filter; skip the push for those runs, e.g. with kaniko's --no-push.

Inputs

Input Required Default Description
audience yes Audience passed to the OIDC ID token request, e.g. u:2:<uuid>

Outputs

Output Description
token The job JWT, usable as a bearer token for the Forgejo API and package registries, or as the password for the container registry

Docker login

Fetches the JWT and stores it as the credentials for the container registry in the Docker config.json, the file Docker and most other container tools read registry credentials from. Existing entries for other registries are kept; an existing entry for the same registry is replaced.

Before writing the file, the action logs in to the registry the way docker login does (/v2/, then the token endpoint the registry points to). If the registry rejects the JWT, the step fails right away with the registry's response and a hint about the likely cause, usually the wrong audience, instead of a vague invalid username/password or UNAUTHORIZED from a later build step (see Troubleshooting). This checks that the token is accepted, not that it may push to a particular image; a missing write:package grant still only shows up at push time. Set verify: false to skip the check, e.g. when the registry isn't reachable from the step that logs in.

The docker-login action is only available from v3 on.

The file is written to <config-dir>/config.json, where config-dir is the config-dir input, else $DOCKER_CONFIG, else ~/.docker. The registry key is the host of server-url (e.g. git.hanse.de) unless registry is set.

permissions:
  id-token: write

steps:
  - uses: actions/checkout@v4
  - name: Log in to the container registry
    uses: https://git.hanse.de/hanse/upload-package/docker-login@v3
    with:
      audience: u:2:50bef203-4429-4694-83b3-a9b4ad683d20
  - name: Build and push
    run: |
      docker build -t git.hanse.de/${{ forgejo.repository }}:latest .
      docker push git.hanse.de/${{ forgejo.repository }}:latest

With kaniko, write to the directory kaniko reads its credentials from. Set config-dir explicitly rather than relying on the image's DOCKER_CONFIG; when config-dir is set, the action also exports DOCKER_CONFIG to later steps:

      - name: Log in to the container registry
        uses: https://git.hanse.de/hanse/upload-package/docker-login@v3
        with:
          audience: u:2:50bef203-4429-4694-83b3-a9b4ad683d20
          config-dir: /kaniko/.docker

      - name: Build and push image
        run: >-
          /kaniko/executor
          --context=dir://${{ forgejo.workspace }}
          --dockerfile=${{ forgejo.workspace }}/Dockerfile
          --destination=git.hanse.de/${{ forgejo.repository }}:latest

If the existing config.json sets credsStore, or credHelpers for the registry, Docker asks that helper instead of reading the entry written here; the action warns when it finds either. The token stays in the file after the job; it expires with the job's JWT, but don't use this on a shared, persistent runner home directory.

Inputs

Input Required Default Description
audience yes Audience passed to the OIDC ID token request, e.g. u:2:<uuid>
server-url no GITHUB_SERVER_URL Base URL of the Forgejo/Gitea instance; its host is the registry
registry no host of server-url Registry host to store credentials for, e.g. git.hanse.de; verified over https unless a scheme is given
username no GITHUB_ACTOR User name stored with the token; the registry doesn't check it
config-dir no $DOCKER_CONFIG, else ~/.docker Directory holding config.json; when set, DOCKER_CONFIG is exported to later steps
verify no true Log in to the registry before writing config.json, failing with a clear error if the token is rejected

Outputs

Output Description
registry Registry host the credentials were stored for
config-path Path of the config.json that was written
token The job JWT that was stored

Development

Source lives in src/; each action's dist/index.js is a committed esbuild bundle so consumers don't need an install step. After changing src/, rebuild all bundles:

npm install
npm run build

Commit the updated upload/dist/index.js, delete/dist/index.js, upload-deb/dist/index.js, delete-deb/dist/index.js, token/dist/index.js, and docker-login/dist/index.js along with your source changes.

Tests use Node's built-in test runner (no extra dependency). npm test rebuilds all bundles first, then runs unit tests for src/lib/ and integration tests that spawn the real bundled actions against a local mock registry:

npm test