139 lines
7.8 KiB
Markdown
139 lines
7.8 KiB
Markdown
---
|
|
id: doc-4
|
|
title: GitHub Actions → Tangled Spindles
|
|
type: other
|
|
created_date: "2026-05-03 16:57"
|
|
updated_date: "2026-05-04 06:54"
|
|
---
|
|
|
|
# CI Pipeline Porting Analysis: GitHub Actions → Tangled Spindles
|
|
|
|
## Current State (GitHub Actions)
|
|
|
|
Three workflows in `.github/workflows/`:
|
|
|
|
1. **`test_and_deploy.yml`** — 3 jobs: `lint` (parallel) + `test` (parallel) → `deploy` (sequential, main only, requires environment approval)
|
|
2. **`verify.yml`** — Manual workflow, re-runs production verification (hurl)
|
|
3. **`dependency_submission.yml`** — Reports Mix dependencies to GitHub dependency graph
|
|
|
|
Key GHA features used: `jdx/mise-action`, `actions/cache`, concurrency control, environment approvals, Fluxon private Hex repo.
|
|
|
|
## Tangled's CI Capabilities (Spindles)
|
|
|
|
Tangled provides **Spindles** — a CI runner service. Workflows are YAML files in `.tangled/workflows/`.
|
|
|
|
**Available features:**
|
|
|
|
- Triggers: `push`, `pull_request`, `manual` — with branch/tag glob filtering
|
|
- Engine: `nixery` (uses Nixpkgs packages via Nixery at `nixery.tangled.sh`)
|
|
- Dependencies: declared from nixpkgs (or custom registries)
|
|
- Environment: global and per-step env vars
|
|
- Steps: sequential only, each runs a Bash command in a fresh Docker container
|
|
- Secrets: repository-level, stored in OpenBao (or SQLite for self-hosted)
|
|
- Auto env vars: `CI`, `TANGLED_REF`, `TANGLED_SHA`, `TANGLED_REF_NAME`, etc.
|
|
- Clone: depth, submodules configurable
|
|
- Volumes persisted across steps within a run: `/tangled/workspace`, `/nix`
|
|
- Timeout: configurable via `SPINDLE_PIPELINES_WORKFLOW_TIMEOUT` (default 5m)
|
|
|
|
**Missing features (vs GitHub Actions):**
|
|
|
|
- ❌ No job dependencies (`needs`) — all steps in a workflow are strictly sequential
|
|
- ❌ No parallel jobs — single linear pipeline per workflow file
|
|
- ❌ No inter-run caching primitive — no equivalent to `actions/cache`
|
|
- ❌ No concurrency control — no `cancel-in-progress`
|
|
- ❌ No environment approvals — no GitHub Environments equivalent
|
|
- ❌ No matrix builds
|
|
- ❌ No built-in coverage threshold enforcement
|
|
- ❌ No dependency graph submission
|
|
|
|
Tangled also provides **Webhooks** (push events, HMAC-signed, 3 retries with backoff).
|
|
|
|
## Caching Profile (Corrected)
|
|
|
|
### What IS cached (between runs, via Docker layer caching)
|
|
|
|
Nixery builds OCI images with each nixpkgs package in a **separate Docker layer**. Docker reuses pre-existing layers, so any nixpkgs dependency declared in the `dependencies` block — `mise`, `nodejs`, `shellcheck`, `hurl`, `sqlite` — is cached at the Docker level between runs.
|
|
|
|
This is confirmed by the Tangled team: _"caching for commonly used packages is free thanks to Docker (pre-existing layers get reused)."_ — [Introducing Spindle blog post](https://blog.tangled.org/ci/)
|
|
|
|
### What is NOT cached (fetched/compiled fresh every run)
|
|
|
|
| Artifact | Reason |
|
|
| -------------------------------- | ------------------------------------------------------- |
|
|
| Elixir 1.20.0-rc.4, Erlang 28.5 | Not in nixpkgs; `mise install` downloads fresh each run |
|
|
| `mix deps` + `_build` | No directory-level cache primitive in Spindle |
|
|
| `npm` packages + asset builds | Same — no cache primitive |
|
|
| Fluxon private Hex repo packages | Fetched each run |
|
|
|
|
### Within a run
|
|
|
|
Per the blog post: _"the `/tangled/workspace` and `/nix` volumes persisted across steps."_ So within a single workflow run, compiled artifacts are available to subsequent steps. But between runs, everything resets except the Nixery Docker layers.
|
|
|
|
### Potential caching strategy: Custom Nix derivation for Elixir 1.20.0-rc.4
|
|
|
|
Spindle supports **custom registries** in the dependencies block. If Elixir 1.20.0-rc.4 were packaged as a Nix derivation and made available via a custom registry (e.g., hosted on Tangled itself), it would benefit from the same Docker layer caching as other nixpkgs packages. This would eliminate the biggest uncached download.
|
|
|
|
Similarly, Erlang 28.5 could be pinned via a custom derivation if not in nixpkgs.
|
|
|
|
## Dependency Availability in Nixpkgs
|
|
|
|
| Tool | In nixpkgs stable? | Version | Notes |
|
|
| ------------ | ------------------ | ----------------- | ------------------------------------------------------------ |
|
|
| `elixir` | ✅ | 1.18.4 / 1.19.5 | **Not 1.20.0-rc.4** — must use mise or custom Nix derivation |
|
|
| `erlang` | ✅ | via beam packages | OTP versions tied to package sets |
|
|
| `nodejs` | ✅ | Yes | |
|
|
| `mise` | ✅ | 2025.11.7 | Can install exact Elixir/Erlang/Node from mise.toml |
|
|
| `shellcheck` | ✅ | Yes | |
|
|
| `hurl` | ✅ (likely) | | |
|
|
| `sqlite` | ✅ | Yes | |
|
|
| `credo` | ❌ | Elixir mix task | Must install via mix after Elixir is available |
|
|
| `sobelow` | ❌ | Elixir mix task | Same |
|
|
| `mix_audit` | ❌ | Elixir mix task | Same |
|
|
|
|
The workflow pattern with mise:
|
|
|
|
1. Add `mise` as a nixpkgs dependency (cached via Docker layers ✅)
|
|
2. `mise install` to fetch Elixir 1.20.0-rc.4, Erlang 28.5, Node 25.9.0 (NOT cached ❌)
|
|
3. Run existing mise tasks
|
|
|
|
## Implementation Routes
|
|
|
|
### Route A: Pure Spindle — Single Linear Workflow
|
|
|
|
One `.tangled/workflows/ci.yml` that runs lint → test → deploy sequentially.
|
|
|
|
**Pros:** Simplest — one file, fully self-contained on Tangled.
|
|
**Cons:** Slowest (no parallelism between lint+test), deploy runs on every PR push to main, no separation of concerns.
|
|
|
|
### Route B: Multiple Spindle Workflows — Split CI/CD
|
|
|
|
Separate files: `lint.yml`, `test.yml`, `deploy.yml`. Deploy is manual-trigger only.
|
|
|
|
**Pros:** Lint and test can trigger on PRs, deploy is intentional. Better separation of concerns.
|
|
**Cons:** No enforcement that test passes before manual deploy. Lint and test still run sequentially within each workflow.
|
|
|
|
### Route C: Spindle for CI + Webhook for Deploy
|
|
|
|
Spindle handles lint+test on PRs. A Tangled webhook on push to main triggers Coolify deployment via an external endpoint.
|
|
|
|
**Pros:** Deploy runs independently. Can add gating logic in webhook receiver.
|
|
**Cons:** Requires a webhook receiver (external service). More infrastructure.
|
|
|
|
### Route D: Spindle for All — Lean Pipeline (Accept Gaps)
|
|
|
|
Full pipeline on Spindle but accept the missing features. Configure longer timeout, accept sequential runs, use manual deploy trigger.
|
|
|
|
**Pros:** Fully native Tangled experience.
|
|
**Cons:** All gaps accepted as-is. May frustrate PR workflow.
|
|
|
|
### Route E: Mirror to GitHub + Keep Actions
|
|
|
|
Mirror the Tangled repo to GitHub (or use Tangled as a mirror of GitHub) and keep existing Actions.
|
|
|
|
**Pros:** Zero CI changes. All existing features preserved.
|
|
**Cons:** Not "replicating CI on tangled.org." Maintains GitHub dependency. Mirror sync complexity.
|
|
|
|
## Additional: Custom Nix Derivation Strategy (Orthogonal Enhancement)
|
|
|
|
Regardless of route chosen, a custom Nix derivation for Elixir 1.20.0-rc.4 could be hosted on Tangled as a custom registry, making it available as a cached nixpkgs-style dependency. This eliminates the mise download bottleneck (~2-5 min per run) and leverages Docker layer caching. This is an orthogonal optimization that benefits any Spindle-based route.
|