Skip to content

feat(cli): implement specify self upgrade#2475

Open
chordpli wants to merge 16 commits intogithub:mainfrom
chordpli:002-self-upgrade-apply
Open

feat(cli): implement specify self upgrade#2475
chordpli wants to merge 16 commits intogithub:mainfrom
chordpli:002-self-upgrade-apply

Conversation

@chordpli
Copy link
Copy Markdown
Contributor

@chordpli chordpli commented May 7, 2026

Summary

Replace the v0.7.5 reserved stub from #2316 with an actually working specify self upgrade command. Closes the unresolved portion of #2282.

The CLI classifies the runtime via a 3-tier detection ladder, runs the appropriate installer subprocess, and verifies the result by spawning a fresh specify --version in a child process (the in-process module is still the pre-upgrade build).

Behavior

  • Bare specify self upgrade — executes immediately. No confirmation prompt. Matches the pip install -U / uv tool upgrade / npm update convention.
  • --dry-run — on upgradable paths (uv tool, pipx) prints the preview block (method / current / target / installer argv) and exits 0 without launching any subprocess. On non-upgradable paths (uvx (ephemeral) / source checkout / unsupported) emits the same path-specific guidance as a non-dry-run invocation and exits 0.
  • --tag vX.Y.Z[suffix] — pins a specific release tag. Validated against ^v\d+\.\d+\.\d+(?:[a-z0-9.+\-]*)?$; rejects bare latest, branch names, hash refs.
  • Detectionuv tool / pipx (auto-upgrade), uvx (ephemeral) / source-checkout / unsupported (path-specific guidance + exit 0, no installer launched).
  • Token hygieneGH_TOKEN / GITHUB_TOKEN scrubbed from child-process environments; never appears in installer argv.

Exit codes

Code Meaning
0 Success, no-op-success (already on latest, --dry-run, or non-upgradable path with guidance)
1 Target-tag resolution failure or --tag regex validation failure
2 Verification mismatch (installer exited 0 but child specify --version does not resolve to target)
3 Installer binary not found on PATH
124 Installer subprocess timed out (only when SPECIFY_UPGRADE_TIMEOUT_SECS is set)
other Installer exit code propagated verbatim

Files changed

  • src/specify_cli/__init__.py — Phase 2 helpers (_InstallMethod, _UpgradePlan, _detect_install_method, _assemble_installer_argv, _build_upgrade_plan, _run_installer, _verify_upgrade, _emit_guidance, _emit_failure, _validate_tag, _scrubbed_env, …) + replaced self_upgrade() orchestrator body. Also refreshes the now-stale self group help and self_check() docstring so specify self --help / specify self check --help reflect the current behavior.
  • tests/test_self_upgrade.py (new) — 44 test cases across 23 classes covering detection, argv assembly, dry-run, tag validation, token scrubbing, verification flows.
  • tests/test_upgrade.py — removed TestSelfUpgradeStub (the stub it pinned no longer exists).
  • README.mdGet Started → Install Specify CLI → Option 1 now leads with specify self upgrade as the recommended path; manual --force retained as fallback.
  • docs/upgrade.md — Quick Reference table, Part 1, Verify, Common Scenarios, and Troubleshooting all updated.
  • docs/installation.md — Verification section gets a "Stay current" pointer.

Test plan

  • uvx ruff check src/ — clean
  • uv sync --extra test + uv run pytest — 2828 passed, 34 skipped on Python 3.11 / 3.12 / 3.13
  • tests/test_self_upgrade.py + tests/test_upgrade.py — 88 passed combined
  • Manual smoke (uv-tool installed working tree):
    • specify self upgrade --dry-run → preview, target=v0.8.6 (auto-resolved)
    • specify self upgrade --dry-run --tag v0.8.0+build.42 → build-metadata tag accepted
    • specify self upgrade --dry-run --tag latestBadParameter (regex rejection)
  • End-to-end automatic upgrade (separate session) — pipx-installed 0.7.6.dev0 → 0.8.1 succeeded

Notes for reviewers

  • Bare invocation executes immediately (clarification Q1 → C1; matches pip install -U / uv tool upgrade / npm update).
  • Phase 1's _fetch_latest_release_tag() (added in feat(cli): add specify self check and self upgrade stub #2316) is reused unchanged. No new GitHub HTTP code.
  • All Phase 2 imports are stdlib (re, dataclasses, enum, urllib.parse); PEP 723 deps unchanged.
  • All Phase 2 symbols are _-prefixed; no new public API surface.
Design and ripple review context

This change was developed through the full speckit workflow:
specify -> plan -> tasks -> blueprint -> implement -> ripple-scan -> ripple-resolve.

Design artifacts (spec, plan, blueprint, ripple-report) were kept in specs/002-self-upgrade-apply/ during development and are not part of this PR.

Ripple review surfaced 9 findings. Each was triaged as either resolved in-branch or accepted risk with explicit rationale.

chordpli added 2 commits May 7, 2026 12:02
Replace the v0.7.5 reserved stub from github#2316 with an actually working
self-upgrade command. Detects the install method (uv tool / pipx /
uvx-ephemeral / source-checkout / unsupported) via a 3-tier ladder
(path-prefix match -> editable-install marker -> PATH+registry
reconciliation), runs the appropriate installer subprocess, and
verifies the result by spawning a child `specify --version`.

- Bare `specify self upgrade` executes immediately, matching the
  `pip install -U` / `uv tool upgrade` / `npm update` convention.
- `--dry-run` prints the preview (method, current, target, argv)
  and exits 0 without launching any subprocess.
- `--tag vX.Y.Z[suffix]` pins a specific release tag (validated
  against `^v\d+\.\d+\.\d+(?:[a-z0-9.+\-]*)?$`; rejects bare
  `latest`, branch names, and hash refs).
- Token scrubbing: GH_TOKEN / GITHUB_TOKEN are never forwarded to
  the installer subprocess environment or surfaced in argv.
- Exit codes: 0 (success / no-op-success), 1 (resolution failure),
  2 (verification mismatch), 3 (installer missing), 124 (subprocess
  timeout when SPECIFY_UPGRADE_TIMEOUT_SECS is set), other
  (installer exit propagated verbatim).
- uvx-ephemeral runs and source checkouts emit path-specific
  guidance and exit 0 instead of running an installer.

Documentation: README and docs/upgrade.md now lead with the
self-management commands as the recommended path; manual
`uv tool install --force` / `pipx install --force` remain as
fallback for older installs and explicit-control users.
docs/installation.md adds a "Stay current" pointer.

Closes the unresolved portion of github#2282.
…Phase 2

The `specify self` Typer help and `self_check()` docstring were still describing
the v0.7.5 stub state ("reserved upgrade command", "future release will use for
actual self-upgrade", "behavior is not implemented in this release"). Now that
`specify self upgrade` is a real, working command, these strings contradicted
the implementation when surfaced through `specify self --help` and
`specify self check --help`.

- self_app help: "Manage the specify CLI itself: check for newer releases
  (read-only) and upgrade in place."
- self_check docstring: points readers at `specify self upgrade` and
  `specify self upgrade --dry-run` as the natural follow-ups, instead of
  claiming the upgrade command is unimplemented.

No behavior change.
@chordpli chordpli requested a review from mnriem as a code owner May 7, 2026 05:12
Copilot AI review requested due to automatic review settings May 7, 2026 06:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements a functional specify self upgrade command to replace the previous reserved stub, enabling in-place upgrades (uv tool / pipx) with install-method detection, dry-run previews, tag pinning, token scrubbing, and post-upgrade verification via a child specify --version.

Changes:

  • Adds install-method detection + upgrade orchestration helpers and replaces the self upgrade stub with a working implementation.
  • Introduces a comprehensive new test suite for specify self upgrade and removes the old stub-pin tests.
  • Updates README and docs to recommend specify self upgrade and document the new flows.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/specify_cli/__init__.py Adds self-upgrade planning/execution/verification logic and updates self command help text.
tests/test_self_upgrade.py New test module covering detection, argv assembly, dry-run behavior, tag validation, token scrubbing, and verification.
tests/test_upgrade.py Removes tests that pinned the previous non-functional stub output.
README.md Documents specify self check / specify self upgrade as the preferred upgrade path.
docs/upgrade.md Refreshes upgrade guide to include self-upgrade commands, verification steps, and troubleshooting.
docs/installation.md Adds “Stay current” pointer to specify self check and upgrade guide.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/specify_cli/__init__.py
Comment thread src/specify_cli/__init__.py
Comment thread src/specify_cli/__init__.py
Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Comment thread src/specify_cli/__init__.py
Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py
Comment thread tests/test_self_upgrade.py Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment thread src/specify_cli/__init__.py
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
@chordpli chordpli requested a review from Copilot May 7, 2026 12:04
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py
Comment thread tests/test_self_upgrade.py Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Comment thread src/specify_cli/__init__.py
Comment thread src/specify_cli/__init__.py
Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/__init__.py Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment thread src/specify_cli/__init__.py Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread src/specify_cli/__init__.py
Comment thread src/specify_cli/__init__.py Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment thread src/specify_cli/__init__.py Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 6/6 changed files
  • Comments generated: 2

Comment on lines +1992 to +2011
"""Return whether the installed distribution appears to come from a git checkout."""
import importlib.metadata as _md

editable_root = _editable_direct_url_path()
if editable_root is not None and _git_ancestor(editable_root) is not None:
return True

try:
dist = _md.distribution("specify-cli")
except _md.PackageNotFoundError:
return False
files = dist.files or []
for f in files:
try:
abs_path = Path(dist.locate_file(f)).resolve()
except Exception:
continue
if _git_ancestor(abs_path) is not None:
return True
return False
+build.42). Rejects everything else (including bare 'latest', hash refs,
branch names, or a numeric version without the 'v' prefix).
"""
if not _TAG_REGEX.match(tag):
mnriem

This comment was marked as duplicate.

@mnriem
Copy link
Copy Markdown
Collaborator

mnriem commented May 7, 2026

Please address Copilot feedback and please resolve conflicts. You probably will have to pull in latest changes from main

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.

3 participants