Saturday morning, before coffee, cidx is public. The URL that used to give a 404 to anyone but me now gives back a README. That part, by itself, isn’t the interesting part — flipping a repo’s visibility is one click and a confirmation dialog, no story there. What I want to write down is the thing that struck me a minute later, when I opened the Actions tab on the freshly-public repo and watched the green checks go by.
The CI that says “cidx is healthy” is cidx running cidx.
I’ll explain. cidx is a CI runner. You write a cidx.toml in your repo, you list the phases — security, code, test, build — each phase points at a preset that knows how to run a tool inside a container. Trivy, Gitleaks, golangci-lint, prettier, commitizen, shellcheck, go-test, go-build, kaniko, that kind of thing. You type cidx run ci locally, the containers run in order. You also push to GitHub, and the workflow there runs the same containers in the same order with the same parameters. The local check and the CI check are not “morally” the same; they are literally the same commands. That’s the load-bearing claim of the project.
The thing that made me sit there an extra minute this morning is that the load-bearing claim is, on the cidx repo itself, the load-bearing thing. The repo’s .github/workflows/ci.yml is fifteen lines of orchestration around four steps, each step a single command:
- name: Run security checks
run: ./bin/cidx run security
- name: Run code quality checks
run: ./bin/cidx run code
- name: Run tests
run: ./bin/cidx run test
- name: Build final binary
run: ./bin/cidx run build
That’s the CI. The bootstrap job builds the cidx binary from source, uploads it as an artifact, and every subsequent job downloads it and uses it to run the phase. The phases themselves — what containers run, in what order, with what severity thresholds — live in cidx.toml, in the same repo, two directories away from the workflow file. If I want to add a tool to the security phase, I edit one line in cidx.toml, and the next push picks it up locally and in CI at the same time. There’s no second YAML to keep in sync. The drift problem I built cidx to solve, cidx solved for cidx, the moment I switched the repo to using itself.
The release pipeline goes one step further. When I push a v* tag, the release workflow calls ./bin/cidx run docker to build the container image with kaniko, then ships the GitHub release. The tag itself was created by cidx action release create, which analyzes the conventional commits since the last tag, bumps the version, writes the bump commit, and pushes. The PR that landed the previous fix was opened with cidx action pr create, watched with cidx action pr watch, merged with cidx action pr merge. The whole life cycle of a change — commit, PR, review, merge, release — is the thing I was building, used on itself, until the only manual step left is deciding what to ship.
Look, I want to be honest about what this is and isn’t.
It’s not “cidx generates its own GitHub Actions workflow.” It could — cidx generate github exists and produces a workflow that runs cidx run for each phase — but the file in the repo right now is hand-written. It’s a thin wrapper, fifteen lines per job, and the heavy lifting is inside cidx run. The wrapper exists because GitHub Actions has its own concerns — caching, artifact upload, checkout depth, registry login — that aren’t the runner’s job. The runner runs the tools. The wrapper handles the platform.
That distinction matters because it’s where most “self-hosting” tool stories oversell themselves. A tool that “runs on itself” sometimes means “the tool generated its own config and the config has been hand-edited fifty times since.” With cidx the line is cleaner: the configuration of what runs is in cidx.toml and is the source of truth, the execution of what runs is cidx run, and the platform glue that gets the binary onto a GitHub runner is fifteen lines of YAML I’m comfortable maintaining. Three layers, each doing one thing.
It’s also not the case that this proves cidx works. It proves cidx works for cidx, which is a Go project with a particular shape. A Rust project will exercise different presets, a Python project will hit different edges, a polyglot monorepo will surface things I haven’t thought about. The dogfood is one meal. The proof is in other people’s projects, and that proof I don’t have yet — that’s why I made the repo public. Until other shapes of code go through the same machinery, the claim “two commands integrate any project into CI” is a claim, not a track record.
But for cidx itself, the loop is closed. The tool that builds, tests, scans, and ships cidx, is cidx. The same commands I’d type in the morning at my desk are the commands the GitHub runner types at midnight when a dependabot PR lands. The release I’d cut by hand is the release the tag workflow cuts. The configuration that decides everything is one file, version-controlled with the code that consumes it, and the day I add a check is the day every place that runs cidx gets the new check, with no second migration.
There’s a particular satisfaction to a thing that uses itself without it being a stunt. Compilers compiling themselves is the canonical version. Init systems init-ing themselves. Package managers managing their own packages. It’s not always a deep claim about the work — sometimes a tool happens to fit its own use case, and that’s nice but not load-bearing. With cidx I think it is load-bearing, because the whole pitch is “the thing you run locally is the thing CI runs,” and the strongest test of that pitch is: do I trust it to be the thing my own project’s CI runs? Right now, on the repo I just made public, the answer is yes, and it has been for months.
You can read the README and decide whether it’s worth a clone. Or you can run cidx init against your own project and find out whether the same trick works for the shape of code you have. The first will tell you what I think it does. The second will tell you whether I’m right.