Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4f94bab
Add apm (Agent Package Manager) ecosystem support
Diogo-Costa Sep 24, 2026
939ea6d
Address PR review for apm ecosystem
Diogo-Costa Sep 24, 2026
69f917a
Address second apm PR review round
Diogo-Costa Sep 24, 2026
1c2c43c
Fix apm file_parser spec under isolated CI test runner
Diogo-Costa Sep 24, 2026
e9781cf
Fix same-line duplicate ref bumps and nested non-GitHub repos
Diogo-Costa Sep 24, 2026
d277b50
Enforce SemVer semantics, deep GitLab metadata, case-insensitive host
Diogo-Costa Sep 24, 2026
194cc29
Make APM git tag resolution strict SemVer aware
Diogo-Costa Sep 24, 2026
185f191
Update apm lockfile ref on bump and parse apm CLI version permissively
Diogo-Costa Sep 24, 2026
4f68767
Bump apm file_fetcher to `# typed: strong`
Diogo-Costa Sep 24, 2026
d7efcab
Align apm lockfile updater with the real apm.lock.yaml schema
Diogo-Costa Sep 24, 2026
a1c7ba3
Register apm in production updater and match lockfile identity by hos…
Diogo-Costa Sep 24, 2026
5b724d7
Do not rewrite apm.lock.yaml; leave it for APM to re-resolve on drift
Diogo-Costa Sep 24, 2026
11fb024
Match prerelease requirement bounds with SemVer ordering
Diogo-Costa Sep 24, 2026
18a8b90
Harden apm manifest parsing against valid-but-unsupported inputs
Diogo-Costa Sep 24, 2026
aa09394
Fix apm dependency identity for case-sensitive hosts and non-APM images
Diogo-Costa Sep 24, 2026
2c7fec5
Skip apm scalars whose decoding differs from their source span
Diogo-Costa Sep 24, 2026
adc33ea
Document unsupported apm scalar spellings in README scope
Diogo-Costa Sep 24, 2026
dc08d5a
Strip the manifest default host from apm dependency names
Diogo-Costa Sep 24, 2026
1e2aeca
Recognise GitHub Enterprise Cloud (*.ghe.com) as GitHub family
Diogo-Costa Sep 24, 2026
862b117
Skip Azure DevOps hosts and preserve trailing whitespace on update
Diogo-Costa Sep 24, 2026
2b5bfb0
Drop non-https ports from explicit clone URLs
Diogo-Costa Sep 24, 2026
f77435b
Classify hosts by hostname and skip http/git clone URLs
Diogo-Costa Sep 24, 2026
7f72b1a
Don't downgrade higher refs when rewriting merged requirements
Diogo-Costa Sep 24, 2026
1fea8bb
Handle uppercase URI schemes and pessimistic version constraints
Diogo-Costa Sep 24, 2026
b4667b9
Address round-22 review: identity, tags, and production classification
Diogo-Costa Sep 24, 2026
a201507
Recognise package-scoped tags in the parser and update checker
Diogo-Costa Sep 24, 2026
6b6f3c3
Preserve custom-port authority in APM metadata source lookup
Diogo-Costa Sep 24, 2026
5569a22
Fix APM identity ports, per-requirement tag families, and beta error
Diogo-Costa Sep 24, 2026
7d4a315
Make BETA_DISABLED_MESSAGE a plain frozen literal for Sorbet strict
Diogo-Costa Sep 24, 2026
c2ec1c7
Resolve tags per-requirement across families and register apm in dry-run
Diogo-Costa Sep 24, 2026
f104e65
Detect vulnerability per requirement ref, not just the merged version
Diogo-Costa Sep 24, 2026
10329c4
Report merged version from lowest post-update pin, not the max tag
Diogo-Costa Sep 24, 2026
d3be314
Allow apm in bin/dry-run.rb package-manager allowlist
Diogo-Costa Sep 24, 2026
9f6cdc2
Fix apm security-fix recognition and per-declaration cooldown for mer…
Diogo-Costa Oct 5, 2026
9ae6e65
Dedupe lockfile version parsing and fix manifest file matching
v-abhishekbhaskar Oct 8, 2026
c9e82e8
APM: drop unreachable security-update logic
Diogo-Costa Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/ci-filters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ dry_run:
- *shared
- 'bin/dry-run.rb'
- 'bin/spec/**'
apm:
- *shared
- 'apm/**'
bazel:
- *shared
- 'bazel/**'
Expand Down
3 changes: 3 additions & 0 deletions .github/issue-labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,6 @@

"L: sbt":
- '(sbt)'

"L: apm":
- '(apm)'
4 changes: 4 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
"L: apm":
- changed-files:
- any-glob-to-any-file: apm/**

"L: ruby:bundler":
- changed-files:
- any-glob-to-any-file: bundler/**
Expand Down
3 changes: 3 additions & 0 deletions .github/smoke-filters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ common: &common
- Dockerfile.updater-core
- 'common/**'
- 'updater/**'
apm:
- *common
- 'apm/**'
bazel:
- *common
- 'bazel/**'
Expand Down
5 changes: 5 additions & 0 deletions .github/smoke-matrix.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
[
{
"core": "apm",
"test": "apm",
"ecosystem": "apm"
},
Comment thread
Copilot marked this conversation as resolved.
{
"core": "bazel",
"test": "bazel",
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
fail-fast: false
matrix:
suite:
- { path: apm, name: apm, ecosystem: apm }
- { path: bazel, name: bazel, ecosystem: bazel }
- { path: bin, name: dry_run, ecosystem: common }
- { path: bun, name: bun, ecosystem: bun }
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.updater-core
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ COPY --chown=dependabot:dependabot --parents */.bundle */*.gemspec common/lib/de
# Entries are either "<name>" (gem lives in a directory matching its name) or
# "<name>:<dir>" when multiple gems share a directory (e.g. dependabot-docker
# and dependabot-docker_compose both ship from docker/).
ARG ECOSYSTEM_LIST="bazel bun bundler cargo composer conda deno devcontainers docker docker_compose:docker dotnet_sdk elm git_submodules github_actions go_modules gradle helm hex julia maven nix npm_and_yarn nuget opentofu pre_commit pub python rust_toolchain sbt silent swift terraform uv vcpkg"
ARG ECOSYSTEM_LIST="apm bazel bun bundler cargo composer conda deno devcontainers docker docker_compose:docker dotnet_sdk elm git_submodules github_actions go_modules gradle helm hex julia maven nix npm_and_yarn nuget opentofu pre_commit pub python rust_toolchain sbt silent swift terraform uv vcpkg"
# prevent having all the source in every ecosystem image
RUN for entry in $ECOSYSTEM_LIST; do \
name="${entry%%:*}"; \
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

source "https://rubygems.org"

gem "dependabot-apm", path: "apm"
gem "dependabot-bazel", path: "bazel"
gem "dependabot-bun", path: "bun"
gem "dependabot-bundler", path: "bundler"
Expand Down
8 changes: 8 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
PATH
remote: apm
specs:
dependabot-apm (0.399.0)
dependabot-common (= 0.399.0)

PATH
remote: bazel
specs:
Expand Down Expand Up @@ -476,6 +482,7 @@ PLATFORMS

DEPENDENCIES
debug (~> 1.9)
dependabot-apm!
dependabot-bazel!
dependabot-bun!
dependabot-bundler!
Expand Down Expand Up @@ -547,6 +554,7 @@ CHECKSUMS
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6
dependabot-apm (0.399.0)
dependabot-bazel (0.399.0)
dependabot-bun (0.399.0)
dependabot-bundler (0.399.0)
Expand Down
1 change: 1 addition & 0 deletions apm/.bundle/config
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
BUNDLE_GEMFILE: "../dependabot-updater/Gemfile"
4 changes: 4 additions & 0 deletions apm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/.bundle/*
!.bundle/config
/tmp
/dependabot-*.gem
1 change: 1 addition & 0 deletions apm/.rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
inherit_from: ../.rubocop.yml
7 changes: 7 additions & 0 deletions apm/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# syntax=docker.io/docker/dockerfile:1.20
FROM ghcr.io/dependabot/dependabot-updater-core

USER dependabot

COPY --chown=dependabot:dependabot --parents apm common $DEPENDABOT_HOME/
COPY --chown=dependabot:dependabot updater $DEPENDABOT_HOME/dependabot-updater
114 changes: 114 additions & 0 deletions apm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
## `dependabot-apm`

[APM (Agent Package Manager)][apm-repo] support for [`dependabot-core`][core-repo].

APM is a git-based package manager for AI agent context — skills, prompts,
chat modes, instructions and other agent primitives — declared in an `apm.yml`
manifest. Because every dependency resolves to a git ref, Dependabot bumps APM
dependencies the same way it bumps other git-sourced ecosystems (GitHub Actions,
git submodules): by resolving the newest semver tag on the remote and rewriting
the manifest ref.

### What Dependabot updates

Dependabot reads `apm.yml` and proposes updates for **string-shorthand git
dependencies that are pinned to a semver tag**, for example:

```yaml
dependencies:
apm:
- microsoft/edge-ai#v1.0.0 # GitHub shorthand pinned to a tag
- gitlab.com/acme/prompts#v2.1.0 # FQDN shorthand for any git host
- octo-org/octo-skills/skills/review#v1.4.0 # virtual sub-path within a repo
- acme.ghe.com/org/repo/skills/review#v1.0.0 # virtual sub-path on a GHE Cloud host
```

Virtual sub-paths (`repo/skills/review`) are resolved on GitHub-family hosts —
`github.com` and GitHub Enterprise Cloud data-residency hosts (`*.ghe.com`),
which APM also treats as GitHub. On other hosts the whole path is treated as the
repository; virtual packages on self-hosted GHES (reachable only via an
arbitrary configured `GITHUB_HOST`) are a follow-up.

For each such entry Dependabot:

1. Resolves the git remote (`https://<host>/<owner>/<repo>`).
2. Finds the highest semver tag that satisfies the update/cooldown/ignore rules,
reusing `Dependabot::GitCommitChecker` (the same tag resolution used by the
GitHub Actions ecosystem).
3. Rewrites only the ref in `apm.yml` (e.g. `#v1.0.0` → `#v1.4.0`), preserving
the rest of the declaration byte-for-byte.

Both plain `v1.4.0` / `1.4.0` tags and APM's package-scoped tags —
`review-v1.4.0`, `review--v1.4.0` and `review_v1.4.0`, where the prefix is the
package's own name (the repository name, or the final virtual-path component) —
are recognised, so a monorepo that tags each package independently is updated
correctly. Build metadata (`+build.5`) is preserved and, per SemVer, ignored for
precedence; equal-precedence tags break ties on the full tag string, so tag
resolution stays deterministic regardless of the order the remote advertises
them.

`apm.lock.yaml` is fetched as a read-only support file (used only to report the
APM CLI version) and is **never modified** — see the lockfile note below.

`devDependencies.apm` entries are updated too and are flagged as non-production
via the `development` dependency group.

### Scope of this version

To keep the first iteration small and reviewable, the following are intentionally
**out of scope** and are ignored (never modified, never erroring):

- **Object-form entries** (`git:`, `registry:`, `id:`, `path:` maps) and `mcp:`
entries — only the string shorthand is parsed.
- **Branch- and SHA-pinned entries** — these are resolved by APM's own lockfile,
which Dependabot does not regenerate, so their manifest ref is left untouched.
- **Local path entries** (`./pkg`, `../pkg`, `/pkg`) — not backed by a remote git
host, so there is nothing to bump.
- **Azure DevOps hosts** (`dev.azure.com`, `ssh.dev.azure.com` and legacy
`*.visualstudio.com`) — APM resolves these to `org/project/_git/repo` clone
URLs, a structure this version's generic `host/owner/repo` builder cannot
construct, so ADO entries are skipped rather than resolved to a wrong remote.
Native `_git` clone-URL support is a follow-up.
- **`http://` and `git://` clone URLs** — Dependabot enumerates tags over HTTPS,
so an `https://` or `ssh://`/SCP explicit URL is resolved (SSH over HTTPS on the
same host, keeping any `https://` port). A plain `http://` or `git://` URL names
a different endpoint (a distinct port, and for `http` an unencrypted service),
so it is skipped rather than silently rewritten to `https://`.
- **Block-scalar and escaped string entries** — a shorthand written as a YAML
block scalar (folded `>` / literal `|`) or as a quoted scalar that relies on
escape sequences (e.g. `"owner/repo\x23v1.0.0"`) decodes to text that is not a
contiguous slice of the manifest source, so its ref cannot be rewritten in
place. These uncommon spellings are skipped rather than producing a failing
update; write the shorthand as a plain or simply-quoted scalar
(`owner/repo#v1.0.0`) to have it updated.
- **Lockfile regeneration** — `apm.lock.yaml` is intentionally **left
unchanged**. Rewriting only its `resolved_ref:` to the new tag would be
actively harmful: APM's install path compares the manifest ref to
`resolved_ref` via `detect_ref_change`, and once they match it stops
detecting drift and replays the *stale* `resolved_commit` — so `apm install`
would keep installing the old commit. A full regeneration is also not
possible offline because `content_hash:` (the hash of the materialised
package tree) cannot be recomputed without downloading and building the
package. Leaving the entry untouched means the bumped manifest ref no longer
matches `resolved_ref`, so APM detects the drift and re-resolves the tag —
regenerating `resolved_commit` and `content_hash` correctly — on the next
`apm install`.

These are natural follow-ups and can be layered on without changing the manifest
parsing model established here.

### Running locally

1. Start a development shell

```
$ bin/docker-dev-shell apm
```

2. Run tests
```
[dependabot-core-dev] ~ $ cd apm && rspec
```

[core-repo]: https://github.com/dependabot/dependabot-core
[apm-repo]: https://github.com/microsoft/apm
36 changes: 36 additions & 0 deletions apm/dependabot-apm.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

Gem::Specification.new do |spec|
common_gemspec =
Bundler.load_gemspec_uncached("../common/dependabot-common.gemspec")

spec.name = "dependabot-apm"
spec.summary = "Provides Dependabot support for APM (Agent Package Manager)"
spec.description = "Dependabot-Apm provides support for bumping APM (Agent Package Manager) " \
"dependencies via Dependabot. " \
"If you want support for multiple package managers, you probably want the meta-gem " \
"dependabot-omnibus."

spec.author = common_gemspec.author
spec.email = common_gemspec.email
spec.homepage = common_gemspec.homepage
spec.license = common_gemspec.license

spec.metadata = {
"bug_tracker_uri" => common_gemspec.metadata["bug_tracker_uri"],
"changelog_uri" => common_gemspec.metadata["changelog_uri"]
}

spec.version = common_gemspec.version
spec.required_ruby_version = common_gemspec.required_ruby_version
spec.required_rubygems_version = common_gemspec.required_ruby_version

spec.require_path = "lib"
spec.files = Dir["lib/**/*"]

spec.add_dependency "dependabot-common", Dependabot::VERSION

common_gemspec.development_dependencies.each do |dep|
spec.add_development_dependency dep.name, *dep.requirement.as_list
end
end
28 changes: 28 additions & 0 deletions apm/lib/dependabot/apm.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# typed: strict
# frozen_string_literal: true

# These all need to be required so the various classes can be registered in a
# lookup table of package manager names to concrete classes.
require "dependabot/apm/file_fetcher"
require "dependabot/apm/file_parser"
require "dependabot/apm/update_checker"
require "dependabot/apm/file_updater"
require "dependabot/apm/metadata_finder"
require "dependabot/apm/requirement"
require "dependabot/apm/version"

require "dependabot/pull_request_creator/labeler"
Dependabot::PullRequestCreator::Labeler
.register_label_details("apm", name: "apm", colour: "0e8a16")

require "dependabot/dependency"
Dependabot::Dependency
.register_production_check(
"apm",
# Production when the explicit "dependencies" marker is present (or when a
# dependency carries no group information at all). Presence-based rather than
# "not development", so a package merged from both `dependencies.apm` and
# `devDependencies.apm` -- whose flattened groups include both markers --
# stays production, while a `devDependencies`-only entry does not.
->(groups) { groups.empty? || groups.include?("dependencies") }
)
95 changes: 95 additions & 0 deletions apm/lib/dependabot/apm/file_fetcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# typed: strict
# frozen_string_literal: true

require "sorbet-runtime"

require "dependabot/errors"
require "dependabot/file_fetchers"
require "dependabot/file_fetchers/base"
require "dependabot/apm/package_manager"

module Dependabot
module Apm
class FileFetcher < Dependabot::FileFetchers::Base
extend T::Sig
extend T::Helpers

MANIFEST_FILENAME = "apm.yml"
LOCKFILE_FILENAME = "apm.lock.yaml"

# Shown when file fetching runs with beta ecosystems disabled. apm is a
# beta ecosystem, so it only operates when a repo opts in; surfacing the
# remediation here avoids the misleading "apm.yml not found" message when
# the manifest actually exists and the real blocker is the disabled flag.
BETA_DISABLED_MESSAGE =
"apm is a beta ecosystem. Set `enable-beta-ecosystems: true` in your " \
"dependabot.yml so Dependabot fetches and updates apm.yml."

sig { override.params(filenames: T::Array[String]).returns(T::Boolean) }
def self.required_files_in?(filenames)
filenames.include?(MANIFEST_FILENAME)
end

sig { override.returns(String) }
def self.required_files_message
"Repo must contain an #{MANIFEST_FILENAME} file."
end

sig { override.returns(T::Array[DependencyFile]) }
def fetch_files
# apm is a beta ecosystem, so file fetching is hidden behind the
# beta-ecosystems feature flag (see NEW_ECOSYSTEMS.md). When the flag is
# off, tell the user to enable it rather than claim the manifest is
# missing, which would hide the real remediation when apm.yml exists.
unless allow_beta_ecosystems?
raise Dependabot::DependencyFileNotFound.new(
File.join(directory, MANIFEST_FILENAME),
BETA_DISABLED_MESSAGE
)
end

fetched_files = T.let([manifest_file], T::Array[DependencyFile])
fetched_files << T.must(lockfile) if lockfile
fetched_files
end

sig { override.returns(T.nilable(T::Hash[Symbol, T.anything])) }
def ecosystem_versions
return unless lockfile

version = parsed_lockfile_apm_version
return unless version

{ package_managers: { "apm" => version } }
end

private

sig { returns(Dependabot::DependencyFile) }
def manifest_file
@manifest_file ||= T.let(
fetch_file_from_host(MANIFEST_FILENAME),
T.nilable(Dependabot::DependencyFile)
)
end

sig { returns(T.nilable(Dependabot::DependencyFile)) }
def lockfile
return @lockfile if defined?(@lockfile)

@lockfile = T.let(
fetch_file_if_present(LOCKFILE_FILENAME)&.tap { |f| f.support_file = true },
T.nilable(Dependabot::DependencyFile)
)
end

sig { returns(T.nilable(String)) }
def parsed_lockfile_apm_version
PackageManager.version_from_lockfile(lockfile&.content)
end
end
end
end

Dependabot::FileFetchers
.register("apm", Dependabot::Apm::FileFetcher)
Loading
Loading