Skip to content
Draft
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
32 changes: 32 additions & 0 deletions checkout-with-cache/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# checkout-with-cache

Composite action that restores a cached repository `.git` directory from RunsOn EFS before `actions/checkout`, then updates the cache after checkout. This keeps `actions/checkout` in charge of the checkout while letting large repositories fetch deltas instead of cloning all history every run.

This is useful for large repositories that use `fetch-depth: 0`. It is usually not worth it for small repositories.

```yaml
jobs:
build:
runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64
steps:
- uses: runs-on/action/checkout-with-cache@v2
with:
encryption_key: ${{ secrets.CHECKOUT_CACHE_KEY }}
```

The action defaults to `/mnt/efs/${GITHUB_REPOSITORY}/.git` and stores files named `repository-<sha>-<timestamp>.tar`. If `encryption_key` is set, cache files are encrypted with `openssl enc -aes-256-cbc -pbkdf2`; otherwise they are plain tar files.

Writes are limited to the repository default branch by default and are skipped for pull request events. The action passes `persist-credentials: false` to `actions/checkout` by default and also removes checkout extraheaders from local Git config before saving `.git`.

## Inputs

| Name | Default | Description |
| --- | --- | --- |
| `encryption_key` | `''` | Optional passphrase used to encrypt/decrypt cache tarballs. |
| `write_branches` | repository default branch | Comma-separated list of branches allowed to write cache files. |
| `keep_files_count` | `5` | Number of cache files to keep. |
| `force_checkout` | `false` | Skip cache restore and force a normal checkout. |
| `force_write` | `false` | Write a cache file even when the current SHA already has one. |
| `cache_dir` | `/mnt/efs/${GITHUB_REPOSITORY}/.git` | Directory where cache files are stored. |
| `fetch_depth` | `0` | Value passed to `actions/checkout`. |
| `persist_credentials` | `false` | Value passed to `actions/checkout`. |
109 changes: 109 additions & 0 deletions checkout-with-cache/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
name: checkout-with-cache
description: 'Restore a RunsOn EFS repository cache before actions/checkout, then update it after checkout'

inputs:
encryption_key:
description: 'Optional passphrase used to encrypt/decrypt the tarball with openssl AES-256-CBC/PBKDF2'
required: false
default: ''
write_branches:
description: 'Comma-separated list of branches allowed to write to the cache; defaults to the repository default branch'
required: false
default: ${{ github.event.repository.default_branch }}
keep_files_count:
description: 'Number of repository cache files to keep on EFS'
required: false
default: '5'
force_checkout:
description: 'When true, skip restoring from the cache before checkout'
required: false
default: 'false'
force_write:
description: 'When true, write a cache file even when one already exists for the current SHA'
required: false
default: 'false'
cache_dir:
description: 'EFS cache directory. Defaults to /mnt/efs/${GITHUB_REPOSITORY}/.git'
required: false
default: ''
fetch_depth:
description: 'fetch-depth passed to actions/checkout'
required: false
default: '0'
persist_credentials:
description: 'persist-credentials passed to actions/checkout'
required: false
default: 'false'

outputs:
cache_dir:
description: 'The EFS directory where the cache is stored'
value: ${{ steps.efs.outputs.cache_dir }}
cache_hit:
description: 'Whether the cache was restored from EFS'
value: ${{ steps.efs.outputs.cache_hit }}
latest_file_name:
description: 'The latest file in the cache directory'
value: ${{ steps.efs.outputs.latest_file_name }}
latest_file_path:
description: 'The path to the latest cache file'
value: ${{ steps.efs.outputs.latest_file_path }}
ref:
description: 'The branch, tag, or SHA that was checked out'
value: ${{ steps.checkout.outputs.ref }}
commit:
description: 'The commit SHA that was checked out'
value: ${{ steps.checkout.outputs.commit }}

runs:
using: composite
steps:
- name: Check if EFS is enabled
id: efs_enabled
shell: bash
env:
EFS_MOUNT_PATH: /mnt/efs
run: bash "${{ github.action_path }}/is_efs_enabled.sh"

- name: Discover EFS cache
id: efs
shell: bash
env:
EFS_ENABLED: ${{ steps.efs_enabled.outputs.enabled }}
CACHE_DIR: ${{ inputs.cache_dir }}
run: bash "${{ github.action_path }}/cache_discover.sh"

- name: Restore .git from EFS
if: ${{ steps.efs.outputs.enabled == 'true' && steps.efs.outputs.cache_hit == 'true' && inputs.force_checkout != 'true' }}
shell: bash
env:
ENC_KEY: ${{ inputs.encryption_key }}
CACHE_FILE: ${{ steps.efs.outputs.latest_file_path }}
WORKSPACE: ${{ github.workspace }}
run: bash "${{ github.action_path }}/cache_restore.sh"

- name: Checkout repository
uses: actions/checkout@v6
id: checkout
with:
clean: 'true'
fetch-depth: ${{ inputs.fetch_depth }}
persist-credentials: ${{ inputs.persist_credentials }}

- name: Update EFS cache if necessary
if: ${{ steps.efs.outputs.enabled == 'true' && github.event_name != 'pull_request' && github.event_name != 'pull_request_target' && contains(inputs.write_branches, github.ref_name) }}
shell: bash
env:
ENC_KEY: ${{ inputs.encryption_key }}
CACHE_DIR: ${{ steps.efs.outputs.cache_dir }}
WORKSPACE: ${{ github.workspace }}
FORCE_WRITE: ${{ inputs.force_write }}
run: bash "${{ github.action_path }}/cache_update.sh"

- name: Cleanup EFS cache
if: ${{ steps.efs.outputs.enabled == 'true' && github.event_name != 'pull_request' && github.event_name != 'pull_request_target' && contains(inputs.write_branches, github.ref_name) }}
shell: bash
env:
CACHE_DIR: ${{ steps.efs.outputs.cache_dir }}
KEEP_FILES_COUNT: ${{ inputs.keep_files_count }}
run: bash "${{ github.action_path }}/cache_rotation.sh"
34 changes: 34 additions & 0 deletions checkout-with-cache/cache_discover.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail

GITHUB_OUTPUT=${GITHUB_OUTPUT:-/dev/null}
CACHE_DIR=${CACHE_DIR:-}
EFS_ENABLED=${EFS_ENABLED:-false}

if [[ -z "${CACHE_DIR}" ]]; then
CACHE_DIR="/mnt/efs/${GITHUB_REPOSITORY}/.git"
fi

latest_file=""
latest_path=""
if [[ "${EFS_ENABLED}" == "true" && -d "${CACHE_DIR}" ]]; then
CACHE_DIR=$(realpath "${CACHE_DIR}")
latest_file=$(find "${CACHE_DIR}" -maxdepth 1 -type f -name 'repository-*.tar' -exec basename {} \; | sort -t '-' -k3,3nr | head -n1 || true)
if [[ -n "${latest_file}" ]]; then
latest_path="${CACHE_DIR}/${latest_file}"
fi
fi

if [[ -n "${latest_file}" ]]; then
cache_hit=true
else
cache_hit=false
fi

{
echo "enabled=${EFS_ENABLED}"
echo "cache_hit=${cache_hit}"
echo "cache_dir=${CACHE_DIR}"
echo "latest_file_name=${latest_file}"
echo "latest_file_path=${latest_path}"
} | tee -a "${GITHUB_OUTPUT}"
35 changes: 35 additions & 0 deletions checkout-with-cache/cache_restore.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail

ENC_KEY=${ENC_KEY:-}
CACHE_FILE=${CACHE_FILE:?CACHE_FILE is required}
WORKSPACE=${WORKSPACE:-${GITHUB_WORKSPACE:-}}

if [[ -z "${WORKSPACE}" ]]; then
echo "Checkout cache restore skipped: WORKSPACE is not set"
exit 0
fi

echo "Restoring checkout cache from ${CACHE_FILE} into ${WORKSPACE}"
mkdir -p "${WORKSPACE}"

restore_ok=false
if [[ -n "${ENC_KEY}" ]]; then
if openssl enc -aes-256-cbc -pbkdf2 -d -pass env:ENC_KEY -in "${CACHE_FILE}" | tar -xf - -C "${WORKSPACE}"; then
restore_ok=true
fi
else
if tar -xf "${CACHE_FILE}" -C "${WORKSPACE}"; then
restore_ok=true
fi
fi

if [[ "${restore_ok}" != "true" || ! -d "${WORKSPACE}/.git" ]]; then
echo "Checkout cache restore failed or did not contain .git; falling back to a normal checkout"
rm -rf "${WORKSPACE}/.git"
exit 0
fi

echo "Reconciling restored checkout cache"
git -C "${WORKSPACE}" update-index --refresh || true
git -C "${WORKSPACE}" reset --hard HEAD || true
18 changes: 18 additions & 0 deletions checkout-with-cache/cache_rotation.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail

CACHE_DIR=${CACHE_DIR:-}
KEEP_FILES_COUNT=${KEEP_FILES_COUNT:-5}

if [[ -z "${CACHE_DIR}" || ! -d "${CACHE_DIR}" ]]; then
exit 0
fi

mapfile -t files < <(find "${CACHE_DIR}" -maxdepth 1 -type f -name 'repository-*.tar' -exec basename {} \; | sort -t '-' -k3,3nr)
if (( ${#files[@]} <= KEEP_FILES_COUNT )); then
exit 0
fi

for file in "${files[@]:KEEP_FILES_COUNT}"; do
rm -vf "${CACHE_DIR}/${file}"
done
39 changes: 39 additions & 0 deletions checkout-with-cache/cache_update.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail

ENC_KEY=${ENC_KEY:-}
CACHE_DIR=${CACHE_DIR:-}
WORKSPACE=${WORKSPACE:-${GITHUB_WORKSPACE:-}}
FORCE_WRITE=${FORCE_WRITE:-false}

if [[ -z "${CACHE_DIR}" || -z "${WORKSPACE}" ]]; then
echo "Checkout cache save skipped: CACHE_DIR or WORKSPACE is not set"
exit 0
fi

mkdir -p "${CACHE_DIR}"

sha="$(git -C "${WORKSPACE}" rev-parse HEAD)"
match="$(find "${CACHE_DIR}" -maxdepth 1 -type f -name "repository-${sha}-*.tar" -exec basename {} \; | sort -t '-' -k3,3nr | head -n1 || true)"
if [[ -n "${match}" && "${FORCE_WRITE}" != "true" ]]; then
echo "Checkout cache for ${sha} already exists; skipping save"
exit 0
fi

while IFS= read -r key; do
[[ -n "${key}" ]] || continue
git -C "${WORKSPACE}" config --local --unset-all "${key}" || true
done < <(git -C "${WORKSPACE}" config --local --name-only --get-regexp '^http\..*\.extraheader$' || true)

file_location="${CACHE_DIR}/repository-${sha}-$(date +%s).tar"
tmp_file="${file_location}.tmp"
rm -f "${tmp_file}"

echo "Saving checkout cache to ${file_location}"
if [[ -n "${ENC_KEY}" ]]; then
tar -C "${WORKSPACE}" -cf - .git | openssl enc -aes-256-cbc -salt -pbkdf2 -pass env:ENC_KEY -out "${tmp_file}"
else
tar -C "${WORKSPACE}" -cf "${tmp_file}" .git
fi

mv "${tmp_file}" "${file_location}"
17 changes: 17 additions & 0 deletions checkout-with-cache/is_efs_enabled.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail

GITHUB_OUTPUT=${GITHUB_OUTPUT:-/dev/null}
EFS_MOUNT_PATH=${EFS_MOUNT_PATH:-/mnt/efs}

enabled=false
if [[ "${RUNNER_OS:-}" == "Linux" ]] && [[ -n "${RUNS_ON_RUNNER_NAME:-}" ]]; then
if [[ -n "${RUNS_ON_EFS_ID:-}" ]] || [[ -d "${EFS_MOUNT_PATH}" && -w "${EFS_MOUNT_PATH}" ]]; then
enabled=true
fi
fi

{
echo "enabled=${enabled}"
echo "mount_path=${EFS_MOUNT_PATH}"
} | tee -a "${GITHUB_OUTPUT}"