···183183 SPINDLE_MICROVM_PIPELINES_AGENT_PORT: "11240"
184184 SPINDLE_S3_LOG_BUCKET: ""
185185 SPINDLE_MICROVM_PIPELINES_ENABLE_CGROUPS: "false"
186186+ SPINDLE_CACHE_BACKEND: disk
186187 # route guest nix substitution + uploads through the local ncps cache.
187188 # ncps re-signs on serve with cache.local's key, so the guest trusts the
188189 # matching public key below (no signing happens in spindle itself).
···965965- `TANGLED_PR_SOURCE_SHA` - The commit SHA of the source
966966 branch
967967968968+### Cache
969969+970970+The `cache` field lets a workflow persist directories across
971971+pipeline runs. Before the first step, the engine looks up
972972+each entry's key and extracts the matching archive into the
973973+workspace; after all steps succeed, the paths are archived
974974+again and stored back under the key.
975975+976976+- `key`: name this cache is saved under. Keys are scoped to
977977+ the repository and the engine specified.
978978+- `hash`: **optional** list of repo files (lockfiles,
979979+ manifests) whose content is folded into the key. The entry
980980+ is stored as `<key>-<hash of those files>`, so editing
981981+ `go.sum` automatically rotates the cache without bumping
982982+ the key by hand. Paths are relative to the repository
983983+ root and are read from git at the commit being built.
984984+- `paths`: paths to archive. Relative paths are anchored at
985985+ the repository checkout, the directory steps start in
986986+ (`/workspace/repo` on microvm, `/tangled/workspace` on
987987+ nixery). Absolute paths work too, for caching directories
988988+ outside the checkout, but note they name engine-specific
989989+ locations. All paths must be writable by the CI user and
990990+ contain no spaces.
991991+- `compression-level`: **optional** zstd level, `1`
992992+ (fastest) to `19` (smallest). defaults to (`5`).
993993+- `when`: **optional** save policy: `on-success` (the
994994+ default) or `always`.
995995+996996+When the exact key (or generation, with `hash`) misses, the
997997+newest older generation under the same key is restored.
998998+999999+```yaml
10001000+cache:
10011001+ - key: go-mod
10021002+ hash:
10031003+ - go.sum
10041004+ - go.mod
10051005+ paths:
10061006+ - .gocache
10071007+```
10081008+10091009+Caches are only restored and saved for trusted pipelines
10101010+(pushes and same-repository pull requests). Pipelines
10111011+building untrusted code, like pull requests from forks,
10121012+skip the cache entirely. Saving follows each entry's `when`
10131013+policy, except on timeout, when nothing is saved. A cache
10141014+miss or failure never fails the workflow.
10151015+10161016+The spindle operator chooses the storage backend. See
10171017+[Running spindle](#running-spindle). If no backend is
10181018+configured, `cache` entries are ignored.
10191019+9681020### Steps
96910219701022The `steps` field allows you to define what steps should run
···15091561 trusted public keys for those caches.
15101562- `SPINDLE_NIX_CACHE_UPLOAD_URL`: Cache URL that paths built
15111563 in the guest are uploaded to.
15641564+15651565+The generic CI cache (the workflow-level
15661566+[`cache`](#cache) field) is configured via prefix
15671567+`SPINDLE_CACHE_`.
15681568+15691569+- `SPINDLE_CACHE_BACKEND`: Storage backend, `disk` or `s3`
15701570+ (default: `""`, caching disabled).
15711571+- `SPINDLE_CACHE_DISK_DIR`: Directory for the `disk` backend
15721572+ (default: a `cache` directory next to the spindle
15731573+ database).
15741574+- `SPINDLE_CACHE_S3_BUCKET`: Unversioned bucket for the `s3`
15751575+ backend. Credentials come from the standard AWS chain and
15761576+ need `s3:GetBucketVersioning` in addition to object access.
15771577+- `SPINDLE_CACHE_S3_PREFIX`: Key prefix inside the bucket
15781578+ (default: `"spindle/cache"`).
15791579+- `SPINDLE_CACHE_RETENTION`: Time since the last restore or
15801580+ save before an entry is deleted (default: `720h`, or 30
15811581+ days). Set to `0` to keep entries indefinitely.
15821582+- `SPINDLE_CACHE_PRUNE_INTERVAL`: How often expired entries
15831583+ are deleted (default: `1h`).
15841584+15851585+Cache metadata and usage are tracked in spindle's SQLite
15861586+database. Storage backends contain opaque objects and are
15871587+never listed during lookup or cleanup.
1512158815131589### Running spindle
15141590
···3030 });
3131 # we don't include gnused, xxd etc. here because busybox has them
3232 # we want to keep the image this image small!
3333- guestTools = [nix bash git curl jq];
3333+ # zstd is not a busybox applet, and the spindle cache saves tar|zstd
3434+ guestTools = [nix bash git curl jq pkgsStatic.zstd];
34353536 # run by busybox at sysinit
3637 setupScript = writeText "spindle-setup" ''
···107107 updated_at text not null
108108 );
109109110110+ create table if not exists cache_entries (
111111+ id text primary key,
112112+ storage_key text unique not null,
113113+ owner_did text not null,
114114+ repo_did text not null,
115115+ engine text not null,
116116+ cache_key text not null,
117117+ cache_hash text not null,
118118+ size_bytes integer not null default 0,
119119+ state text not null check (state in ('pending', 'ready', 'deleting')),
120120+ created_at integer not null,
121121+ last_used_at integer not null
122122+ );
123123+124124+ create index if not exists cache_entries_lookup
125125+ on cache_entries (repo_did, engine, cache_key, cache_hash, created_at desc)
126126+ where state = 'ready';
127127+ create index if not exists cache_entries_ready_expiry
128128+ on cache_entries (last_used_at) where state = 'ready';
129129+ create index if not exists cache_entries_pending_expiry
130130+ on cache_entries (created_at) where state in ('pending', 'deleting');
131131+ create index if not exists cache_entries_owner_usage
132132+ on cache_entries (owner_did) where state in ('ready', 'deleting');
133133+110134 create table if not exists pipelines (
111135 id text primary key,
112136 repo_did text not null,
···145169 return nil, err
146170 }
147171148148- return &DB{db}, nil
172172+ return &DB{DB: db}, nil
149173}
150174151175func runMigrations(_ context.Context, conn *sql.Conn, logger *slog.Logger) error {