Skip to content

fix(deps): git-semver update idempotency in build_update_plan#2165

Open
srobroek wants to merge 1 commit into
microsoft:mainfrom
srobroek:fix/git-semver-update-idempotency
Open

fix(deps): git-semver update idempotency in build_update_plan#2165
srobroek wants to merge 1 commit into
microsoft:mainfrom
srobroek:fix/git-semver-update-idempotency

Conversation

@srobroek

Copy link
Copy Markdown
Contributor

Summary

apm update (and 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 produces the same "N updated" line indefinitely; the lockfile is rewritten with the identical commit SHA each time (only generated_at / resolved_at timestamps change), and no deployed files change on disk.

This is the git-source counterpart of the registry idempotency bug fixed for registry deps in #1908 — that fix did not cover git-semver deps.

Root cause

On apm update, the git-semver resolver (_maybe_resolve_git_semver in install/phases/resolve.py) rewrites dep.reference to the concrete tag and computes its SHA (GitSemverResolution.resolved_sha). But the SHA is stashed in ctx.git_semver_resolutions and the download result — it is never attached back to dep.resolved_reference.

build_update_plan (called from install/pipeline.py right after resolve, 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)   ->  UPDATE

even though old_ref == new_ref (same concrete tag). Verified by instrumenting build_update_plan during a live update:

PLAN-TIME DEP STATE
  key=srobroek/agentic-packages/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'

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 is set), and the ref is unchanged, borrow the locked commit so the comparison correctly yields unchanged.

Scoped deliberately to tags: a branch tip can advance under a stable ref name, so branch deps (no resolved_tag) must still surface real updates. The existing registry fallback immediately above is left untouched.

if (
    new_commit is None
    and old is not None
    and getattr(old, "resolved_tag", None)
    and new_ref == old.resolved_ref
):
    new_commit = old.resolved_commit

Tests

  • test_git_semver_tag_dep_unchanged_when_cached_and_ref_matches — the reproduction; fails before the fix, passes after.
  • test_git_branch_dep_tip_advance_still_shows_update — guards that a real branch-tip advance is not masked.
  • Full tests/unit/install/ and tests/unit/deps/ suites green post-rebase onto current main.

End-to-end verification

Against a real git-semver dep (srobroek/agentic-packages/packages/eli5#>=2.0.0 <3.0.0), built from this branch:

  • Before: every apm updateUpdated N APM dependencies (lockfile diff = timestamps only).
  • After: first update annotates the lockfile once; run 2 and run 3 → All dependencies already at their latest matching refs. — converged.

Closes #2163

`apm update` reported a spurious UPDATE on every run for git-source semver
dependencies already at their locked tag, and never converged: the lockfile
was rewritten with the identical SHA each time (only timestamps changed).

Root cause: the git-semver resolver rewrites `dep.reference` to the concrete
tag and computes its SHA, but stashes the result in
`ctx.git_semver_resolutions` (and the download result) without attaching it
back to `dep.resolved_reference`. At plan time `resolved_reference` is None, so
`build_update_plan` compares the locked commit against `None` and classifies
every git-semver dep as `update`.

Fix mirrors the registry idempotency fix in microsoft#1908, applied to the git-source
path: 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 sees `unchanged`. Scoped to tags -- a branch tip can
advance under a stable ref name, so branch deps (no `resolved_tag`) still
surface real updates.

Adds two regression tests: cached git-semver tag stays `unchanged`; branch-tip
advance still shows `update`.
Copilot AI review requested due to automatic review settings July 12, 2026 08:03

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

Fixes apm update idempotency for git-source semver dependencies that are already locked to an immutable tag, preventing repeated “update” results when nothing has changed.

Changes:

  • Adjusts build_update_plan to treat cached git-semver tag deps as unchanged by borrowing the locked commit when the resolved ref is unchanged and the lockfile pins a resolved_tag.
  • Adds regression tests covering (1) the cached git-semver tag unchanged case and (2) ensuring branch-tip advances still report updates.
  • Adds an Unreleased changelog entry describing the fix.

Reviewed changes

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

File Description
src/apm_cli/install/plan.py Adds a locked-commit fallback for cached git-semver tag deps to prevent spurious updates.
tests/unit/install/test_plan.py Adds unit tests to reproduce the bug and guard against masking branch updates.
CHANGELOG.md Documents the fix under Unreleased.

Comment thread CHANGELOG.md
Comment on lines +12 to +19
- `apm update` no longer reports a spurious update on every run for git-source
semver dependencies already at their locked tag. The git-semver resolver
rewrites the dep reference to the concrete tag but does not attach the
resolved SHA to `resolved_reference`, so the update plan compared the locked
commit against `None` and never converged. `build_update_plan` now borrows the
locked commit for cached, immutable-tag deps whose ref is unchanged, mirroring
the registry fix in #1908 (branch deps are unaffected -- their tips can still
advance under a stable ref name). (closes microsoft/apm#2163)
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.

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

3 participants