Skip to main content
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.

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. 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.