Describe the bug
apm update (including apm update -g) reports a spurious UPDATE on every run for git-source semver dependencies that are already at their locked tag, and never converges. Re-running yields the same "N updated" line indefinitely; the lockfile is rewritten with the identical commit SHA each time — only generated_at / resolved_at timestamps change. No deployed files change on disk, so the effect is cosmetic but persistent and undermines trust in apm update output (you can't tell a real update from noise).
This is the git-source counterpart of the registry idempotency bug fixed in #1908 — that fix corrected the registry path but not git-semver deps.
To Reproduce
apm.yml:
name: repro
version: 0.1.0
targets: [claude]
dependencies:
apm:
- owner/repo/packages/some-pkg#>=2.0.0 <3.0.0 # any git-source semver range
apm install --target claude
apm update --yes --target claude → Updated N APM dependencies
apm update --yes --target claude → Updated N APM dependencies (again)
- …repeats forever.
git diff apm.lock.yaml between two consecutive updates shows only generated_at / resolved_at timestamp lines — zero substantive change.
Expected behavior
After the first apm update, subsequent runs should report "All dependencies already at their latest matching refs." — as already happens for git deps pinned to concrete SHAs, and for registry deps since #1908.
Environment (please complete the following information):
- OS: macOS
- Python Version: 3.14.4
- APM Version: 0.24.0 and 0.24.1 (reproduces on both;
install/plan.py is byte-identical between them)
- Also reproduces on both project-scope and
--global, and on a fresh isolated project (not specific to any one layer).
Root cause
On apm update, the git-semver resolver (_maybe_resolve_git_semver, install/phases/resolve.py) rewrites dep.reference to the concrete tag and computes its SHA (GitSemverResolution.resolved_sha), but stashes the result in ctx.git_semver_resolutions / the download result without attaching it back to dep.resolved_reference.
build_update_plan (install/pipeline.py, invoked right after resolve and before download) reads dep.resolved_reference, which is None at that point, so _extract_new_ref_and_commit returns the correct concrete tag for the ref but None for the commit. The classifier then compares old_commit (real locked SHA) vs new_commit (None) and emits update, even though old_ref == new_ref.
Instrumented plan-time state:
PLAN-TIME DEP STATE
key=.../packages/eli5 reference='eli5--v2.0.0' resolved_reference=None
PLAN ENTRIES
update: ...eli5 old_commit='69148d93...' new_commit=None old_ref='eli5--v2.0.0' new_ref='eli5--v2.0.0'
Proposed fix
Mirror #1908's registry approach on the git-source path: in build_update_plan, when a dep has no freshly-resolved commit, the locked entry pins an immutable tag (resolved_tag set), and the ref is unchanged, borrow the locked commit so the comparison yields unchanged. Scoped to tags so branch-tip advances still surface as real updates.
I have a candidate patch with a failing-then-passing regression test plus a branch-tip guard (112 relevant tests green) and end-to-end verification that apm update converges after the first run. Happy to open a PR against microsoft/apm if the approach looks right — draft on my fork: srobroek#1
Describe the bug
apm update(includingapm update -g) reports a spurious UPDATE on every run for git-source semver dependencies that are already at their locked tag, and never converges. Re-running yields the same "N updated" line indefinitely; the lockfile is rewritten with the identical commit SHA each time — onlygenerated_at/resolved_attimestamps change. No deployed files change on disk, so the effect is cosmetic but persistent and undermines trust inapm updateoutput (you can't tell a real update from noise).This is the git-source counterpart of the registry idempotency bug fixed in #1908 — that fix corrected the registry path but not git-semver deps.
To Reproduce
apm.yml:apm install --target claudeapm update --yes --target claude→Updated N APM dependenciesapm update --yes --target claude→Updated N APM dependencies(again)git diff apm.lock.yamlbetween two consecutive updates shows onlygenerated_at/resolved_attimestamp lines — zero substantive change.Expected behavior
After the first
apm update, subsequent runs should report "All dependencies already at their latest matching refs." — as already happens for git deps pinned to concrete SHAs, and for registry deps since #1908.Environment (please complete the following information):
install/plan.pyis byte-identical between them)--global, and on a fresh isolated project (not specific to any one layer).Root cause
On
apm update, the git-semver resolver (_maybe_resolve_git_semver,install/phases/resolve.py) rewritesdep.referenceto the concrete tag and computes its SHA (GitSemverResolution.resolved_sha), but stashes the result inctx.git_semver_resolutions/ the download result without attaching it back todep.resolved_reference.build_update_plan(install/pipeline.py, invoked right after resolve and before download) readsdep.resolved_reference, which isNoneat that point, so_extract_new_ref_and_commitreturns the correct concrete tag for the ref butNonefor the commit. The classifier then comparesold_commit(real locked SHA) vsnew_commit(None) and emitsupdate, even thoughold_ref == new_ref.Instrumented plan-time state:
Proposed fix
Mirror #1908's registry approach on the git-source path: in
build_update_plan, when a dep has no freshly-resolved commit, the locked entry pins an immutable tag (resolved_tagset), and the ref is unchanged, borrow the locked commit so the comparison yieldsunchanged. Scoped to tags so branch-tip advances still surface as real updates.I have a candidate patch with a failing-then-passing regression test plus a branch-tip guard (112 relevant tests green) and end-to-end verification that
apm updateconverges after the first run. Happy to open a PR againstmicrosoft/apmif the approach looks right — draft on my fork: srobroek#1