
Shadow Testing
- 114 installs
- 70 repo stars
- Updated July 26, 2026
- rysweet/amplihack
Run parallel or non-production shadow tests to compare new behavior against live traffic or baselines before full rollout.
About
Shadow-testing is an amplihack skill for validating implementations in shadow or parallel environments before launch. It helps teams mirror production conditions, compare old versus new behavior, catch regressions early, and define pass criteria so releases proceed only after evidence-backed confidence rather than unit tests alone.
- Parallel execution against production-like inputs
- Diff and regression comparison workflows
- Safe rollout gates before traffic cutover
- Agent guidance for shadow test design
- Observability hooks for mismatch detection
Shadow Testing by the numbers
- 114 all-time installs (skills.sh)
- +1 installs in the week ending Jul 26, 2026 (Skillselion tracking)
- Ranked #960 of 2,155 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rysweet/amplihack --skill shadow-testingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 114 |
|---|---|
| repo stars | ★ 70 |
| Last updated | July 26, 2026 |
| Repository | rysweet/amplihack ↗ |
What it does
Run parallel or non-production shadow tests to compare new behavior against live traffic or baselines before full rollout.
Files
Shadow Testing Skill
Purpose [LEVEL 1]
Shadow testing creates isolated container environments where you can test local uncommitted changes without affecting your host system or pushing to remote repositories.
Key Principle: Test exactly what's on your machine (including uncommitted changes) in a clean, isolated environment that mirrors CI.
When to Use This Skill [LEVEL 1]
Perfect For
- Pre-Push Validation: Test changes before committing/pushing
- Multi-Repo Coordination: Validate changes across multiple repositories work together
- Clean-State Testing: "Does it work on a fresh machine?"
- Library Development: Test library changes with dependent projects
- CI Parity: See what CI will see before pushing
- Destructive Testing: Tests that modify system state won't affect host
Use This Skill When
- Making breaking changes to a library others depend on
- Coordinating changes across multiple repositories
- Unsure if your changes will work in CI
- Need to test with specific dependency versions
- Want to verify install/setup procedures work
- Testing changes that require clean environment state
Don't Use This Skill When
- Running unit tests on already-committed code (use local test runner)
- Need to debug with live code changes (shadow captures snapshots)
- Testing production deployment (use staging environments)
- Simple single-file changes with good test coverage
Core Concepts [LEVEL 1]
Shadow Environment Architecture
A shadow environment is a Docker/Podman container with:
1. Git Bundle Snapshots - Exact working tree state (including uncommitted changes) 2. Embedded Gitea Server - Local git server at localhost:3000 inside container 3. Selective URL Rewriting - Git insteadOf rules redirect specific repos to local Gitea 4. Package Manager Isolation - UV, pip, npm, cargo, go caches isolated per shadow 5. API Key Passthrough - Common API keys automatically forwarded to container
┌─────────────────────────────────────────────────────────┐
│ Shadow Container │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Gitea Server (localhost:3000) │ │
│ │ - myorg/my-library (your snapshot) │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ Git URL Rewriting: │
│ github.com/myorg/my-library → Gitea (local) │
│ github.com/myorg/other-repo → Real GitHub │
│ │
│ /workspace (pre-cloned local sources) │
└─────────────────────────────────────────────────────────┘How Git URL Rewriting Works
When you create a shadow with ~/repos/my-lib:myorg/my-lib:
1. Your working directory is captured exactly as-is (uncommitted changes included) 2. Snapshot is bundled with full git history 3. Container starts with Gitea server 4. Snapshot pushed to Gitea as myorg/my-lib 5. Git config adds insteadOf rules:
[url "http://shadow:shadow@localhost:3000/myorg/my-lib.git"]
insteadOf = https://github.com/myorg/my-lib.git6. Any git clone https://github.com/myorg/my-lib → uses YOUR local snapshot 7. All other GitHub URLs → fetch from real GitHub
Result: Only your specified repos are local; everything else uses production sources.
Quick Start [LEVEL 1]
Installation
For Amplifier Users (native integration):
# Shadow tool is built-in - no installation needed
amplifier run --bundle amplihackFor Other Agents (standalone CLI):
# Install via uvx (recommended)
uvx amplifier-shadow --version
# Or via pip
pip install amplifier-bundle-shadow
# Verify installation
amplifier-shadow --versionPrerequisites:
- Docker or Podman installed and running
- Git installed
Your First Shadow (CLI)
# Create shadow with your local library changes
amplifier-shadow create --local ~/repos/my-library:myorg/my-library --name test-lib
# Inside the shadow, install via git URL
# → my-library uses YOUR LOCAL snapshot
# → all other dependencies fetch from REAL GitHub
amplifier-shadow exec test-lib "uv pip install git+https://github.com/myorg/my-library"
# Run tests
amplifier-shadow exec test-lib "cd /workspace && pytest"
# See what changed
amplifier-shadow diff test-lib
# Clean up when done
amplifier-shadow destroy test-libYour First Shadow (Amplifier Tool)
# Create shadow with local changes
shadow.create(local_sources=["~/repos/my-library:myorg/my-library"])
# Execute commands
shadow.exec(shadow_id, "uv pip install git+https://github.com/myorg/my-library")
shadow.exec(shadow_id, "pytest tests/")
# Extract results
shadow.extract(shadow_id, "/workspace/test-results", "./results")
# Cleanup
shadow.destroy(shadow_id)Tool Reference by Agent Type [LEVEL 2]
Amplifier (Native Integration)
Best experience - shadow is a first-class tool with automatic setup:
# All operations via shadow tool
result = shadow.create(
local_sources=["~/repos/lib:org/lib"],
verify=True # Automatic smoke test
)
# Integrated error handling and observability
if result.ready:
shadow.exec(result.shadow_id, "pytest")Features:
- Automatic API key passthrough
- Built-in smoke tests and health checks
- Integrated with other Amplifier tools
- Session-aware cleanup
Claude Code Standalone
Use the CLI directly from bash tool:
# All operations via amplifier-shadow CLI
uvx amplifier-shadow create --local ~/repos/my-lib:org/my-lib --name test
uvx amplifier-shadow exec test "pip install -e /workspace/org/my-lib"
uvx amplifier-shadow exec test "pytest"
uvx amplifier-shadow destroy testGitHub Copilot
Same CLI interface as Claude Code:
# Install once
pip install amplifier-bundle-shadow
# Use in workflow
amplifier-shadow create --local ~/repos/lib:org/lib
amplifier-shadow exec shadow-xxx "npm install && npm test"Manual/DIY (Any Agent)
Use the provided shell scripts and Docker Compose examples (see Level 3).
Common Patterns [LEVEL 2]
Pattern: Test Library Changes Before Publishing
# Test your library with its dependents
amplifier-shadow create --local ~/repos/my-library:myorg/my-library --name lib-test
# Clone dependent project and install
amplifier-shadow exec lib-test "
cd /workspace &&
git clone https://github.com/myorg/dependent-app &&
cd dependent-app &&
uv venv && . .venv/bin/activate &&
uv pip install git+https://github.com/myorg/my-library &&
pytest
"Pattern: Multi-Repo Changes
# Testing changes across multiple repos
amplifier-shadow create \
--local ~/repos/core-lib:myorg/core-lib \
--local ~/repos/cli-tool:myorg/cli-tool \
--name multi-test
# Both local sources will be used
amplifier-shadow exec multi-test "uv pip install git+https://github.com/myorg/cli-tool"Pattern: Iterate on Failures
# 1. Create shadow and run tests
amplifier-shadow create --local ~/repos/lib:org/lib --name test
amplifier-shadow exec test "pytest" # Fails
# 2. Fix code locally on host
# 3. Destroy and recreate (picks up your local changes)
amplifier-shadow destroy test
amplifier-shadow create --local ~/repos/lib:org/lib --name test
amplifier-shadow exec test "pytest" # Passes
# 4. Commit with confidence!
git commit -m "Fix issue"Pattern: Pre-Push CI Validation
# Run your CI script in shadow before pushing
amplifier-shadow create --local ~/repos/project:org/project --name ci-check
amplifier-shadow exec ci-check "
cd /workspace/org/project &&
./scripts/ci.sh
"
# If CI script passes, your push will likely succeedVerification Best Practices [LEVEL 2]
Always Verify Local Sources Are Used
After creating a shadow, confirm your local code is actually being used:
# Step 1: Check snapshot commits (from create output)
amplifier-shadow create --local ~/repos/lib:org/lib
# Output shows: snapshot_commits: {"org/lib": "abc1234..."}
# Step 2: Compare with install output
amplifier-shadow exec shadow-xxx "uv pip install git+https://github.com/org/lib"
# Look for: lib @ git+...@abc1234
# If commits match, your local code is being used!Pre-Cloned Repository Locations
Local sources are automatically cloned to /workspace/{org}/{repo}:
# Your local source microsoft/my-library is available at:
/workspace/microsoft/my-library
# Use for editable installs (Python)
amplifier-shadow exec shadow-xxx "pip install -e /workspace/microsoft/my-library"
# Or for Node.js
amplifier-shadow exec shadow-xxx "cd /workspace/microsoft/my-package && npm install"Always check this location first - the repo is already there.
Environment Variable Verification
# Don't assume - verify API keys are present!
amplifier-shadow exec shadow-xxx "env | grep API_KEY"
# Check all passed variables
amplifier-shadow status shadow-xxx
# Shows: env_vars_passed: ["ANTHROPIC_API_KEY", ...]Troubleshooting [LEVEL 2]
Common Issues
"UV tool install" uses cache instead of local source:
Problem: UV may bypass git URL rewriting for cached packages.
Solution:
# Option 1: Install from pre-cloned workspace (recommended)
amplifier-shadow exec xxx "pip install -e /workspace/org/lib"
# Option 2: Clear UV cache first
amplifier-shadow exec xxx "rm -rf /tmp/uv-cache && uv tool install git+https://github.com/org/lib""PEP 668: Externally-Managed Environment":
Solution: Always use virtual environments inside shadow:
amplifier-shadow exec xxx "
cd /workspace &&
uv venv &&
. .venv/bin/activate &&
uv pip install ...
""Container image not found":
Solution: Build the image locally:
amplifier-shadow build"/workspace permission denied":
Solution: Use $HOME or /tmp as alternatives:
amplifier-shadow exec xxx "cd $HOME && git clone ..."Level 3: Advanced Topics [LEVEL 3]
Custom Docker Images
Build your own shadow image with additional tools:
FROM ghcr.io/microsoft/amplifier-shadow:latest
# Add your tools
RUN apt-get update && apt-get install -y \
postgresql-client \
redis-tools
# Add custom scripts
COPY my-test-script.sh /usr/local/bin/Build and use:
docker build -t my-shadow:latest .
amplifier-shadow create --image my-shadow:latest --local ~/repos/lib:org/libShell Scripts (DIY Shadow Setup)
For agents without Amplifier access, use these standalone scripts:
Script 1: Create Git Bundle (scripts/create-bundle.sh):
#!/bin/bash
# Create git bundle snapshot of working tree
REPO_PATH=$1
OUTPUT_PATH=$2
cd "$REPO_PATH"
# Fetch all refs to ensure complete history
git fetch --all --tags --quiet 2>/dev/null || true
# Check for uncommitted changes
if [[ -n $(git status --porcelain) ]]; then
# Create temp clone and commit changes
TEMP_DIR=$(mktemp -d)
git clone --quiet "$REPO_PATH" "$TEMP_DIR"
# Sync working tree (including deletions)
rsync -a --delete --exclude='.git' "$REPO_PATH/" "$TEMP_DIR/"
cd "$TEMP_DIR"
git add -A
git commit --allow-empty -m "Shadow snapshot" --author="Shadow <shadow@localhost>"
# Create bundle
git bundle create "$OUTPUT_PATH" --all
cd /
rm -rf "$TEMP_DIR"
else
# Clean repo - just bundle it
git bundle create "$OUTPUT_PATH" --all
fi
echo "Bundle created: $OUTPUT_PATH"Script 2: Setup Shadow Container (scripts/setup-shadow.sh):
#!/bin/bash
# Start container with Gitea and configure git URL rewriting
CONTAINER_NAME=$1
BUNDLE_PATH=$2
ORG=$3
REPO=$4
# Start container
docker run -d \
--name "$CONTAINER_NAME" \
-v "$BUNDLE_PATH:/snapshots/bundle.git:ro" \
ghcr.io/microsoft/amplifier-shadow:latest
# Wait for Gitea
echo "Waiting for Gitea to start..."
until docker exec "$CONTAINER_NAME" curl -sf http://localhost:3000/api/v1/version > /dev/null; do
sleep 1
done
# Create org and repo in Gitea
docker exec "$CONTAINER_NAME" bash -c "
curl -s -u shadow:shadow \
-H 'Content-Type: application/json' \
-d '{\"username\":\"$ORG\"}' \
http://localhost:3000/api/v1/orgs
curl -s -u shadow:shadow \
-H 'Content-Type: application/json' \
-d '{\"name\":\"$REPO\",\"private\":false}' \
http://localhost:3000/api/v1/orgs/$ORG/repos
"
# Push bundle to Gitea
docker exec "$CONTAINER_NAME" bash -c "
cd /tmp &&
git init --bare repo.git &&
cd repo.git &&
git fetch /snapshots/bundle.git refs/heads/*:refs/heads/* &&
git remote add origin http://shadow:shadow@localhost:3000/$ORG/$REPO.git &&
git push origin --all --force
"
# Configure git URL rewriting
docker exec "$CONTAINER_NAME" bash -c "
git config --global url.'http://shadow:shadow@localhost:3000/$ORG/$REPO.git'.insteadOf 'https://github.com/$ORG/$REPO.git'
"
echo "Shadow container ready: $CONTAINER_NAME"
echo "Local source: $ORG/$REPO"Usage:
# Create bundle from your repo
./scripts/create-bundle.sh ~/repos/my-lib /tmp/my-lib.bundle
# Setup shadow container
./scripts/setup-shadow.sh shadow-test /tmp/my-lib.bundle myorg my-lib
# Test
docker exec shadow-test bash -c "
git clone https://github.com/myorg/my-lib /tmp/test &&
cd /tmp/test &&
git log -1 --oneline
"Docker Compose Examples
Example 1: Single Repository (docker-compose/single-repo.yml):
version: "3.8"
services:
shadow:
image: ghcr.io/microsoft/amplifier-shadow:latest
container_name: shadow-single
volumes:
- ./snapshots:/snapshots:ro
- ./workspace:/workspace
environment:
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- OPENAI_API_KEY=${OPENAI_API_KEY}
command: >
bash -c "
/usr/local/bin/gitea-init.sh &&
tail -f /dev/null
"Example 2: Multi-Repository Testing (docker-compose/multi-repo.yml):
version: "3.8"
services:
shadow-multi:
image: ghcr.io/microsoft/amplifier-shadow:latest
container_name: shadow-multi
volumes:
# Mount multiple bundles
- ./snapshots/core-lib.bundle:/snapshots/org/core-lib.bundle:ro
- ./snapshots/cli-tool.bundle:/snapshots/org/cli-tool.bundle:ro
- ./workspace:/workspace
environment:
# Pass API keys from host
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
# UV cache isolation
- UV_CACHE_DIR=/tmp/uv-cache
command: >
bash -c "
/usr/local/bin/gitea-init.sh &&
/usr/local/bin/setup-repos.sh org/core-lib org/cli-tool &&
tail -f /dev/null
"Usage:
# Create bundles for your repos
git -C ~/repos/core-lib bundle create snapshots/core-lib.bundle --all
git -C ~/repos/cli-tool bundle create snapshots/cli-tool.bundle --all
# Start shadow
docker-compose -f docker-compose/multi-repo.yml up -d
# Run tests
docker-compose exec shadow-multi bash -c "
cd /workspace &&
git clone https://github.com/org/cli-tool &&
cd cli-tool &&
uv pip install -e .
pytest
"
# Cleanup
docker-compose downExample 3: CI Integration (docker-compose/ci-shadow.yml):
version: "3.8"
services:
ci-shadow:
image: ghcr.io/microsoft/amplifier-shadow:latest
container_name: ci-shadow
volumes:
- ./snapshots:/snapshots:ro
- ./test-results:/test-results
environment:
- CI=true
- GITHUB_ACTIONS=true
command: >
bash -c "
/usr/local/bin/gitea-init.sh &&
/usr/local/bin/run-ci-tests.sh > /test-results/output.log 2>&1
"GitHub Actions Integration:
# .github/workflows/shadow-test.yml
name: Shadow Test
on: [push, pull_request]
jobs:
shadow-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Create git bundle
run: git bundle create snapshot.bundle --all
- name: Run shadow tests
run: |
docker run --rm \
-v $PWD/snapshot.bundle:/snapshots/bundle.git:ro \
ghcr.io/microsoft/amplifier-shadow:latest \
/usr/local/bin/test-in-shadow.sh org/repoIntegration with Outside-In Testing
Combine shadow environments with agentic outside-in tests:
# Create shadow with local changes
amplifier-shadow create --local ~/repos/lib:org/lib --name test
# Run outside-in test scenarios inside shadow
amplifier-shadow exec test "gadugi-agentic-test run test-scenario.yaml"
# Extract evidence
amplifier-shadow extract test /evidence ./test-evidenceSee the qa-team skill for complete integration examples (outside-in-testing remains an alias).
Best Practices [LEVEL 2]
1. Always Verify Your Sources Are Used
Don't assume - verify that the shadow is actually using your local code:
# Check snapshot commits
amplifier-shadow status shadow-xxx | grep snapshot_commit
# Verify install resolves to that commit
amplifier-shadow exec shadow-xxx "pip install git+https://github.com/org/lib" | grep "org/lib @"2. Use Pre-Cloned Workspace
Local sources are automatically at /workspace/{org}/{repo}:
# ✅ FAST: Use pre-cloned repo
amplifier-shadow exec xxx "pip install -e /workspace/org/lib"
# ❌ SLOWER: Clone again
amplifier-shadow exec xxx "git clone https://github.com/org/lib && pip install -e lib"3. Isolate Package Manager Caches
Shadow environments automatically isolate caches to prevent stale packages:
- Python UV:
/tmp/uv-cache - Python pip:
/tmp/pip-cache - Node npm:
/tmp/npm-cache - Rust cargo:
/tmp/cargo-home - Go modules:
/tmp/go-mod-cache
These are set automatically - no action needed.
4. Pass Required Environment Variables
# Amplifier (automatic for common API keys)
shadow.create(local_sources=["~/repos/lib:org/lib"])
# CLI (explicit)
amplifier-shadow create \
--local ~/repos/lib:org/lib \
--env ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \
--env CUSTOM_VAR=value5. Clean Up After Testing
# Always destroy shadows when done
amplifier-shadow destroy shadow-xxx
# Or destroy all
amplifier-shadow destroy-all6. Use Named Shadows for Clarity
# ✅ GOOD: Descriptive name
amplifier-shadow create --local ~/repos/lib:org/lib --name test-breaking-change
# ❌ BAD: Auto-generated
amplifier-shadow create --local ~/repos/lib:org/lib
# Creates shadow-a3f2b8c1 (hard to remember)Integration Patterns [LEVEL 3]
Pattern: Shadow + Outside-In Tests
Combine shadow isolation with declarative test scenarios:
# test-scenario.yaml
scenario:
name: "Library Integration Test"
type: cli
steps:
- action: launch
target: "/workspace/org/lib/cli.py"
- action: verify_output
contains: "Success"Run in shadow:
amplifier-shadow create --local ~/repos/lib:org/lib --name test
amplifier-shadow exec test "gadugi-agentic-test run test-scenario.yaml"Pattern: Shadow + pytest
amplifier-shadow create --local ~/repos/lib:org/lib --name pytest-run
amplifier-shadow exec pytest-run "
cd /workspace/org/lib &&
uv venv && . .venv/bin/activate &&
pip install -e '.[dev]' &&
pytest --cov=src --cov-report=html
"
# Extract coverage report
amplifier-shadow extract pytest-run /workspace/org/lib/htmlcov ./coverage-reportPattern: Shadow + npm test
amplifier-shadow create --local ~/repos/pkg:org/pkg --name npm-test
amplifier-shadow exec npm-test "
cd /workspace/org/pkg &&
npm install &&
npm test
"Pattern: Shadow + cargo test
amplifier-shadow create --local ~/repos/crate:org/crate --name cargo-test
amplifier-shadow exec cargo-test "
cd /workspace/org/crate &&
cargo build &&
cargo test
"Philosophy Alignment [LEVEL 2]
This skill follows amplihack's core principles:
Ruthless Simplicity
- Minimal abstraction: Shadow = container + gitea + URL rewriting
- No frameworks: Pure Docker, git, and shell scripts
- Essential only: Only captures what's needed (git bundle, not entire filesystems)
Modular Design (Bricks & Studs)
- Self-contained: Each shadow is independent
- Clear contract: Git URLs in → local sources out
- Composable: Combine with other testing tools
Zero-BS Implementation
- No stubs: Every script works completely
- Working defaults: Reasonable defaults for all operations
- Clear errors: Actionable error messages with troubleshooting
Outside-In Thinking
- User perspective: Test what users will see
- Implementation agnostic: Don't care how code works internally
- Behavior-driven: Focus on outcomes
CLI Reference [LEVEL 3]
Commands
# Create shadow environment
amplifier-shadow create [OPTIONS]
--local, -l TEXT Local source mapping: /path/to/repo:org/name (repeatable)
--name, -n TEXT Name for environment (auto-generated if not provided)
--image, -i TEXT Container image (default: amplifier-shadow:local)
--env, -e TEXT Environment variable: KEY=VALUE or KEY to inherit (repeatable)
--env-file FILE File with environment variables (one per line)
--pass-api-keys Auto-pass common API key env vars (default: enabled)
# Execute command in shadow
amplifier-shadow exec SHADOW_ID COMMAND
--timeout INTEGER Timeout in seconds (default: 300)
# Show changed files
amplifier-shadow diff SHADOW_ID [PATH]
# Extract file from shadow
amplifier-shadow extract SHADOW_ID CONTAINER_PATH HOST_PATH
# Inject file into shadow
amplifier-shadow inject SHADOW_ID HOST_PATH CONTAINER_PATH
# List all shadows
amplifier-shadow list
# Show shadow status
amplifier-shadow status SHADOW_ID
# Destroy shadow
amplifier-shadow destroy SHADOW_ID
--force Force destruction even on errors
# Destroy all shadows
amplifier-shadow destroy-all
--force Force destruction even on errors
# Build shadow image locally
amplifier-shadow build
# Open interactive shell
amplifier-shadow shell SHADOW_IDQuick Reference Card [LEVEL 1]
# Typical workflow
amplifier-shadow create --local ~/repos/lib:org/lib --name test
amplifier-shadow exec test "pytest"
amplifier-shadow destroy test
# Multi-repo
amplifier-shadow create \
--local ~/repos/lib1:org/lib1 \
--local ~/repos/lib2:org/lib2 \
--name multi
# With environment variables
amplifier-shadow create \
--local ~/repos/lib:org/lib \
--env API_KEY=$API_KEY \
--name test
# Interactive shell
amplifier-shadow shell test
# Extract results
amplifier-shadow extract test /workspace/results ./local-resultsRelated Skills [LEVEL 1]
- qa-team: Run agentic tests in shadow environments (legacy name:
outside-in-testing) - test-gap-analyzer: Find untested code paths (complement shadow testing)
- philosophy-guardian: Verify shadow scripts follow ruthless simplicity
Troubleshooting Checklist [LEVEL 2]
When shadow tests fail:
- [ ] Verify local sources are being used (check snapshot commits)
- [ ] Check pre-cloned repos exist at
/workspace/{org}/{repo} - [ ] Verify environment variables are passed (run
envinside shadow) - [ ] Clear package manager caches if stale
- [ ] Check git URL rewriting is configured (
git config --list) - [ ] Verify Gitea is accessible (
curl http://localhost:3000/api/v1/version) - [ ] Use virtual environments (avoid PEP 668 errors)
- [ ] Check container is running (
amplifier-shadow status)
Changelog [LEVEL 3]
Version 1.0.0 (2026-01-29)
- Initial skill release
- Support for Amplifier, Claude Code, GitHub Copilot, manual DIY
- Shell scripts for standalone usage
- Docker Compose examples for CI integration
- Complete CLI reference and troubleshooting guide
- Integration patterns with qa-team / outside-in-testing alias
- Philosophy alignment with ruthless simplicity
---
Remember: Shadow environments let you test exactly what's on your machine (uncommitted changes and all) in a clean, isolated environment that mirrors CI. Use them before every significant push to catch issues early.
version: "3.8"
# CI-optimized shadow environment
# Usage:
# In GitHub Actions or other CI:
# 1. Create bundle in CI: git bundle create snapshot.bundle --all
# 2. Run: docker-compose -f docker-compose/ci-shadow.yml run --rm ci-test
# 3. Check exit code for pass/fail
services:
ci-shadow:
image: ghcr.io/microsoft/amplifier-shadow:latest
container_name: ci-shadow
volumes:
# CI creates bundle from current commit
- ./snapshot.bundle:/snapshots/repo.bundle:ro
# Results directory for artifacts
- ./test-results:/test-results
environment:
# CI environment markers
- CI=true
- GITHUB_ACTIONS=${GITHUB_ACTIONS:-false}
# Cache isolation
- UV_NO_GITHUB_FAST_PATH=1
- UV_CACHE_DIR=/tmp/uv-cache
# Run tests and exit with status code
command: >
bash -c "
set -e
echo '==> Starting Gitea' &&
/usr/local/bin/docker-entrypoint.sh &
sleep 5 &&
echo '==> Waiting for Gitea' &&
until curl -sf http://localhost:3000/api/v1/version > /dev/null; do
sleep 1
done &&
echo '==> Setting up repository' &&
ORG=\${REPO_ORG:-myorg}
REPO=\${REPO_NAME:-my-repo}
curl -s -u shadow:shadow -H 'Content-Type: application/json' \
-d \"{\\\"username\\\":\\\"\$ORG\\\"}\" \
http://localhost:3000/api/v1/orgs &&
curl -s -u shadow:shadow -H 'Content-Type: application/json' \
-d \"{\\\"name\\\":\\\"\$REPO\\\",\\\"private\\\":false}\" \
http://localhost:3000/api/v1/orgs/\$ORG/repos &&
cd /tmp && rm -rf _push && mkdir _push && cd _push &&
git init --bare --quiet &&
git bundle list-heads /snapshots/repo.bundle | while read sha ref; do
branch_name=\$(echo \"\$ref\" | sed 's|refs/heads/||; s|refs/remotes/origin/|_upstream_|')
if echo \"\$ref\" | grep -q \"HEAD\"; then continue; fi
git fetch /snapshots/repo.bundle \"\$ref:refs/heads/\$branch_name\" 2>/dev/null || true
done &&
git remote add origin http://shadow:shadow@localhost:3000/\$ORG/\$REPO.git &&
git push origin --all --force 2>&1 | grep -v 'remote:' &&
echo '==> Configuring git' &&
git config --global url.\"http://shadow:shadow@localhost:3000/\$ORG/\$REPO.git\".insteadOf \"https://github.com/\$ORG/\$REPO.git\" &&
echo '==> Cloning to workspace' &&
git clone http://shadow:shadow@localhost:3000/\$ORG/\$REPO.git /workspace/repo --quiet 2>&1 &&
cd /workspace/repo &&
echo '==> Running tests' &&
if [ -f './scripts/ci-test.sh' ]; then
./scripts/ci-test.sh
elif [ -f 'pytest.ini' ] || [ -f 'pyproject.toml' ]; then
uv venv && . .venv/bin/activate && uv pip install -e '.[dev]' && pytest
elif [ -f 'package.json' ]; then
npm install && npm test
elif [ -f 'Cargo.toml' ]; then
cargo test
else
echo 'No test configuration found'
exit 1
fi &&
echo '==> Tests passed!' &&
exit 0
"
version: "3.8"
# Multi-repository shadow environment for coordinated changes
# Usage:
# 1. Create bundles:
# git -C ~/repos/core-lib bundle create snapshots/core-lib.bundle --all
# git -C ~/repos/cli-tool bundle create snapshots/cli-tool.bundle --all
# 2. Start: docker-compose -f docker-compose/multi-repo.yml up -d
# 3. Test: docker-compose exec shadow bash
# 4. Stop: docker-compose down
services:
shadow:
image: ghcr.io/microsoft/amplifier-shadow:latest
container_name: shadow-multi
volumes:
# Mount multiple git bundles
- ./snapshots/core-lib.bundle:/snapshots/myorg/core-lib.bundle:ro
- ./snapshots/cli-tool.bundle:/snapshots/myorg/cli-tool.bundle:ro
# Workspace for testing
- ./workspace:/workspace
environment:
# Pass API keys from host
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- OPENAI_API_KEY=${OPENAI_API_KEY}
# Cache isolation
- UV_NO_GITHUB_FAST_PATH=1
- UV_CACHE_DIR=/tmp/uv-cache
- PIP_CACHE_DIR=/tmp/pip-cache
- NPM_CONFIG_CACHE=/tmp/npm-cache
command: >
bash -c "
echo 'Starting Gitea...' &&
/usr/local/bin/docker-entrypoint.sh &
sleep 5 &&
echo 'Waiting for Gitea to be ready...' &&
until curl -sf http://localhost:3000/api/v1/version > /dev/null; do
sleep 1
done &&
echo 'Creating organization myorg...' &&
curl -s -u shadow:shadow -H 'Content-Type: application/json' \
-d '{\"username\":\"myorg\"}' \
http://localhost:3000/api/v1/orgs &&
echo 'Setting up repository: core-lib...' &&
curl -s -u shadow:shadow -H 'Content-Type: application/json' \
-d '{\"name\":\"core-lib\",\"private\":false}' \
http://localhost:3000/api/v1/orgs/myorg/repos &&
cd /tmp && rm -rf _push_core && mkdir _push_core && cd _push_core &&
git init --bare --quiet &&
git bundle list-heads /snapshots/myorg/core-lib.bundle | while read sha ref; do
branch_name=\$(echo \"\$ref\" | sed 's|refs/heads/||; s|refs/remotes/origin/|_upstream_|')
if echo \"\$ref\" | grep -q \"HEAD\"; then continue; fi
git fetch /snapshots/myorg/core-lib.bundle \"\$ref:refs/heads/\$branch_name\" 2>/dev/null || true
done &&
git remote add origin http://shadow:shadow@localhost:3000/myorg/core-lib.git &&
git push origin --all --force 2>&1 | grep -v 'remote:' &&
echo 'Setting up repository: cli-tool...' &&
curl -s -u shadow:shadow -H 'Content-Type: application/json' \
-d '{\"name\":\"cli-tool\",\"private\":false}' \
http://localhost:3000/api/v1/orgs/myorg/repos &&
cd /tmp && rm -rf _push_cli && mkdir _push_cli && cd _push_cli &&
git init --bare --quiet &&
git bundle list-heads /snapshots/myorg/cli-tool.bundle | while read sha ref; do
branch_name=\$(echo \"\$ref\" | sed 's|refs/heads/||; s|refs/remotes/origin/|_upstream_|')
if echo \"\$ref\" | grep -q \"HEAD\"; then continue; fi
git fetch /snapshots/myorg/cli-tool.bundle \"\$ref:refs/heads/\$branch_name\" 2>/dev/null || true
done &&
git remote add origin http://shadow:shadow@localhost:3000/myorg/cli-tool.git &&
git push origin --all --force 2>&1 | grep -v 'remote:' &&
echo 'Configuring git URL rewriting...' &&
git config --global url.'http://shadow:shadow@localhost:3000/myorg/core-lib.git'.insteadOf 'https://github.com/myorg/core-lib.git' &&
git config --global url.'http://shadow:shadow@localhost:3000/myorg/cli-tool.git'.insteadOf 'https://github.com/myorg/cli-tool.git' &&
echo 'Pre-cloning to workspace...' &&
mkdir -p /workspace/myorg &&
git clone http://shadow:shadow@localhost:3000/myorg/core-lib.git /workspace/myorg/core-lib --quiet 2>&1 &&
git clone http://shadow:shadow@localhost:3000/myorg/cli-tool.git /workspace/myorg/cli-tool --quiet 2>&1 &&
echo 'Shadow environment ready!' &&
echo 'Local sources: myorg/core-lib, myorg/cli-tool' &&
echo 'Test with: docker-compose exec shadow bash' &&
tail -f /dev/null
"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/v1/version"]
interval: 10s
timeout: 5s
retries: 5
Docker Compose Examples for Shadow Testing
These Docker Compose configurations provide declarative shadow environment setups for different use cases.
Files
single-repo.yml- Basic single repository shadowmulti-repo.yml- Multiple coordinated repositoriesci-shadow.yml- CI-optimized automated testing
Prerequisites
- Docker or Podman with Docker Compose support
- Git bundles created from your local repositories
Quick Start
Single Repository
# 1. Create bundle from your local repo
git -C ~/repos/my-lib bundle create snapshots/my-lib.bundle --all
# 2. Create directory structure
mkdir -p snapshots workspace
# 3. Start shadow
docker-compose -f docker-compose/single-repo.yml up -d
# 4. Watch logs
docker-compose -f docker-compose/single-repo.yml logs -f
# 5. Once ready, test
docker-compose exec shadow bashInside the shadow container:
# Verify git URL rewriting works
git clone https://github.com/myorg/my-lib /tmp/test
cd /tmp/test
git log -1 --oneline # Should show your local commit
# Or use pre-cloned workspace
cd /workspace/myorg/my-lib
pip install -e .
pytestMulti-Repository
# 1. Create bundles for each repo
git -C ~/repos/core-lib bundle create snapshots/core-lib.bundle --all
git -C ~/repos/cli-tool bundle create snapshots/cli-tool.bundle --all
# 2. Start shadow
docker-compose -f docker-compose/multi-repo.yml up -d
# 3. Test coordinated changes
docker-compose exec shadow bash -c "
cd /workspace &&
git clone https://github.com/myorg/cli-tool &&
cd cli-tool &&
pip install git+https://github.com/myorg/core-lib &&
pytest
"CI Integration
GitHub Actions
# .github/workflows/shadow-test.yml
name: Shadow Test
on: [push, pull_request]
jobs:
shadow-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Create git bundle
run: |
git bundle create snapshot.bundle --all
mkdir -p test-results
- name: Run shadow tests
run: |
docker-compose -f docker-compose/ci-shadow.yml run --rm ci-shadow
env:
REPO_ORG: myorg
REPO_NAME: my-repo
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: test-results
path: test-results/GitLab CI
# .gitlab-ci.yml
shadow-test:
image: docker:latest
services:
- docker:dind
script:
- git bundle create snapshot.bundle --all
- mkdir -p test-results
- docker-compose -f docker-compose/ci-shadow.yml run --rm ci-shadow
artifacts:
paths:
- test-results/
when: alwaysConfiguration
Environment Variables
Pass environment variables to the shadow:
environment:
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- CUSTOM_VAR=valueOr use .env file:
# .env
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...Custom Repository Names
Edit the compose file to match your org/repo:
# Change these lines in the command section
echo 'Setting up repository myorg/my-lib...' &&
curl -s -u shadow:shadow -H 'Content-Type: application/json' \
-d '{"username":"myorg"}' \
http://localhost:3000/api/v1/orgs &&
curl -s -u shadow:shadow -H 'Content-Type: application/json' \
-d '{"name":"my-lib","private":false}' \
http://localhost:3000/api/v1/orgs/myorg/repos &&Troubleshooting
Container Won't Start
Check logs:
docker-compose logs shadowCommon issues:
- Bundle file not found: Check path in volumes section
- Gitea timeout: Increase sleep time in command
- Port conflict: Change exposed ports in compose file
Tests Fail Inside Shadow
Verify local sources are being used:
docker-compose exec shadow bash -c "
git clone https://github.com/myorg/my-lib /tmp/test &&
cd /tmp/test &&
git log -1 --format='%H'
"
# Compare with your local commit SHAClean Up
# Stop and remove containers
docker-compose down
# Remove volumes (workspace, etc.)
docker-compose down -v
# Remove all shadow-related containers
docker ps -a | grep shadow | awk '{print $1}' | xargs docker rm -fAdvanced Usage
Custom Docker Image
Build your own shadow image with additional tools:
# Dockerfile.custom-shadow
FROM ghcr.io/microsoft/amplifier-shadow:latest
# Add tools
RUN apt-get update && apt-get install -y \
postgresql-client \
redis-tools \
jq
# Add custom scripts
COPY scripts/ /usr/local/bin/Update compose file:
services:
shadow:
build:
context: .
dockerfile: Dockerfile.custom-shadow
# ... rest of configMultiple Shadows in Parallel
# docker-compose.parallel.yml
version: "3.8"
services:
shadow-python:
image: ghcr.io/microsoft/amplifier-shadow:latest
container_name: shadow-python-test
volumes:
- ./snapshots/python-lib.bundle:/snapshots/lib.bundle:ro
# ... config for Python project
shadow-node:
image: ghcr.io/microsoft/amplifier-shadow:latest
container_name: shadow-node-test
volumes:
- ./snapshots/node-pkg.bundle:/snapshots/pkg.bundle:ro
# ... config for Node.js projectRun both:
docker-compose -f docker-compose.parallel.yml up -dSee Also
- Main shadow-testing skill:
../SKILL.md - Shell scripts:
../scripts/ - Amplifier shadow bundle: https://github.com/microsoft/amplifier-bundle-shadow
version: "3.8"
# Single repository shadow environment
# Usage:
# 1. Create bundle: git -C ~/repos/my-lib bundle create snapshots/my-lib.bundle --all
# 2. Start: docker-compose -f docker-compose/single-repo.yml up -d
# 3. Test: docker-compose exec shadow bash
# 4. Stop: docker-compose down
services:
shadow:
image: ghcr.io/microsoft/amplifier-shadow:latest
container_name: shadow-single
volumes:
# Mount your git bundle (create with: git bundle create snapshots/my-lib.bundle --all)
- ./snapshots/my-lib.bundle:/snapshots/myorg/my-lib.bundle:ro
# Workspace for testing
- ./workspace:/workspace
environment:
# Pass API keys from host environment
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- OPENAI_API_KEY=${OPENAI_API_KEY}
# UV cache isolation
- UV_NO_GITHUB_FAST_PATH=1
- UV_CACHE_DIR=/tmp/uv-cache
# Keep container running
command: >
bash -c "
echo 'Starting Gitea...' &&
/usr/local/bin/docker-entrypoint.sh &
sleep 5 &&
echo 'Waiting for Gitea to be ready...' &&
until curl -sf http://localhost:3000/api/v1/version > /dev/null; do
sleep 1
done &&
echo 'Setting up repository myorg/my-lib...' &&
curl -s -u shadow:shadow -H 'Content-Type: application/json' \
-d '{\"username\":\"myorg\"}' \
http://localhost:3000/api/v1/orgs &&
curl -s -u shadow:shadow -H 'Content-Type: application/json' \
-d '{\"name\":\"my-lib\",\"private\":false}' \
http://localhost:3000/api/v1/orgs/myorg/repos &&
cd /tmp &&
rm -rf _push && mkdir _push && cd _push &&
git init --bare --quiet &&
git bundle list-heads /snapshots/myorg/my-lib.bundle | while read sha ref; do
branch_name=\$(echo \"\$ref\" | sed 's|refs/heads/||; s|refs/remotes/origin/|_upstream_|')
if echo \"\$ref\" | grep -q \"HEAD\"; then continue; fi
git fetch /snapshots/myorg/my-lib.bundle \"\$ref:refs/heads/\$branch_name\" 2>/dev/null || true
done &&
git remote add origin http://shadow:shadow@localhost:3000/myorg/my-lib.git &&
git push origin --all --force 2>&1 | grep -v 'remote:' &&
echo 'Configuring git URL rewriting...' &&
git config --global url.'http://shadow:shadow@localhost:3000/myorg/my-lib.git'.insteadOf 'https://github.com/myorg/my-lib.git' &&
echo 'Pre-cloning to workspace...' &&
mkdir -p /workspace/myorg &&
git clone http://shadow:shadow@localhost:3000/myorg/my-lib.git /workspace/myorg/my-lib --quiet 2>&1 &&
echo 'Shadow environment ready!' &&
echo 'Test with: docker-compose exec shadow git clone https://github.com/myorg/my-lib /tmp/test' &&
tail -f /dev/null
"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/v1/version"]
interval: 10s
timeout: 5s
retries: 5
Example: Multi-Repository Coordination
This example shows testing coordinated changes across multiple repositories.
Scenario
You're working on two repositories:
myorg/api-client- HTTP client librarymyorg/cli-tool- CLI that depends on api-client
Both have uncommitted changes that must work together.
Local Changes
api-client (breaking change):
# Old API
client.get(endpoint)
# New API (renamed for clarity)
client.fetch(endpoint) # BREAKING: renamed from get()cli-tool (updated to use new API):
# Updated to use new fetch() method
def download(url):
return client.fetch(url) # Changed from client.get()Setup Shadow with Both Repos
Using Amplifier
result = shadow.create(local_sources=[
"~/repos/api-client:myorg/api-client",
"~/repos/cli-tool:myorg/cli-tool"
])
print("Snapshot commits:")
for repo, commit in result.output['snapshot_commits'].items():
print(f" {repo}: {commit}")Using Standalone CLI
amplifier-shadow create \
--local ~/repos/api-client:myorg/api-client \
--local ~/repos/cli-tool:myorg/cli-tool \
--name multi-test
# Output shows both snapshot commits for verificationTest Coordinated Changes
# Install cli-tool (which depends on api-client)
# Both will use YOUR local snapshots
amplifier-shadow exec multi-test "
cd /workspace &&
git clone https://github.com/myorg/cli-tool test-cli &&
cd test-cli &&
uv venv && . .venv/bin/activate &&
# This installs BOTH local snapshots via git dependencies
uv pip install -e . &&
# Run full test suite
pytest tests/ -v
"Verification
Verify both local sources are being used:
amplifier-shadow exec multi-test "
cd test-cli &&
pip list | grep -E 'api-client|cli-tool'
"
# Should show both installed from git with your snapshot commitsExpected Outcomes
Success Case
✓ cli-tool tests pass
✓ api-client is using your local snapshot (commit abc1234)
✓ cli-tool is using your local snapshot (commit def5678)
Both changes are compatible - safe to push!Failure Case
✗ Tests fail: AttributeError: 'Client' object has no attribute 'fetch'
Diagnosis: api-client wasn't actually installed from your local source.
Possible causes:
- UV cache hit (run with --refresh)
- Git URL rewriting not working
- Wrong org/repo name in local_sourcesTroubleshooting
If only one repo uses local source:
# Check git URL rewriting config
amplifier-shadow exec multi-test "git config --list | grep insteadOf"
# Should show rules for BOTH repositoriesCleanup
amplifier-shadow destroy multi-testPro Tip: Iterative Testing
Shadow environments are cheap to create/destroy:
# Run 1: Test coordinated changes
amplifier-shadow create --local ... --name test
amplifier-shadow exec test "pytest" # Fails
# Fix locally on host
# Run 2: Destroy and recreate (fast)
amplifier-shadow destroy test
amplifier-shadow create --local ... --name test
amplifier-shadow exec test "pytest" # Passes!
# Commit both repos with confidenceExample: Testing Node.js Package Changes
This example shows testing a Node.js package with uncommitted changes.
Scenario
You're developing myorg/ui-components package and want to test changes with a Next.js app that uses it.
Local Changes
// ~/repos/ui-components/src/Button.tsx
// Added new variant prop
export interface ButtonProps {
variant?: "primary" | "secondary" | "danger"; // NEW
children: React.ReactNode;
}
export function Button({ variant = "primary", children }: ButtonProps) {
// ...implementation
}Setup Shadow
amplifier-shadow create \
--local ~/repos/ui-components:myorg/ui-components \
--name ui-testTest with Next.js App
Method 1: Install via Git URL
amplifier-shadow exec ui-test "
cd /workspace &&
git clone https://github.com/myorg/next-app test-app &&
cd test-app &&
# Install ui-components from git (uses your local snapshot)
npm install git+https://github.com/myorg/ui-components &&
# Build and test
npm run build &&
npm test
"Method 2: Link Pre-Cloned Package
amplifier-shadow exec ui-test "
# ui-components is already at /workspace/myorg/ui-components
cd /workspace/myorg/ui-components &&
npm install &&
npm run build &&
npm link &&
# Clone app and link to local package
cd /workspace &&
git clone https://github.com/myorg/next-app test-app &&
cd test-app &&
npm install &&
npm link @myorg/ui-components &&
# Test
npm run build &&
npm test
"Verify Local Package Used
# Check installed version
amplifier-shadow exec ui-test "
cd test-app &&
npm list @myorg/ui-components
"
# Should show: @myorg/ui-components@2.0.0 -> git+https://github.com/...@abc1234
# Verify abc1234 matches your snapshot commitTest Type Safety
# If using TypeScript, verify types work
amplifier-shadow exec ui-test "
cd test-app &&
npm run type-check
"Expected Outcomes
Success
✓ Build successful
✓ Type checking passed
✓ Tests passed (23/23)
Your new variant prop is backward compatible!Failure
✗ Type error: Property 'variant' does not exist on type 'ButtonProps'
Diagnosis: App's node_modules still has old version
Solution: Clear npm cache in shadow:
amplifier-shadow exec ui-test "rm -rf /tmp/npm-cache"Cleanup
amplifier-shadow destroy ui-testPro Tip: Watch Mode Testing
For iterative development, keep shadow running and re-run tests:
# Create shadow once
amplifier-shadow create --local ~/repos/ui-components:myorg/ui-components --name dev
# Test iteration loop
while true; do
# Edit files on host
# Recreate shadow with new snapshot
amplifier-shadow destroy dev
amplifier-shadow create --local ~/repos/ui-components:myorg/ui-components --name dev
# Run tests
amplifier-shadow exec dev "cd /workspace/myorg/ui-components && npm test"
read -p "Continue? (y/n) " yn
[[ $yn != "y" ]] && break
doneExample: Testing Python Library Changes
This example shows testing a Python library's uncommitted changes with a dependent project.
Scenario
You're working on myorg/data-processor library and want to test changes with the CLI tool that depends on it before pushing.
Local Changes
# ~/repos/data-processor/src/data_processor/core.py
# You've added a new parameter to process()
def process(data, validate=True): # NEW: validate parameter
if validate:
check_schema(data)
return transform(data)This is a breaking change if callers don't pass validate. Test with the dependent CLI tool before pushing.
Setup Shadow
Using Amplifier
# Create shadow with your local changes
result = shadow.create(
local_sources=["~/repos/data-processor:myorg/data-processor"]
)
shadow_id = result.output["shadow_id"]
print(f"Created shadow: {shadow_id}")
print(f"Snapshot commit: {result.output['snapshot_commits']['myorg/data-processor']}")Using Standalone CLI
amplifier-shadow create \
--local ~/repos/data-processor:myorg/data-processor \
--name test-breaking-change
# Note the snapshot commit from output for verificationTest the Change
# Install dependent CLI tool (will use YOUR local data-processor)
amplifier-shadow exec test-breaking-change "
cd /workspace &&
git clone https://github.com/myorg/data-cli &&
cd data-cli &&
uv venv && . .venv/bin/activate &&
uv pip install git+https://github.com/myorg/data-processor &&
pytest tests/
"Verify Local Source Used
# Check what commit was installed
amplifier-shadow exec test-breaking-change "
pip show data-processor | grep Location
"
# Should show installed from git with your snapshot commit SHAExpected Outcomes
If Tests Pass
✓ All tests passed
Your breaking change is backward compatible OR
dependent project already handles the new parameter
Safe to push!If Tests Fail
✗ Tests failed in test_process.py::test_basic_process
TypeError: process() got an unexpected keyword argument 'validate'
Action required:
1. Update data-processor to make validate optional (validate=True as default)
2. OR update data-cli to pass validate parameter
3. Test again in shadowCleanup
amplifier-shadow destroy test-breaking-changeAlternative: Use Pre-Cloned Workspace
Shadow automatically clones local sources to /workspace/{org}/{repo}:
amplifier-shadow exec test-breaking-change "
cd /workspace/myorg/data-processor &&
pip install -e . &&
pytest
"This is faster than cloning via git URL.
Shadow Testing Skill
Test local uncommitted changes in isolated container environments before pushing to remote repositories.
What This Skill Provides
This skill teaches shadow testing - a methodology for testing local changes (including uncommitted work) in clean, isolated container environments that mirror CI/CD conditions.
Key Benefits:
- Test exactly what's on your machine (uncommitted changes and all)
- Clean-state validation ("does it work on a fresh machine?")
- Multi-repo coordination (test changes across multiple repositories)
- CI parity (catch issues before pushing)
Quick Start
For Amplifier Users
Shadow tool is built-in - just use it:
# Create shadow with local changes
shadow.create(local_sources=["~/repos/my-lib:org/my-lib"])
# Run tests
shadow.exec(shadow_id, "pytest")
# Cleanup
shadow.destroy(shadow_id)For Other Agents (Claude Code, GitHub Copilot, etc.)
Install standalone CLI:
# Via uvx (recommended)
uvx amplifier-shadow --version
# Or via pip
pip install amplifier-bundle-shadow
# Create shadow
amplifier-shadow create --local ~/repos/my-lib:org/my-lib --name test
# Run tests
amplifier-shadow exec test "pytest"
# Cleanup
amplifier-shadow destroy testWhat's Included
Core Documentation
- SKILL.md - Complete skill with progressive disclosure (Levels 1-4)
- Level 1: Fundamentals and quick start
- Level 2: Common patterns and verification
- Level 3: Advanced topics and DIY setup
- Includes philosophy alignment and troubleshooting
Generalizable Shell Scripts
Located in scripts/:
- create-bundle.sh - Creates git bundle snapshots from any local repo
- setup-shadow.sh - Starts container with Gitea and configures URL rewriting
- test-shadow.sh - Verifies shadow environment is working correctly
These scripts work without Amplifier - pure bash, git, and Docker.
Docker Compose Examples
Located in docker-compose/:
- single-repo.yml - Basic single repository shadow
- multi-repo.yml - Multiple coordinated repositories
- ci-shadow.yml - CI-optimized automated testing
- README.md - Complete Docker Compose usage guide
Includes GitHub Actions and GitLab CI integration examples.
Key Features
1. Exact Working Tree Snapshots
Captures your local state exactly as-is:
- New/untracked files included
- Modified files with current changes
- Deleted files properly removed
- No staging required - what you see is what gets tested
2. Selective Git URL Rewriting
Only your specified repos are local; everything else uses real GitHub:
# github.com/org/my-lib → Your local snapshot
# github.com/org/other-repo → Real GitHubUses git insteadOf rules with boundary markers to prevent prefix collisions.
3. Package Manager Cache Isolation
Automatic cache isolation prevents stale packages:
- Python UV:
/tmp/uv-cache - Python pip:
/tmp/pip-cache - Node npm:
/tmp/npm-cache - Rust cargo:
/tmp/cargo-home - Go modules:
/tmp/go-mod-cache
4. Pre-Cloned Workspace
Local sources automatically cloned to /workspace/{org}/{repo} for convenience.
5. Multi-Language Support
Works with any language/ecosystem:
- Python (uv, pip, poetry)
- Node.js (npm, yarn, pnpm)
- Rust (cargo)
- Go (go modules)
- Any git-based dependency
Integration with Outside-In Testing
Combine shadow environments with agentic outside-in tests for complete pre-push validation:
# Create shadow with local changes
amplifier-shadow create --local ~/repos/lib:org/lib --name test
# Run outside-in test scenarios inside shadow
amplifier-shadow exec test "gadugi-agentic-test run test-scenario.yaml"
# Extract evidence
amplifier-shadow extract test /evidence ./test-evidenceSee the qa-team skill Level 4 for complete integration examples (outside-in-testing remains an alias).
Use Cases
Library Development
Test library changes with dependent projects before publishing:
amplifier-shadow create --local ~/repos/my-lib:org/my-lib --name lib-test
amplifier-shadow exec lib-test "
git clone https://github.com/org/dependent-app &&
cd dependent-app &&
pip install git+https://github.com/org/my-lib &&
pytest
"Multi-Repo Coordination
Validate changes across multiple repositories work together:
amplifier-shadow create \
--local ~/repos/core:org/core \
--local ~/repos/cli:org/cli \
--name multi-test
amplifier-shadow exec multi-test "pip install git+https://github.com/org/cli"Pre-Push CI Validation
Run your CI script in shadow before pushing:
amplifier-shadow create --local ~/repos/project:org/project --name ci-check
amplifier-shadow exec ci-check "./scripts/ci.sh"Philosophy Alignment
This skill follows amplihack's core principles:
- Ruthless Simplicity: Minimal abstraction (container + gitea + URL rewriting)
- Modular Design: Self-contained, composable with other testing tools
- Zero-BS Implementation: Every script works completely, no stubs
- Outside-In Thinking: Test what users see, not implementation details
Agent Compatibility
| Agent | Support | Method |
|---|---|---|
| Amplifier | ✅ Native | Built-in shadow tool |
| Claude Code | ✅ Standalone | amplifier-shadow CLI via bash |
| GitHub Copilot | ✅ Standalone | amplifier-shadow CLI via bash |
| Manual/DIY | ✅ Scripts | Shell scripts + Docker Compose |
Architecture
Shadow environments use this architecture:
┌─────────────────────────────────────────────┐
│ Shadow Container │
│ ┌───────────────────────────────────────┐ │
│ │ Gitea (localhost:3000) │ │
│ │ - Your local snapshots │ │
│ └───────────────────────────────────────┘ │
│ │
│ Git URL Rewriting: │
│ github.com/org/my-lib → Gitea (local) │
│ github.com/org/* → Real GitHub │
│ │
│ /workspace (pre-cloned local sources) │
└─────────────────────────────────────────────┘Related Skills
- qa-team - Agentic behavior-driven tests (legacy alias:
outside-in-testing) - test-gap-analyzer - Find untested code paths
- philosophy-guardian - Verify scripts follow ruthless simplicity
Resources
- Amplifier Shadow Bundle: https://github.com/microsoft/amplifier-bundle-shadow
- Skill Documentation:
SKILL.md(this directory) - Shell Scripts:
scripts/(this directory) - Docker Compose Examples:
docker-compose/(this directory)
Version
1.0.0 (2026-01-29)
- Initial skill release
- Complete documentation with progressive disclosure (Levels 1-3)
- Generalizable shell scripts for DIY setup
- Docker Compose examples for all use cases
- Multi-language support (Python, Node, Rust, Go)
- Integration patterns with qa-team / outside-in-testing alias
- Philosophy alignment with ruthless simplicity
Contributing
This skill is part of the amplihack bundle. For issues or improvements:
1. Test scripts work standalone (without Amplifier) 2. Follow philosophy: ruthless simplicity, zero-BS implementation 3. Maintain agent-agnostic approach (works for all coding agents) 4. Update examples and troubleshooting as needed
---
Remember: Shadow environments let you test exactly what's on your machine in a clean, isolated environment that mirrors CI. Use them before every significant push to catch issues early.
#!/bin/bash
# Create git bundle snapshot of working tree
# Usage: ./create-bundle.sh /path/to/repo /output/path/bundle.git
set -e
REPO_PATH="$1"
OUTPUT_PATH="$2"
if [[ -z "$REPO_PATH" || -z "$OUTPUT_PATH" ]]; then
echo "Usage: $0 <repo-path> <output-path>"
echo "Example: $0 ~/repos/my-lib /tmp/my-lib.bundle"
exit 1
fi
if [[ ! -d "$REPO_PATH/.git" ]]; then
echo "Error: $REPO_PATH is not a git repository"
exit 1
fi
echo "Creating git bundle from $REPO_PATH..."
cd "$REPO_PATH"
# Fetch all refs to ensure complete history
echo "Fetching refs from origin..."
git fetch --all --tags --quiet 2>/dev/null || true
# Check for uncommitted changes
if [[ -n $(git status --porcelain) ]]; then
echo "Uncommitted changes detected - creating snapshot commit..."
# Create temp clone and commit changes
TEMP_DIR=$(mktemp -d)
echo "Cloning to temp directory: $TEMP_DIR"
git clone --quiet "$REPO_PATH" "$TEMP_DIR"
# Sync working tree (including deletions)
echo "Syncing working tree..."
rsync -a --delete --exclude='.git' "$REPO_PATH/" "$TEMP_DIR/"
cd "$TEMP_DIR"
git add -A
git commit --allow-empty -m "Shadow snapshot: uncommitted changes" \
--author="Shadow <shadow@localhost>" --quiet
SNAPSHOT_COMMIT=$(git rev-parse HEAD)
echo "Snapshot commit: $SNAPSHOT_COMMIT"
# Create bundle with all refs
echo "Creating bundle..."
git bundle create "$OUTPUT_PATH" --all --quiet
cd /
rm -rf "$TEMP_DIR"
else
echo "No uncommitted changes - bundling clean repository..."
# Get all refs to bundle (local + remote tracking)
REFS=$(git show-ref --heads --tags | awk '{print $2}')
REMOTE_REFS=$(git show-ref | grep 'refs/remotes/' | awk '{print $2}' || true)
if [[ -n "$REFS" || -n "$REMOTE_REFS" ]]; then
# Bundle with explicit refs to include remote tracking refs
git bundle create "$OUTPUT_PATH" $REFS $REMOTE_REFS --quiet 2>/dev/null
else
# Fallback to --all if no refs found
git bundle create "$OUTPUT_PATH" --all --quiet
fi
SNAPSHOT_COMMIT=$(git rev-parse HEAD)
echo "Current commit: $SNAPSHOT_COMMIT"
fi
BUNDLE_SIZE=$(du -h "$OUTPUT_PATH" | cut -f1)
echo "Bundle created successfully: $OUTPUT_PATH ($BUNDLE_SIZE)"
echo "Commit SHA: $SNAPSHOT_COMMIT"
#!/bin/bash
# Start container with Gitea and configure git URL rewriting
# Usage: ./setup-shadow.sh <container-name> <bundle-path> <org> <repo>
set -e
CONTAINER_NAME="$1"
BUNDLE_PATH="$2"
ORG="$3"
REPO="$4"
if [[ -z "$CONTAINER_NAME" || -z "$BUNDLE_PATH" || -z "$ORG" || -z "$REPO" ]]; then
echo "Usage: $0 <container-name> <bundle-path> <org> <repo>"
echo "Example: $0 shadow-test /tmp/my-lib.bundle myorg my-lib"
exit 1
fi
if [[ ! -f "$BUNDLE_PATH" ]]; then
echo "Error: Bundle not found: $BUNDLE_PATH"
exit 1
fi
# Check if container already exists
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
echo "Error: Container $CONTAINER_NAME already exists"
echo "Remove it first: docker rm -f $CONTAINER_NAME"
exit 1
fi
echo "Starting shadow container: $CONTAINER_NAME"
# Start container with bundle mounted
docker run -d \
--name "$CONTAINER_NAME" \
-v "$BUNDLE_PATH:/snapshots/bundle.git:ro" \
-e UV_NO_GITHUB_FAST_PATH=1 \
-e UV_CACHE_DIR=/tmp/uv-cache \
ghcr.io/microsoft/amplifier-shadow:latest
echo "Waiting for Gitea to start..."
MAX_WAIT=60
WAITED=0
until docker exec "$CONTAINER_NAME" curl -sf http://localhost:3000/api/v1/version > /dev/null 2>&1; do
if [[ $WAITED -ge $MAX_WAIT ]]; then
echo "Error: Gitea did not start within ${MAX_WAIT}s"
docker logs "$CONTAINER_NAME" | tail -20
exit 1
fi
sleep 1
WAITED=$((WAITED + 1))
echo -n "."
done
echo " Done!"
echo "Creating organization: $ORG"
docker exec "$CONTAINER_NAME" bash -c "
curl -s -u shadow:shadow \
-H 'Content-Type: application/json' \
-d '{\"username\":\"$ORG\"}' \
http://localhost:3000/api/v1/orgs > /dev/null 2>&1 || true
"
echo "Creating repository: $ORG/$REPO"
docker exec "$CONTAINER_NAME" bash -c "
curl -s -u shadow:shadow \
-H 'Content-Type: application/json' \
-d '{\"name\":\"$REPO\",\"private\":false}' \
http://localhost:3000/api/v1/orgs/$ORG/repos > /dev/null 2>&1
"
echo "Pushing bundle to Gitea..."
docker exec "$CONTAINER_NAME" bash -c "
set -e
cd /tmp
rm -rf _push_repo
mkdir _push_repo && cd _push_repo
git init --bare --quiet
# Parse bundle refs and fetch each one
git bundle list-heads /snapshots/bundle.git | while read sha ref; do
branch_name=\$(echo \"\$ref\" | sed 's|refs/heads/||; s|refs/remotes/origin/|_upstream_|; s|refs/tags/|tags/|')
if echo \"\$ref\" | grep -q \"HEAD\"; then continue; fi
git fetch /snapshots/bundle.git \"\$ref:refs/heads/\$branch_name\" 2>/dev/null || true
done
git remote add origin http://shadow:shadow@localhost:3000/$ORG/$REPO.git
git push origin --all --force 2>&1 | grep -v 'remote:'
git push origin --tags --force 2>&1 | grep -v 'remote:' || true
"
echo "Configuring git URL rewriting..."
docker exec "$CONTAINER_NAME" bash -c "
git config --global user.email 'shadow@localhost'
git config --global user.name 'Shadow'
git config --global init.defaultBranch main
git config --global advice.detachedHead false
# Add URL rewriting patterns with boundary markers
git config --global --add url.'http://shadow:shadow@localhost:3000/$ORG/$REPO.git'.insteadOf 'https://github.com/$ORG/$REPO.git'
git config --global --add url.'http://shadow:shadow@localhost:3000/$ORG/$REPO.git'.insteadOf 'https://github.com/$ORG/$REPO.git/'
git config --global --add url.'http://shadow:shadow@localhost:3000/$ORG/$REPO.git'.insteadOf 'https://github.com/$ORG/$REPO/'
git config --global --add url.'http://shadow:shadow@localhost:3000/$ORG/$REPO.git'.insteadOf 'https://github.com/$ORG/$REPO@'
git config --global --add url.'http://shadow:shadow@localhost:3000/$ORG/$REPO.git'.insteadOf 'git@github.com:$ORG/$REPO.git'
git config --global --add url.'http://shadow:shadow@localhost:3000/$ORG/$REPO.git'.insteadOf 'git+https://github.com/$ORG/$REPO.git'
git config --global --add url.'http://shadow:shadow@localhost:3000/$ORG/$REPO.git'.insteadOf 'git+https://github.com/$ORG/$REPO@'
# Clear uv cache to ensure fresh resolution
rm -rf /home/amplifier/.cache/uv/git-v0 2>/dev/null || true
"
echo "Pre-cloning repository to /workspace..."
docker exec "$CONTAINER_NAME" bash -c "
mkdir -p /workspace/$ORG
git clone http://shadow:shadow@localhost:3000/$ORG/$REPO.git /workspace/$ORG/$REPO --quiet 2>&1
"
echo ""
echo "✓ Shadow container ready: $CONTAINER_NAME"
echo " Local source: $ORG/$REPO"
echo " Pre-cloned at: /workspace/$ORG/$REPO"
echo ""
echo "Test with:"
echo " docker exec $CONTAINER_NAME git clone https://github.com/$ORG/$REPO /tmp/test"
echo ""
echo "Destroy with:"
echo " docker rm -f $CONTAINER_NAME"
#!/bin/bash
# Test that shadow environment is using local sources
# Usage: ./test-shadow.sh <container-name> <org> <repo> <expected-commit>
set -e
CONTAINER_NAME="$1"
ORG="$2"
REPO="$3"
EXPECTED_COMMIT="$4"
if [[ -z "$CONTAINER_NAME" || -z "$ORG" || -z "$REPO" ]]; then
echo "Usage: $0 <container-name> <org> <repo> [expected-commit]"
echo "Example: $0 shadow-test myorg my-lib abc1234"
exit 1
fi
echo "Testing shadow environment: $CONTAINER_NAME"
echo "Expected local source: $ORG/$REPO"
# Check 1: Container is running
echo -n "✓ Checking container is running... "
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
echo "FAIL"
echo " Container $CONTAINER_NAME is not running"
exit 1
fi
echo "OK"
# Check 2: Gitea is accessible
echo -n "✓ Checking Gitea server... "
if ! docker exec "$CONTAINER_NAME" curl -sf http://localhost:3000/api/v1/version > /dev/null 2>&1; then
echo "FAIL"
echo " Gitea not accessible at localhost:3000"
exit 1
fi
echo "OK"
# Check 3: Repository exists in Gitea
echo -n "✓ Checking repository in Gitea... "
if ! docker exec "$CONTAINER_NAME" curl -sf http://shadow:shadow@localhost:3000/api/v1/repos/$ORG/$REPO > /dev/null 2>&1; then
echo "FAIL"
echo " Repository $ORG/$REPO not found in Gitea"
exit 1
fi
echo "OK"
# Check 4: Git URL rewriting is configured
echo -n "✓ Checking git URL rewriting... "
GIT_CONFIG=$(docker exec "$CONTAINER_NAME" git config --global --get-regexp 'url.*insteadOf')
if ! echo "$GIT_CONFIG" | grep -q "github.com/$ORG/$REPO"; then
echo "FAIL"
echo " Git URL rewriting not configured for $ORG/$REPO"
exit 1
fi
echo "OK"
# Check 5: Pre-cloned workspace exists
echo -n "✓ Checking pre-cloned workspace... "
if ! docker exec "$CONTAINER_NAME" test -d "/workspace/$ORG/$REPO/.git"; then
echo "FAIL"
echo " Pre-cloned repo not found at /workspace/$ORG/$REPO"
exit 1
fi
echo "OK"
# Check 6: Clone uses local source
echo -n "✓ Testing git clone uses local source... "
ACTUAL_COMMIT=$(docker exec "$CONTAINER_NAME" bash -c "
rm -rf /tmp/test-clone 2>/dev/null || true
git clone https://github.com/$ORG/$REPO /tmp/test-clone --quiet 2>&1
cd /tmp/test-clone
git rev-parse HEAD
" | tail -1)
if [[ -z "$ACTUAL_COMMIT" ]]; then
echo "FAIL"
echo " Could not clone repository"
exit 1
fi
echo "OK (commit: ${ACTUAL_COMMIT:0:7})"
# Check 7: Commit matches expected (if provided)
if [[ -n "$EXPECTED_COMMIT" ]]; then
echo -n "✓ Verifying commit matches expected... "
if [[ "${ACTUAL_COMMIT:0:7}" != "${EXPECTED_COMMIT:0:7}" ]]; then
echo "FAIL"
echo " Expected: ${EXPECTED_COMMIT:0:7}"
echo " Actual: ${ACTUAL_COMMIT:0:7}"
echo " WARNING: This might indicate local source is NOT being used!"
exit 1
fi
echo "OK"
fi
echo ""
echo "✓ All checks passed!"
echo " Shadow environment is correctly configured"
echo " Local source $ORG/$REPO is being used"