Monorepo for Tangled
at docs/strings 1870 lines 54 kB view raw view rendered
1--- 2title: Tangled docs 3author: The Tangled Contributors 4date: 21 Sun, Dec 2025 5abstract: | 6 Tangled is a decentralized code hosting and collaboration 7 platform. Every component of Tangled is open-source and 8 self-hostable. [tangled.org](https://tangled.org) also 9 provides hosting and CI services that are free to use. 10 11 There are several models for decentralized code 12 collaboration platforms, ranging from ActivityPub’s 13 (Forgejo) federated model, to Radicle’s entirely P2P model. 14 Our approach attempts to be the best of both worlds by 15 adopting the AT Protocol—a protocol for building decentralized 16 social applications with a central identity 17 18 Our approach to this is the idea of “knots”. Knots are 19 lightweight, headless servers that enable users to host Git 20 repositories with ease. Knots are designed for either single 21 or multi-tenant use which is perfect for self-hosting on a 22 Raspberry Pi at home, or larger “community” servers. By 23 default, Tangled provides managed knots where you can host 24 your repositories for free. 25 26 The appview at tangled.org acts as a consolidated "view" 27 into the whole network, allowing users to access, clone and 28 contribute to repositories hosted across different knots 29 seamlessly. 30--- 31 32# Quick start guide 33 34## Login or sign up 35 36You can [login](https://tangled.org) by using your AT Protocol 37account. If you are unclear on what that means, simply head 38to the [signup](https://tangled.org/signup) page and create 39an account. By doing so, you will be choosing Tangled as 40your account provider (you will be granted a handle of the 41form `user.tngl.sh`). 42 43In the AT Protocol network, users are free to choose their account 44provider (known as a "Personal Data Service", or PDS), and 45login to applications that support AT accounts. 46 47You can think of it as "one account for all of the atmosphere"! 48 49If you already have an AT account (you may have one if you 50signed up to Bluesky, for example), you can login with the 51same handle on Tangled (so just use `user.bsky.social` on 52the login page). 53 54## Add an SSH key 55 56Once you are logged in, you can start creating repositories 57and pushing code. Tangled supports pushing git repositories 58over SSH. 59 60First, you'll need to generate an SSH key if you don't 61already have one: 62 63```bash 64ssh-keygen -t ed25519 -C "foo@bar.com" 65``` 66 67When prompted, save the key to the default location 68(`~/.ssh/id_ed25519`) and optionally set a passphrase. 69 70Copy your public key to your clipboard: 71 72```bash 73# on X11 74cat ~/.ssh/id_ed25519.pub | xclip -sel c 75 76# on wayland 77cat ~/.ssh/id_ed25519.pub | wl-copy 78 79# on macos 80cat ~/.ssh/id_ed25519.pub | pbcopy 81``` 82 83Now, navigate to 'Settings' -> 'Keys' and hit 'Add Key', 84paste your public key, give it a descriptive name, and hit 85save. 86 87## Create a repository 88 89Once your SSH key is added, create your first repository: 90 911. Hit the green `+` icon on the topbar, and select 92 repository 932. Enter a repository name 943. Add a description 954. Choose a knotserver to host this repository on 965. Hit create 97 98Knots are self-hostable, lightweight Git servers that can 99host your repository. Unlike traditional code forges, your 100code can live on any server. Read the [Knots](TODO) section 101for more. 102 103## Configure SSH 104 105To ensure Git uses the correct SSH key and connects smoothly 106to Tangled, add this configuration to your `~/.ssh/config` 107file: 108 109``` 110Host tangled.org 111 Hostname tangled.org 112 User git 113 IdentityFile ~/.ssh/id_ed25519 114 AddressFamily inet 115``` 116 117This tells SSH to use your specific key when connecting to 118Tangled and prevents authentication issues if you have 119multiple SSH keys. 120 121Note that this configuration only works for knotservers that 122are hosted by tangled.org. If you use a custom knot, refer 123to the [Knots](TODO) section. 124 125## Push your first repository 126 127Initialize a new Git repository: 128 129```bash 130mkdir my-project 131cd my-project 132 133git init 134echo "# My Project" > README.md 135``` 136 137Add some content and push! 138 139```bash 140git add README.md 141git commit -m "Initial commit" 142git remote add origin git@tangled.org:user.tngl.sh/my-project 143git push -u origin main 144``` 145 146That's it! Your code is now hosted on Tangled. 147 148## Migrating an existing repository 149 150Moving your repositories from GitHub, GitLab, Bitbucket, or 151any other Git forge to Tangled is straightforward. You'll 152simply change your repository's remote URL. At the moment, 153Tangled does not have any tooling to migrate data such as 154GitHub issues or pull requests. 155 156First, create a new repository on tangled.org as described 157in the [Quick Start Guide](#create-a-repository). 158 159Navigate to your existing local repository: 160 161```bash 162cd /path/to/your/existing/repo 163``` 164 165You can inspect your existing Git remote like so: 166 167```bash 168git remote -v 169``` 170 171You'll see something like: 172 173``` 174origin git@github.com:username/my-project (fetch) 175origin git@github.com:username/my-project (push) 176``` 177 178Update the remote URL to point to tangled: 179 180```bash 181git remote set-url origin git@tangled.org:user.tngl.sh/my-project 182``` 183 184Verify the change: 185 186```bash 187git remote -v 188``` 189 190You should now see: 191 192``` 193origin git@tangled.org:user.tngl.sh/my-project (fetch) 194origin git@tangled.org:user.tngl.sh/my-project (push) 195``` 196 197Push all your branches and tags to Tangled: 198 199```bash 200git push -u origin --all 201git push -u origin --tags 202``` 203 204Your repository is now migrated to Tangled! All commit 205history, branches, and tags have been preserved. 206 207## Mirroring a repository to Tangled 208 209If you want to maintain your repository on multiple forges 210simultaneously, for example, keeping your primary repository 211on GitHub while mirroring to Tangled for backup or 212redundancy, you can do so by adding multiple remotes. 213 214You can configure your local repository to push to both 215Tangled and, say, GitHub. You may already have the following 216setup: 217 218``` 219$ git remote -v 220origin git@github.com:username/my-project (fetch) 221origin git@github.com:username/my-project (push) 222``` 223 224Now add Tangled as an additional push URL to the same 225remote: 226 227```bash 228git remote set-url --add --push origin git@tangled.org:user.tngl.sh/my-project 229``` 230 231You also need to re-add the original URL as a push 232destination (Git replaces the push URL when you use `--add` 233the first time): 234 235```bash 236git remote set-url --add --push origin git@github.com:username/my-project 237``` 238 239Verify your configuration: 240 241``` 242$ git remote -v 243origin git@github.com:username/repo (fetch) 244origin git@tangled.org:username/my-project (push) 245origin git@github.com:username/repo (push) 246``` 247 248Notice that there's one fetch URL (the primary remote) and 249two push URLs. Now, whenever you push, Git will 250automatically push to both remotes: 251 252```bash 253git push origin main 254``` 255 256This single command pushes your `main` branch to both GitHub 257and Tangled simultaneously. 258 259To push all branches and tags: 260 261```bash 262git push origin --all 263git push origin --tags 264``` 265 266If you prefer more control over which remote you push to, 267you can maintain separate remotes: 268 269```bash 270git remote add github git@github.com:username/my-project 271git remote add tangled git@tangled.org:username/my-project 272``` 273 274Then push to each explicitly: 275 276```bash 277git push github main 278git push tangled main 279``` 280 281# Using Tangled Strings 282 283Tangled Strings are text snippets you can create, share, and store on 284Tangled. They live on your AT Protocol identity (your PDS), so you own 285your data and can share links that others can view, star, and access in 286raw form. 287 288## What are Strings for? 289 290Strings are useful for: 291 292- **Code snippets** – Share a small script, config, or example without 293 creating a full repo 294- **Pastebin-style sharing** – Quick sharing of logs, error output, or text 295- **Notes and docs** – Short notes, recipes, or how-tos you want to share 296 publicly 297- **Markdown content** – Use a `.md` filename and your string will render 298 as Markdown 299 300Each string has a filename, optional description, and contents. You can 301edit and delete your strings anytime. 302 303## Finding Strings 304 305- **All strings** – Visit [tangled.org/strings](https://tangled.org/strings) 306 to see a timeline of recent strings from all users 307- **Your strings** – Go to your profile and open the **Strings** tab, or 308 visit `tangled.org/{your-handle}?tab=strings` 309- **Someone else's strings** – Open their profile and click the **Strings** 310 tab 311 312## Creating a String 313 3141. Log in to [tangled.org](https://tangled.org) 3152. Go to [/strings/new](https://tangled.org/strings/new) 3163. Fill in: 317 - **Filename** – A short name (e.g. `hello.sh`, `notes.md`, `config.json`) 318 - **Description** – Optional; helps others understand what the string is 319 - **Contents** – Your text or code 3204. Click **Publish** 321 322Your string gets a unique URL like 323`tangled.org/strings/user.tngl.sh/3abc123xyz` that you can share. 324 325## Viewing a String 326 327- **Rendered** – By default, Markdown files (`.md`) are rendered. Use the 328 **view rendered** / **view code** toggle to switch. 329- **Raw** – Click **view raw** to get plain text, useful for copying or 330 `curl`-ing. 331- **Stars** – You can star strings you find useful; they appear in your 332 starred list. 333 334## Editing and Deleting 335 336- **Edit** – If you own the string, use the **edit** button to change the 337 filename, description, or contents. 338- **Delete** – Use the **delete** button to remove the string. This action 339 cannot be undone. 340 341# Knot self-hosting guide 342 343So you want to run your own knot server? Great! Here are a few prerequisites: 344 3451. A server of some kind (a VPS, a Raspberry Pi, etc.). Preferably running a Linux distribution of some kind. 3462. A (sub)domain name. People generally use `knot.example.com`. 3473. A valid SSL certificate for your domain. 348 349## NixOS 350 351Refer to the [knot 352module](https://tangled.org/tangled.org/core/blob/master/nix/modules/knot.nix) 353for a full list of options. Sample configurations: 354 355- [The test VM](https://tangled.org/tangled.org/core/blob/master/nix/vm.nix#L85) 356- [@pyrox.dev/nix](https://tangled.org/pyrox.dev/nix/blob/d19571cc1b5fe01035e1e6951ec8cf8a476b4dee/hosts/marvin/services/tangled.nix#L15-25) 357 358## Docker 359 360Refer to 361[@tangled.org/knot-docker](https://tangled.org/@tangled.org/knot-docker). 362Note that this is community maintained. 363 364## Manual setup 365 366First, clone this repository: 367 368``` 369git clone https://tangled.org/@tangled.org/core 370``` 371 372Then, build the `knot` CLI. This is the knot administration 373and operation tool. For the purpose of this guide, we're 374only concerned with these subcommands: 375 376- `knot server`: the main knot server process, typically 377 run as a supervised service 378- `knot guard`: handles role-based access control for git 379 over SSH (you'll never have to run this yourself) 380- `knot keys`: fetches SSH keys associated with your knot; 381 we'll use this to generate the SSH 382 `AuthorizedKeysCommand` 383 384``` 385cd core 386export CGO_ENABLED=1 387go build -o knot ./cmd/knot 388``` 389 390Next, move the `knot` binary to a location owned by `root` -- 391`/usr/local/bin/` is a good choice. Make sure the binary itself is also owned by `root`: 392 393``` 394sudo mv knot /usr/local/bin/knot 395sudo chown root:root /usr/local/bin/knot 396``` 397 398This is necessary because SSH `AuthorizedKeysCommand` requires [really 399specific permissions](https://stackoverflow.com/a/27638306). The 400`AuthorizedKeysCommand` specifies a command that is run by `sshd` to 401retrieve a user's public SSH keys dynamically for authentication. Let's 402set that up. 403 404``` 405sudo tee /etc/ssh/sshd_config.d/authorized_keys_command.conf <<EOF 406Match User git 407 AuthorizedKeysCommand /usr/local/bin/knot keys -o authorized-keys 408 AuthorizedKeysCommandUser nobody 409EOF 410``` 411 412Then, reload `sshd`: 413 414``` 415sudo systemctl reload ssh 416``` 417 418Next, create the `git` user. We'll use the `git` user's home directory 419to store repositories: 420 421``` 422sudo adduser git 423``` 424 425Create `/home/git/.knot.env` with the following, updating the values as 426necessary. The `KNOT_SERVER_OWNER` should be set to your 427DID, you can find your DID in the [Settings](https://tangled.sh/settings) page. 428 429``` 430KNOT_REPO_SCAN_PATH=/home/git 431KNOT_SERVER_HOSTNAME=knot.example.com 432APPVIEW_ENDPOINT=https://tangled.org 433KNOT_SERVER_OWNER=did:plc:foobar 434KNOT_SERVER_INTERNAL_LISTEN_ADDR=127.0.0.1:5444 435KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555 436``` 437 438If you run a Linux distribution that uses systemd, you can 439use the provided service file to run the server. Copy 440[`knotserver.service`](https://tangled.org/tangled.org/core/blob/master/systemd/knotserver.service) 441to `/etc/systemd/system/`. Then, run: 442 443``` 444systemctl enable knotserver 445systemctl start knotserver 446``` 447 448The last step is to configure a reverse proxy like Nginx or Caddy to front your 449knot. Here's an example configuration for Nginx: 450 451``` 452server { 453 listen 80; 454 listen [::]:80; 455 server_name knot.example.com; 456 457 location / { 458 proxy_pass http://localhost:5555; 459 proxy_set_header Host $host; 460 proxy_set_header X-Real-IP $remote_addr; 461 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 462 proxy_set_header X-Forwarded-Proto $scheme; 463 } 464 465 # wss endpoint for git events 466 location /events { 467 proxy_set_header X-Forwarded-For $remote_addr; 468 proxy_set_header Host $http_host; 469 proxy_set_header Upgrade websocket; 470 proxy_set_header Connection Upgrade; 471 proxy_pass http://localhost:5555; 472 } 473 # additional config for SSL/TLS go here. 474} 475 476``` 477 478Remember to use Let's Encrypt or similar to procure a certificate for your 479knot domain. 480 481You should now have a running knot server! You can finalize 482your registration by hitting the `verify` button on the 483[/settings/knots](https://tangled.org/settings/knots) page. This simply creates 484a record on your PDS to announce the existence of the knot. 485 486### Custom paths 487 488(This section applies to manual setup only. Docker users should edit the mounts 489in `docker-compose.yml` instead.) 490 491Right now, the database and repositories of your knot lives in `/home/git`. You 492can move these paths if you'd like to store them in another folder. Be careful 493when adjusting these paths: 494 495- Stop your knot when moving data (e.g. `systemctl stop knotserver`) to prevent 496 any possible side effects. Remember to restart it once you're done. 497- Make backups before moving in case something goes wrong. 498- Make sure the `git` user can read and write from the new paths. 499 500#### Database 501 502As an example, let's say the current database is at `/home/git/knotserver.db`, 503and we want to move it to `/home/git/database/knotserver.db`. 504 505Copy the current database to the new location. Make sure to copy the `.db-shm` 506and `.db-wal` files if they exist. 507 508``` 509mkdir /home/git/database 510cp /home/git/knotserver.db* /home/git/database 511``` 512 513In the environment (e.g. `/home/git/.knot.env`), set `KNOT_SERVER_DB_PATH` to 514the new file path (_not_ the directory): 515 516``` 517KNOT_SERVER_DB_PATH=/home/git/database/knotserver.db 518``` 519 520#### Repositories 521 522As an example, let's say the repositories are currently in `/home/git`, and we 523want to move them into `/home/git/repositories`. 524 525Create the new folder, then move the existing repositories (if there are any): 526 527``` 528mkdir /home/git/repositories 529# move all DIDs into the new folder; these will vary for you! 530mv /home/git/did:plc:wshs7t2adsemcrrd4snkeqli /home/git/repositories 531``` 532 533In the environment (e.g. `/home/git/.knot.env`), update `KNOT_REPO_SCAN_PATH` 534to the new directory: 535 536``` 537KNOT_REPO_SCAN_PATH=/home/git/repositories 538``` 539 540Similarly, update your `sshd` `AuthorizedKeysCommand` to use the updated 541repository path: 542 543``` 544sudo tee /etc/ssh/sshd_config.d/authorized_keys_command.conf <<EOF 545Match User git 546 AuthorizedKeysCommand /usr/local/bin/knot keys -o authorized-keys -git-dir /home/git/repositories 547 AuthorizedKeysCommandUser nobody 548EOF 549``` 550 551Make sure to restart your SSH server! 552 553#### MOTD (message of the day) 554 555To configure the MOTD used ("Welcome to this knot!" by default), edit the 556`/home/git/motd` file: 557 558``` 559printf "Hi from this knot!\n" > /home/git/motd 560``` 561 562Note that you should add a newline at the end if setting a non-empty message 563since the knot won't do this for you. 564 565## Troubleshooting 566 567If you run your own knot, you may run into some of these 568common issues. You can always join the 569[IRC](https://web.libera.chat/#tangled) or 570[Discord](https://chat.tangled.org/) if this section does 571not help. 572 573### Unable to push 574 575If you are unable to push to your knot or repository: 576 5771. First, ensure that you have added your SSH public key to 578 your account 5792. Check to see that your knot has synced the key by running 580 `knot keys` 5813. Check to see if git is supplying the correct private key 582 when pushing: `GIT_SSH_COMMAND="ssh -v" git push ...` 5834. Check to see if `sshd` on the knot is rejecting the push 584 for some reason: `journalctl -xeu ssh` (or `sshd`, 585 depending on your machine). These logs are unavailable if 586 using docker. 5875. Check to see if the knot itself is rejecting the push, 588 depending on your setup, the logs might be in one of the 589 following paths: 590 - `/tmp/knotguard.log` 591 - `/home/git/log` 592 - `/home/git/guard.log` 593 594# Spindles 595 596## Pipelines 597 598Spindle workflows allow you to write CI/CD pipelines in a 599simple format. They're located in the `.tangled/workflows` 600directory at the root of your repository, and are defined 601using YAML. 602 603The fields are: 604 605- [Trigger](#trigger): A **required** field that defines 606 when a workflow should be triggered. 607- [Engine](#engine): A **required** field that defines which 608 engine a workflow should run on. 609- [Clone options](#clone-options): An **optional** field 610 that defines how the repository should be cloned. 611- [Dependencies](#dependencies): An **optional** field that 612 allows you to list dependencies you may need. 613- [Environment](#environment): An **optional** field that 614 allows you to define environment variables. 615- [Steps](#steps): An **optional** field that allows you to 616 define what steps should run in the workflow. 617 618### Trigger 619 620The first thing to add to a workflow is the trigger, which 621defines when a workflow runs. This is defined using a `when` 622field, which takes in a list of conditions. Each condition 623has the following fields: 624 625- `event`: This is a **required** field that defines when 626 your workflow should run. It's a list that can take one or 627 more of the following values: 628 - `push`: The workflow should run every time a commit is 629 pushed to the repository. 630 - `pull_request`: The workflow should run every time a 631 pull request is made or updated. 632 - `manual`: The workflow can be triggered manually. 633- `branch`: Defines which branches the workflow should run 634 for. If used with the `push` event, commits to the 635 branch(es) listed here will trigger the workflow. If used 636 with the `pull_request` event, updates to pull requests 637 targeting the branch(es) listed here will trigger the 638 workflow. This field has no effect with the `manual` 639 event. Supports glob patterns using `*` and `**` (e.g., 640 `main`, `develop`, `release-*`). Either `branch` or `tag` 641 (or both) must be specified for `push` events. 642- `tag`: Defines which tags the workflow should run for. 643 Only used with the `push` event - when tags matching the 644 pattern(s) listed here are pushed, the workflow will 645 trigger. This field has no effect with `pull_request` or 646 `manual` events. Supports glob patterns using `*` and `**` 647 (e.g., `v*`, `v1.*`, `release-**`). Either `branch` or 648 `tag` (or both) must be specified for `push` events. 649 650For example, if you'd like to define a workflow that runs 651when commits are pushed to the `main` and `develop` 652branches, or when pull requests that target the `main` 653branch are updated, or manually, you can do so with: 654 655```yaml 656when: 657 - event: ["push", "manual"] 658 branch: ["main", "develop"] 659 - event: ["pull_request"] 660 branch: ["main"] 661``` 662 663You can also trigger workflows on tag pushes. For instance, 664to run a deployment workflow when tags matching `v*` are 665pushed: 666 667```yaml 668when: 669 - event: ["push"] 670 tag: ["v*"] 671``` 672 673You can even combine branch and tag patterns in a single 674constraint (the workflow triggers if either matches): 675 676```yaml 677when: 678 - event: ["push"] 679 branch: ["main", "release-*"] 680 tag: ["v*", "stable"] 681``` 682 683### Engine 684 685Next is the engine on which the workflow should run, defined 686using the **required** `engine` field. The currently 687supported engines are: 688 689- `nixery`: This uses an instance of 690 [Nixery](https://nixery.dev) to run steps, which allows 691 you to add [dependencies](#dependencies) from 692 Nixpkgs (https://github.com/NixOS/nixpkgs). You can 693 search for packages on https://search.nixos.org, and 694 there's a pretty good chance the package(s) you're looking 695 for will be there. 696 697Example: 698 699```yaml 700engine: "nixery" 701``` 702 703### Clone options 704 705When a workflow starts, the first step is to clone the 706repository. You can customize this behavior using the 707**optional** `clone` field. It has the following fields: 708 709- `skip`: Setting this to `true` will skip cloning the 710 repository. This can be useful if your workflow is doing 711 something that doesn't require anything from the 712 repository itself. This is `false` by default. 713- `depth`: This sets the number of commits, or the "clone 714 depth", to fetch from the repository. For example, if you 715 set this to 2, the last 2 commits will be fetched. By 716 default, the depth is set to 1, meaning only the most 717 recent commit will be fetched, which is the commit that 718 triggered the workflow. 719- `submodules`: If you use Git submodules 720 (https://git-scm.com/book/en/v2/Git-Tools-Submodules) 721 in your repository, setting this field to `true` will 722 recursively fetch all submodules. This is `false` by 723 default. 724 725The default settings are: 726 727```yaml 728clone: 729 skip: false 730 depth: 1 731 submodules: false 732``` 733 734### Dependencies 735 736Usually when you're running a workflow, you'll need 737additional dependencies. The `dependencies` field lets you 738define which dependencies to get, and from where. It's a 739key-value map, with the key being the registry to fetch 740dependencies from, and the value being the list of 741dependencies to fetch. 742 743The registry URL syntax can be found [on the nix 744manual](https://nix.dev/manual/nix/2.18/command-ref/new-cli/nix3-registry-add). 745 746Say you want to fetch Node.js and Go from `nixpkgs`, and a 747package called `my_pkg` you've made from your own registry 748at your repository at 749`https://tangled.org/@example.com/my_pkg`. You can define 750those dependencies like so: 751 752```yaml 753dependencies: 754 # nixpkgs 755 nixpkgs: 756 - nodejs 757 - go 758 # unstable 759 nixpkgs/nixpkgs-unstable: 760 - bun 761 # custom registry 762 git+https://tangled.org/@example.com/my_pkg: 763 - my_pkg 764``` 765 766Now these dependencies are available to use in your 767workflow! 768 769### Environment 770 771The `environment` field allows you define environment 772variables that will be available throughout the entire 773workflow. **Do not put secrets here, these environment 774variables are visible to anyone viewing the repository. You 775can add secrets for pipelines in your repository's 776settings.** 777 778Example: 779 780```yaml 781environment: 782 GOOS: "linux" 783 GOARCH: "arm64" 784 NODE_ENV: "production" 785 MY_ENV_VAR: "MY_ENV_VALUE" 786``` 787 788By default, the following environment variables set: 789 790- `CI` - Always set to `true` to indicate a CI environment 791- `TANGLED_PIPELINE_ID` - The AT URI of the current pipeline 792- `TANGLED_REPO_KNOT` - The repository's knot hostname 793- `TANGLED_REPO_DID` - The DID of the repository owner 794- `TANGLED_REPO_NAME` - The name of the repository 795- `TANGLED_REPO_DEFAULT_BRANCH` - The default branch of the 796 repository 797- `TANGLED_REPO_URL` - The full URL to the repository 798 799These variables are only available when the pipeline is 800triggered by a push: 801 802- `TANGLED_REF` - The full git reference (e.g., 803 `refs/heads/main` or `refs/tags/v1.0.0`) 804- `TANGLED_REF_NAME` - The short name of the reference 805 (e.g., `main` or `v1.0.0`) 806- `TANGLED_REF_TYPE` - The type of reference, either 807 `branch` or `tag` 808- `TANGLED_SHA` - The commit SHA that triggered the pipeline 809- `TANGLED_COMMIT_SHA` - Alias for `TANGLED_SHA` 810 811These variables are only available when the pipeline is 812triggered by a pull request: 813 814- `TANGLED_PR_SOURCE_BRANCH` - The source branch of the pull 815 request 816- `TANGLED_PR_TARGET_BRANCH` - The target branch of the pull 817 request 818- `TANGLED_PR_SOURCE_SHA` - The commit SHA of the source 819 branch 820 821### Steps 822 823The `steps` field allows you to define what steps should run 824in the workflow. It's a list of step objects, each with the 825following fields: 826 827- `name`: This field allows you to give your step a name. 828 This name is visible in your workflow runs, and is used to 829 describe what the step is doing. 830- `command`: This field allows you to define a command to 831 run in that step. The step is run in a Bash shell, and the 832 logs from the command will be visible in the pipelines 833 page on the Tangled website. The 834 [dependencies](#dependencies) you added will be available 835 to use here. 836- `environment`: Similar to the global 837 [environment](#environment) config, this **optional** 838 field is a key-value map that allows you to set 839 environment variables for the step. **Do not put secrets 840 here, these environment variables are visible to anyone 841 viewing the repository. You can add secrets for pipelines 842 in your repository's settings.** 843 844Example: 845 846```yaml 847steps: 848 - name: "Build backend" 849 command: "go build" 850 environment: 851 GOOS: "darwin" 852 GOARCH: "arm64" 853 - name: "Build frontend" 854 command: "npm run build" 855 environment: 856 NODE_ENV: "production" 857``` 858 859### Complete workflow 860 861```yaml 862# .tangled/workflows/build.yml 863 864when: 865 - event: ["push", "manual"] 866 branch: ["main", "develop"] 867 - event: ["pull_request"] 868 branch: ["main"] 869 870engine: "nixery" 871 872# using the default values 873clone: 874 skip: false 875 depth: 1 876 submodules: false 877 878dependencies: 879 # nixpkgs 880 nixpkgs: 881 - nodejs 882 - go 883 # custom registry 884 git+https://tangled.org/@example.com/my_pkg: 885 - my_pkg 886 887environment: 888 GOOS: "linux" 889 GOARCH: "arm64" 890 NODE_ENV: "production" 891 MY_ENV_VAR: "MY_ENV_VALUE" 892 893steps: 894 - name: "Build backend" 895 command: "go build" 896 environment: 897 GOOS: "darwin" 898 GOARCH: "arm64" 899 - name: "Build frontend" 900 command: "npm run build" 901 environment: 902 NODE_ENV: "production" 903``` 904 905If you want another example of a workflow, you can look at 906the one [Tangled uses to build the 907project](https://tangled.org/@tangled.org/core/blob/master/.tangled/workflows/build.yml). 908 909## Self-hosting guide 910 911### Prerequisites 912 913- Go 914- Docker (the only supported backend currently) 915 916### Configuration 917 918Spindle is configured using environment variables. The following environment variables are available: 919 920- `SPINDLE_SERVER_LISTEN_ADDR`: The address the server listens on (default: `"0.0.0.0:6555"`). 921- `SPINDLE_SERVER_DB_PATH`: The path to the SQLite database file (default: `"spindle.db"`). 922- `SPINDLE_SERVER_HOSTNAME`: The hostname of the server (required). 923- `SPINDLE_SERVER_JETSTREAM_ENDPOINT`: The endpoint of the Jetstream server (default: `"wss://jetstream1.us-west.bsky.network/subscribe"`). 924- `SPINDLE_SERVER_DEV`: A boolean indicating whether the server is running in development mode (default: `false`). 925- `SPINDLE_SERVER_OWNER`: The DID of the owner (required). 926- `SPINDLE_PIPELINES_NIXERY`: The Nixery URL (default: `"nixery.tangled.sh"`). 927- `SPINDLE_PIPELINES_WORKFLOW_TIMEOUT`: The default workflow timeout (default: `"5m"`). 928- `SPINDLE_PIPELINES_LOG_DIR`: The directory to store workflow logs (default: `"/var/log/spindle"`). 929 930### Running spindle 931 9321. **Set the environment variables.** For example: 933 934 ```shell 935 export SPINDLE_SERVER_HOSTNAME="your-hostname" 936 export SPINDLE_SERVER_OWNER="your-did" 937 ``` 938 9392. **Build the Spindle binary.** 940 941 ```shell 942 cd core 943 go mod download 944 go build -o cmd/spindle/spindle cmd/spindle/main.go 945 ``` 946 9473. **Create the log directory.** 948 949 ```shell 950 sudo mkdir -p /var/log/spindle 951 sudo chown $USER:$USER -R /var/log/spindle 952 ``` 953 9544. **Run the Spindle binary.** 955 956 ```shell 957 ./cmd/spindle/spindle 958 ``` 959 960Spindle will now start, connect to the Jetstream server, and begin processing pipelines. 961 962## Architecture 963 964Spindle is a small CI runner service. Here's a high-level overview of how it operates: 965 966- Listens for [`sh.tangled.spindle.member`](/lexicons/spindle/member.json) and 967 [`sh.tangled.repo`](/lexicons/repo.json) records on the Jetstream. 968- When a new repo record comes through (typically when you add a spindle to a 969 repo from the settings), spindle then resolves the underlying knot and 970 subscribes to repo events (see: 971 [`sh.tangled.pipeline`](/lexicons/pipeline.json)). 972- The spindle engine then handles execution of the pipeline, with results and 973 logs beamed on the spindle event stream over WebSocket 974 975### The engine 976 977At present, the only supported backend is Docker (and Podman, if Docker 978compatibility is enabled, so that `/run/docker.sock` is created). spindle 979executes each step in the pipeline in a fresh container, with state persisted 980across steps within the `/tangled/workspace` directory. 981 982The base image for the container is constructed on the fly using 983[Nixery](https://nixery.dev), which is handy for caching layers for frequently 984used packages. 985 986The pipeline manifest is [specified here](https://docs.tangled.org/spindles.html#pipelines). 987 988## Secrets with openbao 989 990This document covers setting up spindle to use OpenBao for secrets 991management via OpenBao Proxy instead of the default SQLite backend. 992 993### Overview 994 995Spindle now uses OpenBao Proxy for secrets management. The proxy handles 996authentication automatically using AppRole credentials, while spindle 997connects to the local proxy instead of directly to the OpenBao server. 998 999This approach provides better security, automatic token renewal, and 1000simplified application code. 1001 1002### Installation 1003 1004Install OpenBao from Nixpkgs: 1005 1006```bash 1007nix shell nixpkgs#openbao # for a local server 1008``` 1009 1010### Setup 1011 1012The setup process can is documented for both local development and production. 1013 1014#### Local development 1015 1016Start OpenBao in dev mode: 1017 1018```bash 1019bao server -dev -dev-root-token-id="root" -dev-listen-address=127.0.0.1:8201 1020``` 1021 1022This starts OpenBao on `http://localhost:8201` with a root token. 1023 1024Set up environment for bao CLI: 1025 1026```bash 1027export BAO_ADDR=http://localhost:8200 1028export BAO_TOKEN=root 1029``` 1030 1031#### Production 1032 1033You would typically use a systemd service with a 1034configuration file. Refer to 1035[@tangled.org/infra](https://tangled.org/@tangled.org/infra) 1036for how this can be achieved using Nix. 1037 1038Then, initialize the bao server: 1039 1040```bash 1041bao operator init -key-shares=1 -key-threshold=1 1042``` 1043 1044This will print out an unseal key and a root key. Save them 1045somewhere (like a password manager). Then unseal the vault 1046to begin setting it up: 1047 1048```bash 1049bao operator unseal <unseal_key> 1050``` 1051 1052All steps below remain the same across both dev and 1053production setups. 1054 1055#### Configure openbao server 1056 1057Create the spindle KV mount: 1058 1059```bash 1060bao secrets enable -path=spindle -version=2 kv 1061``` 1062 1063Set up AppRole authentication and policy: 1064 1065Create a policy file `spindle-policy.hcl`: 1066 1067```hcl 1068# Full access to spindle KV v2 data 1069path "spindle/data/*" { 1070 capabilities = ["create", "read", "update", "delete"] 1071} 1072 1073# Access to metadata for listing and management 1074path "spindle/metadata/*" { 1075 capabilities = ["list", "read", "delete", "update"] 1076} 1077 1078# Allow listing at root level 1079path "spindle/" { 1080 capabilities = ["list"] 1081} 1082 1083# Required for connection testing and health checks 1084path "auth/token/lookup-self" { 1085 capabilities = ["read"] 1086} 1087``` 1088 1089Apply the policy and create an AppRole: 1090 1091```bash 1092bao policy write spindle-policy spindle-policy.hcl 1093bao auth enable approle 1094bao write auth/approle/role/spindle \ 1095 token_policies="spindle-policy" \ 1096 token_ttl=1h \ 1097 token_max_ttl=4h \ 1098 bind_secret_id=true \ 1099 secret_id_ttl=0 \ 1100 secret_id_num_uses=0 1101``` 1102 1103Get the credentials: 1104 1105```bash 1106# Get role ID (static) 1107ROLE_ID=$(bao read -field=role_id auth/approle/role/spindle/role-id) 1108 1109# Generate secret ID 1110SECRET_ID=$(bao write -f -field=secret_id auth/approle/role/spindle/secret-id) 1111 1112echo "Role ID: $ROLE_ID" 1113echo "Secret ID: $SECRET_ID" 1114``` 1115 1116#### Create proxy configuration 1117 1118Create the credential files: 1119 1120```bash 1121# Create directory for OpenBao files 1122mkdir -p /tmp/openbao 1123 1124# Save credentials 1125echo "$ROLE_ID" > /tmp/openbao/role-id 1126echo "$SECRET_ID" > /tmp/openbao/secret-id 1127chmod 600 /tmp/openbao/role-id /tmp/openbao/secret-id 1128``` 1129 1130Create a proxy configuration file `/tmp/openbao/proxy.hcl`: 1131 1132```hcl 1133# OpenBao server connection 1134vault { 1135 address = "http://localhost:8200" 1136} 1137 1138# Auto-Auth using AppRole 1139auto_auth { 1140 method "approle" { 1141 mount_path = "auth/approle" 1142 config = { 1143 role_id_file_path = "/tmp/openbao/role-id" 1144 secret_id_file_path = "/tmp/openbao/secret-id" 1145 } 1146 } 1147 1148 # Optional: write token to file for debugging 1149 sink "file" { 1150 config = { 1151 path = "/tmp/openbao/token" 1152 mode = 0640 1153 } 1154 } 1155} 1156 1157# Proxy listener for spindle 1158listener "tcp" { 1159 address = "127.0.0.1:8201" 1160 tls_disable = true 1161} 1162 1163# Enable API proxy with auto-auth token 1164api_proxy { 1165 use_auto_auth_token = true 1166} 1167 1168# Enable response caching 1169cache { 1170 use_auto_auth_token = true 1171} 1172 1173# Logging 1174log_level = "info" 1175``` 1176 1177#### Start the proxy 1178 1179Start OpenBao Proxy: 1180 1181```bash 1182bao proxy -config=/tmp/openbao/proxy.hcl 1183``` 1184 1185The proxy will authenticate with OpenBao and start listening on 1186`127.0.0.1:8201`. 1187 1188#### Configure spindle 1189 1190Set these environment variables for spindle: 1191 1192```bash 1193export SPINDLE_SERVER_SECRETS_PROVIDER=openbao 1194export SPINDLE_SERVER_SECRETS_OPENBAO_PROXY_ADDR=http://127.0.0.1:8201 1195export SPINDLE_SERVER_SECRETS_OPENBAO_MOUNT=spindle 1196``` 1197 1198On startup, spindle will now connect to the local proxy, 1199which handles all authentication automatically. 1200 1201### Production setup for proxy 1202 1203For production, you'll want to run the proxy as a service: 1204 1205Place your production configuration in 1206`/etc/openbao/proxy.hcl` with proper TLS settings for the 1207vault connection. 1208 1209### Verifying setup 1210 1211Test the proxy directly: 1212 1213```bash 1214# Check proxy health 1215curl -H "X-Vault-Request: true" http://127.0.0.1:8201/v1/sys/health 1216 1217# Test token lookup through proxy 1218curl -H "X-Vault-Request: true" http://127.0.0.1:8201/v1/auth/token/lookup-self 1219``` 1220 1221Test OpenBao operations through the server: 1222 1223```bash 1224# List all secrets 1225bao kv list spindle/ 1226 1227# Add a test secret via the spindle API, then check it exists 1228bao kv list spindle/repos/ 1229 1230# Get a specific secret 1231bao kv get spindle/repos/your_repo_path/SECRET_NAME 1232``` 1233 1234### How it works 1235 1236- Spindle connects to OpenBao Proxy on localhost (typically 1237 port 8200 or 8201) 1238- The proxy authenticates with OpenBao using AppRole 1239 credentials 1240- All spindle requests go through the proxy, which injects 1241 authentication tokens 1242- Secrets are stored at 1243 `spindle/repos/{sanitized_repo_path}/{secret_key}` 1244- Repository paths like `did:plc:alice/myrepo` become 1245 `did_plc_alice_myrepo` 1246- The proxy handles all token renewal automatically 1247- Spindle no longer manages tokens or authentication 1248 directly 1249 1250### Troubleshooting 1251 1252**Connection refused**: Check that the OpenBao Proxy is 1253running and listening on the configured address. 1254 1255**403 errors**: Verify the AppRole credentials are correct 1256and the policy has the necessary permissions. 1257 1258**404 route errors**: The spindle KV mount probably doesn't 1259exist—run the mount creation step again. 1260 1261**Proxy authentication failures**: Check the proxy logs and 1262verify the role-id and secret-id files are readable and 1263contain valid credentials. 1264 1265**Secret not found after writing**: This can indicate policy 1266permission issues. Verify the policy includes both 1267`spindle/data/*` and `spindle/metadata/*` paths with 1268appropriate capabilities. 1269 1270Check proxy logs: 1271 1272```bash 1273# If running as systemd service 1274journalctl -u openbao-proxy -f 1275 1276# If running directly, check the console output 1277``` 1278 1279Test AppRole authentication manually: 1280 1281```bash 1282bao write auth/approle/login \ 1283 role_id="$(cat /tmp/openbao/role-id)" \ 1284 secret_id="$(cat /tmp/openbao/secret-id)" 1285``` 1286 1287# Webhooks 1288 1289Webhooks allow you to receive HTTP POST notifications when events occur in your repositories. This enables you to integrate Tangled with external services, trigger CI/CD pipelines, send notifications, or automate workflows. 1290 1291## Overview 1292 1293Webhooks send HTTP POST requests to URLs you configure whenever specific events happen. Currently, Tangled supports push events, with more event types coming soon. 1294 1295## Configuring webhooks 1296 1297To set up a webhook for your repository: 1298 12991. Navigate to your repository settings 13002. Click the "hooks" tab 13013. Click "add webhook" 13024. Configure your webhook: 1303 - **Payload URL**: The endpoint that will receive the webhook POST requests 1304 - **Secret**: An optional secret key for verifying webhook authenticity (auto-generated if left blank) 1305 - **Events**: Select which events trigger the webhook (currently only push events) 1306 - **Active**: Toggle whether the webhook is enabled 1307 1308## Webhook payload 1309 1310### Push 1311 1312When a push event occurs, Tangled sends a POST request with a JSON payload of the format: 1313 1314```json 1315{ 1316 "after": "7b320e5cbee2734071e4310c1d9ae401d8f6cab5", 1317 "before": "c04ddf64eddc90e4e2a9846ba3b43e67a0e2865e", 1318 "pusher": { 1319 "did": "did:plc:hwevmowznbiukdf6uk5dwrrq" 1320 }, 1321 "ref": "refs/heads/main", 1322 "repository": { 1323 "clone_url": "https://tangled.org/did:plc:hwevmowznbiukdf6uk5dwrrq/some-repo", 1324 "created_at": "2025-09-15T08:57:23Z", 1325 "description": "an example repository", 1326 "fork": false, 1327 "full_name": "did:plc:hwevmowznbiukdf6uk5dwrrq/some-repo", 1328 "html_url": "https://tangled.org/did:plc:hwevmowznbiukdf6uk5dwrrq/some-repo", 1329 "name": "some-repo", 1330 "open_issues_count": 5, 1331 "owner": { 1332 "did": "did:plc:hwevmowznbiukdf6uk5dwrrq" 1333 }, 1334 "ssh_url": "ssh://git@tangled.org/did:plc:hwevmowznbiukdf6uk5dwrrq/some-repo", 1335 "stars_count": 1, 1336 "updated_at": "2025-09-15T08:57:23Z" 1337 } 1338} 1339``` 1340 1341## HTTP headers 1342 1343Each webhook request includes the following headers: 1344 1345- `Content-Type: application/json` 1346- `User-Agent: Tangled-Hook/<short-sha>` — User agent with short SHA of the commit 1347- `X-Tangled-Event: push` — The event type 1348- `X-Tangled-Hook-ID: <webhook-id>` — The webhook ID 1349- `X-Tangled-Delivery: <uuid>` — Unique delivery ID 1350- `X-Tangled-Signature-256: sha256=<hmac>` — HMAC-SHA256 signature (if secret configured) 1351 1352## Verifying webhook signatures 1353 1354If you configured a secret, you should verify the webhook signature to ensure requests are authentic. For example, in Go: 1355 1356```go 1357package main 1358 1359import ( 1360 "crypto/hmac" 1361 "crypto/sha256" 1362 "encoding/hex" 1363 "io" 1364 "net/http" 1365 "strings" 1366) 1367 1368func verifySignature(payload []byte, signatureHeader, secret string) bool { 1369 // Remove 'sha256=' prefix from signature header 1370 signature := strings.TrimPrefix(signatureHeader, "sha256=") 1371 1372 // Compute expected signature 1373 mac := hmac.New(sha256.New, []byte(secret)) 1374 mac.Write(payload) 1375 expected := hex.EncodeToString(mac.Sum(nil)) 1376 1377 // Use constant-time comparison to prevent timing attacks 1378 return hmac.Equal([]byte(signature), []byte(expected)) 1379} 1380 1381func webhookHandler(w http.ResponseWriter, r *http.Request) { 1382 // Read the request body 1383 payload, err := io.ReadAll(r.Body) 1384 if err != nil { 1385 http.Error(w, "Bad request", http.StatusBadRequest) 1386 return 1387 } 1388 1389 // Get signature from header 1390 signatureHeader := r.Header.Get("X-Tangled-Signature-256") 1391 1392 // Verify signature 1393 if signatureHeader != "" && verifySignature(payload, signatureHeader, yourSecret) { 1394 // Webhook is authentic, process it 1395 processWebhook(payload) 1396 w.WriteHeader(http.StatusOK) 1397 } else { 1398 http.Error(w, "Invalid signature", http.StatusUnauthorized) 1399 } 1400} 1401``` 1402 1403## Delivery retries 1404 1405Webhooks are automatically retried on failure: 1406 1407- **3 total attempts** (1 initial + 2 retries) 1408- **Exponential backoff** starting at 1 second, max 10 seconds 1409- **Retried on**: 1410 - Network errors 1411 - HTTP 5xx server errors 1412- **Not retried on**: 1413 - HTTP 4xx client errors (bad request, unauthorized, etc.) 1414 1415### Timeouts 1416 1417Webhook requests timeout after 30 seconds. If your endpoint needs more time: 1418 14191. Respond with 200 OK immediately 14202. Process the webhook asynchronously in the background 1421 1422## Example integrations 1423 1424### Discord notifications 1425 1426```javascript 1427app.post("/webhook", (req, res) => { 1428 const payload = req.body; 1429 1430 fetch("https://discord.com/api/webhooks/...", { 1431 method: "POST", 1432 headers: { "Content-Type": "application/json" }, 1433 body: JSON.stringify({ 1434 content: `New push to ${payload.repository.full_name}`, 1435 embeds: [ 1436 { 1437 title: `${payload.pusher.did} pushed to ${payload.ref}`, 1438 url: payload.repository.html_url, 1439 color: 0x00ff00, 1440 }, 1441 ], 1442 }), 1443 }); 1444 1445 res.status(200).send("OK"); 1446}); 1447``` 1448 1449# Migrating knots and spindles 1450 1451Sometimes, non-backwards compatible changes are made to the 1452knot/spindle XRPC APIs. If you host a knot or a spindle, you 1453will need to follow this guide to upgrade. Typically, this 1454only requires you to deploy the newest version. 1455 1456This document is laid out in reverse-chronological order. 1457Newer migration guides are listed first, and older guides 1458are further down the page. 1459 1460## Upgrading from v1.8.x 1461 1462After v1.8.2, the HTTP API for knots and spindles has been 1463deprecated and replaced with XRPC. Repositories on outdated 1464knots will not be viewable from the appview. Upgrading is 1465straightforward however. 1466 1467For knots: 1468 1469- Upgrade to the latest tag (v1.9.0 or above) 1470- Head to the [knot dashboard](https://tangled.org/settings/knots) and 1471 hit the "retry" button to verify your knot 1472 1473For spindles: 1474 1475- Upgrade to the latest tag (v1.9.0 or above) 1476- Head to the [spindle 1477 dashboard](https://tangled.org/settings/spindles) and hit the 1478 "retry" button to verify your spindle 1479 1480## Upgrading from v1.7.x 1481 1482After v1.7.0, knot secrets have been deprecated. You no 1483longer need a secret from the appview to run a knot. All 1484authorized commands to knots are managed via [Inter-Service 1485Authentication](https://atproto.com/specs/xrpc#inter-service-authentication-jwt). 1486Knots will be read-only until upgraded. 1487 1488Upgrading is quite easy, in essence: 1489 1490- `KNOT_SERVER_SECRET` is no more, you can remove this 1491 environment variable entirely 1492- `KNOT_SERVER_OWNER` is now required on boot, set this to 1493 your DID. You can find your DID in the 1494 [settings](https://tangled.org/settings) page. 1495- Restart your knot once you have replaced the environment 1496 variable 1497- Head to the [knot dashboard](https://tangled.org/settings/knots) and 1498 hit the "retry" button to verify your knot. This simply 1499 writes a `sh.tangled.knot` record to your PDS. 1500 1501If you use the nix module, simply bump the flake to the 1502latest revision, and change your config block like so: 1503 1504```diff 1505 services.tangled.knot = { 1506 enable = true; 1507 server = { 1508- secretFile = /path/to/secret; 1509+ owner = "did:plc:foo"; 1510 }; 1511 }; 1512``` 1513 1514# Hacking on Tangled 1515 1516We highly recommend [installing 1517Nix](https://nixos.org/download/) (the package manager) 1518before working on the codebase. The Nix flake provides a lot 1519of helpers to get started and most importantly, builds and 1520dev shells are entirely deterministic. 1521 1522To set up your dev environment: 1523 1524```bash 1525nix develop 1526``` 1527 1528Non-Nix users can look at the `devShell` attribute in the 1529`flake.nix` file to determine necessary dependencies. 1530 1531## Running the appview 1532 1533The Nix flake also exposes a few `app` attributes (run `nix 1534flake show` to see a full list of what the flake provides), 1535one of the apps runs the appview with the `air` 1536live-reloader: 1537 1538```bash 1539TANGLED_DEV=true nix run .#watch-appview 1540 1541# TANGLED_DB_PATH might be of interest to point to 1542# different sqlite DBs 1543 1544# in a separate shell, you can live-reload tailwind 1545nix run .#watch-tailwind 1546``` 1547 1548To authenticate with the appview, you will need Redis and 1549OAuth JWKs to be set up: 1550 1551``` 1552# OAuth JWKs should already be set up by the Nix devshell: 1553echo $TANGLED_OAUTH_CLIENT_SECRET 1554z42ty4RT1ovnTopY8B8ekz9NuziF2CuMkZ7rbRFpAR9jBqMc 1555 1556echo $TANGLED_OAUTH_CLIENT_KID 15571761667908 1558 1559# if not, you can set it up yourself: 1560goat key generate -t P-256 1561Key Type: P-256 / secp256r1 / ES256 private key 1562Secret Key (Multibase Syntax): save this securely (eg, add to password manager) 1563 z42tuPDKRfM2mz2Kv953ARen2jmrPA8S9LX9tRq4RVcUMwwL 1564Public Key (DID Key Syntax): share or publish this (eg, in DID document) 1565 did:key:zDnaeUBxtG6Xuv3ATJE4GaWeyXM3jyamJsZw3bSPpxx4bNXDR 1566 1567# the secret key from above 1568export TANGLED_OAUTH_CLIENT_SECRET="z42tuP..." 1569 1570# Run Redis in a new shell to store OAuth sessions 1571redis-server 1572``` 1573 1574## Running knots and spindles 1575 1576An end-to-end knot setup requires setting up a machine with 1577`sshd`, `AuthorizedKeysCommand`, and a Git user, which is 1578quite cumbersome. So the Nix flake provides a 1579`nixosConfiguration` to do so. 1580 1581<details> 1582 <summary><strong>macOS users will have to set up a Nix Builder first</strong></summary> 1583 1584In order to build Tangled's dev VM on macOS, you will 1585first need to set up a Linux Nix builder. The recommended 1586way to do so is to run a [`darwin.linux-builder` 1587VM](https://nixos.org/manual/nixpkgs/unstable/#sec-darwin-builder) 1588and to register it in `nix.conf` as a builder for Linux 1589with the same architecture as your Mac (`linux-aarch64` if 1590you are using Apple Silicon). 1591 1592> IMPORTANT: You must build `darwin.linux-builder` somewhere other than inside 1593> the Tangled repo so that it doesn't conflict with the other VM. For example, 1594> you can do 1595> 1596> ```shell 1597> cd $(mktemp -d buildervm.XXXXX) && nix run nixpkgs#darwin.linux-builder 1598> ``` 1599> 1600> to store the builder VM in a temporary dir. 1601> 1602> You should read and follow [all the other intructions][darwin builder vm] to 1603> avoid subtle problems. 1604 1605Alternatively, you can use any other method to set up a 1606Linux machine with Nix installed that you can `sudo ssh` 1607into (in other words, root user on your Mac has to be able 1608to ssh into the Linux machine without entering a password) 1609and that has the same architecture as your Mac. See 1610[remote builder 1611instructions](https://nix.dev/manual/nix/2.28/advanced-topics/distributed-builds.html#requirements) 1612for how to register such a builder in `nix.conf`. 1613 1614> WARNING: If you'd like to use 1615> [`nixos-lima`](https://github.com/nixos-lima/nixos-lima) or 1616> [Orbstack](https://orbstack.dev/), note that setting them up so that `sudo 1617ssh` works can be tricky. It seems to be [possible with 1618> Orbstack](https://github.com/orgs/orbstack/discussions/1669). 1619 1620</details> 1621 1622To begin, grab your DID from http://localhost:3000/settings. 1623Then, set `TANGLED_VM_KNOT_OWNER` and 1624`TANGLED_VM_SPINDLE_OWNER` to your DID. You can now start a 1625lightweight NixOS VM like so: 1626 1627```bash 1628nix run --impure .#vm 1629 1630# type `poweroff` at the shell to exit the VM 1631``` 1632 1633This starts a knot on port 6444, a spindle on port 6555 1634with `ssh` exposed on port 2222. 1635 1636Once the services are running, head to 1637http://localhost:3000/settings/knots and hit "Verify". It should 1638verify the ownership of the services instantly if everything 1639went smoothly. 1640 1641You can push repositories to this VM with this ssh config 1642block on your main machine: 1643 1644```bash 1645Host nixos-shell 1646 Hostname localhost 1647 Port 2222 1648 User git 1649 IdentityFile ~/.ssh/my_tangled_key 1650``` 1651 1652Set up a remote called `local-dev` on a git repo: 1653 1654```bash 1655git remote add local-dev git@nixos-shell:user/repo 1656git push local-dev main 1657``` 1658 1659The above VM should already be running a spindle on 1660`localhost:6555`. Head to http://localhost:3000/settings/spindles and 1661hit "Verify". You can then configure each repository to use 1662this spindle and run CI jobs. 1663 1664Of interest when debugging spindles: 1665 1666``` 1667# Service logs from journald: 1668journalctl -xeu spindle 1669 1670# CI job logs from disk: 1671ls /var/log/spindle 1672 1673# Debugging spindle database: 1674sqlite3 /var/lib/spindle/spindle.db 1675 1676# litecli has a nicer REPL interface: 1677litecli /var/lib/spindle/spindle.db 1678``` 1679 1680If for any reason you wish to disable either one of the 1681services in the VM, modify [nix/vm.nix](/nix/vm.nix) and set 1682`services.tangled.spindle.enable` (or 1683`services.tangled.knot.enable`) to `false`. 1684 1685# Contribution guide 1686 1687## Commit guidelines 1688 1689We follow a commit style similar to the Go project. Please keep commits: 1690 1691- **atomic**: each commit should represent one logical change 1692- **descriptive**: the commit message should clearly describe what the 1693 change does and why it's needed 1694 1695### Message format 1696 1697``` 1698<service/top-level directory>/<affected package/directory>: <short summary of change> 1699 1700Optional longer description can go here, if necessary. Explain what the 1701change does and why, especially if not obvious. Reference relevant 1702issues or PRs when applicable. These can be links for now since we don't 1703auto-link issues/PRs yet. 1704``` 1705 1706Here are some examples: 1707 1708``` 1709appview/state: fix token expiry check in middleware 1710 1711The previous check did not account for clock drift, leading to premature 1712token invalidation. 1713``` 1714 1715``` 1716knotserver/git/service: improve error checking in upload-pack 1717``` 1718 1719### General notes 1720 1721- PRs get merged "as-is" (fast-forward)—like applying a patch-series 1722 using `git am`. At present, there is no squashing—so please author 1723 your commits as they would appear on `master`, following the above 1724 guidelines. 1725- If there is a lot of nesting, for example "appview: 1726 pages/templates/repo/fragments: ...", these can be truncated down to 1727 just "appview: repo/fragments: ...". If the change affects a lot of 1728 subdirectories, you may abbreviate to just the top-level names, e.g. 1729 "appview: ..." or "knotserver: ...". 1730- Keep commits lowercased with no trailing period. 1731- Use the imperative mood in the summary line (e.g., "fix bug" not 1732 "fixed bug" or "fixes bug"). 1733- Try to keep the summary line under 72 characters, but we aren't too 1734 fussed about this. 1735- Follow the same formatting for PR titles if filled manually. 1736- Don't include unrelated changes in the same commit. 1737- Avoid noisy commit messages like "wip" or "final fix"—rewrite history 1738 before submitting if necessary. 1739 1740## Code formatting 1741 1742We use a variety of tools to format our code, and multiplex them with 1743[`treefmt`](https://treefmt.com). All you need to do to format your changes 1744is run `nix run .#fmt` (or just `treefmt` if you're in the devshell). 1745 1746## Proposals for bigger changes 1747 1748Small fixes like typos, minor bugs, or trivial refactors can be 1749submitted directly as PRs. 1750 1751For larger changes—especially those introducing new features, significant 1752refactoring, or altering system behavior—please open a proposal first. This 1753helps us evaluate the scope, design, and potential impact before implementation. 1754 1755Create a new issue titled: 1756 1757``` 1758proposal: <affected scope>: <summary of change> 1759``` 1760 1761In the description, explain: 1762 1763- What the change is 1764- Why it's needed 1765- How you plan to implement it (roughly) 1766- Any open questions or tradeoffs 1767 1768We'll use the issue thread to discuss and refine the idea before moving 1769forward. 1770 1771## Developer Certificate of Origin (DCO) 1772 1773We require all contributors to certify that they have the right to 1774submit the code they're contributing. To do this, we follow the 1775[Developer Certificate of Origin 1776(DCO)](https://developercertificate.org/). 1777 1778By signing your commits, you're stating that the contribution is your 1779own work, or that you have the right to submit it under the project's 1780license. This helps us keep things clean and legally sound. 1781 1782To sign your commit, just add the `-s` flag when committing: 1783 1784```sh 1785git commit -s -m "your commit message" 1786``` 1787 1788This appends a line like: 1789 1790``` 1791Signed-off-by: Your Name <your.email@example.com> 1792``` 1793 1794We won't merge commits if they aren't signed off. If you forget, you can 1795amend the last commit like this: 1796 1797```sh 1798git commit --amend -s 1799``` 1800 1801If you're submitting a PR with multiple commits, make sure each one is 1802signed. 1803 1804For [jj](https://jj-vcs.github.io/jj/latest/) users, you can run the following command 1805to make it sign off commits in the tangled repo: 1806 1807```shell 1808# Safety check, should say "No matching config key..." 1809jj config list templates.commit_trailers 1810# The command below may need to be adjusted if the command above returned something. 1811jj config set --repo templates.commit_trailers "format_signed_off_by_trailer(self)" 1812``` 1813 1814Refer to the [jujutsu 1815documentation](https://jj-vcs.github.io/jj/latest/config/#commit-trailers) 1816for more information. 1817 1818# Troubleshooting guide 1819 1820## Login issues 1821 1822Owing to the distributed nature of OAuth on AT Protocol, you 1823may run into issues with logging in. If you run a 1824self-hosted PDS: 1825 1826- You may need to ensure that your PDS is timesynced using 1827 NTP: 1828 - Enable the `ntpd` service 1829 - Run `ntpd -qg` to synchronize your clock 1830- You may need to increase the default request timeout: 1831 `NODE_OPTIONS="--network-family-autoselection-attempt-timeout=500"` 1832 1833## Empty punchcard 1834 1835For Tangled to register commits that you make across the 1836network, you need to setup one of following: 1837 1838- The committer email should be a verified email associated 1839 to your account. You can add and verify emails on the 1840 settings page. 1841- Or, the committer email should be set to your account's 1842 DID: `git config user.email "did:plc:foobar"`. You can find 1843 your account's DID on the settings page 1844 1845## Commit is not marked as verified 1846 1847Presently, Tangled only supports SSH commit signatures. 1848 1849To sign commits using an SSH key with git: 1850 1851``` 1852git config --global gpg.format ssh 1853git config --global user.signingkey ~/.ssh/tangled-key 1854``` 1855 1856To sign commits using an SSH key with jj, add this to your 1857config: 1858 1859``` 1860[signing] 1861behavior = "own" 1862backend = "ssh" 1863key = "~/.ssh/tangled-key" 1864``` 1865 1866## Self-hosted knot issues 1867 1868If you need help troubleshooting a self-hosted knot, check 1869out the [knot troubleshooting 1870guide](/knot-self-hosting-guide.html#troubleshooting).