mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 00:16:38 +00:00
* feat(packages): add apk update flow + pkg-helper v2 protocol - APK update checker/executor via helper IPC (runtime detection, upgrade scan via apk list --upgradable) - BREAKING: pkg-helper v2 protocol (5 actions: check_apk/check_pip/check_npm/exec_apk/exec_pip, code/data fields, renewable 10min deadline, apkMutex, 1MB scanner) - Edition gating: SupportsApk + IsAlpineRuntime double-gate (Standard/Full only) - Backend 3-branch wiring: alpine/apt/yum routes + update_registry, dep_installer helpers - i18n: 5 apk keys (EN/VI/ZH catalogs) - Frontend: source pill Alpine badge, APK in updates-list/summary-bar/update-all modal - E2E tests: apk_e2e build tag covering checker/executor/helper protocol - Docs: packages-apk.md, security/changelog updates - Plans + reports under plans/260417-1500-packages-update-phase2b-apk-pkghelper/ + plans/reports/ * docs(packages): journal Phase 2b apk + pkg-helper v2
197 lines
7.9 KiB
Markdown
197 lines
7.9 KiB
Markdown
# pip + npm Package Updates (Phase 2a)
|
|
|
|
Extends the Phase 1 GitHub binary update flow to system-wide pip and npm packages.
|
|
Closes #900 (Phase 2a).
|
|
|
|
See also: [GitHub binary updates](./packages-github.md) · [apk system package updates](./packages-apk.md)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
When the gateway is running in Standard edition with `pip3` and/or `npm` on PATH,
|
|
`GET /v1/packages/updates` includes pip and npm update results alongside GitHub
|
|
binaries. The UI shows a per-source pill filter; sources without a binary on PATH
|
|
are hidden automatically.
|
|
|
|
pip scope: **system-wide** (`--break-system-packages`). pip venv / user-site is not
|
|
supported in Phase 2a.
|
|
|
|
npm scope: **global** (`--global`). Per-project `node_modules` are not touched.
|
|
|
|
---
|
|
|
|
## Command Matrix
|
|
|
|
| Source | Check command | Update command | Check timeout | Update timeout |
|
|
|--------|---------------|----------------|---------------|----------------|
|
|
| pip | `pip3 list --outdated --format json --break-system-packages` | `pip3 install --upgrade --no-cache-dir --break-system-packages --upgrade-strategy only-if-needed <name>` | 30 s | 5 min |
|
|
| npm | `npm outdated --global --json` | `npm install --global <name>@<version>` | 30 s | 5 min |
|
|
|
|
Pre-release pip check appends `--pre` in a secondary call (see Pre-Release Handling).
|
|
|
|
---
|
|
|
|
## Behavior
|
|
|
|
### pip
|
|
|
|
- `pip3 list --outdated --format json` emits a JSON array; each element has
|
|
`name`, `version`, `latest_version`, `latest_filetype`.
|
|
- Exit code is always 0 whether or not updates exist.
|
|
- stderr is classified via `ClassifyPipStderr` into sentinel errors (see Error Classes).
|
|
|
|
### npm
|
|
|
|
npm's exit-code semantics are non-standard:
|
|
|
|
| Condition | Exit code | Interpretation |
|
|
|-----------|-----------|----------------|
|
|
| No outdated packages | 0 | No updates |
|
|
| Outdated packages found | 1 | Updates — parse JSON stdout |
|
|
| Real npm error (ERESOLVE, network, etc.) | 1 | stderr contains `npm ERR!` |
|
|
| Ambiguous (exit 1, no stdout, no stderr) | 1 | Treated as no-updates |
|
|
|
|
The checker inspects exit code **and** stderr for `npm ERR!` before deciding
|
|
whether exit 1 means "updates available" or "real error".
|
|
|
|
---
|
|
|
|
## Pre-Release Handling
|
|
|
|
### pip
|
|
|
|
Two-call merge strategy:
|
|
|
|
1. **Primary call** (stable only, no `--pre`): baseline list of outdated packages.
|
|
2. If any currently-installed package has a pre-release version (`IsPipPreRelease()`):
|
|
**Secondary call** with `--pre` to surface the best available upgrade target.
|
|
3. Results are merged by package name; when a name appears in both, the entry
|
|
with the lexicographically higher `latest_version` wins.
|
|
|
|
Gate: `IsPipPreRelease` matches PEP 440 patterns — `a`, `b`, `rc`, `dev`, `.pre`,
|
|
`.preview` (case-insensitive, digits optional).
|
|
|
|
### npm
|
|
|
|
Single-call strategy with a skip gate:
|
|
|
|
- If `latest` contains an npm pre-release label
|
|
(`-alpha`, `-beta`, `-rc`, `-pre`, `-preview`, `-dev`, `-nightly`, `-snapshot`)
|
|
**and** `current` does not → entry is skipped.
|
|
- If `current` is already a pre-release and `latest` is too → entry kept (user on
|
|
pre-release channel receives the newest pre-release update).
|
|
|
|
This prevents unexpected upgrades from stable channels to unstable channels.
|
|
|
|
---
|
|
|
|
## Availability Detection
|
|
|
|
A source is considered **available** when two gates both pass:
|
|
|
|
1. **Binary present**: `exec.LookPath("pip3")` / `exec.LookPath("npm")` succeeds.
|
|
2. **Edition allows it**: `edition.Current().SupportsPipNpm == true` (always true
|
|
for Standard; always false for Lite desktop).
|
|
|
|
When a source is unavailable:
|
|
|
|
- Its checker returns `UpdateCheckResult{Available: false}` (no error, no updates).
|
|
- `UpdateRegistry.Availability()` maps that source to `false`.
|
|
- `GET /v1/packages/updates` response includes `"availability": {"pip": false, "npm": false}`.
|
|
- The frontend hides that source from the filter bar.
|
|
|
|
Lite edition: `gateway_packages_wiring.go` checks `edition.Current().SupportsPipNpm`
|
|
before calling `RegisterChecker` / `RegisterExecutor`. Pip and npm checkers are
|
|
never instantiated — `registry.Sources()` returns `["github"]` only.
|
|
|
|
---
|
|
|
|
## Error Classes
|
|
|
|
Sentinel errors are defined in `internal/skills/pkg_update_helpers.go`.
|
|
|
|
### pip sentinels
|
|
|
|
| Sentinel | Trigger pattern in stderr | i18n key |
|
|
|----------|--------------------------|----------|
|
|
| `ErrUpdatePipExternallyManaged` | `externally-managed-environment` / `EXTERNALLY-MANAGED` | `packages.update.pip.externally_managed` |
|
|
| `ErrUpdatePipPermission` | `Permission denied` / `EACCES` | `packages.update.pip.permission` |
|
|
| `ErrUpdatePipNotFound` | `No matching distribution` / `Could not find a version` | `packages.update.pip.not_found` |
|
|
| `ErrUpdatePipNetwork` | `Read timed out` / `ConnectionError` / `network` | `packages.update.pip.network` |
|
|
| `ErrUpdatePipConflict` | `incompatible` / `dependency resolver` / `Shallow backtracking` | `packages.update.pip.conflict` |
|
|
|
|
### npm sentinels
|
|
|
|
| Sentinel | Trigger pattern in stderr | i18n key |
|
|
|----------|--------------------------|----------|
|
|
| `ErrUpdateNpmPermission` | `EACCES` | `packages.update.npm.permission` |
|
|
| `ErrUpdateNpmConflict` | `ERESOLVE` | `packages.update.npm.conflict` |
|
|
| `ErrUpdateNpmNetwork` | `ETIMEDOUT` / `ENOTFOUND` / `getaddrinfo` | `packages.update.npm.network` |
|
|
| `ErrUpdateNpmTargetMissing` | `ETARGET` | `packages.update.npm.target_missing` |
|
|
| `ErrUpdateNpmNotFound` | `E404` / `404` / `not in this registry` | `packages.update.npm.not_found` |
|
|
|
|
Unclassified stderr returns a generic wrapped error with a truncated reason
|
|
(≤ 500 chars, ANSI-stripped).
|
|
|
|
---
|
|
|
|
## Runbook
|
|
|
|
| Symptom | Fix |
|
|
|---------|-----|
|
|
| **pip EACCES** — gateway lacks write to site-packages | Run gateway as an owner of `/usr/lib/python3/dist-packages`, or set `PIP_TARGET=/app/data/.pip` + add it to `PYTHONPATH` |
|
|
| **npm EACCES** — global prefix owned by root | `npm config set prefix ~/.npm-global`; add `~/.npm-global/bin` to `PATH` in entrypoint |
|
|
| **npm ERESOLVE** — peer conflict blocks install | SSH into container: `npm install -g <name>@<version> --legacy-peer-deps`; re-check will clear the entry |
|
|
| **pip externally-managed (PEP 668)** | Set env var `PIP_BREAK_SYSTEM_PACKAGES=1`, or upgrade pip to ≥ 23.3 (respects the CLI flag without the env var) |
|
|
|
|
---
|
|
|
|
## Minimum Versions
|
|
|
|
| Runtime | Minimum | Recommended | Notes |
|
|
|---------|---------|-------------|-------|
|
|
| pip | 20.0 | ≥ 23.3 | `--format json` requires 20+; `--break-system-packages` without env var requires 23.3+ |
|
|
| npm | 6.0 | ≥ 10 | Older versions may not emit JSON exit 1 correctly |
|
|
| Node.js | 12 | ≥ 18 LTS | npm 10 requires Node 18+ |
|
|
|
|
---
|
|
|
|
## Shared Locker
|
|
|
|
`InstallSingleDep` (skill dep install) and `PipUpdateExecutor.Update` / `NpmUpdateExecutor.Update`
|
|
(update apply) share a single `PackageLocker` instance injected via `SetSharedPackageLocker`.
|
|
|
|
This means concurrent `pip install requests` (from a skill) and `pip upgrade requests`
|
|
(from the update flow) are serialized by the same per-key mutex. The lock key is
|
|
the bare package name (e.g. `"requests"`) scoped to the source (`"pip"` or `"npm"`).
|
|
|
|
Operators must not bypass the gateway and call `pip install` directly in parallel
|
|
with gateway operations — doing so defeats the shared lock and risks a partial-install
|
|
race.
|
|
|
|
---
|
|
|
|
## Fixture Regeneration
|
|
|
|
Test fixtures capture `pip3 list --outdated --format json` and `npm outdated -g --json`
|
|
output. When the environment's package versions change, regenerate them:
|
|
|
|
```bash
|
|
# pip fixture — include pip version in filename for drift tracking
|
|
pip3 --version # e.g., pip 24.0
|
|
pip3 list --outdated --format json --break-system-packages \
|
|
> internal/skills/testdata/pip_outdated_pip24.json
|
|
|
|
# npm fixture — include npm version in filename
|
|
npm --version # e.g., 10.5.0
|
|
npm outdated --global --json \
|
|
> internal/skills/testdata/npm_outdated_npm10.json
|
|
# Note: npm exits 1 when packages are outdated — that's expected.
|
|
|
|
# Update test cases to reference the new filename and expected values.
|
|
```
|
|
|
|
Fixture files are version-stamped in their names so drift between CI environments
|
|
is detectable by `git diff`.
|