The git-checkout provider enables workflows to checkout code from git repositories using efficient worktree management for multi-workflow execution.
- Git Worktrees: Efficient disk usage by sharing git objects across checkouts
- Dynamic Variables: Support for Liquid templates to resolve branches/refs dynamically
- Parallel Workflows: Multiple workflows can checkout different branches simultaneously
- Automatic Cleanup: Worktrees are cleaned up when workflows complete
- GitHub Actions Compatible: Similar configuration to
actions/checkout@v4
version: "1.0"
steps:
checkout:
type: git-checkout
ref: "{{ pr.head }}"steps:
checkout-with-options:
type: git-checkout
# Required: Git reference to checkout
ref: "{{ pr.head }}" # Branch, tag, commit SHA, or dynamic variable
# Optional: Repository (defaults to current PR repository)
repository: owner/repo # GitHub repository or URL
# Optional: Authentication token (defaults to GITHUB_TOKEN env var)
token: "{{ env.GITHUB_TOKEN }}"
# Optional: Fetch configuration
fetch_depth: 1 # Shallow clone depth (default: full history)
fetch_tags: false # Fetch tags (default: false)
submodules: false # Checkout submodules (default: false)
# Can be: true, false, or 'recursive'
clone_timeout_ms: 300000 # Clone timeout in milliseconds (default: 300000 = 5 min)
# Optional: Working directory (auto-generated if not specified)
working_directory: /tmp/my-checkout
# Optional: Worktree behavior
use_worktree: true # Use git worktrees (default: true)
clean: true # Clean before checkout (default: true)
# Optional: Advanced features
sparse_checkout: [] # Sparse checkout paths
lfs: false # Git LFS support (default: false)
# Standard check options
timeout: 60 # Timeout in seconds (default: 60)
criticality: internal
depends_on: []
if: "true"
# Cleanup behavior
cleanup_on_failure: true # Cleanup if step fails (default: true)
persist_worktree: false # Keep worktree after workflow (default: false)The provider returns the following output structure:
{
success: boolean, // Whether checkout succeeded
path: string, // Absolute path to checked out code
ref: string, // Resolved ref that was checked out
commit: string, // Full commit SHA
worktree_id: string, // Unique worktree identifier
repository: string, // Repository that was checked out
is_worktree: boolean, // Whether this is a worktree
workspace_path?: string, // Human-readable path within workspace (when workspace isolation is enabled)
error?: string, // Error message if failed
}version: "1.0"
steps:
checkout:
type: git-checkout
ref: "{{ pr.head }}"
build:
type: command
depends_on: [checkout]
exec: "npm run build"
working_directory: "{{ outputs.checkout.path }}"version: "1.0"
steps:
checkout-head:
type: git-checkout
ref: "{{ pr.head }}"
checkout-base:
type: git-checkout
ref: "{{ pr.base }}"
compare:
type: command
depends_on: [checkout-head, checkout-base]
exec: |
echo "Comparing branches:"
echo "Head: {{ outputs['checkout-head'].commit }}"
echo "Base: {{ outputs['checkout-base'].commit }}"
diff -r "{{ outputs['checkout-head'].path }}" "{{ outputs['checkout-base'].path }}" || trueversion: "1.0"
steps:
checkout-main:
type: git-checkout
repository: myorg/main-repo
ref: main
checkout-dependency:
type: git-checkout
repository: myorg/dependency-repo
ref: v1.0.0
token: "{{ env.DEPENDENCY_TOKEN }}"
integration-test:
type: command
depends_on: [checkout-main, checkout-dependency]
exec: ./scripts/integration-test.sh
working_directory: "{{ outputs['checkout-main'].path }}"
env:
DEPENDENCY_PATH: "{{ outputs['checkout-dependency'].path }}"version: "1.0"
steps:
determine-branch:
type: command
exec: |
if [ "{{ pr.base }}" == "main" ]; then
echo '{"branch": "stable"}'
else
echo '{"branch": "{{ pr.base }}"}'
fi
transform_js: JSON.parse(output)
checkout-resolved:
type: git-checkout
depends_on: [determine-branch]
ref: "{{ outputs['determine-branch'].branch }}"version: "1.0"
steps:
checkout-partial:
type: git-checkout
ref: main
sparse_checkout:
- src/
- tests/
- package.json
- package-lock.json
test:
type: command
depends_on: [checkout-partial]
exec: npm test
working_directory: "{{ outputs['checkout-partial'].path }}"version: "1.0"
steps:
checkout-full:
type: git-checkout
ref: main
fetch_depth: 0 # Full history
fetch_tags: true # Include all tags
analyze-history:
type: command
depends_on: [checkout-full]
exec: |
git log --oneline --graph --all --decorate
git describe --tags --always
working_directory: "{{ outputs['checkout-full'].path }}"version: "1.0"
steps:
checkout-with-submodules:
type: git-checkout
ref: "{{ pr.head }}"
submodules: recursive
build-all:
type: command
depends_on: [checkout-with-submodules]
exec: |
npm install
npm run build
working_directory: "{{ outputs['checkout-with-submodules'].path }}"version: "1.0"
steps:
check-should-checkout:
type: command
exec: |
# Only checkout if PR has specific label
if [[ "{{ pr.labels }}" == *"needs-checkout"* ]]; then
echo '{"should_checkout": true}'
else
echo '{"should_checkout": false}'
fi
transform_js: JSON.parse(output)
checkout:
type: git-checkout
depends_on: [check-should-checkout]
if: "outputs['check-should-checkout'].should_checkout"
ref: "{{ pr.head }}"
test:
type: command
depends_on: [checkout]
if: "outputs.checkout?.success"
exec: npm test
working_directory: "{{ outputs.checkout.path }}"version: "1.0"
steps:
checkout-persistent:
type: git-checkout
ref: main
persist_worktree: true # Keep after workflow completes
working_directory: /tmp/persistent-workspace
# This worktree will remain after the workflow completes
# and can be reused by subsequent workflowsThe provider handles various error scenarios:
steps:
checkout:
type: git-checkout
ref: "{{ pr.head }}"
guarantee: "output.success == true"
handle-failure:
type: command
depends_on: [checkout]
if: "!outputs.checkout.success"
exec: |
echo "Checkout failed: {{ outputs.checkout.error }}"
exit 1The provider uses git worktrees to efficiently manage multiple checkouts:
- Bare Repository: A bare repository is cached at
${base_path}/repos/ - Fetch Updates: On each checkout, run
git remote update --pruneto get latest code - Worktrees: Working directories are created at
${base_path}/worktrees/ - Shared Objects: Git objects are shared between worktrees, saving disk space
- Automatic Cleanup: Worktrees are cleaned up when workflows complete
Important: The bare repository is updated on every checkout run, ensuring you always get the latest code, similar to GitHub Actions behavior.
By default, worktrees are stored in .visor/worktrees/ in your project root:
.visor/worktrees/
├── repos/
│ └── owner-repo.git/ # Bare repository (shared)
│ ├── objects/ # Git objects (shared)
│ └── worktrees/ # Worktree metadata
└── worktrees/
├── owner-repo-main-abc123/ # Worktree 1
└── owner-repo-dev-def456/ # Worktree 2
Benefits of project-local storage:
- Worktrees stay with the project
- Easy to locate and debug
- Can be excluded from version control (add
.visor/worktrees/to.gitignore) - Automatic cleanup when removing the project
Configure worktree behavior globally in .visor.yaml:
version: "1.0"
worktree_cache:
enabled: true
base_path: .visor/worktrees # Default: .visor/worktrees/ in project root
cleanup_on_exit: true # Default: true
max_age_hours: 24 # Cleanup after 24 hours
steps:
# ... your stepsEnvironment Variable Override:
You can also set the base path via environment variable:
export VISOR_WORKTREE_PATH=/custom/path/to/worktreesThis takes precedence over the config file.
Worktrees are automatically cleaned up in the following scenarios:
- On process exit: When the visor process terminates normally
- On SIGINT/SIGTERM: When the process receives interrupt signals (Ctrl+C)
- Age-based cleanup: Worktrees older than
max_age_hoursare removed on subsequent runs - Stale process cleanup: Worktrees from dead processes are automatically removed
To manually clean up worktrees, you can remove the .visor/worktrees/ directory:
# Remove all worktrees for current project
rm -rf .visor/worktrees/steps:
checkout:
type: git-checkout
ref: "{{ pr.head }}"
fetch_depth: 1 # Only fetch latest commit (much faster)Why shallow clones?
- Faster initial clone: 5-10x faster for large repositories
- Less bandwidth: Only downloads recent commit history
- Sufficient for most workflows: Tests, builds, and deployments rarely need full history
When to use full history (fetch_depth: 0 or omit):
- Git operations that need history (e.g.,
git log,git blame) - Generating changelogs
- License compliance scanning
- Full repository analysis
Default behavior: If fetch_depth is not specified, clones full history (all commits)
steps:
checkout:
type: git-checkout
ref: "{{ pr.head }}"
cleanup_on_failure: true # Remove on failuresteps:
checkout:
type: git-checkout
ref: "{{ pr.head }}"
working_directory: /tmp/my-build-{{ pr.number }}steps:
checkout:
type: git-checkout
ref: "{{ pr.head }}"
guarantee: "output.success == true && output.commit != null"steps:
checkout:
type: git-checkout
ref: "{{ pr.head }}"
build:
type: command
depends_on: [checkout]
exec: npm run build
working_directory: "{{ outputs.checkout.path }}"
assume: "outputs.checkout.success"Solution: Increase the timeout or use shallow clones:
steps:
checkout:
type: git-checkout
ref: "{{ pr.head }}"
fetch_depth: 1 # Shallow clone
timeout: 120 # 2 minutesSolution: Ensure token is set correctly:
steps:
checkout:
type: git-checkout
ref: "{{ pr.head }}"
token: "{{ env.GITHUB_TOKEN }}" # Explicit tokenSolution: Enable cleanup and check configuration:
worktree_cache:
cleanup_on_exit: true
max_age_hours: 24
steps:
checkout:
type: git-checkout
ref: "{{ pr.head }}"
persist_worktree: false # Don't persistSolution: Use unique working directories:
steps:
checkout:
type: git-checkout
ref: "{{ pr.head }}"
working_directory: /tmp/workflow-{{ inputs.workflow_id }}-checkoutSolution: Cleanup stale worktrees regularly:
# Via CLI
visor worktree cleanup
# Or configure automatic cleanup
worktree_cache:
max_age_hours: 12 # Cleanup after 12 hours| Feature | git-checkout | actions/checkout@v4 |
|---|---|---|
| Branch checkout | ✅ | ✅ |
| Tag checkout | ✅ | ✅ |
| Commit checkout | ✅ | ✅ |
| Shallow clone | ✅ | ✅ |
| Submodules | ✅ | ✅ |
| LFS | ✅ | ✅ |
| Sparse checkout | ✅ | ✅ |
| Dynamic variables | ✅ | ❌ |
| Worktrees | ✅ | ❌ |
| Multiple checkouts | ✅ | ✅ |
| Automatic cleanup | ✅ | ✅ |
- Disk savings: Shared objects between checkouts
- Faster checkouts: No need to re-download objects
- Parallel execution: Multiple branches checked out simultaneously
Typical performance (example repository: 100MB):
| Operation | Time |
|---|---|
| First checkout (bare clone) | 30s |
| Subsequent checkout (worktree) | 5s |
| Regular clone (comparison) | 30s |
- Use shallow clones for CI:
fetch_depth: 1 - Limit sparse checkout to needed paths
- Disable tags if not needed:
fetch_tags: false - Reuse worktrees across workflows when possible
- Tokens are never logged or exposed in output
- Use environment variables for tokens:
{{ env.GITHUB_TOKEN }} - Tokens are redacted from error messages
- Working directory paths are validated to prevent traversal attacks
- Worktrees are isolated to configured base path
- Configure
max_age_hoursto prevent disk exhaustion - Use timeouts to prevent hanging checkouts
- Monitor disk usage in
.visor/worktrees/(or customVISOR_WORKTREE_PATH)
- Command Provider - Execute commands in checked out code
- Workflows - Compose multi-step workflows
- Configuration Reference - Full configuration options