Documentation
Jaypore CI: Minimal, Offline, Local CI system.
- Install
- Config
- Environment Vars
- Example workflow
- Distributed CI (server + runner)
- Design constraints
- How it works
- FAQ / Needs / Wants / Todos
- Examples
- Golang Lint Build Test
- Pylint Pytest Coverage
- Docker Compose Api Tests
- Midnight Build Telegram
- Trufflehog Scan
- Lint Fix Precommit
- Sub Pipelines
- Secrets Telegram
- Mail On Failure
- Auto Update Deps
- Build Publish Docker
- Upstream Trigger
- Downstream Trigger
- Jekyll Netlify
- Docusaurus S3
- With Server And Runner
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.shexits 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_TOKENdirectly 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:
- Starts a detached Docker container that does a full clone, checks out the target
commit, runs
git jci run, thengit jci push(always, even on failure, to commitstatus.txt). - Returns to the poll loop immediately — containers run in the background.
- Containers are not started with
--rm. This is intentional: exited containers remain on the host so you can rundocker logs <id>ordocker cpto investigate a failed job. Clean them up yourself withdocker container prune. Containers that are still running past the job timeout (default60m) 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
- No external dependencies — pure Go stdlib + git CLI + SQLite (driver only for server/runner). No separate storage service.
- Results live in the repo — CI artefacts are normal git objects under
refs/jci-runs/*; they travel with the repo on push/pull. - Runner is pull-only — server never initiates contact with a runner; no runner address is stored.
- All Gitea API interaction is server-only — runner uses only plain
gitcommands (clone/push); the coordination layer is opaque to it. - Stateless job containers — no volumes; crash of a container loses only that run.
- Artefacts are arbitrary files — anything written to
$JCI_OUTPUT_DIRis stored. - Single binary —
git-jciplaced on$PATHbecomes available asgit jci. - Server runs in foreground — managed by Docker or systemd; no self-daemonization.
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.
- Results are never checked out to the working directory
- They are pushed and pulled exactly like any other git refs but only when you explicitly run
git jci push/git jci pull. A plaingit pushdoes not send them. - They are garbage collected when the original commit is gone (via
prune) - Each run produces a
run.output.txtartifact plus whatever else yourrun.shwrites toJCI_OUTPUT_DIR
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:

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
- Complex pipeline definitions
run.shcan be any executable. Do whatever you like!
- Artifacts
- Anything written to
JCI_OUTPUT_DIRis an artifact, browsable viagit jci web.
- Anything written to
- Debug CI locally
- Just run
.jci/run.shdirectly. Set the three env vars yourself to simulate a CI environment.
- Just run
- Automate unit, integration, and end-to-end test suites on every commit
- Run linting and static analysis to enforce coding standards
- Link git hooks and run CI whenever you want.
- Produce code coverage reports and surface regressions
- Write HTML coverage reports to
JCI_OUTPUT_DIRand view them viagit jci web. - For regressions,
run.shcan commit example files created by tools like hypothesis back to the repo. This ensures that next runs will use those examples and test for regressions.
- Write HTML coverage reports to
- Build, package, and archive release artifacts across target platforms
- I like building a docker image, building stuff inside that, then publishing.
- See
.jci/run.shin this repository for a real-world cross-compile + publish example.
- Perform dependency and source code security scans (SCA/SAST)
- I like to run Truffle Hog to prevent accidental leaks.
- Generate documentation sites and preview environments for review
- This Jaypore CI site itself is generated and published via CI.
- Schedule recurring workflows (cron-style) for maintenance tasks
- I run a nightly build via cron to ensure I catch dependency failures / security breaks. See .jci/crontab for an example.
- Notify developers and stakeholders when CI statuses change or regress
- As part of our scripts, we can call telegram / slack / email APIs and inform devs of changes.
- Built-in secrets management with masking, rotation, and per-environment scoping
- I currently use Mozilla SOPS for secrets but this might change in the future.
- Build farms / remote runners on cloud
- Community / marketplace runners contributed by external teams
- Shared runner pools across repositories and organizations
- Deploy keys / scoped access tokens so runners can securely pull & push repos
- Merge request / PR status reporting, required-check gating, and review UIs
- It would be great to have some integration into PRs so that we can know if our colleagues have run CI jobs or not.
- Line-by-line coverage overlays and annotations directly on PR/MR diffs
- This might be hard since it will depend a LOT on which remote is being used. Gitlab uses a cobertura file but others might not.
- Deployment environments with history, approvals, and promotion policies
- First-class integration with observability / error tracking tools (e.g., Sentry)
- Ecosystem of reusable actions/tasks with versioned catalogs and templates
- Validate infrastructure-as-code changes and deployment pipelines via dry runs