From ae79b9e1de3802f9b55b4ebb5237ac6401a068ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= Date: Thu, 11 Jan 2024 13:24:21 +0100 Subject: [PATCH 01/10] Implement FileFetcher for devcontainers Co-authored-by: Josh Spicer --- .../dependabot/devcontainers/file_fetcher.rb | 72 ++++++++++++++++++- .../lib/dependabot/devcontainers/utils.rb | 24 +++++++ .../devcontainers/file_fetcher_spec.rb | 67 +++++++++++++++++ .../.devcontainer/devcontainer.json | 8 +++ .../.devcontainer/devcontainer.json | 7 ++ .../config_in_root/.devcontainer.json | 6 ++ .../.devcontainer/bar/devcontainer.json | 6 ++ .../.devcontainer/foo/devcontainer.json | 6 ++ .../multiple_configs/.devcontainer.json | 7 ++ .../.devcontainer/devcontainer.json | 8 +++ 10 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 devcontainers/lib/dependabot/devcontainers/utils.rb create mode 100644 devcontainers/spec/fixtures/projects/config_in_dot_devcontainer_folder/.devcontainer/devcontainer.json create mode 100644 devcontainers/spec/fixtures/projects/config_in_folder/.devcontainer/devcontainer.json create mode 100644 devcontainers/spec/fixtures/projects/config_in_root/.devcontainer.json create mode 100644 devcontainers/spec/fixtures/projects/custom_configs/.devcontainer/bar/devcontainer.json create mode 100644 devcontainers/spec/fixtures/projects/custom_configs/.devcontainer/foo/devcontainer.json create mode 100644 devcontainers/spec/fixtures/projects/multiple_configs/.devcontainer.json create mode 100644 devcontainers/spec/fixtures/projects/multiple_configs/.devcontainer/devcontainer.json diff --git a/devcontainers/lib/dependabot/devcontainers/file_fetcher.rb b/devcontainers/lib/dependabot/devcontainers/file_fetcher.rb index 7e928863d0c..8b419744a81 100644 --- a/devcontainers/lib/dependabot/devcontainers/file_fetcher.rb +++ b/devcontainers/lib/dependabot/devcontainers/file_fetcher.rb @@ -1,12 +1,82 @@ -# typed: strong +# typed: true # frozen_string_literal: true require "dependabot/file_fetchers" require "dependabot/file_fetchers/base" +require "dependabot/devcontainers/utils" module Dependabot module Devcontainers class FileFetcher < Dependabot::FileFetchers::Base + def self.required_files_in?(filenames) + # There's several other places a devcontainer.json can be checked into + # See: https://containers.dev/implementors/spec/#devcontainerjson + filenames.any? { |f| f.end_with?("devcontainer.json") } + end + + def self.required_files_message + "Repo must contain a dev container configuration file." + end + + def fetch_files + fetched_files = [] + fetched_files += root_files + fetched_files += scoped_files + fetched_files += custom_directory_files + return fetched_files if fetched_files.any? + + raise Dependabot::DependencyFileNotFound.new( + nil, + "Neither .devcontainer.json nor .devcontainer/devcontainer.json nor " \ + ".devcontainer//devcontainer.json found in #{directory}" + ) + end + + private + + def root_files + fetch_config_and_lockfile_from(".") + end + + def scoped_files + return [] unless devcontainer_directory + + fetch_config_and_lockfile_from(".devcontainer") + end + + def custom_directory_files + return [] unless devcontainer_directory + + custom_directories.flat_map do |directory| + fetch_config_and_lockfile_from(directory.path) + end + end + + def custom_directories + repo_contents(dir: ".devcontainer").select { |f| f.type == "dir" && f.name != ".devcontainer" } + end + + def devcontainer_directory + return @devcontainer_directory if defined?(@devcontainer_directory) + + @devcontainer_directory = repo_contents.find { |f| f.type == "dir" && f.name == ".devcontainer" } + end + + def fetch_config_and_lockfile_from(directory) + files = [] + + config_name = Utils.expected_config_basename(directory) + config_file = fetch_file_if_present(File.join(directory, config_name)) + return files unless config_file + + files << config_file + + lockfile_name = Utils.expected_lockfile_name(File.basename(config_file.name)) + lockfile = fetch_support_file(File.join(directory, lockfile_name)) + files << lockfile if lockfile + + files + end end end end diff --git a/devcontainers/lib/dependabot/devcontainers/utils.rb b/devcontainers/lib/dependabot/devcontainers/utils.rb new file mode 100644 index 00000000000..d0b5e6a2138 --- /dev/null +++ b/devcontainers/lib/dependabot/devcontainers/utils.rb @@ -0,0 +1,24 @@ +# typed: true +# frozen_string_literal: true + +module Dependabot + module Devcontainers + module Utils + def self.expected_config_basename(directory) + root_directory?(directory) ? ".devcontainer.json" : "devcontainer.json" + end + + def self.root_directory?(directory) + Pathname.new(directory).cleanpath.to_path == Pathname.new(".").cleanpath.to_path + end + + def self.expected_lockfile_name(config_file_name) + if config_file_name.start_with?(".") + ".devcontainer-lock.json" + else + "devcontainer-lock.json" + end + end + end + end +end diff --git a/devcontainers/spec/dependabot/devcontainers/file_fetcher_spec.rb b/devcontainers/spec/dependabot/devcontainers/file_fetcher_spec.rb index f670cbee7b0..e0ffc278958 100644 --- a/devcontainers/spec/dependabot/devcontainers/file_fetcher_spec.rb +++ b/devcontainers/spec/dependabot/devcontainers/file_fetcher_spec.rb @@ -7,4 +7,71 @@ RSpec.describe Dependabot::Devcontainers::FileFetcher do it_behaves_like "a dependency file fetcher" + + let(:source) do + Dependabot::Source.new( + provider: "github", + repo: "mona/devcontainers-example", + directory: directory + ) + end + + let(:file_fetcher_instance) do + described_class.new(source: source, credentials: [], repo_contents_path: repo_contents_path) + end + + let(:repo_contents_path) { build_tmp_repo(project_name) } + + context "with a lone .devcontainer.json in repo root" do + let(:project_name) { "config_in_root" } + let(:directory) { "/" } + + it "fetches the correct files" do + expect(file_fetcher_instance.files.map(&:name)) + .to match_array(%w(.devcontainer.json)) + end + end + + context "with a .devcontainer folder" do + let(:project_name) { "config_in_dot_devcontainer_folder" } + let(:directory) { "/" } + + it "fetches the correct files" do + expect(file_fetcher_instance.files.map(&:name)) + .to match_array(%w(.devcontainer/devcontainer.json)) + end + end + + context "with repo that has multiple, valid dev container configs" do + let(:project_name) { "multiple_configs" } + let(:directory) { "/" } + it "fetches the correct files" do + expect(file_fetcher_instance.files.map(&:name)) + .to match_array(%w(.devcontainer.json .devcontainer/devcontainer.json)) + end + end + + context "with devcontainer.json files inside custom directories inside .devcontainer folder" do + let(:project_name) { "custom_configs" } + let(:directory) { "/" } + + it "fetches the correct files" do + expect(file_fetcher_instance.files.map(&:name)) + .to match_array(%w(.devcontainer/foo/devcontainer.json .devcontainer/bar/devcontainer.json)) + end + end + + context "with a directory that doesn't exist" do + let(:project_name) { "multiple_configs" } + let(:directory) { "/.devcontainer/nonexistent" } + + it "raises a helpful error" do + expect { file_fetcher_instance.files } + .to raise_error(Dependabot::DependencyFileNotFound) + .with_message( + "Neither .devcontainer.json nor .devcontainer/devcontainer.json nor " \ + ".devcontainer//devcontainer.json found in /.devcontainer/nonexistent" + ) + end + end end diff --git a/devcontainers/spec/fixtures/projects/config_in_dot_devcontainer_folder/.devcontainer/devcontainer.json b/devcontainers/spec/fixtures/projects/config_in_dot_devcontainer_folder/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..a29477024e7 --- /dev/null +++ b/devcontainers/spec/fixtures/projects/config_in_dot_devcontainer_folder/.devcontainer/devcontainer.json @@ -0,0 +1,8 @@ +{ + "image": "mcr.microsoft.com/devcontainers/typescript-node:18", + "features": { + "ghcr.io/codspace/versioning/foo:1": {}, + "ghcr.io/codspace/versioning/bar:1": {}, + "ghcr.io/codspace/versioning/baz:1.0": {} + } +} \ No newline at end of file diff --git a/devcontainers/spec/fixtures/projects/config_in_folder/.devcontainer/devcontainer.json b/devcontainers/spec/fixtures/projects/config_in_folder/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..79b80211423 --- /dev/null +++ b/devcontainers/spec/fixtures/projects/config_in_folder/.devcontainer/devcontainer.json @@ -0,0 +1,7 @@ +{ + "image": "mcr.microsoft.com/devcontainers/typescript-node:18", + "features": { + "ghcr.io/codspace/versioning/foo:1": {}, + "ghcr.io/codspace/versioning/bar:1": {} + } +} \ No newline at end of file diff --git a/devcontainers/spec/fixtures/projects/config_in_root/.devcontainer.json b/devcontainers/spec/fixtures/projects/config_in_root/.devcontainer.json new file mode 100644 index 00000000000..cf947fb2199 --- /dev/null +++ b/devcontainers/spec/fixtures/projects/config_in_root/.devcontainer.json @@ -0,0 +1,6 @@ +{ + "image": "mcr.microsoft.com/devcontainers/typescript-node:18", + "features": { + "ghcr.io/codspace/versioning/foo:1": {} + } +} \ No newline at end of file diff --git a/devcontainers/spec/fixtures/projects/custom_configs/.devcontainer/bar/devcontainer.json b/devcontainers/spec/fixtures/projects/custom_configs/.devcontainer/bar/devcontainer.json new file mode 100644 index 00000000000..433688a79d5 --- /dev/null +++ b/devcontainers/spec/fixtures/projects/custom_configs/.devcontainer/bar/devcontainer.json @@ -0,0 +1,6 @@ +{ + "image": "mcr.microsoft.com/devcontainers/typescript-node:18", + "features": { + "ghcr.io/codspace/versioning/bar:1": {} + } +} diff --git a/devcontainers/spec/fixtures/projects/custom_configs/.devcontainer/foo/devcontainer.json b/devcontainers/spec/fixtures/projects/custom_configs/.devcontainer/foo/devcontainer.json new file mode 100644 index 00000000000..8b20a0fc1c3 --- /dev/null +++ b/devcontainers/spec/fixtures/projects/custom_configs/.devcontainer/foo/devcontainer.json @@ -0,0 +1,6 @@ +{ + "image": "mcr.microsoft.com/devcontainers/typescript-node:18", + "features": { + "ghcr.io/codspace/versioning/foo:1": {} + } +} diff --git a/devcontainers/spec/fixtures/projects/multiple_configs/.devcontainer.json b/devcontainers/spec/fixtures/projects/multiple_configs/.devcontainer.json new file mode 100644 index 00000000000..79b80211423 --- /dev/null +++ b/devcontainers/spec/fixtures/projects/multiple_configs/.devcontainer.json @@ -0,0 +1,7 @@ +{ + "image": "mcr.microsoft.com/devcontainers/typescript-node:18", + "features": { + "ghcr.io/codspace/versioning/foo:1": {}, + "ghcr.io/codspace/versioning/bar:1": {} + } +} \ No newline at end of file diff --git a/devcontainers/spec/fixtures/projects/multiple_configs/.devcontainer/devcontainer.json b/devcontainers/spec/fixtures/projects/multiple_configs/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..a29477024e7 --- /dev/null +++ b/devcontainers/spec/fixtures/projects/multiple_configs/.devcontainer/devcontainer.json @@ -0,0 +1,8 @@ +{ + "image": "mcr.microsoft.com/devcontainers/typescript-node:18", + "features": { + "ghcr.io/codspace/versioning/foo:1": {}, + "ghcr.io/codspace/versioning/bar:1": {}, + "ghcr.io/codspace/versioning/baz:1.0": {} + } +} \ No newline at end of file From fefece74618aad4c2b11be9ec47ec05a4c589130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= Date: Thu, 11 Jan 2024 13:24:45 +0100 Subject: [PATCH 02/10] Implemente FileParser for devcontainers Co-authored-by: Josh Spicer --- .../dependabot/devcontainers/file_parser.rb | 34 ++- .../file_parser/feature_dependency_parser.rb | 100 +++++++ .../dependabot/devcontainers/requirement.rb | 4 - .../devcontainers/file_parser_spec.rb | 266 ++++++++++++++++++ .../.devcontainer/devcontainer.json | 7 - .../config_in_root/.devcontainer.json | 3 +- 6 files changed, 400 insertions(+), 14 deletions(-) create mode 100644 devcontainers/lib/dependabot/devcontainers/file_parser/feature_dependency_parser.rb create mode 100644 devcontainers/spec/dependabot/devcontainers/file_parser_spec.rb delete mode 100644 devcontainers/spec/fixtures/projects/config_in_folder/.devcontainer/devcontainer.json diff --git a/devcontainers/lib/dependabot/devcontainers/file_parser.rb b/devcontainers/lib/dependabot/devcontainers/file_parser.rb index c532317761a..9d2fcc72049 100644 --- a/devcontainers/lib/dependabot/devcontainers/file_parser.rb +++ b/devcontainers/lib/dependabot/devcontainers/file_parser.rb @@ -3,17 +3,47 @@ require "dependabot/file_parsers" require "dependabot/file_parsers/base" +require "dependabot/devcontainers/version" +require "dependabot/devcontainers/file_parser/feature_dependency_parser" module Dependabot module Devcontainers class FileParser < Dependabot::FileParsers::Base + require "dependabot/file_parsers/base/dependency_set" + def parse - [] + dependency_set = DependencySet.new + + config_dependency_files.each do |config_dependency_file| + parse_features(config_dependency_file).each do |dep| + dependency_set << dep + end + end + + dependency_set.dependencies end private - def check_required_files; end + def check_required_files + return if config_dependency_files.any? + + raise "No dev container configuration!" + end + + def parse_features(config_dependency_file) + FeatureDependencyParser.new( + config_dependency_file: config_dependency_file, + repo_contents_path: repo_contents_path, + credentials: credentials + ).parse + end + + def config_dependency_files + @config_dependency_files ||= dependency_files.select do |f| + f.name.end_with?("devcontainer.json") + end + end end end end diff --git a/devcontainers/lib/dependabot/devcontainers/file_parser/feature_dependency_parser.rb b/devcontainers/lib/dependabot/devcontainers/file_parser/feature_dependency_parser.rb new file mode 100644 index 00000000000..e0544096bd9 --- /dev/null +++ b/devcontainers/lib/dependabot/devcontainers/file_parser/feature_dependency_parser.rb @@ -0,0 +1,100 @@ +# typed: true +# frozen_string_literal: true + +require "dependabot/devcontainers/requirement" +require "dependabot/file_parsers/base" +require "dependabot/shared_helpers" +require "dependabot/dependency" +require "json" +require "uri" + +module Dependabot + module Devcontainers + class FileParser < Dependabot::FileParsers::Base + class FeatureDependencyParser + def initialize(config_dependency_file:, repo_contents_path:, credentials:) + @config_dependency_file = config_dependency_file + @repo_contents_path = repo_contents_path + @credentials = credentials + end + + def parse + SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do + SharedHelpers.with_git_configured(credentials: credentials) do + parse_cli_json(evaluate_with_cli) + end + end + end + + private + + def base_dir + File.dirname(config_dependency_file.path) + end + + def config_name + File.basename(config_dependency_file.path) + end + + def config_contents + config_dependency_file.content + end + + # https://github.com/devcontainers/cli/blob/9444540283b236298c28f397dea879e7ec222ca1/src/spec-node/devContainersSpecCLI.ts#L1072 + def evaluate_with_cli + raise "config_name must be a string" unless config_name.is_a?(String) && !config_name.empty? + + cmd = "devcontainer outdated --workspace-folder . --config #{config_name} --output-format json" + Dependabot.logger.info("Running command: #{cmd}") + + json = SharedHelpers.run_shell_command( + cmd, + stderr_to_stdout: false + ) + + JSON.parse(json) + end + + def parse_cli_json(json) + dependencies = [] + + features = json["features"] + features.each do |feature, versions_object| + name, requirement = feature.split(":") + current = versions_object["current"] + wanted = versions_object["wanted"] + latest = versions_object["latest"] + wanted_major = versions_object["wantedMajor"] + latest_major = versions_object["latestMajor"] + + dep = Dependency.new( + name: name, + version: current, + package_manager: "devcontainers", + requirements: [ + { + requirement: requirement, + file: config_dependency_file.name, + groups: ["feature"], + source: nil, + metadata: { + wanted: wanted, + latest: latest, + wanted_major: wanted_major, + latest_major: latest_major + } + } + ], + metadata: {} + ) + + dependencies << dep + end + dependencies + end + + attr_reader :config_dependency_file, :repo_contents_path, :credentials + end + end + end +end diff --git a/devcontainers/lib/dependabot/devcontainers/requirement.rb b/devcontainers/lib/dependabot/devcontainers/requirement.rb index 1f401a27532..9983d788cdc 100644 --- a/devcontainers/lib/dependabot/devcontainers/requirement.rb +++ b/devcontainers/lib/dependabot/devcontainers/requirement.rb @@ -19,10 +19,6 @@ def self.requirements_array(requirement_string) [new(requirement_string)] end - def satisfied_by?(version) - super(version.release_part) - end - # Patches Gem::Requirement to make it accept requirement strings like # "~> 4.2.5, >= 4.2.5.1" without first needing to split them. def initialize(*requirements) diff --git a/devcontainers/spec/dependabot/devcontainers/file_parser_spec.rb b/devcontainers/spec/dependabot/devcontainers/file_parser_spec.rb new file mode 100644 index 00000000000..461ba745def --- /dev/null +++ b/devcontainers/spec/dependabot/devcontainers/file_parser_spec.rb @@ -0,0 +1,266 @@ +# typed: false +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/dependency_file" +require "dependabot/source" +require "dependabot/devcontainers/file_parser" +require "dependabot/devcontainers/requirement" +require_common_spec "file_parsers/shared_examples_for_file_parsers" + +RSpec.describe Dependabot::Devcontainers::FileParser do + it_behaves_like "a dependency file parser" + + let(:parser) do + described_class.new(dependency_files: files, source: source, repo_contents_path: repo_contents_path) + end + + let(:source) do + Dependabot::Source.new( + provider: "github", + repo: "mona/Example", + directory: directory + ) + end + + let(:files) do + project_dependency_files(project_name, directory: directory) + end + + let(:repo_contents_path) { build_tmp_repo(project_name, path: "projects") } + + let(:dependencies) { parser.parse } + + shared_examples_for "parse" do + it "parses dependencies fine" do + expect(dependencies.size).to eq(expectations.size) + + expectations.each do |expected| + version = expected[:version] + name = expected[:name] + requirements = expected[:requirements] + metadata = expected[:metadata] + + dependency = dependencies.find { |dep| dep.name == name } + expect(dependency).to have_attributes( + name: name, + version: version, + requirements: requirements, + metadata: metadata + ) + end + end + end + + context "with a .devcontainer.json in repo root" do + let(:project_name) { "config_in_root" } + let(:directory) { "/" } + + let(:expectations) do + [ + { + name: "ghcr.io/codspace/versioning/foo", + version: "1.1.0", + requirements: [ + { + requirement: "1", + file: ".devcontainer.json", + groups: ["feature"], + source: nil, + metadata: { + wanted: "1.1.0", + latest: "2.11.1", + latest_major: "2", + wanted_major: "1" + } + } + ], + metadata: {} + }, + { + name: "ghcr.io/codspace/versioning/bar", + version: "1.0.0", + requirements: [ + { + requirement: "1", + file: ".devcontainer.json", + groups: ["feature"], + source: nil, + metadata: { + wanted: "1.0.0", + latest: "1.0.0", + latest_major: "1", + wanted_major: "1" + } + } + ], + metadata: {} + } + ].freeze + end + + it_behaves_like "parse" + end + + context "with a devcontainer.json in a .devcontainer folder" do + let(:project_name) { "config_in_dot_devcontainer_folder" } + let(:directory) { "/" } + + let(:expectations) do + [ + { + name: "ghcr.io/codspace/versioning/foo", + version: "1.1.0", + requirements: [ + { + requirement: "1", + file: ".devcontainer/devcontainer.json", + groups: ["feature"], + source: nil, + metadata: { + wanted: "1.1.0", + latest: "2.11.1", + latest_major: "2", + wanted_major: "1" + } + } + ], + metadata: {} + }, + { + name: "ghcr.io/codspace/versioning/bar", + version: "1.0.0", + requirements: [ + { + requirement: "1", + file: ".devcontainer/devcontainer.json", + groups: ["feature"], + source: nil, + metadata: { + wanted: "1.0.0", + latest: "1.0.0", + latest_major: "1", + wanted_major: "1" + } + } + ], + metadata: {} + }, + { + name: "ghcr.io/codspace/versioning/baz", + version: "1.0.0", + requirements: [ + { + requirement: "1.0", + file: ".devcontainer/devcontainer.json", + groups: ["feature"], + source: nil, + metadata: { + wanted: "1.0.0", + latest: "2.0.0", + latest_major: "2", + wanted_major: "1" + } + } + ], + metadata: {} + } + ].freeze + end + + it_behaves_like "parse" + end + + context "with multiple, valid devcontainer.json config files in repo" do + let(:project_name) { "multiple_configs" } + let(:directory) { "/" } + + let(:expectations) do + [ + { + name: "ghcr.io/codspace/versioning/foo", + version: "1.1.0", + requirements: [ + { + requirement: "1", + file: ".devcontainer/devcontainer.json", + groups: ["feature"], + source: nil, + metadata: { + wanted: "1.1.0", + latest: "2.11.1", + latest_major: "2", + wanted_major: "1" + } + }, + { + requirement: "1", + file: ".devcontainer.json", + groups: ["feature"], + source: nil, + metadata: { + wanted: "1.1.0", + latest: "2.11.1", + latest_major: "2", + wanted_major: "1" + } + } + ], + metadata: {} + }, + { + name: "ghcr.io/codspace/versioning/bar", + version: "1.0.0", + requirements: [ + { + requirement: "1", + file: ".devcontainer/devcontainer.json", + groups: ["feature"], + source: nil, + metadata: { + wanted: "1.0.0", + latest: "1.0.0", + latest_major: "1", + wanted_major: "1" + } + }, + { + requirement: "1", + file: ".devcontainer.json", + groups: ["feature"], + source: nil, + metadata: { + wanted: "1.0.0", + latest: "1.0.0", + latest_major: "1", + wanted_major: "1" + } + } + ], + metadata: {} + }, + { + name: "ghcr.io/codspace/versioning/baz", + version: "1.0.0", + requirements: [ + { + requirement: "1.0", + file: ".devcontainer/devcontainer.json", + groups: ["feature"], + source: nil, + metadata: { + wanted: "1.0.0", + latest: "2.0.0", + latest_major: "2", + wanted_major: "1" + } + } + ], + metadata: {} + } + ].freeze + end + + it_behaves_like "parse" + end +end diff --git a/devcontainers/spec/fixtures/projects/config_in_folder/.devcontainer/devcontainer.json b/devcontainers/spec/fixtures/projects/config_in_folder/.devcontainer/devcontainer.json deleted file mode 100644 index 79b80211423..00000000000 --- a/devcontainers/spec/fixtures/projects/config_in_folder/.devcontainer/devcontainer.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "image": "mcr.microsoft.com/devcontainers/typescript-node:18", - "features": { - "ghcr.io/codspace/versioning/foo:1": {}, - "ghcr.io/codspace/versioning/bar:1": {} - } -} \ No newline at end of file diff --git a/devcontainers/spec/fixtures/projects/config_in_root/.devcontainer.json b/devcontainers/spec/fixtures/projects/config_in_root/.devcontainer.json index cf947fb2199..79b80211423 100644 --- a/devcontainers/spec/fixtures/projects/config_in_root/.devcontainer.json +++ b/devcontainers/spec/fixtures/projects/config_in_root/.devcontainer.json @@ -1,6 +1,7 @@ { "image": "mcr.microsoft.com/devcontainers/typescript-node:18", "features": { - "ghcr.io/codspace/versioning/foo:1": {} + "ghcr.io/codspace/versioning/foo:1": {}, + "ghcr.io/codspace/versioning/bar:1": {} } } \ No newline at end of file From 7afb6ad0191750b9b6f171e371207184eeb595b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= Date: Wed, 10 Jan 2024 18:33:22 +0100 Subject: [PATCH 03/10] Implement UpdateChecker for devcontainers This class at the moment just delegates to the FileUpdater, because of the heavy lifting is done by the FileParser class, since the `devcontainers outdated` command provides both current dependency and available updates information. Co-authored-by: Josh Spicer --- .../devcontainers/update_checker.rb | 44 +++++++++- .../devcontainers/update_checker_spec.rb | 80 +++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 devcontainers/spec/dependabot/devcontainers/update_checker_spec.rb diff --git a/devcontainers/lib/dependabot/devcontainers/update_checker.rb b/devcontainers/lib/dependabot/devcontainers/update_checker.rb index ef0a88c13a9..e66a6df17a9 100644 --- a/devcontainers/lib/dependabot/devcontainers/update_checker.rb +++ b/devcontainers/lib/dependabot/devcontainers/update_checker.rb @@ -1,12 +1,54 @@ -# typed: strong +# typed: true # frozen_string_literal: true require "dependabot/update_checkers" require "dependabot/update_checkers/base" +require "dependabot/devcontainers/version" +require "dependabot/update_checkers/version_filters" +require "dependabot/devcontainers/requirement" module Dependabot module Devcontainers class UpdateChecker < Dependabot::UpdateCheckers::Base + def latest_version + @latest_version ||= dependency.requirements.map do |requirement| + Version.new(requirement[:metadata][:latest]) + end.max + end + + def latest_resolvable_version + latest_version # TODO + end + + def updated_requirements + dependency.requirements.map do |requirement| + latest_version = requirement[:metadata][:latest] + existing_precision = requirement[:requirement].split(".").size + updated_requirement = latest_version.split(".")[0...existing_precision].join(".") + + { + file: requirement[:file], + requirement: updated_requirement, + groups: requirement[:groups], + source: requirement[:source], + metadata: requirement[:metadata] + } + end + end + + def latest_resolvable_version_with_no_unlock + raise NotImplementedError + end + + private + + def latest_version_resolvable_with_full_unlock? + false # TODO + end + + def updated_dependencies_after_full_unlock + raise NotImplementedError + end end end end diff --git a/devcontainers/spec/dependabot/devcontainers/update_checker_spec.rb b/devcontainers/spec/dependabot/devcontainers/update_checker_spec.rb new file mode 100644 index 00000000000..73c67d00577 --- /dev/null +++ b/devcontainers/spec/dependabot/devcontainers/update_checker_spec.rb @@ -0,0 +1,80 @@ +# typed: false +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/dependency" +require "dependabot/devcontainers/file_parser" +require "dependabot/devcontainers/update_checker" +require_common_spec "update_checkers/shared_examples_for_update_checkers" + +RSpec.describe Dependabot::Devcontainers::UpdateChecker do + it_behaves_like "an update checker" + + let(:checker) do + described_class.new( + dependency: dependency, + dependency_files: dependency_files, + repo_contents_path: repo_contents_path, + credentials: github_credentials, + security_advisories: security_advisories, + ignored_versions: ignored_versions, + raise_on_ignored: raise_on_ignored + ) + end + + let(:repo_contents_path) { build_tmp_repo(project_name, path: "projects") } + let(:dependency_files) { project_dependency_files(project_name, directory: directory) } + let(:security_advisories) { [] } + let(:ignored_versions) { [] } + let(:raise_on_ignored) { false } + + let(:dependencies) do + file_parser.parse + end + + let(:file_parser) do + Dependabot::Devcontainers::FileParser.new( + dependency_files: dependency_files, + repo_contents_path: repo_contents_path, + source: nil + ) + end + + let(:dependency) { dependencies.find { |dep| dep.name == name } } + + context "Feature that is out-of-date" do + let(:name) { "ghcr.io/codspace/versioning/foo" } + + describe "config in root" do + let(:project_name) { "config_in_root" } + let(:directory) { "/" } + subject { checker.up_to_date? } + it { is_expected.to be_falsey } + end + + describe "config in .devcontainer folder " do + let(:project_name) { "config_in_dot_devcontainer_folder" } + let(:directory) { "/.devcontainer" } + subject { checker.up_to_date? } + it { is_expected.to be_falsey } + end + end + + context "Feature that is already up-to-date" do + let(:name) { "ghcr.io/codspace/versioning/bar" } + + describe "config in root" do + let(:project_name) { "config_in_root" } + let(:directory) { "/" } + subject { checker.up_to_date? } + it { is_expected.to be_truthy } + end + + describe "config in .devcontainer folder " do + let(:project_name) { "config_in_dot_devcontainer_folder" } + let(:directory) { "/.devcontainer" } + subject { checker.up_to_date? } + it { is_expected.to be_truthy } + end + end +end From 81e2210f121c85b22ea6071d751b688f5a6905e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= Date: Wed, 10 Jan 2024 18:33:51 +0100 Subject: [PATCH 04/10] Implement FileUpdater for devcontainers Co-authored-by: Josh Spicer --- .../dependabot/devcontainers/file_updater.rb | 72 ++++++- .../file_updater/config_updater.rb | 75 +++++++ .../devcontainers/file_updater_spec.rb | 189 ++++++++++++++++++ .../.devcontainer-lock.json | 14 ++ .../manifest_and_lockfile/.devcontainer.json | 7 + .../.devcontainer-lock.json | 14 ++ .../.devcontainer.json | 7 + 7 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 devcontainers/lib/dependabot/devcontainers/file_updater/config_updater.rb create mode 100644 devcontainers/spec/dependabot/devcontainers/file_updater_spec.rb create mode 100644 devcontainers/spec/fixtures/projects/manifest_and_lockfile/.devcontainer-lock.json create mode 100644 devcontainers/spec/fixtures/projects/manifest_and_lockfile/.devcontainer.json create mode 100644 devcontainers/spec/fixtures/projects/updated_manifest_outdated_lockfile/.devcontainer-lock.json create mode 100644 devcontainers/spec/fixtures/projects/updated_manifest_outdated_lockfile/.devcontainer.json diff --git a/devcontainers/lib/dependabot/devcontainers/file_updater.rb b/devcontainers/lib/dependabot/devcontainers/file_updater.rb index 28a5a22d199..0d1a43e78b5 100644 --- a/devcontainers/lib/dependabot/devcontainers/file_updater.rb +++ b/devcontainers/lib/dependabot/devcontainers/file_updater.rb @@ -1,12 +1,82 @@ -# typed: strong +# typed: true # frozen_string_literal: true require "dependabot/file_updaters" require "dependabot/file_updaters/base" +require "dependabot/devcontainers/file_updater/config_updater" module Dependabot module Devcontainers class FileUpdater < Dependabot::FileUpdaters::Base + def self.updated_files_regex + [ + /^\.?devcontainer\.json$/, + /^\.?devcontainer-lock\.json$/ + ] + end + + def updated_dependency_files + updated_files = [] + + manifests.each do |manifest| + requirement = dependency.requirements.find { |req| req[:file] == manifest.name } + next unless requirement + + config_contents, lockfile_contents = update(manifest, requirement) + + updated_files << updated_file(file: manifest, content: config_contents) if file_changed?(manifest) + + lockfile = lockfile_for(manifest) + + updated_files << updated_file(file: lockfile, content: lockfile_contents) if lockfile && lockfile_contents + end + + updated_files + end + + private + + def dependency + # TODO: Handle one dependency at a time + dependencies.first + end + + def check_required_files + return if dependency_files.any? + + raise "No dev container configuration!" + end + + def manifests + @manifests ||= dependency_files.select do |f| + f.name.end_with?("devcontainer.json") + end + end + + def lockfile_for(manifest) + lockfile_name = lockfile_name_for(manifest) + + dependency_files.find do |f| + f.name == lockfile_name + end + end + + def lockfile_name_for(manifest) + basename = File.basename(manifest.name) + lockfile_name = Utils.expected_lockfile_name(basename) + + manifest.name.delete_suffix(basename).concat(lockfile_name) + end + + def update(manifest, requirement) + ConfigUpdater.new( + feature: dependency.name, + requirement: requirement, + manifest: manifest, + repo_contents_path: repo_contents_path, + credentials: credentials + ).update + end end end end diff --git a/devcontainers/lib/dependabot/devcontainers/file_updater/config_updater.rb b/devcontainers/lib/dependabot/devcontainers/file_updater/config_updater.rb new file mode 100644 index 00000000000..177e7a1204b --- /dev/null +++ b/devcontainers/lib/dependabot/devcontainers/file_updater/config_updater.rb @@ -0,0 +1,75 @@ +# typed: true +# frozen_string_literal: true + +require "dependabot/file_updaters/base" +require "dependabot/shared_helpers" +require "dependabot/logger" +require "dependabot/devcontainers/utils" + +module Dependabot + module Devcontainers + class FileUpdater < Dependabot::FileUpdaters::Base + class ConfigUpdater + def initialize(feature:, requirement:, manifest:, repo_contents_path:, credentials:) + @feature = feature + @requirement = requirement + @manifest = manifest + @repo_contents_path = repo_contents_path + @credentials = credentials + end + + def update + SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do + SharedHelpers.with_git_configured(credentials: credentials) do + update_manifest( + target_requirement: requirement[:requirement], + target_version: requirement[:metadata][:latest] + ) + + [File.read(manifest_name), File.read(lockfile_name)].compact + end + end + end + + private + + def base_dir + File.dirname(manifest.name) + end + + def manifest_name + File.basename(manifest.name) + end + + def lockfile_name + Utils.expected_lockfile_name(manifest_name) + end + + def update_manifest(target_requirement:, target_version:) + # First force target version to upgrade lockfile. I believe the CLI + # shoudl automatically upgrade the lockfile directly, but needs a + # explicit version at the moment. If + # https://github.com/devcontainers/cli/issues/715 is implement, this + # first invocation can be removed + run_devcontainer_upgrade(target_version) + + run_devcontainer_upgrade(target_requirement) + end + + def run_devcontainer_upgrade(target_version) + cmd = "devcontainer upgrade " \ + "--workspace-folder . " \ + "--feature #{feature} " \ + "--config #{manifest_name} " \ + "--target-version #{target_version}" + + Dependabot.logger.info("Running command: `#{cmd}`") + + SharedHelpers.run_shell_command(cmd, stderr_to_stdout: false) + end + + attr_reader :feature, :requirement, :manifest, :repo_contents_path, :credentials + end + end + end +end diff --git a/devcontainers/spec/dependabot/devcontainers/file_updater_spec.rb b/devcontainers/spec/dependabot/devcontainers/file_updater_spec.rb new file mode 100644 index 00000000000..897006b234f --- /dev/null +++ b/devcontainers/spec/dependabot/devcontainers/file_updater_spec.rb @@ -0,0 +1,189 @@ +# typed: false +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/dependency" +require "dependabot/dependency_file" +require "dependabot/devcontainers/file_updater" +require "dependabot/devcontainers/requirement" +require_common_spec "file_updaters/shared_examples_for_file_updaters" + +RSpec.describe Dependabot::Devcontainers::FileUpdater do + it_behaves_like "a dependency file updater" + + subject(:updater) do + described_class.new( + dependency_files: files, + dependencies: dependencies, + credentials: credentials, + repo_contents_path: repo_contents_path + ) + end + + let(:repo_contents_path) { build_tmp_repo(project_name) } + + let(:files) { project_dependency_files(project_name) } + + let(:credentials) do + [{ "type" => "git_source", "host" => "github.com", "username" => "x-access-token", "password" => "token" }] + end + + describe "#updated_dependency_files" do + subject { updater.updated_dependency_files } + + let(:dependencies) do + [ + Dependabot::Dependency.new( + name: "ghcr.io/codspace/versioning/foo", + version: "2.11.1", + previous_version: "1.1.0", + requirements: [{ + requirement: "2", + groups: ["feature"], + file: ".devcontainer.json", + source: nil, + metadata: { + wanted: "1.1.0", + latest: "2.11.1", + wanted_major: "1", + latest_major: "2" + } + }], + previous_requirements: [{ + requirement: "1", + groups: ["feature"], + file: ".devcontainer.json", + source: nil, + metadata: { + wanted: "1.1.0", + latest: "2.11.1", + wanted_major: "1", + latest_major: "2" + } + }], + package_manager: "devcontainers" + ) + ] + end + + context "when there's only a devcontainer.json file" do + let(:project_name) { "config_in_root" } + + it "updates the version in .devcontainer.json" do + expect(subject.size).to eq(1) + + config = subject.first + expect(config.name).to eq(".devcontainer.json") + expect(config.content).to include("ghcr.io/codspace/versioning/foo:2\"") + end + end + + context "when there's both manifest and lockfile" do + let(:project_name) { "manifest_and_lockfile" } + + it "updates the version in both files" do + expect(subject.size).to eq(2) + + config = subject.find { |f| f.name == ".devcontainer.json" } + expect(config.content).to include("ghcr.io/codspace/versioning/foo:2\"") + + lockfile = subject.find { |f| f.name == ".devcontainer-lock.json" } + expect(lockfile.content).to include('"version": "2.11.1"') + end + end + + context "when there are multiple manifests, but only one needs updates" do + let(:project_name) { "multiple_configs" } + + let(:dependencies) do + [ + Dependabot::Dependency.new( + name: "ghcr.io/codspace/versioning/baz", + version: "2.0.0", + previous_version: "1.1.0", + requirements: [{ + requirement: "2.0", + groups: ["feature"], + file: ".devcontainer/devcontainer.json", + source: nil, + metadata: { + wanted: "2.0.0", + latest: "2.0.0", + wanted_major: "2", + latest_major: "2" + } + }], + previous_requirements: [{ + requirement: "1.0", + groups: ["feature"], + file: ".devcontainer/devcontainer.json", + source: nil, + metadata: { + wanted: "1.0.0", + latest: "2.0.0", + wanted_major: "1", + latest_major: "2" + } + }], + package_manager: "devcontainers" + ) + ] + end + + it "updates the version in both manifests" do + expect(subject.size).to eq(1) + + config = subject.first + expect(config.name).to eq(".devcontainer/devcontainer.json") + expect(config.content).to include("ghcr.io/codspace/versioning/baz:2.0\"") + end + end + + context "when there's both manifest and lockfile, but only the lockfile needs updates" do + let(:project_name) { "updated_manifest_outdated_lockfile" } + + let(:dependencies) do + [ + Dependabot::Dependency.new( + name: "ghcr.io/codspace/versioning/foo", + version: "2.11.1", + previous_version: "2.11.0", + requirements: [{ + requirement: "2", + groups: ["feature"], + file: ".devcontainer.json", + source: nil, + metadata: { + wanted: "2.11.1", + latest: "2.11.1", + wanted_major: "2", + latest_major: "2" + } + }], + previous_requirements: [{ + requirement: "2", + groups: ["feature"], + file: ".devcontainer.json", + source: nil, + metadata: { + wanted: "2.11.1", + latest: "2.11.1", + wanted_major: "2", + latest_major: "2" + } + }], + package_manager: "devcontainers" + ) + ] + end + + it "updates the version in lockfile" do + expect(subject.size).to eq(1) + + lockfile = subject.first + expect(lockfile.name).to eq(".devcontainer-lock.json") + expect(lockfile.content).to include('"version": "2.11.1"') + end + end + end +end diff --git a/devcontainers/spec/fixtures/projects/manifest_and_lockfile/.devcontainer-lock.json b/devcontainers/spec/fixtures/projects/manifest_and_lockfile/.devcontainer-lock.json new file mode 100644 index 00000000000..f7dd4b4e749 --- /dev/null +++ b/devcontainers/spec/fixtures/projects/manifest_and_lockfile/.devcontainer-lock.json @@ -0,0 +1,14 @@ +{ + "features": { + "ghcr.io/codspace/versioning/bar:1": { + "version": "1.0.0", + "resolved": "ghcr.io/codspace/versioning/bar@sha256:0eb80a7a45ea6ac6d2057798608be4cacb3d3667d4818118e17acc5037d687d4", + "integrity": "sha256:0eb80a7a45ea6ac6d2057798608be4cacb3d3667d4818118e17acc5037d687d4" + }, + "ghcr.io/codspace/versioning/foo:1": { + "version": "1.1.0", + "resolved": "ghcr.io/codspace/versioning/foo@sha256:80d2d7b58afeaf907451c6f4e24de47b09a327a24a21a2d3323b7abf76d14be5", + "integrity": "sha256:80d2d7b58afeaf907451c6f4e24de47b09a327a24a21a2d3323b7abf76d14be5" + } + } +} \ No newline at end of file diff --git a/devcontainers/spec/fixtures/projects/manifest_and_lockfile/.devcontainer.json b/devcontainers/spec/fixtures/projects/manifest_and_lockfile/.devcontainer.json new file mode 100644 index 00000000000..79b80211423 --- /dev/null +++ b/devcontainers/spec/fixtures/projects/manifest_and_lockfile/.devcontainer.json @@ -0,0 +1,7 @@ +{ + "image": "mcr.microsoft.com/devcontainers/typescript-node:18", + "features": { + "ghcr.io/codspace/versioning/foo:1": {}, + "ghcr.io/codspace/versioning/bar:1": {} + } +} \ No newline at end of file diff --git a/devcontainers/spec/fixtures/projects/updated_manifest_outdated_lockfile/.devcontainer-lock.json b/devcontainers/spec/fixtures/projects/updated_manifest_outdated_lockfile/.devcontainer-lock.json new file mode 100644 index 00000000000..0520cfd43bc --- /dev/null +++ b/devcontainers/spec/fixtures/projects/updated_manifest_outdated_lockfile/.devcontainer-lock.json @@ -0,0 +1,14 @@ +{ + "features": { + "ghcr.io/codspace/versioning/bar:1": { + "version": "1.0.0", + "resolved": "ghcr.io/codspace/versioning/bar@sha256:0eb80a7a45ea6ac6d2057798608be4cacb3d3667d4818118e17acc5037d687d4", + "integrity": "sha256:0eb80a7a45ea6ac6d2057798608be4cacb3d3667d4818118e17acc5037d687d4" + }, + "ghcr.io/codspace/versioning/foo:2": { + "version": "2.11.0", + "resolved": "ghcr.io/codspace/versioning/foo@sha256:9b5b5f165f7bff54d1b70d7228d63cb8d8a6b9f20fee6db772b520c3391beaa2", + "integrity": "sha256:9b5b5f165f7bff54d1b70d7228d63cb8d8a6b9f20fee6db772b520c3391beaa2" + } + } +} \ No newline at end of file diff --git a/devcontainers/spec/fixtures/projects/updated_manifest_outdated_lockfile/.devcontainer.json b/devcontainers/spec/fixtures/projects/updated_manifest_outdated_lockfile/.devcontainer.json new file mode 100644 index 00000000000..22b38fb4335 --- /dev/null +++ b/devcontainers/spec/fixtures/projects/updated_manifest_outdated_lockfile/.devcontainer.json @@ -0,0 +1,7 @@ +{ + "image": "mcr.microsoft.com/devcontainers/typescript-node:18", + "features": { + "ghcr.io/codspace/versioning/foo:2": {}, + "ghcr.io/codspace/versioning/bar:1": {} + } +} From 3dd36dab328da83970b7a65fb44d75f7fcb237bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= Date: Thu, 18 Jan 2024 10:54:12 +0100 Subject: [PATCH 05/10] Skip SHA pinned versions for now --- .../file_parser/feature_dependency_parser.rb | 4 ++++ .../spec/dependabot/devcontainers/file_parser_spec.rb | 9 +++++++++ .../spec/fixtures/projects/sha_pinned/.devcontainer.json | 6 ++++++ 3 files changed, 19 insertions(+) create mode 100644 devcontainers/spec/fixtures/projects/sha_pinned/.devcontainer.json diff --git a/devcontainers/lib/dependabot/devcontainers/file_parser/feature_dependency_parser.rb b/devcontainers/lib/dependabot/devcontainers/file_parser/feature_dependency_parser.rb index e0544096bd9..f3fddd101a6 100644 --- a/devcontainers/lib/dependabot/devcontainers/file_parser/feature_dependency_parser.rb +++ b/devcontainers/lib/dependabot/devcontainers/file_parser/feature_dependency_parser.rb @@ -61,6 +61,10 @@ def parse_cli_json(json) features = json["features"] features.each do |feature, versions_object| name, requirement = feature.split(":") + + # Skip sha pinned tags for now. Ideally the devcontainers CLI would give us updated SHA info + next if name.end_with?("@sha256") + current = versions_object["current"] wanted = versions_object["wanted"] latest = versions_object["latest"] diff --git a/devcontainers/spec/dependabot/devcontainers/file_parser_spec.rb b/devcontainers/spec/dependabot/devcontainers/file_parser_spec.rb index 461ba745def..a922b7075ff 100644 --- a/devcontainers/spec/dependabot/devcontainers/file_parser_spec.rb +++ b/devcontainers/spec/dependabot/devcontainers/file_parser_spec.rb @@ -263,4 +263,13 @@ it_behaves_like "parse" end + + context "with SHA-pinned features" do + let(:project_name) { "sha_pinned" } + let(:directory) { "/" } + + it "ignores them" do + expect(dependencies).to be_empty + end + end end diff --git a/devcontainers/spec/fixtures/projects/sha_pinned/.devcontainer.json b/devcontainers/spec/fixtures/projects/sha_pinned/.devcontainer.json new file mode 100644 index 00000000000..859f5fd69c6 --- /dev/null +++ b/devcontainers/spec/fixtures/projects/sha_pinned/.devcontainer.json @@ -0,0 +1,6 @@ +{ + "image": "mcr.microsoft.com/devcontainers/base:jammy", + "features": { + "ghcr.io/devcontainers/features/aws-cli@sha256:4b67518ec3733df53110c3caf7b9d6007363d1b5aaae2f1fa27f18ad0cc9b2b9": {}, + } +} From c98db9d351ec92f4e5f130771dfa25c47ee7bfae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= Date: Thu, 18 Jan 2024 10:29:15 +0100 Subject: [PATCH 06/10] Support ignores --- .../file_updater/config_updater.rb | 18 ++-- .../devcontainers/update_checker.rb | 75 ++++++++++++++-- .../lib/dependabot/devcontainers/version.rb | 23 ++++- .../devcontainers/update_checker_spec.rb | 90 ++++++++++++++----- 4 files changed, 166 insertions(+), 40 deletions(-) diff --git a/devcontainers/lib/dependabot/devcontainers/file_updater/config_updater.rb b/devcontainers/lib/dependabot/devcontainers/file_updater/config_updater.rb index 177e7a1204b..1a2efbe0a3a 100644 --- a/devcontainers/lib/dependabot/devcontainers/file_updater/config_updater.rb +++ b/devcontainers/lib/dependabot/devcontainers/file_updater/config_updater.rb @@ -21,7 +21,7 @@ def initialize(feature:, requirement:, manifest:, repo_contents_path:, credentia def update SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do SharedHelpers.with_git_configured(credentials: credentials) do - update_manifest( + update_manifests( target_requirement: requirement[:requirement], target_version: requirement[:metadata][:latest] ) @@ -45,15 +45,17 @@ def lockfile_name Utils.expected_lockfile_name(manifest_name) end - def update_manifest(target_requirement:, target_version:) - # First force target version to upgrade lockfile. I believe the CLI - # shoudl automatically upgrade the lockfile directly, but needs a - # explicit version at the moment. If - # https://github.com/devcontainers/cli/issues/715 is implement, this - # first invocation can be removed + def update_manifests(target_requirement:, target_version:) + # First force target version to upgrade lockfile. run_devcontainer_upgrade(target_version) - run_devcontainer_upgrade(target_requirement) + # Now replace specific version back with target requirement + force_target_requirement(manifest_name, from: target_version, to: target_requirement) + force_target_requirement(lockfile_name, from: target_version, to: target_requirement) + end + + def force_target_requirement(file_name, from:, to:) + File.write(file_name, File.read(file_name).gsub("#{feature}:#{from}", "#{feature}:#{to}")) end def run_devcontainer_upgrade(target_version) diff --git a/devcontainers/lib/dependabot/devcontainers/update_checker.rb b/devcontainers/lib/dependabot/devcontainers/update_checker.rb index e66a6df17a9..c63f85535cf 100644 --- a/devcontainers/lib/dependabot/devcontainers/update_checker.rb +++ b/devcontainers/lib/dependabot/devcontainers/update_checker.rb @@ -11,9 +11,7 @@ module Dependabot module Devcontainers class UpdateChecker < Dependabot::UpdateCheckers::Base def latest_version - @latest_version ||= dependency.requirements.map do |requirement| - Version.new(requirement[:metadata][:latest]) - end.max + @latest_version ||= fetch_latest_version end def latest_resolvable_version @@ -22,16 +20,16 @@ def latest_resolvable_version def updated_requirements dependency.requirements.map do |requirement| - latest_version = requirement[:metadata][:latest] - existing_precision = requirement[:requirement].split(".").size - updated_requirement = latest_version.split(".")[0...existing_precision].join(".") + required_version = version_class.new(requirement[:requirement]) + updated_requirement = remove_precision_changes(viable_candidates, required_version).last + updated_metadata = requirement[:metadata].update(latest: latest_version) { file: requirement[:file], requirement: updated_requirement, groups: requirement[:groups], source: requirement[:source], - metadata: requirement[:metadata] + metadata: updated_metadata } end end @@ -42,6 +40,69 @@ def latest_resolvable_version_with_no_unlock private + def viable_candidates + @viable_candidates ||= fetch_viable_candidates + end + + def fetch_viable_candidates + candidates = comparable_versions_from_registry + candidates = filter_ignored(candidates) + candidates.sort + end + + def fetch_latest_version + return current_version unless viable_candidates.any? + + viable_candidates.last + end + + def remove_precision_changes(versions, required_version) + versions.select do |version| + version.same_precision?(required_version) + end + end + + def filter_ignored(versions) + filtered = + versions.reject do |version| + ignore_requirements.any? { |r| version.satisfies?(r) } + end + + if @raise_on_ignored && + filter_lower_versions(filtered).empty? && + filter_lower_versions(versions).any? + raise AllVersionsIgnored + end + + filtered + end + + def comparable_versions_from_registry + tags_from_registry.filter_map do |tag| + version_class.correct?(tag) && version_class.new(tag) + end + end + + def tags_from_registry + @tags_from_registry ||= fetch_tags_from_registry + end + + def fetch_tags_from_registry + cmd = "devcontainer features info tags #{dependency.name} --output-format json" + + Dependabot.logger.info("Running command: `#{cmd}`") + + output = SharedHelpers.run_shell_command(cmd, stderr_to_stdout: false) + + JSON.parse(output).fetch("publishedTags") + end + + def filter_lower_versions(versions) + versions.select do |version| + version > current_version + end + end + def latest_version_resolvable_with_full_unlock? false # TODO end diff --git a/devcontainers/lib/dependabot/devcontainers/version.rb b/devcontainers/lib/dependabot/devcontainers/version.rb index 9d19a11c6e7..7077ef5bc46 100644 --- a/devcontainers/lib/dependabot/devcontainers/version.rb +++ b/devcontainers/lib/dependabot/devcontainers/version.rb @@ -1,4 +1,4 @@ -# typed: strong +# typed: true # frozen_string_literal: true require "dependabot/version" @@ -7,6 +7,27 @@ module Dependabot module Devcontainers class Version < Dependabot::Version + def same_precision?(other) + precision == other.precision + end + + def satisfies?(requirement) + requirement.satisfied_by?(self) + end + + def <=>(other) + if self == other + precision <=> other.precision + else + super + end + end + + protected + + def precision + segments.size + end end end end diff --git a/devcontainers/spec/dependabot/devcontainers/update_checker_spec.rb b/devcontainers/spec/dependabot/devcontainers/update_checker_spec.rb index 73c67d00577..bf5b5d56be6 100644 --- a/devcontainers/spec/dependabot/devcontainers/update_checker_spec.rb +++ b/devcontainers/spec/dependabot/devcontainers/update_checker_spec.rb @@ -42,39 +42,81 @@ let(:dependency) { dependencies.find { |dep| dep.name == name } } - context "Feature that is out-of-date" do - let(:name) { "ghcr.io/codspace/versioning/foo" } + shared_context "in root" do + let(:project_name) { "config_in_root" } + let(:directory) { "/" } + end + + describe "#up_to_date?" do + subject { checker.up_to_date? } + + context "when feature is out-of-date" do + let(:name) { "ghcr.io/codspace/versioning/foo" } + + context "and config in root" do + include_context "in root" - describe "config in root" do - let(:project_name) { "config_in_root" } - let(:directory) { "/" } - subject { checker.up_to_date? } - it { is_expected.to be_falsey } + it { is_expected.to be_falsey } + end + + context "and config in .devcontainer folder " do + let(:project_name) { "config_in_dot_devcontainer_folder" } + let(:directory) { "/.devcontainer" } + + it { is_expected.to be_falsey } + end end - describe "config in .devcontainer folder " do - let(:project_name) { "config_in_dot_devcontainer_folder" } - let(:directory) { "/.devcontainer" } - subject { checker.up_to_date? } - it { is_expected.to be_falsey } + context "when feature is already up-to-date" do + let(:name) { "ghcr.io/codspace/versioning/bar" } + + context "and config in root" do + include_context "in root" + + it { is_expected.to be_truthy } + end + + context "and config in .devcontainer folder " do + let(:project_name) { "config_in_dot_devcontainer_folder" } + let(:directory) { "/.devcontainer" } + + it { is_expected.to be_truthy } + end end end - context "Feature that is already up-to-date" do - let(:name) { "ghcr.io/codspace/versioning/bar" } + describe "#latest_version" do + subject { checker.latest_version.to_s } + + let(:name) { "ghcr.io/codspace/versioning/foo" } + let(:current_version) { "1.1.0" } + + include_context "in root" + + context "when all later versions are being ignored" do + let(:ignored_versions) { ["> #{current_version}"] } - describe "config in root" do - let(:project_name) { "config_in_root" } - let(:directory) { "/" } - subject { checker.up_to_date? } - it { is_expected.to be_truthy } + it { is_expected.to eq(current_version) } + + context "raise_on_ignored" do + let(:raise_on_ignored) { true } + + it "raises an error" do + expect { subject }.to raise_error(Dependabot::AllVersionsIgnored) + end + end end - describe "config in .devcontainer folder " do - let(:project_name) { "config_in_dot_devcontainer_folder" } - let(:directory) { "/.devcontainer" } - subject { checker.up_to_date? } - it { is_expected.to be_truthy } + context "when some later versions are not ignored" do + let(:ignored_versions) { [">= 2.1.0"] } + + it { is_expected.to eq("2.0.0") } + + context "raise_on_ignored" do + let(:raise_on_ignored) { true } + + it { is_expected.to eq("2.0.0") } + end end end end From 7f838f6ce65069a7b6640789574c8e58cd497ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= Date: Thu, 18 Jan 2024 14:07:35 +0100 Subject: [PATCH 07/10] Fix updater misbehaving when a custom `directory` is configured For example, ``` bin/dry-run.rb devcontainers devcontainers/images --dir /src/go ``` would update manifests with the wrong content because it was taking the contents of the manifests in root as the updated content. --- .../file_updater/config_updater.rb | 4 +- .../devcontainers/file_updater_spec.rb | 52 ++++++++++++++++++- .../.devcontainer/devcontainer.json | 7 +++ .../go/.devcontainer/devcontainer-lock.json | 9 ++++ .../src/go/.devcontainer/devcontainer.json | 15 ++++++ 5 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 devcontainers/spec/fixtures/projects/multiple_roots/.devcontainer/devcontainer.json create mode 100644 devcontainers/spec/fixtures/projects/multiple_roots/src/go/.devcontainer/devcontainer-lock.json create mode 100644 devcontainers/spec/fixtures/projects/multiple_roots/src/go/.devcontainer/devcontainer.json diff --git a/devcontainers/lib/dependabot/devcontainers/file_updater/config_updater.rb b/devcontainers/lib/dependabot/devcontainers/file_updater/config_updater.rb index 1a2efbe0a3a..168b3ef968c 100644 --- a/devcontainers/lib/dependabot/devcontainers/file_updater/config_updater.rb +++ b/devcontainers/lib/dependabot/devcontainers/file_updater/config_updater.rb @@ -34,11 +34,11 @@ def update private def base_dir - File.dirname(manifest.name) + File.dirname(manifest.path) end def manifest_name - File.basename(manifest.name) + File.basename(manifest.path) end def lockfile_name diff --git a/devcontainers/spec/dependabot/devcontainers/file_updater_spec.rb b/devcontainers/spec/dependabot/devcontainers/file_updater_spec.rb index 897006b234f..dc221801d5e 100644 --- a/devcontainers/spec/dependabot/devcontainers/file_updater_spec.rb +++ b/devcontainers/spec/dependabot/devcontainers/file_updater_spec.rb @@ -22,7 +22,8 @@ let(:repo_contents_path) { build_tmp_repo(project_name) } - let(:files) { project_dependency_files(project_name) } + let(:files) { project_dependency_files(project_name, directory: directory) } + let(:directory) { "/" } let(:credentials) do [{ "type" => "git_source", "host" => "github.com", "username" => "x-access-token", "password" => "token" }] @@ -185,5 +186,54 @@ expect(lockfile.content).to include('"version": "2.11.1"') end end + + context "when a custom directory is configured" do + let(:directory) { "src/go" } + let(:project_name) { "multiple_roots" } + + let(:dependencies) do + [ + Dependabot::Dependency.new( + name: "ghcr.io/devcontainers/features/common-utils", + version: "2.3.2", + previous_version: "2.4.0", + requirements: [{ + requirement: "2", + groups: ["feature"], + file: ".devcontainer/devcontainer.json", + source: nil, + metadata: { + wanted: "2.4.0", + latest: "2.4.0", + wanted_major: "2", + latest_major: "2" + } + }], + previous_requirements: [{ + requirement: "2", + groups: ["feature"], + file: ".devcontainer/devcontainer.json", + source: nil, + metadata: { + wanted: "2.4.0", + latest: "2.4.0", + wanted_major: "2", + latest_major: "2" + } + }], + package_manager: "devcontainers" + ) + ] + end + + it "updates the version in lockfile" do + expect(subject.size).to eq(1) + + config = subject.first + expect(config.name).to eq(".devcontainer/devcontainer-lock.json") + expect(config.content).to include("ghcr.io/devcontainers/features/common-utils:2") + expect(config.content).to include('"version": "2.4.0"') + end + end end end diff --git a/devcontainers/spec/fixtures/projects/multiple_roots/.devcontainer/devcontainer.json b/devcontainers/spec/fixtures/projects/multiple_roots/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..ff4d459b63d --- /dev/null +++ b/devcontainers/spec/fixtures/projects/multiple_roots/.devcontainer/devcontainer.json @@ -0,0 +1,7 @@ +{ + "image": "mcr.microsoft.com/devcontainers/javascript-node:0-18", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers/features/azure-cli:1": {} + } +} diff --git a/devcontainers/spec/fixtures/projects/multiple_roots/src/go/.devcontainer/devcontainer-lock.json b/devcontainers/spec/fixtures/projects/multiple_roots/src/go/.devcontainer/devcontainer-lock.json new file mode 100644 index 00000000000..fc241c2fc2a --- /dev/null +++ b/devcontainers/spec/fixtures/projects/multiple_roots/src/go/.devcontainer/devcontainer-lock.json @@ -0,0 +1,9 @@ +{ + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "version": "2.4.0", + "resolved": "ghcr.io/devcontainers/features/common-utils@sha256:cd9c4413255c3b71fb716e63ee2df245c81c7262b858cf77406a68c80d09f12e", + "integrity": "sha256:cd9c4413255c3b71fb716e63ee2df245c81c7262b858cf77406a68c80d09f12e" + } + } +} diff --git a/devcontainers/spec/fixtures/projects/multiple_roots/src/go/.devcontainer/devcontainer.json b/devcontainers/spec/fixtures/projects/multiple_roots/src/go/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..619cffd1db3 --- /dev/null +++ b/devcontainers/spec/fixtures/projects/multiple_roots/src/go/.devcontainer/devcontainer.json @@ -0,0 +1,15 @@ +{ + "build": { + "dockerfile": "./Dockerfile", + "context": "." + }, + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": "true", + "username": "vscode", + "userUid": "1000", + "userGid": "1000", + "upgradePackages": "true" + } + } +} From 4abd40222a2e197f65c7dd47f3adcf89dcbb3a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= Date: Thu, 18 Jan 2024 16:53:40 +0100 Subject: [PATCH 08/10] We don't need to pass metadata for now --- .../file_parser/feature_dependency_parser.rb | 15 +--- .../dependabot/devcontainers/file_updater.rb | 3 +- .../file_updater/config_updater.rb | 9 ++- .../devcontainers/update_checker.rb | 4 +- .../devcontainers/file_parser_spec.rb | 80 +++---------------- .../devcontainers/file_updater_spec.rb | 68 +++------------- 6 files changed, 30 insertions(+), 149 deletions(-) diff --git a/devcontainers/lib/dependabot/devcontainers/file_parser/feature_dependency_parser.rb b/devcontainers/lib/dependabot/devcontainers/file_parser/feature_dependency_parser.rb index f3fddd101a6..ee49dc69863 100644 --- a/devcontainers/lib/dependabot/devcontainers/file_parser/feature_dependency_parser.rb +++ b/devcontainers/lib/dependabot/devcontainers/file_parser/feature_dependency_parser.rb @@ -66,10 +66,6 @@ def parse_cli_json(json) next if name.end_with?("@sha256") current = versions_object["current"] - wanted = versions_object["wanted"] - latest = versions_object["latest"] - wanted_major = versions_object["wantedMajor"] - latest_major = versions_object["latestMajor"] dep = Dependency.new( name: name, @@ -80,16 +76,9 @@ def parse_cli_json(json) requirement: requirement, file: config_dependency_file.name, groups: ["feature"], - source: nil, - metadata: { - wanted: wanted, - latest: latest, - wanted_major: wanted_major, - latest_major: latest_major - } + source: nil } - ], - metadata: {} + ] ) dependencies << dep diff --git a/devcontainers/lib/dependabot/devcontainers/file_updater.rb b/devcontainers/lib/dependabot/devcontainers/file_updater.rb index 0d1a43e78b5..30fb765cdf9 100644 --- a/devcontainers/lib/dependabot/devcontainers/file_updater.rb +++ b/devcontainers/lib/dependabot/devcontainers/file_updater.rb @@ -71,7 +71,8 @@ def lockfile_name_for(manifest) def update(manifest, requirement) ConfigUpdater.new( feature: dependency.name, - requirement: requirement, + requirement: requirement[:requirement], + version: dependency.version, manifest: manifest, repo_contents_path: repo_contents_path, credentials: credentials diff --git a/devcontainers/lib/dependabot/devcontainers/file_updater/config_updater.rb b/devcontainers/lib/dependabot/devcontainers/file_updater/config_updater.rb index 168b3ef968c..f24f5e7d8b5 100644 --- a/devcontainers/lib/dependabot/devcontainers/file_updater/config_updater.rb +++ b/devcontainers/lib/dependabot/devcontainers/file_updater/config_updater.rb @@ -10,9 +10,10 @@ module Dependabot module Devcontainers class FileUpdater < Dependabot::FileUpdaters::Base class ConfigUpdater - def initialize(feature:, requirement:, manifest:, repo_contents_path:, credentials:) + def initialize(feature:, requirement:, version:, manifest:, repo_contents_path:, credentials:) @feature = feature @requirement = requirement + @version = version @manifest = manifest @repo_contents_path = repo_contents_path @credentials = credentials @@ -22,8 +23,8 @@ def update SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do SharedHelpers.with_git_configured(credentials: credentials) do update_manifests( - target_requirement: requirement[:requirement], - target_version: requirement[:metadata][:latest] + target_requirement: requirement, + target_version: version ) [File.read(manifest_name), File.read(lockfile_name)].compact @@ -70,7 +71,7 @@ def run_devcontainer_upgrade(target_version) SharedHelpers.run_shell_command(cmd, stderr_to_stdout: false) end - attr_reader :feature, :requirement, :manifest, :repo_contents_path, :credentials + attr_reader :feature, :requirement, :version, :manifest, :repo_contents_path, :credentials end end end diff --git a/devcontainers/lib/dependabot/devcontainers/update_checker.rb b/devcontainers/lib/dependabot/devcontainers/update_checker.rb index c63f85535cf..08a1da621ef 100644 --- a/devcontainers/lib/dependabot/devcontainers/update_checker.rb +++ b/devcontainers/lib/dependabot/devcontainers/update_checker.rb @@ -22,14 +22,12 @@ def updated_requirements dependency.requirements.map do |requirement| required_version = version_class.new(requirement[:requirement]) updated_requirement = remove_precision_changes(viable_candidates, required_version).last - updated_metadata = requirement[:metadata].update(latest: latest_version) { file: requirement[:file], requirement: updated_requirement, groups: requirement[:groups], - source: requirement[:source], - metadata: updated_metadata + source: requirement[:source] } end end diff --git a/devcontainers/spec/dependabot/devcontainers/file_parser_spec.rb b/devcontainers/spec/dependabot/devcontainers/file_parser_spec.rb index a922b7075ff..04d809d81d9 100644 --- a/devcontainers/spec/dependabot/devcontainers/file_parser_spec.rb +++ b/devcontainers/spec/dependabot/devcontainers/file_parser_spec.rb @@ -66,13 +66,7 @@ requirement: "1", file: ".devcontainer.json", groups: ["feature"], - source: nil, - metadata: { - wanted: "1.1.0", - latest: "2.11.1", - latest_major: "2", - wanted_major: "1" - } + source: nil } ], metadata: {} @@ -85,13 +79,7 @@ requirement: "1", file: ".devcontainer.json", groups: ["feature"], - source: nil, - metadata: { - wanted: "1.0.0", - latest: "1.0.0", - latest_major: "1", - wanted_major: "1" - } + source: nil } ], metadata: {} @@ -116,13 +104,7 @@ requirement: "1", file: ".devcontainer/devcontainer.json", groups: ["feature"], - source: nil, - metadata: { - wanted: "1.1.0", - latest: "2.11.1", - latest_major: "2", - wanted_major: "1" - } + source: nil } ], metadata: {} @@ -135,13 +117,7 @@ requirement: "1", file: ".devcontainer/devcontainer.json", groups: ["feature"], - source: nil, - metadata: { - wanted: "1.0.0", - latest: "1.0.0", - latest_major: "1", - wanted_major: "1" - } + source: nil } ], metadata: {} @@ -154,13 +130,7 @@ requirement: "1.0", file: ".devcontainer/devcontainer.json", groups: ["feature"], - source: nil, - metadata: { - wanted: "1.0.0", - latest: "2.0.0", - latest_major: "2", - wanted_major: "1" - } + source: nil } ], metadata: {} @@ -185,25 +155,13 @@ requirement: "1", file: ".devcontainer/devcontainer.json", groups: ["feature"], - source: nil, - metadata: { - wanted: "1.1.0", - latest: "2.11.1", - latest_major: "2", - wanted_major: "1" - } + source: nil }, { requirement: "1", file: ".devcontainer.json", groups: ["feature"], - source: nil, - metadata: { - wanted: "1.1.0", - latest: "2.11.1", - latest_major: "2", - wanted_major: "1" - } + source: nil } ], metadata: {} @@ -216,25 +174,13 @@ requirement: "1", file: ".devcontainer/devcontainer.json", groups: ["feature"], - source: nil, - metadata: { - wanted: "1.0.0", - latest: "1.0.0", - latest_major: "1", - wanted_major: "1" - } + source: nil }, { requirement: "1", file: ".devcontainer.json", groups: ["feature"], - source: nil, - metadata: { - wanted: "1.0.0", - latest: "1.0.0", - latest_major: "1", - wanted_major: "1" - } + source: nil } ], metadata: {} @@ -247,13 +193,7 @@ requirement: "1.0", file: ".devcontainer/devcontainer.json", groups: ["feature"], - source: nil, - metadata: { - wanted: "1.0.0", - latest: "2.0.0", - latest_major: "2", - wanted_major: "1" - } + source: nil } ], metadata: {} diff --git a/devcontainers/spec/dependabot/devcontainers/file_updater_spec.rb b/devcontainers/spec/dependabot/devcontainers/file_updater_spec.rb index dc221801d5e..bf67fb72672 100644 --- a/devcontainers/spec/dependabot/devcontainers/file_updater_spec.rb +++ b/devcontainers/spec/dependabot/devcontainers/file_updater_spec.rb @@ -42,25 +42,13 @@ requirement: "2", groups: ["feature"], file: ".devcontainer.json", - source: nil, - metadata: { - wanted: "1.1.0", - latest: "2.11.1", - wanted_major: "1", - latest_major: "2" - } + source: nil }], previous_requirements: [{ requirement: "1", groups: ["feature"], file: ".devcontainer.json", - source: nil, - metadata: { - wanted: "1.1.0", - latest: "2.11.1", - wanted_major: "1", - latest_major: "2" - } + source: nil }], package_manager: "devcontainers" ) @@ -106,25 +94,13 @@ requirement: "2.0", groups: ["feature"], file: ".devcontainer/devcontainer.json", - source: nil, - metadata: { - wanted: "2.0.0", - latest: "2.0.0", - wanted_major: "2", - latest_major: "2" - } + source: nil }], previous_requirements: [{ requirement: "1.0", groups: ["feature"], file: ".devcontainer/devcontainer.json", - source: nil, - metadata: { - wanted: "1.0.0", - latest: "2.0.0", - wanted_major: "1", - latest_major: "2" - } + source: nil }], package_manager: "devcontainers" ) @@ -153,25 +129,13 @@ requirement: "2", groups: ["feature"], file: ".devcontainer.json", - source: nil, - metadata: { - wanted: "2.11.1", - latest: "2.11.1", - wanted_major: "2", - latest_major: "2" - } + source: nil }], previous_requirements: [{ requirement: "2", groups: ["feature"], file: ".devcontainer.json", - source: nil, - metadata: { - wanted: "2.11.1", - latest: "2.11.1", - wanted_major: "2", - latest_major: "2" - } + source: nil }], package_manager: "devcontainers" ) @@ -195,31 +159,19 @@ [ Dependabot::Dependency.new( name: "ghcr.io/devcontainers/features/common-utils", - version: "2.3.2", - previous_version: "2.4.0", + version: "2.4.0", + previous_version: "2.3.2", requirements: [{ requirement: "2", groups: ["feature"], file: ".devcontainer/devcontainer.json", - source: nil, - metadata: { - wanted: "2.4.0", - latest: "2.4.0", - wanted_major: "2", - latest_major: "2" - } + source: nil }], previous_requirements: [{ requirement: "2", groups: ["feature"], file: ".devcontainer/devcontainer.json", - source: nil, - metadata: { - wanted: "2.4.0", - latest: "2.4.0", - wanted_major: "2", - latest_major: "2" - } + source: nil }], package_manager: "devcontainers" ) From 21f36e66ce944a53f0e175801c8c3dcf258e3bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= Date: Thu, 18 Jan 2024 17:06:18 +0100 Subject: [PATCH 09/10] Skip deprecated features for now --- .../file_parser/feature_dependency_parser.rb | 4 ++++ .../spec/dependabot/devcontainers/file_parser_spec.rb | 9 +++++++++ .../spec/fixtures/projects/deprecated/.devcontainer.json | 6 ++++++ 3 files changed, 19 insertions(+) create mode 100644 devcontainers/spec/fixtures/projects/deprecated/.devcontainer.json diff --git a/devcontainers/lib/dependabot/devcontainers/file_parser/feature_dependency_parser.rb b/devcontainers/lib/dependabot/devcontainers/file_parser/feature_dependency_parser.rb index ee49dc69863..46dc04074d0 100644 --- a/devcontainers/lib/dependabot/devcontainers/file_parser/feature_dependency_parser.rb +++ b/devcontainers/lib/dependabot/devcontainers/file_parser/feature_dependency_parser.rb @@ -65,6 +65,10 @@ def parse_cli_json(json) # Skip sha pinned tags for now. Ideally the devcontainers CLI would give us updated SHA info next if name.end_with?("@sha256") + # Skip deprecated features until `devcontainer features info tag` + # and `devcontainer upgrade` work with them. See https://github.com/devcontainers/cli/issues/712 + next unless name.include?("/") + current = versions_object["current"] dep = Dependency.new( diff --git a/devcontainers/spec/dependabot/devcontainers/file_parser_spec.rb b/devcontainers/spec/dependabot/devcontainers/file_parser_spec.rb index 04d809d81d9..c5ab9204318 100644 --- a/devcontainers/spec/dependabot/devcontainers/file_parser_spec.rb +++ b/devcontainers/spec/dependabot/devcontainers/file_parser_spec.rb @@ -212,4 +212,13 @@ expect(dependencies).to be_empty end end + + context "with deprecated features" do + let(:project_name) { "deprecated" } + let(:directory) { "/" } + + it "ignores them" do + expect(dependencies).to be_empty + end + end end diff --git a/devcontainers/spec/fixtures/projects/deprecated/.devcontainer.json b/devcontainers/spec/fixtures/projects/deprecated/.devcontainer.json new file mode 100644 index 00000000000..4b6ebbb52be --- /dev/null +++ b/devcontainers/spec/fixtures/projects/deprecated/.devcontainer.json @@ -0,0 +1,6 @@ +{ + "name": "docs.github.com", + "features": { + "sshd": "latest" + } +} From ae771dcc3a190652d50300a59b436dc78bb5629f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= Date: Thu, 18 Jan 2024 17:17:01 +0100 Subject: [PATCH 10/10] Cover file updater not updating past target version --- .../devcontainers/file_updater_spec.rb | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/devcontainers/spec/dependabot/devcontainers/file_updater_spec.rb b/devcontainers/spec/dependabot/devcontainers/file_updater_spec.rb index bf67fb72672..30ae8d87d29 100644 --- a/devcontainers/spec/dependabot/devcontainers/file_updater_spec.rb +++ b/devcontainers/spec/dependabot/devcontainers/file_updater_spec.rb @@ -187,5 +187,40 @@ expect(config.content).to include('"version": "2.4.0"') end end + + context "when target version is not the latest" do + let(:dependencies) do + [ + Dependabot::Dependency.new( + name: "ghcr.io/codspace/versioning/foo", + version: "2.10.0", + previous_version: "1.1.0", + requirements: [{ + requirement: "2", + groups: ["feature"], + file: ".devcontainer.json", + source: nil + }], + previous_requirements: [{ + requirement: "2", + groups: ["feature"], + file: ".devcontainer.json", + source: nil + }], + package_manager: "devcontainers" + ) + ] + end + + let(:project_name) { "updated_manifest_outdated_lockfile" } + + it "does not go past the target version in the lockfile" do + expect(subject.size).to eq(1) + + lockfile = subject.first + expect(lockfile.name).to eq(".devcontainer-lock.json") + expect(lockfile.content).to include('"version": "2.10.0"') + end + end end end