Skip to main content

Package registry

fremforge includes a built-in package registry. Packages live inside your org. There are no long-lived registry tokens: CI jobs use the token every Forgejo Actions job already gets, and people use the fremforge CLI, which hands each tool a token that lasts at most an hour.

Every registry is served from one host, frem.sh, under a path per org and package type:

https://frem.sh/api/packages/<org>/<type>/

There are no per-ecosystem hostnames. Container images are the one exception to the path rule: they use the OCI registry at frem.sh itself.

To pull public images and packages (Docker Hub, quay.io, npm, PyPI, Maven Central, NuGet, Go modules, crates.io and more) through your org, with caching, verification, optional malware scanning and your package policy applied, use the dependency proxy.

Supported package types

Paths in the table are relative to https://frem.sh/api/packages/<org>/. Container images and Go modules are the exceptions, and are shown in full.

TypePathClient setup
npmnpm/.npmrc
Docker / OCIfrem.sh/<org>/<image>:<tag>docker login
OCI artifacts (SBOM, WASM, …)frem.sh/<org>/<name>:<tag>oras login
Mavenmavenpom.xml, settings.xml
NuGetnuget/index.jsondotnet nuget
PyPIpypi to upload, pypi/simple/ to installtwine, pip
Composercomposercomposer.json, auth.json
RubyGemsrubygemsgem push --host
Cargocargo/, as a sparse+https:// index.cargo/config.toml
Go modulesfrem.sh/<org>/<repo>GOPRIVATE
Helm (classic repository)helmhelm repo add
Helm (OCI)oci://frem.sh/<org>/<chart>helm registry login
Debiandebiansources.list
RPMrpm.repodnf config-manager
Alpinealpine/<branch>/<repository>/etc/apk/repositories
Archarch/<repository>/<arch>pacman.conf
Condacondaconda channel
CRANcranR repos option
Conanconanconan remote add
Chefchefknife
Pubpubdart pub hosted repository
Swiftswiftswift package-registry
Vagrantvagrantvagrant box add
Go module proxygoGOPROXY
Generic filesgeneric/<name>/<version>/<file>curl

Every type in the table is enabled for every org. Conda, CRAN, Conan, Chef, Pub, Swift, Vagrant, the Go module proxy and generic files have no recipe on this page yet: point the client at the path in the table and authenticate as described for other package types.

For a private org every path above answers 401 without a token, including the index and key files. Only the NuGet service index (nuget/index.json) is readable without one, because the NuGet client fetches it before it authenticates.

Authentication

There are three ways to authenticate:

WhereCredentialLifetime
A Forgejo Actions jobThe job’s automatic token, ${{ secrets.FORGEJO_TOKEN }} (alias ${{ secrets.GITHUB_TOKEN }})Until the job ends
Your machinefremforge auth login, then fremforge auth configure <tool>At most 1 hour, renewed automatically
CI outside fremforge (GitHub Actions), image pull secrets (OpenShift, Kubernetes)A registry robot your org admin createsUntil the robot expires, at most 365 days

Who may read, publish and delete each package is the organisation’s package access, organisation-wide by default; owners can narrow it per package and protect names. See Package access.

API tokens (ffp_…) are not accepted by the registries. They are for the fremforge REST API only. Personal access tokens with package scopes are not issued: use a registry robot.

Publishing from Forgejo Actions

Every job’s automatic token can use the registry of its repository’s owner:

  • Read any package of the owner (the org the repository belongs to).
  • Write a package that is linked to the job’s own repository, or create a new package. A package created by a job is linked to that job’s repository automatically.
  • Pull requests from forks get read access only, never write.

Nothing needs to be declared: there is no permissions: packages: write to add. A write to a package linked to another repository, or to a package that is not linked to any repository, is refused with 403:

forbidden: this job's token can only write packages linked to its own repository; npm package "@acme/ui" belongs to another repository

For a package on per-package access, an owner can also grant another repository’s jobs read or write on that one package (an Actions grant); in a private organisation, jobs of repositories without a grant or the link can then no longer read it.

To publish an existing unlinked package from a job, link it to the repository first, on the package’s Settings page.

The token works as a password (any username) for every client, and as a Bearer token for npm. One workflow that publishes each common type:

name: publish
on:
  push:
    tags: ['v*']

jobs:
  publish:
    runs-on: ubuntu-latest
    env:
      TOKEN: ${{ secrets.FORGEJO_TOKEN }}
      ORG: ${{ github.repository_owner }}
    steps:
      - uses: actions/checkout@v4

      - name: Container image
        run: |
          echo "$TOKEN" | docker login frem.sh -u "${{ github.actor }}" --password-stdin
          docker build -t "frem.sh/$ORG/app:${GITHUB_REF_NAME}" .
          docker push "frem.sh/$ORG/app:${GITHUB_REF_NAME}"

      - name: npm
        run: |
          printf '//frem.sh/api/packages/%s/npm/:_authToken=%s\n' "$ORG" "$TOKEN" >> "$HOME/.npmrc"
          npm publish

      - name: PyPI
        run: |
          python -m pip install build twine
          python -m build
          twine upload --repository-url "https://frem.sh/api/packages/$ORG/pypi" \
            -u "${{ github.actor }}" -p "$TOKEN" dist/*

      - name: Maven
        run: mvn --batch-mode deploy -s .ci/settings.xml
        env:
          FREMFORGE_USER: ${{ github.actor }}

.ci/settings.xml reads the token from the environment, so it can be committed:

<settings>
  <servers>
    <server>
      <id>fremforge</id>
      <username>${env.FREMFORGE_USER}</username>
      <password>${env.TOKEN}</password>
    </server>
  </servers>
</settings>

For Gradle, pass the same values as properties, for example -PfremforgeUsername="${{ github.actor }}" -PfremforgePassword="$TOKEN", and read them as shown in Gradle.

On your machine: fremforge auth

Install the fremforge CLI and sign in. The sign-in goes through your org’s single sign-on and MFA:

fremforge auth login                 # opens a browser
fremforge auth login --device        # headless or SSH box: shows a code to approve in any
                                     # browser at https://frem.sh/_app/device

Device sign-in needs CLI 0.5.0 or later.

Then set up each tool once:

fremforge auth configure docker pip npm maven gradle
# or in one go:
fremforge auth login --configure=docker,pip,npm,maven,gradle

What configure does per tool:

ToolHow it gets the tokenWritten to disk
docker, podmanA credential helper, docker-credential-fremforge, registered for frem.sh under credHelpers in ~/.docker/config.json. Docker asks it for a fresh token on every pull and push.Nothing. A stored docker login credential for frem.sh is removed.
pip, uvA keyring command that answers for frem.sh and passes every other lookup to your real keyring, if you have one.Nothing
npmThe token in your user ~/.npmrc: once as //frem.sh/:_authToken (installs) and once per org you belong to as //frem.sh/api/packages/<org>/npm/:_authToken (npm only sends a token on publish when it matches the registry URL exactly). After you join an org, run fremforge auth configure npm again.Yes, mode 600, rewritten when it changes
MavenA <server> with <id>fremforge</id> in ~/.m2/settings.xml, keeping the rest of the fileYes, mode 600, rewritten when it changes
GradlefremforgeUsername and fremforgePassword in ~/.gradle/gradle.propertiesYes, mode 600, rewritten when it changes

The helper and the keyring command are small scripts in ~/.local/bin (on Windows, %LOCALAPPDATA%\fremforge\bin) that call the CLI. configure warns you when that directory is not on your PATH.

The token is renewed automatically. npm, Maven and Gradle have no way to ask for a fresh token, so the copy in their config is rewritten whenever you run any fremforge command, or explicitly with:

fremforge auth refresh     # makes sure the written token is valid for at least 30 more minutes

Run it before a build, for example as an npm preinstall script ("preinstall": "fremforge auth refresh --quiet") or as an IDE pre-build step.

Your sign-in itself (the refresh token, valid 30 days) is kept in the operating system’s keychain: macOS Keychain, Windows Credential Manager, or the Secret Service on Linux. Where there is no keychain, or it cannot be used, it falls back to ~/.config/fremforge/auth.json with mode 600, and the CLI says so. Keychain support on macOS and Windows is new in 0.5.0 and so far tested only by automated tests, not on real machines; the file fallback is what keeps it working if a keychain refuses.

fremforge auth logout removes the sign-in and every credential configure wrote: the npm, Maven and Gradle token lines, the docker credHelpers entry, and the helper and keyring scripts. It only removes what fremforge wrote, recognised by a fremforge-managed comment, so your own lines stay. It cannot revoke the token on the server: an access token that was already issued stays valid until it expires, at most 1 hour. To end the sign-in at once, remove fremforge-cli under Settings › Applications on frem.sh.

Other package types

The CLI configures docker, pip/uv, npm, Maven and Gradle. The other clients on this page (NuGet, Composer, RubyGems, Cargo, Helm, apt, dnf, apk, pacman and Go) have no helper yet: publish them from Forgejo Actions, where <token> in the recipes below is ${{ secrets.FORGEJO_TOKEN }} and <username> is ${{ github.actor }}.

npm

Add a .npmrc file to your project root:

@<scope>:registry=https://frem.sh/api/packages/<org>/npm/

fremforge auth configure npm supplies the token (see Authentication); in Actions, add the _authToken line shown in the workflow above.

<scope> is your package’s npm scope. It does not have to match the org name: the internal-fremverk org, for example, publishes @fremverk/* packages.

Publish:

npm publish

Install:

npm install @<scope>/package-name

Docker / OCI

On your machine, run fremforge auth configure docker once; there is no docker login. In Actions, log in with the job token as in the workflow above.

Push and pull:

docker push frem.sh/<org>/<image>:<tag>
docker pull frem.sh/<org>/<image>:<tag>

OCI artifacts with oras

The registry also stores OCI artifacts that are not container images: SBOMs, WASM modules, policy bundles, signatures and anything else with its own artifactType. Use oras 1.2 or later. Log in with a registry robot or, in Actions, with the job token:

echo "<token>" | oras login frem.sh -u <username> --password-stdin

Push a file set under one tag, with a custom artifact type and, optionally, a config blob of your own. Without --config, oras writes the empty config (application/vnd.oci.empty.v1+json):

oras push frem.sh/<org>/<name>:1.0.0 \
  --artifact-type application/vnd.example.module.v1 \
  --config config.json:application/vnd.example.config.v1+json \
  module.wasm:application/vnd.wasm.content.layer.v1+wasm \
  sbom.spdx.json:application/spdx+json

oras pull frem.sh/<org>/<name>:1.0.0 -o ./out
oras manifest fetch frem.sh/<org>/<name>:1.0.0

Attach an artifact to an existing image, such as an SBOM, and list what is attached:

oras attach frem.sh/<org>/<image>:<tag> --artifact-type application/spdx+json \
  sbom.spdx.json:application/spdx+json
oras discover frem.sh/<org>/<image>:<tag>
oras discover frem.sh/<org>/<image>:<tag> --artifact-type application/spdx+json

The attached manifest carries a subject pointing at the image. The registry has no OCI 1.1 referrers endpoint (/v2/<name>/referrers/<digest> answers 404), which is the same as ghcr.io: oras falls back by itself to the referrers tag sha256-<digest>, an index it keeps up to date with every attach. Every tool that follows the OCI 1.1 tag-schema fallback, such as cosign, reads it the same way.

Group artifacts in an index, and copy a tag with everything under it:

oras manifest index create frem.sh/<org>/<name>:1.0.0-bundle 1.0.0 1.0.0-sbom
oras copy --recursive frem.sh/<org>/<name>:1.0.0-bundle --to-oci-layout ./layout:bundle

What applies to an artifact:

  • Malware scan. Every uploaded blob is scanned, tar or not. A blob with a malware signature is refused at the upload with DENIED, and an artifact cannot be pulled until its blobs are scanned. See Malware scan.
  • Image signature policy. An artifact is not a container image. A manifest whose config is not an image config, or whose layers are all signatures, attestations or SBOMs, is not gated by the org’s image signature policy, so oras pull and cosign verify work under enforce. The same holds for an index every entry of which declares an artifactType: an oras manifest index create bundle, and the sha256-<digest> referrers tag that oras discover reads, whatever types are attached. An image with an artifact attached is still gated on the image itself, and so is an image listed in such an index when it is pulled by digest.
  • Vulnerability scanning. The daily Trivy scan skips artifacts: it scans container images only.
  • Immutable tags. Under the org’s container-tag policy (semver or all, see Immutable releases), a frozen tag cannot be moved to another digest or deleted, artifacts included. oras push writes the current time into the org.opencontainers.image.created annotation, so pushing the same files again makes a new digest, which a frozen tag refuses. To make a re-run push idempotent, fix the annotation: --annotation org.opencontainers.image.created=<timestamp>. Attaching to an image behind a frozen tag works: the attachment is pushed by digest, and the sha256-<digest> referrers tag is not a version tag.
  • Deleting. oras manifest delete frem.sh/<org>/<name>@<digest> needs an org admin’s own login. Registry robots can publish but not delete.
  • Access log and audit. Pushes, pulls, refusals and deletes of artifacts are recorded in the access log and the audit log, like image operations.

Maven

Add the distribution repository to pom.xml:

<distributionManagement>
  <repository>
    <id>fremforge</id>
    <url>https://frem.sh/api/packages/<org>/maven</url>
  </repository>
</distributionManagement>

To install from it, add the same URL under <repositories> with the same <id>.

Credentials come from fremforge auth configure maven, which writes the fremforge server entry to ~/.m2/settings.xml, or in Actions from a settings file that reads the job token (see Publishing from Forgejo Actions).

Gradle

Declare the repository and read the credentials from the properties that fremforge auth configure gradle writes (or that you pass with -P in Actions):

repositories {
    maven {
        url = uri("https://frem.sh/api/packages/<org>/maven")
        credentials {
            username = providers.gradleProperty("fremforgeUsername").get()
            password = providers.gradleProperty("fremforgePassword").get()
        }
    }
}

NuGet

Register the source:

dotnet nuget add source https://frem.sh/api/packages/<org>/nuget/index.json \
  --name fremforge \
  --username <username> \
  --password <token>

PyPI

On your machine, after fremforge auth configure pip, tell pip and uv to ask keyring. The username in the index URL is required by pip; use your fremforge username:

pip config set global.keyring-provider subprocess
pip install --index-url https://<username>@frem.sh/api/packages/<org>/pypi/simple/ package-name

UV_KEYRING_PROVIDER=subprocess \
  uv pip install --index-url https://<username>@frem.sh/api/packages/<org>/pypi/simple/ package-name

Publish from Actions with twine, as in the workflow above:

twine upload --repository-url https://frem.sh/api/packages/<org>/pypi -u <username> -p <token> dist/*

Composer

Add the repository to composer.json:

{
  "repositories": [
    { "type": "composer", "url": "https://frem.sh/api/packages/<org>/composer" }
  ]
}

Add credentials to auth.json in your project root:

{
  "http-basic": {
    "frem.sh": {
      "username": "<username>",
      "password": "<token>"
    }
  }
}

Publish a package archive. The version query parameter is needed unless composer.json in the archive sets one:

curl --user <username>:<token> -X PUT \
  --upload-file package.zip \
  "https://frem.sh/api/packages/<org>/composer?version=1.0.0"

RubyGems

Push a gem with an API key:

gem push --key fremforge \
  --host https://frem.sh/api/packages/<org>/rubygems \
  your-gem-0.1.0.gem

Cargo

Add the registry to .cargo/config.toml:

[registries]
fremforge = { index = "sparse+https://frem.sh/api/packages/<org>/cargo/" }

Add credentials to ~/.cargo/credentials.toml:

[registries.fremforge]
token = "Bearer <token>"

Publish:

cargo publish --registry fremforge

Go modules

Go modules are served directly from the repository URL:

go get frem.sh/<org>/<repo>@v1.2.3

If your Go module is private, mark it as such so the Go toolchain skips the public proxy and checksum database:

export GOPRIVATE=frem.sh/<org>/*

GOPRIVATE is the canonical Go env var for this; it disables GOPROXY and GOSUMDB lookups for matching paths in one setting. For go get to authenticate, Git needs a credential for frem.sh: in Actions the checkout token, on your machine your normal Git sign-in.

Helm

Helm charts can be stored in two forms, and you can use either or both:

  • As OCI artifacts in the container registry at frem.sh, next to your images: oci://frem.sh/<org>/<chart>.
  • In a classic chart repository (index.yaml plus chart archives) at https://frem.sh/api/packages/<org>/helm.

The two are separate stores. A chart pushed as OCI does not appear in the classic index.yaml, and the other way round.

<username> and <token> are as described in Other package types.

Helm charts as OCI artifacts

Needs Helm 3.8 or later. Log in once, then push the packaged chart to the org. The chart’s name and version become the repository and the tag:

echo "<token>" | helm registry login frem.sh -u <username> --password-stdin
helm package ./<chart>                                    # writes <chart>-1.0.0.tgz
helm push <chart>-1.0.0.tgz oci://frem.sh/<org>

Install or pull straight from the registry; there is no helm repo add for OCI:

helm install my-release oci://frem.sh/<org>/<chart> --version 1.0.0
helm pull oci://frem.sh/<org>/<chart> --version 1.0.0

The chart is stored as a container package named <chart>, so it is listed under the org’s Packages with your images, and a pull of it passes the same malware-scan gate as an image pull.

To remove a chart version, delete it with the package API, type container. A push also creates a version named after the manifest digest (sha256:…); delete that one as well, or the chart’s content stays stored.

Classic Helm chart repository

Publish a packaged chart:

curl --user <username>:<token> -X POST \
  --upload-file ./<chart>-1.0.0.tgz \
  https://frem.sh/api/packages/<org>/helm/api/charts

A successful upload answers 201.

A published chart version cannot be overwritten. Uploading the same version again with different content answers 409 with a message that starts immutable package: and says the file is already published; nothing is replaced. To publish a change, bump version in Chart.yaml, package the chart again and upload the new version. Uploading the identical archive again, as a re-run CI job does, answers 201 and changes nothing. See Immutable releases.

Add the repository and install from it. For a private org the index answers 401 without a token:

echo "<token>" | helm repo add fremforge https://frem.sh/api/packages/<org>/helm \
  --username <username> --password-stdin
helm repo update
helm search repo fremforge/ --versions
helm install my-release fremforge/<chart> --version 1.0.0

Remove a version with the package API, type helm. A deleted version’s number stays retired: uploading that version again, with any content, answers 409.

Debian

The repository is signed. Its public key is at .../debian/repository.key:

sudo curl --user <username>:<token> \
  https://frem.sh/api/packages/<org>/debian/repository.key \
  -o /etc/apt/keyrings/fremforge-<org>.asc
echo "deb [signed-by=/etc/apt/keyrings/fremforge-<org>.asc] https://frem.sh/api/packages/<org>/debian <distribution> <component>" \
  | sudo tee /etc/apt/sources.list.d/fremforge.list

For a private org, give apt the token in /etc/apt/auth.conf.d/fremforge.conf (machine frem.sh login <username> password <token>).

Publish a .deb into a distribution and component of your choosing:

curl --user <username>:<token> -X PUT \
  --upload-file package_1.0.0_amd64.deb \
  https://frem.sh/api/packages/<org>/debian/pool/<distribution>/<component>/upload

RPM

Add the repository. The .repo file it serves points gpgkey at the signing key, so dnf verifies the packages:

sudo dnf config-manager --add-repo https://frem.sh/api/packages/<org>/rpm.repo

For a private org, add username=<username> and password=<token> to the repository’s section in /etc/yum.repos.d/.

Publish:

curl --user <username>:<token> -X PUT \
  --upload-file package-1.0.0-1.x86_64.rpm \
  https://frem.sh/api/packages/<org>/rpm/upload

Alpine

Install the signing key, then add the repository. <branch> and <repository> are names you choose when you publish, for example v3.20 and main:

cd /etc/apk/keys && curl --user <username>:<token> -JO \
  https://frem.sh/api/packages/<org>/alpine/key
echo "https://<username>:<token>@frem.sh/api/packages/<org>/alpine/<branch>/<repository>" \
  >> /etc/apk/repositories

Publish:

curl --user <username>:<token> -X PUT \
  --upload-file package-1.0.0-r0.apk \
  https://frem.sh/api/packages/<org>/alpine/<branch>/<repository>

Arch

Import the signing key, then add the repository to /etc/pacman.conf:

curl --user <username>:<token> -o fremforge.gpg \
  https://frem.sh/api/packages/<org>/arch/repository.key
sudo pacman-key --add fremforge.gpg
sudo pacman-key --lsign-key <key-id>
[<org>.frem.sh]
SigLevel = Required
Server = https://frem.sh/api/packages/<org>/arch/<repository>/$arch

Publish:

curl --user <username>:<token> -X PUT \
  --upload-file package-1.0.0-1-x86_64.pkg.tar.zst \
  https://frem.sh/api/packages/<org>/arch/<repository>

Container image scanning

Every container image in the registry is scanned with Trivy in a daily sweep. Results appear in Security › Findings › Container images. See Container image scanning for what is scanned and how to block pulls, and Malicious packages for the malware check on images.

Retention and cleanup

Packages are retained indefinitely by default.

  • Delete a version via UI: package detail page → Delete version.
  • Delete via API: DELETE https://frem.sh/api/v1/packages/<org>/<type>/<name>/<version>
  • Bulk cleanup: Org admin → Packages → Cleanup policies, match tags by regex, keep the latest N versions.

Storage quota

ItemDefault
Quota5 GB per seat, pooled across the org. This pool is separate from the repository and LFS pools
Usage viewOrg admin → Packages → Storage usage
Quota increaseAdd seats, enable storage overage, or contact support@frem.sh

What the dependency proxy caches counts toward this same package pool, next to your own packages. See Storage limits and quotas.

Cross-references