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 publishThe 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 deploywith 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.2OpenShift 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
| Host | Port | Used for |
|---|---|---|
frem.sh | 443 | Every registry request: logins, indexes, uploads, and container image manifests and layers |
obs.eu-de.otc.t-systems.com | 443 | Downloads 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.