# Disks (/runners/disks)



StarSling Disks are persistent filesystems mounted into StarSling Runners as ordinary directories. Jobs read and write files in place, without packing, uploading, downloading, or extracting an archive. Use them for dependency stores, build output, toolchains, datasets, and reference data shared across a repository's jobs.

Each disk has a name scoped to its repository. A job can mount several disks, with one [Disks action](https://github.com/starslingdev/disks) step per disk. Many jobs can read the same published generation concurrently; each job's writes stay private until a successful publication makes them available to later jobs.

Disks are separate from the disk size on a runner label. Those tables describe the runner's local disk. See [Compute Specifications](https://docs.starsling.dev/runners/compute-sizing).

<Callout type="info">
  The [Disks action](https://github.com/starslingdev/disks) is being prepared for release in its public GitHub repository. The examples below use the planned `starslingdev/disks@v0` release. Your organization must be enrolled for Disks. Once released, pin a full commit SHA for production workflows.
</Callout>

## Get started

Use a [StarSling runner label](https://docs.starsling.dev/runners/instance-types), grant the job `id-token: write`, and put the Disks step before the tool writes to the target directory. The OIDC token lets StarSling verify the job; the action does not need a separate StarSling API token.

This workflow keeps pnpm's store warm. It asks to publish from the repository's default branch, while pull requests and other branches use the published contents and discard their own writes.

```yaml
name: Build
on: [push, pull_request]

jobs:
  build:
    runs-on: starsling-ubuntu-26.04
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@v4

      - uses: starslingdev/disks@v0
        with:
          preset: pnpm-store
          publish: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && 'on-change' || 'never' }}

      - run: pnpm install --frozen-lockfile
      - run: pnpm test
```

The first use of a new name mounts an empty directory. We call that a **cold** mount. The first successful publication populates the disk; later jobs mount its published generation **warm**. A step with `publish` omitted defaults to `never` and does not keep its writes for later jobs.

## How jobs share a disk

A **generation** is the disk's single published state. Each job reads one fixed generation, selected at its first mount. Another job publishing while it runs does not change the files it sees. Disks currently allow one publisher at a time and many concurrent readers.

* **Only a default-branch job can publish.** Same-repository pull requests and jobs on other branches or tags can read, but cannot publish. A request to publish from those jobs is denied while the disk remains usable.
* **One publisher at a time.** If concurrent jobs ask to publish the same disk, only one gets the publisher lease. The others report `lease-held` and discard their writes at job end. Writes are never merged; serialize publishing jobs for a disk when every job's changes must land.
* **Publication waits for success.** StarSling publishes after the job succeeds. Failed or cancelled jobs leave the shared generation unchanged. There is no save step, and a publication failure does not change the job's result.

Fork pull requests, Dependabot pull requests, and events outside the read allow-list receive no mount. A job without `id-token: write` also runs without its disks. See the action's [error codes and failure handling](https://github.com/starslingdev/disks/blob/main/docs/errors-and-failures.md) for how to handle these cases.

[Let us know](mailto:founders@starsling.dev) if you need hundreds of CI jobs to write to distinct files in the same generation concurrently.

## Auto-mount and paths

Before the steps of a trusted job with `id-token: write`, the runner mounts published disks under the job user's home whose last publication allowed auto-mount. These mounts are read-write; writes remain private and are discarded unless a step asks to publish them. The job log's StarSling group lists each disk's path, access, and generation, or the reason it did not mount.

Workspace disks and disks published with `auto-mount: false` always need an explicit action step. If job verification is still in progress before the steps run, home-directory disks also need the step for reads; the job log reports that case.

The action resolves the mount path in this order:

1. The step's explicit `path`.
2. The disk's stored path.
3. The preset's default path.
4. `~/.disks/<name>` when no stored path or preset supplies one.

A leading `~/` resolves under the job user's home; a relative path resolves under `GITHUB_WORKSPACE`. Targets must be empty and within the job home or workspace, and cannot overlap another disk. The workspace root itself, Docker's data root, and container jobs are outside the action's scope.

Resolved paths must contain only printable ASCII characters without spaces. Paths containing whitespace or non-ASCII characters, and `~user` paths, fail with `invalid-input` even when `on-degraded: continue` is set.

If the disk is already mounted, the step can attach a publish intent to it. Earlier writes in the same job count toward publication too. A repeat asking for a different path or access fails with `conflicting-repeat`; it cannot turn a pre-job read-write mount into a read-only one.

### Build output in the workspace

Mount workspace disks after checkout and before the build tool writes to the directory. For example, a Rust job can keep `target/` across runs. This step belongs in a job with the same runner and permissions as the quickstart:

```yaml
- uses: actions/checkout@v4

- uses: starslingdev/disks@v0
  with:
    name: rust-target
    path: target
    publish: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && 'on-change' || 'never' }}

- run: cargo test --locked
```

For dependencies, the `cargo` preset mounts the registry, rather than all of `~/.cargo`, where the runner keeps rustup's binaries. Leave `CARGO_HOME` unset. Cargo Git dependencies need a separate named disk; build output uses a workspace path as above.

### Read-only reference data

Use `access: read-only` with `publish: never` for datasets or fixtures the job must not rewrite. If the disk lives under the home directory, its publisher must store `auto-mount: false` so the reader can request read-only access before it is mounted. `on-degraded: fail` stops the step if that access cannot be supplied.

A cold mount is still a successful mount: strict handling does not prove that reference data exists. Check the `mount` output or validate the expected files when the job requires an existing generation.

## Publish modes

Every publishing mode waits until the job succeeds and requires the job to be the disk's publisher.

| `publish`    | When writes become a new generation                                                        |
| ------------ | ------------------------------------------------------------------------------------------ |
| `never`      | Never; changes stay private to this job and are discarded.                                 |
| `on-change`  | The change test detects a change, or the step's settings differ from the stored ones.      |
| `always`     | After success, even if nothing changed.                                                    |
| `if-missing` | Only when no usable generation exists. An existing usable generation reports `not-needed`. |

The change test is what `publish: on-change` counts as a change. `content-only` counts file contents, types, owners, modes, and links. `content-and-mtime` also counts modification times, for tools that read them as freshness. The preset supplies the change test; the action has no separate change-test input.

The last successful default-branch publication stores the preset, size cap, change test, and auto-mount setting. Omitted settings return to their defaults: an omitted `size-cap` uses the preset's or API default, rather than keeping a previously customized cap.

## Disk lifetime and changes

A disk retires after a publication over its size cap, after 30 days without any step naming it, or when a new name's first publication takes its path. A retired disk mounts cold; its next publication starts again from that job's writes. Rewriting files can still grow the disk toward its cap.

To move a published disk, use a new name. Publishing the old name at a different path reports `settings-mismatch`. To replace a name at the same path, first publish the old name with `auto-mount: false`, then publish the new name there; that first publication retires the old disk. A reader can choose another path for one job only if the runner has not already mounted the disk.

## Presets and limits

[List StarSling Disks presets](https://docs.starsling.dev/api/disks/get-disk-presets) returns every preset a step may name in `preset`, plus the limits every disk is held to. The catalog is the same for every caller.

[List an org's StarSling Disks](https://docs.starsling.dev/api/disks/list-disks) returns the disks an organization's repositories keep. Each entry includes the mount path, what the disk currently holds, and when that was published. Pass `repository_id` to limit the list to one repository. The caller must be a member of the org.

## Security and deletion

StarSling Disks encrypts file contents and filenames at rest using a unique encryption key for each immutable storage layer. Deleted Disks data is removed within **48 hours**, and account deletion includes stored Disks.

See [Data Handling](https://docs.starsling.dev/security/data-handling#starsling-disks) for key scope, retention, and the [deletion process](https://docs.starsling.dev/security/data-handling#disks-deletion). The 48-hour billing delay below is separate from the data-deletion guarantee.

## Price

Storage is **$0.50 per GB-month**, with no per-job attach fee. We calculate usage by snapshotting your total storage once a day, so billed usage reflects changes in the amount you store over the month. When you delete data, you may still be billed for that storage for up to **48 hours**.

Organizations on the free trial do not get Disks storage. See [Pricing](https://docs.starsling.dev/pricing#disks).

<Cards>
  <Card title="Presets" href="/api/disks/get-disk-presets" description="Preset directories and the limits every disk is held to" />

  <Card title="Your disks" href="/api/disks/list-disks" description="Where each disk mounts, and what it currently holds" />

  <Card title="Pricing" href="/pricing#disks" description="Storage rate and who can keep Disks" />
</Cards>
