Documentation

Jaypore CI: Minimal, Offline, Local CI system.



Install

Download the binary for your platform from www.jayporeci.in. Available builds:

Platform Binary
Linux x86-64 git-jci-linux-amd64
Linux ARM64 git-jci-linux-arm64
Linux ARMv7 git-jci-linux-armv7
Linux 32-bit git-jci-linux-386
Windows x86-64 git-jci-windows-amd64.exe
Windows ARM64 git-jci-windows-arm64.exe

macOS is not currently offered as a pre-built binary. You can build from source with go build ./cmd/git-jci.

After downloading, move the binary to somewhere on your PATH:

sudo mv git-jci-linux-amd64 /usr/local/bin/git-jci

The binary is fully static (no dependencies) and works on most systems. Once installed, git will automatically find it as a subcommand and you can use it via git jci.

Config

Create a .jci folder in your repository and add a run.sh file. Optionally add a crontab file for scheduled runs.

.jci/
├── run.sh       # required: defines your CI pipeline
└── crontab      # optional: schedule recurring runs

Make run.sh executable:

chmod +x .jci/run.sh

run.sh can be anything ; a shell script, a wrapper that calls a Python program, a binary, a docker-compose invocation ; whatever you like.

Scheduling with crontab

.jci/crontab uses standard 5-field cron syntax with two optional keyword arguments:

# SCHEDULE         [branch:BRANCH]  [name:NAME]
0 0 * * *                                    # midnight, current branch
0 0 * * *          branch:main               # midnight, main branch
*/15 * * * *       name:quick-check          # every 15 min, named job
0 0 * * *          branch:main  name:nightly # named midnight build on main

After editing .jci/crontab, run git jci cron sync to install the entries into your system’s crontab. Each installed entry runs git-jci run from the repository root. For example, the branch:main name:nightly entry above becomes:

0 0 * * *  cd '/path/to/repo' && git fetch --quiet 2>/dev/null; git checkout --quiet main 2>/dev/null && git pull --quiet 2>/dev/null; git-jci run  # JCI:<repoID> [nightly]

Cron jobs run with the repository root as the working directory and have access to all three JCI_* environment variables.

Environment Vars

Your run.sh script receives these environment variables:

Variable Description
JCI_COMMIT Full commit hash
JCI_REPO_ROOT Absolute path to the repository root
JCI_OUTPUT_DIR Absolute path to the output directory for this run

All three are always set. The script’s initial working directory is JCI_OUTPUT_DIR. Any files written to JCI_OUTPUT_DIR become CI artifacts , browsable and downloadable via git jci web. The output directory is a temporary directory that is cleaned up after its contents are stored in git.

Non-zero exit = failure. If run.sh exits with a non-zero code, the run is marked as failed (✗ err). The artifacts are still saved and viewable.

Example workflow

# 1. Enter the repository
cd my-project

# 2. Create the CI script (only needed once)
mkdir -p .jci
cat > .jci/run.sh << 'EOF'
#!/usr/bin/env bash
set -euo pipefail
cd "$JCI_REPO_ROOT"
echo "Running tests..."
go test ./... | tee "$JCI_OUTPUT_DIR/test-results.txt"
EOF
chmod +x .jci/run.sh

# 3. Commit and run CI
git add -A
git commit -m "add CI"
git jci run                 # execute .jci/run.sh and store results for this commit

# 4. View results locally (opens at http://localhost:8000)
git jci web

# 5. Share results with teammates
git jci push                # push CI refs to origin
git jci pull                # fetch CI refs pushed by teammates

# 6. Maintenance
git jci prune               # dry-run: show what would be removed
git jci prune --commit      # actually delete CI refs for gone commits
git jci prune --commit --older-than=14d   # also delete refs older than 14 days

# 7. Cron
git jci cron ls             # list scheduled jobs in .jci/crontab
git jci cron sync           # install/update entries in the system crontab

You can also trigger git jci run automatically via git hooks so CI runs on every commit without any manual step.

Distributed CI (server + runner)

git jci server and git jci runner add webhook-driven, distributed CI on top of the local workflow. The server is a thin coordination layer; all actual CI work runs inside runner-managed Docker containers.

Gitea ──webhook──▶ jci server ◀──poll── jci runner (Docker / systemd)
                       │                      │
                       │  Gitea API           │  docker socket
                       ▼                      ▼
                    Gitea                 job container
                  (set status)           (git-jci run → git-jci push)

git jci server

Runs in the foreground (managed by Docker or systemd). Receives Gitea webhooks, queues jobs in a local SQLite database, and hands them to runners on demand.

Environment variables

Variable Description
GITEA_HOST Gitea hostname (e.g. gitea.example.com)
GITEA_USER Service account username
GITEA_TOKEN Token with read access to the target repos
GITEA_WEBHOOK_SECRET HMAC secret shared with the Gitea webhook
JCI_RUNNER_SECRET Shared secret runners must present when polling
JCI_MAX_JOBS Max concurrent jobs per runner (default: 4)
JCI_JOB_TIMEOUT Server-wide job timeout (default: 60m)

Endpoints

Method + path Description
POST /webhook Receives Gitea push events; verifies HMAC, enqueues job
POST /poll Runner poll: returns next job or null; auto-registers new runners

On startup the server verifies that GITEA_TOKEN is valid by performing a repo search. If the check fails it exits immediately with a clear error.

Note — per-job scoped tokens: The architecture calls for creating a short-lived, repo-scoped token per job (so that each job container only has access to one repo for the duration of that job). This is not yet implemented because the relevant Gitea API endpoints are still unstable. For now the server embeds the long-lived GITEA_TOKEN directly in the clone URL. Per-job token creation and revocation will be added once the Gitea API stabilises.

git jci runner

Runs in the foreground alongside a Docker daemon. Polls the server every 5 s (+ 0–2 s random jitter) and launches one Docker container per job.

Environment variables

Variable Description
JCI_SERVER Server URL (e.g. https://jci.example.com)
JCI_RUNNER_SECRET Shared secret matching JCI_RUNNER_SECRET on the server

The runner stores its identity in a SQLite database on a mounted volume so the same runner ID is reused across restarts.

Job execution — for each job the runner:

  1. Starts a detached Docker container that does a full clone, checks out the target commit, runs git jci run, then git jci push (always, even on failure, to commit status.txt).
  2. Returns to the poll loop immediately — containers run in the background.
  3. Containers are not started with --rm. This is intentional: exited containers remain on the host so you can run docker logs <id> or docker cp to investigate a failed job. Clean them up yourself with docker container prune. Containers that are still running past the job timeout (default 60m) are force-removed by the runner’s reaper goroutine, which checks every 60 seconds.

The job container never contacts the server directly; all Gitea API interaction happens server-side.

End-to-end flow

1.  Dev pushes → Gitea sends webhook → server enqueues job (status=pending)
2.  Runner polls → server responds with job payload (clone URL embeds GITEA_TOKEN)
3.  Runner starts job container (full clone → git checkout → git jci run → git jci push)
4.  Runner polls again; server reads status.txt from refs/jci-runs/* on Gitea
5.  Server sees ok/err → sets Gitea commit status → removes job

Design constraints


How it works

CI results are stored as git tree objects under the refs/jci-runs/ namespace. Every run — whether triggered manually, by cron, or by a distributed runner — gets a unique run ID, so multiple runs on the same commit are stored independently. This keeps them completely separate from your regular branches and tags while still being part of the repository.

Viewing results

git jci web starts a local web server at http://localhost:8000. It shows all commits on your branches, their CI status, and lets you browse and download artifacts. Clicking a commit shows its output files in a three-panel layout:

git jci web screenshot

You can pass a custom port: git jci web 9000.

Sharing results with teammates

# You: after running CI
git jci push              # pushes refs/jci-runs/* to origin

# Teammate: to see your results
git jci pull              # fetches refs/jci-runs/* from origin
git jci web               # now shows your CI results too

CI refs are pushed to and pulled from origin by default. You can specify a different remote: git jci push my-remote.

FAQ / Needs / Wants / Todos