diff --git a/.github/workflows/build-reference.yml b/.github/workflows/build-reference.yml index 5f53302..3f8fa3a 100644 --- a/.github/workflows/build-reference.yml +++ b/.github/workflows/build-reference.yml @@ -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] @@ -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 @@ -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 diff --git a/README.md b/README.md index 6452d75..22e804d 100644 --- a/README.md +++ b/README.md @@ -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. @@ -279,8 +281,6 @@ 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 @@ -288,14 +288,50 @@ self-hosted runner: - 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. @@ -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 @@ -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 ``` @@ -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. diff --git a/actions/setup-rust-cache/action.yml b/actions/setup-rust-cache/action.yml new file mode 100644 index 0000000..e500fd3 --- /dev/null +++ b/actions/setup-rust-cache/action.yml @@ -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}" diff --git a/actions/setup-uv-cache/action.yml b/actions/setup-uv-cache/action.yml new file mode 100644 index 0000000..2d60c6c --- /dev/null +++ b/actions/setup-uv-cache/action.yml @@ -0,0 +1,184 @@ +name: Set up Tensorlake uv cache +description: Restore, synchronize, and persist a uv environment with a Tensorlake Cloud Volume. + +inputs: + python-version: + description: Python version passed to uv python install and uv python find. + required: true + sync-args: + description: Whitespace-separated arguments passed to uv sync. + required: false + default: --locked + working-directory: + description: Project directory containing the uv lockfile. + required: false + default: . + lockfile: + description: Lockfile path relative to the working directory. + required: false + default: uv.lock + cache-version: + description: Manual cache namespace version used for invalidation. + required: false + default: v1 + +outputs: + state: + description: Whether the environment archive was cold or warm. + value: ${{ steps.cache.outputs.state }} + key: + description: Compatibility key for the environment archive. + value: ${{ steps.cache.outputs.key }} + restore-duration-ms: + description: Time spent restoring the environment archive. + value: ${{ steps.cache.outputs.restore_duration_ms }} + sync-duration-ms: + description: Time spent running uv sync. + value: ${{ steps.cache.outputs.sync_duration_ms }} + publish-duration-ms: + description: Time spent creating and settling a cold environment archive. + value: ${{ steps.cache.outputs.publish_duration_ms }} + +runs: + using: composite + steps: + - name: Verify persistent cache mount + shell: bash + 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 + + - name: Set up uv + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + enable-cache: false + + - name: Install Python + shell: bash + env: + INPUT_PYTHON_VERSION: ${{ inputs.python-version }} + run: | + set -euo pipefail + uv python install "${INPUT_PYTHON_VERSION}" + + - name: Restore, synchronize, and persist uv environment + id: cache + shell: bash + env: + INPUT_CACHE_VERSION: ${{ inputs.cache-version }} + INPUT_LOCKFILE: ${{ inputs.lockfile }} + INPUT_PYTHON_VERSION: ${{ inputs.python-version }} + INPUT_SYNC_ARGS: ${{ inputs.sync-args }} + INPUT_WORKING_DIRECTORY: ${{ inputs.working-directory }} + run: | + set -euo pipefail + + 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 + + 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)" + + if [[ "${INPUT_LOCKFILE}" = /* ]]; then + lockfile_path="${INPUT_LOCKFILE}" + else + lockfile_path="${working_directory}/${INPUT_LOCKFILE}" + fi + if [[ ! -f "${lockfile_path}" ]]; then + echo "::error::uv lockfile does not exist: ${lockfile_path}" + exit 1 + fi + + cd "${working_directory}" + python_executable="$(uv python find "${INPUT_PYTHON_VERSION}")" + python_identity="$("${python_executable}" -c 'import platform, sys; print(f"{sys.implementation.name}-{platform.python_version()}")')" + lock_hash="$(sha256sum "${lockfile_path}" | cut -d ' ' -f 1)" + scope_hash="$(printf '%s\0%s\0%s' "${GITHUB_WORKFLOW}" "${GITHUB_REF}" "${INPUT_WORKING_DIRECTORY}" | sha256sum | cut -c 1-16)" + environment_key="${RUNNER_OS}-${RUNNER_ARCH}-${python_identity}/${GITHUB_JOB}-${scope_hash}-${lock_hash}" + environment_dir="${RUNNER_TEMP}/tensorlake-uv-environments/${environment_key}" + environment_archive="${TENSORLAKE_CACHE_DIR}/uv-environment-archives-${INPUT_CACHE_VERSION}/${environment_key}.tar.gz" + restore_duration_ms=0 + publish_duration_ms=0 + mkdir -p "$(dirname "${environment_dir}")" "$(dirname "${environment_archive}")" + + if [[ -f "${environment_archive}" ]]; then + environment_state="warm" + restore_started_ns="$(date +%s%N)" + tar -xzf "${environment_archive}" -C "$(dirname "${environment_dir}")" + restore_finished_ns="$(date +%s%N)" + restore_duration_ms="$(( (restore_finished_ns - restore_started_ns) / 1000000 ))" + if [[ ! -f "${environment_dir}/pyvenv.cfg" ]]; then + echo "::error::The persistent uv environment archive is invalid." + exit 1 + fi + else + environment_state="cold" + fi + + { + echo "UV_PROJECT_ENVIRONMENT=${environment_dir}" + echo "TENSORLAKE_UV_ENVIRONMENT_KEY=${environment_key}" + echo "TENSORLAKE_UV_ENVIRONMENT_STATE=${environment_state}" + } >> "${GITHUB_ENV}" + export UV_PROJECT_ENVIRONMENT="${environment_dir}" + + uv_sync_args=() + if [[ -n "${INPUT_SYNC_ARGS}" ]]; then + read -r -a uv_sync_args <<< "${INPUT_SYNC_ARGS}" + fi + sync_started_ns="$(date +%s%N)" + uv sync "${uv_sync_args[@]}" + sync_finished_ns="$(date +%s%N)" + sync_duration_ms="$(( (sync_finished_ns - sync_started_ns) / 1000000 ))" + + if [[ "${environment_state}" == "cold" ]]; then + publish_started_ns="$(date +%s%N)" + temporary_archive="${environment_archive}.tmp.${GITHUB_RUN_ID}.${GITHUB_RUN_ATTEMPT}" + trap 'rm -f "${temporary_archive}"' EXIT + tar -czf "${temporary_archive}" \ + -C "$(dirname "${environment_dir}")" \ + "$(basename "${environment_dir}")" + mv -f "${temporary_archive}" "${environment_archive}" + trap - EXIT + sync + sleep 6 + publish_finished_ns="$(date +%s%N)" + publish_duration_ms="$(( (publish_finished_ns - publish_started_ns) / 1000000 ))" + fi + + { + echo "state=${environment_state}" + echo "key=${environment_key}" + echo "restore_duration_ms=${restore_duration_ms}" + echo "sync_duration_ms=${sync_duration_ms}" + echo "publish_duration_ms=${publish_duration_ms}" + } >> "${GITHUB_OUTPUT}" + + echo "UV_SYNC_TIMING duration_ms=${sync_duration_ms} environment_state=${environment_state} key=${environment_key}" + echo "UV_ARCHIVE_TIMING restore_duration_ms=${restore_duration_ms} publish_duration_ms=${publish_duration_ms} environment_state=${environment_state}" + { + echo "### Tensorlake uv environment cache" + echo + echo "- State: \`${environment_state}\`" + echo "- Archive restore: \`${restore_duration_ms}ms\`" + echo "- uv sync: \`${sync_duration_ms}ms\`" + echo "- Archive publish: \`${publish_duration_ms}ms\`" + echo "- Key: \`${environment_key}\`" + } >> "${GITHUB_STEP_SUMMARY}" diff --git a/pyproject.toml b/pyproject.toml index f09c2f2..cf5352c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,9 @@ dependencies = [ requires = ["setuptools>=69"] build-backend = "setuptools.build_meta" +[tool.setuptools.packages.find] +include = ["github_runner_orchestrator*"] + [project.optional-dependencies] dev = [ "pytest>=8.0.0", diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 931f178..e570300 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -48,8 +48,7 @@ def test_readme_explains_the_webhook_and_deployment_order() -> None: def test_self_hosted_workflow_builds_on_tensorlake_runner() -> None: workflow = Path(".github/workflows/build-reference.yml").read_text() assert "runs-on: [self-hosted, tensorlake, tensorlake-small]" in workflow - assert "astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2" in workflow - assert "uv sync --locked --all-extras" in workflow + assert "uses: ./actions/setup-uv-cache" in workflow assert "uv build" in workflow assert "uv run --no-sync pytest -q" in workflow assert "docker run --rm hello-world" in workflow