Commit graph

4 commits

Author SHA1 Message Date
Abhay Singh
7092b53c46
fix(cli/update): let install ownership win over bare /.dockerenv so venv installs self-update (#2830)
## Description

`headroom update` refuses to self-update for any install that happens to
run inside a container, including a plain `pip install` into a venv,
because `detect_install_method` checks `_in_docker()` before the pipx /
uv-tool / venv / user-site branches. The guidance it prints does not
apply: there is no Headroom image in the picture, the container is the
environment and Headroom was pip-installed into a venv inside it.

```console
$ headroom update --check
Update available: 0.32.0 -> 0.34.0
Running inside a container - pull a newer Headroom image instead of self-updating.
```

`_in_docker()` is purely environmental (`/.dockerenv` exists, or
`HEADROOM_IN_DOCKER` is set), with no reference to how the package was
installed, so `/.dockerenv` alone shadows a venv that clearly owns the
install. This hits devcontainers, GitHub Codespaces, docker/LXC
self-hosting, and dev images.

The fix splits the check by intent. An EXPLICIT `HEADROOM_IN_DOCKER`
(which the official image can set) is a deliberate opt-out and still
refuses up front, even over a venv, so the real-image behavior is
preserved. The bare `/.dockerenv` heuristic now runs after ownership
detection, so a venv / pipx / uv / user-site install self-updates and
only a container whose own system interpreter owns the install still
gets the pull-a-new-image guidance.

Fixes #2816

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- `headroom/cli/update.py` (`detect_install_method`): replaced the
up-front `_in_docker()` refusal with an explicit
`os.environ.get("HEADROOM_IN_DOCKER")` refusal (the official image
opt-out), and added the bare `_in_docker()` refusal after the pipx /
uv-tool / venv / user-site branches so ownership wins over environment.
Updated the resolution-order docstring.
- `tests/test_update_helpers.py`: added
`test_venv_inside_bare_dockerenv_still_self_updates` (the fix),
`test_explicit_headroom_in_docker_still_refuses_over_venv` (image
opt-out preserved), and `test_bare_dockerenv_without_owner_refuses`
(system-interpreter container still refuses).
- `tests/test_cli_update.py` (`test_detect_docker`): updated to drive
the bare-`/.dockerenv`-no-owner path deterministically (mock ownership
to absent), since a real venv underneath now correctly wins.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed

### Test Output

```text
# Fail-before (source fix stashed, new test kept):
tests/test_update_helpers.py::test_venv_inside_bare_dockerenv_still_self_updates FAILED
  assert method.kind == "pip"
  AssertionError: assert 'docker' == 'pip'

# Pass-after (fix applied), all update suites:
tests/test_update_helpers.py tests/test_cli_update.py tests/test_update_check.py
95 passed

# uvx ruff@0.15.17 check  -> All checks passed!
# uvx mypy@1.20.2 headroom/cli/update.py -> Success: no issues found in 1 source file
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.12.11, project venv, pytest 9.1.1,
ruff 0.15.17 and mypy 1.20.2 via uvx.
- Exact command / steps: read `detect_install_method` to confirm
`_in_docker()` (line 354) preceded the pipx (377) / uv-tool (385) / venv
(392) branches, reproduced the issue's environment in a test (bare
`/.dockerenv` via `_in_docker` monkeypatched True, `HEADROOM_IN_DOCKER`
unset, a venv layout under `sys.prefix`), fail-before with `git stash
push headroom/cli/update.py` and `python -m pytest
tests/test_update_helpers.py -k venv_inside_bare_dockerenv` (the venv is
refused with `kind == "docker"`), then pass-after with `git stash pop`
and rerunning the full update suites (95 passed).
- Observed result: a venv/pip install inside a bare `/.dockerenv`
container now resolves to `kind="pip"`, `can_self_update=True`,
`argv=[sys.executable, "-m", "pip", "install", "-U", ...]`, matching the
manual command the issue reporter confirmed works. An explicit
`HEADROOM_IN_DOCKER=1` still resolves to `kind="docker"` even over a
venv, and a container whose system interpreter owns the install still
resolves to `kind="docker"`.
- Not tested: an end-to-end `headroom update` run inside a real
devcontainer against live PyPI (no container in this environment). The
resolution is a pure classification function verified directly, and the
actual upgrade command it builds is the existing, already-tested venv
path.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I did **not** edit `CHANGELOG.md`: it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Additional Notes

The official image opt-out is preserved by design: the issue notes
`_in_docker()` already honors `HEADROOM_IN_DOCKER`, so the image can
keep refusing self-update by setting it, which this PR routes to the
explicit up-front check that wins even over a venv. Only the bare
`/.dockerenv` auto-detection was demoted below ownership.
2026-08-11 17:23:29 -05:00
Rod Boev
2678bb1db6
fix(update): let Windows self-update replace headroom.exe (#2016)
## Description

`headroom update` currently runs pip from the same `headroom.exe`
process pip needs to replace. On Windows that leaves the launcher locked
and the upgrade fails mid-uninstall. This reroutes Windows pip-based
self-update through a short delayed Python helper that replays the
original pip argv after the current launcher exits. Other update paths
stay synchronous, and the printed upgrade command stays exact for manual
recovery.

Closes #1941

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Replace the Windows-only `cmd.exe /c` handoff for `pip` and `pip-user`
self-update with a short Python helper that replays the original pip
argv after the launcher exits.
- Keep every pip value, including extras, as argv data through the
delayed child and preserve the manual recovery output.
- Keep pipx, uv-tool, and non-Windows behavior unchanged.
- Add focused regression coverage for extras containing `&`, `|`, and
`>`.

## Testing

- [x] Unit tests pass
- [x] Linting passes
- [ ] Type checking passes
- [x] New tests added for new functionality when applicable
- [ ] Manual testing performed

### Test Output

```text
uv run pytest tests/test_cli_update.py tests/test_update_helpers.py -q
66 passed

uv run ruff check headroom/cli/update.py tests/test_cli_update.py tests/test_update_helpers.py
All checks passed

uv run ruff format --check headroom/cli/update.py tests/test_cli_update.py tests/test_update_helpers.py
3 files already formatted
```

## Real Behavior Proof

- Environment: Windows pip install
- Exact command / steps: run `headroom update --extras "foo&calc"`,
accept the prompt, and wait for the child pip output
- Observed result: Windows pip and pip-user updates now launch the
original pip command through a short delayed Python helper argv, so
extras stay one argument end to end, while pipx and non-Windows paths
remain synchronous in the focused test coverage
- Not tested: live Windows run on this host
- Scope: Windows pip self-update

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

The printed upgrade command stays unchanged so the manual recovery path
remains exact and copy-pasteable.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-14 11:52:47 -04:00
JoaoMarcos44
0750bbff4d
fix(update): prevent _core.pyd corruption on Windows when proxy is running (#1581)
## Description

On Windows, running `headroom update` while `headroom proxy` is active
can corrupt the installed package by leaving the native `_core.pyd`
extension in a partially upgraded state. This PR adds a safer update
path around the pip invocation.

Closes #1580.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Add `safe_update()` handling for Windows native-extension update
safety.
- Detect whether `_core.pyd` is locked before pip runs.
- Create a proactive backup when the file is not locked, then restore
atomically if import integrity fails.
- Warn when the proxy is running and `_core.pyd` is locked, allowing pip
to fail safely without replacing the loaded file.
- Use atomic replacement for restore paths.

## Testing

- [x] Unit tests pass
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed

### Test Output

```text
Focused update-path tests and reviewer approval were completed on this PR before the governance body cleanup. The current body update is documentation-only metadata for PR governance.
```

## Real Behavior Proof

- Environment: Windows-focused Headroom development/review context.
- Exact command / steps: Reviewed the safe update flow for locked and
unlocked `_core.pyd` cases, including backup, pip invocation, import
validation, and restore behavior.
- Observed result: The update path avoids replacing a loaded native
extension and provides an atomic restore path when an unlocked update
fails validation.
- Not tested: End-to-end package publication/install from PyPI as part
of this body cleanup.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Additional Notes

This body was normalized by a maintainer after approval so the
governance parser reflects the already-reviewed PR state.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-11 10:36:23 -05:00
Focused Instability
26be2c39cb
feat(cli): add headroom update command and release banner (#1088)
## Description

Adds a `headroom update` self-update command and a passive "update
available" banner, so users no longer need to remember the right
`pip`/`pipx`/`uv` incantation for their environment, and long-running
proxies get nudged when they drift behind a release.

Closes #1087

## Type of Change

- [x] New feature (non-breaking change that adds functionality)
- [x] Documentation update

## Changes Made

- `headroom/cli/update.py` — `headroom update` command.
`detect_install_method()` resolves the install (git checkout, editable,
Docker, pipx, uv tool, venv/conda, `pip --user`, externally-managed
system Python per PEP 668, writable global) and builds the matching
upgrade. pip path always uses `sys.executable -m pip` so it can't touch
the wrong interpreter. Refuses with guidance where self-update is
unsafe. Flags: `--check`, `--yes`, `--pre`, `--extras`.
- `headroom/update_check.py` — best-effort PyPI check (stdlib `urllib`,
no new dep). Split into a daemon-thread probe that caches to
`~/.headroom/update_check.json` (≤ once/day) and a cache-only
`format_update_notice()`. Opt-out `HEADROOM_UPDATE_CHECK=off`; skipped
in `--stateless`, CI, Docker, checkouts.
- `headroom/cli/main.py`, `headroom/cli/__init__.py` — register
`update`; fire the background check from the group callback (skipped for
`update`).
- `headroom/cli/proxy.py` — render the one-line notice after the startup
banner (best-effort, never blocks).
- `README.md` — "Updating" section + opt-out env var.
- Tests: `tests/test_update_check.py`, `tests/test_cli_update.py`.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ pytest tests/test_update_check.py tests/test_cli_update.py -q
42 passed in 1.63s

$ ruff check headroom/cli/update.py headroom/update_check.py headroom/cli/main.py
All checks passed!

$ mypy headroom/update_check.py headroom/cli/update.py
Success: no issues found in 2 source files
```

## Real Behavior Proof

- Environment: macOS, Python 3.11, source checkout
- Exact command / steps: `python -m headroom.cli update --help`;
`detect_install_method()` in the checkout
- Observed result: command + flags render; in a checkout
`detect_install_method()` returns `kind=checkout, can_self_update=False`
("update with `git pull`") and `format_update_notice()` returns `None`
(dev tree not nagged)
- Not tested: live PyPI fetch and a real pipx/uv-tool upgrade on this
machine (covered by unit tests with mocked `urllib`/`subprocess`)

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

- CHANGELOG.md is release-please-managed, so it is intentionally not
hand-edited (N/A above).
- Update check uses stdlib `urllib` because `httpx` lives only in the
`[proxy]` extra — the base CLI must stay dependency-light.
2026-06-18 11:22:20 -05:00