From a759622b1f3dcd406bafddc2dbb0d012c5ed41a9 Mon Sep 17 00:00:00 2001 From: Maxwell Weru Date: Sun, 5 Feb 2023 23:07:20 +0300 Subject: [PATCH] Added listing via Rubocop (#503) --- .github/workflows/docker.yml | 7 +- .gitignore | 95 +++------ .rubocop.yml | 338 +++++++++++++++++++++++++++++++++ .ruby-version | 1 + Gemfile | 3 + Rakefile | 2 - updater/.rubocop.yml | 1 + updater/Gemfile | 3 +- updater/Gemfile.lock | 24 ++- updater/bin/azure_helpers.rb | 30 +-- updater/bin/update-script.rb | 195 ++++++++++--------- updater/bin/vulnerabilities.rb | 15 +- 12 files changed, 535 insertions(+), 179 deletions(-) create mode 100644 .rubocop.yml create mode 100644 .ruby-version create mode 100644 Gemfile create mode 100644 updater/.rubocop.yml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index dbef9be6..199e6033 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -28,6 +28,8 @@ jobs: env: IMAGE_NAME: 'dependabot-azure-devops' DOCKER_BUILDKIT: 1 # Enable Docker BuildKit + # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/updater/Gemfile steps: - name: Checkout @@ -49,9 +51,12 @@ jobs: - name: Setup Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: '3.1' bundler-cache: true + - name: bundle exec rubocop + run: bundle exec rubocop + working-directory: updater + - name: bundle exec rspec spec run: bundle exec rspec spec working-directory: updater diff --git a/.gitignore b/.gitignore index 8af4335f..7a75cf24 100644 --- a/.gitignore +++ b/.gitignore @@ -1,66 +1,33 @@ -*.gem -*.rbc -/.config -/coverage/ -/InstalledFiles -/pkg/ -/spec/reports/ -/spec/examples.txt -/test/tmp/ -/test/version_tmp/ -/tmp/ - -# Used by dotenv library to load environment variables. -# .env - -# Ignore Byebug command history file. -.byebug_history - -## Specific to RubyMotion: -.dat* -.repl_history -build/ -*.bridgesupport -build-iPhoneOS/ -build-iPhoneSimulator/ - -## Specific to RubyMotion (use of CocoaPods): -# -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -# -# vendor/Pods/ - -## Documentation cache and generated files: -/.yardoc/ -/_yardoc/ -/doc/ -/rdoc/ - -## Environment normalization: /.bundle/ -/vendor/bundle -/lib/bundler/man/ - -# for a library or gem, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# Gemfile.lock -# .ruby-version -# .ruby-gemset - -# unless supporting rvm < 1.11.0 or doing something fancy, ignore this: -.rvmrc - -# Used by RuboCop. Remote config files pulled in from inherit_from directive. -# .rubocop-https?--* - -node_modules -dist -*.log -.idea +/node_modules +/.env +/.envrc +/tmp +/pkg +/dependabot-*.gem +!bundler/spec/fixtures/projects/**/Gemfile.lock +Gemfile.lock +!updater/spec/fixtures/**/Gemfile.lock +!updater/Gemfile.lock +vendor +!bundler/spec/fixtures/vendored_gems/vendor +!common/spec/fixtures/projects/**/*/vendor +!go_modules/spec/fixtures/projects/**/* .DS_Store - -# local caching -script/tmp -*git.store \ No newline at end of file +*.pyc +*git.store +/.vscode/ +/.vscode-server/ +/.vscode-server-insiders/ +**/helpers/install-dir +/npm_and_yarn/helpers/node_modules +/npm_and_yarn/helpers/.node-version +/dry-run +**/bin/helper +/.core-bash_history +coverage/ +.ruby-gemset +.tool-versions +.rspec_status +.rdbg_history +.idea diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..0d79f070 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,338 @@ +--- +require: rubocop-performance + +AllCops: + DisplayCopNames: true + Exclude: + - "dependabot-core/**/*" + - "../*/bin/**/*" # TODO: remove this once files in bin have been distributed + - "updater/tmp/**/*" + - "updater/spec/fixtures/**/*" + - "updater/vendor/**/*" + NewCops: enable + SuggestExtensions: false +Gemspec/DeprecatedAttributeAssignment: + Enabled: true +Gemspec/RequireMFA: + Enabled: false +Layout/DotPosition: + EnforcedStyle: trailing +Layout/EmptyLinesAroundAttributeAccessor: + Enabled: false +Layout/FirstArrayElementIndentation: + EnforcedStyle: consistent +Layout/FirstHashElementIndentation: + EnforcedStyle: consistent +Layout/LineLength: + Max: 120 +Layout/RescueEnsureAlignment: + Enabled: false +Layout/SpaceAroundMethodCallOperator: + Enabled: false +Layout/SpaceBeforeBrackets: + Enabled: true +Layout/LineContinuationLeadingSpace: + Enabled: false +Lint/AmbiguousAssignment: + Enabled: true +Lint/BinaryOperatorWithIdenticalOperands: + Enabled: true +Lint/DeprecatedConstants: + Enabled: true +Lint/DeprecatedOpenSSLConstant: + Enabled: false +Lint/DuplicateBranch: + Enabled: false +Lint/DuplicateElsifCondition: + Enabled: false +Lint/DuplicateRegexpCharacterClassElement: + Enabled: true +Lint/DuplicateRequire: + Enabled: true +Lint/DuplicateRescueException: + Enabled: true +Lint/EmptyBlock: + Enabled: true +Lint/EmptyClass: + Enabled: true +Lint/EmptyConditionalBody: + Enabled: true +Lint/EmptyFile: + Enabled: true +Lint/EmptyInPattern: + Enabled: true +Lint/FloatComparison: + Enabled: true +Lint/LambdaWithoutLiteralBlock: + Enabled: true +Lint/MissingSuper: + Enabled: false +Lint/MixedRegexpCaptureTypes: + Enabled: false +Lint/NoReturnInBeginEndBlocks: + Enabled: false +Lint/NumberedParameterAssignment: + Enabled: true +Lint/OrAssignmentToConstant: + Enabled: true +Lint/OutOfRangeRegexpRef: + Enabled: true +Lint/RaiseException: + Enabled: false +Lint/RedundantDirGlobSort: + Enabled: true +Lint/SelfAssignment: + Enabled: true +Lint/StructNewOverride: + Enabled: false +Lint/SymbolConversion: + Enabled: true +Lint/ToEnumArguments: + Enabled: true +Lint/TopLevelReturnWithArgument: + Enabled: true +Lint/TrailingCommaInAttributeDeclaration: + Enabled: true +Lint/TripleQuotes: + Enabled: true +Lint/UnexpectedBlockArity: + Enabled: true +Lint/UnmodifiedReduceAccumulator: + Enabled: true +Lint/UnreachableLoop: + Enabled: true +Lint/UselessMethodDefinition: + Enabled: true +Metrics/AbcSize: + Max: 35 +Metrics/BlockLength: + Exclude: + - "../*/Rakefile" + - "../**/spec/**/*" + - "../*/dependabot-*.gemspec" + Max: 35 +Metrics/ClassLength: + Max: 350 +Metrics/CyclomaticComplexity: + Max: 15 +Metrics/MethodLength: + Max: 35 +Metrics/ModuleLength: + Max: 350 +Metrics/ParameterLists: + CountKeywordArgs: false +Metrics/PerceivedComplexity: + Max: 10 +Naming/FileName: + Enabled: false +Performance/AncestorsInclude: + Enabled: true +Performance/BigDecimalWithNumericArgument: + Enabled: true +Performance/BindCall: + Enabled: true +Performance/BlockGivenWithExplicitBlock: + Enabled: true +Performance/Caller: + Enabled: true +Performance/CaseWhenSplat: + Enabled: true +Performance/Casecmp: + Enabled: true +Performance/ChainArrayAllocation: + Enabled: false +Performance/CollectionLiteralInLoop: + Enabled: true +Performance/CompareWithBlock: + Enabled: true +Performance/ConcurrentMonotonicTime: + Enabled: true +Performance/ConstantRegexp: + Enabled: true +Performance/Count: + Enabled: true +Performance/DeletePrefix: + Enabled: true +Performance/DeleteSuffix: + Enabled: true +Performance/Detect: + Enabled: true +Performance/DoubleStartEndWith: + Enabled: true +Performance/EndWith: + Enabled: true +Performance/FixedSize: + Enabled: true +Performance/FlatMap: + Enabled: true +Performance/InefficientHashSearch: + Enabled: true +Performance/IoReadlines: + Enabled: true +Performance/MapCompact: + Enabled: true +Performance/MethodObjectAsBlock: + Enabled: true +Performance/OpenStruct: + Enabled: false +Performance/RangeInclude: + Enabled: true +Performance/RedundantBlockCall: + Enabled: true +Performance/RedundantEqualityComparisonBlock: + Enabled: true +Performance/RedundantMatch: + Enabled: true +Performance/RedundantMerge: + Enabled: true +Performance/RedundantSortBlock: + Enabled: true +Performance/RedundantSplitRegexpArgument: + Enabled: true +Performance/RedundantStringChars: + Enabled: true +Performance/RegexpMatch: + Enabled: true +Performance/ReverseEach: + Enabled: true +Performance/ReverseFirst: + Enabled: true +Performance/SelectMap: + Enabled: false +Performance/Size: + Enabled: true +Performance/SortReverse: + Enabled: true +Performance/Squeeze: + Enabled: true +Performance/StartWith: + Enabled: true +Performance/StringIdentifierArgument: + Enabled: true +Performance/StringInclude: + Enabled: true +Performance/StringReplacement: + Enabled: true +Performance/Sum: + Enabled: true +Performance/TimesMap: + Enabled: true +Performance/UnfreezeString: + Enabled: true +Performance/UriDefaultParser: + Enabled: true +Style/AccessorGrouping: + Enabled: false +Style/ArgumentsForwarding: + Enabled: true +Style/ArrayCoercion: + Enabled: false +Style/BisectedAttrAccessor: + Enabled: false +Style/CaseLikeIf: + Enabled: false +Style/CollectionCompact: + Enabled: true +Style/CombinableLoops: + Enabled: true +Style/DocumentDynamicEvalDefinition: + Enabled: true +Style/Documentation: + Enabled: false +Style/EndlessMethod: + Enabled: true +Style/ExplicitBlockArgument: + Enabled: true +Style/ExponentialNotation: + Enabled: false +Style/GlobalStdStream: + Enabled: true +Style/HashAsLastArrayItem: + Enabled: false +Style/HashConversion: + Enabled: true +Style/HashEachMethods: + Enabled: false +Style/HashExcept: + Enabled: true +Style/HashLikeCase: + Enabled: false +Style/HashSyntax: + EnforcedShorthandSyntax: either +Style/HashTransformKeys: + Enabled: false +Style/HashTransformValues: + Enabled: false +Style/IfWithBooleanLiteralBranches: + Enabled: true +Style/InPatternThen: + Enabled: true +Style/KeywordParametersOrder: + Enabled: false +Style/MultilineInPatternThen: + Enabled: true +Style/MultipleComparison: + Enabled: false +Style/NegatedIfElseCondition: + Enabled: true +Style/NilLambda: + Enabled: true +Style/NumericPredicate: + Exclude: + - "../*/spec/**/*" +Style/OptionalBooleanParameter: + Enabled: false +Style/PercentLiteralDelimiters: + PreferredDelimiters: + "%I": "()" + "%W": "()" + "%i": "()" + "%w": "()" +Style/QuotedSymbols: + Enabled: true +Style/RedundantArgument: + Enabled: true +Style/RedundantAssignment: + Enabled: false +Style/RedundantConstantBase: + Enabled: false +Style/RedundantFetchBlock: + Enabled: false +Style/RedundantFileExtensionInRequire: + Enabled: false +Style/RedundantRegexpCharacterClass: + Enabled: false +Style/RedundantRegexpEscape: + Enabled: false +Style/RedundantSelfAssignment: + Enabled: true +Style/SignalException: + EnforcedStyle: only_raise +Style/SingleArgumentDig: + Enabled: true +Style/SlicingWithRange: + Enabled: false +Style/SoleNestedConditional: + Enabled: true +Style/StringChars: + Enabled: true +Style/StringConcatenation: + Enabled: false +Style/StringLiterals: + EnforcedStyle: double_quotes +Style/SwapValues: + Enabled: true +Style/OpenStructUse: + Enabled: false +Style/SpecialGlobalVars: + Enabled: false +Style/SelectByRegexp: + Enabled: false + +# TODO these were temporarily disabled during the Ruby 2.7 -> 3.1 upgrade +# in order to keep the upgrade diff small, they will be enabled/fixed in +# a follow-on PR. +Naming/BlockForwarding: + Enabled: false +Style/MutableConstant: + Enabled: false diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..ff365e06 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.1.3 diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..0755230d --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +eval_gemfile File.join(File.dirname(__FILE__), "updater/Gemfile") diff --git a/Rakefile b/Rakefile index d4c87d48..2e6f3a3f 100644 --- a/Rakefile +++ b/Rakefile @@ -37,7 +37,6 @@ def run_command(command) exit 1 unless system(command) end -# rubocop:disable Metrics/BlockLength namespace :gems do task build: :clean do root_path = Dir.getwd @@ -127,4 +126,3 @@ def rubygems_release_exists?(name, version) existing_versions = body.map { |b| b["number"] } existing_versions.include?(version) end -# rubocop:enable Metrics/BlockLength diff --git a/updater/.rubocop.yml b/updater/.rubocop.yml new file mode 100644 index 00000000..fc2019d4 --- /dev/null +++ b/updater/.rubocop.yml @@ -0,0 +1 @@ +inherit_from: ../.rubocop.yml diff --git a/updater/Gemfile b/updater/Gemfile index 9d875580..8f7ab7e8 100644 --- a/updater/Gemfile +++ b/updater/Gemfile @@ -24,8 +24,9 @@ group :test do common_gemspec = File.expand_path("../dependabot-core/common/dependabot-common.gemspec", __dir__) deps_shared_with_core = %w( - debug rspec + rubocop + rubocop-performance vcr webmock ) diff --git a/updater/Gemfile.lock b/updater/Gemfile.lock index 571554cf..7335b198 100644 --- a/updater/Gemfile.lock +++ b/updater/Gemfile.lock @@ -146,7 +146,6 @@ GEM concurrent-ruby (1.2.0) crack (0.4.5) rexml - debug (1.7.1) diff-lcs (1.5.0) docker_registry2 (1.13.0) rest-client (>= 1.8.0) @@ -172,6 +171,7 @@ GEM i18n (1.12.0) concurrent-ruby (~> 1.0) jmespath (1.6.2) + json (2.6.3) mime-types (3.4.1) mime-types-data (~> 3.2015) mime-types-data (3.2022.0105) @@ -186,11 +186,14 @@ GEM octokit (6.0.1) faraday (>= 1, < 3) sawyer (~> 0.9) + parallel (1.22.1) parseconfig (1.0.8) parser (3.2.0.0) ast (~> 2.4.1) public_suffix (5.0.1) racc (1.6.2) + rainbow (3.1.1) + regexp_parser (2.6.2) rest-client (2.1.0) http-accept (>= 1.7.0, < 2.0) http-cookie (>= 1.0.2, < 2.0) @@ -210,6 +213,22 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-support (3.12.0) + rubocop (1.44.1) + json (~> 2.3) + parallel (~> 1.10) + parser (>= 3.2.0.0) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.24.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.24.1) + parser (>= 3.1.1.0) + rubocop-performance (1.15.2) + rubocop (>= 1.7.0, < 2.0) + rubocop-ast (>= 0.4.0) + ruby-progressbar (1.11.0) ruby2_keywords (0.0.5) sawyer (0.9.2) addressable (>= 2.3.5) @@ -235,7 +254,6 @@ PLATFORMS x86_64-linux DEPENDENCIES - debug (~> 1.7.1) dependabot-bundler! dependabot-cargo! dependabot-common! @@ -254,6 +272,8 @@ DEPENDENCIES dependabot-python! dependabot-terraform! rspec (~> 3.12) + rubocop (~> 1.44.0) + rubocop-performance (~> 1.15.2) vcr (~> 6.1) webmock (~> 3.18) diff --git a/updater/bin/azure_helpers.rb b/updater/bin/azure_helpers.rb index d98b39ba..56bdc280 100644 --- a/updater/bin/azure_helpers.rb +++ b/updater/bin/azure_helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "json" require "dependabot/shared_helpers" require "excon" @@ -5,15 +7,14 @@ module Dependabot module Clients class Azure - def pull_requests_active(user_id, default_branch) # https://learn.microsoft.com/en-us/rest/api/azure/devops/git/pull-requests/get-pull-requests?view=azure-devops-rest-6.0&tabs=HTTP response = get(source.api_endpoint + source.organization + "/" + source.project + "/_apis/git/repositories/" + source.unscoped_repo + "/pullrequests?api-version=6.0&searchCriteria.status=active" \ - "&searchCriteria.creatorId=#{user_id}" \ - "&searchCriteria.targetRefName=refs/heads/#{default_branch}") + "&searchCriteria.creatorId=#{user_id}" \ + "&searchCriteria.targetRefName=refs/heads/#{default_branch}") JSON.parse(response.body).fetch("value") end @@ -23,7 +24,7 @@ def pull_request_abandon(pull_request_id) status: "abandoned" } - response = patch(source.api_endpoint + + patch(source.api_endpoint + source.organization + "/" + source.project + "/_apis/git/repositories/" + source.unscoped_repo + "/pullrequests/#{pull_request_id}?api-version=6.0", content.to_json) @@ -44,7 +45,7 @@ def branch_delete(name) } ] - response = post(source.api_endpoint + + post(source.api_endpoint + source.organization + "/" + source.project + "/_apis/git/repositories/" + source.unscoped_repo + "/refs?api-version=6.0", content.to_json) @@ -73,7 +74,7 @@ def pull_request_auto_complete(pull_request_id, user_id, merge_strategy, ignore_ } } - response = patch(source.api_endpoint + + patch(source.api_endpoint + source.organization + "/" + source.project + "/_apis/git/repositories/" + source.unscoped_repo + "/pullrequests/#{pull_request_id}?api-version=6.0", content.to_json) @@ -82,10 +83,12 @@ def pull_request_auto_complete(pull_request_id, user_id, merge_strategy, ignore_ def get_user_id(token = nil) # https://learn.microsoft.com/en-us/javascript/api/azure-devops-extension-api/connectiondata # https://stackoverflow.com/a/53227325 - response = token ? - get_with_token(source.api_endpoint + source.organization + "/_apis/connectionData", token) : + response = if token + get_with_token(source.api_endpoint + source.organization + "/_apis/connectionData", token) + else get(source.api_endpoint + source.organization + "/_apis/connectionData") - JSON.parse(response.body).fetch("authenticatedUser")['id'] + end + JSON.parse(response.body).fetch("authenticatedUser")["id"] end def pull_request_approve(pull_request_id, reviewer_token) @@ -97,11 +100,10 @@ def pull_request_approve(pull_request_id, reviewer_token) vote: 10 } - response = put_with_token(source.api_endpoint + - source.organization + "/" + source.project + - "/_apis/git/repositories/" + source.unscoped_repo + - "/pullrequests/#{pull_request_id}/reviewers/#{user_id}?api-version=6.0", - content.to_json, reviewer_token) + put_with_token(source.api_endpoint + source.organization + "/" + source.project + + "/_apis/git/repositories/" + source.unscoped_repo + + "/pullrequests/#{pull_request_id}/reviewers/#{user_id}" \ + "?api-version=6.0", content.to_json, reviewer_token) end def get_with_token(url, token) diff --git a/updater/bin/update-script.rb b/updater/bin/update-script.rb index 412d702d..a46e5f76 100644 --- a/updater/bin/update-script.rb +++ b/updater/bin/update-script.rb @@ -1,10 +1,14 @@ +# frozen_string_literal: true + +# rubocop:disable Style/GlobalVars + require "json" require "logger" require "dependabot/logger" Dependabot.logger = Logger.new($stdout) - # ensure logs are output immediately. Useful when running in certain hosts like ContainerGroups +# ensure logs are output immediately. Useful when running in certain hosts like ContainerGroups $stdout.sync = true require "dependabot/file_fetchers" @@ -46,7 +50,7 @@ branch: ENV["DEPENDABOT_TARGET_BRANCH"] || nil, # Branch against which to create PRs allow_conditions: [], - reject_external_code: ENV['DEPENDABOT_REJECT_EXTERNAL_CODE'] == "true", + reject_external_code: ENV["DEPENDABOT_REJECT_EXTERNAL_CODE"] == "true", requirements_update_strategy: nil, security_advisories: [], security_updates_only: false, @@ -56,27 +60,27 @@ reviewers: nil, # nil instead of empty array to avoid API rejection assignees: nil, # nil instead of empty array to avoid API rejection branch_name_separator: ENV["DEPENDABOT_BRANCH_NAME_SEPARATOR"] || "/", # Separator used for created branches. - milestone: ENV['DEPENDABOT_MILESTONE'] || nil, # Get the work item to attach - vendor_dependencies: ENV['DEPENDABOT_VENDOR'] == "true", - repo_contents_path: ENV['DEPENDABOT_REPO_CONTENTS_PATH'] || nil, + milestone: ENV["DEPENDABOT_MILESTONE"] || nil, # Get the work item to attach + vendor_dependencies: ENV["DEPENDABOT_VENDOR"] == "true", + repo_contents_path: ENV["DEPENDABOT_REPO_CONTENTS_PATH"] || nil, updater_options: {}, author_details: { email: ENV["DEPENDABOT_AUTHOR_EMAIL"] || "noreply@github.com", - name: ENV["DEPENDABOT_AUTHOR_NAME"] || "dependabot[bot]", + name: ENV["DEPENDABOT_AUTHOR_NAME"] || "dependabot[bot]" }, - fail_on_exception: ENV['DEPENDABOT_FAIL_ON_EXCEPTION'] == "true", # Stop the job if an exception occurs - skip_pull_requests: ENV['DEPENDABOT_SKIP_PULL_REQUESTS'] == "true", # Skip creating/updating Pull Requests - close_unwanted: ENV['DEPENDABOT_CLOSE_PULL_REQUESTS'] == "true", # Close unwanted Pull Requests + fail_on_exception: ENV["DEPENDABOT_FAIL_ON_EXCEPTION"] == "true", # Stop the job if an exception occurs + skip_pull_requests: ENV["DEPENDABOT_SKIP_PULL_REQUESTS"] == "true", # Skip creating/updating Pull Requests + close_unwanted: ENV["DEPENDABOT_CLOSE_PULL_REQUESTS"] == "true", # Close unwanted Pull Requests # See description of requirements here: # https://github.com/dependabot/dependabot-core/issues/600#issuecomment-407808103 # https://github.com/wemake-services/kira-dependencies/pull/210 - excluded_requirements: ENV['DEPENDABOT_EXCLUDE_REQUIREMENTS_TO_UNLOCK']&.split(" ")&.map(&:to_sym) || [], + excluded_requirements: ENV["DEPENDABOT_EXCLUDE_REQUIREMENTS_TO_UNLOCK"]&.split(" ")&.map(&:to_sym) || [], # Details on the location of the repository - azure_organization: ENV["AZURE_ORGANIZATION"], - azure_project: ENV["AZURE_PROJECT"], - azure_repository: ENV["AZURE_REPOSITORY"], + azure_organization: ENV.fetch("AZURE_ORGANIZATION", nil), + azure_project: ENV.fetch("AZURE_PROJECT", nil), + azure_repository: ENV.fetch("AZURE_REPOSITORY", nil), azure_hostname: ENV["AZURE_HOSTNAME"] || "dev.azure.com", azure_protocol: ENV["AZURE_PROTOCOL"] || "https", azure_port: nil, @@ -84,12 +88,12 @@ # Automatic completion set_auto_complete: ENV["AZURE_SET_AUTO_COMPLETE"] == "true", # Set auto complete on created pull requests - auto_complete_ignore_config_ids: JSON.parse(ENV['AZURE_AUTO_COMPLETE_IGNORE_CONFIG_IDS'] || '[]'), # default to empty array + auto_complete_ignore_config_ids: JSON.parse(ENV["AZURE_AUTO_COMPLETE_IGNORE_CONFIG_IDS"] || "[]"), # default to empty merge_strategy: ENV["AZURE_MERGE_STRATEGY"] || "squash", # default to squash # Automatic Approval auto_approve_pr: ENV["AZURE_AUTO_APPROVE_PR"] == "true", - auto_approve_user_token: ENV["AZURE_AUTO_APPROVE_USER_TOKEN"] || ENV["AZURE_ACCESS_TOKEN"], + auto_approve_user_token: ENV["AZURE_AUTO_APPROVE_USER_TOKEN"] || ENV.fetch("AZURE_ACCESS_TOKEN", nil) } # Name of the package manager you'd like to do the update for. Options are: @@ -123,7 +127,7 @@ "yarn" => "npm_and_yarn", "pipenv" => "pip", "pip-compile" => "pip", - "poetry" => "pip", + "poetry" => "pip" }.freeze $package_manager = PACKAGE_ECOSYSTEM_MAPPING.fetch($package_manager, $package_manager) @@ -136,13 +140,13 @@ "type" => "git_source", "host" => $options[:azure_hostname], "username" => ENV["AZURE_ACCESS_USERNAME"] || "x-access-token", - "password" => ENV["AZURE_ACCESS_TOKEN"] + "password" => ENV.fetch("AZURE_ACCESS_TOKEN", nil) } $vulnerabilities_fetcher = nil unless ENV["GITHUB_ACCESS_TOKEN"].to_s.strip.empty? puts "GitHub access token has been provided." - github_token = ENV["GITHUB_ACCESS_TOKEN"] # A GitHub access token with read access to public repos + github_token = ENV.fetch("GITHUB_ACCESS_TOKEN", nil) # A GitHub access token with read access to public repos $options[:credentials] << { "type" => "git_source", "host" => "github.com", @@ -155,7 +159,7 @@ # DEPENDABOT_EXTRA_CREDENTIALS, for example: # "[{\"type\":\"npm_registry\",\"registry\":\"registry.npmjs.org\",\"token\":\"123\"}]" unless ENV["DEPENDABOT_EXTRA_CREDENTIALS"].to_s.strip.empty? - $options[:credentials] += JSON.parse(ENV["DEPENDABOT_EXTRA_CREDENTIALS"]) + $options[:credentials] += JSON.parse(ENV.fetch("DEPENDABOT_EXTRA_CREDENTIALS", nil)) end ########################################## @@ -179,22 +183,18 @@ # https://github.com/dependabot/dependabot-core/blob/5926b243b2875ad0d8c0a52c09210c4f5f274c5e/composer/lib/dependabot/composer/update_checker/requirements_updater.rb#L23-L24 if $package_manager == "npm_and_yarn" || $package_manager == "composer" strategy = $options[:requirements_update_strategy] - if strategy == :auto || strategy == :lockfile_only - $options[:requirements_update_strategy] = :bump_versions - end + $options[:requirements_update_strategy] = :bump_versions if strategy == :auto || strategy == :lockfile_only end # For pub, we also correct the strategy # https://github.com/dependabot/dependabot-core/blob/ca9f236591ba49fa6e2a8d5f06e538614033a628/pub/lib/dependabot/pub/update_checker.rb#L110 if $package_manager == "pub" strategy = $options[:requirements_update_strategy] - if strategy == :auto - $options[:requirements_update_strategy] = nil - elsif strategy == :lockfile_only - $options[:requirements_update_strategy] = "bump_versions" - else - $options[:requirements_update_strategy] = strategy.to_s - end + $options[:requirements_update_strategy] = case strategy + when :auto then nil + when :lockfile_only then "bump_versions" + else strategy.to_s + end end end @@ -204,7 +204,7 @@ # [{"dependency-name":"sphinx","dependency-type":"production"}] ################################################################# unless ENV["DEPENDABOT_ALLOW_CONDITIONS"].to_s.strip.empty? - $options[:allow_conditions] = JSON.parse(ENV["DEPENDABOT_ALLOW_CONDITIONS"]) + $options[:allow_conditions] = JSON.parse(ENV.fetch("DEPENDABOT_ALLOW_CONDITIONS", nil)) end # Get allow versions for a dependency @@ -220,8 +220,8 @@ def allow_conditions_for(dep) # Find where the name matches then get the type e.g. production, direct, etc - found = $options[:allow_conditions].find { |al| dep.name.match?(al['dependency-name']) } - found ? found['dependency-type'] : nil + found = $options[:allow_conditions].find { |al| dep.name.match?(al["dependency-name"]) } + found ? found["dependency-type"] : nil end ################################################################# @@ -230,8 +230,8 @@ def allow_conditions_for(dep) # [{"dependency-name":"name","patched-versions":[],"unaffected-versions":[],"affected-versions":["< 0.10.0"]}] ################################################################# unless ENV["DEPENDABOT_SECURITY_ADVISORIES_FILE"].to_s.strip.empty? - security_advisories_file_name = ENV["DEPENDABOT_SECURITY_ADVISORIES_FILE"] - if File.exists?(security_advisories_file_name) + security_advisories_file_name = ENV.fetch("DEPENDABOT_SECURITY_ADVISORIES_FILE", nil) + if File.exist?(security_advisories_file_name) $options[:security_advisories] += JSON.parse(File.read(security_advisories_file_name)) end end @@ -241,7 +241,7 @@ def allow_conditions_for(dep) # DEPENDABOT_IGNORE_CONDITIONS Example: [{"dependency-name":"ruby","versions":[">= 3.a", "< 4"]}] ################################################################################################## unless ENV["DEPENDABOT_IGNORE_CONDITIONS"].to_s.strip.empty? - $options[:ignore_conditions] = JSON.parse(ENV["DEPENDABOT_IGNORE_CONDITIONS"]) + $options[:ignore_conditions] = JSON.parse(ENV.fetch("DEPENDABOT_IGNORE_CONDITIONS", nil)) end ################################################################# @@ -249,7 +249,7 @@ def allow_conditions_for(dep) # DEPENDABOT_LABELS Example: ["npm dependencies","triage-board"] ################################################################# unless ENV["DEPENDABOT_LABELS"].to_s.strip.empty? - $options[:custom_labels] = JSON.parse(ENV["DEPENDABOT_LABELS"]) + $options[:custom_labels] = JSON.parse(ENV.fetch("DEPENDABOT_LABELS", nil)) end ######################################################################### @@ -257,7 +257,7 @@ def allow_conditions_for(dep) # DEPENDABOT_REVIEWERS Example: ["be9321e2-f404-4ffa-8d6b-44efddb04865"] ######################################################################### unless ENV["DEPENDABOT_REVIEWERS"].to_s.strip.empty? - $options[:reviewers] = JSON.parse(ENV["DEPENDABOT_REVIEWERS"]) + $options[:reviewers] = JSON.parse(ENV.fetch("DEPENDABOT_REVIEWERS", nil)) end ######################################################################### @@ -265,7 +265,7 @@ def allow_conditions_for(dep) # DEPENDABOT_ASSIGNEES Example: ["be9321e2-f404-4ffa-8d6b-44efddb04865"] ######################################################################### unless ENV["DEPENDABOT_ASSIGNEES"].to_s.strip.empty? - $options[:assignees] = JSON.parse(ENV["DEPENDABOT_ASSIGNEES"]) + $options[:assignees] = JSON.parse(ENV.fetch("DEPENDABOT_ASSIGNEES", nil)) end # Get ignore versions for a dependency @@ -281,18 +281,21 @@ def ignored_versions_for(dep) Dependabot::Config::UpdateConfig.new(ignore_conditions: ignore_conditions). ignored_versions_for( dep, - security_updates_only: $options[:security_updates_only]) + security_updates_only: $options[:security_updates_only] + ) else $update_config.ignored_versions_for( dep, - security_updates_only: $options[:security_updates_only]) + security_updates_only: $options[:security_updates_only] + ) end end +# rubocop:disable Metrics/PerceivedComplexity def security_advisories_for(dep) relevant_advisories = $options[:security_advisories]. - select { |adv| adv.fetch("dependency-name").casecmp(dep.name).zero? } + select { |adv| adv.fetch("dependency-name").casecmp(dep.name).zero? } # add relevant advisories from the fetcher if present relevant_advisories += $vulnerabilities_fetcher&.fetch(dep.name) || [] @@ -320,6 +323,7 @@ def security_advisories_for(dep) ) end end +# rubocop:enable Metrics/PerceivedComplexity # Create an update checker def update_checker_for(dependency, files, security_advisories) @@ -356,23 +360,22 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, # peer dependency getting updated return false if $options[:security_updates_only] - updated_deps - .reject { |dep| dep.name == dependency_name } - .any? do |dep| - original_peer_dep = ::Dependabot::Dependency.new( - name: dep.name, - version: dep.previous_version, - requirements: dep.previous_requirements, - package_manager: dep.package_manager - ) - update_checker_for(original_peer_dep, files, security_advisories) - .can_update?(requirements_to_unlock: :own) + updated_deps. + reject { |dep| dep.name == dependency_name }. + any? do |dep| + original_peer_dep = ::Dependabot::Dependency.new( + name: dep.name, + version: dep.previous_version, + requirements: dep.previous_requirements, + package_manager: dep.package_manager + ) + update_checker_for(original_peer_dep, files, security_advisories). + can_update?(requirements_to_unlock: :own) end end ActiveSupport::Notifications.subscribe(/excon/) do |*args| name = args.first - return unless name == 'excon.request' || name == 'excon.response' payload = args.last if name == "excon.request" || name == "excon.response" @@ -403,14 +406,14 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, end # Enable security only updates if not enabled and limits is zero -if !$options[:security_updates_only] && $options[:pull_requests_limit] == 0 +if !$options[:security_updates_only] && ($options[:pull_requests_limit]).zero? puts "Pull requests limit is set to zero. Security only updates are implied." $options[:security_updates_only] = true end # Security updates cannot be performed without GitHub token if $options[:security_updates_only] && $vulnerabilities_fetcher.nil? - raise StandardError.new "Security updates are enabled but a GitHub token is not supplied! Cannot proceed" + raise StandardError, "Security updates are enabled but a GitHub token is not supplied! Cannot proceed" end #################################################### @@ -418,8 +421,11 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, #################################################### $options[:azure_port] = ENV["AZURE_PORT"] || ($options[:azure_protocol] == "http" ? "80" : "443") $api_endpoint = "#{$options[:azure_protocol]}://#{$options[:azure_hostname]}:#{$options[:azure_port]}/" -$api_endpoint = $api_endpoint + "#{$options[:azure_virtual_directory]}/" unless $options[:azure_virtual_directory].empty? -$repo_name = "#{$options[:azure_organization]}/#{$options[:azure_project]}/_git/#{$options[:azure_repository]}" # Full name of the repo targeted. +unless $options[:azure_virtual_directory].empty? + $api_endpoint = $api_endpoint + "#{$options[:azure_virtual_directory]}/" +end +# Full name of the repo targeted. +$repo_name = "#{$options[:azure_organization]}/#{$options[:azure_project]}/_git/#{$options[:azure_repository]}" puts "Using '#{$api_endpoint}' as API endpoint" puts "Pull Requests shall be linked to milestone (work item) #{$options[:milestone]}" if $options[:milestone] puts "Pull Requests shall be labeled #{$options[:custom_labels]}" if $options[:custom_labels] @@ -431,7 +437,7 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, api_endpoint: $api_endpoint, repo: $repo_name, directory: $options[:directory], - branch: $options[:branch], + branch: $options[:branch] ) ## Read the update configuration if present @@ -452,7 +458,7 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, cfg_file = Dependabot::Config::FileFetcher.new( source: cfg_source, credentials: $options[:credentials], - options: $options[:updater_options], + options: $options[:updater_options] ).config_file puts "Using configuration file at '#{cfg_file.path}' 😎" Dependabot::Config::File.parse(cfg_file.content) @@ -466,7 +472,9 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, target_branch: $options[:branch] ) -puts "Using '#{$options[:requirements_update_strategy]}' requirements update strategy" if $options[:requirements_update_strategy] +if $options[:requirements_update_strategy] + puts "Using '#{$options[:requirements_update_strategy]}' requirements update strategy" +end ############################## # Fetch the dependency files # @@ -477,7 +485,7 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, source: $source, credentials: $options[:credentials], repo_contents_path: $options[:repo_contents_path], - options: $options[:updater_options], + options: $options[:updater_options] } fetcher = Dependabot::FileFetchers.for_package_manager($package_manager).new(**fetcher_args) if clone @@ -505,7 +513,7 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, ) dependencies = parser.parse -puts "Found #{dependencies.select(&:top_level?).length} dependencies" +puts "Found #{dependencies.count(&:top_level?)} dependencies" dependencies.select(&:top_level?).each { |d| puts " - #{d.name} (#{d.version})" } ################################################ @@ -513,7 +521,7 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, ################################################ azure_client = Dependabot::Clients::Azure.for_source( source: $source, - credentials: $options[:credentials], + credentials: $options[:credentials] ) user_id = azure_client.get_user_id default_branch_name = azure_client.fetch_default_branch($source.repo) @@ -521,19 +529,20 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, pull_requests_count = 0 +# rubocop:disable Metrics/BlockLength dependencies.select(&:top_level?).each do |dep| # Check if we have reached maximum number of open pull requests - if $options[:pull_requests_limit] > 0 && pull_requests_count >= $options[:pull_requests_limit] + if ($options[:pull_requests_limit]).positive? && pull_requests_count >= $options[:pull_requests_limit] puts "Limit of open pull requests (#{$options[:pull_requests_limit]}) reached." break end begin - ######################################### # Get update details for the dependency # ######################################### - puts "Checking if #{dep.name} #{dep.version} #{$options[:security_updates_only] ? 'is vulnerable' : 'needs updating'}" + puts "Checking if #{dep.name} #{dep.version} is vulnerable" if $options[:security_updates_only] + puts "Checking if #{dep.name} #{dep.version} needs updating" unless $options[:security_updates_only] security_advisories = security_advisories_for(dep) checker = update_checker_for(dep, files, security_advisories) @@ -551,7 +560,8 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, # For vulnerable dependencies if checker.vulnerable? if checker.lowest_security_fix_version - puts "#{dep.name} #{dep.version} is vulnerable. Earliest non-vulnerable is #{checker.lowest_security_fix_version}" + puts "#{dep.name} #{dep.version} is vulnerable. Earliest non-vulnerable is " \ + "#{checker.lowest_security_fix_version}" else puts "#{dep.name} #{dep.version} is vulnerable. Can't find non-vulnerable version. 🚨" end @@ -562,18 +572,20 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, next end + # rubocop:disable Lint/ElseLayout requirements_to_unlock = if !checker.requirements_unlocked_or_can_be? if !$options[:excluded_requirements].include?(:none) && - checker.can_update?(requirements_to_unlock: :none) then :none + checker.can_update?(requirements_to_unlock: :none) then :none else :update_not_possible end elsif !$options[:excluded_requirements].include?(:own) && - checker.can_update?(requirements_to_unlock: :own) then :own + checker.can_update?(requirements_to_unlock: :own) then :own elsif !$options[:excluded_requirements].include?(:all) && - checker.can_update?(requirements_to_unlock: :all) then :all + checker.can_update?(requirements_to_unlock: :all) then :all else :update_not_possible end + # rubocop:enable Lint/ElseLayout puts "Requirements to unlock #{requirements_to_unlock}" unless $options[:security_updates_only] if checker.respond_to?(:requirements_update_strategy) @@ -587,7 +599,9 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, # Check if the dependency is allowed allow_type = allow_conditions_for(dep) - allowed = checker.vulnerable? || $options[:allow_conditions].empty? || (allow_type && TYPE_HANDLERS[allow_type].call(dep, checker)) + allowed = checker.vulnerable? || + $options[:allow_conditions].empty? || + (allow_type && TYPE_HANDLERS[allow_type].call(dep, checker)) unless allowed puts "Updating #{dep.name} is not allowed" next @@ -616,9 +630,11 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, ##################################### # Generate updated dependency files # ##################################### - latest_allowed_version = checker.vulnerable? ? + latest_allowed_version = if checker.vulnerable? checker.lowest_resolvable_security_fix_version - : checker.latest_resolvable_version + else + checker.latest_resolvable_version + end if updated_deps.count == 1 dep_first = updated_deps.first prev_v = dep_first.previous_version @@ -685,12 +701,11 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, # chore(deps): bump dotenv from 9.0.1 to 9.0.2 in /server next unless title.include?(" #{dep.display_name} from #{dep.version} to ") - # In case the Pull Request title contains an explicit path, check that path to make sure it is the same Pull Request - # For example: - # 'Bump hashicorp/azurerm from 3.1.0 to 3.12.3 in /projectA' denotes a different Pull Request than 'Bump hashicorp/azurerm from 3.1.0 to 3.12.3 in /projectB' - if updated_files.first.directory != "/" - next unless title.end_with?(" in #{updated_files.first.directory}") - end + # In case the Pull Request title contains an explicit path, check that path + # to make sure it is the same Pull Request. For example: + # 'Bump hashicorp/azurerm from 3.1.0 to 3.12.3 in /projectA' denotes a different + # Pull Request from 'Bump hashicorp/azurerm from 3.1.0 to 3.12.3 in /projectB' + next if updated_files.first.directory != "/" && !title.end_with?(" in #{updated_files.first.directory}") # If the title does not contain the updated version, we need to abandon the PR and delete # it's branch, because there is a newer version available. @@ -721,7 +736,6 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, break end - pull_request = nil pull_request_id = nil if conflict_pull_request_commit && conflict_pull_request_id ############################################## @@ -734,7 +748,7 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, files: updated_files, credentials: $options[:credentials], pull_request_number: conflict_pull_request_id, - author_details: $options[:author_details], + author_details: $options[:author_details] ) puts "Submitting pull request (##{conflict_pull_request_id}) update for #{dep.name}." @@ -762,7 +776,7 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, automerge_candidate: $options[:set_auto_complete], github_redirection_service: Dependabot::PullRequestCreator::DEFAULT_GITHUB_REDIRECTION_SERVICE, provider_metadata: { - work_item: $options[:milestone], + work_item: $options[:milestone] } ) @@ -780,7 +794,7 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, message = content["message"] puts "Failed! PR already exists or an error has occurred." # throw exception here because pull_request.create does not throw - raise StandardError.new "Pull Request creation failed with status #{req_status}. Message: #{message}" + raise StandardError, "Pull Request creation failed with status #{req_status}. Message: #{message}" end else puts "Seems PR is already present." @@ -809,7 +823,7 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, # Pull requests that pass all policies will be merged automatically. # Optional policies can be ignored by passing their identifiers if $options[:set_auto_complete] - auto_complete_user_id = pull_request['createdBy']['id'] + auto_complete_user_id = pull_request["createdBy"]["id"] puts "Setting auto complete on ##{pull_request_id}." azure_client.pull_request_auto_complete( # Adding argument names will fail! Maybe because there is no spec? @@ -819,9 +833,9 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, $options[:auto_complete_ignore_config_ids] ) end - rescue StandardError => e raise e if $options[:fail_on_exception] + puts "Error working on updates for #{dep.name} #{dep.version} (continuing)" puts e.full_message end @@ -849,8 +863,9 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, requirement_class = Dependabot::Utils.requirement_class_for_package_manager(dep.package_manager) version_class = Dependabot::Utils.version_class_for_package_manager(dep.package_manager) # necessary for npm next unless version_class.correct?(dep.version) # git_submodules don't work here - ignore_reqs = ignored_versions_for(dep) - .flat_map { |req| requirement_class.requirements_array(req) } + + ignore_reqs = ignored_versions_for(dep). + flat_map { |req| requirement_class.requirements_array(req) } if ignore_reqs.any? { |req| req.satisfied_by?(version_class.new(dep.version)) } puts "Update for #{dep.name} #{dep.version} is no longer required." next @@ -880,9 +895,9 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, azure_client.pull_request_abandon(pr_id) end end - rescue StandardError => e raise e if $options[:fail_on_exception] + puts "Error checking whether to abandon (or abandoning) PR ##{pr_id} (continuing)" puts e.full_message end @@ -890,3 +905,7 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps, files, end puts "Done" + +# rubocop:enable Metrics/BlockLength + +# rubocop:enable Style/GlobalVars diff --git a/updater/bin/vulnerabilities.rb b/updater/bin/vulnerabilities.rb index 25d8bf93..950b773c 100644 --- a/updater/bin/vulnerabilities.rb +++ b/updater/bin/vulnerabilities.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "octokit" module Dependabot @@ -16,7 +18,7 @@ class QueryError < StandardError; end "pip" => "PIP", "pub" => "PUB", "bundler" => "RUBYGEMS", - "cargo" => "RUST", + "cargo" => "RUST" }.freeze GRAPHQL_QUERY = <<-GRAPHQL @@ -34,31 +36,30 @@ class QueryError < StandardError; end def initialize(package_manager, github_token) @ecosystem = ECOSYSTEM_LOOKUP.fetch(package_manager, nil) - @client ||= Octokit::Client.new(:access_token => github_token) + @client ||= Octokit::Client.new(access_token: github_token) end def fetch(dependency_name) [] unless @ecosystem variables = { ecosystem: @ecosystem, package: dependency_name } - response = @client.post'/graphql', { query: GRAPHQL_QUERY, variables: variables }.to_json - raise(QueryError, response[:errors]&.map{|e| e.message }&.join(", ")) if response[:errors] + response = @client.post "/graphql", { query: GRAPHQL_QUERY, variables: variables }.to_json + raise(QueryError, response[:errors]&.map(&:message)&.join(", ")) if response[:errors] vulnerabilities = [] - response.data[:securityVulnerabilities][:nodes].map do | node | + response.data[:securityVulnerabilities][:nodes].map do |node| vulnerable_version_range = node[:vulnerableVersionRange] first_patched_version = node.dig :firstPatchedVersion, :identifier vulnerabilities << { "dependency-name" => dependency_name, "affected-versions" => [vulnerable_version_range], "patched-versions" => [first_patched_version], - "unaffected-versions" => [], + "unaffected-versions" => [] } end vulnerabilities end - end end end