> ## Documentation Index
> Fetch the complete documentation index at: https://docs-stg.outerlayer.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Prepare builds with a setup script

> Commit .outerlayer/setup.sh to install a repository's dependencies once, and let later builds start from the state an earlier build saved.

A repository commits one file, `.outerlayer/setup.sh`. It installs the
repository's dependencies. The runner runs it in every container and microVM
build. After the scripts pass, the runner saves the build's state. A later
build with the same key starts from that state, so it does not install
everything again.

## Write the script

The script is a shell script. Put the commands that prepare a checkout in it,
such as `npm ci` or `pip install -r requirements.txt`.

The runner runs each script in its own repository's checkout, as the build
user. It runs the primary repository's script first. It runs the script after
the clone, the recipe's lifecycle commands and `mise install`. It runs the
script before the build's command.

The script runs in every build, even a build that starts from a saved state.
So write it to run twice without harm. On a saved state the second run takes
seconds, because the work is already done.

The script reaches only the hosts that `runner.build.allowHosts` allows. It
goes through the build's tunnel, as every other command does.

## What ends the build

A script that exits with a code other than 0 ends the build at stage
`provision`. A script that runs past `runner.build.setupMinutes` does the same.
The default limit is 30 minutes for each script.

The runner's log names the script, the exit code or the limit, and the last 20
lines the script printed. The build's command does not start, and the runner
saves no state.

## In host checks

[Host checks](https://docs-stg.outerlayer.ai/reference/build-reference.md#host-checks) run in a new container
or microVM, so nothing the build installed is there. The runner runs each
script again in that environment. It runs the scripts after the lifecycle
commands and before the first check. A check can then use what the script
installed, such as `node_modules`.

The script runs from the branch the build pushed. Each script has the same
limit as in the build, `runner.build.setupMinutes`. A script that fails, or
runs past the limit, fails every host check of the build. The reason names
the script.

The check environment never starts from a saved state, so the script does all
its work there.

## Repositories with no script

When a repository has no `.outerlayer/setup.sh`, the build runs without it. The
runner sets `OUTERLAYER_MISSING_SETUP_SCRIPTS` for the build's command. Its
value is a JSON list of the repositories that have no script, such as
`["acme/app"]`. The `outerlayer-build` skill reads it. The agent writes the
script from the commands it ran to prepare the checkout, and the build's pull
request adds the file.

A build that finds no script saves no state.

## The saved state

A saved state is a copy of the build user's home and the checkouts. A container
build keeps it in a Docker volume. A microVM build keeps it in a tar file under
`<runner directory>/saved-states/`. A record next to it holds the key and the
age.

The runner saves a state when all of these hold:

* Every setup script passed, and at least one script ran.
* No usable saved state had the build's key.
* Each checkout started at the head of its default branch.
* The build's image is the universal image, not an image built from a
  devcontainer file.
* The runner is not a managed runner.
* `runner.housekeeping.savedStateMax` is not `off`.

Before the runner copies the disk, it stops every process that the script left
running and flushes the file system. The runner marks the state usable only
after the copy ends.

A build never changes the state it starts from. It works on a copy.

### The key

A saved state is used only by a build that matches it in all of these:

* The factory.
* The repositories, in order.
* The universal image.
* The size of the build disk, `runner.build.disk`.
* The content of each repository's setup script and version files.

The runner reads the setup script and the version files from each repository's
default branch, never from the branch a build works on. A build cannot choose
the state that later builds use. The version files are the files in
[The runtimes in the universal image](https://docs-stg.outerlayer.ai/build-image-from-devcontainer.md#the-runtimes-in-the-universal-image).
For `go.mod`, only the `go` and `toolchain` lines count. The tools layer is not
in the key, so a CLI update does not discard saved states.

### When a build starts from a fresh disk

A build starts from a fresh disk, and saves a new state, in these cases:

* No saved state has the key.
* The saved state is seven days old or older.
* A setup script or a version file changed on the default branch. The runner's
  log names the file.
* A checkout in the saved state cannot switch to the claim's branch. The runner
  discards the saved state and provisions the build again. The log says why.
* The build user has another numeric id than it had when the state was saved.

When a build starts from a saved state, the log says so and gives the state's
age.

## Disk

`runner.housekeeping.savedStateMax` limits the total size of one runner's saved
states. The default is `20g`. Set it to `off` to save no states.

A saved state carries a label of its own, so the sweep that removes finished
builds leaves it. Housekeeping removes a state that is seven days old. When the
total passes `savedStateMax`, housekeeping removes the oldest states first.

Before the runner stops claiming work because free disk is low, it removes its
own oldest saved states.

On Docker Desktop and in the Lima VM, a container build keeps `/build` in a
Docker volume inside the VM. The runner copies the volume inside the VM. The
copy does not use Docker Desktop's file sharing. The VM's disk also holds every image
and volume on the Mac. `outerlayer runner init --vm` adds room for
`savedStateMax` to the VM's disk.

## Secrets in the saved state

A file the script writes stays in the saved state. For example, an `~/.npmrc`
with a token from a build variable stays. Only builds of the same factory use
the state, so the file stays inside the factory. Do not write a secret into a
file when you can avoid it.

## What does not use it

* A build whose image comes from a devcontainer file. It runs the script, and
  saves no state.
* A build on a managed runner. It runs the script, and saves no state.
* A process build. It runs no script.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.