Skip to content

[BUG] Git-semver subdirectory dep: --frozen redeploys the older release, repeat install drops constraint/resolved_tag #3382

Open

Description

Found while writing the real-binary contract for #3224. Reproduces on unmodified main 2920043 with the unmodified binary; the #3224 branch produces byte-for-byte the same lock and deployed state at every step, so it is not caused by that change.

Setup

One local bare repo acme/mono with a subdirectory package packages/pkg-a (its own apm.yml, one skill). Commit 1 is pkg-a 1.0.0, tagged v1.0.0 and pkg-a-v1.0.0. Commit 2 is pkg-a 1.1.0, tagged pkg-a-v1.1.0 and pkg-a--v1.1.0. Consumer declares acme/mono/packages/pkg-a#^1.0.0, target copilot, --no-policy, --parallel-downloads 0, isolated environment with the URL rewritten to the local origin.

What happens

install        constraint='^1.0.0'  resolved_tag='pkg-a-v1.1.0'  resolved_ref='pkg-a-v1.1.0'  version='1.1.0'  commit=<c2>  deployed='# two'
install --frozen
               constraint='^1.0.0'  resolved_tag='pkg-a-v1.1.0'  resolved_ref='pkg-a-v1.1.0'  version='1.1.0'  commit=<c2>  deployed='# one'   <-- content from 1.0.0
install        constraint=None      resolved_tag=None            resolved_ref='^1.0.0'        version='1.1.0'  commit=<c2>  deployed='# two'   <-- raw range as resolved_ref
update --yes   constraint='^1.0.0'  resolved_tag='pkg-a-v1.1.0'  resolved_ref='pkg-a-v1.1.0'  version='1.1.0'  commit=<c2>  deployed='# two'
install --frozen
               ... same lock ...                                                                                            deployed='# one'

Every command exits 0. apm audit --ci after the frozen replay reports content-integrity hash drift on the deployed skill, which is correct: the lock says 1.1.0 and the file is the 1.0.0 one.

Two defects, probably one cause

  1. Frozen replay deploys the wrong release. The lock is right, the file is from the other commit. Since both tags for pkg-a 1.1.0 and the root v1.0.0 tag exist, it looks like the frozen path materialises the subdirectory from a cached checkout of the wrong ref rather than from resolved_commit.
  2. A repeat ordinary install loses the semver provenance: constraint and resolved_tag are dropped and resolved_ref becomes the literal string ^1.0.0. update writes them back. This is the shape [BUG] apm install records a stale dependency version in apm.lock.yaml when apm_modules/ holds the previous version #3246 / fix(install): capture acquired manifest versions on first install #3311 were about, from the other direction: the metadata on a replay no longer describes the snapshot that was installed.

Repro

Probe script, run from a checkout with the dev environment (it uses the repo's own test utilities). Pass the apm binary and a scratch directory:

update_probe.py
"""install -> reinstall -> update on a git-semver subdirectory dep, --no-policy; print lock fields per step.
usage: python update_probe.py <apm-binary> <scratch-root>"""
import os, sys, yaml
from pathlib import Path
sys.path.insert(0, "<checkout>")  # harness from the working tree
from tests.utils.apm_lifecycle_runner import ApmLifecycleRunner
from tests.utils.isolated_apm_environment import IsolatedApmEnvironment
from tests.utils.local_git_repository import LocalGitRepositoryFactory
from tests.utils.local_package import LocalPackageFactory
from apm_cli.utils.yaml_io import dump_yaml

binary, root = Path(sys.argv[1]), Path(sys.argv[2])
iso = IsolatedApmEnvironment.create(root / "iso", base_env=dict(os.environ)); env = iso.subprocess_env()
pk = LocalPackageFactory(iso.package_root); b = pk.create("mono", targets=("copilot",))
def sub(r, v, m):
    p = r / "packages" / "pkg-a"; (p/".apm"/"skills"/"s").mkdir(parents=True, exist_ok=True)
    dump_yaml({"name":"pkg-a","version":v,"targets":["copilot"]}, p/"apm.yml"); (p/".apm"/"skills"/"s"/"SKILL.md").write_text(f"---\nname: s\ndescription: d\n---\n# {m}\n")
sub(b.root, "1.0.0", "one")
repos = LocalGitRepositoryFactory(iso.repository_root, env=env); repo = repos.create("mono", source_tree=b.root)
c1 = repos.commit(repo, message="1.0.0"); repos.tag(repo, "v1.0.0", c1); repos.tag(repo, "pkg-a-v1.0.0", c1)
sub(repo.worktree, "1.1.0", "two"); c2 = repos.commit(repo, message="1.1.0"); repos.tag(repo, "pkg-a-v1.1.0", c2); repos.tag(repo, "pkg-a--v1.1.0", c2)
env = repos.url_rewrite_subprocess_env(repo, "https://github.com/acme/mono.git")
proj = LocalPackageFactory(iso.work_root).create("consumer", dependencies=("acme/mono/packages/pkg-a#^1.0.0",), targets=("copilot",)).root
r = ApmLifecycleRunner((str(binary),), timeout_seconds=120, scenario_timeout_seconds=600)
print("  c1(1.0.0)", c1.sha[:7], " c2(1.1.0)", c2.sha[:7])
def lock(tag):
    d = yaml.safe_load(open(proj/"apm.lock.yaml"))["dependencies"][0]
    marker = (proj/'.agents/skills/s/SKILL.md').read_text().strip().splitlines()[-1]
    print(f"  [{tag:15}] constraint={d.get('constraint')!r:9} resolved_tag={d.get('resolved_tag')!r:15} resolved_ref={d.get('resolved_ref')!r:15} version={d.get('version')!r:8} commit={str(d.get('resolved_commit'))[:7]} deployed={marker!r}")
common = ("--target","copilot","--no-policy","--parallel-downloads","0")
for tag, args in (("install", ("install",*common)), ("frozen", ("install",*common,"--frozen")), ("reinstall", ("install",*common)), ("update", ("update","--yes","--target","copilot","--parallel-downloads","0")), ("frozen-again", ("install",*common,"--frozen"))):
    res = r.run(args, scenario_id=tag, cwd=proj, env=env)
    print(f"  {tag} exit {res.returncode}" + ("" if res.returncode == 0 else "  :: " + (res.stdout+res.stderr).strip().splitlines()[-1][:140]))
    lock(tag)

Happy to turn this into an APMLifecycle contract if it's accepted; I kept it out of #3224's PR because it isn't a policy problem.

Activity

  1. dajiaohuang commented on Oct 25, 2026

    @dajiaohuang
    Contributor

    I reviewed current main at 2920043 without modifying or checking out the repository. The closest existing test, TestPositionalVirtualSubdirectorySemver.test_positional_git_semver_uses_real_bare_remote_and_replays_lockfile, uses a different tag arrangement; it checks the initial semver lock fields and byte-identical lock after a subsequent ordinary install, but does not run --frozen or assert the deployed file contents after replay:

    @pytest.mark.parametrize(
    ("constraint", "expected_tag", "expected_version"),
    [
    (">=1.0.0", "pkg-v1.2.0", "1.2.0"),
    ("~1.0.0", "pkg-v1.0.0", "1.0.0"),
    ("^1.0.0", "pkg-v1.2.0", "1.2.0"),
    ],
    )
    def test_positional_git_semver_uses_real_bare_remote_and_replays_lockfile(
    self,
    tmp_path: Path,
    constraint: str,
    expected_tag: str,
    expected_version: str,
    ) -> None:
    """Positional virtual ranges resolve a local bare remote before preflight."""
    isolated = IsolatedApmEnvironment.create(tmp_path / "isolated", base_env=dict(os.environ))
    environment = isolated.subprocess_env()
    source = isolated.package_root / "mono"
    package = source / "packages" / "pkg"
    package.mkdir(parents=True)
    (package / "apm.yml").write_text(
    "name: pkg\nversion: 1.0.0\ndescription: fixture package\n",
    encoding="utf-8",
    )
    instructions = package / ".apm" / "instructions"
    instructions.mkdir(parents=True)
    (instructions / "fixture.instructions.md").write_text(
    "# Fixture\n",
    encoding="utf-8",
    )
    repositories = LocalGitRepositoryFactory(isolated.repository_root, env=environment)
    repository = repositories.create("mono", source_tree=source)
    first_commit = repositories.commit(repository, message="pkg v1.0.0")
    repositories.tag(repository, "pkg-v1.0.0", first_commit)
    (repository.worktree / "packages" / "pkg" / "README.md").write_text(
    "# pkg 1.2.0\n",
    encoding="utf-8",
    )
    second_commit = repositories.commit(repository, message="pkg v1.2.0")
    repositories.tag(repository, "pkg-v1.2.0", second_commit)
    child_env = _source_cli_environment(
    repositories.url_rewrite_subprocess_env(
    repository,
    "https://github.com/acme/mono.git",
    )
    )
    trace_path = isolated.work_root / "git-trace.log"
    child_env["GIT_TRACE"] = str(trace_path)
    project = isolated.work_root / "consumer"
    _write_apm_yml(project, [])
    raw_reference = f"acme/mono/packages/pkg#{constraint}"
    command = _source_cli_command(
    "install",
    "--no-policy",
    "--https",
    "--parallel-downloads",
    "0",
    raw_reference,
    )
    first = subprocess.run(
    command,
    cwd=project,
    env=child_env,
    capture_output=True,
    text=True,
    timeout=120,
    )
    assert first.returncode == 0, f"stdout={first.stdout!r}\nstderr={first.stderr!r}"
    assert raw_reference in first.stdout
    trace = trace_path.read_text(encoding="utf-8")
    assert "git ls-remote --tags --heads" in trace
    parsed_trace_urls = [
    urlparse(token.strip("'\"")) for token in trace.split() if "://" in token
    ]
    assert ("https", "github.com", "/acme/mono.git") in [
    (parsed.scheme, parsed.hostname, parsed.path) for parsed in parsed_trace_urls
    ]
    lock_path = project / "apm.lock.yaml"
    first_lock = lock_path.read_bytes()
    locked = _find_locked(_read_lockfile(project), "acme/mono")
    assert locked is not None
    assert locked["constraint"] == constraint
    assert locked["resolved_tag"] == expected_tag
    assert locked["resolved_commit"] == (
    first_commit.sha if expected_tag == "pkg-v1.0.0" else second_commit.sha
    )
    assert locked["version"] == expected_version
    installed = project / ".github" / "instructions" / "fixture.instructions.md"
    assert installed.exists(), "the resolved virtual subdirectory was not installed"
    second = subprocess.run(
    command[:-1],
    cwd=project,
    env=child_env,
    capture_output=True,
    text=True,
    timeout=120,
    )
    assert second.returncode == 0, f"stdout={second.stdout!r}\nstderr={second.stderr!r}"
    assert lock_path.read_bytes() == first_lock
    @pytest.mark.parametrize(
    .

    PR #3381 also says it observed these exact frozen/reinstall behaviors on unmodified main and kept them out of #3224's accepted scope; that PR is therefore not a fix for this issue: #3381.

    I did not rerun the provided reproduction, so this is a source/test-coverage observation, not independent runtime validation or a root-cause claim. I found no human scope-approval comment on #3382; under current GOVERNANCE.md/CONTRIBUTING.md, I have not started an implementation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions