diff --git a/common/lib/dependabot/ecosystem.rb b/common/lib/dependabot/ecosystem.rb index 8842382e50..4da21bf803 100644 --- a/common/lib/dependabot/ecosystem.rb +++ b/common/lib/dependabot/ecosystem.rb @@ -90,6 +90,22 @@ def initialize( sig { returns(T.nilable(Dependabot::Requirement)) } attr_reader :requirement + # The version of the package manager or language as a string. + # @example + # version_to_s #=> "2.1" + sig { returns(String) } + def version_to_s + version.to_s + end + + # The raw version of the package manager or language. + # @example + # raw_version #=> "2.1.4" + sig { returns(String) } + def version_to_raw_s + version&.to_semver.to_s + end + # Checks if the current version is deprecated. # Returns true if the version is in the deprecated_versions array; false otherwise. # @example diff --git a/github_actions/lib/dependabot/github_actions.rb b/github_actions/lib/dependabot/github_actions.rb index ec1d95378c..6f89593efa 100644 --- a/github_actions/lib/dependabot/github_actions.rb +++ b/github_actions/lib/dependabot/github_actions.rb @@ -3,6 +3,7 @@ # 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/github_actions/constants" require "dependabot/github_actions/file_fetcher" require "dependabot/github_actions/file_parser" require "dependabot/github_actions/update_checker" @@ -10,6 +11,7 @@ require "dependabot/github_actions/metadata_finder" require "dependabot/github_actions/requirement" require "dependabot/github_actions/version" +require "dependabot/github_actions/package_manager" require "dependabot/pull_request_creator/labeler" Dependabot::PullRequestCreator::Labeler diff --git a/github_actions/lib/dependabot/github_actions/constants.rb b/github_actions/lib/dependabot/github_actions/constants.rb new file mode 100644 index 0000000000..7563846b46 --- /dev/null +++ b/github_actions/lib/dependabot/github_actions/constants.rb @@ -0,0 +1,44 @@ +# typed: strong +# frozen_string_literal: true + +module Dependabot + module GithubActions + # Reference to the GitHub.com domain + GITHUB_COM = T.let("github.com", String) + + # Regular expression to match a GitHub repository reference + GITHUB_REPO_REFERENCE = T.let(%r{ + ^(?[\w.-]+)/ + (?[\w.-]+) + (?/[^\@]+)? + @(?.+) + }x, Regexp) + + # Matches .yml or .yaml files in the .github/workflows directories + WORKFLOW_YAML_REGEX = %r{\.github/workflows/.+\.ya?ml$} + # Matches .yml or .yaml files anywhere + ALL_YAML_FILES = %r{(?:^|/).+\.ya?ml$} + + # The ecosystem name for GitHub Actions + ECOSYSTEM = T.let("github_actions", String) + + # The pattern to match manifest files + MANIFEST_FILE_PATTERN = /\.ya?ml$/ + # The name of the manifest file + MANIFEST_FILE_YML = T.let("action.yml", String) + # The name of the manifest file + MANIFEST_FILE_YAML = T.let("action.yaml", String) + # The pattern to match any .yml or .yaml file + ANYTHING_YML = T.let(".yml", String) + # The path to the workflow directory + WORKFLOW_DIRECTORY = T.let(".github/workflows", String) + # The path to the config .yml file + CONFIG_YMLS = T.let("#{WORKFLOW_DIRECTORY}/#{ANYTHING_YML}".freeze, String) + + OWNER_KEY = T.let("owner", String) + REPO_KEY = T.let("repo", String) + REF_KEY = T.let("ref", String) + USES_KEY = T.let("uses", String) + STEPS_KEY = T.let("steps", String) + end +end diff --git a/github_actions/lib/dependabot/github_actions/file_fetcher.rb b/github_actions/lib/dependabot/github_actions/file_fetcher.rb index 81000266de..664ee435e3 100644 --- a/github_actions/lib/dependabot/github_actions/file_fetcher.rb +++ b/github_actions/lib/dependabot/github_actions/file_fetcher.rb @@ -5,6 +5,7 @@ require "dependabot/file_fetchers" require "dependabot/file_fetchers/base" +require "dependabot/github_actions/constants" module Dependabot module GithubActions @@ -12,11 +13,9 @@ class FileFetcher < Dependabot::FileFetchers::Base extend T::Sig extend T::Helpers - FILENAME_PATTERN = /\.ya?ml$/ - sig { override.params(filenames: T::Array[String]).returns(T::Boolean) } def self.required_files_in?(filenames) - filenames.any? { |f| f.match?(FILENAME_PATTERN) } + filenames.any? { |f| f.match?(MANIFEST_FILE_PATTERN) } end sig { override.returns(String) } @@ -49,9 +48,9 @@ def fetch_files if incorrectly_encoded_workflow_files.none? expected_paths = if directory == "/" - File.join(directory, "action.yml") + " or /.github/workflows/.yml" + File.join(directory, MANIFEST_FILE_YML) + " or /#{CONFIG_YMLS}" else - File.join(directory, ".yml") + File.join(directory, ANYTHING_YML) end raise( @@ -75,16 +74,19 @@ def workflow_files # In the special case where the root directory is defined we also scan # the .github/workflows/ folder. if directory == "/" - @workflow_files += [fetch_file_if_present("action.yml"), fetch_file_if_present("action.yaml")].compact + @workflow_files += [ + fetch_file_if_present(MANIFEST_FILE_YML), + fetch_file_if_present(MANIFEST_FILE_YAML) + ].compact - workflows_dir = ".github/workflows" + workflows_dir = WORKFLOW_DIRECTORY else workflows_dir = "." end @workflow_files += repo_contents(dir: workflows_dir, raise_errors: false) - .select { |f| f.type == "file" && f.name.match?(FILENAME_PATTERN) } + .select { |f| f.type == "file" && f.name.match?(MANIFEST_FILE_PATTERN) } .map { |f| fetch_file_from_host("#{workflows_dir}/#{f.name}") } end diff --git a/github_actions/lib/dependabot/github_actions/file_parser.rb b/github_actions/lib/dependabot/github_actions/file_parser.rb index 4198ebc221..fc53914e0d 100644 --- a/github_actions/lib/dependabot/github_actions/file_parser.rb +++ b/github_actions/lib/dependabot/github_actions/file_parser.rb @@ -8,7 +8,9 @@ require "dependabot/errors" require "dependabot/file_parsers" require "dependabot/file_parsers/base" +require "dependabot/github_actions/constants" require "dependabot/github_actions/version" +require "dependabot/github_actions/package_manager" # For docs, see # https://help.github.com/en/articles/configuring-a-workflow#referencing-actions-in-your-workflow @@ -20,13 +22,6 @@ class FileParser < Dependabot::FileParsers::Base require "dependabot/file_parsers/base/dependency_set" - GITHUB_REPO_REFERENCE = %r{ - ^(?[\w.-]+)/ - (?[\w.-]+) - (?/[^\@]+)? - @(?.+) - }x - sig { override.returns(T::Array[Dependabot::Dependency]) } def parse dependency_set = DependencySet.new @@ -44,8 +39,24 @@ def parse dependency_set.dependencies end + sig { returns(Ecosystem) } + def ecosystem + @ecosystem ||= T.let( + Ecosystem.new( + name: ECOSYSTEM, + package_manager: package_manager + ), + T.nilable(Ecosystem) + ) + end + private + sig { returns(Ecosystem::VersionManager) } + def package_manager + @package_manager ||= T.let(PackageManager.new, T.nilable(Dependabot::GithubActions::PackageManager)) + end + sig { params(file: Dependabot::DependencyFile).returns(Dependabot::FileParsers::Base::DependencySet) } def workfile_file_dependencies(file) dependency_set = DependencySet.new @@ -94,20 +105,20 @@ def workfile_file_dependencies(file) sig { params(file: Dependabot::DependencyFile, string: String).returns(Dependabot::Dependency) } def build_github_dependency(file, string) - unless source&.hostname == "github.com" + unless source&.hostname == GITHUB_COM dep = github_dependency(file, string, T.must(source).hostname) git_checker = Dependabot::GitCommitChecker.new(dependency: dep, credentials: credentials) return dep if git_checker.git_repo_reachable? end - github_dependency(file, string, "github.com") + github_dependency(file, string, GITHUB_COM) end sig { params(file: Dependabot::DependencyFile, string: String, hostname: String).returns(Dependabot::Dependency) } def github_dependency(file, string, hostname) details = T.must(string.match(GITHUB_REPO_REFERENCE)).named_captures - name = "#{details.fetch('owner')}/#{details.fetch('repo')}" - ref = details.fetch("ref") + name = "#{details.fetch(OWNER_KEY)}/#{details.fetch(REPO_KEY)}" + ref = details.fetch(REF_KEY) version = version_class.new(ref).to_s if version_class.correct?(ref) Dependency.new( name: name, @@ -124,7 +135,7 @@ def github_dependency(file, string, hostname) file: file.name, metadata: { declaration_string: string } }], - package_manager: "github_actions" + package_manager: PackageManager::NAME ) end @@ -139,11 +150,11 @@ def deep_fetch_uses(json_obj, found_uses = []) sig { params(json_object: T::Hash[String, T.untyped], found_uses: T::Array[String]).returns(T::Array[String]) } def deep_fetch_uses_from_hash(json_object, found_uses) - if json_object.key?("uses") - found_uses << json_object["uses"] - elsif json_object.key?("steps") + if json_object.key?(USES_KEY) + found_uses << json_object[USES_KEY] + elsif json_object.key?(STEPS_KEY) # Bypass other fields as uses are under steps if they exist - deep_fetch_uses(json_object["steps"], found_uses) + deep_fetch_uses(json_object[STEPS_KEY], found_uses) else json_object.values.flat_map { |obj| deep_fetch_uses(obj, found_uses) } end diff --git a/github_actions/lib/dependabot/github_actions/file_updater.rb b/github_actions/lib/dependabot/github_actions/file_updater.rb index 1a06fa0453..4147246bc7 100644 --- a/github_actions/lib/dependabot/github_actions/file_updater.rb +++ b/github_actions/lib/dependabot/github_actions/file_updater.rb @@ -6,6 +6,7 @@ require "dependabot/errors" require "dependabot/file_updaters" require "dependabot/file_updaters/base" +require "dependabot/github_actions/constants" module Dependabot module GithubActions @@ -16,10 +17,10 @@ class FileUpdater < Dependabot::FileUpdaters::Base def self.updated_files_regex [ # Matches .yml or .yaml files in the .github/workflows directories - %r{\.github/workflows/.+\.ya?ml$}, + WORKFLOW_YAML_REGEX, # Matches .yml or .yaml files in the root directory or any subdirectory - %r{(?:^|/).+\.ya?ml$} + ALL_YAML_FILES ] end diff --git a/github_actions/lib/dependabot/github_actions/metadata_finder.rb b/github_actions/lib/dependabot/github_actions/metadata_finder.rb index 9740877b34..2d77b7900a 100644 --- a/github_actions/lib/dependabot/github_actions/metadata_finder.rb +++ b/github_actions/lib/dependabot/github_actions/metadata_finder.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true require "sorbet-runtime" - +require "dependabot/github_actions/constants" require "dependabot/metadata_finders" require "dependabot/metadata_finders/base" @@ -19,7 +19,7 @@ def look_up_source url = if info.nil? - "https://github.com/#{dependency.name}" + "https://#{GITHUB_COM}/#{dependency.name}" else info[:url] || info.fetch("url") end diff --git a/github_actions/lib/dependabot/github_actions/package_manager.rb b/github_actions/lib/dependabot/github_actions/package_manager.rb new file mode 100644 index 0000000000..d360ca5079 --- /dev/null +++ b/github_actions/lib/dependabot/github_actions/package_manager.rb @@ -0,0 +1,40 @@ +# typed: strong +# frozen_string_literal: true + +require "sorbet-runtime" +require "dependabot/github_actions/constants" +require "dependabot/github_actions/version" +require "dependabot/ecosystem" +require "dependabot/github_actions/requirement" + +module Dependabot + module GithubActions + class PackageManager < Dependabot::Ecosystem::VersionManager + extend T::Sig + + # The package manager name for GitHub Actions + NAME = T.let("github_actions", String) + + # The version of the package manager + VERSION = T.let("1.0.0", String) + + sig { void } + def initialize + super( + name: NAME, + version: Version.new(VERSION) + ) + end + + sig { override.returns(T::Boolean) } + def deprecated? + false + end + + sig { override.returns(T::Boolean) } + def unsupported? + false + end + end + end +end diff --git a/github_actions/lib/dependabot/github_actions/update_checker.rb b/github_actions/lib/dependabot/github_actions/update_checker.rb index db384b63de..d886a33171 100644 --- a/github_actions/lib/dependabot/github_actions/update_checker.rb +++ b/github_actions/lib/dependabot/github_actions/update_checker.rb @@ -4,6 +4,7 @@ require "sorbet-runtime" require "dependabot/errors" +require "dependabot/github_actions/constants" require "dependabot/github_actions/requirement" require "dependabot/github_actions/version" require "dependabot/update_checkers" diff --git a/github_actions/spec/dependabot/github_actions/file_parser_spec.rb b/github_actions/spec/dependabot/github_actions/file_parser_spec.rb index 6ee4d2e6bd..40e0be6a30 100644 --- a/github_actions/spec/dependabot/github_actions/file_parser_spec.rb +++ b/github_actions/spec/dependabot/github_actions/file_parser_spec.rb @@ -582,4 +582,15 @@ def mock_service_pack_request(nwo) end end end + + describe "#ecosystem" do + it "returns the correct ecosystem" do + expect(parser.ecosystem).to be_a(Dependabot::Ecosystem) + end + + it "returns package manager with version" do + expect(parser.ecosystem.package_manager).to be_a(Dependabot::GithubActions::PackageManager) + expect(parser.ecosystem.package_manager.version.to_s).to eq("1.0.0") + end + end end diff --git a/github_actions/spec/dependabot/github_actions/package_manager_spec.rb b/github_actions/spec/dependabot/github_actions/package_manager_spec.rb new file mode 100644 index 0000000000..bbaf69b1a2 --- /dev/null +++ b/github_actions/spec/dependabot/github_actions/package_manager_spec.rb @@ -0,0 +1,34 @@ +# typed: false +# frozen_string_literal: true + +require "dependabot/github_actions/package_manager" +require "dependabot/ecosystem" +require "spec_helper" + +RSpec.describe Dependabot::GithubActions::PackageManager do + let(:package_manager) { described_class.new } + + describe "#version_to_s" do + it "returns the package manager version empty" do + expect(package_manager.version_to_s).to eq("1.0.0") + end + end + + describe "#version_to_raw_s" do + it "returns the package manager raw version empty" do + expect(package_manager.version_to_raw_s).to eq("1.0.0") + end + end + + describe "#deprecated?" do + it "returns always false" do + expect(package_manager.deprecated?).to be false + end + end + + describe "#unsupported?" do + it "returns always false" do + expect(package_manager.unsupported?).to be false + end + end +end diff --git a/updater/lib/dependabot/api_client.rb b/updater/lib/dependabot/api_client.rb index 7896b00d2f..fa879c888b 100644 --- a/updater/lib/dependabot/api_client.rb +++ b/updater/lib/dependabot/api_client.rb @@ -330,13 +330,13 @@ def record_ecosystem_meta(ecosystem) def version_manager_json(version_manager) return nil unless version_manager - raw_version = version_manager.version&.to_semver.to_s - version = version_manager.version&.to_semver.to_s + version = version_manager.version_to_s + raw_version = version_manager.version_to_raw_s { name: version_manager.name, - raw_version: raw_version.empty? ? "N/A" : raw_version, version: version.empty? ? "N/A" : version, + raw_version: raw_version.empty? ? "N/A" : raw_version, requirement: version_manager_requirement_json(version_manager) } end diff --git a/updater/spec/dependabot/api_client_spec.rb b/updater/spec/dependabot/api_client_spec.rb index af0cd8dded..45fcc686ab 100644 --- a/updater/spec/dependabot/api_client_spec.rb +++ b/updater/spec/dependabot/api_client_spec.rb @@ -522,6 +522,8 @@ Dependabot::Ecosystem::VersionManager, name: "bundler", version: Dependabot::Version.new("2.1.4"), + version_to_s: "2.1.4", + version_to_raw_s: "2.1.4", requirement: instance_double( Dependabot::Requirement, constraints: [">= 2.0"], @@ -533,6 +535,8 @@ Dependabot::Ecosystem::VersionManager, name: "ruby", version: Dependabot::Version.new("2.7.0"), + version_to_s: "2.7.0", + version_to_raw_s: "2.7.0", requirement: nil ) )