-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Replace Job's untyped hashes with T::ImmutableStruct #14616
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,10 @@ | |
| require "dependabot/package/release_cooldown_options" | ||
| require "dependabot/updater/update_type_helper" | ||
|
|
||
| require "dependabot/job/allowed_update" | ||
| require "dependabot/job/ignore_condition" | ||
| require "dependabot/job/security_advisory_entry" | ||
|
|
||
| # Describes a single Dependabot workload within the GitHub-integrated Service | ||
| # | ||
| # This primarily acts as a value class to hold inputs for various Core objects | ||
|
|
@@ -61,7 +65,7 @@ class Job # rubocop:disable Metrics/ClassLength | |
| T::Array[Symbol] | ||
| ) | ||
|
|
||
| sig { returns(T::Array[T::Hash[String, T.untyped]]) } | ||
| sig { returns(T::Array[Dependabot::Job::AllowedUpdate]) } | ||
| attr_reader :allowed_updates | ||
|
|
||
| sig { returns(T::Array[Dependabot::Credential]) } | ||
|
|
@@ -85,7 +89,7 @@ class Job # rubocop:disable Metrics/ClassLength | |
| sig { returns(String) } | ||
| attr_reader :command | ||
|
|
||
| sig { returns(T::Array[T.untyped]) } | ||
| sig { returns(T::Array[Dependabot::Job::IgnoreCondition]) } | ||
| attr_reader :ignore_conditions | ||
|
|
||
| sig { returns(String) } | ||
|
|
@@ -94,7 +98,7 @@ class Job # rubocop:disable Metrics/ClassLength | |
| sig { returns(T.nilable(Dependabot::RequirementsUpdateStrategy)) } | ||
| attr_reader :requirements_update_strategy | ||
|
|
||
| sig { returns(T::Array[T.untyped]) } | ||
| sig { returns(T::Array[Dependabot::Job::SecurityAdvisoryEntry]) } | ||
| attr_reader :security_advisories | ||
|
Comment on lines
+101
to
102
|
||
|
|
||
| sig { returns(T::Boolean) } | ||
|
|
@@ -161,8 +165,11 @@ def self.standardise_keys(hash) | |
| def initialize(attributes) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength | ||
| @id = T.let(attributes.fetch(:id), String) | ||
| @command = T.let(attributes.fetch(:command, ""), String) | ||
| @allowed_updates = T.let(attributes.fetch(:allowed_updates), T::Array[T.untyped]) | ||
| @commit_message_options = T.let( | ||
| @allowed_updates = T.let( | ||
| attributes.fetch(:allowed_updates).map { |h| AllowedUpdate.from_hash(h) }, | ||
| T::Array[AllowedUpdate] | ||
| ) | ||
| @commit_message_options = T.let( | ||
| attributes.fetch(:commit_message_options, {}), | ||
| T.nilable(T::Hash[T.untyped, T.untyped]) | ||
| ) | ||
|
|
@@ -188,7 +195,10 @@ def initialize(attributes) # rubocop:disable Metrics/AbcSize,Metrics/MethodLengt | |
| attributes.fetch(:experiments, {}), | ||
| T.nilable(T::Hash[T.untyped, T.untyped]) | ||
| ) | ||
| @ignore_conditions = T.let(attributes.fetch(:ignore_conditions), T::Array[T.untyped]) | ||
| @ignore_conditions = T.let( | ||
| attributes.fetch(:ignore_conditions).map { |h| Job::IgnoreCondition.from_hash(h) }, | ||
| T::Array[Job::IgnoreCondition] | ||
| ) | ||
| @package_manager = T.let(attributes.fetch(:package_manager), String) | ||
| @reject_external_code = T.let(attributes.fetch(:reject_external_code, false), T::Boolean) | ||
| @repo_contents_path = T.let(attributes.fetch(:repo_contents_path, nil), T.nilable(String)) | ||
|
|
@@ -200,7 +210,10 @@ def initialize(attributes) # rubocop:disable Metrics/AbcSize,Metrics/MethodLengt | |
| T.nilable(Dependabot::RequirementsUpdateStrategy) | ||
| ) | ||
|
|
||
| @security_advisories = T.let(attributes.fetch(:security_advisories), T::Array[T.untyped]) | ||
| @security_advisories = T.let( | ||
| attributes.fetch(:security_advisories).map { |h| SecurityAdvisoryEntry.from_hash(h) }, | ||
| T::Array[SecurityAdvisoryEntry] | ||
| ) | ||
| @security_updates_only = T.let(attributes.fetch(:security_updates_only), T::Boolean) | ||
| @source = T.let(build_source(attributes.fetch(:source)), Dependabot::Source) | ||
| @token = T.let(attributes.fetch(:token, nil), T.nilable(String)) | ||
|
|
@@ -316,18 +329,17 @@ def allowed_update?(dependency, check_previous_version: false) | |
|
|
||
| allowed_updates.any? do |update| | ||
| # Check the update-type (defaulting to all) | ||
| update_type = update.fetch("update-type", "all") | ||
| # NOTE: Preview supports specifying a "security" update type whereas | ||
| # native will say "security-updates-only" | ||
| security_update = update_type == "security" || security_updates_only? | ||
| security_update = update.update_type == "security" || security_updates_only? | ||
| next false if security_update && !vulnerable?(dependency, check_previous_version) | ||
|
|
||
| # Check the dependency-name (defaulting to matching) | ||
| condition_name = update.fetch("dependency-name", dependency.name) | ||
| condition_name = update.dependency_name || dependency.name | ||
| next false unless name_match?(condition_name, dependency.name) | ||
|
|
||
| # Check the dependency-type (defaulting to all) | ||
| dep_type = update.fetch("dependency-type", "all") | ||
| dep_type = update.dependency_type | ||
| next false if dep_type == "indirect" && | ||
| dependency.requirements.any? | ||
| # In dependabot-api, dependency-type is defaulting to "direct" not "all". Ignoring | ||
|
|
@@ -417,18 +429,14 @@ def commit_message_options | |
| def security_advisories_for(dependency) | ||
| relevant_advisories = | ||
| security_advisories | ||
| .select { |adv| adv.fetch("dependency-name").casecmp(dependency.name).zero? } | ||
| .select { |adv| T.must(adv.dependency_name.casecmp(dependency.name)).zero? } | ||
|
|
||
| relevant_advisories.map do |adv| | ||
| vulnerable_versions = adv["affected-versions"] || [] | ||
| safe_versions = (adv["patched-versions"] || []) + | ||
| (adv["unaffected-versions"] || []) | ||
|
|
||
| Dependabot::SecurityAdvisory.new( | ||
| dependency_name: dependency.name, | ||
| package_manager: package_manager, | ||
| vulnerable_versions: vulnerable_versions, | ||
| safe_versions: safe_versions | ||
| vulnerable_versions: T.unsafe(adv.affected_versions), | ||
| safe_versions: T.unsafe(adv.patched_versions + adv.unaffected_versions) | ||
| ) | ||
|
Comment on lines
+432
to
440
|
||
| end | ||
| end | ||
|
|
@@ -459,17 +467,15 @@ def ignore_conditions_for(dependency) | |
| # were created via "@dependabot ignore version" commands | ||
| sig { params(dependency: Dependabot::Dependency).void } | ||
| def log_ignore_conditions_for(dependency) | ||
| conditions = ignore_conditions.select { |ic| name_match?(ic["dependency-name"], dependency.name) } | ||
| conditions = ignore_conditions.select { |ic| name_match?(ic.dependency_name, dependency.name) } | ||
| return if conditions.empty? | ||
|
|
||
| Dependabot.logger.info("Ignored versions:") | ||
| conditions.each do |ic| | ||
| unless ic["version-requirement"].nil? | ||
| Dependabot.logger.info(" #{ic['version-requirement']} - from #{ic['source']}") | ||
| end | ||
| Dependabot.logger.info(" #{ic.version_requirement} - from #{ic.source}") unless ic.version_requirement.nil? | ||
|
|
||
| ic["update-types"]&.each do |update_type| | ||
| msg = " #{update_type} - from #{ic['source']}" | ||
| ic.update_types&.each do |update_type| | ||
| msg = " #{update_type} - from #{ic.source}" | ||
| msg += " (doesn't apply to security update)" if security_updates_only? | ||
| Dependabot.logger.info(msg) | ||
| end | ||
|
|
@@ -524,26 +530,26 @@ def collect_permitted_update_types(dependency) | |
| return [] if matching_rules.any? { |r| allow_rule_permits_all_types?(r) } | ||
|
|
||
| matching_rules | ||
| .flat_map { |r| r.fetch("update-types", []) } | ||
| .flat_map(&:update_types) | ||
| .filter_map { |t| t.is_a?(String) ? t.downcase.strip : nil } | ||
| .select { |t| Dependabot::Updater::UpdateTypeHelper::ALL_SEMVER_UPDATE_TYPES.include?(t) } | ||
| .uniq | ||
| end | ||
|
|
||
| sig { params(dependency: Dependabot::Dependency).returns(T::Array[T::Hash[String, T.untyped]]) } | ||
| sig { params(dependency: Dependabot::Dependency).returns(T::Array[AllowedUpdate]) } | ||
| def matching_allow_rules(dependency) | ||
| allowed_updates.select do |update| | ||
| allow_rule_matches_dependency?(update, dependency) | ||
| end | ||
| end | ||
|
|
||
| sig { params(update: T::Hash[String, T.untyped], dependency: Dependabot::Dependency).returns(T::Boolean) } | ||
| sig { params(update: AllowedUpdate, dependency: Dependabot::Dependency).returns(T::Boolean) } | ||
| def allow_rule_matches_dependency?(update, dependency) | ||
| condition_name = update.fetch("dependency-name", nil) | ||
| condition_name = update.dependency_name | ||
| return false if condition_name && !name_match?(condition_name, dependency.name) | ||
|
|
||
| dep_type = update.fetch("dependency-type", nil) | ||
| return true if dep_type.nil? || dep_type == "all" | ||
| dep_type = update.dependency_type | ||
| return true if dep_type == "all" | ||
|
|
||
| # Indirect deps don't match top-level type rules (matching allowed_update? behavior) | ||
| return false if dependency.requirements.none? && TOP_LEVEL_DEPENDENCY_TYPES.include?(dep_type) | ||
|
|
@@ -557,9 +563,9 @@ def allow_rule_matches_dependency?(update, dependency) | |
| end | ||
| end | ||
|
|
||
| sig { params(rule: T::Hash[String, T.untyped]).returns(T::Boolean) } | ||
| sig { params(rule: AllowedUpdate).returns(T::Boolean) } | ||
| def allow_rule_permits_all_types?(rule) | ||
| !rule.key?("update-types") || !rule["update-types"].is_a?(Array) || rule["update-types"].empty? | ||
| rule.update_types.empty? | ||
| end | ||
|
|
||
| sig { params(dependency: Dependabot::Dependency).returns(T.nilable(Dependabot::Version)) } | ||
|
|
@@ -678,17 +684,16 @@ def clean_directories(source_details) | |
| def calculate_update_config | ||
| update_config_ignore_conditions = ignore_conditions.map do |ic| | ||
| Dependabot::Config::IgnoreCondition.new( | ||
| dependency_name: T.let(ic["dependency-name"], String), | ||
| versions: T.let([ic["version-requirement"]].compact, T::Array[String]), | ||
| update_types: T.let(ic["update-types"], T.nilable(T::Array[String])) | ||
| dependency_name: ic.dependency_name, | ||
| versions: [ic.version_requirement].compact, | ||
| update_types: ic.update_types | ||
| ) | ||
| end | ||
|
|
||
| update_config = Dependabot::Config::UpdateConfig.new( | ||
| ignore_conditions: T.let(update_config_ignore_conditions, T::Array[Dependabot::Config::IgnoreCondition]), | ||
| exclude_paths: T.let(exclude_paths, T.nilable(T::Array[String])) | ||
| Dependabot::Config::UpdateConfig.new( | ||
| ignore_conditions: update_config_ignore_conditions, | ||
| exclude_paths: exclude_paths | ||
| ) | ||
| T.let(update_config, Dependabot::Config::UpdateConfig) | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| # typed: strict | ||
| # frozen_string_literal: true | ||
|
|
||
| require "sorbet-runtime" | ||
|
|
||
| module Dependabot | ||
| class Job | ||
| # Parsed representation of an allowed update rule from the job definition. | ||
| # | ||
| # Replaces the raw T::Hash[String, T.untyped] with a typed struct so | ||
| # downstream code gets compile-time checked field access instead of | ||
| # hash key lookups that return T.untyped. | ||
| class AllowedUpdate < T::ImmutableStruct | ||
| extend T::Sig | ||
|
|
||
| const :dependency_name, T.nilable(String) | ||
| const :dependency_type, String, default: "all" | ||
| const :update_type, String, default: "all" | ||
| const :update_types, T::Array[String], default: [] | ||
|
|
||
| sig { params(hash: T::Hash[String, T.untyped]).returns(AllowedUpdate) } | ||
| def self.from_hash(hash) | ||
| new( | ||
| dependency_name: hash["dependency-name"], | ||
| dependency_type: hash.fetch("dependency-type", "all"), | ||
| update_type: hash.fetch("update-type", "all"), | ||
| update_types: hash.fetch("update-types", []) || [] | ||
| ) | ||
| end | ||
|
|
||
| # Temporary bridge: returns the original hash format for code that | ||
| # still expects hash access. Remove once all callers are migrated. | ||
| sig { returns(T::Hash[String, T.untyped]) } | ||
| def to_hash | ||
| h = T.let({}, T::Hash[String, T.untyped]) | ||
| h["dependency-name"] = dependency_name if dependency_name | ||
| h["dependency-type"] = dependency_type | ||
| h["update-type"] = update_type | ||
| h["update-types"] = update_types unless update_types.empty? | ||
| h | ||
| end | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # typed: strict | ||
| # frozen_string_literal: true | ||
|
|
||
| require "sorbet-runtime" | ||
|
|
||
| module Dependabot | ||
| class Job | ||
| # Parsed representation of an ignore condition from the job definition. | ||
| # | ||
| # The raw JSON uses string keys like "dependency-name" and "version-requirement". | ||
| # This struct parses those into typed fields at the boundary so the rest of | ||
| # the codebase gets compile-time field access. | ||
| class IgnoreCondition < T::ImmutableStruct | ||
| extend T::Sig | ||
|
|
||
| const :dependency_name, String | ||
| const :version_requirement, T.nilable(String) | ||
| const :update_types, T.nilable(T::Array[String]) | ||
| const :source, T.nilable(String) | ||
|
|
||
| sig { params(hash: T::Hash[String, T.untyped]).returns(IgnoreCondition) } | ||
| def self.from_hash(hash) | ||
| new( | ||
| dependency_name: hash.fetch("dependency-name"), | ||
| version_requirement: hash["version-requirement"], | ||
| update_types: hash["update-types"], | ||
| source: hash["source"] | ||
| ) | ||
|
Comment on lines
+16
to
+28
|
||
| end | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # typed: strict | ||
| # frozen_string_literal: true | ||
|
|
||
| require "sorbet-runtime" | ||
|
|
||
| module Dependabot | ||
| class Job | ||
| # Parsed representation of a security advisory from the job definition. | ||
| class SecurityAdvisoryEntry < T::ImmutableStruct | ||
| extend T::Sig | ||
|
|
||
| const :dependency_name, String | ||
| const :affected_versions, T::Array[String], default: [] | ||
| const :patched_versions, T::Array[String], default: [] | ||
| const :unaffected_versions, T::Array[String], default: [] | ||
|
|
||
| sig { params(hash: T::Hash[String, T.untyped]).returns(SecurityAdvisoryEntry) } | ||
| def self.from_hash(hash) | ||
| new( | ||
| dependency_name: hash.fetch("dependency-name"), | ||
| affected_versions: hash.fetch("affected-versions", []) || [], | ||
| patched_versions: hash.fetch("patched-versions", []) || [], | ||
| unaffected_versions: hash.fetch("unaffected-versions", []) || [] | ||
| ) | ||
| end | ||
| end | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When passing ignore conditions into
Dependabot::PullRequestCreator::MessageBuilder, the mapping currently omits"updated-at".MessageBuilder#ignore_conditions_tableuses that field to sort and show the latest 20 ignore conditions; without it, all entries sort asTime.at(0)and the “most recent” table becomes misleading. Preserve and forwardupdated-at(and any other fieldsMessageBuilderdepends on) fromJob::IgnoreConditionwhen building this hash payload.