+4
-4
flake.lock
+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
+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
+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
+

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
+

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
+

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
static/img/pipeline-logs.png
This is a binary file and will not be displayed.
static/img/pipeline-secrets.png
static/img/pipeline-secrets.png
This is a binary file and will not be displayed.
static/img/spindle-arch.png
static/img/spindle-arch.png
This is a binary file and will not be displayed.