Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 23 additions & 22 deletions .github/workflows/build-reference.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ on:
permissions:
contents: read

concurrency:
group: build-reference-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

jobs:
build:
runs-on: [self-hosted, tensorlake, tensorlake-small]
Expand All @@ -18,28 +22,12 @@ jobs:
- name: Check out repository
uses: actions/checkout@v6

- name: Verify persistent cache mount
shell: bash
run: |
if [[ -z "${TENSORLAKE_CACHE_DIR:-}" ]]; then
echo "::error::The runner did not provide a persistent cache directory."
exit 1
fi
if ! mountpoint -q "${TENSORLAKE_CACHE_DIR}"; then
echo "::error::${TENSORLAKE_CACHE_DIR} is not a mounted Cloud Volume."
exit 1
fi

- name: Set up uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
- name: Set up cached uv environment
id: uv-environment
uses: ./actions/setup-uv-cache
with:
cache-local-path: /mnt/tensorlake-cache/uv

- name: Install Python
run: uv python install 3.11

- name: Install dependencies
run: uv sync --locked --all-extras
python-version: "3.11"
sync-args: --locked --all-extras

- name: Build Python package
run: uv build
Expand All @@ -54,7 +42,20 @@ jobs:
run: bash -n scripts/*.sh

- name: Run tests
run: uv run --no-sync pytest -q
id: tests
shell: bash
run: |
started_ns="$(date +%s%N)"
uv run --no-sync pytest -q
finished_ns="$(date +%s%N)"
duration_ms="$(( (finished_ns - started_ns) / 1000000 ))"
duration_seconds="$(awk -v ms="${duration_ms}" 'BEGIN { printf "%.3f", ms / 1000 }')"

echo "duration-ms=${duration_ms}" >> "${GITHUB_OUTPUT}"
echo "TEST_TIMING duration_ms=${duration_ms} environment_state=${TENSORLAKE_UV_ENVIRONMENT_STATE}"
{
echo "- Test duration: \`${duration_seconds}s\`"
} >> "${GITHUB_STEP_SUMMARY}"

- name: Verify Docker
run: docker run --rm hello-world
111 changes: 70 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ keeps the GitHub contract and removes the AWS scheduling layer:
- `github_runner_orchestrator/cache.py` - Per-repository cache volume naming and provisioning.
- `github_runner_orchestrator/github.py` - GitHub App JWT, installation token, and JIT runner APIs.
- `github_runner_orchestrator/webhook.py` - HMAC verification and `workflow_job` filtering.
- `actions/setup-uv-cache/action.yml` - Reusable uv environment archive cache action.
- `actions/setup-rust-cache/action.yml` - Reusable Cargo, sccache, and target cache action.
- `sandbox-image/Dockerfile` - Reusable GitHub Actions runner sandbox image built from an OCI base.
- `uv.lock` - Locked application and development dependency versions.

Expand Down Expand Up @@ -279,23 +281,57 @@ input. Its default `enable-cache: auto` mode does not upload a GitHub Actions ca
self-hosted runner:

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

- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
cache-local-path: /mnt/tensorlake-cache/uv

- run: uv sync --locked
```

`setup-uv` sets `UV_CACHE_DIR` for the job from this input. If cache provisioning is unavailable,
the same path is created on the sandbox's ephemeral disk, so a workflow that omits the verification
step still runs without persistence.

The cache and `.venv` are on different file systems, so uv may copy artifacts instead of
hard-linking them. The volume still avoids repeated downloads and source builds, but benchmark the
result for dependency sets dominated by small prebuilt wheels.

For a dependency set where those cross-filesystem copies are expensive, keep the live environment
on the sandbox's local disk and store a compressed, compatibility-keyed environment archive on the
volume with the repository's composite action:

```yaml
concurrency:
group: uv-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

- uses: actions/checkout@v6

- name: Set up cached uv environment
id: uv-cache
uses: tensorlakeai/tensorlake-github-runners/actions/setup-uv-cache@main
with:
python-version: "3.11"
sync-args: --locked --all-extras

- run: uv run --no-sync pytest
```

`setup-uv-cache` installs uv and the requested Python version, restores a warm archive when one
exists, runs `uv sync`, and atomically publishes the environment after a cold sync. It accepts
`working-directory`, `lockfile`, and `cache-version` inputs for monorepos and manual invalidation.
Its `state`, `key`, `restore-duration-ms`, `sync-duration-ms`, and `publish-duration-ms` outputs are
also written to the job summary. Pin the action to a release tag or full commit in production
instead of tracking `main`.

The archive key separates incompatible operating systems, CPU architectures, exact Python versions,
jobs, Git refs, working directories, and lockfiles. The local restore path is stable across runner
sandboxes so virtual environment script paths remain valid. The concurrency group serializes
workflows for the same ref, and the archive is published with an atomic rename so another run never
restores a partial file. Different refs get separate archives even when they use the same lockfile.

This layout avoids both cross-filesystem package installation and Python's small-file reads over
FUSE. It also presents the Cloud Volume autosave engine with one sequential archive instead of
thousands of independently changing environment files. Remove obsolete archives under
`${TENSORLAKE_CACHE_DIR}/uv-environment-archives-v1` when their refs or lockfiles are no longer
needed.

### Rust, Cargo, and sccache

For Rust compiler artifacts, prefer sccache over sharing a complete mutable Cargo `target` tree.
Expand All @@ -309,32 +345,26 @@ Give each compatible build family its own cache namespace:
cache: false

# Install sccache with your preferred pinned action, package, or runner image.
- name: Configure persistent Rust caches
shell: bash
env:
CACHE_NAMESPACE: rust-workspace-v1
run: |
platform="${RUNNER_OS}-${RUNNER_ARCH}"
cargo_home="${TENSORLAKE_CACHE_DIR}/cargo-home/${platform}"
sccache_dir="${TENSORLAKE_CACHE_DIR}/sccache/${platform}/${CACHE_NAMESPACE}"
mkdir -p "${cargo_home}" "${sccache_dir}"
echo "CARGO_HOME=${cargo_home}" >> "${GITHUB_ENV}"
echo "SCCACHE_DIR=${sccache_dir}" >> "${GITHUB_ENV}"
echo "SCCACHE_CACHE_SIZE=50G" >> "${GITHUB_ENV}"
echo "SCCACHE_BASEDIRS=${GITHUB_WORKSPACE}" >> "${GITHUB_ENV}"
echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"

- name: Set up persistent Rust caches
id: rust-cache
uses: tensorlakeai/tensorlake-github-runners/actions/setup-rust-cache@main
with:
cache-namespace: rust-workspace-v1
disable-incremental: "true"

- name: Build
run: |
sccache --start-server
cargo build --locked --workspace
sccache --show-stats

- name: Stop sccache
if: always()
run: sccache --stop-server || true
```

`setup-rust-cache` configures a filesystem-safe cache tree for the requested namespace and exports
`CARGO_HOME`, `SCCACHE_DIR`, `SCCACHE_CACHE_SIZE`, `SCCACHE_BASEDIRS`, and `RUSTC_WRAPPER`.
Toolchain and sccache installation stay under workflow control. Set `cache-sccache: "false"` when
sccache is unavailable, and use `sccache-cache-size` to change the default `50G` limit. The action
does not need to start the sccache server explicitly; the first wrapped compilation starts it.

Persisting `CARGO_HOME` reuses registry downloads, Git dependencies, and installed Cargo tools.
Authenticate private registries with environment variables; do not run `cargo login` against the
persistent `CARGO_HOME`, because it writes credentials. Cloud Volumes reconcile concurrent
Expand All @@ -346,15 +376,12 @@ Some release builds benefit from reusing the complete `target` directory. Give e
build family an explicit versioned namespace:

```yaml
- name: Select Cargo target cache
shell: bash
env:
- name: Set up persistent Rust caches
uses: tensorlakeai/tensorlake-github-runners/actions/setup-rust-cache@main
with:
# Include the runtime ABI, Rust target, profile, and a manual version bump.
CACHE_NAMESPACE: rust-release-glibc-2.35-x86_64-unknown-linux-gnu-v2
run: |
target_dir="${TENSORLAKE_CACHE_DIR}/cargo-target/${CACHE_NAMESPACE}"
mkdir -p "${target_dir}"
echo "CARGO_TARGET_DIR=${target_dir}" >> "${GITHUB_ENV}"
cache-namespace: rust-release-glibc-2.35-x86_64-unknown-linux-gnu-v2
cache-target: "true"

- run: cargo build --locked --release --target x86_64-unknown-linux-gnu
```
Expand Down Expand Up @@ -414,11 +441,13 @@ volume snapshots for disposable cache data.

## Self-Test Workflow

`.github/workflows/build-reference.yml` runs on a `tensorlake-small` runner. It points uv at the
persistent volume automatically when available and uses the sandbox-local uv cache otherwise. It
installs and lints the Python project, builds its distribution, validates the setup scripts, runs
the test suite, and runs Docker's `hello-world` image to verify the runner's Docker daemon. The
reusable runner sandbox image still builds through `scripts/build-runner-image.sh`, because
`tensorlake/ubuntu-systemd` is a Tensorlake registered base rather than a public Docker Hub image.
Once the organization webhook is configured, pushes to `main`, pull requests, and manual dispatches
exercise the runner implementation from its own repository.
`.github/workflows/build-reference.yml` runs on a `tensorlake-small` runner. It keeps uv's live
environment on local sandbox storage and caches a compatibility-keyed compressed environment on the
persistent volume. The job summary records cold/warm state plus archive restore, `uv sync`, archive
publish, and test durations. It installs and lints the Python project, builds its distribution,
validates the setup scripts, runs the test suite, and runs Docker's `hello-world` image to verify the
runner's Docker daemon. The reusable runner sandbox image still builds through
`scripts/build-runner-image.sh`, because `tensorlake/ubuntu-systemd` is a Tensorlake registered base
rather than a public Docker Hub image. Once the organization webhook is configured, pushes to
`main`, pull requests, and manual dispatches exercise the runner implementation from its own
repository.
162 changes: 162 additions & 0 deletions actions/setup-rust-cache/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
name: Set up Tensorlake Rust cache
description: Configure Cargo, sccache, and optional target caches on a Tensorlake Cloud Volume.

inputs:
cache-namespace:
description: Compatibility namespace shared only by equivalent Rust builds.
required: true
cache-version:
description: Manual cache namespace version used for invalidation.
required: false
default: v1
working-directory:
description: Rust workspace directory used to normalize sccache paths.
required: false
default: .
cache-cargo-home:
description: Persist CARGO_HOME for registry downloads, Git dependencies, and installed tools.
required: false
default: "true"
cache-sccache:
description: Configure SCCACHE_DIR and RUSTC_WRAPPER. sccache must already be installed.
required: false
default: "true"
sccache-cache-size:
description: Maximum size passed through SCCACHE_CACHE_SIZE.
required: false
default: 50G
cache-target:
description: Persist CARGO_TARGET_DIR. Serialize writers and use only with compatible builds.
required: false
default: "false"
disable-incremental:
description: Set CARGO_INCREMENTAL=0 so Rust compilations are eligible for sccache.
required: false
default: "false"

outputs:
key:
description: Filesystem-safe key derived from the namespace, version, OS, and architecture.
value: ${{ steps.configure.outputs.key }}
cargo-home:
description: Persistent CARGO_HOME, or an empty string when disabled.
value: ${{ steps.configure.outputs.cargo_home }}
sccache-dir:
description: Persistent SCCACHE_DIR, or an empty string when disabled.
value: ${{ steps.configure.outputs.sccache_dir }}
target-dir:
description: Persistent CARGO_TARGET_DIR, or an empty string when disabled.
value: ${{ steps.configure.outputs.target_dir }}

runs:
using: composite
steps:
- name: Configure persistent Rust caches
id: configure
shell: bash
env:
INPUT_CACHE_CARGO_HOME: ${{ inputs.cache-cargo-home }}
INPUT_CACHE_NAMESPACE: ${{ inputs.cache-namespace }}
INPUT_CACHE_SCCACHE: ${{ inputs.cache-sccache }}
INPUT_CACHE_TARGET: ${{ inputs.cache-target }}
INPUT_CACHE_VERSION: ${{ inputs.cache-version }}
INPUT_DISABLE_INCREMENTAL: ${{ inputs.disable-incremental }}
INPUT_SCCACHE_CACHE_SIZE: ${{ inputs.sccache-cache-size }}
INPUT_WORKING_DIRECTORY: ${{ inputs.working-directory }}
run: |
set -euo pipefail

if [[ -z "${TENSORLAKE_CACHE_DIR:-}" ]]; then
echo "::error::The runner did not provide TENSORLAKE_CACHE_DIR."
exit 1
fi
if ! mountpoint -q "${TENSORLAKE_CACHE_DIR}"; then
echo "::error::${TENSORLAKE_CACHE_DIR} is not a mounted Tensorlake Cloud Volume."
exit 1
fi
if [[ ! "${INPUT_CACHE_VERSION}" =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "::error::cache-version may contain only letters, numbers, dots, underscores, and hyphens."
exit 1
fi

for value in \
"${INPUT_CACHE_CARGO_HOME}" \
"${INPUT_CACHE_SCCACHE}" \
"${INPUT_CACHE_TARGET}" \
"${INPUT_DISABLE_INCREMENTAL}"; do
if [[ "${value}" != "true" && "${value}" != "false" ]]; then
echo "::error::Boolean inputs must be either true or false."
exit 1
fi
done

if [[ "${INPUT_WORKING_DIRECTORY}" = /* ]]; then
working_directory="${INPUT_WORKING_DIRECTORY}"
else
working_directory="${GITHUB_WORKSPACE}/${INPUT_WORKING_DIRECTORY}"
fi
if [[ ! -d "${working_directory}" ]]; then
echo "::error::Working directory does not exist: ${working_directory}"
exit 1
fi
working_directory="$(cd "${working_directory}" && pwd -P)"

namespace_slug="$(printf '%s' "${INPUT_CACHE_NAMESPACE}" \
| tr '[:upper:]' '[:lower:]' \
| sed -E 's/[^a-z0-9._-]+/-/g; s/^-+//; s/-+$//' \
| cut -c 1-48)"
namespace_slug="${namespace_slug:-rust-cache}"
namespace_hash="$(printf '%s\0%s' "${INPUT_CACHE_VERSION}" "${INPUT_CACHE_NAMESPACE}" | sha256sum | cut -c 1-12)"
cache_key="${namespace_slug}-${namespace_hash}"
platform="${RUNNER_OS}-${RUNNER_ARCH}"
cache_root="${TENSORLAKE_CACHE_DIR}/rust-cache-${INPUT_CACHE_VERSION}/${platform}/${cache_key}"
cargo_home=""
sccache_dir=""
target_dir=""

if [[ "${INPUT_CACHE_CARGO_HOME}" == "true" ]]; then
cargo_home="${cache_root}/cargo-home"
mkdir -p "${cargo_home}"
echo "CARGO_HOME=${cargo_home}" >> "${GITHUB_ENV}"
fi

if [[ "${INPUT_CACHE_SCCACHE}" == "true" ]]; then
if ! command -v sccache >/dev/null 2>&1; then
echo "::error::cache-sccache is enabled but sccache is not installed."
exit 1
fi
sccache_dir="${cache_root}/sccache"
mkdir -p "${sccache_dir}"
{
echo "SCCACHE_DIR=${sccache_dir}"
echo "SCCACHE_CACHE_SIZE=${INPUT_SCCACHE_CACHE_SIZE}"
echo "SCCACHE_BASEDIRS=${working_directory}"
echo "RUSTC_WRAPPER=sccache"
} >> "${GITHUB_ENV}"
fi

if [[ "${INPUT_CACHE_TARGET}" == "true" ]]; then
target_dir="${cache_root}/target"
mkdir -p "${target_dir}"
echo "CARGO_TARGET_DIR=${target_dir}" >> "${GITHUB_ENV}"
fi

if [[ "${INPUT_DISABLE_INCREMENTAL}" == "true" ]]; then
echo "CARGO_INCREMENTAL=0" >> "${GITHUB_ENV}"
fi

{
echo "key=${cache_key}"
echo "cargo_home=${cargo_home}"
echo "sccache_dir=${sccache_dir}"
echo "target_dir=${target_dir}"
} >> "${GITHUB_OUTPUT}"

{
echo "### Tensorlake Rust cache"
echo
echo "- Key: \`${cache_key}\`"
echo "- Cargo home: \`${cargo_home:-disabled}\`"
echo "- sccache: \`${sccache_dir:-disabled}\`"
echo "- Cargo target: \`${target_dir:-disabled}\`"
} >> "${GITHUB_STEP_SUMMARY}"
Loading