Push images from CI
A job on a hosted runner can push container images to your organization’s private registry. You do not create a token or store a secret for this. The runner carries a one-job ticket; the job exchanges it for a short-lived registry credential, pushes, and the credential is gone when the job ends. The pushed image is then captured, signed and scanned like any other push.
Before you start
Section titled “Before you start”- Hosted runners are on for your organization, and Let CI jobs push images to this organisation’s registry is on in the Settings tab of CI / Pipelines. An Admin changes these; they are on by default.
- The person who triggered the run may push to the registry (the Create registry permission, which Members have by default). Sencai asks for the permission of that person, not of the repository. See which runs get a credential.
- The image name is
git.sencai.space/<organization>/<image>. The Access panel on the registry page shows the exact Image prefix.
The workflow step
Section titled “The workflow step”Every hosted job has two values in its environment. SENCAI_CI_TICKET is the
ticket and SENCAI_CI_API is the address to exchange it at. Add a step like
this before you build, and write the registry host literally, exactly as shown
by the Access panel:
name: build-imageon: [push]
jobs: image: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Log in to the Sencai registry run: | resp="$(mktemp)" printf 'header = "Authorization: Sencai-CI-Ticket %s"\n' "$SENCAI_CI_TICKET" \ | curl -sS --fail-with-body --retry 10 --retry-delay 2 --retry-all-errors \ --config - -X POST -o "$resp" "$SENCAI_CI_API/ci/registry-credentials" \ || { echo "The registry did not give this job a credential:" >&2; cat "$resp" >&2; rm -f "$resp"; exit 1; } user="$(jq -r .username "$resp")" pw="$(jq -r .password "$resp")" rm -f "$resp" echo "::add-mask::$pw" printf '%s' "$pw" | docker login git.sencai.space -u "$user" --password-stdin
- name: Build and push run: | docker build -t git.sencai.space/acme/api:${{ github.sha }} . docker push git.sencai.space/acme/api:${{ github.sha }}What the login step does:
- It sends the ticket in a request header that
curlreads from standard input, so the ticket is not part of the command line. The credential comes back in a response file that is deleted as soon as it is read, is masked in the log, and goes todocker loginthrough standard input, again off the command line. - It retries for up to about twenty seconds. A job asks for its credential in the first seconds of its life, before Sencai has finished recording which job the runner took; the answer in that moment is “not yet” and a retry gets it.
- If Sencai refuses, the step prints the reason (see When a job does not get a credential) instead of failing without a message.
- It needs
curl(7.76 or later) andjqin the job image. If yours lacks them, install them in an earlier step. The Settings tab shows a similar step with your registry host filled in, and a Copy button.
Replace acme/api with your own organization and image. Keep the host written
in the workflow, not taken from a variable someone can change: a variable that
pointed elsewhere would send the credential to that host.
The run’s own automatic token (github.token) cannot push images. You need the
exchange above.
Which runs get a credential
Section titled “Which runs get a credential”A credential is issued only when all of these hold:
| Requirement | Detail |
|---|---|
| The job runs on a hosted runner of your organization | The ticket belongs to one runner, and the runner to one organization |
| Hosted CI and registry push are on | Both switches are on in the Settings tab, and the platform has them on |
| The run has an event that may get a credential | By default push, workflow_dispatch, schedule, release and create. An Admin can choose a narrower list in the Settings tab |
| The run is not from a fork | Pull requests from forks never get a credential |
| The person behind the run may push | The member who triggered it holds the registry Create permission now |
Pull requests from the same repository get a credential only if an Admin
turned on Allow pull requests from the same repository and kept
pull_request in the list. Runs of the pull_request_target event never get
one, because they run with your secrets on code that a pull request’s author
chose.
A run with no Sencai member behind it - started by a deploy key, a mirror sync, or the schedule of a member who has since left - gets nothing unless an Admin switched on Also allow runs with no Sencai member behind them. An explicit deny for a member is never overridden by that switch.
What the credential can do
Section titled “What the credential can do”- It can push images to your organization’s registry, and nothing else. It does not work for any other organization’s registry.
- It belongs to this job alone. Sencai creates a temporary registry login for
the job; it appears on the image page as the pusher, with a name that begins
sencai-a-. - A job can request it up to three times; a new request replaces the previous credential.
- It ends with the job. When the job finishes, fails, times out, is stopped, or its runner is lost, the credential is deleted and its login is switched off, which also ends registry sessions the job had already opened. If a runner disappears without notice, Sencai removes the credential within minutes.
- Turning Let CI jobs push images to this organisation’s registry off, or turning hosted runners off, revokes credentials that are open right now.
When a job does not get a credential
Section titled “When a job does not get a credential”The step prints the answer. The code in it says why:
| Code | Meaning | What to do |
|---|---|---|
ci_job_not_identified_yet |
Sencai has not recorded which job the runner took yet | Nothing; the step retries |
ci_registry_push_not_allowed |
Hosted CI or registry push is off for the organization, or the organization is not active | An Admin checks the Settings tab |
ci_registry_push_disabled |
Registry push from CI is not switched on in this environment | Use the CI push credential instead |
ci_fork_pull_request |
The run is a pull request from a fork | Build forks without pushing |
ci_event_not_allowed |
The run’s event is not in the allowed list | An Admin adds the event, or push from a different trigger |
ci_trigger_not_permitted |
The person behind the run may not push, or there is no member behind it and the opt-in is off | Give that person the Create registry permission, or use the opt-in in the Settings tab |
ci_credential_limit |
The job already asked for the most credentials it may have | Log in once per job and keep using that login; the latest credential stays valid |
ci_ticket_invalid, ci_ticket_expired |
The ticket is unknown, or its job has ended or is being stopped | Run the workflow again; do not copy a ticket out of a job |
ci_ticket_wrong_origin |
The ticket was presented from outside the platform’s CI network | Use it only inside the job |
ci_rate_limited, ci_broker_unavailable |
Too many requests, or a temporary problem | Retry; if it persists, contact support |
Signing, scanning and who built it
Section titled “Signing, scanning and who built it”Pushes from CI are handled by the same steps as every other push, described in What happens on every push: Sencai records the exact image content, signs it and scans it.
On the image page, a version pushed by a hosted job has a line below the push time that begins “Built by” and names the CI, the repository, the run number, the job and the first seven characters of the commit. The same run and commit are recorded in the signed payload, so a signature you verify later tells you which run built the image. If a push cannot be tied to exactly one live job, the page says “Pushed by” a CI credential and that no running job could be matched.
A push is signed only if Sencai can match it to the job that made it, while that job was running, and the person who triggered the run still may push. A push with a credential used after its job ended, from another job, or by a person who lost the permission, is refused: the version shows the refused state and is never signed, and the members with access to security findings are notified.
What’s next
Section titled “What’s next”- Hosted runners - the Queue, Hosted runners and Settings tabs, limits and isolation
- Container registry - tokens, the CI push credential for other CI systems, statuses and findings
- Image signing and verification - check that an image is the one a run pushed
- CI & pipelines - workflow files and secrets