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.
| Type | Path | Client setup |
|---|---|---|
| npm | npm/ | .npmrc |
| Docker / OCI | frem.sh/<org>/<image>:<tag> | docker login |
| OCI artifacts (SBOM, WASM, …) | frem.sh/<org>/<name>:<tag> | oras login |
| Maven | maven | pom.xml, settings.xml |
| NuGet | nuget/index.json | dotnet nuget |
| PyPI | pypi to upload, pypi/simple/ to install | twine, pip |
| Composer | composer | composer.json, auth.json |
| RubyGems | rubygems | gem push --host |
| Cargo | cargo/, as a sparse+https:// index | .cargo/config.toml |
| Go modules | frem.sh/<org>/<repo> | GOPRIVATE |
| Helm (classic repository) | helm | helm repo add |
| Helm (OCI) | oci://frem.sh/<org>/<chart> | helm registry login |
| Debian | debian | sources.list |
| RPM | rpm.repo | dnf config-manager |
| Alpine | alpine/<branch>/<repository> | /etc/apk/repositories |
| Arch | arch/<repository>/<arch> | pacman.conf |
| Conda | conda | conda channel |
| CRAN | cran | R repos option |
| Conan | conan | conan remote add |
| Chef | chef | knife |
| Pub | pub | dart pub hosted repository |
| Swift | swift | swift package-registry |
| Vagrant | vagrant | vagrant box add |
| Go module proxy | go | GOPROXY |
| Generic files | generic/<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:
| Where | Credential | Lifetime |
|---|---|---|
| A Forgejo Actions job | The job’s automatic token, ${{ secrets.FORGEJO_TOKEN }} (alias ${{ secrets.GITHUB_TOKEN }}) | Until the job ends |
| Your machine | fremforge 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 creates | Until 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 repositoryFor 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/deviceDevice 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,gradleWhat configure does per tool:
| Tool | How it gets the token | Written to disk |
|---|---|---|
| docker, podman | A 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, uv | A keyring command that answers for frem.sh and passes every other lookup to your real keyring, if you have one. | Nothing |
| npm | The 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 |
| Maven | A <server> with <id>fremforge</id> in ~/.m2/settings.xml, keeping the rest of the file | Yes, mode 600, rewritten when it changes |
| Gradle | fremforgeUsername and fremforgePassword in ~/.gradle/gradle.properties | Yes, 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 minutesRun 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 publishInstall:
npm install @<scope>/package-nameDocker / 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-stdinPush 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.0Attach 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+jsonThe 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:bundleWhat 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 pullandcosign verifywork underenforce. The same holds for an index every entry of which declares anartifactType: anoras manifest index createbundle, and thesha256-<digest>referrers tag thatoras discoverreads, 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 (
semverorall, see Immutable releases), a frozen tag cannot be moved to another digest or deleted, artifacts included.oras pushwrites the current time into theorg.opencontainers.image.createdannotation, 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 thesha256-<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-namePublish 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.gemCargo
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 fremforgeGo modules
Go modules are served directly from the repository URL:
go get frem.sh/<org>/<repo>@v1.2.3If 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.yamlplus chart archives) athttps://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.0The 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/chartsA 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.0Remove 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.listFor 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>/uploadRPM
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.repoFor 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/uploadAlpine
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/repositoriesPublish:
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>/$archPublish:
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
| Item | Default |
|---|---|
| Quota | 5 GB per seat, pooled across the org. This pool is separate from the repository and LFS pools |
| Usage view | Org admin → Packages → Storage usage |
| Quota increase | Add 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
- Security and supply chain, container image scanning configuration
- CI runners, publishing packages from workflow jobs
- Public REST API, programmatic package management