Files
pmg/.github/workflows/pmg-e2e.yml
T
dc0e3202cc feat: Linux system-wide setup (pmg setup install --system) (#377)
* feat: add Linux system-wide setup

Install shared shims, managed configuration, and login-shell PATH integration so golden images and multi-user hosts can protect package installs for every user.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: keep local design documents untracked

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: harden and simplify Linux system install

Tighten shim detection, profile repair, and install ordering while
trimming over-specific doctor/info hints from the system-install path.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: clarify system-install doctor alias and shim path checks

Use UserBinDir for PATH checks and pass aliases as not required under
system install without treating that as active interception.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: tighten event-log soft-fail warning prefix

Prefix the warning with [pmg] and drop the redundant continuing clause.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: add Linux system-install e2e and pin pnpm for add flake

Cover root system setup, PATH/profile.d, managed config, non-root
interception, and remove. Pin pnpm 11.10.0 on the package-manager e2e
job after an integrity crash on pnpm add.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: bump packageManager to pnpm 11.10.0 for e2e

Align package.json with the pnpm version we want in CI so action-setup
stops erroring on a version mismatch after the e2e integrity flake.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: use npm init for pnpm e2e to avoid integrity crash

pnpm 11.x `pnpm init` still writes onFail:download; `pnpm add` then
fails after PMG analysis even on 11.10.0. Seed the temp package with
npm init instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: revert packageManager pin to pnpm 11.1.3

The e2e integrity crash is avoided by npm init; the 11.10.0 bump is
no longer needed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: harden system-install review findings

Require root-owned, non-group/other-writable pmg for --system install;
allow remove without that validation. Doctor checks npm resolution for
PATH precedence, uses ImpliesInterception instead of message matching,
and documents version-manager shadowing. Pass profile bin dir from the
shim manager and note that system config ignores per-user files.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: harden doctor PATH checks and attribute cloud events by OS user

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>

* fix: address system-install review findings

- shim: make system executable resolution injectable so tests pass under
  umask 002; skip the root-owner test when running as root
- doctor: treat resolution into either the system or per-user shim dir as
  intercepted, and collapse the shim-in-PATH check to a single call site
- setup: make remove (both --system and per-user) best-effort with
  errors.Join so one failed step no longer strands the other artifact
- shim: allow a group-writable install parent dir (Debian/Ubuntu ship
  /usr/local/bin as root:staff 2775) while still rejecting world-writable
  and non-root-owned parents
- audit: attribute cloud events to SUDO_USER when running under sudo
- docs: drop the soft-fail event-logging claim (hard-fail is retained)

* ci: normalize /usr/local/bin perms before system-install e2e

The GitHub ubuntu-latest runner ships /usr/local/bin world-writable so
tooling can install without sudo. System install correctly refuses a
world-writable dir for the shared binary (any local user could replace
it and hijack every user's npm/pip). No FHS-compliant distro or Docker
image ships it world-writable — it is always root:root 0755 or
root:staff 2775 — so this normalizes only the anomalous CI runner back
to standard perms and still exercises the real /usr/local/bin path.

* fix: actionable remedy for root-created per-user config dir

A pmg run as root with a preserved HOME (GitHub runners, sudo -E, su
without -) creates the invoking user's ~/.config/safedep as root-owned,
and event-log init then fail-closes every later non-root command.
Make that state self-solvable:

- event-log init permission errors exit with a usefulerror naming the
  likely cause and the chown fix instead of a bare fatal
- pmg setup doctor probes event-log dir writability and reports the
  same fix via a new per-result Fix override
- document the mechanism and remedy in system-install.md, along with
  the binary ownership requirements for --system
- consolidate this branch's doctor tests into doctor_test.go

* fix: resolve per-user paths from root's own home when running as root

Path resolution trusted HOME (and XDG_*), which sudo and su can preserve
from the invoking user (GitHub runners, sudo -E, su without -). Any pmg
run as root then created root-owned ~/.config/safedep inside that user's
home, and event-log init fail-closed every later non-root pmg/npm/pip
run for them. System install made sudo pmg the documented flow, turning
this latent bug into the happy path.

When euid is 0, configDir and cacheDir now resolve from root's passwd
home instead of the environment, so root state lands under /root and
user homes are never touched. PMG_CONFIG_DIR/PMG_CACHE_DIR still win,
non-root resolution is unchanged, and Windows is unaffected (no euid).
Event-log init stays fatal on failure; sudo-run package events are
attributed via SUDO_USER and synced by the exit auto-sync as usual.

E2E: GitHub runners preserve HOME under sudo, so assert that no sudo
pmg run leaks state into the runner's home, and that the managed-config
refusal fails for the documented reason rather than a permission brick.

* fix: triage the unwritable config dir remedy by cause

The chown hint is only correct when another account created files
inside the current user's own home. When a leaked HOME or
XDG_CONFIG_HOME points at another user's home (e.g. sudo -u on GitHub
runners), following it would chown that user's directory and brick
their pmg instead. Classify the failure against the passwd home,
which the leaked environment cannot influence, and prescribe:

- dir inside own home: restore ownership with chown
- dir outside own home: fix the leaked environment, never chown
- explicit PMG_CONFIG_DIR: make it writable

Used by both the fatal event-log error and the doctor check, and the
docs troubleshooting now carries the same two-case triage.

* ci: pin XDG_CONFIG_HOME for the cross-user e2e step; terse doctor fix

GitHub runners export XDG_CONFIG_HOME=/home/runner/.config and it leaks
through sudo -u, so the pmgtest pmg resolved the runner user's config
dir and fail-closed on its runner-owned log file (run 29289868727 shows
the triaged error catching exactly this). Set it inside the login shell
so it wins regardless of how the leak is delivered.

The remedy now returns a full-help and doctor-table pair from a single
triage, and drops the do-not-chown tail from the leak message.

* fix: adapt event-log error to the two-value remedy signature

Belongs with the previous commit; it was left unstaged and e0580b1
does not compile without it.

* fix: fall back to env path resolution when root has no passwd entry

Running as uid 0 without a resolvable root passwd entry (scratch
containers, minimal chroots) panicked at startup on every command,
because the euid-based path resolution had no fallback. Fall back to
env-derived resolution there: without a passwd database there is no
user switching, so the cross-user poisoning that branch prevents
cannot occur.

Also restore the underlying cause in the generic event-log init error
(minimal output hid it after the usefulerror change), and document
that root's per-user data lives under /root regardless of a preserved
HOME.

* fix: gate SUDO_USER trust and root path diversion; add doctor binary check

Address review findings on the system-install PR:

- cloud_sink: honor SUDO_USER for audit attribution only when euid==0.
  Without the gate any user could set SUDO_USER and spoof cloud-audit
  attribution to another account. Matches the guard in cmd/setup/cert.go.

- config: divert per-user paths to root's passwd home only on an actual
  sudo elevation (euid==0 && SUDO_USER set), not for every root euid.
  The blanket root diversion ignored HOME/XDG_CONFIG_HOME and silently
  stopped reading genuine root users' config (golden Docker images),
  regressing two tests that only fail when the suite runs as root.
  Genuine root honors the environment as before; su without - leaves no
  marker and stays a documented, loud-failing residual.

- doctor: add a system-only check re-validating that the binary the
  installed shims exec is still root-owned and non-writable, catching
  permission/ownership drift after install.

- shim: fold the duplicated shim-scan loop into firstShimContent.

* fix: harden system dirs at install; keep sudo attribution without passwd

Address remaining review comments:

- shim: force root:root 0755 on the managed system dirs (shim tree and
  profile.d) after MkdirAll. A pre-created dir with weaker ownership,
  possible under Debian's group-writable /usr/local/lib, would let a
  non-root user replace the shims every account executes.

- audit: when SUDO_USER has no passwd entry (minimal containers),
  attribute cloud events from sudo's recorded SUDO_USER/SUDO_UID env
  instead of falling back to root. Still gated on euid 0.

- setup: reword the root-without---system warning; alias/shim install
  follows HOME, so claiming it configures only root's home was wrong.

- shim: skip the non-root-owner validation test on Windows, where file
  ownership is not resolvable.

* fix: reject system binaries unreachable by other users; consistent info

The system-install validation checked the binary's own permissions and
the parent's tamper-safety but never reachability: a 0755 root-owned
binary under a 0700 directory (e.g. /root/pmg) passed every check while
every non-root user's shim failed with exit 127. Walk the directory
chain to / and require the search bit for others; doctor's system
binary check inherits this. E2E gains a reject case for a binary under
a non-searchable directory.

setup info: render alias/user-shim/system-shim rows through one
installed-state formatter (location when installed, "not installed"
otherwise) instead of a mix of booleans, paths, and prose.

* fix: stop reowning /etc/profile.d; document group-writable and su gaps

writeSystemProfile chowned/chmod'd /etc/profile.d itself, a shared
system directory pmg does not own, silently overriding any perms a
sysadmin set on it. Secure only the file pmg writes (pmg.sh) via
secureSystemFile, which also forces 0644 explicitly so a restrictive
root umask cannot leave the snippet non-world-readable (which would
drop the shim dir from other users' login-shell PATH).

Docs: add Limitations entries for the group-writable install dir
bypass (validation is defeatable on non-sticky group-writable dirs
like Debian's /usr/local/bin) and the elevation-only scope (su without
- can still poison the caller's home; sudo -u cannot poison another
account). Trim the requireSafeParentDir comment to a pointer.

* refactor: separate unwritable-dir diagnosis from remedy rendering

Address the open review threads on #376:
- rename realUserHomeDir to currentUserHomeDir and fail when the passwd
  entry has no home directory
- split UnwritableConfigDirRemedy into classifyUnwritableDir (cause
  diagnosis) and pure message rendering so each function has one job
- replace cmd/setup's duplicate pathIsUnderDir with the shared
  config.PathWithinDir, now guarding empty inputs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rWmR6NS47FpedJsrTWk8A

* fix: repair umask-clamped modes on system shims and managed config

os.WriteFile and os.MkdirAll honor the process umask, so a hardened root
umask (e.g. 077) produced 0700 shims other users cannot execute and a
0600/0700 managed config non-root pmg runs cannot read - silently
disabling the system-wide policy. Chmod/chown the artifacts explicitly
after writing, with unix regression tests running under umask 077.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rWmR6NS47FpedJsrTWk8A

* refactor: address system-install maintainability review

- move PathWithinDir and the root-owned mkdir/chmod helpers into a new
  internal/fsutil package; config no longer exports a generic fs util
- create managed-config directories with MkdirAllRootOwned, which only
  sets ownership and mode on directories it creates - drops the
  hardcoded safedep parent-dir heuristic and never touches pre-existing
  directories
- collapse NewSystemShimManagerForRemove into NewSystemShimManager and
  validate the executable in Install, where the action needs it; Remove
  works regardless of binary state
- rename ShimConfig.ManageProfile to SystemProfile and document it
- name the Linux-only system paths linuxSystemBinDir/ProfilePath and
  document the Unix-only validation semantics
- share the PMG_BIN shim variable name between writeShimScript and
  parseShimPMGBin via the shimPMGBinVar constant
- cloud attribution falls back to the effective uid when SUDO_UID is
  absent, so sudo-invoked commands are not misattributed to root

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rWmR6NS47FpedJsrTWk8A

---------

Co-authored-by: Sahilb315 <bansalsahil315@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-14 21:34:22 +05:30

1042 lines
40 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: Reject unreachable PMG binary for system install
run: |
# Binary is 0755 and root-owned, but sits under a 0700 dir: other
# users cannot traverse to it, so every shim would exit 127.
sudo mkdir -p /root/pmg-unreachable
sudo install -m 755 bin/pmg /root/pmg-unreachable/pmg
sudo chmod 700 /root/pmg-unreachable
if sudo /root/pmg-unreachable/pmg setup install --system; then
echo "ERROR: system install accepted a binary under a non-searchable directory"
exit 1
fi
echo "SUCCESS: unreachable binary rejected"
- name: Install PMG system-wide
run: |
# GitHub runners ship /usr/local/bin world-writable; system install
# (correctly) requires a root-owned, non-world-writable dir for the
# shared binary. Normalize to the standard production perms first.
ls -ld /usr/local/bin
sudo chown root:root /usr/local/bin
sudo chmod 755 /usr/local/bin
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: Root runs keep per-user state out of the invoking user's home
run: |
# GitHub runner sudo preserves HOME. Every sudo pmg run above used to
# create root-owned ~/.config/safedep for the runner user, which
# fail-closes all their later pmg/npm runs. Must run before any
# non-root pmg invocation legitimately creates that directory.
sudo sh -c 'echo "sudo sees HOME=$HOME"'
if [ -e "$HOME/.config/safedep" ]; then
echo "ERROR: root-created state leaked into $HOME/.config/safedep"
ls -laR "$HOME/.config/safedep"
exit 1
fi
sudo test -d /root/.config/safedep/pmg/logs
echo "SUCCESS: root state stayed under /root"
- 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: |
# Assert the refusal reason: a permission-denied brick (poisoned home)
# would also make config set fail and mask a regression.
if out=$(pmg config set dependency_cooldown.days 7 2>&1); then
echo "ERROR: config set should fail under system config"
exit 1
fi
echo "$out" | grep -qi 'globally managed' || { echo "ERROR: failed for the wrong reason:"; echo "$out"; exit 1; }
if out=$(sudo pmg config set dependency_cooldown.days 7 2>&1); then
echo "ERROR: config set should fail under system config even as root"
exit 1
fi
echo "$out" | grep -qi 'globally managed' || { echo "ERROR: root run failed for the wrong reason:"; echo "$out"; exit 1; }
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
# GH runners export XDG_CONFIG_HOME=/home/runner/.config and it
# leaks through sudo -u, so pmg would resolve the runner user
# config dir and fail on its runner-owned log file.
export XDG_CONFIG_HOME="$HOME/.config"
echo "HOME=$HOME XDG_CONFIG_HOME=$XDG_CONFIG_HOME"
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"