name: PMG E2E Tests on: pull_request: branches: - main push: branches: - main workflow_dispatch: # PRs share a concurrency group (cancel/serialize); Pushes use unique groups to avoid cancellation/queuing concurrency: group: ${{ github.workflow }}-${{ (github.event_name == 'pull_request' && github.ref) || github.run_id }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} permissions: contents: read jobs: e2e-test: name: PMG E2E Tests - All Package Managers runs-on: ${{ matrix.os }} timeout-minutes: 20 strategy: fail-fast: false matrix: os: [ubuntu-latest] defaults: run: shell: bash steps: - name: Checkout Source uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: go-version-file: go.mod cache: true - name: Enable corepack run: corepack enable - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "24" - name: Setup PNPM uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: latest - name: Setup Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: "3.11" - name: Setup uv uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Install Poetry uses: snok/install-poetry@76e04a911780d5b312d89783f7b1cd627778900a - name: Build PMG run: make - name: Add pmg to PATH run: echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH - name: Setup PMG run: pmg setup install - name: Test PMG - Config File is Created run: | test -f $HOME/.config/safedep/pmg/config.yml - name: Test PMG Aliases and Shims are Installed run: | test -f $HOME/.pmg.rc test -d $HOME/.pmg/bin for shim in npm pip pip3 pnpm bun uv yarn poetry npx pnpx; do test -x $HOME/.pmg/bin/$shim || { echo "Missing shim: $shim"; exit 1; } done - name: Test NPM - Single Package & Manifest run: | echo "Testing NPM single package installation..." mkdir npm-test && cd npm-test pmg --proxy-mode=false npm init -y pmg --proxy-mode=false npm install express@5.2.1 pmg --proxy-mode=false npm install lodash@4.17.21 # Verification: npm added packages present and manifest updated test -d node_modules/express test -d node_modules/lodash grep -q '"express"' package.json grep -q '"lodash"' package.json echo "Testing NPM manifest installation..." rm -rf node_modules package-lock.json pmg --proxy-mode=false npm install # Verification: npm lockfile and installed modules exist after manifest install test -f package-lock.json test -d node_modules/express test -d node_modules/lodash cd .. && rm -rf npm-test - name: Test NPM - Proxy Mode run: | echo "Testing NPM with proxy-based interception..." mkdir npm-proxy-test && cd npm-proxy-test pmg npm init -y echo "Testing proxy mode single package installation..." pmg npm install express@5.2.1 pmg npm install lodash@4.17.21 # Verification: packages installed via proxy mode test -d node_modules/express test -d node_modules/lodash grep -q '"express"' package.json grep -q '"lodash"' package.json echo "Testing proxy mode manifest installation..." rm -rf node_modules package-lock.json pmg npm install # Verification: manifest install via proxy mode works test -f package-lock.json test -d node_modules/express test -d node_modules/lodash echo "Testing proxy mode with scoped package..." pmg npm install @types/node@18.0.0 # Verification: scoped package installed via proxy test -d node_modules/@types test -d node_modules/@types/node grep -q '"@types/node"' package.json echo "Testing proxy mode dry-run (should not create files)..." rm -rf node_modules package-lock.json pmg --dry-run npm install # Verification: dry-run doesn't create files even in proxy mode test ! -d node_modules test ! -f package-lock.json cd .. && rm -rf npm-proxy-test - name: Test PyPI - Proxy Mode run: | echo "Testing PyPI package managers with proxy-based interception..." mkdir pypi-proxy-test && cd pypi-proxy-test echo "Setting up Python virtual environment for pip and pip3 tests..." python -m venv venv && source venv/bin/activate python --version pip --version echo "Testing pip single package installation via proxy mode..." pmg pip install requests==2.32.4 pmg pip install numpy==2.3.5 # Verification: packages installed and importable python -c "import requests, numpy; print('pip ok:', requests.__version__, numpy.__version__)" echo "Testing pip manifest installation via proxy mode..." pmg pip freeze > requirements.txt pmg pip uninstall -y requests numpy pmg pip install -r <(grep -v '^PMG:' requirements.txt) python -c "import requests, numpy; print('pip manifest ok:', requests.__version__, numpy.__version__)" deactivate echo "Setting up Python virtual environment for pip3 tests..." python -m venv venv3 && source venv3/bin/activate python --version pip3 --version echo "Testing pip3 single package installation via proxy mode..." pmg pip3 install requests==2.32.4 pmg pip3 install numpy==2.3.5 # Verification: packages installed and importable python -c "import requests, numpy; print('pip3 ok:', requests.__version__, numpy.__version__)" echo "Testing pip3 manifest installation via proxy mode..." pmg pip3 freeze > requirements3.txt pmg pip3 uninstall -y requests numpy pmg pip3 install -r <(grep -v '^PMG:' requirements3.txt) python -c "import requests, numpy; print('pip3 manifest ok:', requests.__version__, numpy.__version__)" deactivate echo "Testing uv add and uv pip install via proxy mode..." mkdir uv-proxy && cd uv-proxy pmg uv init --no-readme pmg uv add requests==2.32.4 pmg uv add numpy==2.3.5 # Verification: pyproject.toml lists expected dependencies test -f pyproject.toml grep -q 'requests' pyproject.toml grep -q 'numpy' pyproject.toml echo "Sync environment and verify installations..." pmg uv sync pmg uv pip show requests >/dev/null pmg uv pip show numpy >/dev/null echo "Testing uv pip install from requirements via proxy mode..." pmg uv pip freeze > requirements.txt pmg uv pip install -r <(grep -v '^PMG:' requirements.txt) pmg uv pip show requests >/dev/null pmg uv pip show numpy >/dev/null cd .. cd .. && rm -rf pypi-proxy-test - name: Test PNPM - Single Package & Manifest run: | echo "Testing PNPM single package installation..." PNPM_TESTDIR=$(mktemp -d) && cd "$PNPM_TESTDIR" pmg --proxy-mode=false pnpm init pmg --proxy-mode=false pnpm add express@5.2.1 pmg --proxy-mode=false pnpm add lodash@4.17.21 # Verification: pnpm packages installed and lockfile created test -d node_modules/express test -d node_modules/lodash test -f pnpm-lock.yaml echo "Testing PNPM manifest installation..." rm -rf node_modules pnpm-lock.yaml pmg --proxy-mode=false pnpm install # Verification: pnpm lockfile and modules exist after manifest install test -f pnpm-lock.yaml test -d node_modules/express test -d node_modules/lodash cd - && rm -rf "$PNPM_TESTDIR" - name: Test Bun - Single Package & Manifest run: | echo "Testing Bun single package installation..." mkdir bun-test && cd bun-test pmg --proxy-mode=false bun init -y pmg --proxy-mode=false bun add express@5.2.1 pmg --proxy-mode=false bun add lodash@4.17.21 # Verification: bun packages installed and lockfile created test -d node_modules/express test -d node_modules/lodash test -f bun.lock echo "Testing Bun manifest installation..." rm -rf node_modules bun.lock pmg --proxy-mode=false bun install # Verification: bun lockfile and modules exist after manifest install test -f bun.lock test -d node_modules/express test -d node_modules/lodash echo "Testing Bun frozen manifest installation with bun ci..." rm -rf node_modules pmg --proxy-mode=false bun ci # Verification: bun lockfile and modules exist after frozen manifest install test -f bun.lock test -d node_modules/express test -d node_modules/lodash cd .. && rm -rf bun-test - name: Test Yarn - Single Package & Manifest run: | echo "Testing Yarn single package installation..." export YARN_ENABLE_HARDENED_MODE=0 npm install -g yarn@1.22.22 yarn --version YARN_TESTDIR=$(mktemp -d) && cd "$YARN_TESTDIR" pmg --proxy-mode=false yarn init -y pmg --proxy-mode=false yarn add express@5.2.1 pmg --proxy-mode=false yarn add lodash@4.17.21 # Verification: yarn packages installed and lockfile created test -d node_modules/express test -d node_modules/lodash test -f yarn.lock echo "Testing Yarn manifest installation..." rm -rf node_modules yarn.lock pmg --proxy-mode=false yarn install # Verification: yarn lockfile and modules exist after manifest install test -f yarn.lock test -d node_modules/express test -d node_modules/lodash cd - && rm -rf "$YARN_TESTDIR" - name: Test NPX - Package Execution run: | echo "Testing NPX package execution..." mkdir npx-test && cd npx-test echo "Testing npx with a simple package..." pmg --proxy-mode=false npx cowsay@1.6.0 "Hello from pmg npx" | tee npx-output.txt # Verification: cowsay output contains our message grep -q "Hello from pmg npx" npx-output.txt echo "Testing npx with --package flag..." pmg --proxy-mode=false npx --package cowsay@1.6.0 -- cowsay "Hello with package flag" | tee npx-pkg-output.txt # Verification: package flag execution produces expected output grep -q "Hello with package flag" npx-pkg-output.txt echo "Testing npx dry-run mode..." pmg --proxy-mode=false --dry-run npx cowsay@1.6.0 "This should not execute" | tee npx-dry-output.txt # Verification: dry-run should NOT produce cowsay ASCII art (cow face ^__^ should not appear) ! grep -q '\^__\^' npx-dry-output.txt cd .. && rm -rf npx-test - name: Test PNPX - Package Execution run: | echo "Testing PNPX package execution..." PNPX_TESTDIR=$(mktemp -d) && cd "$PNPX_TESTDIR" echo "Testing pnpx with a simple package..." pmg --proxy-mode=false pnpx cowsay@1.6.0 "Hello from pmg pnpx" | tee pnpx-output.txt # Verification: cowsay output contains our message grep -q "Hello from pmg pnpx" pnpx-output.txt echo "Testing pnpx with --package flag..." pmg --proxy-mode=false pnpx --package cowsay@1.6.0 -- cowsay "Hello with package flag" | tee pnpx-pkg-output.txt # Verification: package flag execution produces expected output grep -q "Hello with package flag" pnpx-pkg-output.txt echo "Testing pnpx dry-run mode..." pmg --proxy-mode=false --dry-run pnpx cowsay@1.6.0 "This should not execute" | tee pnpx-dry-output.txt # Verification: dry-run should NOT produce cowsay ASCII art (cow face ^__^ should not appear) ! grep -q '\^__\^' pnpx-dry-output.txt cd - && rm -rf "$PNPX_TESTDIR" - name: Test Pip - Single Package & Manifest run: | echo "Testing Pip single package installation..." mkdir pip-test && cd pip-test python -m venv venv && source venv/bin/activate pmg --proxy-mode=false pip install requests==2.32.4 pmg --proxy-mode=false pip install numpy==2.3.5 pmg --proxy-mode=false pip freeze > requirements.txt # Verification: requirements.txt contains expected packages test -s requirements.txt grep -E '^requests==' requirements.txt grep -E '^numpy==' requirements.txt echo "Testing Pip manifest installation..." pmg --proxy-mode=false pip uninstall -y requests numpy pmg --proxy-mode=false pip install -r requirements.txt # Verification: imported packages are available in the environment python -c "import requests, numpy; print(requests.__version__); print(numpy.__version__)" deactivate cd .. && rm -rf pip-test - name: Test Pip3 - Single Package & Manifest run: | echo "Testing Pip3 single package installation..." mkdir pip3-test && cd pip3-test python -m venv venv && source venv/bin/activate pmg --proxy-mode=false pip3 install requests==2.32.4 pmg --proxy-mode=false pip3 install numpy==2.3.5 pmg --proxy-mode=false pip3 freeze > requirements.txt # Verification: requirements.txt contains expected packages test -s requirements.txt grep -E '^requests==' requirements.txt grep -E '^numpy==' requirements.txt echo "Testing Pip3 manifest installation..." pmg --proxy-mode=false pip3 uninstall -y requests numpy pmg --proxy-mode=false pip3 install -r requirements.txt # Verification: imported packages are available in the environment python -c "import requests, numpy; print(requests.__version__); print(numpy.__version__)" deactivate cd .. && rm -rf pip3-test - name: Test UV - Single Package & Manifest run: | echo "Testing UV single package installation..." mkdir uv-test && cd uv-test pmg --proxy-mode=false uv init --no-readme pmg --proxy-mode=false uv add requests==2.32.4 pmg --proxy-mode=false uv add numpy==2.3.5 # Verification: pyproject.toml lists expected dependencies test -f pyproject.toml grep -q 'requests' pyproject.toml grep -q 'numpy' pyproject.toml echo "Testing UV manifest installation..." rm -rf .venv uv.lock pmg --proxy-mode=false uv sync # Verification: uv lockfile and virtualenv created; packages present test -d .venv test -f uv.lock pmg --proxy-mode=false uv pip show requests >/dev/null pmg --proxy-mode=false uv pip show numpy >/dev/null echo "Testing UV pip commands..." pmg --proxy-mode=false uv pip freeze > requirements.txt pmg --proxy-mode=false uv pip install -r requirements.txt pmg --proxy-mode=false uv pip sync requirements.txt # Verification: uv pip can show installed packages after requirements sync pmg --proxy-mode=false uv pip show requests >/dev/null pmg --proxy-mode=false uv pip show numpy >/dev/null cd .. && rm -rf uv-test - name: Test Poetry - Single Package & Manifest run: | echo "Testing Poetry single package installation..." mkdir poetry-test && cd poetry-test pmg --proxy-mode=false poetry init --name poetry-test --no-interaction --quiet pmg --proxy-mode=false poetry add requests==2.32.4 pmg --proxy-mode=false poetry add numpy==2.3.5 # Verification: pyproject.toml dependencies updated test -f pyproject.toml grep -q 'requests' pyproject.toml grep -q 'numpy' pyproject.toml echo "Testing Poetry manifest installation..." rm -rf .venv poetry.lock pmg --proxy-mode=false poetry install --no-root cd .. && rm -rf poetry-test - name: Test Malicious Package Detection run: | echo "Testing malicious package detection..." mkdir malicious-test && cd malicious-test pmg --proxy-mode=false npm init -y ! pmg --proxy-mode=false npm install nyc-config@10.0.0 || echo "Malicious package correctly blocked" cd .. && rm -rf malicious-test - name: Test safedep-test-pkg is Blocked using Proxy mode run: | echo "Testing that safedep-test-pkg is blocked..." mkdir safedep-test-pkg-test && cd safedep-test-pkg-test pmg npm init -y # Attempt to install safedep-test-pkg - should fail if pmg npm --no-cache --prefer-online i safedep-test-pkg@0.1.3; then echo "ERROR: safedep-test-pkg was not blocked!" exit 1 else echo "SUCCESS: safedep-test-pkg correctly blocked" fi # Verify package is not installed locally if [ -d "node_modules/safedep-test-pkg" ]; then echo "ERROR: safedep-test-pkg found in node_modules!" exit 1 else echo "SUCCESS: safedep-test-pkg not present in node_modules" fi cd .. && rm -rf safedep-test-pkg-test - name: Test PMG Modes run: | echo "Testing different PMG modes..." mkdir pmg-modes-test && cd pmg-modes-test pmg npm init -y # Mode: --dry-run should not create node_modules or lockfiles pmg --proxy-mode=false --dry-run npm install express # Verification: no files created during dry-run test ! -d node_modules test ! -f package-lock.json # Mode: --silent should install without noisy output pmg --proxy-mode=false --silent npm install express # Verification: package installed test -d node_modules/express # Clean and test --verbose installation rm -rf node_modules package-lock.json pmg --proxy-mode=false --verbose npm install express # Verification: package installed test -d node_modules/express # Clean and test --debug with log output rm -rf node_modules package-lock.json pmg --proxy-mode=false --debug --log debug.json npm install express # Verification: debug log written test -f debug.json # Mode: --paranoid may require cloud credentials; run non-blocking with dry-run pmg --proxy-mode=false --paranoid --dry-run npm install express || true cd .. && rm -rf pmg-modes-test sandbox-e2e-macos: name: Sandbox E2E - macOS runs-on: macos-latest timeout-minutes: 10 steps: - name: Checkout Source uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: go-version-file: go.mod cache: true - name: Enable corepack run: corepack enable - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "24" - name: Setup PNPM uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - name: Build PMG run: make - name: Add pmg to PATH run: echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH - name: Setup PMG run: pmg setup install - name: Create Test Directories for Sandbox Permissions Tests run: mkdir -p ~/.aws ~/.gcloud ~/.kube ~/.ssh - name: Create Test Files for Sandbox Permissions Tests run: | touch ~/.aws/credentials touch ~/.gcloud/credentials.json touch ~/.kube/config touch ~/.ssh/id_rsa touch ./.env - name: Run Sandbox E2E Test run: pmg --sandbox --sandbox-enforce npm exec -- node test/sandbox-e2e.js - name: Run Package Manager E2E Test run: pmg --sandbox --sandbox-enforce npm exec -- node test/pm-e2e.js sandbox-e2e-linux: name: Sandbox E2E - Linux (Bubblewrap) runs-on: ubuntu-latest timeout-minutes: 10 defaults: run: shell: bash env: PMG_SANDBOX_DRIVER: bubblewrap steps: - name: Checkout Source uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: go-version-file: go.mod cache: true - name: Enable corepack run: corepack enable - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "24" - name: Setup PNPM uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - name: Install Bubblewrap run: sudo apt-get update && sudo apt-get install -y bubblewrap - name: Verify Bubblewrap Installation run: bwrap --version - name: Build PMG run: make - name: Add pmg to PATH run: echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH - name: Setup PMG run: pmg setup install - name: Create Test Directories for Sandbox Permissions Tests run: mkdir -p ~/.aws ~/.gcloud ~/.kube ~/.ssh ~/.gnupg ~/.docker - name: Create Test Files for Sandbox Permissions Tests run: | touch ~/.aws/credentials touch ~/.gcloud/credentials.json touch ~/.kube/config touch ~/.ssh/id_rsa touch ~/.gnupg/pubring.kbx touch ~/.docker/config.json touch ./.env - name: Disable AppArmor for Bubblewrap run: | sudo systemctl stop apparmor sudo systemctl disable apparmor sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - name: Run Sandbox E2E Test run: pmg --sandbox --sandbox-enforce --sandbox-profile npm-restrictive npm exec -- node test/sandbox-e2e.js - name: Run Package Manager E2E Test run: pmg --sandbox --sandbox-enforce --sandbox-profile npm-restrictive npm exec -- node test/pm-e2e.js sandbox-e2e-linux-landlock: name: Sandbox E2E - Linux (Landlock) runs-on: ubuntu-latest timeout-minutes: 10 defaults: run: shell: bash env: PMG_SANDBOX_DRIVER: landlock PMG_LANDLOCK_E2E: "1" # Redirect npm's cache into /tmp so it sits outside any pre-existing # state in /home/runner/.npm (which setup-node / the runner image may # have populated with state the sandbox policy doesn't account for). # The npm-restrictive profile already grants /tmp/** read+write. npm_config_cache: /tmp/npm-cache steps: - name: Checkout Source uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: go-version-file: go.mod cache: true - name: Enable corepack run: corepack enable - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "24" - name: Setup PNPM uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - name: Build PMG run: make - name: Add pmg to PATH run: echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH - name: Setup PMG run: pmg setup install - name: Create Test Directories for Sandbox Permissions Tests run: mkdir -p ~/.aws ~/.gcloud ~/.kube ~/.ssh ~/.gnupg ~/.docker - name: Create Test Files for Sandbox Permissions Tests run: | touch ~/.aws/credentials touch ~/.gcloud/credentials.json touch ~/.kube/config touch ~/.ssh/id_rsa touch ~/.gnupg/pubring.kbx touch ~/.docker/config.json touch ./.env - name: Disable AppArmor for User Namespaces run: | sudo systemctl stop apparmor sudo systemctl disable apparmor sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - name: Verify Landlock Available run: | if [ ! -d /sys/kernel/security/landlock ] && ! grep -q landlock /proc/kallsyms 2>/dev/null; then echo "Landlock not detected by sysfs probe (continuing — driver will fail loudly if unavailable)" fi uname -a - name: Run Landlock Helper E2E Tests (Go) run: go test -count=1 -v -run TestLandlockHelper ./sandbox/platform/... - name: Run Sandbox E2E Test run: pmg --sandbox --sandbox-enforce --sandbox-profile npm-restrictive npm exec -- node test/sandbox-e2e.js - name: Run Package Manager E2E Test run: pmg --sandbox --sandbox-enforce --sandbox-profile npm-restrictive npm exec -- node test/pm-e2e.js