Skip to main content

Using fremforge registries from GitHub Actions and OpenShift

Your source and CI can stay on GitHub, and your clusters on OpenShift, while the packages live in your fremforge organisation. Both reach the registry through a registry robot: a login that belongs to the organisation, not to a person.

A registry robot:

  • reads, or reads and publishes, the packages of one organisation, of every package type;
  • cannot read code, open the web UI or reach any other organisation;
  • holds no seat;
  • has an expiry of at most 365 days, after which it and every token it holds stop working;
  • is listed, with who created it, under Org admin → Security → Access → Registry robots, and every create, rotation and deletion is in the organisation’s audit log.

Inside fremforge’s own CI you do not need one: a Forgejo Actions job already has a token for its own organisation’s packages. See Publishing from Forgejo Actions.

Create a robot

In the console: Org admin → Security → Access → Registry robots → Create a robot. Choose Read packages for a pull secret and Read and publish packages for a CI job that publishes.

The page then shows the robot’s username and token once. Copy both; the token is not shown again. If it is lost, Rotate token issues a new one and revokes the old one at once.

Or with the API, using an API token with the policy:write scope:

curl -sS -X POST "https://frem.sh/_app/api/v1/orgs/<org>/registry-robots" \
  -H "Authorization: Bearer $FREMFORGE_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "github-publish", "access": "write", "expires_in_days": 90}'

The response carries username and token. access is read or write, and expires_in_days is 1 to 365 (default 90). Rotate with POST /orgs/<org>/registry-robots/<id>/rotate, optionally with {"expires_in_days": n} to set a new expiry, and delete with DELETE /orgs/<org>/registry-robots/<id>.

The username has the form <org>-fremforge-bot-reg-<8 hex characters>. Every client below takes it as the user name and the token as the password.

GitHub Actions

Store the robot in the GitHub repository or organisation: the username as the variable FREMFORGE_REGISTRY_USER and the token as the secret FREMFORGE_REGISTRY_TOKEN. Set ORG to your fremforge organisation.

Container images

- name: Push to fremforge
  env:
    FREMFORGE_USER: ${{ vars.FREMFORGE_REGISTRY_USER }}
    FREMFORGE_TOKEN: ${{ secrets.FREMFORGE_REGISTRY_TOKEN }}
    ORG: <org>
  run: |
    echo "$FREMFORGE_TOKEN" | docker login frem.sh -u "$FREMFORGE_USER" --password-stdin
    docker build -t "frem.sh/$ORG/app:$GITHUB_SHA" .
    docker push "frem.sh/$ORG/app:$GITHUB_SHA"

Pull the same way, with docker pull frem.sh/<org>/app:<tag>. A pushed tag cannot be overwritten when immutable releases are on for the organisation.

npm

npm sends a token only to a registry URL that matches exactly, so give it the scope’s registry and the token for that URL:

- name: Publish to fremforge
  env:
    FREMFORGE_TOKEN: ${{ secrets.FREMFORGE_REGISTRY_TOKEN }}
    ORG: <org>
  run: |
    printf '@<scope>:registry=https://frem.sh/api/packages/%s/npm/\n//frem.sh/api/packages/%s/npm/:_authToken=%s\n' \
      "$ORG" "$ORG" "$FREMFORGE_TOKEN" > .npmrc
    npm publish

The same .npmrc installs @<scope>/… packages with npm install.

PyPI

- name: Publish to fremforge
  env:
    FREMFORGE_USER: ${{ vars.FREMFORGE_REGISTRY_USER }}
    FREMFORGE_TOKEN: ${{ secrets.FREMFORGE_REGISTRY_TOKEN }}
    ORG: <org>
  run: |
    python -m pip install twine
    twine upload --repository-url "https://frem.sh/api/packages/$ORG/pypi" \
      -u "$FREMFORGE_USER" -p "$FREMFORGE_TOKEN" dist/*

Install with the robot’s credentials in the index URL:

pip install --index-url "https://$FREMFORGE_USER:$FREMFORGE_TOKEN@frem.sh/api/packages/$ORG/pypi/simple/" <package>

Maven

Add a server with the id your pom.xml uses for the fremforge repository, reading the robot from the environment:

<settings>
  <servers>
    <server>
      <id>fremforge</id>
      <username>${env.FREMFORGE_USER}</username>
      <password>${env.FREMFORGE_TOKEN}</password>
    </server>
  </servers>
</settings>
- name: Publish to fremforge
  env:
    FREMFORGE_USER: ${{ vars.FREMFORGE_REGISTRY_USER }}
    FREMFORGE_TOKEN: ${{ secrets.FREMFORGE_REGISTRY_TOKEN }}
  run: mvn --batch-mode -s .ci/settings.xml deploy

with the repository in pom.xml as in Maven: https://frem.sh/api/packages/<org>/maven.

Without a stored secret: GitHub’s OIDC token

A robot can also accept GitHub Actions’ own OIDC token in place of a stored registry token. Give the robot your GitHub organisation’s (or user’s) numeric id, and optionally one repository, under GitHub Actions on the robot, or with github_repository_owner_id and github_repository in the API. A job of that owner then requests its OIDC token with the audience https://frem.sh/<org> (permissions: id-token: write) and exchanges it at POST https://frem.sh/_app/api/v1/registry/oidc-token (org, robot, id_token) for the robot’s username and a token that lasts 1 hour. The exchange is in the API reference under Registry robots; every token it issues is in the audit log with the repository, ref and run id.

SBOMs

SBOMs go to the fremforge API, not the registry, with an API token that has the sboms:write scope. See Upload SBOMs from GitHub Actions.

OpenShift and Kubernetes

Pull images from fremforge with a read-only robot as an image pull secret:

kubectl create secret docker-registry frem-sh \
  --docker-server=frem.sh \
  --docker-username=<robot username> \
  --docker-password=<robot token> \
  --namespace=<namespace>

and reference it from the workload:

spec:
  imagePullSecrets:
    - name: frem-sh
  containers:
    - name: app
      image: frem.sh/<org>/app:1.4.2

OpenShift reads the same kubernetes.io/dockerconfigjson secret. Rotate the robot before it expires and replace the secret with one made from the new token.

Hosts to allow through an egress firewall

HostPortUsed for
frem.sh443Every registry request: logins, indexes, uploads, and container image manifests and layers
obs.eu-de.otc.t-systems.com443Downloads of package files other than container images: npm tarballs, wheels, JARs and so on

A download of a package file other than a container image answers 303 with a short-lived signed link to the file in object storage at obs.eu-de.otc.t-systems.com, which the client follows. That name is a CNAME of obs.lz01.eu-de.otc.t-systems.com, so a firewall that checks every name in the chain needs both. Container image layers are served by frem.sh itself.

frem.sh is served through a CDN, so its addresses change: allow it by name, not by IP address.

Dependency proxy

A pull-through cache for Docker Hub, npm, PyPI and other public registries, used through the same robot, is coming after 2026-11-06.