- Go 84%
- Nix 9%
- CSS 4%
- HTML 2%
- JavaScript 1%
ci #
This is my personal Tangled-compatible CI/CD engine and status board. It integrates with Tangled's public runners and runs my private CD tasks, showing an overall status board that reflects public workflows on Tangled and those it runs on its own engine.
It has a few interesting quirks (features) that I like:
- Steps lay out the happy path through your CI (test, then lint, then whatever)
- Any step can change the flow by returning a structured JSON response that indicates a different action to take
- All the actions any step can take are listed in the workflow file itself
- Actions may be gated so that a human must push the button in the CI interface before they execute
- Actions can be grouped so that one holds an exclusive lock and cannot be clobbered by a subsequent push while it's working
The way I use it:
- Services run their test suites on Tangled public spindles
- Those runs show up in the
cidashboard - If everything looks good, I trigger a private repo run for the NixOS configuration of my servers, which runs tests, diffs config output, then waits for the "gate" (me reviewing what's going to happen and pushing the deploy button)
Writing a workflow #
A workflow lives at .tangled/workflows/<name>.yml:
when:
- event: push
branch: main
engine: devenv
# the happy path: each step names an action
steps: [test, plan, deploy]
# everything the workflow can do; results may route only to these
actions:
test: { command: go test ./... }
plan: { command: just plan }
deploy:
command: just deploy
gate: { prompt: "Deploy to production?" } # waits for you to press the button
concurrency: fleet # one deploy at a time
rollback:
command: just rollback
style: danger
manual: true # a button on the run page
gate: true
A push to main runs test, then plan, then waits at the gate before
deploy. Any action can change course by writing a result, e.g.
ci-result fail --next rollback from a health check. ci validate checks the
file and ci run deploy runs it locally, gates and all.
The reference is in docs/:
- Workflow files: triggers, extra repos, steps, every action field, examples
- Results and routing: the result file,
ci-result, how a run moves and ends - Action environment: the
CI_*andTANGLED_*variables - Running workflows:
ci run,ci resume, rehearsing with--untiland--stub, exit codes
The reasoning behind the design is in DESIGN.md.
Trying the appview #
To see the appview with something in it:
devenv up -d # bridge, runner and appview on 127.0.0.1:8475-8477
devenv tasks run ci:seed # pushes a demo commit; its run waits at "Deploy demo?"
# open http://127.0.0.1:8477 and sign in with the password "dev"
devenv processes logs runner # or: appview, bridge
devenv processes down
ci:seed creates a demo repo on a directory "forge" (.devenv/state/ci-dev) with a post-receive hook for the bridge, so a push starts a gated run. Run it again for another run. Production has the same hook on Gitolite, where a push starts only the workflows with event: [push]; its deploys are manual. Actions use plain bash, not devenv; the dev credentials (dev, an agent token for claude) are fixed strings and only for this. The first start downloads Go modules, so give it a minute.
Security #
ci protects a host from accidents by code you trust; it does not contain code you don't. Every job runs with the runner's secrets, so don't run unreviewed changes on it, and leave outsiders' code to the hosted spindle. See SECURITY.md for the model and where it fits.
License #
MIT, see LICENSE.
Using it yourself #
It doesn't have extensive configuration options. These days I think a better model is to take the parts of the design you like, fork it, hard code your own values in it, and make it your own.
Using the engine in another repo #
Add the flake as a devenv input and put the package on PATH:
# devenv.yaml
inputs:
ci:
url: git+https://knot.rtw.run/rtw.run/ci
# devenv.nix
{ pkgs, inputs, ... }: {
packages = [ inputs.ci.packages.${pkgs.stdenv.system}.ci ];
}