engineering blog at https://blog.tangled.sh

spindle launch post

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

anirudh.fi c3ff11a5 c6cd1589

verified
+4 -4
flake.lock
··· 69 69 "nixpkgs": "nixpkgs_2" 70 70 }, 71 71 "locked": { 72 - "lastModified": 1744745948, 73 - "narHash": "sha256-8Sda70LhLVDXChifA52xdaIyCS5Sr7UB2bN0Qe4v4eY=", 72 + "lastModified": 1751977408, 73 + "narHash": "sha256-g9N7+p0LKWdFV/LHEhFX8fJajyzegkxOOhIkTNAicGw=", 74 74 "ref": "refs/heads/master", 75 - "rev": "92c9c9f24b0f0b78c4a81ab2d3490c756813fe71", 76 - "revCount": 89, 75 + "rev": "d3aff34752cd06a62e4177da654a84b93bca4824", 76 + "revCount": 93, 77 77 "type": "git", 78 78 "url": "https://tangled.sh/@icyphox.sh/vite" 79 79 },
+35 -35
input.css
··· 2 2 @tailwind components; 3 3 @tailwind utilities; 4 4 @layer base { 5 - @font-face { 6 - font-family: "InterVariable"; 7 - src: url("/static/fonts/InterVariable.woff2") format("woff2"); 8 - font-weight: normal; 9 - font-style: normal; 10 - font-display: swap; 11 - } 5 + @font-face { 6 + font-family: "InterVariable"; 7 + src: url("/static/fonts/InterVariable.woff2") format("woff2"); 8 + font-weight: normal; 9 + font-style: normal; 10 + font-display: swap; 11 + } 12 12 13 - @font-face { 14 - font-family: "InterVariable"; 15 - src: url("/static/fonts/InterVariable-Italic.woff2") format("woff2"); 16 - font-weight: normal; 17 - font-style: italic; 18 - font-display: swap; 19 - } 13 + @font-face { 14 + font-family: "InterVariable"; 15 + src: url("/static/fonts/InterVariable-Italic.woff2") format("woff2"); 16 + font-weight: 400; 17 + font-style: italic; 18 + font-display: swap; 19 + } 20 20 21 - @font-face { 22 - font-family: "InterDisplay"; 23 - src: url("/static/fonts/InterDisplay-Regular.woff2") format("woff2"); 24 - font-weight: normal; 25 - font-style: normal; 26 - font-display: swap; 27 - } 21 + @font-face { 22 + font-family: "InterVariable"; 23 + src: url("/static/fonts/InterDisplay-SemiBold.woff2") format("woff2"); 24 + font-weight: 600; 25 + font-style: bold; 26 + font-display: swap; 27 + } 28 28 29 - @font-face { 30 - font-family: "IBMPlexMono"; 31 - src: url("/static/fonts/IBMPlexMono-Regular.woff2") format("woff2"); 32 - font-weight: normal; 33 - font-style: italic; 34 - font-display: swap; 35 - } 29 + @font-face { 30 + font-family: "IBMPlexMono"; 31 + src: url("/static/fonts/IBMPlexMono-Regular.woff2") format("woff2"); 32 + font-weight: normal; 33 + font-style: normal; 34 + font-display: swap; 35 + } 36 36 37 - h1 { 37 + h1 { 38 38 @apply text-2xl; 39 39 @apply text-black; 40 40 @apply font-bold; ··· 52 52 } 53 53 @supports (font-variation-settings: normal) { 54 54 html { 55 - font-feature-settings: 56 - "ss01" 1, 57 - "kern" 1, 58 - "liga" 1, 59 - "cv05" 1, 60 - "tnum" 1; 55 + font-feature-settings: 56 + "ss01" 1, 57 + "kern" 1, 58 + "liga" 1, 59 + "cv05" 1, 60 + "tnum" 1; 61 61 } 62 62 } 63 63
+166
pages/blog/ci.md
··· 1 + --- 2 + atroot: true 3 + template: 4 + slug: ci 5 + title: introducing spindle 6 + subtitle: tangled's new CI runner is now generally available 7 + date: 2025-08-06 8 + draft: true 9 + authors: 10 + - name: Anirudh 11 + email: anirudh@tangled.sh 12 + handle: icyphox.sh 13 + --- 14 + 15 + Since launching Tangled, continuous integration has consistently topped our 16 + feature request list. And rightfully so -- modern software development is 17 + unthinkable without automated testing, building, and deployment pipelines. 18 + Today, we're excited to announce that CI is no longer a wishlist item, but a 19 + fully-featured reality. 20 + 21 + Meet **spindle**: Tangled's new CI runner that brings powerful automation 22 + directly to your repositories. In typical Tangled fashion we've been dogfooding 23 + spindle for a while now; this very blog post you're reading was [built and 24 + published using spindle](link to CI run here). 25 + 26 + ![spindle architecture](/static/img/spindle-arch.png) 27 + 28 + ## how spindle works 29 + 30 + Spindle is designed around simplicity and the decentralized nature of the AT 31 + Protocol. Here's the flow: when you push code or open a pull request, the knot 32 + hosting your repository emits a pipeline event (`sh.tangled.pipeline`). Running 33 + as a dedicated service, spindle subscribes to these events via websocket 34 + connections to your knot. 35 + 36 + Once triggered, spindle reads your pipeline manifest, spins up the necessary 37 + execution environment (covered below), and runs your defined workflow steps. 38 + Throughout execution, it streams real-time logs and status updates 39 + (`sh.tangled.pipeline.status`) back through websocktes, which the Tangled 40 + appview subscribes to for live updates. 41 + 42 + This architecture keeps everything responsive and real-time while maintaining 43 + the distributed spirit that makes Tangled unique. 44 + 45 + ## spindle pipelines 46 + 47 + The pipeline manifest is defined in YAML, and should be relatively familiar to 48 + those that have used other CI products. Here's a minimal example: 49 + 50 + ```yaml 51 + # test.yaml 52 + 53 + when: 54 + - event: ["push", "pull_request"] 55 + branch: ["master"] 56 + 57 + dependencies: 58 + nixpkgs: 59 + - go 60 + 61 + steps: 62 + - name: patch static dir 63 + command: | 64 + mkdir -p appview/pages/static; touch appview/pages/static/x 65 + 66 + - name: run all tests 67 + environment: 68 + CGO_ENABLED: 1 69 + command: | 70 + go test -v ./... 71 + ``` 72 + 73 + Manifests are stored under your repo's `.tangled/workflows` directory. There may 74 + be multiple manifests here describing different workflows -- for example, a 75 + `build.yaml`, `test.yaml` and a `lint.yaml`. You can read the [full manifest spec 76 + here](https://tangled.sh/@tangled.sh/core/blob/master/docs/spindle/pipeline.md). 77 + 78 + The `when` block defines the set of events that can trigger the workflow run. 79 + The above example will run tests on any push or pull request targeting the 80 + `master` branch. 81 + 82 + Now the `dependencies` block is the real interesting bit. Dependencies for your 83 + workflow, like Go, Node.js, Python etc. can be pulled in from nixpkgs. nixpkgs 84 + -- for the uninitiated -- is a vast collection of packages for the Nix package 85 + manager. Fortunately, you needn't know nor care about Nix to use it! Just head 86 + to https://search.nixos.org to find your package of choice (I'll bet 1€ that 87 + it's there[^1]), toss it in the list and run your build. The Nix-savvy of you 88 + lot will be happy to know that you can toss in custom registries there. 89 + 90 + [^1]: I mean, if it isn't there, it's nowhere. 91 + 92 + Finally, define your steps, neccesary environment variables and commands. 93 + Commands can be multi-lined. Let's take a look at how spindle executes workflow 94 + steps. 95 + 96 + ## workflow execution 97 + 98 + At present, the spindle "engine" supports just the Docker backend[^2]. Podman is 99 + known to work with the Docker socket feature enabled. Each step is run in a 100 + separate container, with the `/tangled/workspace` and `/nix` volumes persisted 101 + across steps. 102 + 103 + [^2]: Support for additional backends like Firecracker are planned. 104 + Contributions welcome! 105 + 106 + The container image is built using [Nixery](https://nixery.dev). Nixery is a 107 + nifty little tool that takes a path-separated set of Nix packages and returns an 108 + OCI image with each package in a separate layer. Try this in your terminal if 109 + you've got Docker installed: 110 + 111 + ``` 112 + docker run nixery.dev/bash/hello-go hello 113 + ``` 114 + 115 + This should output `Hello, world!`. This is running the 116 + [hello-go](https://search.nixos.org/packages?channel=25.05&show=hello-go) 117 + package from nixpkgs. 118 + 119 + Nixery is super handy since we can construct these images for CI environments on 120 + the fly, with all dependencies baked in, and the best part: caching for commonly 121 + used packages is free thanks to Docker (pre-existing layers get reused). We run 122 + a Nixery instance of our own at https://nixery.tangled.sh but you may override 123 + that if you choose not to trust us. 124 + 125 + ## pipeline statuses and log streaming 126 + 127 + Now that your workflow is running, watching it run to completion is half the 128 + fun! Or watching it fail inexplicably for the hundredth time... which is 129 + decidedly unfun. In any case, logs and pipeline statuses are streamed websockets 130 + exposed by spindle, and show up in the brand new "pipelines" tab in your 131 + repository. 132 + 133 + ![pipeline logs](/static/img/pipeline-logs.png) 134 + 135 + ## pipeline secrets 136 + 137 + Secrets are a bit tricky since atproto has no notion of private data. Secrets 138 + are instead written directly from the appview to the spindle instance using 139 + [service 140 + auth](https://docs.bsky.app/docs/api/com-atproto-server-get-service-auth). In 141 + essence, the appview makes a signed request using the logged-in user's DID key; 142 + spindle verifies this signature by fetching the public key from the DID 143 + document. 144 + 145 + ![pipeline secrets](/static/img/pipeline-secrets.png) 146 + 147 + The secrets themselves are stored in a configured secret manager. By default, 148 + this is the same sqlite database that spindle uses. This is *fine* for 149 + self-hosters. The hosted, flagship instance at https://spindle.tangled.sh 150 + however uses [OpenBao](https://openbao.org), an OSS fork of HashiCorp Vault. 151 + 152 + ## get started now 153 + 154 + You can run your own spindle instance pretty easily: the [spindle self-hosting 155 + guide](https://tangled.sh/@tangled.sh/core/blob/master/docs/spindle/hosting.md) 156 + should have you covered. Once done, head to your repository's settings tab and 157 + set it up! Doesn't work? Feel free to pop into 158 + [Discord](https://chat.tangled.sh) to get help -- we have a nice little crew 159 + that's always around to help. 160 + 161 + All Tangled users have access to our hosted spindle instance, free of 162 + charge[^3]. You don't have any more excuses to not migrate to Tangled now -- 163 + [get started](https://tangled.sh/login) with your AT Protocol account today. 164 + 165 + [^3]: We can't promise we won't charge for it at some point but there will 166 + always be a free tier.
static/img/pipeline-logs.png

This is a binary file and will not be displayed.

static/img/pipeline-secrets.png

This is a binary file and will not be displayed.

static/img/spindle-arch.png

This is a binary file and will not be displayed.