Loading...
Loading...
Creates isolated container environments for testing local uncommitted changes before pushing. Use when testing library changes, multi-repo coordination, or validating "works on my machine" → "works in CI". Provides git bundle snapshots, embedded git server, selective URL rewriting, and package manager cache isolation. Works with any coding agent via standalone CLI, shell scripts, or Docker Compose.
npx skill4agent add rysweet/amplihack shadow-testinginsteadOf┌─────────────────────────────────────────────────────────┐
│ 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) │
└─────────────────────────────────────────────────────────┘~/repos/my-lib:myorg/my-libmyorg/my-libinsteadOf[url "http://shadow:shadow@localhost:3000/myorg/my-lib.git"]
insteadOf = https://github.com/myorg/my-lib.gitgit clone https://github.com/myorg/my-lib# Shadow tool is built-in - no installation needed
amplifier run --bundle amplihack# Install via uvx (recommended)
uvx amplifier-shadow --version
# Or via pip
pip install amplifier-bundle-shadow
# Verify installation
amplifier-shadow --version# 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-lib# 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)# 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")# 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 test# 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"# 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
"# 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"# 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"# 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 succeed# 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!/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"# 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", ...]# 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"amplifier-shadow exec xxx "
cd /workspace &&
uv venv &&
. .venv/bin/activate &&
uv pip install ...
"amplifier-shadow build$HOME/tmpamplifier-shadow exec xxx "cd $HOME && git clone ..."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/docker build -t my-shadow:latest .
amplifier-shadow create --image my-shadow:latest --local ~/repos/lib:org/libscripts/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"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"# 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/single-repo.ymlversion: '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
"docker-compose/multi-repo.ymlversion: '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
"# 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 downdocker-compose/ci-shadow.ymlversion: '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/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/repo# 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-evidenceoutside-in-testing# 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 @"/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"/tmp/uv-cache/tmp/pip-cache/tmp/npm-cache/tmp/cargo-home/tmp/go-mod-cache# 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=value# Always destroy shadows when done
amplifier-shadow destroy shadow-xxx
# Or destroy all
amplifier-shadow destroy-all# ✅ 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)# test-scenario.yaml
scenario:
name: "Library Integration Test"
type: cli
steps:
- action: launch
target: "/workspace/org/lib/cli.py"
- action: verify_output
contains: "Success"amplifier-shadow create --local ~/repos/lib:org/lib --name test
amplifier-shadow exec test "gadugi-agentic-test run test-scenario.yaml"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-reportamplifier-shadow create --local ~/repos/pkg:org/pkg --name npm-test
amplifier-shadow exec npm-test "
cd /workspace/org/pkg &&
npm install &&
npm 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
"# 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_ID# 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-results/workspace/{org}/{repo}envgit config --listcurl http://localhost:3000/api/v1/versionamplifier-shadow status