GitHub Actions
Run a Ciresk CI check as a GitHub Actions step with no repository secret, and gate pushes with a git hook.
The action
The Ciresk CI action runs one action (check, test, build, lint or a task from .ciresk/ci.yaml) on remote workers and makes the job's result a GitHub result: the step fails with the CLI's own exit code, failing tests become file and line annotations, and the summary lands in the job summary. None of it calls GitHub's API: annotations are workflow commands on the step's output and the summary and outputs are files the runner reads, so the action needs no checks: write or other GitHub permission beyond id-token: write. The job's OIDC token is exchanged for a one-hour cell token; your organization's trust rule decides which repository and ref may run what and with which trust tier.
Two things are not self-service today. The action is not published yet: its files are in tools/ci/integrations/github-action/ of the Ciresk source repository, which is not public, so outside Ciresk a workflow can neither reference nor copy it. And an OIDC trust rule, which maps your repository and ref to a principal, has no API, CLI or console page yet, so you cannot create one; without one the exchange fails with oidc_rejected, reason: unknown_repo. Until then GitHub Actions authenticates with a stored key: install the CLI with the installer (minisign first), put ~/.ciresk/bin on $GITHUB_PATH, keep a csk_live_eu_... API key in a repository secret, and run ciresk ci --pass-token run check --wait --format github with CIRESK_TOKEN set from it, the path Install the CLI documents for machines. --format github gives that step the same annotations, job summary and outputs as the action (below). That key is long-lived, unlike the one-hour exchange.
Inside Ciresk, copy the action into the repository as .github/actions/ciresk-ci/, with these files from tools/ci/integrations/github-action/:
| File | Purpose |
|---|---|
action.yml | The action's inputs, outputs and steps. |
checksums.txt | The sha256 of each CLI archive the install step accepts. |
scripts/install.sh | Downloads and verifies the CLI. |
scripts/exchange.sh | Exchanges the OIDC token. |
scripts/run.sh | Runs the job and writes the outputs, annotations and summary. |
Keep the scripts executable (chmod +x .github/actions/ciresk-ci/scripts/*.sh) and reference the action by its local path:
jobs:
ciresk:
runs-on: ubuntu-latest
permissions:
id-token: write # the OIDC token the action exchanges
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # the CLI walks ancestors to find a commit the mirror has
- uses: ./.github/actions/ciresk-ci # a local action is read from the checkout above
with:
action: testInputs
| Input | Default | Meaning |
|---|---|---|
action | required | The kind to run: check, test, build, lint or a task name. |
targets | "" | Space-separated arguments after the kind, for example -p platform-api; empty runs the task's run line unchanged. |
api-url | https://ci.eu.ciresk.com | The cell. |
timeout | 30m | Forwarded to the CLI as --max-wait: how long the step waits for the job before it exits 7. The job keeps running. |
priority | "" | Forwarded as the job label priority=<value>, read where the policy allows adjustments. |
version | 1.0.4 | The Ciresk CI CLI release to install; it must have a line in the action's checksums.txt and know --format github. The pinned 1.0.4 predates that flag: the default moves to the first release that has it, with its checksums. |
audience | ci.ciresk.com | The OIDC audience requested from GitHub and matched by your organization's trust rule. |
Outputs
| Output | Content |
|---|---|
job-id | job_..., empty when no job was created. |
verdict | succeeded, failed, cancelled, timed_out, infra_error, running when the wait ended first, or error when the CLI returned a problem. |
url | The job URL. |
exit-code | The CLI's exit code, 0 to 8. |
summary-path | The job's summary as the --json envelope on disk (ciresk ci summary <job> --json); upload it as an artifact for a Codex or Claude follow-up. Empty when no job was created. |
What the steps do
| Step | Behavior |
|---|---|
| Install | Downloads the Ciresk CI CLI release for the runner's OS and architecture, verifies its sha256 against the action's checksums.txt and adds it to PATH. For the pinned release that is Linux on x86_64 and aarch64 and Windows on x86_64; macOS runners have no archive. |
| Exchange | Asks GitHub for an OIDC token with audience, posts it to <api-url>/v1/ci/auth/exchange, masks the returned cix_ token and writes it to a 0600 file in the runner's temp directory; exit 3 with the problem code when the exchange is refused. |
| Run | Deletes the token file and keeps the token for the CLI's own processes; runs ciresk ci run <action> [targets] --wait --format github --max-wait <timeout>, which prints the annotations and writes the job summary and the job-id, verdict and url outputs; then reads ciresk ci summary <job> --json into summary-path and writes exit-code; exits with the CLI's code. |
The exchanged token never enters GITHUB_ENV, GITHUB_OUTPUT or the log, and it expires with the hour. The CLI attributes the job to ci:github-actions:<actor> with the run id and attempt as session. The exchange asks for ci:jobs.run ci:jobs.read ci:repos.read ci:blobs.read ci:blobs.write; override with CIRESK_REQUESTED_SCOPES in the job's env. The repository must be registered before the first CI run, since ci:repos.write is left out.
What a failed run shows
--format github is a CLI flag, so the action and a step that runs the CLI itself report the same way. It applies to the commands that print a job summary: run --wait, check, test, build, lint, wait and summary.
| Where | What |
|---|---|
| Annotations | One ::error per failing test, with file and line when the test report has them, so it shows on the diff of a pull request. GitHub shows at most 10 error annotations per step and drops the rest without a word, so the CLI writes at most 9 for tests and a 10th for the verdict, which names the job, the first failed task and how many failing tests were not annotated. When no test failed but the job did (a compiler error, a lint), the first failing line the worker saw is annotated where it points. |
| File paths | A report names a file as its runner saw it: an absolute path in the worker's workspace, a package-qualified name, or a path relative to the task's directory. The CLI maps each onto the tracked file of the checkout it runs in, so annotations land on repository-relative paths. A file it cannot map unambiguously keeps the reported name. |
| Job summary | Appended to $GITHUB_STEP_SUMMARY: the verdict, the job link, trust tier, duration, cost units and cache counts; a table of every task with its verdict, duration and whether the task cache answered it; the failing tests (up to 100) with task, location and first message line; the last lines of the first failed task; the next command. |
| Outputs | job-id, verdict and url appended to $GITHUB_OUTPUT, for later steps. A command that ends in a problem (auth, policy, budget) writes one annotation, a "did not run" summary and verdict=error. |
| Exit code | Unchanged: the CLI's own, 0 to 8 (exit codes). |
| The log | The human summary is printed too. Everything the job's text can reach (its log, test messages, the summary) is printed between ::stop-commands:: and a random token, so a line of the repository's output that starts with :: is never run as a workflow command. |
A step that runs the CLI directly:
- id: ciresk
run: ciresk ci --pass-token test --format github
env:
CIRESK_TOKEN: ${{ secrets.CIRESK_TOKEN }}
- if: always()
run: echo "job ${{ steps.ciresk.outputs.job-id }} ${{ steps.ciresk.outputs.verdict }}"Check runs per task, which a branch protection rule could require by name, are not built: they need Ciresk CI to call GitHub's API. Until they are, the step's result is the status a pull request shows.
Hooks that gate a push
For people and agents that do not run through the Claude Code hook, a git pre-push hook runs ciresk ci run check --wait and refuses the push with the CLI's exit code when the job did not succeed. The CLI prints the summary itself. The script is the one file tools/ci/integrations/pre-commit/pre-push in the Ciresk source, printed here in full because that repository is not public yet. Save it as .git/hooks/pre-push and make it executable (chmod +x .git/hooks/pre-push):
#!/usr/bin/env sh
# runs the repository's check on Ciresk CI before anything leaves the machine and refuses the push
# with the cli's exit code when the job did not succeed. skip once: CIRESK_CI_SKIP=1 git push
set -u
if [ "${CIRESK_CI_SKIP:-0}" = "1" ]; then
exit 0
fi
# git feeds "<local ref> <local sha> <remote ref> <remote sha>" per ref on stdin; a push that only
# deletes refs has nothing to check. with no stdin (the pre-commit framework) the check always runs
seen=0
pushing=0
while read -r _local_ref local_sha _remote_ref _remote_sha; do
seen=1
case "$local_sha" in
0000000000000000000000000000000000000000) ;;
*) pushing=1 ;;
esac
done
if [ "$seen" = 1 ] && [ "$pushing" = 0 ]; then
exit 0
fi
bin="${CIRESK_CI_BIN:-ciresk-ci}"
if ! command -v "$bin" > /dev/null 2>&1; then
echo "pre-push: ${bin} is not on PATH; install the ciresk cli, or push once with CIRESK_CI_SKIP=1" >&2
exit 1
fi
"$bin" run check --wait
code=$?
if [ "$code" -ne 0 ]; then
echo "pre-push: ciresk ci check exited ${code}; fix it, or push once with CIRESK_CI_SKIP=1" >&2
fi
exit "$code"| Knob | Effect |
|---|---|
CIRESK_CI_SKIP=1 git push | Skip the check once. |
CIRESK_CI_BIN | The CLI binary the hook runs; by default the program behind ciresk ci. |
| A push that only deletes refs | Nothing to check; the hook exits 0. |
Binary not on PATH | Exit 1 with the install hint; the hook never passes silently. |
Exit codes 3 (auth), 4 (policy) and 5 (budget) block the push like 1 does: they mean the check did not run, and the message names the fix. The hook is POSIX sh and needs nothing but the CLI. The pre-commit framework takes the same script as a repo: local entry with pre-commit install --hook-type pre-push.