Skip to content

feat(deps): update revision pins#1738

Open
danielmeppiel wants to merge 3 commits into
mainfrom
danielmeppiel/1209-update-revision-pins
Open

feat(deps): update revision pins#1738
danielmeppiel wants to merge 3 commits into
mainfrom
danielmeppiel/1209-update-revision-pins

Conversation

@danielmeppiel

Copy link
Copy Markdown
Collaborator

feat(deps): update revision pins

TL;DR

apm update now has an additive SHA-pin path: it resolves full 40-character revision pins to the latest annotated semver tag SHA, rewrites apm.yml with a # <tag> annotation, and then runs the existing install/update pipeline. apm outdated now reports SHA-pinned deps against that same annotated-tag target instead of treating them as unknown branch/commit refs. The capability-surface behavior-diff idea from the issue discussion is deliberately out of scope for this PR.

Note

Closes #1209. The board-approved fences are preserved: authoritative upstream ref fetch, branch/lightweight-tag rejection, atomic manifest writes, and no behavior-diff tooling.

Problem (WHY)

Approach (WHAT)

# Fix Guardrail
1 Add a revision-pin helper module that finds the highest stable annotated semver tag from git ls-remote refs. Reject branch refs and lightweight tags; skip prereleases by default.
2 Run that helper before the normal apm update install path only when direct manifest deps contain full-SHA refs. Strictly additive: non-SHA update flows continue through the existing plan callback.
3 Rewrite apm.yml through a sibling temp file and os.replace, then reload the manifest before install. Old pin survives failed writes; --dry-run never writes.
4 Record the resolved tag in apm.lock.yaml after the install pipeline writes the new SHA. The lockfile can explain which tag the SHA represents.
5 Teach apm outdated to compare full-SHA pins against the same annotated-tag candidate. Read-only reporting path; local, registry, and Artifactory skips stay unchanged.

Implementation (HOW)

File Intent
src/apm_cli/deps/revision_pins.py New pure helper surface for detecting full-SHA pins, selecting the latest annotated stable semver tag, rendering the revision-pin plan, and atomically rewriting manifest lines with # <tag>.
src/apm_cli/commands/update.py Adds the pre-update revision-pin pass, consent/non-TTY handling, dry-run preview, manifest reload, and post-install lockfile tag annotation. Existing update plan/install behavior remains the path for non-SHA deps.
src/apm_cli/commands/outdated.py Adds a read-only full-SHA branch that reports latest annotated-tag SHA candidates, while preserving existing tag, branch, marketplace, and registry checks.
src/apm_cli/deps/git_remote_ops.py + src/apm_cli/models/dependency/types.py Carries an annotated bit on remote tag refs by detecting peeled refs/tags/<tag>^{} lines from git ls-remote.
src/apm_cli/utils/yaml_io.py Adds a shared sibling-file atomic text replace helper for YAML-adjacent writes.
src/apm_cli/install/phases/lockfile.py Preserves existing resolved_tag metadata for unchanged SHA pins during normal lockfile regeneration.
tests/unit/deps/test_revision_pin_resolver.py Covers annotated-tag detection, branch/lightweight rejection, stable-over-prerelease selection, atomic manifest rewrite, and failed-write rollback.
tests/unit/commands/test_update_command.py Covers dry-run no-write behavior, --yes manifest rewrite, non-TTY abort, and lockfile tag annotation failure checks.
tests/unit/test_outdated_command.py Covers SHA pins reporting against annotated tags and refusing branch-only replacements.
Docs + packages/apm-guide/.apm/skills/apm-usage/* + CHANGELOG.md Documents the new update/outdated SHA-pin behavior and security model without editing README.

Diagrams

Legend: the dashed nodes are the new revision-pin path layered ahead of the existing update pipeline.

flowchart LR
    subgraph Detect[Detect]
        A["apm.yml full SHA pin"] --> B["git ls-remote upstream refs"]
        B --> C{"stable annotated semver tag?"}
    end
    subgraph Apply[Apply]
        C -->|"yes"| D["atomic apm.yml rewrite with tag comment"]
        C -->|"no"| E["fail closed"]
        D --> F["existing install update pipeline"]
        F --> G["record resolved_tag in lockfile"]
    end
    classDef new stroke-dasharray: 5 5;
    class B,C,D,G new;
Loading

Trade-offs

  • Annotated tags only. Chose fail-closed annotated-tag verification; rejected branch or lightweight-tag targets because they do not satisfy the board's anti-spoofing fence.
  • Stable releases only. Chose to skip prerelease tags by default to match existing git-semver resolver behavior; rejected implicit rc upgrades for SHA pins.
  • Line-preserving manifest rewrite. Chose a surgical text rewrite over re-serializing YAML so user formatting survives; rejected broad YAML dumping because it would churn apm.yml diffs.
  • No capability-surface diff. Left the behavior-diff review tool out of scope exactly as the board decision required.

Benefits

  1. SHA-pinned direct deps now produce an actionable apm update --dry-run plan instead of staying stale silently.
  2. apm.yml diffs show both the immutable SHA and the human-readable tag (# vX.Y.Z) for review.
  3. apm outdated reports full-SHA pins using the same annotated-tag rule as update, so preview and apply agree.
  4. Failed manifest writes leave the old SHA pin untouched; dry-run and non-TTY aborts do not mutate files.
  5. Existing non-SHA update, registry, marketplace, local, and Artifactory flows remain unchanged.

Validation

uv run --extra dev pytest tests/unit/deps/test_revision_pin_resolver.py tests/unit/test_outdated_command.py tests/unit/commands/test_update_command.py -q:

..................................................................................                               [100%]
82 passed in 2.53s
Full validation commands

uv run --extra dev pytest tests/unit tests/test_console.py -q:

completed with exit code 0

uv run --extra dev ruff check src/ tests/ && uv run --extra dev ruff format --check src/ tests/ && ... && bash scripts/lint-auth-signals.sh:

All checks passed!
1239 files already formatted

--------------------------------------------------------------------
Your code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)

[*] Rule A: get_bearer_provider boundary (any reference)
[*] Rule B: git ls-remote auth-delegated annotation
[+] auth-signal lint clean

Scenario Evidence

# Scenario (user promise) Principle(s) Test(s) proving it Type
1 apm update --dry-run previews a SHA/tag rewrite without changing apm.yml. Secure by default, DevX tests/unit/commands/test_update_command.py::TestUpdateDryRun::test_dry_run_renders_revision_pin_plan_without_writing_manifest unit
2 apm update --yes rewrites the SHA pin, annotates it with the tag, and records the tag in the lockfile. Governed by policy, DevX tests/unit/commands/test_update_command.py::TestUpdateAssumeYes::test_yes_updates_revision_pin_manifest_before_install
tests/unit/commands/test_update_command.py::TestRevisionPinLockfileAnnotation::test_annotates_lockfile_with_resolved_tag
unit
3 Branches, lightweight tags, and prerelease tags cannot replace a SHA pin. Secure by default, Governed by policy tests/unit/deps/test_revision_pin_resolver.py::test_latest_revision_pin_tag_ignores_branches_and_lightweight_tags
tests/unit/deps/test_revision_pin_resolver.py::test_latest_revision_pin_tag_ignores_prereleases_by_default
unit
4 A failed manifest write preserves the old SHA pin. Secure by default tests/unit/deps/test_revision_pin_resolver.py::test_apply_revision_pin_updates_keeps_old_pin_when_replace_fails unit
5 apm outdated reports SHA-pinned deps against the latest annotated release tag. DevX, Governed by policy tests/unit/test_outdated_command.py::TestOutdatedCommand::test_commit_ref_reports_latest_annotated_tag unit

How to test

  • Create an apm.yml dependency pinned to a full 40-character SHA from an older annotated release tag; run apm update --dry-run; expect a revision-pin plan and no file changes.
  • Re-run with apm update --yes; expect the manifest entry to change to the latest annotated tag's SHA with # <tag> appended.
  • Inspect apm.lock.yaml; expect resolved_commit to match the new SHA and resolved_tag to match the annotation.
  • Run apm outdated; expect the same latest tag/SHA target in the table.
  • Try a repo whose only newer ref is a branch or lightweight tag; expect the SHA pin path to fail closed instead of updating.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

danielmeppiel and others added 2 commits June 11, 2026 00:29
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 10, 2026 22:37

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

Adds first-class handling for full 40-character SHA revision pins in the dependency workflow so that apm update / apm outdated can treat them as updateable (by comparing against the commit behind the latest annotated stable semver tag) while preserving the “no branch / no lightweight tag” fence.

Changes:

  • Introduces a revision-pin resolver + manifest rewriter that maps full SHA pins to the latest annotated semver tag’s commit and annotates apm.yml lines with # <tag>.
  • Extends git remote ref parsing to distinguish annotated vs lightweight tags (via peeled ^{} refs) and uses that in both update and outdated.
  • Updates lockfile regeneration to preserve existing resolved_tag metadata for unchanged SHA pins, plus adds unit coverage and docs updates.
Show a summary per file
File Description
src/apm_cli/deps/revision_pins.py New helper module for detecting full-SHA pins, selecting latest annotated stable semver tag, rendering a plan, and rewriting apm.yml atomically.
src/apm_cli/commands/update.py Adds pre-update revision-pin resolution / consent flow, applies manifest rewrite, and annotates lockfile with resolved_tag.
src/apm_cli/commands/outdated.py Adds SHA-pin check path to compare pinned SHA against latest annotated semver tag commit.
src/apm_cli/deps/git_remote_ops.py Detects annotated tags by tracking peeled refs/tags/<tag>^{} output and sets RemoteRef.annotated.
src/apm_cli/models/dependency/types.py Extends RemoteRef with an annotated: bool = False field.
src/apm_cli/utils/yaml_io.py Adds a sibling-temp-file + os.replace helper for atomic text replacement.
src/apm_cli/install/phases/lockfile.py Preserves resolved_tag across installs when SHA pins are unchanged.
tests/unit/deps/test_revision_pin_resolver.py New unit tests for peeled-tag detection, annotated-only selection, prerelease skipping, and atomic manifest rewrite rollback.
tests/unit/commands/test_update_command.py Adds unit tests for revision-pin dry-run behavior, --yes apply path, non-TTY abort, and lockfile tag annotation.
tests/unit/test_outdated_command.py Adds unit coverage for SHA pins reporting against latest annotated tag candidates.
packages/apm-guide/.apm/skills/apm-usage/dependencies.md Documents new SHA-pin update behavior and tag annotation.
packages/apm-guide/.apm/skills/apm-usage/commands.md Updates apm update / apm outdated command docs to mention SHA-pin semantics.
docs/src/content/docs/reference/cli/update.md Documents revision-pin behavior and --dry-run semantics.
docs/src/content/docs/reference/cli/outdated.md Documents SHA-pin comparison against annotated tags.
docs/src/content/docs/enterprise/security.md Extends security model docs to describe revision-pin update fences + lockfile tagging.
docs/src/content/docs/consumer/manage-dependencies.md Adds user-facing explanation + example of SHA pin rewrite and # <tag> annotation.
CHANGELOG.md Adds an Unreleased entry for the revision-pin support.

Copilot's findings

  • Files reviewed: 17/17 changed files
  • Comments generated: 8

Comment thread src/apm_cli/commands/update.py Outdated
Comment on lines +456 to +457
if dry_run and revision_pin_updates:
return
Comment thread src/apm_cli/commands/update.py Outdated
Comment on lines +470 to +495
@@ -351,6 +489,10 @@ def _plan_callback(plan: UpdatePlan) -> bool:
)
return False

if revision_pin_updates:
plan_state["proceeded"] = True
return True

Comment thread src/apm_cli/commands/update.py Outdated

def _resolve_and_maybe_apply_revision_pin_updates(
*,
all_declared_deps,
Comment thread src/apm_cli/commands/outdated.py Outdated
current_ref: str,
package_name: str,
package_basename: str,
remote_refs,
Comment thread src/apm_cli/deps/revision_pins.py Outdated

def resolve_revision_pin_updates(
dependencies: Iterable[DependencyReference],
downloader,
Comment thread src/apm_cli/deps/revision_pins.py Outdated
Comment on lines +200 to +202
def dependency_ref_from_locked(locked) -> DependencyReference:
"""Rebuild a DependencyReference suitable for authoritative upstream checks."""
return locked.to_dependency_ref()
Comment on lines +133 to +136
lines = ["[i] Revision pin updates for apm.yml", ""]
for update in ordered:
lines.append(f" [~] {update.display_name}")
lines.append(f" ref: {update.old_sha[:7]} -> {update.new_sha[:7]} (# {update.tag})")
Comment thread CHANGELOG.md Outdated

### Added

- `apm update` and `apm outdated` now understand full-SHA revision pins, resolving the latest annotated semver tag SHA and annotating updated manifest pins with the tag. (#1209)
Folds copilot-pull-request-reviewer follow-ups: --dry-run prints full plan, preserve consent gate, type hints, STATUS_SYMBOLS, changelog PR number.

Refs #1209

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

[FEATURE] Update revision-pins for external dependencies

2 participants