···11+---
22+atroot: true
33+template:
44+slug: ci
55+title: introducing spindle
66+subtitle: tangled's new CI runner is now generally available
77+date: 2025-08-06
88+draft: true
99+authors:
1010+ - name: Anirudh
1111+ email: anirudh@tangled.sh
1212+ handle: icyphox.sh
1313+---
1414+1515+Since launching Tangled, continuous integration has consistently topped our
1616+feature request list. And rightfully so -- modern software development is
1717+unthinkable without automated testing, building, and deployment pipelines.
1818+Today, we're excited to announce that CI is no longer a wishlist item, but a
1919+fully-featured reality.
2020+2121+Meet **spindle**: Tangled's new CI runner that brings powerful automation
2222+directly to your repositories. In typical Tangled fashion we've been dogfooding
2323+spindle for a while now; this very blog post you're reading was [built and
2424+published using spindle](link to CI run here).
2525+2626+
2727+2828+## how spindle works
2929+3030+Spindle is designed around simplicity and the decentralized nature of the AT
3131+Protocol. Here's the flow: when you push code or open a pull request, the knot
3232+hosting your repository emits a pipeline event (`sh.tangled.pipeline`). Running
3333+as a dedicated service, spindle subscribes to these events via websocket
3434+connections to your knot.
3535+3636+Once triggered, spindle reads your pipeline manifest, spins up the necessary
3737+execution environment (covered below), and runs your defined workflow steps.
3838+Throughout execution, it streams real-time logs and status updates
3939+(`sh.tangled.pipeline.status`) back through websocktes, which the Tangled
4040+appview subscribes to for live updates.
4141+4242+This architecture keeps everything responsive and real-time while maintaining
4343+the distributed spirit that makes Tangled unique.
4444+4545+## spindle pipelines
4646+4747+The pipeline manifest is defined in YAML, and should be relatively familiar to
4848+those that have used other CI products. Here's a minimal example:
4949+5050+```yaml
5151+# test.yaml
5252+5353+when:
5454+ - event: ["push", "pull_request"]
5555+ branch: ["master"]
5656+5757+dependencies:
5858+ nixpkgs:
5959+ - go
6060+6161+steps:
6262+ - name: patch static dir
6363+ command: |
6464+ mkdir -p appview/pages/static; touch appview/pages/static/x
6565+6666+ - name: run all tests
6767+ environment:
6868+ CGO_ENABLED: 1
6969+ command: |
7070+ go test -v ./...
7171+```
7272+7373+Manifests are stored under your repo's `.tangled/workflows` directory. There may
7474+be multiple manifests here describing different workflows -- for example, a
7575+`build.yaml`, `test.yaml` and a `lint.yaml`. You can read the [full manifest spec
7676+here](https://tangled.sh/@tangled.sh/core/blob/master/docs/spindle/pipeline.md).
7777+7878+The `when` block defines the set of events that can trigger the workflow run.
7979+The above example will run tests on any push or pull request targeting the
8080+`master` branch.
8181+8282+Now the `dependencies` block is the real interesting bit. Dependencies for your
8383+workflow, like Go, Node.js, Python etc. can be pulled in from nixpkgs. nixpkgs
8484+-- for the uninitiated -- is a vast collection of packages for the Nix package
8585+manager. Fortunately, you needn't know nor care about Nix to use it! Just head
8686+to https://search.nixos.org to find your package of choice (I'll bet 1€ that
8787+it's there[^1]), toss it in the list and run your build. The Nix-savvy of you
8888+lot will be happy to know that you can toss in custom registries there.
8989+9090+[^1]: I mean, if it isn't there, it's nowhere.
9191+9292+Finally, define your steps, neccesary environment variables and commands.
9393+Commands can be multi-lined. Let's take a look at how spindle executes workflow
9494+steps.
9595+9696+## workflow execution
9797+9898+At present, the spindle "engine" supports just the Docker backend[^2]. Podman is
9999+known to work with the Docker socket feature enabled. Each step is run in a
100100+separate container, with the `/tangled/workspace` and `/nix` volumes persisted
101101+across steps.
102102+103103+[^2]: Support for additional backends like Firecracker are planned.
104104+Contributions welcome!
105105+106106+The container image is built using [Nixery](https://nixery.dev). Nixery is a
107107+nifty little tool that takes a path-separated set of Nix packages and returns an
108108+OCI image with each package in a separate layer. Try this in your terminal if
109109+you've got Docker installed:
110110+111111+```
112112+docker run nixery.dev/bash/hello-go hello
113113+```
114114+115115+This should output `Hello, world!`. This is running the
116116+[hello-go](https://search.nixos.org/packages?channel=25.05&show=hello-go)
117117+package from nixpkgs.
118118+119119+Nixery is super handy since we can construct these images for CI environments on
120120+the fly, with all dependencies baked in, and the best part: caching for commonly
121121+used packages is free thanks to Docker (pre-existing layers get reused). We run
122122+a Nixery instance of our own at https://nixery.tangled.sh but you may override
123123+that if you choose not to trust us.
124124+125125+## pipeline statuses and log streaming
126126+127127+Now that your workflow is running, watching it run to completion is half the
128128+fun! Or watching it fail inexplicably for the hundredth time... which is
129129+decidedly unfun. In any case, logs and pipeline statuses are streamed websockets
130130+exposed by spindle, and show up in the brand new "pipelines" tab in your
131131+repository.
132132+133133+
134134+135135+## pipeline secrets
136136+137137+Secrets are a bit tricky since atproto has no notion of private data. Secrets
138138+are instead written directly from the appview to the spindle instance using
139139+[service
140140+auth](https://docs.bsky.app/docs/api/com-atproto-server-get-service-auth). In
141141+essence, the appview makes a signed request using the logged-in user's DID key;
142142+spindle verifies this signature by fetching the public key from the DID
143143+document.
144144+145145+
146146+147147+The secrets themselves are stored in a configured secret manager. By default,
148148+this is the same sqlite database that spindle uses. This is *fine* for
149149+self-hosters. The hosted, flagship instance at https://spindle.tangled.sh
150150+however uses [OpenBao](https://openbao.org), an OSS fork of HashiCorp Vault.
151151+152152+## get started now
153153+154154+You can run your own spindle instance pretty easily: the [spindle self-hosting
155155+guide](https://tangled.sh/@tangled.sh/core/blob/master/docs/spindle/hosting.md)
156156+should have you covered. Once done, head to your repository's settings tab and
157157+set it up! Doesn't work? Feel free to pop into
158158+[Discord](https://chat.tangled.sh) to get help -- we have a nice little crew
159159+that's always around to help.
160160+161161+All Tangled users have access to our hosted spindle instance, free of
162162+charge[^3]. You don't have any more excuses to not migrate to Tangled now --
163163+[get started](https://tangled.sh/login) with your AT Protocol account today.
164164+165165+[^3]: We can't promise we won't charge for it at some point but there will
166166+ always be a free tier.