diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs
index 5460cc5d2..71a01db49 100644
--- a/docs/astro.config.mjs
+++ b/docs/astro.config.mjs
@@ -84,7 +84,7 @@ export default defineConfig({
MobileMenuFooter: "./src/components/MobileMenuFooter.astro",
PageTitle: "./src/components/PageTitle.astro",
Sidebar: "./src/components/Sidebar.astro",
- ThemeSelect: "./src/components/ThemeToggle.astro",
+ ThemeProvider: "./src/components/ThemeProvider.astro",
},
expressiveCode: {
// same palettes as the web app's syntax tokens
@@ -137,13 +137,12 @@ export default defineConfig({
],
},
{
- label: "CI",
+ label: "Running CI",
items: [
"spindles",
"spindles/writing-workflows",
"spindles/workflows",
"spindles/secrets",
- "spindles/runs",
"spindles/webhooks",
],
},
diff --git a/docs/src/components/Header.astro b/docs/src/components/Header.astro
index b93ff311a..2ed081c32 100644
--- a/docs/src/components/Header.astro
+++ b/docs/src/components/Header.astro
@@ -4,7 +4,6 @@
// there is none: one search per page
import Search from "./Search.astro";
import SiteTitle from "@astrojs/starlight/components/SiteTitle.astro";
-import ThemeToggle from "./ThemeToggle.astro";
const { hasSidebar } = Astro.locals.starlightRoute;
---
@@ -13,6 +12,5 @@ const { hasSidebar } = Astro.locals.starlightRoute;
{!hasSidebar && }
-
diff --git a/docs/src/components/MobileMenuFooter.astro b/docs/src/components/MobileMenuFooter.astro
index a587cc778..0bf4c5a2d 100644
--- a/docs/src/components/MobileMenuFooter.astro
+++ b/docs/src/components/MobileMenuFooter.astro
@@ -1,4 +1,3 @@
---
-// the header already has the theme toggle and there are no social links or
-// languages, so the mobile menu needs no footer
+// there are no social links or languages, so the mobile menu needs no footer
---
diff --git a/docs/src/components/Sidebar.astro b/docs/src/components/Sidebar.astro
index e46cfb7cb..a8a9ac332 100644
--- a/docs/src/components/Sidebar.astro
+++ b/docs/src/components/Sidebar.astro
@@ -1,12 +1,11 @@
---
-// the top of the page lives here instead of a top bar: logo and theme toggle,
+// the top of the page lives here instead of a top bar: logo,
// then search, then the nav. the nav is starlight's sidebar data rendered flat:
// group headings and links, no collapsing, no indentation (styles in tangled.css)
import type { StarlightRouteData } from "@astrojs/starlight/route-data";
import Search from "./Search.astro";
import SidebarPersister from "@astrojs/starlight/components/SidebarPersister.astro";
import SiteTitle from "@astrojs/starlight/components/SiteTitle.astro";
-import ThemeToggle from "./ThemeToggle.astro";
type Entry = StarlightRouteData["sidebar"][number];
@@ -16,7 +15,6 @@ const links = (entries: Entry[]) => entries.filter((e) => e.type === "link");
-
diff --git a/docs/src/components/ThemeProvider.astro b/docs/src/components/ThemeProvider.astro
new file mode 100644
index 000000000..cab622dc9
--- /dev/null
+++ b/docs/src/components/ThemeProvider.astro
@@ -0,0 +1,10 @@
+
diff --git a/docs/src/components/ThemeToggle.astro b/docs/src/components/ThemeToggle.astro
deleted file mode 100644
index b77255916..000000000
--- a/docs/src/components/ThemeToggle.astro
+++ /dev/null
@@ -1,53 +0,0 @@
----
-// replaces starlight's three-way theme select with a light/dark toggle. it
-// keeps starlight's storage key, so the head script still applies the stored
-// theme before paint, and follows the system until someone picks one
-import Button from "$lib/components/ui/Button.svelte";
-import Sun from "~icons/lucide/sun";
-import Moon from "~icons/lucide/moon";
----
-
-
-
-
-
-
-
diff --git a/docs/src/content/docs/architecture/overview.mdx b/docs/src/content/docs/architecture/overview.mdx
index c02cc0f00..9485df677 100644
--- a/docs/src/content/docs/architecture/overview.mdx
+++ b/docs/src/content/docs/architecture/overview.mdx
@@ -44,7 +44,7 @@ that repo's account. Its state is git: there is no SQL database. See
hosting its repos, reads `.tangled/workflows/*.yml` at the pushed
commit, and runs each workflow in a microVM or container. Pipelines,
logs, secrets and webhooks live in the spindle's own database, not in
-records. See [What Spindles Do](/docs/spindles/).
+records. See [How CI works](/docs/spindles/).
**Hydrant.** Hydrant subscribes to the `com.atproto.sync.subscribeRepos`
firehoses of PDSes and knots, backfills by crawling them, keeps the
diff --git a/docs/src/content/docs/reference/glossary.md b/docs/src/content/docs/reference/glossary.md
index 39e7af1f5..9b6e82a43 100644
--- a/docs/src/content/docs/reference/glossary.md
+++ b/docs/src/content/docs/reference/glossary.md
@@ -154,7 +154,7 @@ your profile, stars and follows; a knot is the PDS for its repositories.
One CI run on a spindle for a commit and trigger. Owned by the spindle,
not stored as a record. See
-[Runs, Logs, and Artifacts](/docs/spindles/runs/).
+[How CI works](/docs/spindles/).
## PLC
@@ -186,7 +186,7 @@ call uses it. See [Security Model](/docs/architecture/security/).
## Spindle
Tangled's self-hostable CI runner. A repository chooses one. See
-[What Spindles Do](/docs/spindles/).
+[How CI works](/docs/spindles/).
## String
diff --git a/docs/src/content/docs/spindles/index.md b/docs/src/content/docs/spindles/index.md
index 9a17e3c02..f3a3e6c9e 100644
--- a/docs/src/content/docs/spindles/index.md
+++ b/docs/src/content/docs/spindles/index.md
@@ -1,5 +1,5 @@
---
-title: What spindles do
+title: How CI works
description:
Spindles are Tangled's CI runners. They watch your knot for pushes and
run the workflows in .tangled/workflows/.
@@ -7,8 +7,8 @@ sidebar:
order: 0
---
-A spindle is Tangled's CI runner. It watches the knot that hosts your
-repository, and when you push it runs the workflow files in
+Spindle is a small CI runner service. It watches the knot that hosts
+your repository. When you push, it runs the workflow files in
`.tangled/workflows/` on that commit. You can use Tangled's hosted
spindle or [run your own](/docs/self-hosting/spindle/).
@@ -18,56 +18,77 @@ when:
- event: push
branch: main
-engine: nixery
+engine: microvm
+image: nixos
dependencies:
- nixpkgs:
- - go
+ - go
steps:
- name: test
command: go test ./...
```
-Dependencies are [nixpkgs](https://search.nixos.org) package names.
-There is no action marketplace and no toolchain setup step.
-[Writing workflows](/docs/spindles/writing-workflows/) walks through a first
-workflow.
+[Writing workflows](/docs/spindles/writing-workflows/) explains this file
+one part at a time.
-## Engines at a glance
+## The CI model
-Each workflow picks an engine with the `engine` field.
-
-| Engine | Runs on | Use it for |
-| --------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
-| `nixery` | One OCI container per workflow (Docker or Podman), image built from your Nix packages by [Nixery](https://nixery.dev) | Most builds and tests. |
-| `microvm` | A QEMU microVM with its own network namespace and DNS filtering | Stronger isolation, NixOS services (`services.postgresql.enable: true`), Docker inside CI. |
-| `dummy` | Nothing; steps are accepted but not executed | Testing spindle itself. |
+```
+push ──▶ pipeline ──┬──▶ workflow (test.yml) ──▶ step, step, step
+ └──▶ workflow (lint.yml) ──▶ step, step
+```
-Porting a nixery workflow to microVM is usually a matter of changing
-`engine: nixery` to `engine: microvm`. The
-[workflow reference](/docs/spindles/workflows/#engines) lists what each
-engine accepts.
+- One trigger event makes one **pipeline**.
+- Each workflow file that matches the event becomes a **workflow** in
+ that pipeline. Workflows run in parallel, each in its own container or
+ microVM.
+- A workflow is a list of **steps** that run in order. The first step
+ that exits non-zero fails the workflow. The steps after it do not run.
## From push to run
+```
+you ──git push──▶ knot ──notifyEvent──▶ spindle ──▶ pipeline
+```
+
1. You push to your knot.
-2. The spindle notices the new commit. It ignores refs other than
- branches and tags, and pushes made with the `skip-ci` push option.
-3. It reads the workflow files at the pushed commit and keeps those
- whose `when` block matches the event, branch or tag, and changed
- paths. If none match, nothing runs.
+2. The knot tells the repository's spindle about the push. The spindle
+ ignores refs that are not branches or tags, and pushes made with the
+ `skip-ci` push option.
+3. The spindle reads the workflow files at the pushed commit and keeps
+ those whose `when` block matches the event, branch or tag, and
+ changed paths. If none match, nothing runs.
4. Each matching workflow becomes a run in a pipeline. The repository's
**Pipelines** tab and the commit show its status and logs.
-Push webhooks fire at step 2, even when CI is skipped. See
-[Webhooks](/docs/spindles/webhooks/).
+Behind this:
+
+- The repository's `org.tangled.repo.manifest` record names its
+ spindle. When you pick a spindle in **Settings → Pipelines**, the
+ manifest changes, and that spindle starts to serve the repository.
+- The spindle watches the Jetstream for changes to repositories,
+ collaborators, pull requests and patches. It reads the records
+ themselves from the knot.
+- Pushes and manual runs reach the spindle as an
+ `org.tangled.event.notifyEvent` call. Only the repository's own knot
+ can send a push. Pull request runs start from the pull request and
+ patch records.
+- The spindle writes no records. Pipelines, statuses, logs, secrets and
+ webhooks live in the spindle's own database. The web app reads them
+ with `org.tangled.ci.*` queries, and logs stream over a WebSocket.
+
+## The engines
+
+Spindle has two execution backends, picked per-workflow with the
+`engine` field:
-Secrets are stored on the spindle, and only people with access to the
-repository's settings can manage them. See
-[Secrets](/docs/spindles/secrets/).
+| Engine | Runs on | Use it for |
+| --------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
+| `nixery` | A fresh Docker or Podman container for each step, with the image built from your Nix packages by Nixery | Short, stateless builds and tests. |
+| `microvm` | The whole workflow inside its own microVM, with a choice of images | Stronger isolation, NixOS services such as PostgreSQL, Docker inside CI. |
-[Workflow reference](/docs/spindles/workflows/) documents every field;
-[Runs, logs, and artifacts](/docs/spindles/runs/) covers watching runs.
-[How data moves](/docs/architecture/data-flow/) explains what happens behind
-this.
+With nixery, state persists across steps only in the
+`/tangled/workspace` directory. With microvm, files, packages and
+services persist for the whole workflow. Which engines and images exist
+depends on the spindle operator.
diff --git a/docs/src/content/docs/spindles/runs.md b/docs/src/content/docs/spindles/runs.md
deleted file mode 100644
index d23e99275..000000000
--- a/docs/src/content/docs/spindles/runs.md
+++ /dev/null
@@ -1,107 +0,0 @@
----
-title: Runs, logs, and artifacts
-description:
- Find pipeline runs, follow live logs, read commit statuses, start
- manual runs, and know where logs are kept.
----
-
-Every run your spindle executes is listed on the repository's
-**Pipelines** tab. The list and the logs come straight from the spindle,
-so they're only available while that spindle is reachable.
-
-## The pipelines list
-
-Open a repository and select **Pipelines**. Each card is one pipeline:
-the trigger (**Push to** a branch or tag, **Pull request** from a
-branch, or **Manual dispatch**), the commit, and the status of each
-workflow in it.
-
-- The tabs filter by trigger: **All**, **Push**, **Pull request**.
- Manual and scheduled runs only appear under **All**.
-- The search box narrows the list by commit hash, branch or tag, pull
- request source branch, or workflow name.
-- The list goes back 250 runs.
-
-A pipeline's overall status is derived from its workflows. Each workflow
-is one of `pending`, `scheduled`, `running`, `success`, `failed`,
-`timeout` or `cancelled`.
-
-If the spindle can't be reached, the page says **Could not reach**
-followed by the spindle's hostname, and the rest of the repository still
-works.
-
-## Live logs
-
-Select a workflow to open its log view. Steps are listed in order,
-starting with the spindle's own steps such as **Clone repository into
-workspace**. While the workflow runs, output streams in live; when it
-finishes, the same view replays the stored log.
-
-Start with the first failed step; later errors are usually consequences
-of it. A workflow can also fail before any of your steps start, when the
-image, dependencies or a field in the file is invalid; that error shows
-in the log too. Secret values are masked as `***`.
-
-### Logs in a terminal
-
-If the knot operator configured a CI log address, `git push` prints a
-command to follow the run's logs in your terminal:
-
-```sh
-ssh -t -p 3333 logs.example.com did:plc:… 4f1c…
-```
-
-Run it as printed; the host, port, repository DID and commit come from
-the deployment.
-
-### Debug shell
-
-On microVM spindles with debug SSH enabled, a failed step keeps the VM
-alive for a grace period (5 minutes by default). The log ends with a
-**Debug shell** step:
-
-```
-Workflow failed, connect within 5m0s to debug until shell exit or workflow timeout:
- ssh -t …
-```
-
-Connect with an SSH key that has push access to the repository. You land
-in the VM as the workflow user, with the workspace, environment and
-secrets of the failed run. The VM is destroyed when you exit the shell
-or the workflow timeout is reached.
-
-## Commit statuses
-
-Commit lists, the commit page and the repository's front page show each
-commit's pipeline status next to it, and pull requests show the status
-of their commits. These come from the repository's spindle. Statuses
-aren't stored anywhere else: changing the repository's spindle hides the
-old ones.
-
-## Manual runs
-
-A manual run executes workflows at a commit you choose, regardless of
-their `when` conditions. Anyone with push access to the repository can
-start one. You can pick which workflows to run and pass inputs, which
-steps read as `TANGLED_INPUT_` variables. A manual run of a fork's
-code is untrusted and gets no secrets, caches or ID tokens. Cancelling a
-pipeline stops its workflows but keeps the pipeline and its logs.
-
-## Where logs and artifacts live
-
-Pipelines, workflow statuses and logs belong to the spindle, not to your
-account or your knot. How long logs are kept is up to the spindle's
-operator.
-
-Spindles don't collect build outputs. To keep a binary or archive,
-upload it from a step: push a container image, or upload to object
-storage.
-
-:::caution
-
-Switching a repository to a different spindle drops access to its run
-history, logs and statuses. They stay on the old spindle and don't move.
-
-::: [Workflow reference](/docs/spindles/workflows/) explains triggers and
-statuses in detail; [What spindles do](/docs/spindles/) covers how a push
-becomes a run.
diff --git a/docs/src/content/docs/spindles/secrets.md b/docs/src/content/docs/spindles/secrets.md
index e5e8b1471..be6d8e9b1 100644
--- a/docs/src/content/docs/spindles/secrets.md
+++ b/docs/src/content/docs/spindles/secrets.md
@@ -2,13 +2,12 @@
title: Secrets
description:
Store tokens and passwords for a repository's workflows on its
- spindle, and how they reach your steps.
+ spindle, and how they reach your workflows.
---
A repository secret is a named value, such as an API token, that your
workflows can read but that never appears in the repository. Secrets are
-stored on the repository's spindle, not on your knot or with your
-account provider.
+stored on the repository's spindle.
## Adding a secret
@@ -20,8 +19,9 @@ account provider.
The list shows each secret's name, when it was added, and by whom.
Values are never shown again; to change one, delete it and add it again.
-Delete removes it from the spindle immediately, and runs that start
-afterwards no longer see it.
+In fact, there is no API to read the value of a secret, these are only
+exposed to each workflow at execution time. Delete removes it from the
+spindle immediately.
Names can be 1 to 50 characters. Values can be up to 4096 characters.
Use names that are valid shell variable names, since that's how steps
@@ -29,8 +29,10 @@ read them.
## Using a secret in a step
-Every secret is set as an environment variable in every step of every
-workflow in the repository:
+Every secret is automatically set as an environment variable in every
+step of every workflow in the repository. So adding a secret with the
+name `CLOUDFLARE_API_TOKEN` will make that environment variable
+available to all steps in the workflow:
```yaml
steps:
@@ -38,20 +40,17 @@ steps:
command: pnpm exec wrangler deploy # reads $CLOUDFLARE_API_TOKEN
```
-You don't list secrets in the workflow file. There's no per-workflow or
-per-branch scoping: any workflow that runs for the repository gets all
-of its secrets.
+Any workflow that runs for the repository gets all of the configured
+secrets.
## Masking
The spindle replaces secret values with `***` in stored and streamed
logs. It also masks the base64 encoding of each value and each line of a
-multi-line value. Masking is a safety net, not a guarantee: a step that
-transforms a value (reverses it, hashes it, prints it one character at a
-time) can still leak it, and anyone who can run a workflow can read the
-secrets.
+multi-line value. Masking is a safety net, not a guarantee and anyone
+who can run a workflow can read the secrets.
-## Who can see and use secrets
+## Visibility of secrets
- **Managing** secrets needs settings access to the repository on the
spindle: the repository owner and collaborators.
@@ -61,16 +60,15 @@ secrets.
- **Untrusted runs get no secrets.** When a run checks out code from
another repository, such as a manual run of a fork, the spindle
withholds secrets, caches and OIDC tokens. Pull requests from forks
- don't start runs automatically.
+ never start runs automatically.
- **The spindle operator** can read secrets. They're stored in the
spindle's database, or in OpenBao if the operator configured it
- ([Secrets with OpenBao](/docs/self-hosting/openbao/)). On our spindle that is
- us; run your own spindle if you want to hold them yourself.
+ ([Secrets with OpenBao](/docs/self-hosting/openbao/)).
:::note
-Secrets live on one spindle. If you move the repository between our
-spindle and your own, add them again there.
+Secrets live on one spindle and are non-transferrable. If you switch
+from one spindle to another, you will need to add secrets again there.
:::
@@ -81,6 +79,3 @@ For cloud providers that accept OpenID Connect, set
storing a key. The spindle mints a short-lived token per run as
`TANGLED_ID_TOKEN`, masked like a secret. This needs the spindle
operator to have enabled OIDC.
-
-[Writing workflows](/docs/spindles/writing-workflows/#recipes) has recipes
-that use secrets for container registries and deployments.
diff --git a/docs/src/content/docs/spindles/webhooks.md b/docs/src/content/docs/spindles/webhooks.md
index 6d406c8ed..76463f390 100644
--- a/docs/src/content/docs/spindles/webhooks.md
+++ b/docs/src/content/docs/spindles/webhooks.md
@@ -89,5 +89,4 @@ Webhooks live on the spindle. Switching the repository to another
spindle makes the old spindle delete the repository's webhooks and
delivery history; recreate them on the new spindle.
-::: [Webhook payloads](/docs/reference/webhooks/) documents the request
-format; [What spindles do](/docs/spindles/) explains the spindle's role.
+:::
diff --git a/docs/src/content/docs/spindles/workflows.md b/docs/src/content/docs/spindles/workflows.md
index 4c62f2b47..f4261407e 100644
--- a/docs/src/content/docs/spindles/workflows.md
+++ b/docs/src/content/docs/spindles/workflows.md
@@ -11,17 +11,60 @@ at the triggering commit and runs the ones whose trigger matches.
[Writing Workflows](/docs/spindles/writing-workflows/) walks through a first
workflow; this page lists every field.
+## All fields
+
+Every field a workflow file accepts. Each one links to its section
+below.
+
```yaml
-# .tangled/workflows/test.yml
-when:
- - event: [push, pull_request]
- branch: main
-engine: microvm
-image: nixos
-dependencies: [go]
+# common fields, every engine
+when: # list of conditions, any one matches
+ - event: [push, pull_request, manual, schedule]
+ branch: ["main", "release-*"] # push and pull_request
+ tag: ["v*"] # push only
+ types: [opened, reopened, synchronize, closed, merged] # pull_request only
+ paths: ["src/**"] # changed files
+ schedule: # schedule only
+ - cron: "H 2 * * *"
+ timezone: Europe/London # default UTC
+engine: microvm # microvm, nixery or dummy (required)
+clone:
+ skip: false
+ depth: 1
+ submodules: true
+ tags: true
+environment:
+ KEY: value
+cache:
+ - key: go-mod # required
+ hash: [go.sum]
+ fallback: false
+ paths: [.gocache] # required
+ compression-level: 5 # 1 to 19
+ when: on-success # or always
steps:
- name: test
command: go test ./...
+ environment:
+ KEY: value
+runs_on: [arm64] # executor labels
+audience: sts.amazonaws.com # oidc token audience
+
+# microvm only
+image: nixos
+dependencies: [go, "unstable#zig", "github:nixos/nixpkgs#hello"]
+registry:
+ nixpkgs: github:nixos/nixpkgs/nixos-unstable
+substituters:
+ https://nix-community.cachix.org: "nix-community.cachix.org-1:..."
+services:
+ postgresql: true
+virtualisation:
+ docker: true
+
+# nixery only
+dependencies:
+ nixpkgs: [go]
```
## Pipelines
@@ -65,8 +108,7 @@ condition has these fields:
See `types`.
- `manual`: marks the workflow as one you start by hand. The spindle
doesn't evaluate `when` for manual runs at all: it runs the
- workflows the caller names, or every workflow. See
- [Manual Runs](/docs/spindles/runs/#manual-runs).
+ workflows the caller names, or every workflow.
- `schedule`: the workflow runs according to its `cron` expressions
using the default-branch name and commit saved when spindle last
refreshed the repository's schedules.
@@ -332,7 +374,7 @@ paths are archived again and stored.
workflow file, ref and commit that produced it.
- `hash`: **optional** list of repo files (lockfiles, manifests) whose
content is folded into the entry. The entry is stored as
- `-`, so editing `go.sum` rotates the cache
+ `-`, so editing `go.sum` invalidates the cache
without bumping the key by hand. Paths are relative to the repository
root, are read from git at the commit being built, and can't contain
globs.
@@ -652,14 +694,9 @@ virtualisation:
If the operator enabled debug SSH, a microVM whose step fails is kept
alive for a grace period (5 minutes by default). The step's log ends
with a **Debug shell** entry containing the exact `ssh` command. You
-need an SSH key with push access to the repository. See
-[Runs, Logs, and Artifacts](/docs/spindles/runs/#debug-shell).
+need an SSH key with push access to the repository.
### Dummy engine
`engine: dummy` accepts `steps` and `environment` and doesn't execute
anything. It exists for testing spindle itself.
-
-Ready-made workflows for Node, Go, Rust, PostgreSQL, Docker, Cloudflare
-and release artifacts are in
-[Writing Workflows](/docs/spindles/writing-workflows/#recipes).
diff --git a/docs/src/content/docs/spindles/writing-workflows.md b/docs/src/content/docs/spindles/writing-workflows.md
index 845e455c2..41049323f 100644
--- a/docs/src/content/docs/spindles/writing-workflows.md
+++ b/docs/src/content/docs/spindles/writing-workflows.md
@@ -1,28 +1,27 @@
---
title: Writing workflows
description:
- Add a CI workflow to a repository, choose triggers and an engine, and
- reuse ready-made recipes.
+ A walkthrough of the workflow syntax, one field at a time, with small
+ examples and ready-made recipes.
---
-A workflow is a YAML file in `.tangled/workflows/` that tells your
-repository's spindle what to run and when. This page walks through a
-first workflow and the choices you'll make; the
-[workflow reference](/docs/spindles/workflows/) lists every field.
+Spindle workflows allow you to write CI/CD pipelines in a simple format.
+They're located in the `.tangled/workflows` directory at the root of
+your repository, and are defined using YAML. This page shows each part
+with a small example. The [workflow reference](/docs/spindles/workflows/)
+lists every field.
-Before a workflow can run, the repository needs a spindle: open the
+Before a workflow can run, the repository needs a spindle. Open the
repository's **Settings → Pipelines** and, under **Spindle**, run CI
-with Tangled or select your own spindle. [What spindles do](/docs/spindles/)
-explains how that works.
+with Tangled or select your own spindle.
## Your first workflow
-Create `.tangled/workflows/check.yml` in a Go repository whose default
-branch is `main`:
+Create `.tangled/workflows/check.yml`:
```yaml
when:
- - event: [push, pull_request]
+ - event: push
branch: main
engine: microvm
@@ -44,126 +43,233 @@ git commit -m "Add CI workflow"
git push
```
-The push itself is the first trigger. The spindle fetches the commit,
-boots a NixOS microVM with Go installed, and runs `go test ./...` from
-the repository root. Open the repository's **Pipelines** tab to watch
-it; see [Runs, logs, and artifacts](/docs/spindles/runs/).
+The push starts the first run. Open the repository's **Pipelines** tab
+to watch it.
If a workflow file has a problem, `git push` prints it. Add
-`-o verbose-ci` to see the report even when everything is fine.
+`-o verbose-ci` to see the report when there is no problem.
+
+A workflow has four main parts:
+
+| Part | Says |
+| -------------- | ------------------------ |
+| `when` | When the workflow runs. |
+| `engine` | Where the workflow runs. |
+| `dependencies` | What tools it needs. |
+| `steps` | What commands to run. |
+
+## When it runs
-## Choosing triggers
+The `when` field is a list of conditions. The workflow runs if any
+condition matches.
-`when` is a list of conditions; the workflow runs if any of them
-matches.
+Run on every push to `main`:
```yaml
when:
- event: push
- branch: [main, "release-*"]
- tag: "v*"
+ branch: main
+```
+
+Run on pull requests that target `main`:
+
+```yaml
+when:
- event: pull_request
branch: main
- types: [opened, synchronize, reopened, merged]
+```
+
+Run when tags matching `v*` are pushed:
+
+```yaml
+when:
+ - event: push
+ tag: "v*"
+```
+
+Run only when files in `docs/` change:
+
+```yaml
+when:
+ - event: push
+ branch: main
+ paths: ["docs/**"]
+```
+
+Run every night, at a stable minute in the 02:00 hour:
+
+```yaml
+when:
- event: schedule
schedule:
- - cron: "H 3 * * *"
- timezone: Europe/Berlin
+ - cron: "H 2 * * *"
+```
+
+Let people start it by hand:
+
+```yaml
+when:
- event: manual
```
-- **Push** needs `branch`, `tag`, or both. Add `paths` to run only when
- matching files changed.
-- **Pull request** matches on the **target** branch. Only pull requests
- from a branch of the same repository start runs automatically; fork
- and patch pull requests don't.
-- **Schedule** runs on the default branch's latest commit. `H` picks a
- stable, per-repository minute so scheduled jobs across a spindle don't
- all fire at once. Missed runs while the spindle is down are not
- replayed.
-- **Manual** runs are started on request, at any commit. See
- [Manual runs](/docs/spindles/runs/#manual-runs).
+Combine them. This runs on pushes to `main` or `develop`, on pull
+requests that target `main`, and by hand:
-To push without starting CI, use `git push -o skip-ci` (or `ci-skip`).
+```yaml
+when:
+ - event: ["push", "manual"]
+ branch: ["main", "develop"]
+ - event: ["pull_request"]
+ branch: ["main"]
+```
-## Picking an engine
+To skip CI for a push, pass a Git push option:
-| | `microvm` | `nixery` |
-| ------------------- | ----------------------------------------------------------------- | ------------------------------- |
-| Isolation | One VM per workflow | One container per step |
-| State between steps | Files, packages and services persist | Only the workspace persists |
-| `dependencies` | List of nixpkgs names or `flake#attr` (NixOS images only) | Map of Nix registry to packages |
-| Extras | `image`, `services`, `virtualisation`, `registry`, `substituters` | none |
+```sh
+git push -o skip-ci
+```
-Pick `microvm` when you need a database, Docker, or a non-Nix
-distribution (`image: alpine` or `almalinux10`). Pick `nixery` for
-short, stateless steps on a spindle that offers it. Which engines and
-images exist is up to the spindle's operator.
+## Where it runs
-## Dependencies
+The `engine` field picks the engine:
-On a NixOS microVM image, list packages by nixpkgs name, or reference
-any flake:
+```yaml
+engine: microvm
+image: nixos
+```
+
+```yaml
+engine: nixery
+```
+
+- `microvm` runs the whole workflow inside its own microVM. Files,
+ packages and services stay from one step to the next.
+- `nixery` runs each step in a fresh container. Only the workspace
+ stays between steps.
+
+Use `microvm` if you are not sure. With `microvm`, `image` picks the
+image to boot: `nixos`, or a non-NixOS image such as `alpine`.
+
+## Tools
+
+On `microvm` with a NixOS image, `dependencies` is a list of nixpkgs
+packages. You can search for packages on https://search.nixos.org.
```yaml
dependencies:
- nodejs
- pnpm
- - github:nixos/nixpkgs/nixos-unstable#bun
```
-Use `registry` to pin `nixpkgs` once instead of on every entry, and
-`substituters` to pull prebuilt paths from an extra binary cache. On
-non-NixOS images, install packages in a step with the image's package
-manager (`apk add go`, `dnf install -y go`).
+Take a package from any flake with `flakeref#attr`:
+
+```yaml
+dependencies:
+ - github:nixos/nixpkgs#hello
+```
+
+On `nixery`, `dependencies` is a map from registry to packages:
+
+```yaml
+dependencies:
+ nixpkgs:
+ - nodejs
+ - go
+```
+
+On a non-NixOS image, install tools in a step:
+
+```yaml
+steps:
+ - name: "Install go"
+ command: apk add go
+```
+
+## Steps
+
+Each step has a `name` and a `command`. The step runs in a Bash shell.
+Its logs show on the pipelines page.
+
+```yaml
+steps:
+ - name: "Build"
+ command: go build ./...
+ - name: "Test"
+ command: go test ./...
+```
+
+Use `|` for a command with more than one line:
+
+```yaml
+steps:
+ - name: "Lint and test"
+ command: |
+ pnpm run lint
+ pnpm test
+```
-## Environment and secrets
+## Environment variables
-`environment` sets variables for every step; a step's own `environment`
-overrides it. These values are part of the repository and anyone can
-read them.
+Set variables for every step with `environment`. Set variables for one
+step with that step's `environment`:
```yaml
environment:
- NODE_ENV: production
+ NODE_ENV: "production"
+
+steps:
+ - name: "Build backend"
+ command: "go build"
+ environment:
+ GOOS: "darwin"
+ GOARCH: "arm64"
```
-For tokens and passwords, add a secret under **Settings → Pipelines →
-Secrets**. Secrets reach every step as environment variables and are
-masked in logs. See [Secrets](/docs/spindles/secrets/). The spindle also sets
-`CI=true` and `TANGLED_*` variables describing the run, such as
-`TANGLED_SHA` and `TANGLED_REF_NAME`.
+Do not put secrets here, these environment variables are visible to
+anyone viewing the repository. Add secrets in the repository's
+**Settings → Pipelines → Secrets**. They reach every step as environment
+variables. See [Secrets](/docs/spindles/secrets/).
+
+The spindle also sets `CI=true` and `TANGLED_*` variables that describe
+the run:
+
+```sh
+echo "$TANGLED_REF_NAME" # main
+echo "$TANGLED_SHA" # the commit that triggered the pipeline
+```
## Caching
-Keep dependency downloads between runs with `cache`:
+Keep directories between runs with `cache`:
```yaml
cache:
- - key: pnpm-store
- hash: [pnpm-lock.yaml]
- fallback: true
- paths: [.pnpm-store]
+ - key: go-mod
+ hash: [go.sum]
+ paths: [.gocache]
```
-`hash` ties the entry to the lockfile's content. `fallback: true`
-restores the previous entry when the lockfile changed, so the run only
-downloads what's new. Caches are only used for trusted runs, and only
-when the spindle operator configured cache storage.
+`hash` folds the content of `go.sum` into the key, so editing `go.sum`
+invalidates the cache. Caches are only used for trusted runs, and only when
+the spindle operator configured cache storage.
-## Clone options
+## Cloning
-By default the spindle fetches only the triggering commit (depth 1),
-plus tags and submodules. Fetch more history when a tool needs it, such
-as `git describe`:
+By default the spindle fetches only the commit that triggered the
+workflow. Fetch more history when a tool needs it, such as
+`git describe`:
```yaml
clone:
depth: 50
```
-Set `clone.skip: true` when the workflow doesn't need the repository at
-all.
+Skip the clone when the workflow needs nothing from the repository:
+
+```yaml
+clone:
+ skip: true
+```
## Recipes
@@ -353,6 +459,3 @@ steps:
export AWS_WEB_IDENTITY_TOKEN_FILE=/tmp/token
aws s3 sync dist/ s3://my-bucket/
```
-
-The [workflow reference](/docs/spindles/workflows/) documents every field
-used above.
diff --git a/docs/src/styles/tangled.css b/docs/src/styles/tangled.css
index b04fac9e8..a41f5e034 100644
--- a/docs/src/styles/tangled.css
+++ b/docs/src/styles/tangled.css
@@ -347,7 +347,7 @@ site-search dialog::backdrop {
display: none;
}
-/* the mobile menu toggle is a ghost button, like the theme toggle beside it */
+/* the mobile menu toggle is a ghost button */
starlight-menu-button button {
background-color: transparent;
box-shadow: none;
@@ -365,8 +365,7 @@ starlight-menu-button button:focus-visible {
outline-offset: 2px;
}
-/* on mobile the header reserves room for the fixed menu button: keep the gap
- between the theme toggle and that button at 0.5rem */
+/* on mobile the header reserves room for the fixed menu button, with a 0.5rem gap */
@media (max-width: 49.999rem) {
:root[data-has-sidebar] .header {
padding-inline-end: calc(0.5rem + var(--sl-nav-pad-x) + var(--sl-menu-button-size));
diff --git a/localinfra/pc/services/avatar/run b/localinfra/pc/services/avatar/run
index b6e8d1beb..cfccf1a1c 100755
--- a/localinfra/pc/services/avatar/run
+++ b/localinfra/pc/services/avatar/run
@@ -3,9 +3,10 @@ set -eu
export AVATAR_SHARED_SECRET=${AVATAR_SHARED_SECRET:-localinfra-avatar-secret}
export BOBBIN_URL=${BOBBIN_URL:-https://bobbin.tngl.boltless.dev}
export WRANGLER_SEND_METRICS=false
+export TNGL_AVATAR_INSPECTOR_PORT=${TNGL_AVATAR_INSPECTOR_PORT:-9230}
export SSL_CERT_FILE=$TNGL_CERTS/root.crt
export NODE_EXTRA_CA_CERTS=$TNGL_CERTS/root.crt
# wrangler keeps its local state in the project dir, so dev has to cd there first
-default='cd "$TNGL_ROOT/avatar" && exec "$AVATAR_BIN" dev --ip 127.0.0.1 --port "$TNGL_AVATAR_PORT" --var "AVATAR_SHARED_SECRET:$AVATAR_SHARED_SECRET" --var "BOBBIN_URL:$BOBBIN_URL"'
+default='cd "$TNGL_ROOT/avatar" && exec "$AVATAR_BIN" dev --ip 127.0.0.1 --port "$TNGL_AVATAR_PORT" --inspector-port "$TNGL_AVATAR_INSPECTOR_PORT" --var "AVATAR_SHARED_SECRET:$AVATAR_SHARED_SECRET" --var "BOBBIN_URL:$BOBBIN_URL"'
exec sh -c "${AVATAR_CMD:-$default}"
diff --git a/web/src/routes/login/+page.svelte b/web/src/routes/login/+page.svelte
index 135439981..cbd56ce75 100644
--- a/web/src/routes/login/+page.svelte
+++ b/web/src/routes/login/+page.svelte
@@ -120,10 +120,10 @@
icon={LogIn}
loading={login.loading}
spinnerClass="size-4"
- aria-label={login.loading ? "Signing in" : undefined}
+ aria-label={login.loading ? "Logging in" : undefined}
class="w-full typography-paragraph-large disabled:opacity-100"
>
- Sign in
+ Login