Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/connect-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
- 'src/vip/idp.py'
- 'src/vip/config.py'
- 'src/vip/plugin.py'
- 'src/vip/cli.py'
- 'src/vip/cli/**'
- 'src/vip_tests/conftest.py'
- 'pyproject.toml'
- 'uv.lock'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/copilot-setup-steps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0

# AGENTS.md documents `vip report`, which shells out to a separate
# `quarto` executable (src/vip/cli.py) -- pyproject.toml only installs
# `quarto` executable (src/vip/cli/report.py) -- pyproject.toml only installs
# the Python/Jupyter side. Same action, same pin, same version as
# example-report.yml's Quarto install.
- uses: quarto-dev/quarto-actions/setup@8a96df13519ee81fd526f2dfca5962811136661b # v2
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/install-flow-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ on:
branches: [main]
paths:
- 'src/vip/install/**'
- 'src/vip/cli.py'
- 'src/vip/cli/**'
- 'pyproject.toml'
- 'uv.lock'
- '.github/workflows/install-flow-smoke.yml'
pull_request:
paths:
- 'src/vip/install/**'
- 'src/vip/cli.py'
- 'src/vip/cli/**'
- 'pyproject.toml'
- 'uv.lock'
- '.github/workflows/install-flow-smoke.yml'
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/mock-idp-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ on:
- 'src/vip/auth/**'
- 'src/vip/idp.py'
- 'src/vip/totp.py'
- 'src/vip/cli.py'
- 'src/vip/cli/**'
- 'src/vip_tests/conftest.py'
- 'src/vip_tests/connect/test_auth.py'
- 'src/vip_tests/connect/test_auth.feature'
Expand Down Expand Up @@ -62,7 +62,7 @@ jobs:
- 'src/vip/auth/**'
- 'src/vip/idp.py'
- 'src/vip/totp.py'
- 'src/vip/cli.py'
- 'src/vip/cli/**'
- 'src/vip_tests/conftest.py'
- 'src/vip_tests/connect/test_auth.py'
- 'src/vip_tests/connect/test_auth.feature'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/packagemanager-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
- 'src/vip/idp.py'
- 'src/vip/config.py'
- 'src/vip/plugin.py'
- 'src/vip/cli.py'
- 'src/vip/cli/**'
- 'src/vip/clients/base.py'
- 'src/vip/timeouts.py'
- 'src/vip_tests/conftest.py'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/workbench-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
- 'src/vip/idp.py'
- 'src/vip/config.py'
- 'src/vip/plugin.py'
- 'src/vip/cli.py'
- 'src/vip/cli/**'
- 'src/vip/clients/base.py'
- 'src/vip/timeouts.py'
- 'src/vip_tests/conftest.py'
Expand Down
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,10 @@ Key principles:

| File | Purpose |
|------------------------------------|------------------------------------|
| `src/vip/cli.py` | CLI entry point: version, verify (including `--basic` to skip `@slow`-tagged scenarios), cleanup (Connect content + orphaned Workbench sessions via `--workbench-url`), install, uninstall, auth, scaffold commands; `--version` flag |
| `src/vip/cli/` | CLI entry point, one module per command: `version.py`, `verify.py` (including `--basic` to skip `@slow`-tagged scenarios), `cleanup.py` (Connect content + orphaned Workbench sessions via `--workbench-url`), `install.py` (install, uninstall), `auth.py`, `report.py`, `status.py`, `scaffold.py`; `app.py` builds the parser and holds `main()` and the `--version` flag; `_common.py` holds helpers shared by several commands; `__init__.py` re-exports the names callers import from `vip.cli`, but a selftest must patch a helper on the submodule that calls it |
| `src/vip/config.py` | TOML config loader and dataclasses (includes `[proxy]` → `ProxyConfig`) |
| `src/vip/proxy.py` | Single source of truth for outbound-proxy resolution. `ProxyConfig` + `build_proxy_map` (mirrors httpx's `get_environment_proxies`, incl. NO_PROXY formatting), `build_mounts` (per-scheme `HTTPTransport` mounts that keep `verify`), `proxy_for_url` (httpx-identical most-specific-pattern selection, used by non-httpx probes), `playwright_proxy` (renders a Playwright `launch(proxy=)` dict). Every HTTP egress path routes through this so VIP never diverges from httpx's own env-proxy behavior — see "Outbound proxy support" below |
| `src/vip/auth/` | Interactive and headless browser authentication for OIDC providers, split into `browser.py` (Chromium launch, `InteractiveAuthSession`), `cache.py`, `flows.py` (`start_interactive_auth`, `start_headless_auth`), `sso.py`, `workbench.py`, `scheme.py` (`resolve_url_scheme`) and `apikey.py` (Connect API-key minting); `__init__.py` re-exports the names callers import from `vip.auth`, but a selftest must patch a helper on the submodule that calls it; `authenticated_page` opens a headless page from a cached auth session for `vip cleanup --workbench-url`; `auth_cache_path()` is the single source of truth for the `.vip-auth-cache.json` location (both `plugin.py` and `cli.py` must use it), and `_load_cached_auth` probes Workbench before trusting a cached session; `refresh_auth_cache_from_storage_state` writes a live context's cookies back over a cache whose session has been invalidated (atomic, 0600, existing caches only) |
| `src/vip/auth/` | Interactive and headless browser authentication for OIDC providers, split into `browser.py` (Chromium launch, `InteractiveAuthSession`), `cache.py`, `flows.py` (`start_interactive_auth`, `start_headless_auth`), `sso.py`, `workbench.py`, `scheme.py` (`resolve_url_scheme`) and `apikey.py` (Connect API-key minting); `__init__.py` re-exports the names callers import from `vip.auth`, but a selftest must patch a helper on the submodule that calls it; `authenticated_page` opens a headless page from a cached auth session for `vip cleanup --workbench-url`; `auth_cache_path()` is the single source of truth for the `.vip-auth-cache.json` location (both `plugin.py` and `cli/cleanup.py` must use it), and `_load_cached_auth` probes Workbench before trusting a cached session; `refresh_auth_cache_from_storage_state` writes a live context's cookies back over a cache whose session has been invalidated (atomic, 0600, existing caches only) |
| `src/vip/idp.py` | IdP login form strategies for headless auth (Keycloak, Okta) |
| `src/vip/attest.py` | The two skip helpers (`not_applicable`, `unproven`) that record whether a skipped check was out of scope or simply never verified |
| `src/vip/plugin.py` | pytest plugin: markers (including `slow`, used by `verify --basic`), auto-skip, JSON report output; `pytest_configure` also registers `vip.fixtures` as its own named plugin (`"vip-fixtures"`) so core fixtures resolve regardless of directory ancestry — see that module's docstring |
Expand Down Expand Up @@ -292,7 +292,7 @@ VIP talks to deployments over three different HTTP mechanisms, and left alone th
- **Bare httpx call sites** (auth mint/probe/delete, cache-liveness probe, `fetch_content`, scheme resolution): pass an explicit `proxy=proxy_for_url(url, build_proxy_map(proxy))` **and** `trust_env=False`, so the resolved per-URL proxy (which honors NO_PROXY) is authoritative rather than httpx re-reading the env.
- **Playwright** (`_launch_chromium`, and the in-suite `browser_context_args`): pass `proxy=playwright_proxy(build_proxy_map(proxy), target_url)` so the browser shares the same proxy. Always pass the URL that browser is about to navigate. Playwright takes one `server` per browser and rewrites it to a single `scheme://host:port` (its `normalizeProxySettings`), so Chromium's per-scheme `--proxy-server=http=a;https=b` form is unavailable — `target_url`'s scheme is what keeps the browser on the same gateway as httpx when `HTTP_PROXY` and `HTTPS_PROXY` differ and the product is served over plain http. `target_url` chooses *which* proxy, never *whether*: a bypassed target still returns a dict, because the login browser also navigates the IdP, which usually is not bypassed.

`ProxyConfig` (from `[proxy]` in `vip.toml`, or `--proxy`/`--no-proxy`) threads from `VIPConfig.proxy` through the conftest client fixtures, the plugin auth entrypoints, and every `cli.py` command. Default (`trust_env=True`, no `url`) reads the ambient environment exactly as httpx would — so the no-config case is unchanged.
`ProxyConfig` (from `[proxy]` in `vip.toml`, or `--proxy`/`--no-proxy`) threads from `VIPConfig.proxy` through the conftest client fixtures, the plugin auth entrypoints, and every `vip.cli` command. Default (`trust_env=True`, no `url`) reads the ambient environment exactly as httpx would — so the no-config case is unchanged.

**One deliberate divergence from httpx** (`_promote_http_proxy_to_https`): httpx keys its env map by *target* scheme, so a lone `HTTP_PROXY` (no `HTTPS_PROXY`/`ALL_PROXY`) yields `{'http://': …}` and httpx sends **https direct**. Many orgs run a single forward proxy as their *only* outbound tunnel and set just `http_proxy`, expecting https to tunnel through it via `CONNECT`; on a proxy-only network httpx's default means VIP's https traffic can never leave the host. So when the env gives an `http://` proxy with no explicit https/all coverage, `build_proxy_map` promotes it to cover `https://` too — applied to the whole map, so `proxy_for_url`, `build_mounts`, and `playwright_proxy` all agree (browser and API take the same route by construction). An explicit `HTTPS_PROXY`/`ALL_PROXY` is never overridden, and `NO_PROXY` still bypasses. This is why `build_proxy_map` is *not* byte-for-byte identical to `get_environment_proxies` in the http-only case.

Expand Down Expand Up @@ -364,7 +364,7 @@ Every render also produces `_output/vip-report.pdf` from `report/vip-report.qmd`
`workbench-smoke.yml` additionally splits into two tiers via a `suite` value: PR/push runs `gate` (the fast subset), the nightly schedule runs `full` (every Workbench file), and `workflow_dispatch` can pick either. The split is based on a measured run rather than taste — `full` buys 8 more real passes for ~523s more test time, which is worth a nightly but not a merge gate. Skip reasons do **not** appear in the log even with `-rs`, because VIP's plugin owns the terminal reporter; read them from `<skipped message=...>` in the uploaded `smoke-results.xml`.
The three cross-cutting suites (`cross_product/test_resources`, `security/test_auth_policy`, `config_hygiene/test_secrets`) run in all three product workflows. `test_secrets` asserts no plaintext `api_key`/`password` in the generated `vip.toml`, so credentials must be passed to pytest as step `env` (`VIP_CONNECT_API_KEY`, `VIP_TEST_PASSWORD`) and never written into the config file.
- **`connect-integration.yml`** -- push to `main`, `schedule` (daily), and `workflow_dispatch`; deliberately no `pull_request` trigger and not a required check (decided in #421). Runs the full VIP Connect category (`vip verify --api-auth --categories connect`) against ephemeral `release` and `preview` (nightly build) Connect servers via `with-connect`, so an unreleased upstream build never blocks a PR. It's a drift detector, not a gate: failures post to Slack via the `SLACK_WEBHOOK_CONNECT_CI` secret instead of failing anyone's merge.
- **`install-flow-smoke.yml`** -- push to `main` or PR (paths-gated to `src/vip/install/**`, `src/vip/cli.py`, `pyproject.toml`, `uv.lock`) or `workflow_dispatch`; reproduces the documented default install flow from the README (`uv tool install posit-vip` then `vip install`) on Ubuntu-as-root and macOS. Distinct from the product smoke workflows above: `uv tool install` only puts vip's own entry point on `PATH`, a topology every other install test misses because it runs `uv run vip install` from inside the project venv.
- **`install-flow-smoke.yml`** -- push to `main` or PR (paths-gated to `src/vip/install/**`, `src/vip/cli/**`, `pyproject.toml`, `uv.lock`) or `workflow_dispatch`; reproduces the documented default install flow from the README (`uv tool install posit-vip` then `vip install`) on Ubuntu-as-root and macOS. Distinct from the product smoke workflows above: `uv tool install` only puts vip's own entry point on `PATH`, a topology every other install test misses because it runs `uv run vip install` from inside the project venv.
- **`linux-smoke.yml`** -- push to `main` or PR (paths-gated to `docker/rhel*/`, `docker/opensuse-leap/`, `docker/ubuntu2404/`, `docker/playwright-smoke.py`, `scripts/rhel-smoke.sh`, `scripts/opensuse-leap-smoke.sh`, `scripts/ubuntu2404-smoke.sh`, `justfile`, `pyproject.toml`, `uv.lock`, `src/**`, `selftests/**`); builds a Docker image per distro (`rhel9`, `rhel10`, `opensuse-leap`, `ubuntu2404`) that exercises `vip install`'s `apt`/`zypper` system-package path, then runs the smoke script inside it. `ubuntu2404`'s Dockerfile also proves the #621 t64-detection fix at build time: it installs the four renamed packages under their old names, then a second `vip install --dry-run` must report nothing left to install and the written manifest must name the t64 packages apt actually installed rather than the requested aliases, failing the build (not just the container run) if either regresses.
- **`mac-smoke.yml`** -- push to `main` or PR (paths-gated to `docker/playwright-smoke.py`, `pyproject.toml`, `uv.lock`, `src/**`); runs `vip install` and a Playwright smoke script natively on `macos-latest`, the one CI job that exercises the install flow on real macOS rather than in a container.
- **`add-to-team-project.yml`** -- when a `team: connect`, `team: workbench`, or `team: package manager` label is added to an issue, adds it to that product team's org-level GitHub project board. Ported from rstudio/helm. Requires the cross-org `POSIT_PLATFORM_CLIENT_ID`/`POSIT_PLATFORM_PEM` app secrets.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ packages = ["src/vip", "src/vip_tests"]
"examples/custom_tests" = "vip/_scaffold/custom_tests"
# AGENTS.md source shared by every scaffold template; copied into the output
# dir by run_scaffold() after copytree(). Keep this path in sync with
# _resolve_scaffold_source("_shared") in src/vip/cli.py.
# _resolve_scaffold_source("_shared") in src/vip/cli/scaffold.py.
"examples/_shared" = "vip/_scaffold/_shared"
# Quarto report templates so `vip report` works when installed as a wheel,
# not only from a source checkout. Copied into the working ./report dir by
Expand Down
2 changes: 1 addition & 1 deletion selftests/install/test_cli_uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ def _parse_uninstall(monkeypatch, *argv: str):
from vip import cli

seen: list[argparse.Namespace] = []
monkeypatch.setattr(cli, "run_uninstall", seen.append)
monkeypatch.setattr("vip.cli.app.run_uninstall", seen.append)
monkeypatch.setattr(sys, "argv", ["vip", "uninstall", *argv])
cli.main()
assert seen, "run_uninstall was never reached"
Expand Down
4 changes: 2 additions & 2 deletions selftests/test_auth_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,10 +568,10 @@ def test_call_sites_do_not_build_the_path_inline(self):
from pathlib import Path as _Path

import vip.auth.cache
import vip.cli
import vip.cli.cleanup
import vip.plugin

for module in (vip.cli, vip.plugin):
for module in (vip.cli.cleanup, vip.plugin):
source = _Path(module.__file__).read_text()
assert ".vip-auth-cache.json" not in source, (
f"{module.__name__} builds the auth cache path inline; "
Expand Down
2 changes: 1 addition & 1 deletion selftests/test_auth_scheme.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def test_defaults_return_true(self):

def test_insecure_wins_over_ca_bundle(self, tmp_path):
"""When both insecure=True and a ca_bundle path are provided,
insecure wins — mirrors cli.py:391 logic.
insecure wins — mirrors vip.cli._common._resolve_effective_ca_bundle.
"""
from vip.auth import _httpx_verify

Expand Down
11 changes: 6 additions & 5 deletions selftests/test_cli_cleanup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import vip.auth
import vip.cli
import vip.cli.cleanup
import vip.workbench_ui
from vip.auth import InteractiveAuthSession
from vip.clients.workbench import is_vip_session
Expand Down Expand Up @@ -126,7 +127,7 @@ def cleanup_vip_content(self):
def _fail(*a, **k):
pytest.fail("workbench cleanup should not run without a workbench URL")

monkeypatch.setattr(vip.cli, "_cleanup_workbench_sessions", _fail)
monkeypatch.setattr(vip.cli.cleanup, "_cleanup_workbench_sessions", _fail)

vip.cli.run_cleanup(_make_args(connect_url="https://c.example.com"))

Expand All @@ -147,7 +148,7 @@ def _fail(*a, **k):

called = {}
monkeypatch.setattr(
vip.cli,
vip.cli.cleanup,
"_cleanup_workbench_sessions",
lambda url, args, config: called.setdefault("url", url),
)
Expand Down Expand Up @@ -177,7 +178,7 @@ def cleanup_vip_content(self):

called = {}
monkeypatch.setattr(
vip.cli,
vip.cli.cleanup,
"_cleanup_workbench_sessions",
lambda url, args, config: called.setdefault("url", url),
)
Expand Down Expand Up @@ -226,7 +227,7 @@ def test_workbench_url_falls_back_to_vip_toml(self, tmp_path, monkeypatch):

called = {}
monkeypatch.setattr(
vip.cli,
vip.cli.cleanup,
"_cleanup_workbench_sessions",
lambda url, args, config: called.setdefault("url", url),
)
Expand Down Expand Up @@ -459,7 +460,7 @@ def _parse_cleanup(self, *argv: str) -> argparse.Namespace:
"""
seen: list[argparse.Namespace] = []
with (
patch("vip.cli.run_cleanup", side_effect=seen.append),
patch("vip.cli.app.run_cleanup", side_effect=seen.append),
patch.object(sys, "argv", ["vip", "cleanup", *argv]),
):
from vip.cli import main
Expand Down
Loading
Loading