From 857cceec67014823953f45b8b1a43b4d2ef81b85 Mon Sep 17 00:00:00 2001 From: Jamie Magee Date: Thu, 19 Mar 2026 23:02:00 -0700 Subject: [PATCH 1/3] Initial nix support --- .github/ci-filters.yml | 3 + .github/issue-labeler.yml | 3 + .github/smoke-filters.yml | 3 + .github/smoke-matrix.json | 5 + .github/workflows/ci.yml | 1 + .github/workflows/images-branch.yml | 1 + .github/workflows/images-latest.yml | 1 + Dockerfile.updater-core | 2 +- bin/dry-run.rb | 1 + common/lib/dependabot/config/file.rb | 1 + nix/.bundle/config | 1 + nix/.gitignore | 4 + nix/.rubocop.yml | 1 + nix/Dockerfile | 19 ++ nix/README.md | 42 ++++ nix/dependabot-nix.gemspec | 35 +++ nix/lib/dependabot/nix.rb | 20 ++ nix/lib/dependabot/nix/file_fetcher.rb | 66 ++++++ nix/lib/dependabot/nix/file_parser.rb | 215 ++++++++++++++++++ nix/lib/dependabot/nix/file_updater.rb | 81 +++++++ nix/lib/dependabot/nix/metadata_finder.rb | 27 +++ .../nix/package/package_details_fetcher.rb | 142 ++++++++++++ nix/lib/dependabot/nix/package_manager.rb | 39 ++++ nix/lib/dependabot/nix/requirement.rb | 32 +++ nix/lib/dependabot/nix/update_checker.rb | 78 +++++++ .../update_checker/latest_version_finder.rb | 109 +++++++++ nix/lib/dependabot/nix/version.rb | 15 ++ nix/script/build | 7 + nix/script/ci-test | 11 + nix/spec/dependabot/nix/file_fetcher_spec.rb | 105 +++++++++ nix/spec/dependabot/nix/file_parser_spec.rb | 152 +++++++++++++ nix/spec/dependabot/nix/file_updater_spec.rb | 120 ++++++++++ nix/spec/dependabot/nix/fixtures/README.md | 15 ++ nix/spec/dependabot/nix/fixtures/flake.lock | 43 ++++ nix/spec/dependabot/nix/fixtures/flake.nix | 15 ++ .../nix/fixtures/flake_single_input.lock | 27 +++ .../nix/fixtures/flake_with_custom_host.lock | 28 +++ .../nix/fixtures/flake_with_follows.lock | 68 ++++++ .../nix/fixtures/flake_with_gitlab.lock | 44 ++++ .../nix/fixtures/flake_with_path_input.lock | 38 ++++ .../dependabot/nix/metadata_finder_spec.rb | 69 ++++++ .../dependabot/nix/package_manager_spec.rb | 33 +++ .../dependabot/nix/update_checker_spec.rb | 65 ++++++ nix/spec/dependabot/nix_spec.rb | 10 + nix/spec/spec_helper.rb | 12 + omnibus/dependabot-omnibus.gemspec | 1 + omnibus/lib/dependabot/omnibus.rb | 1 + rakelib/support/helpers.rb | 1 + script/dependabot | 1 + updater/lib/dependabot/setup.rb | 1 + 50 files changed, 1813 insertions(+), 1 deletion(-) create mode 100644 nix/.bundle/config create mode 100644 nix/.gitignore create mode 100644 nix/.rubocop.yml create mode 100644 nix/Dockerfile create mode 100644 nix/README.md create mode 100644 nix/dependabot-nix.gemspec create mode 100644 nix/lib/dependabot/nix.rb create mode 100644 nix/lib/dependabot/nix/file_fetcher.rb create mode 100644 nix/lib/dependabot/nix/file_parser.rb create mode 100644 nix/lib/dependabot/nix/file_updater.rb create mode 100644 nix/lib/dependabot/nix/metadata_finder.rb create mode 100644 nix/lib/dependabot/nix/package/package_details_fetcher.rb create mode 100644 nix/lib/dependabot/nix/package_manager.rb create mode 100644 nix/lib/dependabot/nix/requirement.rb create mode 100644 nix/lib/dependabot/nix/update_checker.rb create mode 100644 nix/lib/dependabot/nix/update_checker/latest_version_finder.rb create mode 100644 nix/lib/dependabot/nix/version.rb create mode 100755 nix/script/build create mode 100755 nix/script/ci-test create mode 100644 nix/spec/dependabot/nix/file_fetcher_spec.rb create mode 100644 nix/spec/dependabot/nix/file_parser_spec.rb create mode 100644 nix/spec/dependabot/nix/file_updater_spec.rb create mode 100644 nix/spec/dependabot/nix/fixtures/README.md create mode 100644 nix/spec/dependabot/nix/fixtures/flake.lock create mode 100644 nix/spec/dependabot/nix/fixtures/flake.nix create mode 100644 nix/spec/dependabot/nix/fixtures/flake_single_input.lock create mode 100644 nix/spec/dependabot/nix/fixtures/flake_with_custom_host.lock create mode 100644 nix/spec/dependabot/nix/fixtures/flake_with_follows.lock create mode 100644 nix/spec/dependabot/nix/fixtures/flake_with_gitlab.lock create mode 100644 nix/spec/dependabot/nix/fixtures/flake_with_path_input.lock create mode 100644 nix/spec/dependabot/nix/metadata_finder_spec.rb create mode 100644 nix/spec/dependabot/nix/package_manager_spec.rb create mode 100644 nix/spec/dependabot/nix/update_checker_spec.rb create mode 100644 nix/spec/dependabot/nix_spec.rb create mode 100644 nix/spec/spec_helper.rb diff --git a/.github/ci-filters.yml b/.github/ci-filters.yml index fe058c6d8a1..5ec71d9c564 100644 --- a/.github/ci-filters.yml +++ b/.github/ci-filters.yml @@ -71,6 +71,9 @@ julia: maven: - *shared - 'maven/**' +nix: + - *shared + - 'nix/**' npm_and_yarn: - *shared - 'npm_and_yarn/**' diff --git a/.github/issue-labeler.yml b/.github/issue-labeler.yml index da7df7b505e..0933fdfc14c 100644 --- a/.github/issue-labeler.yml +++ b/.github/issue-labeler.yml @@ -84,3 +84,6 @@ "L: pre:commit": - '(pre_commit)' + +"L: nix": + - '(nix)' diff --git a/.github/smoke-filters.yml b/.github/smoke-filters.yml index f46f6c46d2c..12b83cf2d70 100644 --- a/.github/smoke-filters.yml +++ b/.github/smoke-filters.yml @@ -47,6 +47,9 @@ hex: maven: - *common - 'maven/**' +nix: + - *common + - 'nix/**' npm_and_yarn: - *common - 'npm_and_yarn/**' diff --git a/.github/smoke-matrix.json b/.github/smoke-matrix.json index 724f1abe93e..57448bab3df 100644 --- a/.github/smoke-matrix.json +++ b/.github/smoke-matrix.json @@ -79,6 +79,11 @@ "test": "maven", "ecosystem": "maven" }, + { + "core": "nix", + "test": "nix", + "ecosystem": "nix" + }, { "core": "npm_and_yarn", "test": "npm", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78ec1abef2f..2ae56d3e2ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,7 @@ jobs: - { path: hex, name: hex, ecosystem: mix } - { path: julia, name: julia, ecosystem: julia } - { path: maven, name: maven, ecosystem: maven } + - { path: nix, name: nix, ecosystem: nix } - { path: npm_and_yarn, name: npm_and_yarn, ecosystem: npm } - { path: nuget, name: nuget, ecosystem: nuget } - { path: pre_commit, name: pre_commit, ecosystem: pre-commit } diff --git a/.github/workflows/images-branch.yml b/.github/workflows/images-branch.yml index b858cdf4795..fc3765f2469 100644 --- a/.github/workflows/images-branch.yml +++ b/.github/workflows/images-branch.yml @@ -77,6 +77,7 @@ jobs: - { name: hex, ecosystem: mix } - { name: julia, ecosystem: julia } - { name: maven, ecosystem: maven } + - { name: nix, ecosystem: nix } - { name: npm_and_yarn, ecosystem: npm } - { name: nuget, ecosystem: nuget } - { name: pre_commit, ecosystem: pre-commit } diff --git a/.github/workflows/images-latest.yml b/.github/workflows/images-latest.yml index 22399db59cd..7b4e20c4d0e 100644 --- a/.github/workflows/images-latest.yml +++ b/.github/workflows/images-latest.yml @@ -55,6 +55,7 @@ jobs: - { name: hex, ecosystem: mix } - { name: julia, ecosystem: julia } - { name: maven, ecosystem: maven } + - { name: nix, ecosystem: nix } - { name: npm_and_yarn, ecosystem: npm } - { name: nuget, ecosystem: nuget } - { name: pre_commit, ecosystem: pre-commit } diff --git a/Dockerfile.updater-core b/Dockerfile.updater-core index 62515fe61a0..06fe8f7c20a 100644 --- a/Dockerfile.updater-core +++ b/Dockerfile.updater-core @@ -162,7 +162,7 @@ COPY --chown=dependabot:dependabot updater/Gemfile updater/Gemfile.lock dependab COPY --chown=dependabot:dependabot --parents */.bundle */*.gemspec common/lib/dependabot.rb LICENSE omnibus $DEPENDABOT_HOME # This ARG must be updated when adding/removing ecosystems - it invalidates Docker layer cache -ARG ECOSYSTEM_LIST="bazel bun bundler cargo composer conda devcontainers docker docker_compose dotnet_sdk elm git_submodules github_actions go_modules gradle helm hex julia maven npm_and_yarn nuget opentofu pre_commit pub python rust_toolchain silent swift terraform uv vcpkg" +ARG ECOSYSTEM_LIST="bazel bun bundler cargo composer conda devcontainers docker docker_compose 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 silent swift terraform uv vcpkg" # prevent having all the source in every ecosystem image RUN for ecosystem in $ECOSYSTEM_LIST; do \ mkdir -p $ecosystem/lib/dependabot; \ diff --git a/bin/dry-run.rb b/bin/dry-run.rb index 19ff2510e52..80296c394dd 100755 --- a/bin/dry-run.rb +++ b/bin/dry-run.rb @@ -79,6 +79,7 @@ $LOAD_PATH << "./hex/lib" $LOAD_PATH << "./julia/lib" $LOAD_PATH << "./maven/lib" +$LOAD_PATH << "./nix/lib" $LOAD_PATH << "./npm_and_yarn/lib" $LOAD_PATH << "./nuget/lib" $LOAD_PATH << "./pre_commit/lib" diff --git a/common/lib/dependabot/config/file.rb b/common/lib/dependabot/config/file.rb index 57e27b0ea30..fe903ad31eb 100644 --- a/common/lib/dependabot/config/file.rb +++ b/common/lib/dependabot/config/file.rb @@ -79,6 +79,7 @@ def self.parse(config) "julia" => "julia", "maven" => "maven", "mix" => "hex", + "nix" => "nix", "npm" => "npm_and_yarn", "nuget" => "nuget", "opentofu" => "opentofu", diff --git a/nix/.bundle/config b/nix/.bundle/config new file mode 100644 index 00000000000..3faf5cfe5e6 --- /dev/null +++ b/nix/.bundle/config @@ -0,0 +1 @@ +BUNDLE_GEMFILE: "../dependabot-updater/Gemfile" diff --git a/nix/.gitignore b/nix/.gitignore new file mode 100644 index 00000000000..e8fae25a381 --- /dev/null +++ b/nix/.gitignore @@ -0,0 +1,4 @@ +/.bundle/* +!.bundle/config +/tmp +/dependabot-*.gem diff --git a/nix/.rubocop.yml b/nix/.rubocop.yml new file mode 100644 index 00000000000..fc2019d46a3 --- /dev/null +++ b/nix/.rubocop.yml @@ -0,0 +1 @@ +inherit_from: ../.rubocop.yml diff --git a/nix/Dockerfile b/nix/Dockerfile new file mode 100644 index 00000000000..6371c745437 --- /dev/null +++ b/nix/Dockerfile @@ -0,0 +1,19 @@ +# syntax=docker.io/docker/dockerfile:1.20 +FROM docker.io/nixos/nix:2.34.1 AS nix + +FROM ghcr.io/dependabot/dependabot-updater-core + +# Copy Nix from the official image +COPY --from=nix /nix /nix + +# Configure Nix for single-user mode with flakes enabled +RUN mkdir -p /etc/nix \ + && echo "experimental-features = nix-command flakes" > /etc/nix/nix.conf \ + && echo "sandbox = false" >> /etc/nix/nix.conf + +ENV PATH="/nix/var/nix/profiles/default/bin:${PATH}" + +USER dependabot + +COPY --chown=dependabot:dependabot --parents nix common $DEPENDABOT_HOME/ +COPY --chown=dependabot:dependabot updater $DEPENDABOT_HOME/dependabot-updater diff --git a/nix/README.md b/nix/README.md new file mode 100644 index 00000000000..2460c17a773 --- /dev/null +++ b/nix/README.md @@ -0,0 +1,42 @@ +## `dependabot-nix` + +Nix support for [`dependabot-core`][core-repo]. + +### Running locally + +1. Start a development shell + + ```sh + bin/docker-dev-shell nix + ``` + +1. Run tests + + ```sh + [dependabot-core-dev] ~ $ cd nix && rspec + ``` + +[core-repo]: https://github.com/dependabot/dependabot-core + +### Implementation Status + +This ecosystem is currently under development. See [NEW_ECOSYSTEMS.md](../NEW_ECOSYSTEMS.md) for implementation guidelines. + +#### Required Classes + +- [x] FileFetcher +- [x] FileParser +- [x] UpdateChecker +- [x] FileUpdater + +#### Optional Classes + +- [x] MetadataFinder +- [x] Version +- [x] Requirement + +#### Supporting Infrastructure + +- [x] Comprehensive unit tests +- [x] CI/CD integration +- [x] Documentation diff --git a/nix/dependabot-nix.gemspec b/nix/dependabot-nix.gemspec new file mode 100644 index 00000000000..0fcd94130ab --- /dev/null +++ b/nix/dependabot-nix.gemspec @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +Gem::Specification.new do |spec| + common_gemspec = + Bundler.load_gemspec_uncached("../common/dependabot-common.gemspec") + + spec.name = "dependabot-nix" + spec.summary = "Provides Dependabot support for Nix" + spec.description = "Dependabot-Nix provides support for bumping Nix 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 diff --git a/nix/lib/dependabot/nix.rb b/nix/lib/dependabot/nix.rb new file mode 100644 index 00000000000..4a53bc3f86c --- /dev/null +++ b/nix/lib/dependabot/nix.rb @@ -0,0 +1,20 @@ +# typed: strong +# 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/nix/file_fetcher" +require "dependabot/nix/file_parser" +require "dependabot/nix/update_checker" +require "dependabot/nix/file_updater" +require "dependabot/nix/metadata_finder" +require "dependabot/nix/version" +require "dependabot/nix/requirement" + +require "dependabot/pull_request_creator/labeler" +Dependabot::PullRequestCreator::Labeler + .register_label_details("nix", name: "nix", colour: "3E6399") + +require "dependabot/dependency" +Dependabot::Dependency + .register_production_check("nix", ->(_) { true }) diff --git a/nix/lib/dependabot/nix/file_fetcher.rb b/nix/lib/dependabot/nix/file_fetcher.rb new file mode 100644 index 00000000000..e79dbb54d4d --- /dev/null +++ b/nix/lib/dependabot/nix/file_fetcher.rb @@ -0,0 +1,66 @@ +# typed: strong +# frozen_string_literal: true + +require "sorbet-runtime" +require "dependabot/file_fetchers" +require "dependabot/file_fetchers/base" + +module Dependabot + module Nix + class FileFetcher < Dependabot::FileFetchers::Base + extend T::Sig + + sig { override.params(filenames: T::Array[String]).returns(T::Boolean) } + def self.required_files_in?(filenames) + filenames.include?("flake.nix") && filenames.include?("flake.lock") + end + + sig { override.returns(String) } + def self.required_files_message + "Repo must contain a flake.nix and flake.lock file." + end + + sig { override.returns(T::Array[DependencyFile]) } + def fetch_files + unless allow_beta_ecosystems? + raise Dependabot::DependencyFileNotFound.new( + nil, + "Nix support is currently in beta. Set ALLOW_BETA_ECOSYSTEMS=true to enable it." + ) + end + + fetched_files = [] + fetched_files << flake_nix + fetched_files << flake_lock + fetched_files + end + + sig { override.returns(T.nilable(T::Hash[Symbol, T.untyped])) } + def ecosystem_versions + nil + end + + private + + sig { returns(Dependabot::DependencyFile) } + def flake_nix + @flake_nix ||= + T.let( + fetch_file_from_host("flake.nix"), + T.nilable(Dependabot::DependencyFile) + ) + end + + sig { returns(Dependabot::DependencyFile) } + def flake_lock + @flake_lock ||= + T.let( + fetch_file_from_host("flake.lock"), + T.nilable(Dependabot::DependencyFile) + ) + end + end + end +end + +Dependabot::FileFetchers.register("nix", Dependabot::Nix::FileFetcher) diff --git a/nix/lib/dependabot/nix/file_parser.rb b/nix/lib/dependabot/nix/file_parser.rb new file mode 100644 index 00000000000..d065a217669 --- /dev/null +++ b/nix/lib/dependabot/nix/file_parser.rb @@ -0,0 +1,215 @@ +# typed: strict +# frozen_string_literal: true + +require "json" +require "sorbet-runtime" + +require "dependabot/dependency" +require "dependabot/file_parsers" +require "dependabot/file_parsers/base" +require "dependabot/shared_helpers" +require "dependabot/nix/package_manager" + +module Dependabot + module Nix + class FileParser < Dependabot::FileParsers::Base + extend T::Sig + + # Source types that are backed by git and can be updated via revision tracking + SUPPORTED_SOURCE_TYPES = T.let(%w(github gitlab sourcehut git).freeze, T::Array[String]) + + SUPPORTED_LOCK_VERSION = 7 + + DEFAULT_HOSTS = T.let( + { + "github" => "github.com", + "gitlab" => "gitlab.com", + "sourcehut" => "git.sr.ht" + }.freeze, + T::Hash[String, String] + ) + + sig { override.returns(T::Array[Dependabot::Dependency]) } + def parse + lock_content = JSON.parse(T.must(flake_lock.content)) + + lock_version = lock_content["version"] + if lock_version != SUPPORTED_LOCK_VERSION + Dependabot.logger.warn( + "flake.lock version #{lock_version.inspect} differs from expected #{SUPPORTED_LOCK_VERSION}" + ) + end + + root_name = lock_content.fetch("root", "root") + nodes = lock_content.fetch("nodes", {}) + root_node = nodes.fetch(root_name, {}) + root_inputs = root_node.fetch("inputs", {}) + + root_inputs.filter_map do |input_name, node_label| + node = resolve_node(node_label, nodes) + next unless node + + build_dependency(input_name, node) + end + end + + sig { returns(Ecosystem) } + def ecosystem + @ecosystem ||= T.let( + Ecosystem.new( + name: ECOSYSTEM, + package_manager: package_manager + ), + T.nilable(Dependabot::Ecosystem) + ) + end + + private + + # Resolves a node_label to its node in the lock file. + # node_label is either a string (direct reference) or an array ("follows" path + # that must be walked through nested inputs maps). + sig do + params( + node_label: T.any(String, T::Array[String]), + nodes: T::Hash[String, T.untyped] + ).returns(T.nilable(T::Hash[String, T.untyped])) + end + def resolve_node(node_label, nodes) + return nodes[node_label] unless node_label.is_a?(Array) + return nil if node_label.empty? + + # Walk the "follows" path: e.g. ["nixpkgs", "flake-utils"] means + # follow root -> nixpkgs node -> its inputs -> flake-utils + resolved_label = resolve_follows_path(node_label, nodes) + resolved_label ? nodes[resolved_label] : nil + end + + # Walks a "follows" path through nested inputs to find the final node label. + sig do + params( + path: T::Array[String], + nodes: T::Hash[String, T.untyped] + ).returns(T.nilable(String)) + end + def resolve_follows_path(path, nodes) + current_node_label = T.let(nil, T.nilable(String)) + + path.each_with_index do |segment, index| + # For the first segment, look up in the root's inputs via nodes directly + target = if index.zero? + # The first segment references a top-level node by name + segment + else + # Subsequent segments look up inputs within the current node + node = nodes[T.must(current_node_label)] + return nil unless node.is_a?(Hash) + + inputs = node.fetch("inputs", nil) + return nil unless inputs.is_a?(Hash) + + label = inputs[segment] + return nil unless label.is_a?(String) + + label + end + + current_node_label = target + end + + current_node_label + end + + sig do + params( + input_name: String, + node: T::Hash[String, T.untyped] + ).returns(T.nilable(Dependabot::Dependency)) + end + def build_dependency(input_name, node) + locked = node.fetch("locked", nil) + original = node.fetch("original", nil) + return unless locked && original + + source_type = locked.fetch("type", nil) + return unless SUPPORTED_SOURCE_TYPES.include?(source_type) + + rev = locked.fetch("rev", nil) + return unless rev + + url = build_url(locked) + return unless url + + ref = original.fetch("ref", nil) + + Dependency.new( + name: input_name, + version: rev, + package_manager: "nix", + requirements: [{ + requirement: nil, + file: "flake.lock", + source: { type: "git", url: url, branch: ref, ref: ref }, + groups: [] + }] + ) + end + + sig { params(locked: T::Hash[String, T.untyped]).returns(T.nilable(String)) } + def build_url(locked) + case locked["type"] + when "github" + host = locked["host"] || DEFAULT_HOSTS["github"] + "https://#{host}/#{locked['owner']}/#{locked['repo']}" + when "gitlab" + host = locked["host"] || DEFAULT_HOSTS["gitlab"] + "https://#{host}/#{locked['owner']}/#{locked['repo']}" + when "sourcehut" + host = locked["host"] || DEFAULT_HOSTS["sourcehut"] + "https://#{host}/~#{locked['owner']}/#{locked['repo']}" + when "git" + locked["url"] + end + end + + sig { returns(Dependabot::DependencyFile) } + def flake_lock + @flake_lock ||= + T.let( + T.must(get_original_file("flake.lock")), + T.nilable(Dependabot::DependencyFile) + ) + end + + sig { override.void } + def check_required_files + %w(flake.nix flake.lock).each do |filename| + raise "No #{filename}!" unless get_original_file(filename) + end + end + + sig { returns(Ecosystem::VersionManager) } + def package_manager + @package_manager ||= T.let( + PackageManager.new(T.must(nix_version)), + T.nilable(Dependabot::Nix::PackageManager) + ) + end + + sig { returns(T.nilable(String)) } + def nix_version + @nix_version ||= T.let( + begin + version_output = SharedHelpers.run_shell_command("nix --version") + version_output.match(/nix.*?(\d+\.\d+[\.\d]*)/)&.captures&.first || "0.0.0" + rescue StandardError + "0.0.0" + end, + T.nilable(String) + ) + end + end + end +end + +Dependabot::FileParsers.register("nix", Dependabot::Nix::FileParser) diff --git a/nix/lib/dependabot/nix/file_updater.rb b/nix/lib/dependabot/nix/file_updater.rb new file mode 100644 index 00000000000..1264da655f3 --- /dev/null +++ b/nix/lib/dependabot/nix/file_updater.rb @@ -0,0 +1,81 @@ +# typed: strong +# frozen_string_literal: true + +require "sorbet-runtime" + +require "dependabot/errors" +require "dependabot/file_updaters" +require "dependabot/file_updaters/base" +require "dependabot/shared_helpers" + +module Dependabot + module Nix + class FileUpdater < Dependabot::FileUpdaters::Base + extend T::Sig + + sig { override.returns(T::Array[Dependabot::DependencyFile]) } + def updated_dependency_files + updated_lockfile_content = update_flake_lock + + if updated_lockfile_content == flake_lock.content + raise Dependabot::DependencyFileContentNotChanged, + "Expected flake.lock to change for #{dependency.name}, but it didn't" + end + + [updated_file(file: flake_lock, content: updated_lockfile_content)] + end + + private + + sig { returns(Dependabot::Dependency) } + def dependency + T.must(dependencies.first) + end + + sig { returns(String) } + def update_flake_lock + SharedHelpers.in_a_temporary_repo_directory( + flake_lock.directory, + repo_contents_path + ) do + File.write("flake.nix", T.must(flake_nix.content)) + File.write("flake.lock", T.must(flake_lock.content)) + + SharedHelpers.run_shell_command( + "nix flake update #{dependency.name}", + fingerprint: "nix flake update " + ) + + File.read("flake.lock") + end + end + + sig { override.void } + def check_required_files + %w(flake.nix flake.lock).each do |filename| + raise "No #{filename}!" unless get_original_file(filename) + end + end + + sig { returns(Dependabot::DependencyFile) } + def flake_lock + @flake_lock ||= + T.let( + T.must(get_original_file("flake.lock")), + T.nilable(Dependabot::DependencyFile) + ) + end + + sig { returns(Dependabot::DependencyFile) } + def flake_nix + @flake_nix ||= + T.let( + T.must(get_original_file("flake.nix")), + T.nilable(Dependabot::DependencyFile) + ) + end + end + end +end + +Dependabot::FileUpdaters.register("nix", Dependabot::Nix::FileUpdater) diff --git a/nix/lib/dependabot/nix/metadata_finder.rb b/nix/lib/dependabot/nix/metadata_finder.rb new file mode 100644 index 00000000000..67283810abe --- /dev/null +++ b/nix/lib/dependabot/nix/metadata_finder.rb @@ -0,0 +1,27 @@ +# typed: strict +# frozen_string_literal: true + +require "sorbet-runtime" + +require "dependabot/metadata_finders" +require "dependabot/metadata_finders/base" + +module Dependabot + module Nix + class MetadataFinder < Dependabot::MetadataFinders::Base + extend T::Sig + + private + + sig { override.returns(T.nilable(Dependabot::Source)) } + def look_up_source + url = dependency.requirements.first&.fetch(:source)&.fetch(:url) || + dependency.requirements.first&.fetch(:source)&.fetch("url") + + Source.from_url(url) + end + end + end +end + +Dependabot::MetadataFinders.register("nix", Dependabot::Nix::MetadataFinder) diff --git a/nix/lib/dependabot/nix/package/package_details_fetcher.rb b/nix/lib/dependabot/nix/package/package_details_fetcher.rb new file mode 100644 index 00000000000..2be8f5f6d7f --- /dev/null +++ b/nix/lib/dependabot/nix/package/package_details_fetcher.rb @@ -0,0 +1,142 @@ +# typed: strict +# frozen_string_literal: true + +require "json" +require "time" +require "sorbet-runtime" +require "dependabot/nix" +require "dependabot/package/package_release" +require "dependabot/package/package_details" +require "dependabot/git_commit_checker" +require "dependabot/git_metadata_fetcher" + +module Dependabot + module Nix + module Package + class PackageDetailsFetcher + extend T::Sig + + sig do + params( + dependency: Dependabot::Dependency, + credentials: T::Array[Dependabot::Credential] + ).void + end + def initialize(dependency:, credentials:) + @dependency = dependency + @credentials = credentials + end + + sig { returns(Dependabot::Dependency) } + attr_reader :dependency + + sig { returns(T::Array[T.untyped]) } + attr_reader :credentials + + sig { returns(T.nilable(T::Array[Dependabot::Package::PackageRelease])) } + def available_versions + versions_metadata = fetch_tags_and_release_date + + versions_metadata = fetch_latest_tag_info if versions_metadata.empty? + + pseudo_version = versions_metadata.length + 1 + + versions_metadata.flat_map do |version_details| + pseudo_version -= 1 + tag = version_details[:tag] + release_date = version_details[:release_date] + + Dependabot::Package::PackageRelease.new( + version: Nix::Version.new("0.0.0-0.#{pseudo_version}"), + tag: tag, + released_at: release_date ? Time.parse(release_date) : nil + ) + rescue ArgumentError + Dependabot::Package::PackageRelease.new( + version: Nix::Version.new("0.0.0-0.#{pseudo_version}"), + tag: tag + ) + end + end + + private + + TARGET_COMMITS_TO_FETCH = 500 + private_constant :TARGET_COMMITS_TO_FETCH + + sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) } + def fetch_latest_tag_info + client = build_client + head = client.head_commit_for_current_branch + return [] unless head + + [{ tag: head }] + end + + sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) } + def fetch_tags_and_release_date + parsed_results = T.let([], T::Array[T::Hash[Symbol, T.untyped]]) + + begin + Dependabot.logger.info("Fetching release info for Nix flake input: #{dependency.name}") + client = build_client + + sha = T.let(nil, T.nilable(String)) + catch :found do + while parsed_results.length < TARGET_COMMITS_TO_FETCH + commits = get_commits(client, sha) + break if commits.empty? + + commits.each do |commit| + sha = commit["sha"] + parsed_results << { + tag: sha, + release_date: commit.dig("commit", "committer", "date") + } + throw :found if sha == dependency.version + end + break if commits.length < Dependabot::GitMetadataFetcher::MAX_COMMITS_PER_PAGE + end + end + parsed_results + rescue StandardError => e + Dependabot.logger.error("Error while fetching package info for nix flake input: #{e.message}") + parsed_results + end + end + + sig { returns(Dependabot::GitCommitChecker) } + def build_client + Dependabot::GitCommitChecker.new( + dependency: dependency, + credentials: credentials + ) + end + + sig do + params( + client: Dependabot::GitCommitChecker, + sha: T.nilable(String) + ).returns(T::Array[T::Hash[String, T.untyped]]) + end + def get_commits(client, sha) + response = sha.nil? ? client.ref_details_for_pinned_ref : client.ref_details(sha) + + unless response.status == 200 + Dependabot.logger.error( + "Error while fetching details for #{dependency.name}: #{response.body}" + ) + end + + return [] unless response.status == 200 + + commits = JSON.parse(response.body) + sha.nil? || commits.empty? ? commits : commits[1..] + rescue StandardError => e + Dependabot.logger.error("Error fetching commits: #{e.message}") + [] + end + end + end + end +end diff --git a/nix/lib/dependabot/nix/package_manager.rb b/nix/lib/dependabot/nix/package_manager.rb new file mode 100644 index 00000000000..97120b06012 --- /dev/null +++ b/nix/lib/dependabot/nix/package_manager.rb @@ -0,0 +1,39 @@ +# typed: strong +# frozen_string_literal: true + +require "sorbet-runtime" +require "dependabot/ecosystem" +require "dependabot/nix/version" + +module Dependabot + module Nix + ECOSYSTEM = "nix" + PACKAGE_MANAGER = "nix" + SUPPORTED_NIX_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version]) + DEPRECATED_NIX_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version]) + + class PackageManager < Dependabot::Ecosystem::VersionManager + extend T::Sig + + sig { params(raw_version: String).void } + def initialize(raw_version) + super( + name: PACKAGE_MANAGER, + version: Version.new(raw_version), + deprecated_versions: DEPRECATED_NIX_VERSIONS, + supported_versions: SUPPORTED_NIX_VERSIONS + ) + end + + sig { returns(T::Boolean) } + def deprecated? + false + end + + sig { returns(T::Boolean) } + def unsupported? + false + end + end + end +end diff --git a/nix/lib/dependabot/nix/requirement.rb b/nix/lib/dependabot/nix/requirement.rb new file mode 100644 index 00000000000..6bece8fdc5d --- /dev/null +++ b/nix/lib/dependabot/nix/requirement.rb @@ -0,0 +1,32 @@ +# typed: strong +# frozen_string_literal: true + +require "sorbet-runtime" + +require "dependabot/requirement" +require "dependabot/utils" + +module Dependabot + module Nix + class Requirement < Dependabot::Requirement + extend T::Sig + + sig { override.params(requirement_string: T.nilable(String)).returns(T::Array[Requirement]) } + def self.requirements_array(requirement_string) + [new(requirement_string)] + end + + sig { params(requirements: T.nilable(String)).void } + def initialize(*requirements) + requirements = requirements.flatten.flat_map do |req_string| + req_string&.split(",")&.map(&:strip) + end + + super(requirements) + end + end + end +end + +Dependabot::Utils + .register_requirement_class("nix", Dependabot::Nix::Requirement) diff --git a/nix/lib/dependabot/nix/update_checker.rb b/nix/lib/dependabot/nix/update_checker.rb new file mode 100644 index 00000000000..0590f80f2c5 --- /dev/null +++ b/nix/lib/dependabot/nix/update_checker.rb @@ -0,0 +1,78 @@ +# typed: strong +# frozen_string_literal: true + +require "sorbet-runtime" + +require "dependabot/update_checkers" +require "dependabot/update_checkers/base" +require "dependabot/nix/version" +require "dependabot/nix/requirement" +require "dependabot/git_commit_checker" + +module Dependabot + module Nix + class UpdateChecker < Dependabot::UpdateCheckers::Base + extend T::Sig + + require_relative "update_checker/latest_version_finder" + + sig { override.returns(T.nilable(T.any(String, Dependabot::Version))) } + def latest_version + @latest_version ||= + T.let( + fetch_latest_version, + T.nilable(T.any(String, Dependabot::Version)) + ) + end + + sig { override.returns(T.nilable(T.any(String, Dependabot::Version))) } + def latest_resolvable_version + # Resolvability isn't an issue for flake inputs — they're independent. + latest_version + end + + sig { override.returns(T.nilable(T.any(String, Dependabot::Version))) } + def latest_resolvable_version_with_no_unlock + # No concept of "unlocking" for flake inputs + latest_version + end + + sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) } + def updated_requirements + # Flake input requirements are the URL and branch — we never update those. + dependency.requirements + end + + private + + sig { override.returns(T::Boolean) } + def latest_version_resolvable_with_full_unlock? + # Full unlock checks aren't relevant for flake inputs + false + end + + sig { override.returns(T::Array[Dependabot::Dependency]) } + def updated_dependencies_after_full_unlock + raise NotImplementedError + end + + sig { returns(T.nilable(String)) } + def fetch_latest_version + T.let( + LatestVersionFinder.new( + dependency: dependency, + dependency_files: dependency_files, + credentials: credentials, + ignored_versions: ignored_versions, + security_advisories: security_advisories, + cooldown_options: update_cooldown, + raise_on_ignored: raise_on_ignored + ).latest_tag, + T.nilable(String) + ) + end + end + end +end + +Dependabot::UpdateCheckers.register("nix", Dependabot::Nix::UpdateChecker) diff --git a/nix/lib/dependabot/nix/update_checker/latest_version_finder.rb b/nix/lib/dependabot/nix/update_checker/latest_version_finder.rb new file mode 100644 index 00000000000..20cacc8d65f --- /dev/null +++ b/nix/lib/dependabot/nix/update_checker/latest_version_finder.rb @@ -0,0 +1,109 @@ +# typed: strong +# frozen_string_literal: true + +require "excon" +require "json" +require "sorbet-runtime" + +require "dependabot/errors" +require "dependabot/shared_helpers" +require "dependabot/update_checkers/version_filters" +require "dependabot/package/package_latest_version_finder" +require "dependabot/nix/update_checker" +require "dependabot/nix/package/package_details_fetcher" + +module Dependabot + module Nix + class UpdateChecker + class LatestVersionFinder < Dependabot::Package::PackageLatestVersionFinder + extend T::Sig + + sig { returns(T.nilable(String)) } + def latest_tag + releases = version_list + + releases = filter_by_cooldown(T.must(releases)) + releases = filter_ignored_versions(releases) + releases = apply_post_fetch_latest_versions_filter(releases) + releases.max_by(&:version)&.tag + end + + sig { returns(T.nilable(T::Array[Dependabot::Package::PackageRelease])) } + def version_list + @version_list ||= + T.let( + Package::PackageDetailsFetcher.new( + dependency: dependency, + credentials: credentials + ).available_versions, + T.nilable(T::Array[Dependabot::Package::PackageRelease]) + ) + end + + sig { params(release: Dependabot::Package::PackageRelease).returns(T::Boolean) } + def in_cooldown_period?(release) + unless release.released_at + Dependabot.logger.info("Release date not available for ref tag #{release.tag}") + return false + end + + days = cooldown_days + passed_seconds = Time.now.to_i - release.released_at.to_i + passed_days = passed_seconds / DAY_IN_SECONDS + + if passed_days < days + Dependabot.logger.info( + "Filtered #{release.tag}, Released on: " \ + "#{T.must(release.released_at).strftime('%Y-%m-%d')} " \ + "(#{passed_days}/#{days} cooldown days)" + ) + end + + passed_seconds < days * DAY_IN_SECONDS + end + + sig { returns(Integer) } + def cooldown_days + cooldown = @cooldown_options + return 0 if cooldown.nil? + return 0 unless cooldown_enabled? + return 0 unless cooldown.included?(dependency.name) + + return cooldown.default_days if cooldown.default_days.positive? + return cooldown.semver_major_days if cooldown.semver_major_days.positive? + return cooldown.semver_minor_days if cooldown.semver_minor_days.positive? + return cooldown.semver_patch_days if cooldown.semver_patch_days.positive? + + cooldown.default_days + end + + sig { returns(T::Boolean) } + def cooldown_enabled? + true + end + + sig do + params(releases: T::Array[Dependabot::Package::PackageRelease]) + .returns(T::Array[Dependabot::Package::PackageRelease]) + end + def apply_post_fetch_latest_versions_filter(releases) + if releases.empty? + Dependabot.logger.info("No releases found for #{dependency.name} after applying filters.") + return releases + end + + releases << Dependabot::Package::PackageRelease.new( + version: Nix::Version.new("0.0.0-0.0"), + tag: dependency.version + ) + releases + end + + private + + sig { override.returns(T.nilable(Dependabot::Package::PackageDetails)) } + def package_details; end + end + end + end +end diff --git a/nix/lib/dependabot/nix/version.rb b/nix/lib/dependabot/nix/version.rb new file mode 100644 index 00000000000..ee22984a7c8 --- /dev/null +++ b/nix/lib/dependabot/nix/version.rb @@ -0,0 +1,15 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/version" +require "dependabot/utils" + +module Dependabot + module Nix + class Version < Dependabot::Version + end + end +end + +Dependabot::Utils + .register_version_class("nix", Dependabot::Nix::Version) diff --git a/nix/script/build b/nix/script/build new file mode 100755 index 00000000000..9a254835b9e --- /dev/null +++ b/nix/script/build @@ -0,0 +1,7 @@ +#!/bin/bash + +set -e + +# Script for building native helpers (if needed) +# TODO: Implement build logic for native helpers +echo "No native helpers to build for nix" diff --git a/nix/script/ci-test b/nix/script/ci-test new file mode 100755 index 00000000000..c7acc95dac5 --- /dev/null +++ b/nix/script/ci-test @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +bundle install +bundle exec turbo_tests --verbose + +# TODO: If you have native helpers or additional tests, add them here +# Example: +# cd /opt/nix && npm run lint && cd - +# cd /opt/nix && npm test && cd - diff --git a/nix/spec/dependabot/nix/file_fetcher_spec.rb b/nix/spec/dependabot/nix/file_fetcher_spec.rb new file mode 100644 index 00000000000..fd9ecf39746 --- /dev/null +++ b/nix/spec/dependabot/nix/file_fetcher_spec.rb @@ -0,0 +1,105 @@ +# typed: false +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/nix/file_fetcher" +require_common_spec "file_fetchers/shared_examples_for_file_fetchers" + +RSpec.describe Dependabot::Nix::FileFetcher do + let(:credentials) do + [{ + "type" => "git_source", + "host" => "github.com", + "username" => "x-access-token", + "password" => "token" + }] + end + let(:url) { github_url + "repos/example/repo/contents/" } + let(:github_url) { "https://api.github.com/" } + let(:directory) { "/" } + let(:source) do + Dependabot::Source.new( + provider: "github", + repo: "example/repo", + directory: directory + ) + end + let(:file_fetcher_instance) do + described_class.new( + source: source, + credentials: credentials, + repo_contents_path: nil + ) + end + + before do + allow(file_fetcher_instance).to receive_messages(commit: "sha", allow_beta_ecosystems?: true) + end + + describe ".required_files_in?" do + it "returns true when both flake.nix and flake.lock are present" do + expect(described_class.required_files_in?(%w(flake.nix flake.lock))).to be true + end + + it "returns false when only flake.nix is present" do + expect(described_class.required_files_in?(%w(flake.nix))).to be false + end + + it "returns false when only flake.lock is present" do + expect(described_class.required_files_in?(%w(flake.lock))).to be false + end + + it "returns false when neither file is present" do + expect(described_class.required_files_in?(%w(package.json))).to be false + end + end + + describe "#fetch_files" do + before do + stub_request(:get, url + "flake.nix?ref=sha") + .with(headers: { "Authorization" => "token token" }) + .to_return( + status: 200, + body: fixture_body("flake.nix"), + headers: { "content-type" => "application/json" } + ) + stub_request(:get, url + "flake.lock?ref=sha") + .with(headers: { "Authorization" => "token token" }) + .to_return( + status: 200, + body: fixture_body("flake.lock"), + headers: { "content-type" => "application/json" } + ) + end + + it "fetches both flake.nix and flake.lock" do + expect(file_fetcher_instance.files.count).to eq(2) + expect(file_fetcher_instance.files.map(&:name)).to match_array(%w(flake.nix flake.lock)) + end + + context "when beta ecosystems are not allowed" do + before do + allow(file_fetcher_instance).to receive(:allow_beta_ecosystems?).and_return(false) + end + + it "raises a DependencyFileNotFound error" do + expect { file_fetcher_instance.files } + .to raise_error(Dependabot::DependencyFileNotFound) + end + end + end + + private + + def fixture_body(filename) + content = File.read( + File.join(__dir__, "fixtures", filename) + ) + { + "name" => filename, + "content" => Base64.encode64(content), + "encoding" => "base64", + "path" => filename + }.to_json + end +end diff --git a/nix/spec/dependabot/nix/file_parser_spec.rb b/nix/spec/dependabot/nix/file_parser_spec.rb new file mode 100644 index 00000000000..454b2181d62 --- /dev/null +++ b/nix/spec/dependabot/nix/file_parser_spec.rb @@ -0,0 +1,152 @@ +# typed: false +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/dependency_file" +require "dependabot/nix/file_parser" +require_common_spec "file_parsers/shared_examples_for_file_parsers" + +RSpec.describe Dependabot::Nix::FileParser do + subject(:parser) do + described_class.new( + dependency_files: dependency_files, + source: source + ) + end + + let(:source) do + Dependabot::Source.new( + provider: "github", + repo: "example/nix-project", + directory: "/" + ) + end + + let(:dependency_files) { [flake_nix, flake_lock] } + + let(:flake_nix) do + Dependabot::DependencyFile.new( + name: "flake.nix", + content: flake_nix_content + ) + end + + let(:flake_lock) do + Dependabot::DependencyFile.new( + name: "flake.lock", + content: flake_lock_content + ) + end + + let(:flake_nix_content) { fixture("flake.nix") } + let(:flake_lock_content) { fixture("flake.lock") } + + def fixture(filename) + File.read(File.join(__dir__, "fixtures", filename)) + end + + it_behaves_like "a dependency file parser" + + describe "#parse" do + subject(:dependencies) { parser.parse } + + context "with a standard flake.lock" do + it "returns the correct number of dependencies" do + expect(dependencies.length).to eq(2) + end + + it "parses nixpkgs correctly" do + nixpkgs = dependencies.find { |d| d.name == "nixpkgs" } + expect(nixpkgs).to be_a(Dependabot::Dependency) + expect(nixpkgs.version).to eq("3030f185ba6a4bf4f18b87f345f104e6a6961f34") + expect(nixpkgs.package_manager).to eq("nix") + expect(nixpkgs.requirements).to eq( + [{ + requirement: nil, + file: "flake.lock", + source: { + type: "git", + url: "https://github.com/NixOS/nixpkgs", + branch: "nixos-unstable", + ref: "nixos-unstable" + }, + groups: [] + }] + ) + end + + it "parses flake-utils correctly" do + flake_utils = dependencies.find { |d| d.name == "flake-utils" } + expect(flake_utils).to be_a(Dependabot::Dependency) + expect(flake_utils.version).to eq("b1d9ab70662946ef0850d488da1c9019f3a9752a") + expect(flake_utils.requirements).to eq( + [{ + requirement: nil, + file: "flake.lock", + source: { + type: "git", + url: "https://github.com/numtide/flake-utils", + branch: nil, + ref: nil + }, + groups: [] + }] + ) + end + end + + context "with a single-input flake.lock" do + let(:flake_lock_content) { fixture("flake_single_input.lock") } + + it "returns one dependency" do + expect(dependencies.length).to eq(1) + expect(dependencies.first.name).to eq("nixpkgs") + end + end + + context "with path inputs that should be skipped" do + let(:flake_lock_content) { fixture("flake_with_path_input.lock") } + + it "skips path-type inputs" do + expect(dependencies.length).to eq(1) + expect(dependencies.first.name).to eq("nixpkgs") + end + end + + context "with gitlab inputs" do + let(:flake_lock_content) { fixture("flake_with_gitlab.lock") } + + it "builds gitlab URLs correctly" do + gitlab_dep = dependencies.find { |d| d.name == "my-gitlab-dep" } + expect(gitlab_dep.requirements.first[:source][:url]) + .to eq("https://gitlab.com/myorg/myrepo") + end + end + + context "with follows inputs" do + let(:flake_lock_content) { fixture("flake_with_follows.lock") } + + it "resolves all three root inputs" do + expect(dependencies.length).to eq(3) + expect(dependencies.map(&:name)).to contain_exactly("nixpkgs", "flake-utils", "my-overlay") + end + + it "resolves the overlay dependency correctly" do + overlay = dependencies.find { |d| d.name == "my-overlay" } + expect(overlay.version).to eq("aaaa1111bbbb2222cccc3333dddd4444eeee5555") + expect(overlay.requirements.first[:source][:url]) + .to eq("https://github.com/example/my-overlay") + end + end + + context "with a custom host (self-hosted GitHub Enterprise)" do + let(:flake_lock_content) { fixture("flake_with_custom_host.lock") } + + it "uses the host field in the URL" do + dep = dependencies.find { |d| d.name == "internal-lib" } + expect(dep.requirements.first[:source][:url]) + .to eq("https://github.corp.example.com/myteam/internal-lib") + end + end + end +end diff --git a/nix/spec/dependabot/nix/file_updater_spec.rb b/nix/spec/dependabot/nix/file_updater_spec.rb new file mode 100644 index 00000000000..d3f570bf171 --- /dev/null +++ b/nix/spec/dependabot/nix/file_updater_spec.rb @@ -0,0 +1,120 @@ +# typed: false +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/dependency" +require "dependabot/dependency_file" +require "dependabot/nix/file_updater" +require_common_spec "file_updaters/shared_examples_for_file_updaters" + +RSpec.describe Dependabot::Nix::FileUpdater do + let(:dependency) do + Dependabot::Dependency.new( + name: "nixpkgs", + version: "new_sha_abc123", + previous_version: "3030f185ba6a4bf4f18b87f345f104e6a6961f34", + requirements: [{ + file: "flake.lock", + requirement: nil, + source: { + type: "git", + url: "https://github.com/NixOS/nixpkgs", + branch: "nixos-unstable", + ref: "nixos-unstable" + }, + groups: [] + }], + previous_requirements: [{ + file: "flake.lock", + requirement: nil, + source: { + type: "git", + url: "https://github.com/NixOS/nixpkgs", + branch: "nixos-unstable", + ref: "nixos-unstable" + }, + groups: [] + }], + package_manager: "nix" + ) + end + let(:updater) do + described_class.new( + dependency_files: [flake_nix, flake_lock], + dependencies: [dependency], + credentials: [{ + "type" => "git_source", + "host" => "github.com", + "username" => "x-access-token", + "password" => "token" + }] + ) + end + + let(:flake_nix) do + Dependabot::DependencyFile.new( + name: "flake.nix", + content: flake_nix_content + ) + end + + let(:flake_lock) do + Dependabot::DependencyFile.new( + name: "flake.lock", + content: flake_lock_content + ) + end + + let(:flake_nix_content) do + fixture("flake.nix") + end + + let(:flake_lock_content) do + fixture("flake.lock") + end + + def fixture(filename) + File.read(File.join(__dir__, "fixtures", filename)) + end + + it_behaves_like "a dependency file updater" + + describe "#updated_dependency_files" do + subject(:updated_files) { updater.updated_dependency_files } + + let(:updated_lock_content) do + flake_lock_content.gsub( + "3030f185ba6a4bf4f18b87f345f104e6a6961f34", + "new_sha_abc123" + ) + end + + before do + allow(Dependabot::SharedHelpers) + .to receive(:in_a_temporary_repo_directory) + .and_yield + allow(Dependabot::SharedHelpers) + .to receive(:run_shell_command) + allow(File).to receive(:write).and_call_original + allow(File).to receive(:write).with("flake.nix", anything) + allow(File).to receive(:write).with("flake.lock", anything) + allow(File).to receive(:read).and_call_original + allow(File).to receive(:read).with("flake.lock").and_return(updated_lock_content) + end + + it "returns one updated file" do + expect(updated_files.length).to eq(1) + end + + it "returns the updated flake.lock" do + expect(updated_files.first.name).to eq("flake.lock") + end + + it "calls nix flake update with the input name" do + updated_files + expect(Dependabot::SharedHelpers) + .to have_received(:run_shell_command) + .with("nix flake update nixpkgs", fingerprint: "nix flake update ") + end + end +end diff --git a/nix/spec/dependabot/nix/fixtures/README.md b/nix/spec/dependabot/nix/fixtures/README.md new file mode 100644 index 00000000000..871d72fbff8 --- /dev/null +++ b/nix/spec/dependabot/nix/fixtures/README.md @@ -0,0 +1,15 @@ +# Test Fixtures + +This directory contains test fixtures for the nix ecosystem. + +Add sample manifest files, lockfiles, and other test data here. + +Example structure: +``` +fixtures/ +├── manifest.json +├── lockfile.lock +└── projects/ + ├── simple/ + └── complex/ +``` diff --git a/nix/spec/dependabot/nix/fixtures/flake.lock b/nix/spec/dependabot/nix/fixtures/flake.lock new file mode 100644 index 00000000000..0978407f227 --- /dev/null +++ b/nix/spec/dependabot/nix/fixtures/flake.lock @@ -0,0 +1,43 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1709961763, + "narHash": "sha256-6H95HGJHhEZtyYA3rIQpvamMKAGoa8Yh2rFV29QCiGM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3030f185ba6a4bf4f18b87f345f104e6a6961f34", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/nix/spec/dependabot/nix/fixtures/flake.nix b/nix/spec/dependabot/nix/fixtures/flake.nix new file mode 100644 index 00000000000..7dae7ca28b0 --- /dev/null +++ b/nix/spec/dependabot/nix/fixtures/flake.nix @@ -0,0 +1,15 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + self, + nixpkgs, + flake-utils, + }: + { + }; +} diff --git a/nix/spec/dependabot/nix/fixtures/flake_single_input.lock b/nix/spec/dependabot/nix/fixtures/flake_single_input.lock new file mode 100644 index 00000000000..face2052df2 --- /dev/null +++ b/nix/spec/dependabot/nix/fixtures/flake_single_input.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1709961763, + "narHash": "sha256-6H95HGJHhEZtyYA3rIQpvamMKAGoa8Yh2rFV29QCiGM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3030f185ba6a4bf4f18b87f345f104e6a6961f34", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/nix/spec/dependabot/nix/fixtures/flake_with_custom_host.lock b/nix/spec/dependabot/nix/fixtures/flake_with_custom_host.lock new file mode 100644 index 00000000000..54449cef73b --- /dev/null +++ b/nix/spec/dependabot/nix/fixtures/flake_with_custom_host.lock @@ -0,0 +1,28 @@ +{ + "nodes": { + "internal-lib": { + "locked": { + "lastModified": 1710000000, + "narHash": "sha256-selfhosted123=", + "owner": "myteam", + "repo": "internal-lib", + "rev": "ffff0000aaaa1111bbbb2222cccc3333dddd4444", + "type": "github", + "host": "github.corp.example.com" + }, + "original": { + "owner": "myteam", + "repo": "internal-lib", + "type": "github", + "host": "github.corp.example.com" + } + }, + "root": { + "inputs": { + "internal-lib": "internal-lib" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/nix/spec/dependabot/nix/fixtures/flake_with_follows.lock b/nix/spec/dependabot/nix/fixtures/flake_with_follows.lock new file mode 100644 index 00000000000..9443dd21efc --- /dev/null +++ b/nix/spec/dependabot/nix/fixtures/flake_with_follows.lock @@ -0,0 +1,68 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "my-overlay": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1710000000, + "narHash": "sha256-overlay123=", + "owner": "example", + "repo": "my-overlay", + "rev": "aaaa1111bbbb2222cccc3333dddd4444eeee5555", + "type": "github" + }, + "original": { + "owner": "example", + "ref": "main", + "repo": "my-overlay", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1709961763, + "narHash": "sha256-6H95HGJHhEZtyYA3rIQpvamMKAGoa8Yh2rFV29QCiGM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3030f185ba6a4bf4f18b87f345f104e6a6961f34", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "my-overlay": "my-overlay", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/nix/spec/dependabot/nix/fixtures/flake_with_gitlab.lock b/nix/spec/dependabot/nix/fixtures/flake_with_gitlab.lock new file mode 100644 index 00000000000..11be6b40c34 --- /dev/null +++ b/nix/spec/dependabot/nix/fixtures/flake_with_gitlab.lock @@ -0,0 +1,44 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1709961763, + "narHash": "sha256-6H95HGJHhEZtyYA3rIQpvamMKAGoa8Yh2rFV29QCiGM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3030f185ba6a4bf4f18b87f345f104e6a6961f34", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "my-gitlab-dep": { + "locked": { + "lastModified": 1709900000, + "narHash": "sha256-abc123=", + "owner": "myorg", + "repo": "myrepo", + "rev": "abc123def456789012345678901234567890abcd", + "type": "gitlab" + }, + "original": { + "owner": "myorg", + "ref": "main", + "repo": "myrepo", + "type": "gitlab" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "my-gitlab-dep": "my-gitlab-dep" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/nix/spec/dependabot/nix/fixtures/flake_with_path_input.lock b/nix/spec/dependabot/nix/fixtures/flake_with_path_input.lock new file mode 100644 index 00000000000..f2b4af14953 --- /dev/null +++ b/nix/spec/dependabot/nix/fixtures/flake_with_path_input.lock @@ -0,0 +1,38 @@ +{ + "nodes": { + "local-dep": { + "locked": { + "path": "../local-flake", + "type": "path" + }, + "original": { + "path": "../local-flake", + "type": "path" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1709961763, + "narHash": "sha256-6H95HGJHhEZtyYA3rIQpvamMKAGoa8Yh2rFV29QCiGM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3030f185ba6a4bf4f18b87f345f104e6a6961f34", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "local-dep": "local-dep", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/nix/spec/dependabot/nix/metadata_finder_spec.rb b/nix/spec/dependabot/nix/metadata_finder_spec.rb new file mode 100644 index 00000000000..01d4cf5aed2 --- /dev/null +++ b/nix/spec/dependabot/nix/metadata_finder_spec.rb @@ -0,0 +1,69 @@ +# typed: false +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/dependency" +require "dependabot/nix/metadata_finder" +require_common_spec "metadata_finders/shared_examples_for_metadata_finders" + +RSpec.describe Dependabot::Nix::MetadataFinder do + subject(:finder) do + described_class.new(dependency: dependency, credentials: credentials) + end + + let(:credentials) do + [{ + "type" => "git_source", + "host" => "github.com", + "username" => "x-access-token", + "password" => "token" + }] + end + let(:url) { "https://github.com/NixOS/nixpkgs" } + let(:dependency) do + Dependabot::Dependency.new( + name: "nixpkgs", + version: "3030f185ba6a4bf4f18b87f345f104e6a6961f34", + previous_version: "oldsha123", + requirements: [{ + file: "flake.lock", + requirement: nil, + groups: [], + source: { type: "git", url: url, branch: "nixos-unstable", ref: "nixos-unstable" } + }], + previous_requirements: [{ + file: "flake.lock", + requirement: nil, + groups: [], + source: { type: "git", url: url, branch: "nixos-unstable", ref: "nixos-unstable" } + }], + package_manager: "nix" + ) + end + + before do + stub_request(:get, "https://example.com/status").to_return( + status: 200, + body: "Not GHES", + headers: {} + ) + end + + it_behaves_like "a dependency metadata finder" + + describe "#source_url" do + subject(:source_url) { finder.source_url } + + context "when the URL is a github one" do + let(:url) { "https://github.com/NixOS/nixpkgs" } + + it { is_expected.to eq("https://github.com/NixOS/nixpkgs") } + end + + context "when the URL is a gitlab one" do + let(:url) { "https://gitlab.com/myorg/myrepo" } + + it { is_expected.to eq("https://gitlab.com/myorg/myrepo") } + end + end +end diff --git a/nix/spec/dependabot/nix/package_manager_spec.rb b/nix/spec/dependabot/nix/package_manager_spec.rb new file mode 100644 index 00000000000..3efca0e6a69 --- /dev/null +++ b/nix/spec/dependabot/nix/package_manager_spec.rb @@ -0,0 +1,33 @@ +# typed: false +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/nix/package_manager" + +RSpec.describe Dependabot::Nix::PackageManager do + subject(:package_manager) { described_class.new("2.28.3") } + + describe "#name" do + it "returns nix" do + expect(package_manager.name).to eq("nix") + end + end + + describe "#version" do + it "returns the parsed version" do + expect(package_manager.version.to_s).to eq("2.28.3") + end + end + + describe "#deprecated?" do + it "returns false" do + expect(package_manager.deprecated?).to be false + end + end + + describe "#unsupported?" do + it "returns false" do + expect(package_manager.unsupported?).to be false + end + end +end diff --git a/nix/spec/dependabot/nix/update_checker_spec.rb b/nix/spec/dependabot/nix/update_checker_spec.rb new file mode 100644 index 00000000000..05862052997 --- /dev/null +++ b/nix/spec/dependabot/nix/update_checker_spec.rb @@ -0,0 +1,65 @@ +# typed: false +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/dependency" +require "dependabot/nix/update_checker" +require_common_spec "update_checkers/shared_examples_for_update_checkers" + +RSpec.describe Dependabot::Nix::UpdateChecker do + let(:branch) { "nixos-unstable" } + let(:url) { "https://github.com/NixOS/nixpkgs" } + let(:dependency) do + Dependabot::Dependency.new( + name: "nixpkgs", + version: "3030f185ba6a4bf4f18b87f345f104e6a6961f34", + requirements: [{ + file: "flake.lock", + requirement: nil, + groups: [], + source: { type: "git", url: url, branch: branch, ref: branch } + }], + package_manager: "nix" + ) + end + let(:checker) do + described_class.new( + dependency: dependency, + dependency_files: [], + credentials: [{ + "type" => "git_source", + "host" => "github.com", + "username" => "x-access-token", + "password" => "token" + }] + ) + end + + it_behaves_like "an update checker" + + describe "#can_update?" do + subject { checker.can_update?(requirements_to_unlock: :own) } + + context "when the dependency is outdated" do + before { allow(checker).to receive(:latest_version).and_return("new_sha") } + + it { is_expected.to be_truthy } + end + + context "when the dependency is up-to-date" do + before do + allow(checker) + .to receive(:latest_version) + .and_return("3030f185ba6a4bf4f18b87f345f104e6a6961f34") + end + + it { is_expected.to be_falsey } + end + end + + describe "#updated_requirements" do + it "returns the existing requirements unchanged" do + expect(checker.updated_requirements).to eq(dependency.requirements) + end + end +end diff --git a/nix/spec/dependabot/nix_spec.rb b/nix/spec/dependabot/nix_spec.rb new file mode 100644 index 00000000000..22e4da23a99 --- /dev/null +++ b/nix/spec/dependabot/nix_spec.rb @@ -0,0 +1,10 @@ +# typed: false +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/nix" +require_common_spec "shared_examples_for_autoloading" + +RSpec.describe Dependabot::Nix do + it_behaves_like "it registers the required classes", "nix" +end diff --git a/nix/spec/spec_helper.rb b/nix/spec/spec_helper.rb new file mode 100644 index 00000000000..50d7c79d638 --- /dev/null +++ b/nix/spec/spec_helper.rb @@ -0,0 +1,12 @@ +# typed: true +# frozen_string_literal: true + +def common_dir + @common_dir ||= Gem::Specification.find_by_name("dependabot-common").gem_dir +end + +def require_common_spec(path) + require "#{common_dir}/spec/dependabot/#{path}" +end + +require "#{common_dir}/spec/spec_helper.rb" diff --git a/omnibus/dependabot-omnibus.gemspec b/omnibus/dependabot-omnibus.gemspec index d948f307a3b..6f551e83d5e 100644 --- a/omnibus/dependabot-omnibus.gemspec +++ b/omnibus/dependabot-omnibus.gemspec @@ -46,6 +46,7 @@ Gem::Specification.new do |spec| spec.add_dependency "dependabot-hex", Dependabot::VERSION spec.add_dependency "dependabot-julia", Dependabot::VERSION spec.add_dependency "dependabot-maven", Dependabot::VERSION + spec.add_dependency "dependabot-nix", Dependabot::VERSION spec.add_dependency "dependabot-npm_and_yarn", Dependabot::VERSION spec.add_dependency "dependabot-nuget", Dependabot::VERSION spec.add_dependency "dependabot-opentofu", Dependabot::VERSION diff --git a/omnibus/lib/dependabot/omnibus.rb b/omnibus/lib/dependabot/omnibus.rb index 5db8220958f..2361359867f 100644 --- a/omnibus/lib/dependabot/omnibus.rb +++ b/omnibus/lib/dependabot/omnibus.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "dependabot/bazel" +require "dependabot/nix" require "dependabot/pre_commit" require "dependabot/python" require "dependabot/terraform" diff --git a/rakelib/support/helpers.rb b/rakelib/support/helpers.rb index 40510ba50cc..9804bb95a47 100644 --- a/rakelib/support/helpers.rb +++ b/rakelib/support/helpers.rb @@ -40,6 +40,7 @@ class RakeHelpers hex/dependabot-hex.gemspec julia/dependabot-julia.gemspec maven/dependabot-maven.gemspec + nix/dependabot-nix.gemspec npm_and_yarn/dependabot-npm_and_yarn.gemspec nuget/dependabot-nuget.gemspec omnibus/dependabot-omnibus.gemspec diff --git a/script/dependabot b/script/dependabot index d11805f281e..b2c3878763c 100755 --- a/script/dependabot +++ b/script/dependabot @@ -4,6 +4,7 @@ touch .core-bash_history # allow bash history to persist across invocations dependabot \ -v "$(pwd)"/.core-bash_history:/home/dependabot/.bash_history \ + -v "$(pwd)"/nix:/home/dependabot/nix \ -v "$(pwd)"/updater/bin:/home/dependabot/dependabot-updater/bin \ -v "$(pwd)"/updater/lib:/home/dependabot/dependabot-updater/lib \ -v "$(pwd)"/bin:/home/dependabot/bin \ diff --git a/updater/lib/dependabot/setup.rb b/updater/lib/dependabot/setup.rb index 9c06cce7ae2..31d652ae1ad 100644 --- a/updater/lib/dependabot/setup.rb +++ b/updater/lib/dependabot/setup.rb @@ -45,6 +45,7 @@ hex| julia| maven| + nix| npm_and_yarn| nuget| pre_commit| From 404f3ba2a1cfff6b63b8348f6cb02200b4082d90 Mon Sep 17 00:00:00 2001 From: Jamie Magee Date: Fri, 20 Mar 2026 15:40:26 -0700 Subject: [PATCH 2/3] nix: drop custom cooldown logic from LatestVersionFinder The base class PackageLatestVersionFinder already handles cooldown filtering, ignored version filtering, and fallback behavior. Remove the redundant in_cooldown_period?, cooldown_days, and cooldown_enabled? overrides along with five unused requires. --- .../update_checker/latest_version_finder.rb | 55 ++----------------- 1 file changed, 5 insertions(+), 50 deletions(-) diff --git a/nix/lib/dependabot/nix/update_checker/latest_version_finder.rb b/nix/lib/dependabot/nix/update_checker/latest_version_finder.rb index 20cacc8d65f..260d572f8e2 100644 --- a/nix/lib/dependabot/nix/update_checker/latest_version_finder.rb +++ b/nix/lib/dependabot/nix/update_checker/latest_version_finder.rb @@ -1,13 +1,8 @@ # typed: strong # frozen_string_literal: true -require "excon" -require "json" require "sorbet-runtime" -require "dependabot/errors" -require "dependabot/shared_helpers" -require "dependabot/update_checkers/version_filters" require "dependabot/package/package_latest_version_finder" require "dependabot/nix/update_checker" require "dependabot/nix/package/package_details_fetcher" @@ -21,8 +16,9 @@ class LatestVersionFinder < Dependabot::Package::PackageLatestVersionFinder sig { returns(T.nilable(String)) } def latest_tag releases = version_list + return nil unless releases - releases = filter_by_cooldown(T.must(releases)) + releases = filter_by_cooldown(releases) releases = filter_ignored_versions(releases) releases = apply_post_fetch_latest_versions_filter(releases) releases.max_by(&:version)&.tag @@ -40,51 +36,9 @@ def version_list ) end - sig { params(release: Dependabot::Package::PackageRelease).returns(T::Boolean) } - def in_cooldown_period?(release) - unless release.released_at - Dependabot.logger.info("Release date not available for ref tag #{release.tag}") - return false - end - - days = cooldown_days - passed_seconds = Time.now.to_i - release.released_at.to_i - passed_days = passed_seconds / DAY_IN_SECONDS - - if passed_days < days - Dependabot.logger.info( - "Filtered #{release.tag}, Released on: " \ - "#{T.must(release.released_at).strftime('%Y-%m-%d')} " \ - "(#{passed_days}/#{days} cooldown days)" - ) - end - - passed_seconds < days * DAY_IN_SECONDS - end - - sig { returns(Integer) } - def cooldown_days - cooldown = @cooldown_options - return 0 if cooldown.nil? - return 0 unless cooldown_enabled? - return 0 unless cooldown.included?(dependency.name) - - return cooldown.default_days if cooldown.default_days.positive? - return cooldown.semver_major_days if cooldown.semver_major_days.positive? - return cooldown.semver_minor_days if cooldown.semver_minor_days.positive? - return cooldown.semver_patch_days if cooldown.semver_patch_days.positive? - - cooldown.default_days - end - - sig { returns(T::Boolean) } - def cooldown_enabled? - true - end - sig do - params(releases: T::Array[Dependabot::Package::PackageRelease]) - .returns(T::Array[Dependabot::Package::PackageRelease]) + override.params(releases: T::Array[Dependabot::Package::PackageRelease]) + .returns(T::Array[Dependabot::Package::PackageRelease]) end def apply_post_fetch_latest_versions_filter(releases) if releases.empty? @@ -92,6 +46,7 @@ def apply_post_fetch_latest_versions_filter(releases) return releases end + # Fallback so the current version is always in the candidate set releases << Dependabot::Package::PackageRelease.new( version: Nix::Version.new("0.0.0-0.0"), tag: dependency.version From 29573e28ed2b7131363a108bb0fef469f6ec123f Mon Sep 17 00:00:00 2001 From: Jamie Magee Date: Sun, 22 Mar 2026 12:47:26 -0700 Subject: [PATCH 3/3] nix: wire LatestVersionFinder into parent filter pipeline Implement package_details so the parent class's available_versions, latest_version, latest_tag, etc. all work instead of returning nil. Remove the custom latest_tag override and version_list method that bypassed the parent pipeline (skipping yanked, unsupported, and prerelease filters). Override wants_prerelease? to return true since all Nix pseudo-versions have prerelease segments (0.0.0-0.N). --- .../update_checker/latest_version_finder.rb | 44 +++++++++---------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/nix/lib/dependabot/nix/update_checker/latest_version_finder.rb b/nix/lib/dependabot/nix/update_checker/latest_version_finder.rb index 260d572f8e2..6ee63dc6889 100644 --- a/nix/lib/dependabot/nix/update_checker/latest_version_finder.rb +++ b/nix/lib/dependabot/nix/update_checker/latest_version_finder.rb @@ -4,6 +4,7 @@ require "sorbet-runtime" require "dependabot/package/package_latest_version_finder" +require "dependabot/package/package_details" require "dependabot/nix/update_checker" require "dependabot/nix/package/package_details_fetcher" @@ -13,29 +14,6 @@ class UpdateChecker class LatestVersionFinder < Dependabot::Package::PackageLatestVersionFinder extend T::Sig - sig { returns(T.nilable(String)) } - def latest_tag - releases = version_list - return nil unless releases - - releases = filter_by_cooldown(releases) - releases = filter_ignored_versions(releases) - releases = apply_post_fetch_latest_versions_filter(releases) - releases.max_by(&:version)&.tag - end - - sig { returns(T.nilable(T::Array[Dependabot::Package::PackageRelease])) } - def version_list - @version_list ||= - T.let( - Package::PackageDetailsFetcher.new( - dependency: dependency, - credentials: credentials - ).available_versions, - T.nilable(T::Array[Dependabot::Package::PackageRelease]) - ) - end - sig do override.params(releases: T::Array[Dependabot::Package::PackageRelease]) .returns(T::Array[Dependabot::Package::PackageRelease]) @@ -54,10 +32,28 @@ def apply_post_fetch_latest_versions_filter(releases) releases end + # All Nix versions are pseudo-versions with prerelease segments (0.0.0-0.N), + # so we must always include prereleases to avoid filtering everything out. + sig { override.returns(T::Boolean) } + def wants_prerelease? + true + end + private sig { override.returns(T.nilable(Dependabot::Package::PackageDetails)) } - def package_details; end + def package_details + @package_details ||= T.let( + Dependabot::Package::PackageDetails.new( + dependency: dependency, + releases: Package::PackageDetailsFetcher.new( + dependency: dependency, + credentials: credentials + ).available_versions || [] + ), + T.nilable(Dependabot::Package::PackageDetails) + ) + end end end end