# Crank CI

A pure-Python CI system with zero external dependencies. Runs job scripts, tracks results in SQLite, and works locally or over SSH.

## Components

| Binary | Role |
|--------|------|
| `crankd` | Daemon — Unix socket server, thread pool executor, SQLite storage |
| `crankc` | Client — sends trigger/queue/abort commands to the daemon |
| `crank-ctl` | CLI — full management interface (local or SSH) |

## Install

```bash
# From PyPI
pip install crank-ci
# or
pipx install crank-ci

# From source
pip install .
```

## Quick Start

```bash
# Initialize (creates dirs, systemd user service, starts daemon)
crank-ctl init

# Or initialize on a remote host
crank-ctl -r user@host init

# Create and run a job
crank-ctl create my-deploy
crank-ctl edit my-deploy
crank-ctl trigger my-deploy BRANCH=main
```

No root required — runs as a systemd user service via `systemctl --user`.

## Usage

```bash
# Set remote once (or pass -r each time, or omit for local)
export CRANK_REMOTE=user@host

# Job management
crank-ctl list                          # All jobs with last run status
crank-ctl status                        # Currently running/queued jobs
crank-ctl trigger <job> [KEY=VAL...]    # Run a job (blocking)
crank-ctl trigger <job> -q              # Queue a job (non-blocking)
crank-ctl abort <job>                   # Kill a running/queued build
crank-ctl retry <job>                   # Re-run the last failed build (replays env vars)
crank-ctl wait <job> [-T 300]           # Wait for completion (optional timeout)
crank-ctl tail <job>                    # Stream live output

# History and diagnostics
crank-ctl logs <job> [-n 1]             # Output from recent runs
crank-ctl history <job> [-n 10]         # Pass/fail timeline
crank-ctl stats <job>                   # Success rate, duration min/avg/max

# Job scripts
crank-ctl cat <job>                     # Print job script
crank-ctl edit <job>                    # Open in $EDITOR
crank-ctl create <job> [-t template]    # Scaffold a new job
crank-ctl delete <job> -y               # Delete a job
crank-ctl disable <job>                 # Disable without deleting
crank-ctl enable <job>                  # Re-enable

# Configuration and scheduling
crank-ctl config <job> [-t env] [-e]    # View/edit .conf/.env/.before/.after
crank-ctl schedule                      # List jobs with cron schedules
crank-ctl diff <job> [<job2>] [-l file] # Compare two jobs, or job vs local file
crank-ctl export [-o backup.tar.gz]     # Backup all configs

# All query commands support --json for machine-readable output
```

## How It Works

**Jobs** are executable scripts at `$CRANK_HOME/cfg/jobs/<name>.run`. Each run gets environment variables `$JOB`, `$RUN`, `$WORKSPACE`, plus any `KEY=VAL` pairs passed at trigger time.

**The daemon** (`crankd`) listens on a Unix domain socket and dispatches jobs to a thread pool. Output is streamed to log files in real-time and stored in SQLite. A 10MB memory cap (`MAX_OUTPUT`) prevents runaway jobs from crashing the daemon (full output is always available in the log file). A built-in scheduler thread runs cron-scheduled jobs, and pipeline chaining triggers downstream jobs on success or failure. Connections are capped at 64 concurrent clients.

**The CLI** (`crank-ctl`) queries SQLite (via Python snippets executed on the target) and delegates job triggers to `crankc`. Everything works identically over SSH — just set `$CRANK_REMOTE` or pass `-r user@host`.

## File Layout

```
~/.local/share/crank/           # $CRANK_HOME (default)
├── crank.conf                  # Daemon config (key=value, inline # comments OK)
├── cfg/
│   └── jobs/
│       ├── my-job.run          # Job script (executable)
│       ├── my-job.conf         # Job config (optional)
│       ├── my-job.env          # Job env file (optional)
│       ├── my-job.before       # Pre-run hook (optional)
│       └── my-job.after        # Post-run hook (optional)
├── run/
│   └── my-job/
│       └── 1/
│           └── log             # Build output (streamed live)
├── my-job.last                 # Last result state file
├── crank.sqlite                # Build history database
└── crankd.sock                 # Daemon socket
```

## Configuration

| Variable | Default | Description |
|----------|---------|-------------|
| `CRANK_HOME` | `~/.local/share/crank` | Data directory |
| `CRANK_REMOTE` | *(none)* | SSH target for CLI (`user@host`) |
| `CRANK_NUM_EXECUTORS` | `2` | Worker thread count |

Config file: `$CRANK_HOME/crank.conf` (key=value format, inline `#` comments supported). Also loaded by systemd via `EnvironmentFile`.

For system-wide installs, set `CRANK_HOME=/var/lib/crank` and write your own systemd system unit.

### Job Config (`.conf`)

Each job can have an optional `.conf` file at `$CRANK_HOME/cfg/jobs/<name>.conf`:

```bash
schedule=*/5 * * * *          # Cron schedule (minute hour dom month dow)
timeout=300                   # Kill job after N seconds
on_success=deploy,notify      # Jobs to trigger on success
on_failure=alert              # Jobs to trigger on failure
on_complete=notify.sh         # Script to run after every build
```

Inline `#` comments are stripped from values.

**Hooks:** Jobs can have `.before` and `.after` scripts alongside the `.run` file. The `.before` hook runs first — if it fails, the job is skipped. The `.after` hook runs regardless of result, with `$RESULT` and `$RESULT_NAME` in env.

**Scheduling:** The daemon checks schedules every 30s. Supports `*`, ranges (`1-5`), steps (`*/5`), and lists (`1,3,5`). Jobs already queued/running are skipped. POSIX behavior: when both day-of-month and day-of-week are restricted (neither is `*`), the job runs if either matches (OR logic).

**Pipelines:** Downstream jobs inherit the parent's env vars and are tracked via `parentJob`/`parentBuild` in the database. Visible in `crank-ctl history`.

**Retry with params:** Env vars passed at trigger time are stored in the database. `crank-ctl retry` replays them automatically.

## Init Details

`crank-ctl init` performs:
1. Creates directory structure under `$CRANK_HOME`
2. Installs a systemd user service (`crankd.service`)
3. Enables lingering (`loginctl enable-linger`) so the service survives logout
4. Enables and starts the service

Flags: `--no-start` (skip service start), `--executors N` (set thread count).

## Requirements

Python 3.10+. No external dependencies.

## License

MIT
