Skip to content

[BUG] apm update repeatedly re-updates git-semver deps already at their locked tag #2163

Description

@srobroek

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

  1. 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
  2. apm install --target claude
  3. apm update --yes --target claudeUpdated N APM dependencies
  4. apm update --yes --target claudeUpdated N APM dependencies (again)
  5. …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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/lockfileLockfile schema, per-file provenance, integrity hashes, drift detection.priority/highShips in current or next milestonestatus/acceptedDirection approved, safe to start work.status/triagedInitial agentic triage complete; pending maintainer ratification (silence = approval).theme/portabilityOne manifest, every target. Multi-target deploy, marketplace, packaging, install.type/bugSomething does not work as documented.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    In Progress

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions