Skip to content

feat(config): persist non-secret installer prefs for self-update (closes #667)#1915

Merged
danielmeppiel merged 5 commits into
mainfrom
impl/667-installer-config
Jun 25, 2026
Merged

feat(config): persist non-secret installer prefs for self-update (closes #667)#1915
danielmeppiel merged 5 commits into
mainfrom
impl/667-installer-config

Conversation

@danielmeppiel

Copy link
Copy Markdown
Collaborator

feat(config): persist non-secret installer prefs for self-update

TL;DR

This PR closes #667 by adding two bounded apm config keys for self-update installer defaults: self-update.channel and self-update.install-dir. apm self-update now reads those non-secret preferences, while credentials, registry tokens, mirror URLs, commands, and installer args stay out of persisted self-update config and continue through the existing auth/env paths.

Important

Trust boundary: this intentionally persists only non-secret, non-executable installer preferences.

Problem (WHY)

  • The accepted issue asks for self-update installer settings to be persisted through apm config, with a hard scope limit: "Scope the first cut to non-secret, non-executable installer preferences only".
  • The security boundary is explicit: "credentials and registry tokens stay OUT of persisted config and continue to resolve via the existing auth path".
  • This PR keeps that boundary narrow by rejecting any other self-update.* key instead of creating an open-ended installer schema.

Approach (WHAT)

Area Change
Config schema Add self_update_channel and self_update_install_dir only.
CLI surface Add apm config get/set/unset self-update.channel and self-update.install-dir.
Self-update Read config after env vars, and pass only allowed installer defaults to the subprocess env.
Trust boundary Reject secret/executable-like self-update.* keys with an actionable error.
Docs Update CLI docs and apm-usage resources with precedence and non-persistence rules.

Implementation (HOW)

File(s) What changed
src/apm_cli/config.py Added typed helpers to validate, persist, read, and unset the two allowed self-update keys.
src/apm_cli/commands/config.py Routed get/set/unset for the two keys and added a fail-closed branch for unsupported self-update.* keys.
src/apm_cli/commands/self_update.py Builds installer subprocess env from config only when env vars are absent; prerelease channel pins the selected version for the installer.
src/apm_cli/utils/version_checker.py Added optional prerelease lookup via the GitHub releases list endpoint while preserving stable default behavior.
tests/unit/* Added storage, CLI, self-update env, prerelease lookup, and negative trust-boundary tests.
docs/... and packages/apm-guide/... Documented keys, precedence, and secret/mirror exclusions.

Diagram

Legend: the dashed nodes are new in this PR; env vars remain the highest-priority override.

flowchart LR
    subgraph Config[Config]
        C1[self-update.channel]
        C2[self-update.install-dir]
        C3[unsupported self-update keys]
    end
    subgraph Resolve[Self-update resolution]
        R1[env vars]
        R2[config fallback]
        R3[installer subprocess env]
    end
    subgraph Boundary[Trust boundary]
        B1[credentials and tokens]
        B2[mirror URLs]
        B3[commands and args]
    end
    C1 --> R2
    C2 --> R2
    R1 --> R3
    R2 --> R3
    C3 --> B1
    C3 --> B2
    C3 --> B3
    classDef new stroke-dasharray: 5 5;
    class C1,C2,R2,R3 new;
Loading

Trade-offs

  • Chose the minimal safe key set: update channel plus installer target directory. Mirror URLs remain env-only because persisting them would durably redirect binary downloads.
  • Added prerelease channel support because an update channel needs observable behavior; stable remains the default.
  • Did not migrate registry tokens or credentials; those stay on existing auth resolution paths.
  • Unsupported self-update.* keys fail closed instead of being stored as raw config.

Benefits

  1. Users can persist self-update channel and install directory once instead of re-exporting safe env vars every shell session.
  2. Env vars still override config for one-off invocations and CI.
  3. Negative tests protect the supply-chain trust boundary from future schema widening.
  4. Docs now state exactly which self-update keys persist and which values must remain env/auth-only.

Validation

Command output
$ uv run --extra dev pytest tests/unit/test_config.py tests/unit/test_config_command.py tests/unit/test_self_update_config.py -q
199 passed in 0.71s

$ uv run --extra dev pytest tests/unit -q --tb=short --disable-warnings | tail -n 3
........................................................................ [ 99%]
...................                                                      [100%]
17624 passed, 2 skipped, 21 xfailed, 19 warnings, 84 subtests passed in 182.79s (0:03:02)

$ uv run --extra dev pytest tests/ -k "config or update" -q --tb=short --disable-warnings | tail -n 3
ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss [ 99%]
s                                                                        [100%]
2161 skipped, 26745 deselected in 15.20s

$ uv run --extra dev ruff check src/ tests/ && uv run --extra dev ruff format --check src/ tests/ && uv run --extra dev python -m pylint --disable=all --enable=R0801 --min-similarity-lines=10 --fail-on=R0801 src/apm_cli/ && bash scripts/lint-auth-signals.sh
All checks passed!
1316 files already formatted

--------------------------------------------------------------------
Your code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)

[*] Rule A: get_bearer_provider boundary (any reference)
[*] Rule B: git ls-remote auth-delegated annotation
[+] auth-signal lint clean

Scenario evidence

Scenario Test evidence Principle
apm config can set/get allowed installer keys tests/unit/test_config.py, tests/unit/test_config_command.py Safe user defaults
apm self-update reads persisted prefs tests/unit/test_self_update_config.py::test_update_installer_env_reads_non_secret_config Persisted preference reuse
Env vars outrank config tests/unit/test_self_update_config.py::test_update_installer_env_preserves_invocation_env Invocation-scoped override
Secret/executable-like keys are rejected tests/unit/test_config_command.py::test_set_rejects_secret_or_executable_self_update_keys Supply-chain trust boundary

How to test

  • Run apm config set self-update.channel prerelease and apm config get self-update.channel.
  • Run apm config set self-update.install-dir ~/.local/bin and apm config get self-update.install-dir.
  • Run apm config set self-update.token secret and confirm it exits non-zero.
  • Run apm self-update --check with and without APM_SELF_UPDATE_CHANNEL to verify env override behavior.

Closes #667

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Persist bounded non-secret self-update installer preferences through apm config: the update channel and installer target directory. Self-update now reads those preferences while keeping credentials, tokens, mirror URLs, and executable settings out of persisted self-update config.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 25, 2026 22:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a narrowly scoped, non-secret persistence mechanism for apm self-update installer defaults via apm config, plus prerelease channel support in the GitHub version lookup, and updates docs/tests to match the new behavior.

Changes:

  • Introduces persisted config keys self-update.channel and self-update.install-dir, with validation and unset support.
  • Updates apm self-update to apply config-backed installer environment defaults when invocation env vars are absent, and adds prerelease lookup via the GitHub releases list endpoint.
  • Adds unit tests and updates CLI documentation + apm-usage resources to document precedence and the “non-secret only” trust boundary.
Show a summary per file
File Description
tests/unit/test_self_update_config.py New tests covering env-vs-config precedence and prerelease release-list lookup.
tests/unit/test_config.py Adds roundtrip tests for the new self-update config helpers.
tests/unit/test_config_command.py Adds CLI routing tests for get/set/unset and rejects unsupported self-update.* keys on set.
src/apm_cli/utils/version_checker.py Adds prerelease-capable lookup and shared tag normalization.
src/apm_cli/config.py Adds typed config helpers for self_update_channel and self_update_install_dir.
src/apm_cli/commands/self_update.py Reads effective channel + config-backed installer env defaults for self-update.
src/apm_cli/commands/config.py Routes new keys and adds fail-closed behavior for unsupported self-update.* keys on set.
packages/apm-guide/.apm/skills/apm-usage/governance.md Documents the user-config trust boundary and what must not be persisted.
packages/apm-guide/.apm/skills/apm-usage/commands.md Updates command reference to include the new config keys and precedence.
docs/src/content/docs/reference/cli/self-update.md Documents the new keys, precedence, and non-persistence of secrets/mirrors.
docs/src/content/docs/reference/cli/config.md Adds the new config keys and their resolution order to the config reference.

Copilot's findings

  • Files reviewed: 11/11 changed files
  • Comments generated: 3

Comment on lines +407 to +411
if key.startswith("self-update."):
logger.error(
"self-update config only supports non-secret installer preferences: "
"self-update.channel, self-update.install-dir"
)
if _channel != "stable":
logger.progress(f"Self-update channel: {_channel}")

_pinned = os.environ.get("VERSION", "")
Comment on lines +192 to +193
tag_name = data.get("tag_name", "")
return _normalize_release_tag(tag_name)
danielmeppiel and others added 4 commits June 26, 2026 00:57
Bounds the prerelease release-list fetch, suppresses the default self-update channel in config summaries, improves the prerelease miss message, and adds regression coverage for malformed install-dir values. Addresses apm-review-panel follow-ups for PR #1915.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Suppresses unset self-update config defaults consistently, makes the prerelease recovery command copy-safe, and closes the documentation loop for reverting persisted self-update preferences. Addresses the final panel output-UX and docs follow-ups.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel

Copy link
Copy Markdown
Collaborator Author

APM Review Panel: ship_now

PR #1915 safely persists exactly two non-secret self-update preferences while preserving the token, mirror, and executable trust boundary.

cc @danielmeppiel @sergio-sisternes-epam -- a fresh advisory pass is ready for your review.

Panel signals converged: the implementation keeps self-update.channel and self-update.install-dir as the only persisted self-update keys, rejects unsupported self-update.* inputs, and leaves credentials, registry tokens, mirror URLs, commands, and args on existing auth/env paths. Follow-up passes focused on small hardening and UX polish; those items were folded before this comment.

Aligned with: secure-by-default: secrets and executable controls stay out of persisted config; governed-by-policy: self-update config is allowlisted and tested; pragmatic-as-npm: prerelease opt-in is a one-time config preference.

Panel summary

Persona B R N Takeaway
Python Architect 0 0 1 Clean config triplet extension; future dispatch-table refactor can wait until more self-update keys exist.
CLI Logging Expert 0 0 0 Default and unset self-update values are now suppressed consistently in config summaries.
DevX UX Expert 0 0 0 Prerelease miss path now tells users how to return to stable explicitly.
Supply Chain Security Expert 0 0 0 Path validation and tests now guard malformed install-dir values without widening persisted config.
OSS Growth Hacker 0 0 0 Docs now include the set, use, and unset loop for self-update preferences.
Auth Expert 0 0 0 Auth surface remains untouched; no persisted credentials or token paths were introduced.
Doc Writer 0 0 0 Docs match the bounded key set and preserve the no-secrets boundary.
Test Coverage Expert 0 0 0 Targeted tests passed; malformed path guard was mutation-checked.
Performance Expert 0 0 0 Prerelease lookup now bounds GitHub release-list payload with per_page=5.

B = highest-severity findings, R = recommended, N = nits. Counts are signal strength, not gates. The maintainer ships.

Architecture

flowchart TD
    A["apm config set self-update.channel prerelease"] --> B["allowlisted config helper"]
    C["apm config set self-update.install-dir PATH"] --> D["path validation + config helper"]
    B --> E["config.json non-secret prefs"]
    D --> E
    F["apm self-update"] --> G{"env var present?"}
    G -->|yes| H["invocation env wins"]
    G -->|no| E
    E --> I["installer env: channel/install-dir only"]
    H --> I
    I --> J["installer subprocess"]
    K["tokens, mirrors, commands, args"] --> L["not persisted"]
Loading

Recommendation

Ready for maintainer review. The panel recommendation is to ship the current head after the required human review gate clears.

Folded in this run

  • (panel) Bound prerelease release-list fetch with per_page=5 -- resolved in 6ee00de7.
  • (panel) Added path validation and malformed install-dir regression coverage -- resolved in 6ee00de7.
  • (panel) Suppressed default self-update.channel from config summaries -- resolved in 6ee00de7.
  • (panel) Added channel-aware prerelease lookup failure guidance -- resolved in 6ee00de7.
  • (panel) Suppressed unset self-update.install-dir from apm config get summaries -- resolved in 22270c9d.
  • (panel) Added self-update docs revert steps and clarified the config example's why -- resolved in 22270c9d.

Copilot signals reviewed

  • No Copilot inline comments were present after either fetch round. Copilot's top-level review was overview-only, with no actionable inline findings to fold.

Trust-surface boundary

No panel recommendation widened persisted config toward credentials, registry tokens, mirror URLs, commands, or args. That expansion remains out of scope for this PR and must continue to resolve through existing auth/env paths.

Regression-trap evidence

  • tests/unit/test_config.py::TestSelfUpdateInstallerConfig::test_install_dir_rejects_malformed_paths -- disabled the malformed path guards; test FAILED as expected; guards restored.

Lint contract

uv run --extra dev ruff check src/ tests/, uv run --extra dev ruff format --check src/ tests/, pylint R0801, and bash scripts/lint-auth-signals.sh all exited 0 before push.

CI

gh pr checks 1915 --repo microsoft/apm --watch observed all required checks green on head 025654194f8bddbd8819cfc3a60fca24162c9766.

Mergeability status

PR head SHA CEO stance iters folds defers Copilot rounds CI mergeable mergeStateStatus notes
#1915 0256541 ship_now 2 6 0 2 green MERGEABLE BLOCKED pending required human review

Convergence

2 outer iteration(s); 2 Copilot round(s). Final panel stance: ship_now. Ready for maintainer review.


Full per-persona findings
  • Python Architect: no remaining in-scope architecture finding; future self-update dispatch-table refactor deferred until the key set grows beyond this PR.
  • CLI Logging Expert: config output polish folded.
  • DevX UX Expert: prerelease recovery wording folded.
  • Supply Chain Security Expert: no persisted secret/executable/mirror expansion; path hardening folded.
  • OSS Growth Hacker: docs loop folded.
  • Auth Expert: no auth bypass or token persistence concern.
  • Doc Writer: docs align with code.
  • Test Coverage Expert: targeted tests passed; mutation-break evidence recorded.
  • Performance Expert: release-list payload bound folded; broader transport optimizations are outside this PR.

This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.

@danielmeppiel
danielmeppiel merged commit 6bd88cc into main Jun 25, 2026
15 checks passed
@danielmeppiel
danielmeppiel deleted the impl/667-installer-config branch June 25, 2026 23:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Persist installer settings via apm config for self-update

2 participants