mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
Doctor now verifies every installed package manager against the shim directory, and system-install validation only requires a safe parent directory. Cloud sync records username/uid on invocation context for multi-user hosts sharing one endpoint. Co-authored-by: Cursor <cursoragent@cursor.com>
999 lines
37 KiB
YAML
999 lines
37 KiB
YAML
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 uvx 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 PyPI - httpx NO_PROXY Regression (#339)
|
|
run: |
|
|
echo "Testing that proxy-mode NO_PROXY does not crash Python httpx (#339)..."
|
|
HTTPX_TESTDIR=$(mktemp -d) && cd "$HTTPX_TESTDIR"
|
|
pmg uv init --no-readme
|
|
pmg uv add httpx==0.28.1
|
|
|
|
# Run under proxy mode (default). PMG injects NO_PROXY into the child env;
|
|
# the bracketed [::1] it used to inject made urllib/httpx raise
|
|
# "InvalidURL: Invalid port: ':1]'" when constructing a client.
|
|
pmg uv run python -c '
|
|
import os, httpx
|
|
np = os.environ.get("NO_PROXY") or os.environ.get("no_proxy") or ""
|
|
assert "::1" in np, "proxy-mode NO_PROXY not set, test is not exercising the fix: %r" % np
|
|
assert "[::1]" not in np, "NO_PROXY still contains bracketed loopback: %r" % np
|
|
httpx.Client().close()
|
|
print("httpx ok; NO_PROXY=" + np)
|
|
'
|
|
|
|
cd - && rm -rf "$HTTPX_TESTDIR"
|
|
|
|
- name: Test Go Modules - Proxy Mode (Experimental)
|
|
run: |
|
|
echo "Testing experimental Go module support via proxy mode..."
|
|
GO_TESTDIR=$(mktemp -d) && cd "$GO_TESTDIR"
|
|
go mod init example.com/pmg-go-e2e
|
|
cat > main.go <<'EOF'
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func main() { fmt.Println(uuid.NewString()) }
|
|
EOF
|
|
|
|
echo "Testing pmg go get with a pinned version..."
|
|
pmg go get github.com/google/uuid@v1.6.0
|
|
|
|
# Verification: module resolved and checksummed through the proxy
|
|
grep -q 'github.com/google/uuid v1.6.0' go.mod
|
|
grep -q 'github.com/google/uuid v1.6.0' go.sum
|
|
|
|
echo "Testing pmg go mod tidy..."
|
|
rm go.sum
|
|
pmg go mod tidy
|
|
|
|
# Verification: go.sum regenerated via proxied module fetches
|
|
grep -q 'github.com/google/uuid v1.6.0' go.sum
|
|
|
|
echo "Testing pmg go run..."
|
|
pmg go run . | grep -Eq '^[0-9a-f-]{36}$'
|
|
|
|
cd - && rm -rf "$GO_TESTDIR"
|
|
|
|
# Regression: PMG injects HTTP(S)_PROXY into its child, and `go test`
|
|
# inherits it. The hermetic proxye2e harness must not route its
|
|
# in-process proxy traffic through the outer PMG proxy.
|
|
- name: Test Go - Hermetic Proxy Tests Under PMG
|
|
run: pmg go test -count=1 -run 'TestProxyFlow_Go' ./test/proxye2e/
|
|
|
|
- name: Test PNPM - Single Package & Manifest
|
|
run: |
|
|
echo "Testing PNPM single package installation..."
|
|
PNPM_TESTDIR=$(mktemp -d) && cd "$PNPM_TESTDIR"
|
|
# Avoid `pnpm init`: it writes devEngines.packageManager with
|
|
# onFail:download, and the following `pnpm add` then crashes with
|
|
# "Cannot use 'in' operator to search for 'integrity' in undefined".
|
|
npm init -y
|
|
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
|
|
|
|
# Runs before the classic-yarn step so corepack's Berry shim stays the `yarn`
|
|
# on PATH (the later `npm install -g yarn@1.22.22` would shadow it).
|
|
- name: Test safedep-test-pkg is Blocked using Yarn (Berry) Proxy mode
|
|
env:
|
|
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0"
|
|
YARN_NODE_LINKER: node-modules
|
|
YARN_ENABLE_HARDENED_MODE: "0"
|
|
# CI=true would enable immutable installs and fail before the proxy can block.
|
|
YARN_ENABLE_IMMUTABLE_INSTALLS: "false"
|
|
run: |
|
|
echo "Testing that safedep-test-pkg is blocked with Yarn Berry in proxy mode..."
|
|
YARN_BERRY_TESTDIR=$(mktemp -d) && cd "$YARN_BERRY_TESTDIR"
|
|
npm init -y
|
|
|
|
# Pin Yarn Berry without running an install (corepack use would trigger one).
|
|
npm pkg set packageManager=yarn@4.16.0
|
|
yarn --version
|
|
|
|
# A non-zero exit alone is not enough — yarn could fail for unrelated
|
|
# reasons (setup, network, CA trust) and falsely pass. Require PMG's
|
|
# malware block signal in the output.
|
|
if output=$(pmg yarn add safedep-test-pkg@0.1.3 2>&1); then
|
|
echo "$output"
|
|
echo "ERROR: safedep-test-pkg was not blocked with Yarn Berry!"
|
|
exit 1
|
|
fi
|
|
echo "$output"
|
|
if ! echo "$output" | grep -q "Malicious package blocked"; then
|
|
echo "ERROR: yarn add failed, but not due to a PMG malware block"
|
|
exit 1
|
|
fi
|
|
echo "SUCCESS: safedep-test-pkg correctly blocked with Yarn Berry"
|
|
|
|
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 "$YARN_BERRY_TESTDIR"
|
|
|
|
- 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 UVX - Package Execution
|
|
run: |
|
|
echo "Testing UVX package execution (uv tool run)..."
|
|
UVX_TESTDIR=$(mktemp -d) && cd "$UVX_TESTDIR"
|
|
|
|
echo "Testing uvx with a simple tool in proxy mode (default)..."
|
|
pmg uvx ruff@0.6.9 --version | tee uvx-output.txt
|
|
# Verification: pinned version is resolved and executed via the proxy
|
|
grep -q "0.6.9" uvx-output.txt
|
|
|
|
echo "Testing uvx version pin via @ syntax (non-proxy)..."
|
|
pmg --proxy-mode=false uvx ruff@0.6.9 --version | tee uvx-pin-output.txt
|
|
grep -q "0.6.9" uvx-pin-output.txt
|
|
|
|
echo "Testing uvx with --from (command name differs from package)..."
|
|
pmg --proxy-mode=false uvx --from cowsay cowsay -t "Hello from pmg uvx" | tee uvx-from-output.txt
|
|
# Verification: cowsay output contains our message and cow art
|
|
grep -q "Hello from pmg uvx" uvx-from-output.txt
|
|
grep -q '\^__\^' uvx-from-output.txt
|
|
|
|
echo "Testing uvx dry-run mode..."
|
|
pmg --proxy-mode=false --dry-run uvx --from cowsay cowsay -t "This should not execute" | tee uvx-dry-output.txt
|
|
# Verification: dry-run should NOT produce cowsay ASCII art (cow face ^__^)
|
|
! grep -q '\^__\^' uvx-dry-output.txt
|
|
|
|
cd - && rm -rf "$UVX_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
|
|
env:
|
|
E2E_ENV_SEEDED: "1"
|
|
GITHUB_TOKEN: pmg-e2e-canary
|
|
gh_token: pmg-e2e-canary
|
|
AWS_SECRET_ACCESS_KEY: pmg-e2e-canary
|
|
OP_SERVICE_ACCOUNT_TOKEN: pmg-e2e-canary
|
|
CLOUDFLARE_API_TOKEN: pmg-e2e-canary
|
|
TWINE_PASSWORD: pmg-e2e-canary
|
|
NPM_TOKEN: pmg-e2e-keep
|
|
NODE_AUTH_TOKEN: pmg-e2e-keep
|
|
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
|
|
env:
|
|
E2E_ENV_SEEDED: "1"
|
|
GITHUB_TOKEN: pmg-e2e-canary
|
|
gh_token: pmg-e2e-canary
|
|
AWS_SECRET_ACCESS_KEY: pmg-e2e-canary
|
|
OP_SERVICE_ACCOUNT_TOKEN: pmg-e2e-canary
|
|
CLOUDFLARE_API_TOKEN: pmg-e2e-canary
|
|
TWINE_PASSWORD: pmg-e2e-canary
|
|
NPM_TOKEN: pmg-e2e-keep
|
|
NODE_AUTH_TOKEN: pmg-e2e-keep
|
|
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-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/...
|
|
|
|
# The npm leaf profile's npm_config_* env allow is what keeps the
|
|
# job-level npm_config_cache redirect alive.
|
|
- name: Run Sandbox E2E Test
|
|
env:
|
|
E2E_ENV_SEEDED: "1"
|
|
GITHUB_TOKEN: pmg-e2e-canary
|
|
gh_token: pmg-e2e-canary
|
|
AWS_SECRET_ACCESS_KEY: pmg-e2e-canary
|
|
OP_SERVICE_ACCOUNT_TOKEN: pmg-e2e-canary
|
|
CLOUDFLARE_API_TOKEN: pmg-e2e-canary
|
|
TWINE_PASSWORD: pmg-e2e-canary
|
|
NPM_TOKEN: pmg-e2e-keep
|
|
NODE_AUTH_TOKEN: pmg-e2e-keep
|
|
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
|
|
|
|
# Linux system-wide install: root install, ENV PATH (Docker-style), non-root user,
|
|
# managed config, and remove. Profile.d login shells are covered by sourcing the snippet.
|
|
e2e-system-install:
|
|
name: PMG E2E - System Install (Linux)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
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: Setup Node.js
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: "24"
|
|
|
|
- name: Build PMG
|
|
run: make
|
|
|
|
- name: Reject private PMG binary for system install
|
|
run: |
|
|
sudo mkdir -p /root/pmg-private
|
|
sudo cp bin/pmg /root/pmg-private/pmg
|
|
sudo chmod 700 /root/pmg-private /root/pmg-private/pmg
|
|
if sudo /root/pmg-private/pmg setup install --system; then
|
|
echo "ERROR: system install accepted a non-world-executable binary"
|
|
exit 1
|
|
fi
|
|
echo "SUCCESS: private binary rejected"
|
|
|
|
- name: Reject user-owned PMG binary for system install
|
|
run: |
|
|
mkdir -p "$HOME/pmg-user-writable"
|
|
cp bin/pmg "$HOME/pmg-user-writable/pmg"
|
|
chmod 755 "$HOME/pmg-user-writable/pmg"
|
|
if sudo "$HOME/pmg-user-writable/pmg" setup install --system; then
|
|
echo "ERROR: system install accepted a user-owned binary"
|
|
exit 1
|
|
fi
|
|
echo "SUCCESS: user-owned binary rejected"
|
|
|
|
- name: Install PMG system-wide
|
|
run: |
|
|
sudo install -m 755 bin/pmg /usr/local/bin/pmg
|
|
sudo pmg setup install --system
|
|
|
|
- name: Verify system install artifacts
|
|
run: |
|
|
test -f /etc/safedep/pmg/config.yml
|
|
test -f /etc/profile.d/pmg.sh
|
|
grep -q '/usr/local/lib/pmg/bin' /etc/profile.d/pmg.sh
|
|
for shim in npm pip pip3 pipx pnpm bun uv uvx yarn poetry npx pnpx; do
|
|
test -x "/usr/local/lib/pmg/bin/$shim" || { echo "Missing shim: $shim"; exit 1; }
|
|
done
|
|
|
|
- name: PATH and profile.d activate shims
|
|
run: |
|
|
# Docker-style: non-login shells need PATH (or source profile.d)
|
|
export PATH="/usr/local/lib/pmg/bin:$PATH"
|
|
which npm | grep -q '/usr/local/lib/pmg/bin/npm'
|
|
source /etc/profile.d/pmg.sh
|
|
which npm | grep -q '/usr/local/lib/pmg/bin/npm'
|
|
|
|
- name: Managed config refuses CLI mutation
|
|
run: |
|
|
if pmg config set dependency_cooldown.days 7; then
|
|
echo "ERROR: config set should fail under system config"
|
|
exit 1
|
|
fi
|
|
if sudo pmg config set dependency_cooldown.days 7; then
|
|
echo "ERROR: config set should fail under system config even as root"
|
|
exit 1
|
|
fi
|
|
echo "SUCCESS: managed config is locked"
|
|
|
|
- name: Doctor reports system install state
|
|
run: |
|
|
export PATH="/usr/local/lib/pmg/bin:$PATH"
|
|
out=$(pmg setup doctor 2>&1 || true)
|
|
echo "$out"
|
|
echo "$out" | grep -q 'No aliases (system install)'
|
|
echo "$out" | grep -Eq 'Package managers resolve to System shim directory|System shim directory is in PATH'
|
|
|
|
- name: Non-root user interception via system shims
|
|
run: |
|
|
sudo useradd -m pmgtest || true
|
|
# Pass runner PATH so setup-node's npm remains visible after FilterPMGFromPath.
|
|
sudo -u pmgtest env "PATH=/usr/local/lib/pmg/bin:$PATH" HOME=/home/pmgtest bash -lc '
|
|
set -euo pipefail
|
|
which npm | grep -q /usr/local/lib/pmg/bin/npm
|
|
mkdir -p "$HOME/sys-e2e" && cd "$HOME/sys-e2e"
|
|
npm init -y
|
|
if npm install --no-cache --prefer-online safedep-test-pkg@0.1.3; then
|
|
echo "ERROR: safedep-test-pkg was not blocked for non-root user"
|
|
exit 1
|
|
fi
|
|
if [ -d node_modules/safedep-test-pkg ]; then
|
|
echo "ERROR: safedep-test-pkg present in node_modules"
|
|
exit 1
|
|
fi
|
|
echo "SUCCESS: non-root user blocked malicious package via system shims"
|
|
'
|
|
|
|
- name: Remove system install
|
|
run: |
|
|
sudo pmg setup remove --system --config-file
|
|
test ! -e /etc/profile.d/pmg.sh
|
|
test ! -e /etc/safedep/pmg/config.yml
|
|
test ! -d /usr/local/lib/pmg/bin
|
|
echo "SUCCESS: system install removed"
|