diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 8767c6ab75f40..b6466a15e6dd6 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -112,7 +112,7 @@ jobs: - name: browsers run: ./go update_browsers ${{ inputs.chrome_channel }} - name: devtools - run: ./go all:update_cdp ${{ inputs.chrome_channel }} + run: ./go update_cdp ${{ inputs.chrome_channel }} - name: manager run: ./go update_manager - name: dependencies diff --git a/BUILD.bazel b/BUILD.bazel index 6a156f8f327bd..efeacc6f1b3f4 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -25,7 +25,7 @@ filegroup( name = "rakefile", srcs = [ "Rakefile", - ], + ] + glob(["rake_tasks/*.rake", "rake_tasks/*.rb"]), visibility = ["//rb:__subpackages__"], ) diff --git a/Rakefile b/Rakefile index b4e4798fc72cd..2ba6b6e6b64d6 100644 --- a/Rakefile +++ b/Rakefile @@ -14,90 +14,42 @@ require 'fileutils' require 'open-uri' require 'git' require 'find' -require 'set' Rake.application.instance_variable_set(:@name, 'go') orig_verbose = verbose verbose(false) -# Location of all new (non-CrazyFun) methods -require 'rake/task' require 'rake_tasks/bazel' +require 'rake_tasks/common' $DEBUG = orig_verbose != Rake::FileUtilsExt::DEFAULT $DEBUG = true if ENV['debug'] == 'true' verbose($DEBUG) -@git = Git.open(__dir__) - -def java_version - File.foreach('java/version.bzl') do |line| - return line.split('=').last.strip.tr('"', '') if line.include?('SE_VERSION') - end -end +SeleniumRake.git = Git.open(__dir__) + +# Load language-specific rake files within namespaces +namespace(:java) { load 'rake_tasks/java.rake' } +namespace(:rb) { load 'rake_tasks/ruby.rake' } +namespace(:ruby) { load 'rake_tasks/ruby.rake' } +namespace(:py) { load 'rake_tasks/python.rake' } +namespace(:python) { load 'rake_tasks/python.rake' } # alias +namespace(:node) { load 'rake_tasks/node.rake' } +namespace(:js) { load 'rake_tasks/node.rake' } # alias +namespace(:javascript) { load 'rake_tasks/node.rake' } # alias +namespace(:dotnet) { load 'rake_tasks/dotnet.rake' } +namespace(:rust) { load 'rake_tasks/rust.rake' } +namespace(:grid) { load 'rake_tasks/grid.rake' } +namespace(:bazel) { load 'rake_tasks/bazel.rake' } +namespace(:appium) { load 'rake_tasks/appium.rake' } # If it looks like a bazel target, build it with bazel rule(%r{//.*}) do |task| Bazel.execute('build', %w[], task.name) end -# use #java_release_targets to access this list -JAVA_RELEASE_TARGETS = %w[ - //java/src/org/openqa/selenium/chrome:chrome.publish - //java/src/org/openqa/selenium/chromium:chromium.publish - //java/src/org/openqa/selenium/devtools/v143:v143.publish - //java/src/org/openqa/selenium/devtools/v144:v144.publish - //java/src/org/openqa/selenium/devtools/v142:v142.publish - //java/src/org/openqa/selenium/edge:edge.publish - //java/src/org/openqa/selenium/firefox:firefox.publish - //java/src/org/openqa/selenium/grid/sessionmap/jdbc:jdbc.publish - //java/src/org/openqa/selenium/grid/sessionmap/redis:redis.publish - //java/src/org/openqa/selenium/grid:bom-dependencies.publish - //java/src/org/openqa/selenium/grid:bom.publish - //java/src/org/openqa/selenium/grid:grid.publish - //java/src/org/openqa/selenium/ie:ie.publish - //java/src/org/openqa/selenium/json:json.publish - //java/src/org/openqa/selenium/manager:manager.publish - //java/src/org/openqa/selenium/os:os.publish - //java/src/org/openqa/selenium/remote/http:http.publish - //java/src/org/openqa/selenium/remote:remote.publish - //java/src/org/openqa/selenium/safari:safari.publish - //java/src/org/openqa/selenium/support:support.publish - //java/src/org/openqa/selenium:client-combined.publish - //java/src/org/openqa/selenium:core.publish -].freeze - -def java_release_targets - @targets_verified ||= verify_java_release_targets - - JAVA_RELEASE_TARGETS -end - -def verify_java_release_targets - query = 'kind(maven_publish, set(//java/... //third_party/...))' - current_targets = [] - - Bazel.execute('query', [], query) do |output| - current_targets = output.lines.map(&:strip).reject(&:empty?).select { |line| line.start_with?('//') } - end - - missing_targets = current_targets - JAVA_RELEASE_TARGETS - extra_targets = JAVA_RELEASE_TARGETS - current_targets - - return if missing_targets.empty? && extra_targets.empty? - - error_message = 'Java release targets are out of sync with Bazel query results.' - - error_message += "\nMissing targets: #{missing_targets.join(', ')}" unless missing_targets.empty? - - error_message += "\nObsolete targets: #{extra_targets.join(', ')}" unless extra_targets.empty? - - raise error_message -end - -# Notice that because we're using rake, anything you can do in a normal rake -# build can also be done here. For example, here we set the default task task default: [:grid] +task grid: [:'java:grid'] # ./go update_browser stable # ./go update_browser beta @@ -105,11 +57,11 @@ desc 'Update pinned browser versions' task :update_browsers, [:channel] do |_task, arguments| chrome_channel = arguments[:channel] || 'Stable' chrome_channel = 'beta' if chrome_channel == 'early-stable' - args = Array(chrome_channel) ? ['--', "--chrome_channel=#{chrome_channel.capitalize}"] : [] + args = ['--', "--chrome_channel=#{chrome_channel.capitalize}"] puts 'pinning updated browsers and drivers' Bazel.execute('run', args, '//scripts:pinned_browsers') - @git.add('common/repositories.bzl') + SeleniumRake.git.add('common/repositories.bzl') end desc 'Update Selenium Manager to latest release' @@ -117,1016 +69,115 @@ task :update_manager do |_task, _arguments| puts 'Updating Selenium Manager references' Bazel.execute('run', [], '//scripts:selenium_manager') - @git.add('common/selenium_manager.bzl') -end - -task grid: ['java:grid'] - -desc 'Generate Javadocs' -task javadocs: %i[//java/src/org/openqa/selenium/grid:all-javadocs] do - FileUtils.rm_rf('build/docs/api/java') - FileUtils.mkdir_p('build/docs/api/java') - out = 'bazel-bin/java/src/org/openqa/selenium/grid/all-javadocs.jar' - - cmd = %(cd build/docs/api/java && jar xf "../../../../#{out}" 2>&1) - windows = RbConfig::CONFIG['host_os'] =~ /mswin|msys|mingw32/ - cmd = cmd.tr('/', '\\').tr(':', ';') if windows - raise 'could not unpack javadocs' unless system(cmd) - - File.open('build/docs/api/java/stylesheet.css', 'a') do |file| - file.write(<<~STYLE - /* Custom selenium-specific styling */ - .blink { - animation: 2s cubic-bezier(0.5, 0, 0.85, 0.85) infinite blink; - } - - @keyframes blink { - 50% { - opacity: 0; - } - } - - STYLE - ) - end -end - -desc 'Update dependencies for the release' -task :release_update do |_task, _arguments| - Rake::Task[:update_multitool].invoke - Rake::Task['java:update'].invoke - Rake::Task['node:update'].invoke + SeleniumRake.git.add('common/selenium_manager.bzl') end desc 'Update multitool binaries to latest releases' task :update_multitool do |_task, _arguments| puts 'Updating multitool binary versions' Bazel.execute('run', [], '//scripts:update_multitool_binaries') - @git.add('multitool.lock.json') -end - -task ios_driver: [ - '//javascript/atoms/fragments:get_visible_text:ios', - '//javascript/atoms/fragments:click:ios', - '//javascript/atoms/fragments:back:ios', - '//javascript/atoms/fragments:forward:ios', - '//javascript/atoms/fragments:submit:ios', - '//javascript/atoms/fragments:xpath:ios', - '//javascript/atoms/fragments:xpaths:ios', - '//javascript/atoms/fragments:type:ios', - '//javascript/atoms/fragments:get_attribute:ios', - '//javascript/atoms/fragments:clear:ios', - '//javascript/atoms/fragments:is_selected:ios', - '//javascript/atoms/fragments:is_enabled:ios', - '//javascript/atoms/fragments:is_shown:ios', - '//javascript/atoms/fragments:stringify:ios', - '//javascript/atoms/fragments:link_text:ios', - '//javascript/atoms/fragments:link_texts:ios', - '//javascript/atoms/fragments:partial_link_text:ios', - '//javascript/atoms/fragments:partial_link_texts:ios', - '//javascript/atoms/fragments:get_interactable_size:ios', - '//javascript/atoms/fragments:scroll_into_view:ios', - '//javascript/atoms/fragments:get_effective_style:ios', - '//javascript/atoms/fragments:get_element_size:ios', - '//javascript/webdriver/atoms/fragments:get_location_in_view:ios' -] - -# This task does not allow running RBE, to run stamped with RBE use -# ./go java:package['--config=release'] -desc 'Create stamped zipped assets for Java for uploading to GitHub' -task :'java-release-zip' do - Rake::Task['java:package'].invoke('--config=rbe_release') -end - -task 'release-java': %i[java-release-zip publish-maven] - -RELEASE_CREDENTIALS = { - java: { - env: [%w[MAVEN_USER SEL_M2_USER], %w[MAVEN_PASSWORD SEL_M2_PASS]], - file: -> { File.exist?("#{Dir.home}/.m2/settings.xml") && File.read("#{Dir.home}/.m2/settings.xml").include?('central') } - }, - java_gpg: {cmd: 'gpg'}, - dotnet: {env: [%w[NUGET_API_KEY]]}, - dotnet_nightly: {env: [%w[GITHUB_TOKEN]]} -}.freeze - -def verify_package_published(url) - puts "Verifying #{url}..." - uri = URI(url) - res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') { |http| http.request(Net::HTTP::Get.new(uri)) } - raise "Package not published: #{url}" unless res.is_a?(Net::HTTPSuccess) - - puts 'Verified!' -end - -def sonatype_api_post(url, token) - uri = URI(url) - req = Net::HTTP::Post.new(uri) - req['Authorization'] = "Basic #{token}" - - res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) } - raise "Sonatype API error (#{res.code}): #{res.body}" unless res.is_a?(Net::HTTPSuccess) - - res.body.to_s.empty? ? {} : JSON.parse(res.body) -end - -def credential_valid?(cred) - has_env = cred[:env]&.all? { |vars| vars.any? { |v| ENV.fetch(v, nil) } } - has_file = cred[:file]&.call - has_cmd = cred[:cmd] && (system('which', cred[:cmd], out: File::NULL, err: File::NULL) || system('where', cred[:cmd], out: File::NULL, err: File::NULL)) - has_env || has_file || has_cmd -end - -def setup_npm_auth - npmrc = File.join(Dir.home, '.npmrc') - return if File.exist?(npmrc) && File.read(npmrc).include?('//registry.npmjs.org/:_authToken=') - - token = ENV.fetch('NODE_AUTH_TOKEN', nil) - raise 'Missing npm credentials: set NODE_AUTH_TOKEN or configure ~/.npmrc' if token.nil? || token.empty? - - auth_line = "//registry.npmjs.org/:_authToken=#{token}" - if File.exist?(npmrc) - File.open(npmrc, 'a') { |f| f.puts(auth_line) } - else - File.write(npmrc, "#{auth_line}\n") - end - File.chmod(0o600, npmrc) -end - -def setup_gem_credentials - gem_dir = File.join(Dir.home, '.gem') - credentials = File.join(gem_dir, 'credentials') - return if File.exist?(credentials) && File.read(credentials).include?(':rubygems_api_key:') - - token = ENV.fetch('GEM_HOST_API_KEY', nil) - if token.nil? || token.empty? - raise 'Missing RubyGems credentials: set GEM_HOST_API_KEY or configure ~/.gem/credentials' - end - - FileUtils.mkdir_p(gem_dir) - if File.exist?(credentials) - File.open(credentials, 'a') { |f| f.puts(":rubygems_api_key: #{token}") } - else - File.write(credentials, ":rubygems_api_key: #{token}\n") - end - File.chmod(0o600, credentials) -end - -def setup_pypirc - pypirc = File.join(Dir.home, '.pypirc') - return if File.exist?(pypirc) && File.read(pypirc).match?(/^\[pypi\]/m) - - token = ENV.fetch('TWINE_PASSWORD', nil) - raise 'Missing PyPI credentials: set TWINE_PASSWORD or configure ~/.pypirc' if token.nil? || token.empty? - - pypi_section = <<~PYPIRC - [pypi] - username = __token__ - password = #{token} - PYPIRC - - if File.exist?(pypirc) - File.open(pypirc, 'a') { |f| f.puts("\n#{pypi_section}") } - else - File.write(pypirc, pypi_section) - end - File.chmod(0o600, pypirc) -end - -def check_credentials(langs) - missing = langs.select { |lang| RELEASE_CREDENTIALS[lang] && !credential_valid?(RELEASE_CREDENTIALS[lang]) } - raise "Missing credentials: #{missing.join(', ')}" if missing.any? + SeleniumRake.git.add('multitool.lock.json') end -def read_m2_user_pass - puts 'Maven environment variables not set, inspecting ~/.m2/settings.xml.' - settings = File.read("#{Dir.home}/.m2/settings.xml") - found_section = false - settings.each_line do |line| - if !found_section - found_section = line.include? 'central' - elsif line.include?('') - ENV['MAVEN_USER'] = line[%r{(.*?)}, 1] - elsif line.include?('') - ENV['MAVEN_PASSWORD'] = line[%r{(.*?)}, 1] - end - break if ENV['MAVEN_PASSWORD'] && ENV['MAVEN_USER'] - end +desc 'Update dependencies for release' +task :release_update do |_task, _arguments| + Rake::Task[:update_multitool].invoke end -desc 'Publish all Java jars to Maven as stable release' -task 'publish-maven' do - Rake::Task['java:release'].invoke -end +desc 'Update Chrome DevTools support' +task :update_cdp, [:channel] do |_task, arguments| + chrome_channel = arguments[:channel] || 'stable' + chrome_channel = 'beta' if chrome_channel == 'early-stable' + args = ['--', "--chrome_channel=#{chrome_channel.capitalize}"] -desc 'Publish all Java jars to Maven as nightly release' -task 'publish-maven-snapshot' do - Rake::Task['java:release'].invoke('nightly') -end + puts "Updating Chrome DevTools references to include latest from #{chrome_channel} channel" + Bazel.execute('run', args, '//scripts:update_cdp') -desc 'Install jars to local m2 directory' -task :'maven-install' do - java_release_targets.each do |p| - Bazel.execute('run', - ['--stamp', - '--define', - "maven_repo=file://#{Dir.home}/.m2/repository", - '--define', - 'gpg_sign=false'], - p) - end + ['common/devtools/', + 'dotnet/src/webdriver/DevTools/', + 'dotnet/src/webdriver/Selenium.WebDriver.csproj', + 'dotnet/test/common/DevTools/', + 'dotnet/test/common/CustomDriverConfigs/', + 'dotnet/selenium-dotnet-version.bzl', + 'java/src/org/openqa/selenium/devtools/', + 'javascript/selenium-webdriver/BUILD.bazel', + 'py/BUILD.bazel', + 'rb/lib/selenium/devtools/', + 'rb/Gemfile.lock', + 'rake_tasks/java.rake'].each { |file| SeleniumRake.git.add(file) } end -desc 'Build the selenium client jars' -task 'selenium-java' => '//java/src/org/openqa/selenium:client-combined' +task ios_driver: 'appium:build' desc 'Update AUTHORS file' task :authors do puts 'Updating AUTHORS file' sh "(git log --use-mailmap --format='%aN <%aE>' ; cat .OLD_AUTHORS) | sort -uf > AUTHORS" - @git.add('AUTHORS') -end - -def node_version - File.foreach('javascript/selenium-webdriver/package.json') do |line| - return line.split(':').last.strip.tr('",', '') if line.include?('version') - end -end -namespace :node do - desc 'Build Node npm package' - task :build do |_task, arguments| - args = arguments.to_a.compact - Bazel.execute('build', args, '//javascript/selenium-webdriver') - end - - desc 'Pin JavaScript dependencies via pnpm lockfile' - task :pin do - Bazel.execute('run', ['--', 'install', '--dir', Dir.pwd, '--lockfile-only'], '@pnpm//:pnpm') - @git.add('pnpm-lock.yaml') - end - - desc 'Update JavaScript dependencies and refresh lockfile (use "latest" to bump ranges)' - task :update, [:latest] do |_task, arguments| - args = ['--', 'update', '-r'] - args << '--latest' if arguments[:latest] == 'latest' - args += ['--dir', Dir.pwd] - Bazel.execute('run', args, '@pnpm//:pnpm') - @git.add('javascript/selenium-webdriver/package.json') - Rake::Task['node:pin'].invoke - end - - task :'dry-run' do - Bazel.execute('run', ['--stamp'], - '//javascript/selenium-webdriver:selenium-webdriver.publish -- --dry-run=true') - end - - desc 'Release Node npm package' - task :release do |_task, arguments| - nightly = arguments.to_a.include?('nightly') - setup_npm_auth unless nightly - - if nightly - puts 'Updating Node version to nightly...' - Rake::Task['node:version'].invoke('nightly') if nightly - end - - puts 'Running Node package release...' - Bazel.execute('run', ['--config=release'], '//javascript/selenium-webdriver:selenium-webdriver.publish') - end - - desc 'Verify Node package is published on npm' - task :verify do - verify_package_published("https://registry.npmjs.org/selenium-webdriver/#{node_version}") - end - - task deploy: :release - - desc 'Generate Node documentation' - task :docs do |_task, arguments| - if node_version.include?('nightly') && !arguments.to_a.include?('force') - abort('Aborting documentation update: nightly versions should not update docs.') - end - - puts 'Generating Node documentation' - FileUtils.rm_rf('build/docs/api/javascript/') - Bazel.execute('run', [], '//javascript/selenium-webdriver:docs') - end - - desc 'Update JavaScript changelog' - task :changelog do - header = "## #{node_version}\n" - update_changelog(node_version, 'javascript', 'javascript/selenium-webdriver/', - 'javascript/selenium-webdriver/CHANGES.md', header) - end - - desc 'Update Node version' - task :version, [:version] do |_task, arguments| - old_version = node_version - nightly = "-nightly#{Time.now.strftime('%Y%m%d%H%M')}" - new_version = updated_version(old_version, arguments[:version], nightly) - puts "Updating Node from #{old_version} to #{new_version}" - - %w[javascript/selenium-webdriver/package.json javascript/selenium-webdriver/BUILD.bazel].each do |file| - text = File.read(file).gsub(old_version, new_version) - File.open(file, 'w') { |f| f.puts text } - @git.add(file) - end - end -end - -def python_version - File.foreach('py/BUILD.bazel') do |line| - return line.split('=').last.strip.tr('"', '') if line.include?('SE_VERSION') - end -end -namespace :py do - desc 'Build Python wheel and sdist with optional arguments' - task :build do |_task, arguments| - args = arguments.to_a.compact - Bazel.execute('build', args, '//py:selenium-wheel') - Bazel.execute('build', args, '//py:selenium-sdist') - end - - desc 'Release Python wheel and sdist to pypi' - task :release do |_task, arguments| - nightly = arguments.to_a.include?('nightly') - setup_pypirc unless nightly - - if nightly - puts 'Updating Python version to nightly...' - Rake::Task['py:version'].invoke('nightly') - end - - command = nightly ? '//py:selenium-release-nightly' : '//py:selenium-release' - puts "Running Python release command: #{command}" - Bazel.execute('run', ['--config=release'], command) - end - - desc 'Verify Python package is published on PyPI' - task :verify do - verify_package_published("https://pypi.org/pypi/selenium/#{python_version}/json") - end - - desc 'generate and copy files required for local development' - task :local_dev do - Bazel.execute('build', [], '//py:selenium') - Rake::Task['grid'].invoke - - FileUtils.rm_rf('py/selenium/webdriver/common/devtools/') - FileUtils.cp_r('bazel-bin/py/selenium/webdriver/.', 'py/selenium/webdriver', remove_destination: true) - end - - desc 'Update generated Python files for local development' - task :clean do - Bazel.execute('build', [], '//py:selenium') - bazel_bin_path = 'bazel-bin/py/selenium/webdriver' - lib_path = 'py/selenium/webdriver' - - dirs = %w[devtools linux mac windows] - dirs.each { |dir| FileUtils.rm_rf("#{lib_path}/common/#{dir}") } - - Find.find(bazel_bin_path) do |path| - if File.directory?(path) && dirs.any? { |dir| path.include?("common/#{dir}") } - Find.prune - next - end - next if File.directory?(path) - - target_file = File.join(lib_path, path.sub(%r{^#{bazel_bin_path}/}, '')) - if File.exist?(target_file) - puts "Removing target file: #{target_file}" - FileUtils.rm(target_file) - end - end - end - - desc 'Generate Python documentation' - task :docs do |_task, arguments| - if python_version.match?(/^\d+\.\d+\.\d+\.\d+$/) && !arguments.to_a.include?('force') - abort('Aborting documentation update: nightly versions should not update docs.') - end - puts 'Generating Python documentation' - - FileUtils.rm_rf('build/docs/api/py/') - - # Generate API listing and stub files in source tree - Bazel.execute('run', [], '//py:generate-api-listing') - Bazel.execute('run', [], '//py:sphinx-autogen') - - # Build docs (outputs to bazel-bin) - Bazel.execute('build', [], '//py:docs') - - FileUtils.mkdir_p('build/docs/api') - FileUtils.cp_r('bazel-bin/py/docs/_build/html/.', 'build/docs/api/py') - end - - desc 'Install Python wheel locally' - task :install do - Bazel.execute('build', [], '//py:selenium-wheel') - begin - sh 'pip install bazel-bin/py/selenium-*.whl' - rescue StandardError - puts 'Ensure that Python and pip are installed on your system' - raise - end - end - - desc 'Update Python changelog' - task :changelog do - header = "Selenium #{python_version}" - update_changelog(python_version, 'py', 'py/selenium/webdriver', 'py/CHANGES', header) - end - - desc 'Update Python version' - task :version, [:version] do |_task, arguments| - old_version = python_version - nightly = ".#{Time.now.strftime('%Y%m%d%H%M')}" - new_version = updated_version(old_version, arguments[:version], nightly) - puts "Updating Python from #{old_version} to #{new_version}" - - ['py/pyproject.toml', - 'py/BUILD.bazel', - 'py/selenium/__init__.py', - 'py/selenium/webdriver/__init__.py', - 'py/docs/source/conf.py'].each do |file| - text = File.read(file).gsub(old_version, new_version) - File.open(file, 'w') { |f| f.puts text } - @git.add(file) - end - - old_short_version = old_version.split('.')[0..1].join('.') - new_short_version = new_version.split('.')[0..1].join('.') - - conf = 'py/docs/source/conf.py' - text = File.read(conf).gsub(old_short_version, new_short_version) - File.open(conf, 'w') { |f| f.puts text } - @git.add(conf) - end - - namespace :test do - desc 'Python unit tests' - task :unit do - Rake::Task['py:clean'].invoke - Bazel.execute('test', ['--test_size_filters=small'], '//py/...') - end - - %i[chrome edge firefox safari].each do |browser| - desc "Python #{browser} tests" - task browser do - Rake::Task['py:clean'].invoke - Bazel.execute('test', [], "//py:common-#{browser}") - Bazel.execute('test', [], "//py:test-#{browser}") - end - end - - desc 'Python Remote tests with Chrome' - task :remote do - Rake::Task['py:clean'].invoke - Bazel.execute('test', [], '//py:test-remote') - end - end - - namespace :test do - desc 'Python unit tests' - task :unit do - Rake::Task['py:clean'].invoke - Bazel.execute('test', ['--test_size_filters=small'], '//py/...') - end - - %i[chrome edge firefox safari].each do |browser| - desc "Python #{browser} tests" - task browser do - Rake::Task['py:clean'].invoke - Bazel.execute('test', %w[--test_output all], "//py:common-#{browser}") - Bazel.execute('test', %w[--test_output all], "//py:test-#{browser}") - end - end - end -end - -def ruby_version - File.foreach('rb/lib/selenium/webdriver/version.rb') do |line| - return line.split('=').last.strip.tr("'", '') if line.include?('VERSION') - end -end -namespace :rb do - desc 'Generate Ruby gems' - task :build do |_task, arguments| - args = arguments.to_a.compact - webdriver = args.delete('webdriver') - devtools = args.delete('devtools') - - Bazel.execute('build', args, '//rb:selenium-webdriver') if webdriver || !devtools - Bazel.execute('build', args, '//rb:selenium-devtools') if devtools || !webdriver - end - - desc 'Update generated Ruby files for local development' - task :local_dev do - puts 'installing ruby, this may take a minute' - Bazel.execute('build', [], '@bundle//:bundle') - Rake::Task['rb:build'].invoke - Rake::Task['grid'].invoke - # A command like this is required to move ruby binary into working directory - Bazel.execute('build', %w[--test_arg --dry-run], '@bundle//bin:rubocop') - end - - desc 'Push Ruby gems to rubygems' - task :release do |_task, arguments| - if arguments.to_a.include?('nightly') - puts 'Bumping Ruby nightly version...' - Bazel.execute('run', [], '//rb:selenium-webdriver-bump-nightly-version') - - puts 'Releasing nightly WebDriver gem...' - Bazel.execute('run', ['--config=release'], '//rb:selenium-webdriver-release-nightly') - else - setup_gem_credentials - patch_release = ruby_version.split('.').fetch(2, '0').to_i.positive? - - puts 'Releasing Ruby gems...' - Bazel.execute('run', ['--config=release'], '//rb:selenium-webdriver-release') - Bazel.execute('run', ['--config=release'], '//rb:selenium-devtools-release') unless patch_release - end - end - - desc 'Verify Ruby packages are published on RubyGems' - task :verify do - patch_release = ruby_version.split('.').fetch(2, '0').to_i.positive? - - verify_package_published("https://rubygems.org/api/v2/rubygems/selenium-webdriver/versions/#{ruby_version}.json") - unless patch_release - verify_package_published("https://rubygems.org/api/v2/rubygems/selenium-devtools/versions/#{ruby_version}.json") - end - end - - desc 'Generate Ruby documentation' - task :docs do |_task, arguments| - if ruby_version.include?('nightly') && !arguments.to_a.include?('force') - abort('Aborting documentation update: nightly versions should not update docs.') - end - puts 'Generating Ruby documentation' - - FileUtils.rm_rf('build/docs/api/rb/') - Bazel.execute('run', [], '//rb:docs') - FileUtils.mkdir_p('build/docs/api') - FileUtils.cp_r('bazel-bin/rb/docs.sh.runfiles/_main/docs/api/rb/.', 'build/docs/api/rb') - end - - desc 'Update Ruby changelog' - task :changelog do - header = "#{ruby_version} (#{Time.now.strftime('%Y-%m-%d')})\n=========================" - update_changelog(ruby_version, 'rb', 'rb/lib/', 'rb/CHANGES', header) - end - - desc 'Update Ruby version' - task :version, [:version] do |_task, arguments| - old_version = ruby_version - new_version = updated_version(old_version, arguments[:version], '.nightly') - puts "Updating Ruby from #{old_version} to #{new_version}" - - file = 'rb/lib/selenium/webdriver/version.rb' - text = File.read(file).gsub(old_version, new_version) - File.open(file, 'w') { |f| f.puts text } - @git.add(file) - - Rake::Task['rb:update'].invoke - end - - desc 'Update Ruby Syntax' - task :lint do |_task, arguments| - args = arguments.to_a.compact - Bazel.execute('run', args, '//rb:lint') - end - - desc 'Sync gem checksums from Gemfile.lock to MODULE.bazel (use force to re-download all)' - task :pin, [:force] do |_task, arguments| - require 'digest' - - gemfile_lock = 'rb/Gemfile.lock' - module_bazel = 'MODULE.bazel' - force = arguments[:force] == 'force' - - lock_content = File.read(gemfile_lock) - gem_section = lock_content[/GEM\n\s+remote:.*?\n\s+specs:\n(.*?)(?=\n[A-Z]|\Z)/m, 1] - gems = gem_section.scan(/^ ([a-zA-Z0-9_-]+) \(([^)]+)\)$/) - needed_gems = gems.map { |name, version| "#{name}-#{version}" } - - # Parse existing checksums from MODULE.bazel - module_content = File.read(module_bazel) - existing = module_content.scan(/"([^"]+)":\s*"([a-f0-9]{64})"/).to_h - - # Keep existing checksums for gems still in Gemfile.lock (unless force) - checksums = force ? {} : existing.slice(*needed_gems) - to_download = needed_gems - checksums.keys - - puts "Found #{gems.size} gems: #{checksums.size} cached, #{to_download.size} to download..." - - failed = [] - to_download.each do |key| - uri = URI("https://rubygems.org/gems/#{key}.gem") - - 5.times do - response = Net::HTTP.get_response(uri) - break unless response.is_a?(Net::HTTPRedirection) - - uri = URI(response['location']) - end - - unless response.is_a?(Net::HTTPSuccess) - puts " #{key}: failed (HTTP #{response.code})" - failed << key - next - end - - sha = Digest::SHA256.hexdigest(response.body) - checksums[key] = sha - puts " #{key}: #{sha[0, 16]}..." - rescue StandardError => e - puts " #{key}: failed (#{e.message})" - failed << key - end - - raise "Failed to download checksums for: #{failed.join(', ')}" if failed.any? - - checksums_lines = checksums.keys.sort.map { |k| " \"#{k}\": \"#{checksums[k]}\"," } - formatted = " gem_checksums = {\n#{checksums_lines.join("\n")}\n }," - - new_content = module_content.sub(/ gem_checksums = \{[^}]+\},/m, formatted) - File.write(module_bazel, new_content) - - @git.add(module_bazel) - end - - desc 'Update Ruby dependencies and sync checksums to MODULE.bazel' - task :update do - puts 'updating and pinning gem versions' - Bazel.execute('run', [], '//rb:bundle-update') - @git.add('rb/Gemfile.lock') - Bazel.execute('run', [], '//rb:rbs-update') - @git.add('rb/rbs_collection.lock.yaml') - Rake::Task['rb:pin'].invoke - end -end - -def dotnet_version - File.foreach('dotnet/selenium-dotnet-version.bzl') do |line| - return line.split('=').last.strip.tr('"', '') if line.include?('SE_VERSION') - end + SeleniumRake.git.add('AUTHORS') end -namespace :dotnet do - desc 'Build nupkg files' - task :build do |_task, arguments| - args = arguments.to_a.compact - Bazel.execute('build', args, '//dotnet:all') - end - - desc 'Package .NET bindings into zipped assets and stage for release' - task :package do |_task, arguments| - args = arguments.to_a.compact.empty? ? ['--stamp'] : arguments.to_a.compact - Rake::Task['dotnet:build'].invoke(*args) - mkdir_p 'build/dist' - FileUtils.rm_f(Dir.glob('build/dist/*dotnet*')) - - FileUtils.copy('bazel-bin/dotnet/release.zip', "build/dist/selenium-dotnet-#{dotnet_version}.zip") - FileUtils.chmod(0o666, "build/dist/selenium-dotnet-#{dotnet_version}.zip") - FileUtils.copy('bazel-bin/dotnet/strongnamed.zip', "build/dist/selenium-dotnet-strongnamed-#{dotnet_version}.zip") - FileUtils.chmod(0o666, "build/dist/selenium-dotnet-strongnamed-#{dotnet_version}.zip") - end - - desc 'Build, package, and push nupkg files to NuGet' - task :release do |_task, arguments| - nightly = arguments.to_a.include?('nightly') - check_credentials(nightly ? %i[dotnet_nightly] : %i[dotnet]) - - if nightly - puts 'Updating .NET version to nightly...' - Rake::Task['dotnet:version'].invoke('nightly') - ENV['NUGET_API_KEY'] = ENV.fetch('GITHUB_TOKEN', nil) - ENV['NUGET_SOURCE'] = 'https://nuget.pkg.github.com/seleniumhq/index.json' - else - ENV['NUGET_SOURCE'] = 'https://api.nuget.org/v3/index.json' - end - - puts 'Building and packaging .NET artifacts...' - Rake::Task['dotnet:package'].invoke('--config=release') - - puts "Pushing .NET packages to #{ENV.fetch('NUGET_SOURCE', nil)}..." - Bazel.execute('run', ['--config=release'], '//dotnet:publish') - end - - desc 'Verify .NET packages are published on NuGet' - task :verify do - verify_package_published("https://api.nuget.org/v3/registration5-semver1/selenium.webdriver/#{dotnet_version}.json") - verify_package_published("https://api.nuget.org/v3/registration5-semver1/selenium.support/#{dotnet_version}.json") - end - - desc 'Generate .NET documentation' - task :docs do |_task, arguments| - if dotnet_version.include?('nightly') && !arguments.to_a.include?('force') - abort('Aborting documentation update: nightly versions should not update docs.') - end - - puts 'Generating .NET documentation' - FileUtils.rm_rf('build/docs/api/dotnet/') - Bazel.execute('run', [], '//dotnet:docs') - end - - desc 'Update .NET changelog' - task :changelog do - header = "v#{dotnet_version}\n======" - update_changelog(dotnet_version, 'dotnet', 'dotnet/src/', 'dotnet/CHANGELOG', header) - end - desc 'Update .NET version' - task :version, [:version] do |_task, arguments| - old_version = dotnet_version - nightly = "-nightly#{Time.now.strftime('%Y%m%d%H%M')}" - new_version = updated_version(old_version, arguments[:version], nightly) - puts "Updating .NET from #{old_version} to #{new_version}" +# Example: `./go prep_release[4.31.0,early-stable]` +# Equivalent to .github/workflows/pre-release.yml in a single command +desc 'Update everything in preparation for a release' +task :prep_release, [:version, :channel] do |_task, arguments| + version = arguments[:version] + raise 'Missing required version: ./go prep_release[4.31.0,early-stable]' if version.nil? || version.empty? - file = 'dotnet/selenium-dotnet-version.bzl' - text = File.read(file).gsub(old_version, new_version) - File.open(file, 'w') { |f| f.puts text } - @git.add(file) - end + Rake::Task['update_browsers'].invoke(arguments[:channel]) + Rake::Task['update_cdp'].invoke(arguments[:channel]) + Rake::Task['update_manager'].invoke + Rake::Task['java:update'].invoke + Rake::Task['authors'].invoke + Rake::Task['all:version'].invoke(version) + Rake::Task['all:changelogs'].invoke end -namespace :java do - desc 'Build Java Client Jars' - task :build do |_task, arguments| - args = arguments.to_a.compact - java_release_targets.each { |target| Bazel.execute('build', args, target) } - end - - desc 'Build Grid Server' - task :grid do |_task, arguments| - args = arguments.to_a.compact - Bazel.execute('build', args, '//java/src/org/openqa/selenium/grid:executable-grid') - end - - desc 'Package Java bindings and grid into releasable packages and stage for release' - task :package do |_task, arguments| - args = arguments.to_a.compact.empty? ? ['--config=release'] : arguments.to_a.compact - Bazel.execute('build', args, '//java/src/org/openqa/selenium:client-zip') - Bazel.execute('build', args, '//java/src/org/openqa/selenium/grid:server-zip') - Bazel.execute('build', args, '//java/src/org/openqa/selenium/grid:executable-grid') - - mkdir_p 'build/dist' - Dir.glob('build/dist/*{java,server}*').each { |file| FileUtils.rm_f(file) } - - FileUtils.copy('bazel-bin/java/src/org/openqa/selenium/grid/server-zip.zip', - "build/dist/selenium-server-#{java_version}.zip") - FileUtils.chmod(0o666, "build/dist/selenium-server-#{java_version}.zip") - FileUtils.copy('bazel-bin/java/src/org/openqa/selenium/client-zip.zip', - "build/dist/selenium-java-#{java_version}.zip") - FileUtils.chmod(0o666, "build/dist/selenium-java-#{java_version}.zip") - FileUtils.copy('bazel-bin/java/src/org/openqa/selenium/grid/selenium', - "build/dist/selenium-server-#{java_version}.jar") - FileUtils.chmod(0o777, "build/dist/selenium-server-#{java_version}.jar") - end - - desc 'Deploy all jars to Maven' - task :release do |_task, arguments| - nightly = arguments.to_a.include?('nightly') - check_credentials(nightly ? %i[java] : %i[java java_gpg]) - - ENV['MAVEN_USER'] ||= ENV.fetch('SEL_M2_USER', nil) - ENV['MAVEN_PASSWORD'] ||= ENV.fetch('SEL_M2_PASS', nil) - read_m2_user_pass unless ENV['MAVEN_PASSWORD'] && ENV['MAVEN_USER'] - repo_domain = 'central.sonatype.com' - repo = nightly ? "#{repo_domain}/repository/maven-snapshots" : "ossrh-staging-api.#{repo_domain}/service/local/staging/deploy/maven2/" - ENV['MAVEN_REPO'] = "https://#{repo}" - ENV['GPG_SIGN'] = (!nightly).to_s - - if nightly - puts 'Updating Java version to nightly...' - Rake::Task['java:version'].invoke('nightly') - end - - puts 'Packaging Java artifacts...' - Rake::Task['java:package'].invoke('--config=release') - Rake::Task['java:build'].invoke('--config=release') - - puts "Releasing Java artifacts to Maven repository at '#{ENV.fetch('MAVEN_REPO', nil)}'" - java_release_targets.each { |target| Bazel.execute('run', ['--config=release'], target) } - - Rake::Task['java:publish'].invoke unless nightly - end - - desc 'Publish to sonatype' - task :publish do - read_m2_user_pass unless ENV['MAVEN_PASSWORD'] && ENV['MAVEN_USER'] - user = ENV.fetch('MAVEN_USER') - pass = ENV.fetch('MAVEN_PASSWORD') - token = Base64.strict_encode64("#{user}:#{pass}") - - puts 'Triggering Sonatype validation...' - uri = URI('https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/org.seleniumhq') - - req = Net::HTTP::Post.new(uri) - req['Authorization'] = "Basic #{token}" - req['Accept'] = '*/*' - req['Content-Length'] = '0' - - begin - res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, - open_timeout: 10, read_timeout: 180) do |http| - http.request(req) - end - rescue Net::ReadTimeout, Net::OpenTimeout => e - warn <<~MSG - Request timed out waiting for deployment ID. - The deployment may still have been created on the server. - Check https://central.sonatype.com/publishing/deployments for pending deployments, - then run: ./go java:publish_deployment - MSG - raise e - end - - if res.is_a?(Net::HTTPSuccess) - deployment_id = res.body.strip - puts "Got deployment ID: #{deployment_id}" - Rake::Task['java:publish_deployment'].invoke(deployment_id) - else - warn "Failed to get deployment ID (HTTP #{res.code}): #{res.body}" - exit(1) - end - end - - desc 'Publish a Sonatype deployment by ID' - task :publish_deployment, [:deployment_id] do |_task, arguments| - deployment_id = arguments[:deployment_id] || ENV.fetch('DEPLOYMENT_ID', nil) - if deployment_id.nil? || deployment_id.empty? - raise 'Deployment ID required: ./go java:publish_deployment[ID] or set DEPLOYMENT_ID' - end - - read_m2_user_pass unless ENV['MAVEN_PASSWORD'] && ENV['MAVEN_USER'] - token = Base64.strict_encode64("#{ENV.fetch('MAVEN_USER')}:#{ENV.fetch('MAVEN_PASSWORD')}") - - encoded_id = URI.encode_www_form_component(deployment_id.strip) - status = {} - max_attempts = 60 - delay = 5 - max_attempts.times do |attempt| - status = sonatype_api_post("https://central.sonatype.com/api/v1/publisher/status?id=#{encoded_id}", token) - state = status['deploymentState'] - puts "Deployment state: #{state}" - - case state - when 'VALIDATED', 'PUBLISHED' then break - when 'FAILED' then raise "Deployment failed: #{status['errors']}" - end - sleep(delay) unless attempt == max_attempts - 1 - rescue StandardError => e - raise if e.message.start_with?('Deployment failed') - - warn "API error (attempt #{attempt + 1}/#{max_attempts}): #{e.message}" - sleep(delay) unless attempt == max_attempts - 1 - end - - state = status['deploymentState'] - next if state == 'PUBLISHED' - - raise "Timed out after #{(max_attempts * delay) / 60} minutes waiting for validation" unless state == 'VALIDATED' +desc 'Run linters for all languages (skip languages with: ./go lint -rb -rust)' +task :lint do |_task, arguments| + failures = [] - expected = java_release_targets.size - actual = status['purls']&.size || 0 - if actual != expected - raise "Expected #{expected} packages but found #{actual}. " \ - 'Drop the deployment at https://central.sonatype.com/publishing/deployments and redeploy.' - end - - puts 'Publishing deployed packages...' - sonatype_api_post("https://central.sonatype.com/api/v1/publisher/deployment/#{encoded_id}", token) - puts "Published! Deployment ID: #{deployment_id}" - end - - desc 'Verify Java packages are published on Maven Central' - task :verify do - verify_package_published("https://repo1.maven.org/maven2/org/seleniumhq/selenium/selenium-java/#{java_version}/selenium-java-#{java_version}.pom") - end - - desc 'Install jars to local m2 directory' - task install: :'maven-install' - - desc 'Generate Java documentation' - task :docs do |_task, arguments| - if java_version.include?('SNAPSHOT') && !arguments.to_a.include?('force') - abort('Aborting documentation update: snapshot versions should not update docs.') - end - - puts 'Generating Java documentation' - Rake::Task['javadocs'].invoke - end - - desc 'Update Maven dependencies' - task :update do - puts 'Updating Maven dependencies' - # Make sure things are in a good state to start with - Rake::Task['java:pin'].invoke - - file_path = 'MODULE.bazel' - content = File.read(file_path) - output = nil - Bazel.execute('run', [], '@maven//:outdated') do |out| - output = out - end - - versions = output.scan(/(\S+) \[\S+ -> (\S+)\]/).to_h - versions.each do |artifact, version| - if artifact.match?('graphql') - # https://github.com/graphql-java/graphql-java/discussions/3187 - puts 'WARNING — Cannot automatically update graphql' - next - end - content.sub!(/#{Regexp.escape(artifact)}:([\d.-]+(?:[-.]?[A-Za-z0-9]+)*)/, "#{artifact}:#{version}") - end - File.write(file_path, content) - - Rake::Task['java:pin'].invoke + begin + Rake::Task['all:lint'].invoke(*arguments.to_a) + rescue StandardError => e + failures << e.message end - desc 'Pin Maven dependencies' - task :pin do - args = ['--action_env=RULES_JVM_EXTERNAL_REPIN=1'] - Bazel.execute('run', args, '@maven//:pin') - %w[MODULE.bazel java/maven_install.json].each { |file| @git.add(file) } + puts 'Linting Bazel files...' + begin + Bazel.execute('run', [], '//:buildifier') + rescue StandardError => e + failures << "buildifier: #{e.message}" end - desc 'Update Java changelog' - task :changelog do - header = "v#{java_version}\n======" - update_changelog(java_version, 'java', 'java/src/org/', 'java/CHANGELOG', header) + puts 'Linting shell scripts and GitHub Actions...' + begin + shellcheck = Bazel.execute('build', [], '@multitool//tools/shellcheck') + Bazel.execute('run', ['--', '-shellcheck', shellcheck], '@multitool//tools/actionlint:cwd') + rescue StandardError => e + failures << "shellcheck/actionlint: #{e.message}" end - desc 'Update Java version' - task :version, [:version] do |_task, arguments| - old_version = java_version - new_version = updated_version(old_version, arguments[:version], '-SNAPSHOT') - puts "Updating Java from #{old_version} to #{new_version}" - - file = 'java/version.bzl' - text = File.read(file).gsub(old_version, new_version) - File.open(file, 'w') { |f| f.puts text } - @git.add(file) + puts 'Updating copyright headers...' + begin + Bazel.execute('run', [], '//scripts:update_copyright') + rescue StandardError => e + failures << "copyright: #{e.message}" end -end -def rust_version - File.foreach('rust/BUILD.bazel') do |line| - return line.split('=').last.strip.tr('",', '') if line.include?('version =') - end + raise "Lint failed:\n#{failures.join("\n")}" unless failures.empty? end -namespace :rust do - desc 'Build Selenium Manager' - task :build do |_task, arguments| - args = arguments.to_a.compact - Bazel.execute('build', args, '//rust:selenium-manager') - end - - desc 'Update the rust lock files' - task :update do - puts 'pinning cargo versions' - ENV['CARGO_BAZEL_REPIN'] = 'true' - Bazel.execute('fetch', [], '@crates//:all') - @git.add('rust/Cargo.Bazel.lock') - @git.add('rust/Cargo.lock') - end - - desc 'Pin Rust dependencies' - task pin: :update - - desc 'Update Rust changelog' - task :changelog do - header = "#{rust_version}\n======" - version = rust_version.split('.').tap(&:shift).join('.') - update_changelog(version, 'rust', 'rust/src', 'rust/CHANGELOG.md', header) - end - # Rust versioning is currently difficult compared to the others because we are using the 0.4.x pattern - # until Selenium Manager comes out of beta - desc 'Update Rust version' - task :version, [:version] do |_task, arguments| - old_version = rust_version.dup - equivalent_version = if old_version.include?('nightly') - "#{old_version.split(/\.|-/)[0...-1].tap(&:shift).join('.')}.0-nightly" - else - old_version.split('.').tap(&:shift).append('0').join('.') - end - updated = updated_version(equivalent_version, arguments[:version], '-nightly') - new_version = updated.split(/\.|-/).tap { |v| v.delete_at(2) }.unshift('0').join('.').gsub('.nightly', '-nightly') - puts "Updating Rust from #{old_version} to #{new_version}" - - ['rust/Cargo.toml', 'rust/BUILD.bazel'].each do |file| - text = File.read(file).gsub(old_version, new_version) - File.open(file, 'w') { |f| f.puts text } - @git.add(file) - end - - Rake::Task['rust:update'].invoke - @git.add('rust/Cargo.Bazel.lock') - @git.add('rust/Cargo.lock') - end +# Legacy aliases - call namespaced tasks +task 'selenium-server-standalone' => 'java:grid' +task 'selenium-java' => 'java:client' +task javadocs: 'java:docs' +task 'java-release-zip': 'java:package' +task 'maven-install': 'java:install' +task 'publish-maven' => 'java:release' +task 'publish-maven-snapshot' do + Rake::Task['java:release'].invoke('nightly') end +task 'release-java' => 'java:release' namespace :all do desc 'Pin dependencies for all languages' @@ -1135,34 +186,21 @@ namespace :all do Rake::Task['rb:pin'].invoke Rake::Task['rust:pin'].invoke Rake::Task['node:pin'].invoke + Rake::Task['dotnet:pin'].invoke end - desc 'Update Chrome DevTools support' - task :update_cdp, [:channel] do |_task, arguments| - chrome_channel = arguments[:channel] || 'stable' - chrome_channel = 'beta' if chrome_channel == 'early-stable' - args = Array(chrome_channel) ? ['--', "--chrome_channel=#{chrome_channel.capitalize}"] : [] - - puts "Updating Chrome DevTools references to include latest from #{chrome_channel} channel" - Bazel.execute('run', args, '//scripts:update_cdp') - - ['common/devtools/', - 'dotnet/src/webdriver/DevTools/', - 'dotnet/src/webdriver/Selenium.WebDriver.csproj', - 'dotnet/test/common/DevTools/', - 'dotnet/test/common/CustomDriverConfigs/', - 'dotnet/selenium-dotnet-version.bzl', - 'java/src/org/openqa/selenium/devtools/', - 'javascript/selenium-webdriver/BUILD.bazel', - 'py/BUILD.bazel', - 'rb/lib/selenium/devtools/', - 'rb/Gemfile.lock', - 'Rakefile'].each { |file| @git.add(file) } + desc 'Update dependencies for all languages' + task :update do + Rake::Task['java:update'].invoke + Rake::Task['rb:update'].invoke + Rake::Task['rust:update'].invoke + Rake::Task['node:update'].invoke + Rake::Task['dotnet:update'].invoke end desc 'Build all API Documentation' task :docs do |_task, arguments| - args = arguments.to_a.compact + args = arguments.to_a Rake::Task['java:docs'].invoke(*args) Rake::Task['py:docs'].invoke(*args) Rake::Task['rb:docs'].invoke(*args) @@ -1172,33 +210,29 @@ namespace :all do desc 'Build all artifacts for all language bindings' task :build do |_task, arguments| - args = arguments.to_a.compact - Rake::Task['java:build'].invoke(*args) - Rake::Task['py:build'].invoke(*args) - Rake::Task['rb:build'].invoke(*args) - Rake::Task['dotnet:build'].invoke(*args) - Rake::Task['node:build'].invoke(*args) + Rake::Task['java:build'].invoke(*arguments.to_a) + Rake::Task['py:build'].invoke(*arguments.to_a) + Rake::Task['rb:build'].invoke(*arguments.to_a) + Rake::Task['dotnet:build'].invoke(*arguments.to_a) + Rake::Task['node:build'].invoke(*arguments.to_a) end desc 'Package or build stamped artifacts for distribution in GitHub Release assets' task :package do |_task, arguments| - args = arguments.to_a.compact - Rake::Task['java:package'].invoke(*args) - Rake::Task['dotnet:package'].invoke(*args) + Rake::Task['java:package'].invoke(*arguments.to_a) + Rake::Task['dotnet:package'].invoke(*arguments.to_a) end - desc 'Validate release credentials for all languages without releasing' + desc 'Validate release credentials for all languages' task :check_credentials do |_task, arguments| - nightly = arguments.to_a.include?('nightly') - - if nightly - check_credentials(%i[java dotnet_nightly]) - else - check_credentials(%i[java java_gpg dotnet]) - setup_pypirc - setup_gem_credentials - setup_npm_auth + args = arguments.to_a + failures = [] + %w[java py rb dotnet node].each do |lang| + Rake::Task["#{lang}:check_credentials"].invoke(*args) + rescue StandardError => e + failures << "#{lang}: #{e.message}" end + raise "Credential check failed:\n#{failures.join("\n")}" unless failures.empty? end desc 'Verify all packages are published to their registries' @@ -1214,8 +248,6 @@ namespace :all do desc 'Release all artifacts for all language bindings' task :release do |_task, arguments| - Rake::Task['clean'].invoke - args = arguments.to_a.include?('nightly') ? ['nightly'] : [] Rake::Task['java:release'].invoke(*args) Rake::Task['py:release'].invoke(*args) @@ -1224,38 +256,22 @@ namespace :all do Rake::Task['node:release'].invoke(*args) end - task :lint do - before_diff = `git diff` - - ext = /mswin|msys|mingw|cygwin|bccwin|wince|emc/.match?(RbConfig::CONFIG['host_os']) ? 'ps1' : 'sh' - sh "./scripts/format.#{ext}", verbose: true + desc 'Run linters for all languages (skip with: ./go all:lint -rb -rust)' + task :lint do |_task, arguments| + all_langs = %w[java py rb node rust] + skip = arguments.to_a.select { |a| a.start_with?('-') }.map { |a| a.delete_prefix('-') } + invalid = skip - all_langs + raise "Unknown languages: #{invalid.join(', ')}. Valid: #{all_langs.join(', ')}" if invalid.any? - after_diff = `git diff` - if before_diff != after_diff - changed_files = `git diff --name-only`.strip - raise "Formatting updated files:\n#{changed_files}\nPlease review, stage, and commit the changes." + langs = all_langs - skip + failures = [] + langs.each do |lang| + puts "Linting #{lang}..." + Rake::Task["#{lang}:lint"].invoke + rescue StandardError => e + failures << "#{lang}: #{e.message}" end - - Bazel.execute('run', [], '//py:mypy') - Bazel.execute('run', [], '//py:ruff') - Bazel.execute('run', [], '//rb:steep') - shellcheck = Bazel.execute('build', [], '@multitool//tools/shellcheck') - Bazel.execute('run', ['--', '-shellcheck', shellcheck], '@multitool//tools/actionlint:cwd') - end - - # Example: `./go all:prepare[4.31.0,early-stable]` - # Equivalent to .github/workflows/pre-release.yml in a single command - desc 'Update everything in preparation for a release' - task :prepare, [:version, :channel] do |_task, arguments| - version = arguments[:version] - - Rake::Task['update_browsers'].invoke(arguments[:channel]) - Rake::Task['all:update_cdp'].invoke(arguments[:channel]) - Rake::Task['update_manager'].invoke - Rake::Task['java:update'].invoke - Rake::Task['authors'].invoke - Rake::Task['all:version'].invoke(version) - Rake::Task['all:changelogs'].invoke + raise "Lint failed:\n#{failures.join("\n")}" unless failures.empty? end desc 'Update all versions' @@ -1277,134 +293,18 @@ namespace :all do text = File.read(file).gsub(old_version_pattern, "The latest released version of Selenium is #{major_minor}") File.write(file, text) - @git.add(file) + SeleniumRake.git.add(file) end end desc 'Update all changelogs' task :changelogs do |_task, _arguments| puts 'Updating all changelogs' - Rake::Task['java:changelog'].invoke - Rake::Task['rb:changelog'].invoke - Rake::Task['node:changelog'].invoke - Rake::Task['py:changelog'].invoke - Rake::Task['dotnet:changelog'].invoke - Rake::Task['rust:changelog'].invoke - end -end - -def updated_version(current, desired = nil, nightly = nil) - if !desired.nil? && desired != 'nightly' - # If desired is present, return full 3 digit version - desired.split('.').tap { |v| v << 0 while v.size < 3 }.join('.') - elsif current.split(/\.|-/).size > 3 - # if current version is already nightly, just need to bump it; this will be noop for some languages - pattern = /-?\.?(nightly|SNAPSHOT|dev|\d{12})\d*$/ - current.gsub(pattern, nightly) - elsif current.split(/\.|-/).size == 3 - # if current version is not nightly, need to bump the version and make nightly - "#{current.split(/\.|-/).tap { |i| (i[1] = i[1].to_i + 1) && (i[2] = 0) }.join('.')}#{nightly}" - end -end - -def previous_tag(current_version, language = nil) - version = current_version.split(/\.|-/) - if version.size > 3 - puts 'WARNING - Changelogs not updated when set to prerelease' - elsif version[2].to_i > 1 - # specified as patch release - patch_version = (version[2].to_i - 1).to_s - "selenium-#{[[version[0]], version[1], patch_version].join('.')}-#{language}" - elsif version[2] == '1' - # specified as patch release; special case - "selenium-#{[[version[0]], version[1], '0'].join('.')}" - else - minor_version = (version[1].to_i - 1) - tags = @git.tags.map(&:name) - tag = language ? tags.reverse.find { |t| t.match?(/selenium-4\.#{minor_version}.*-#{language}/) } : nil - tag || "selenium-#{[[version[0]], minor_version, '0'].join('.')}" - end -end - -def update_changelog(version, language, path, changelog, header) - tag = previous_tag(version, language) - bullet = language == 'javascript' ? '-' : '*' - skip_patterns = /^(bump|update.*version|Bumping to nightly)/i - tags_to_remove = /\[(dotnet|rb|py|java|js|rust)\]:?\s?/ - - command = "git log #{tag}...HEAD --pretty=format:'%s' --reverse -- #{path}" - log = `#{command}` - - entries = log.lines - .map(&:strip) - .grep(/\(#\d+\)/) - .grep_v(skip_patterns) - .map { |line| line.gsub(tags_to_remove, '') } - .map { |line| "#{bullet} #{line}" } - .join("\n") - - content = File.read(changelog) - File.write(changelog, "#{header}\n#{entries}\n\n#{content}") - @git.add(changelog) -end - -BINDING_TARGETS = { - 'java' => '//java/...', - 'py' => '//py/...', - 'rb' => '//rb/...', - 'dotnet' => '//dotnet/...', - 'javascript' => '//javascript/selenium-webdriver/...' -}.freeze - -namespace :bazel do - # ./go bazel:build_test_index --> 'build/bazel-test-target-index.json' - # ./go bazel:build_test_index index.json --> 'index.json' - desc 'Build test target index for faster affected target lookup' - task :build_test_index, [:index_file] do |_task, args| - output = args[:index_file] || 'build/bazel-test-target-index.json' - - index = {} - tests = [] - - exclude_tags = %w[manual spotbugs ie] - all_bindings = BINDING_TARGETS.values.join(' + ') - tag_exclusions = exclude_tags.map { |tag| "except attr(tags, #{tag}, #{all_bindings})" }.join(' ') - kind = '_test' # do not match test_suite or pytest_runner - - puts "Finding all test targets for #{all_bindings}, excluding: #{exclude_tags}" - Bazel.execute('query', ['--output=label'], "kind(#{kind}, #{all_bindings}) #{tag_exclusions}") do |out| - tests = out.lines.map(&:strip).select { |l| l.start_with?('//') } - end - puts "Found #{tests.size} tests" - - tests.each_with_index do |test, i| - puts "Processing #{i + 1}/#{tests.size}: #{test}" if (i % 100).zero? - - deps = [] - Bazel.execute('query', ['--output=label'], "deps(#{test})") do |out| - deps = out.lines.map(&:strip).select { |l| l.start_with?('//', '@selenium//') } - end - - deps.each do |dep| - pkg = bazel_label_to_package(dep) - next if pkg.nil? || pkg.empty? - - index[pkg] ||= [] - index[pkg] << test unless index[pkg].include?(test) - end - end - - sorted_index = index.keys.sort.each_with_object({}) { |k, h| h[k] = index[k].sort } - File.write(output, JSON.pretty_generate(sorted_index)) - puts "Wrote #{sorted_index.size} packages to #{output}" + Rake::Task['java:changelogs'].invoke + Rake::Task['rb:changelogs'].invoke + Rake::Task['node:changelogs'].invoke + Rake::Task['py:changelogs'].invoke + Rake::Task['dotnet:changelogs'].invoke + Rake::Task['rust:changelogs'].invoke end end - -def bazel_label_to_package(label) - # Skip external deps (but allow @selenium// which is internal) - return nil if label.start_with?('@') && !label.start_with?('@selenium//') - - # Normalize @selenium//foo to foo, //foo to foo - label = label.sub(%r{^@selenium//}, '').sub(%r{^//}, '') - label.split(':').first -end diff --git a/rake_tasks/appium.rake b/rake_tasks/appium.rake new file mode 100644 index 0000000000000..b7624df96acb3 --- /dev/null +++ b/rake_tasks/appium.rake @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +desc 'Build iOS driver atoms for Appium' +task build: [ + '//javascript/atoms/fragments:get_visible_text:ios', + '//javascript/atoms/fragments:click:ios', + '//javascript/atoms/fragments:back:ios', + '//javascript/atoms/fragments:forward:ios', + '//javascript/atoms/fragments:submit:ios', + '//javascript/atoms/fragments:xpath:ios', + '//javascript/atoms/fragments:xpaths:ios', + '//javascript/atoms/fragments:type:ios', + '//javascript/atoms/fragments:get_attribute:ios', + '//javascript/atoms/fragments:clear:ios', + '//javascript/atoms/fragments:is_selected:ios', + '//javascript/atoms/fragments:is_enabled:ios', + '//javascript/atoms/fragments:is_shown:ios', + '//javascript/atoms/fragments:stringify:ios', + '//javascript/atoms/fragments:link_text:ios', + '//javascript/atoms/fragments:link_texts:ios', + '//javascript/atoms/fragments:partial_link_text:ios', + '//javascript/atoms/fragments:partial_link_texts:ios', + '//javascript/atoms/fragments:get_interactable_size:ios', + '//javascript/atoms/fragments:scroll_into_view:ios', + '//javascript/atoms/fragments:get_effective_style:ios', + '//javascript/atoms/fragments:get_element_size:ios', + '//javascript/webdriver/atoms/fragments:get_location_in_view:ios' +] diff --git a/rake_tasks/bazel.rake b/rake_tasks/bazel.rake new file mode 100644 index 0000000000000..6844ee856eb8e --- /dev/null +++ b/rake_tasks/bazel.rake @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'json' + +# ./go bazel:build_test_index --> 'build/bazel-test-target-index.json' +# ./go bazel:build_test_index index.json --> 'index.json' +desc 'Build test target index for faster affected target lookup' +task :build_test_index, [:index_file] do |_task, args| + output = args[:index_file] || 'build/bazel-test-target-index.json' + + index = {} + tests = [] + + exclude_tags = %w[manual spotbugs ie] + all_bindings = BINDING_TARGETS.values.join(' + ') + tag_exclusions = exclude_tags.map { |tag| "except attr('tags', '#{tag}', #{all_bindings})" }.join(' ') + kind = '_test' # do not match test_suite or pytest_runner + + puts "Finding all test targets for #{all_bindings}, excluding: #{exclude_tags}" + Bazel.execute('query', ['--output=label'], "kind(#{kind}, #{all_bindings}) #{tag_exclusions}") do |out| + tests = out.lines.map(&:strip).select { |l| l.start_with?('//') } + end + puts "Found #{tests.size} tests" + + tests.each_with_index do |test, i| + puts "Processing #{i + 1}/#{tests.size}: #{test}" if (i % 100).zero? + + deps = [] + Bazel.execute('query', ['--output=label'], "deps(#{test})") do |out| + deps = out.lines.map(&:strip).select { |l| l.start_with?('//', '@selenium//') } + end + + deps.each do |dep| + pkg = bazel_label_to_package(dep) + next if pkg.nil? || pkg.empty? + + index[pkg] ||= [] + index[pkg] << test unless index[pkg].include?(test) + end + end + + sorted_index = index.keys.sort.each_with_object({}) { |k, h| h[k] = index[k].sort } + FileUtils.mkdir_p(File.dirname(output)) + File.write(output, JSON.pretty_generate(sorted_index)) + puts "Wrote #{sorted_index.size} packages to #{output}" +end + +def bazel_label_to_package(label) + # Skip external deps (but allow @selenium// which is internal) + return nil if label.start_with?('@') && !label.start_with?('@selenium//') + + # Normalize @selenium//foo to foo, //foo to foo + label = label.sub(%r{^@selenium//}, '').sub(%r{^//}, '') + label.split(':').first +end diff --git a/rake_tasks/common.rb b/rake_tasks/common.rb new file mode 100644 index 0000000000000..cf25b2f5b7cd1 --- /dev/null +++ b/rake_tasks/common.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'git' +require 'net/http' +require 'fileutils' + +BINDING_TARGETS = { + 'java' => '//java/...', + 'py' => '//py/...', + 'rb' => '//rb/...', + 'dotnet' => '//dotnet/...', + 'javascript' => '//javascript/selenium-webdriver/...' +}.freeze + +# Shared utilities used by language-specific rake tasks +module SeleniumRake + class << self + attr_accessor :git + end + + def self.updated_version(current, desired = nil, nightly = nil) + if !desired.nil? && desired != 'nightly' + # If desired is present, return full 3 digit version + desired.split('.').tap { |v| v << 0 while v.size < 3 }.join('.') + elsif current.split(/\.|-/).size > 3 + # if current version is already nightly, just need to bump it; this will be noop for some languages + pattern = /-?\.?(nightly|SNAPSHOT|dev|\d{12})\d*$/ + current.gsub(pattern, nightly) + elsif current.split(/\.|-/).size == 3 + # if current version is not nightly, need to bump the version and make nightly + "#{current.split(/\.|-/).tap { |i| (i[1] = i[1].to_i + 1) && (i[2] = 0) }.join('.')}#{nightly}" + end + end + + def self.previous_tag(current_version, language = nil) + version = current_version.split(/\.|-/) + if version.size > 3 + puts 'WARNING - Changelogs not updated when set to prerelease' + elsif version[2].to_i > 1 + # specified as patch release + patch_version = (version[2].to_i - 1).to_s + "selenium-#{[[version[0]], version[1], patch_version].join('.')}-#{language}" + elsif version[2] == '1' + # specified as patch release; special case + "selenium-#{[[version[0]], version[1], '0'].join('.')}" + else + minor_version = (version[1].to_i - 1) + tags = git.tags.map(&:name) + tag = language ? tags.reverse.find { |t| t.match?(/selenium-4\.#{minor_version}.*-#{language}/) } : nil + tag || "selenium-#{[[version[0]], minor_version, '0'].join('.')}" + end + end + + def self.update_changelog(version, language, path, changelog, header) + tag = previous_tag(version, language) + bullet = language == 'javascript' ? '-' : '*' + skip_patterns = /^(bump|update.*version|Bumping to nightly)/i + tags_to_remove = /\[(dotnet|rb|py|java|js|rust)\]:?\s?/ + + command = "git log #{tag}...HEAD --pretty=format:'%s' --reverse -- #{path}" + log = `#{command}` + + entries = log.lines + .map(&:strip) + .grep(/\(#\d+\)/) + .grep_v(skip_patterns) + .map { |line| line.gsub(tags_to_remove, '') } + .map { |line| "#{bullet} #{line}" } + .join("\n") + + content = File.read(changelog) + File.write(changelog, "#{header}\n#{entries}\n\n#{content}") + git.add(changelog) + end + + def self.verify_package_published(url) + puts "Verifying #{url}..." + uri = URI(url) + res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') { |http| http.request(Net::HTTP::Get.new(uri)) } + raise "Package not published: #{url}" unless res.is_a?(Net::HTTPSuccess) + + puts 'Verified!' + end +end diff --git a/rake_tasks/dotnet.rake b/rake_tasks/dotnet.rake new file mode 100644 index 0000000000000..2c0b8d01777f4 --- /dev/null +++ b/rake_tasks/dotnet.rake @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +def dotnet_version + File.foreach('dotnet/selenium-dotnet-version.bzl') do |line| + return line.split('=').last.strip.tr('"', '') if line.include?('SE_VERSION') + end +end + +desc 'Build nupkg files' +task :build do |_task, arguments| + Bazel.execute('build', arguments.to_a, '//dotnet:all') +end + +desc 'Package .NET bindings into zipped assets and stage for release' +task :package do |_task, arguments| + args = arguments.to_a.empty? ? ['--stamp'] : arguments.to_a + Rake::Task['dotnet:build'].invoke(*args) + mkdir_p 'build/dist' + FileUtils.rm_f(Dir.glob('build/dist/*dotnet*')) + + FileUtils.copy('bazel-bin/dotnet/release.zip', "build/dist/selenium-dotnet-#{dotnet_version}.zip") + FileUtils.chmod(0o644, "build/dist/selenium-dotnet-#{dotnet_version}.zip") + FileUtils.copy('bazel-bin/dotnet/strongnamed.zip', "build/dist/selenium-dotnet-strongnamed-#{dotnet_version}.zip") + FileUtils.chmod(0o644, "build/dist/selenium-dotnet-strongnamed-#{dotnet_version}.zip") +end + +desc 'Validate .NET release credentials' +task :check_credentials do |_task, arguments| + nightly = arguments.to_a.include?('nightly') + if nightly && (ENV['GITHUB_TOKEN'].nil? || ENV['GITHUB_TOKEN'].empty?) + raise 'Missing GitHub token: set GITHUB_TOKEN for nightly releases' + elsif !nightly && (ENV['NUGET_API_KEY'].nil? || ENV['NUGET_API_KEY'].empty?) + raise 'Missing NuGet API key: set NUGET_API_KEY' + end +end + +desc 'Build, package, and push nupkg files to NuGet' +task :release do |_task, arguments| + nightly = arguments.to_a.include?('nightly') + Rake::Task['dotnet:check_credentials'].invoke(*arguments.to_a) + + if nightly + puts 'Updating .NET version to nightly...' + Rake::Task['dotnet:version'].invoke('nightly') + ENV['NUGET_API_KEY'] = ENV.fetch('GITHUB_TOKEN', nil) + ENV['NUGET_SOURCE'] = 'https://nuget.pkg.github.com/seleniumhq/index.json' + else + ENV['NUGET_SOURCE'] = 'https://api.nuget.org/v3/index.json' + end + + puts 'Building and packaging .NET artifacts...' + Rake::Task['dotnet:package'].invoke('--config=release') + + puts "Pushing .NET packages to #{ENV.fetch('NUGET_SOURCE', nil)}..." + Bazel.execute('run', ['--config=release'], '//dotnet:publish') +end + +desc 'Verify .NET packages are published on NuGet' +task :verify do + SeleniumRake.verify_package_published("https://api.nuget.org/v3/registration5-semver1/selenium.webdriver/#{dotnet_version}.json") + SeleniumRake.verify_package_published("https://api.nuget.org/v3/registration5-semver1/selenium.support/#{dotnet_version}.json") +end + +desc 'Generate .NET documentation' +task :docs do |_task, arguments| + if dotnet_version.include?('nightly') && !arguments.to_a.include?('force') + abort('Aborting documentation update: nightly versions should not update docs.') + end + + puts 'Generating .NET documentation' + FileUtils.rm_rf('build/docs/api/dotnet/') + Bazel.execute('run', [], '//dotnet:docs') +end + +desc 'Install .NET packages to local NuGet cache' +task :install do + Bazel.execute('build', [], '//dotnet/src/webdriver:webdriver-pack') + Bazel.execute('build', [], '//dotnet/src/support:support-pack') + Dir.glob('bazel-bin/dotnet/src/**/*.nupkg').each do |nupkg| + sh 'dotnet', 'nuget', 'push', nupkg, '--source', "#{Dir.home}/.nuget/packages" + end +end + +desc 'Update .NET changelog' +task :changelogs do + header = "v#{dotnet_version}\n======" + SeleniumRake.update_changelog(dotnet_version, 'dotnet', 'dotnet/src/', 'dotnet/CHANGELOG', header) +end + +desc 'Update .NET version' +task :version, [:version] do |_task, arguments| + old_version = dotnet_version + nightly = "-nightly#{Time.now.strftime('%Y%m%d%H%M')}" + new_version = SeleniumRake.updated_version(old_version, arguments[:version], nightly) + puts "Updating .NET from #{old_version} to #{new_version}" + + file = 'dotnet/selenium-dotnet-version.bzl' + text = File.read(file).gsub(old_version, new_version) + File.open(file, 'w') { |f| f.puts text } + SeleniumRake.git.add(file) +end + +desc 'Update .NET dependencies to latest versions' +task :update do + Bazel.execute('run', [], '//dotnet:paket-update') + Rake::Task['dotnet:pin'].invoke +end + +desc 'Pin .NET dependencies (sync lockfile)' +task :pin do + Bazel.execute('run', [], '//dotnet:paket-install') + Bazel.execute('run', ['--', '--dependencies-file', "#{Dir.pwd}/dotnet/paket.dependencies", + '--output-folder', "#{Dir.pwd}/dotnet"], + '@rules_dotnet//tools/paket2bazel:paket2bazel') + %w[dotnet/paket.lock dotnet/paket.nuget.bzl].each { |f| SeleniumRake.git.add(f) } +end diff --git a/rake_tasks/grid.rake b/rake_tasks/grid.rake new file mode 100644 index 0000000000000..eb451fef4052f --- /dev/null +++ b/rake_tasks/grid.rake @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +desc 'Build Grid Server' +task :build do |_task, arguments| + Bazel.execute('build', arguments.to_a, '//java/src/org/openqa/selenium/grid:executable-grid') +end + +desc 'Package Grid server into releasable artifacts' +task :package do |_task, arguments| + args = arguments.to_a.empty? ? ['--config=release'] : arguments.to_a + Bazel.execute('build', args, '//java/src/org/openqa/selenium/grid:server-zip') + Bazel.execute('build', args, '//java/src/org/openqa/selenium/grid:executable-grid') + + mkdir_p 'build/dist' + Dir.glob('build/dist/*server*').each { |file| FileUtils.rm_f(file) } + + FileUtils.copy('bazel-bin/java/src/org/openqa/selenium/grid/server-zip.zip', + "build/dist/selenium-server-#{java_version}.zip") + FileUtils.chmod(0o644, "build/dist/selenium-server-#{java_version}.zip") + FileUtils.copy('bazel-bin/java/src/org/openqa/selenium/grid/selenium', + "build/dist/selenium-server-#{java_version}.jar") + FileUtils.chmod(0o755, "build/dist/selenium-server-#{java_version}.jar") +end + +desc 'Package Grid for nightly release' +task :release do |_task, _arguments| + # Grid doesn't publish to a registry, just packages artifacts for GitHub release + Rake::Task['grid:package'].invoke('--config=release') +end diff --git a/rake_tasks/java.rake b/rake_tasks/java.rake new file mode 100644 index 0000000000000..7cbe86906d201 --- /dev/null +++ b/rake_tasks/java.rake @@ -0,0 +1,396 @@ +# frozen_string_literal: true + +require 'base64' +require 'json' +require 'net/http' + +# use #java_release_targets to access this list +JAVA_RELEASE_TARGETS = %w[ + //java/src/org/openqa/selenium/chrome:chrome.publish + //java/src/org/openqa/selenium/chromium:chromium.publish + //java/src/org/openqa/selenium/devtools/v143:v143.publish + //java/src/org/openqa/selenium/devtools/v144:v144.publish + //java/src/org/openqa/selenium/devtools/v142:v142.publish + //java/src/org/openqa/selenium/edge:edge.publish + //java/src/org/openqa/selenium/firefox:firefox.publish + //java/src/org/openqa/selenium/grid/sessionmap/jdbc:jdbc.publish + //java/src/org/openqa/selenium/grid/sessionmap/redis:redis.publish + //java/src/org/openqa/selenium/grid:bom-dependencies.publish + //java/src/org/openqa/selenium/grid:bom.publish + //java/src/org/openqa/selenium/grid:grid.publish + //java/src/org/openqa/selenium/ie:ie.publish + //java/src/org/openqa/selenium/json:json.publish + //java/src/org/openqa/selenium/manager:manager.publish + //java/src/org/openqa/selenium/os:os.publish + //java/src/org/openqa/selenium/remote/http:http.publish + //java/src/org/openqa/selenium/remote:remote.publish + //java/src/org/openqa/selenium/safari:safari.publish + //java/src/org/openqa/selenium/support:support.publish + //java/src/org/openqa/selenium:client-combined.publish + //java/src/org/openqa/selenium:core.publish +].freeze + +def java_version + File.foreach('java/version.bzl') do |line| + return line.split('=').last.strip.tr('"', '') if line.include?('SE_VERSION') + end +end + +def java_release_targets + unless @targets_verified + verify_java_release_targets + @targets_verified = true + end + + JAVA_RELEASE_TARGETS +end + +def verify_java_release_targets + query = 'kind(maven_publish, set(//java/... //third_party/...))' + current_targets = [] + + Bazel.execute('query', [], query) do |output| + current_targets = output.lines.map(&:strip).reject(&:empty?).select { |line| line.start_with?('//') } + end + + missing_targets = JAVA_RELEASE_TARGETS - current_targets + extra_targets = current_targets - JAVA_RELEASE_TARGETS + + return if missing_targets.empty? && extra_targets.empty? + + error_message = 'Java release targets are out of sync with Bazel query results.' + + unless missing_targets.empty? + error_message += "\nObsolete targets (in list but not in Bazel): #{missing_targets.join(', ')}" + end + + unless extra_targets.empty? + error_message += "\nMissing targets (in Bazel but not in list): #{extra_targets.join(', ')}" + end + + raise error_message +end + +def sonatype_api_post(url, token) + uri = URI(url) + req = Net::HTTP::Post.new(uri) + req['Authorization'] = "Basic #{token}" + + res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) } + raise "Sonatype API error (#{res.code}): #{res.body}" unless res.is_a?(Net::HTTPSuccess) + + res.body.to_s.empty? ? {} : JSON.parse(res.body) +end + +def read_m2_user_pass + settings_path = File.join(Dir.home, '.m2', 'settings.xml') + unless File.exist?(settings_path) + warn "Maven settings file not found at #{settings_path}" + return + end + + puts 'Maven environment variables not set, inspecting ~/.m2/settings.xml.' + settings = File.read(settings_path) + found_section = false + settings.each_line do |line| + if !found_section + found_section = line.include? 'central' + elsif line.include?('') + ENV['MAVEN_USER'] = line[%r{(.*?)}, 1] + elsif line.include?('') + ENV['MAVEN_PASSWORD'] = line[%r{(.*?)}, 1] + end + break if ENV['MAVEN_PASSWORD'] && ENV['MAVEN_USER'] + end +end + +def sonatype_auth_token + read_m2_user_pass unless ENV['MAVEN_PASSWORD'] && ENV['MAVEN_USER'] + Base64.strict_encode64("#{ENV.fetch('MAVEN_USER')}:#{ENV.fetch('MAVEN_PASSWORD')}") +end + +def trigger_sonatype_validation(token) + puts 'Triggering Sonatype validation...' + uri = URI('https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/org.seleniumhq') + + req = Net::HTTP::Post.new(uri) + req['Authorization'] = "Basic #{token}" + req['Accept'] = '*/*' + req['Content-Length'] = '0' + + begin + res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, + open_timeout: 10, read_timeout: 180) do |http| + http.request(req) + end + rescue Net::ReadTimeout, Net::OpenTimeout => e + warn <<~MSG + Request timed out waiting for deployment ID. + The deployment may still have been created on the server. + Check https://central.sonatype.com/publishing/deployments for pending deployments, + then run: ./go java:release + MSG + raise e + end + + unless res.is_a?(Net::HTTPSuccess) + warn "Failed to get deployment ID (HTTP #{res.code}): #{res.body}" + exit(1) + end + + res.body.strip +end + +def poll_and_publish_deployment(deployment_id, token) + encoded_id = URI.encode_www_form_component(deployment_id.strip) + status = {} + max_attempts = 60 + delay = 5 + + max_attempts.times do |attempt| + status = sonatype_api_post("https://central.sonatype.com/api/v1/publisher/status?id=#{encoded_id}", token) + state = status['deploymentState'] + puts "Deployment state: #{state}" + + case state + when 'VALIDATED', 'PUBLISHED' then break + when 'FAILED' then raise "Deployment failed: #{status['errors']}" + end + sleep(delay) unless attempt == max_attempts - 1 + rescue StandardError => e + raise if e.message.start_with?('Deployment failed') + + warn "API error (attempt #{attempt + 1}/#{max_attempts}): #{e.message}" + sleep(delay) unless attempt == max_attempts - 1 + end + + state = status['deploymentState'] + return if state == 'PUBLISHED' + + raise "Timed out after #{(max_attempts * delay) / 60} minutes waiting for validation" unless state == 'VALIDATED' + + expected = java_release_targets.size + actual = status['purls']&.size || 0 + if actual != expected + raise "Expected #{expected} packages but found #{actual}. " \ + 'Drop the deployment at https://central.sonatype.com/publishing/deployments and redeploy.' + end + + puts 'Publishing deployed packages...' + sonatype_api_post("https://central.sonatype.com/api/v1/publisher/deployment/#{encoded_id}", token) + puts "Published! Deployment ID: #{deployment_id}" +end + +desc 'Build Java Client Jars' +task :build do |_task, arguments| + java_release_targets.each { |target| Bazel.execute('build', arguments.to_a, target) } +end + +desc 'Build the selenium client jars' +task :client do |_task, arguments| + Bazel.execute('build', arguments.to_a, '//java/src/org/openqa/selenium:client-combined') +end + +desc 'Build Grid Server' +task :grid do |_task, arguments| + Bazel.execute('build', arguments.to_a, '//java/src/org/openqa/selenium/grid:executable-grid') +end + +desc 'Package Java bindings and grid into releasable packages and stage for release' +task :package do |_task, arguments| + args = arguments.to_a.empty? ? ['--config=release'] : arguments.to_a + Bazel.execute('build', args, '//java/src/org/openqa/selenium:client-zip') + Bazel.execute('build', args, '//java/src/org/openqa/selenium/grid:server-zip') + Bazel.execute('build', args, '//java/src/org/openqa/selenium/grid:executable-grid') + + mkdir_p 'build/dist' + Dir.glob('build/dist/*{java,server}*').each { |file| FileUtils.rm_f(file) } + + FileUtils.copy('bazel-bin/java/src/org/openqa/selenium/grid/server-zip.zip', + "build/dist/selenium-server-#{java_version}.zip") + FileUtils.chmod(0o644, "build/dist/selenium-server-#{java_version}.zip") + FileUtils.copy('bazel-bin/java/src/org/openqa/selenium/client-zip.zip', + "build/dist/selenium-java-#{java_version}.zip") + FileUtils.chmod(0o644, "build/dist/selenium-java-#{java_version}.zip") + FileUtils.copy('bazel-bin/java/src/org/openqa/selenium/grid/selenium', + "build/dist/selenium-server-#{java_version}.jar") + FileUtils.chmod(0o755, "build/dist/selenium-server-#{java_version}.jar") +end + +desc 'Validate Java release credentials' +task :check_credentials do |_task, arguments| + nightly = arguments.to_a.include?('nightly') + + has_env = (ENV['MAVEN_USER'] || ENV.fetch('SEL_M2_USER', + nil)) && (ENV['MAVEN_PASSWORD'] || ENV.fetch('SEL_M2_PASS', nil)) + settings = File.join(Dir.home, '.m2', 'settings.xml') + has_file = File.exist?(settings) && File.read(settings).include?('central') + unless has_env || has_file + raise 'Missing Maven credentials: set MAVEN_USER/MAVEN_PASSWORD or configure ~/.m2/settings.xml' + end + + next if nightly + + has_gpg = system('which gpg >/dev/null 2>&1') || system('where gpg >NUL 2>&1') + raise 'Missing GPG: gpg command not found (required for signing releases)' unless has_gpg +end + +desc 'Deploy all jars to Maven (pass deployment_id to retry a failed publish)' +task :release do |_task, arguments| + args = arguments.to_a + nightly = args.delete('nightly') + deployment_id = args.first + + Rake::Task['java:check_credentials'].invoke(*(nightly ? ['nightly'] : [])) + + ENV['MAVEN_USER'] ||= ENV.fetch('SEL_M2_USER', nil) + ENV['MAVEN_PASSWORD'] ||= ENV.fetch('SEL_M2_PASS', nil) + token = sonatype_auth_token + + # Retry mode: just poll and publish an existing deployment + if deployment_id + puts "Retrying deployment: #{deployment_id}" + poll_and_publish_deployment(deployment_id, token) + return + end + + repo_domain = 'central.sonatype.com' + repo = if nightly + "#{repo_domain}/repository/maven-snapshots" + else + "ossrh-staging-api.#{repo_domain}/service/local/staging/deploy/maven2/" + end + ENV['MAVEN_REPO'] = "https://#{repo}" + ENV['GPG_SIGN'] = (!nightly).to_s + + if nightly + puts 'Updating Java version to nightly...' + Rake::Task['java:version'].invoke('nightly') + end + + puts 'Packaging Java artifacts...' + Rake::Task['java:package'].invoke('--config=release') + Rake::Task['java:build'].invoke('--config=release') + + puts "Releasing Java artifacts to Maven repository at '#{ENV.fetch('MAVEN_REPO', nil)}'" + java_release_targets.each { |target| Bazel.execute('run', ['--config=release'], target) } + + return if nightly + + deployment_id = trigger_sonatype_validation(token) + puts "Got deployment ID: #{deployment_id}" + poll_and_publish_deployment(deployment_id, token) +end + +desc 'Verify Java packages are published on Maven Central' +task :verify do + SeleniumRake.verify_package_published("https://repo1.maven.org/maven2/org/seleniumhq/selenium/selenium-java/#{java_version}/selenium-java-#{java_version}.pom") +end + +desc 'Install jars to local m2 directory' +task :install do + java_release_targets.each do |p| + Bazel.execute('run', + ['--stamp', + '--define', + "maven_repo=file://#{Dir.home}/.m2/repository", + '--define', + 'gpg_sign=false'], + p) + end +end + +desc 'Generate Java documentation' +task docs: %i[//java/src/org/openqa/selenium/grid:all-javadocs] do |_task, arguments| + if java_version.include?('SNAPSHOT') && !arguments.to_a.include?('force') + abort('Aborting documentation update: snapshot versions should not update docs.') + end + + puts 'Generating Java documentation' + FileUtils.rm_rf('build/docs/api/java') + FileUtils.mkdir_p('build/docs/api/java') + out = 'bazel-bin/java/src/org/openqa/selenium/grid/all-javadocs.jar' + + cmd = %(cd build/docs/api/java && jar xf "../../../../#{out}" 2>&1) + cmd = cmd.tr('/', '\\').tr(':', ';') if /mswin|msys|mingw32/.match?(RbConfig::CONFIG['host_os']) + raise 'could not unpack javadocs' unless system(cmd) + + File.open('build/docs/api/java/stylesheet.css', 'a') do |file| + file.write(<<~STYLE + /* Custom selenium-specific styling */ + .blink { + animation: 2s cubic-bezier(0.5, 0, 0.85, 0.85) infinite blink; + } + + @keyframes blink { + 50% { + opacity: 0; + } + } + + STYLE + ) + end +end + +desc 'Update Maven dependencies' +task :update do + puts 'Updating Maven dependencies' + # Make sure things are in a good state to start with + Rake::Task['java:pin'].invoke + + file_path = 'MODULE.bazel' + content = File.read(file_path) + output = nil + Bazel.execute('run', [], '@maven//:outdated') do |out| + output = out + end + + versions = output.scan(/(\S+) \[\S+ -> (\S+)\]/).to_h + versions.each do |artifact, version| + if artifact.match?('graphql') + # https://github.com/graphql-java/graphql-java/discussions/3187 + puts 'WARNING — Cannot automatically update graphql' + next + end + content.sub!(/#{Regexp.escape(artifact)}:([\d.-]+(?:[-.]?[A-Za-z0-9]+)*)/, "#{artifact}:#{version}") + end + File.write(file_path, content) + + Rake::Task['java:pin'].invoke +end + +desc 'Pin Maven dependencies' +task :pin do + args = ['--action_env=RULES_JVM_EXTERNAL_REPIN=1'] + Bazel.execute('run', args, '@maven//:pin') + %w[MODULE.bazel java/maven_install.json].each { |file| SeleniumRake.git.add(file) } +end + +desc 'Update Java changelog' +task :changelogs do + header = "v#{java_version}\n======" + SeleniumRake.update_changelog(java_version, 'java', 'java/src/org/', 'java/CHANGELOG', header) +end + +desc 'Update Java version' +task :version, [:version] do |_task, arguments| + old_version = java_version + new_version = SeleniumRake.updated_version(old_version, arguments[:version], '-SNAPSHOT') + puts "Updating Java from #{old_version} to #{new_version}" + + file = 'java/version.bzl' + text = File.read(file).gsub(old_version, new_version) + File.open(file, 'w') { |f| f.puts text } + SeleniumRake.git.add(file) +end + +desc 'Run Java formatter (google-java-format)' +task :lint do + puts ' Running google-java-format...' + formatter = nil + Bazel.execute('run', ['--run_under=echo'], '//scripts:google-java-format') do |output| + formatter = output.lines.last.strip + end + sh formatter, '--replace', *Dir.glob('java/**/*.java') +end diff --git a/rake_tasks/node.rake b/rake_tasks/node.rake new file mode 100644 index 0000000000000..1cf8d59137b51 --- /dev/null +++ b/rake_tasks/node.rake @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +def node_version + File.foreach('javascript/selenium-webdriver/package.json') do |line| + return line.split(':').last.strip.tr('",', '') if line.include?('version') + end +end + +def setup_npm_auth + npmrc = File.join(Dir.home, '.npmrc') + return if File.exist?(npmrc) && File.read(npmrc).include?('//registry.npmjs.org/:_authToken=') + + token = ENV.fetch('NODE_AUTH_TOKEN', nil) + raise 'Missing npm credentials: set NODE_AUTH_TOKEN or configure ~/.npmrc' if token.nil? || token.empty? + + auth_line = "//registry.npmjs.org/:_authToken=#{token}" + if File.exist?(npmrc) + File.open(npmrc, 'a') { |f| f.puts(auth_line) } + else + File.write(npmrc, "#{auth_line}\n") + end + File.chmod(0o600, npmrc) +end + +desc 'Build Node npm package' +task :build do |_task, arguments| + args = arguments.to_a + Bazel.execute('build', args, '//javascript/selenium-webdriver') +end + +desc 'Pin JavaScript dependencies via pnpm lockfile' +task :pin do + Bazel.execute('run', ['--', 'install', '--dir', Dir.pwd, '--lockfile-only'], '@pnpm//:pnpm') + SeleniumRake.git.add('pnpm-lock.yaml') +end + +desc 'Update JavaScript dependencies and refresh lockfile (use "latest" to bump ranges)' +task :update, [:latest] do |_task, arguments| + args = ['--', 'update', '-r'] + args << '--latest' if arguments[:latest] == 'latest' + args += ['--dir', Dir.pwd] + Bazel.execute('run', args, '@pnpm//:pnpm') + Rake::Task['node:pin'].invoke +end + +desc 'Validate Node release credentials' +task :check_credentials do |_task, arguments| + nightly = arguments.to_a.include?('nightly') + next if nightly + + npmrc = File.join(Dir.home, '.npmrc') + has_file = File.exist?(npmrc) && File.read(npmrc).include?('//registry.npmjs.org/:_authToken=') + has_env = ENV.fetch('NODE_AUTH_TOKEN', nil) && !ENV['NODE_AUTH_TOKEN'].empty? + raise 'Missing npm credentials: set NODE_AUTH_TOKEN or configure ~/.npmrc' unless has_file || has_env +end + +desc 'Release Node npm package (use dry-run to test without publishing)' +task :release do |_task, arguments| + args = arguments.to_a + nightly = args.delete('nightly') + dry_run = args.delete('dry-run') + + Rake::Task['node:check_credentials'].invoke(*(nightly ? ['nightly'] : [])) unless dry_run + setup_npm_auth unless nightly || dry_run + + if nightly + puts 'Updating Node version to nightly...' + Rake::Task['node:version'].invoke('nightly') + end + + puts dry_run ? 'Running Node package dry-run...' : 'Running Node package release...' + target = '//javascript/selenium-webdriver:selenium-webdriver.publish' + bazel_args = ['--config=release'] + bazel_args += ['--', '--dry-run=true'] if dry_run + Bazel.execute('run', bazel_args, target) +end + +desc 'Verify Node package is published on npm' +task :verify do + SeleniumRake.verify_package_published("https://registry.npmjs.org/selenium-webdriver/#{node_version}") +end + +desc 'Alias for node:release' +task deploy: :release + +desc 'Generate Node documentation' +task :docs do |_task, arguments| + if node_version.include?('nightly') && !arguments.to_a.include?('force') + abort('Aborting documentation update: nightly versions should not update docs.') + end + + puts 'Generating Node documentation' + FileUtils.rm_rf('build/docs/api/javascript/') + Bazel.execute('run', [], '//javascript/selenium-webdriver:docs') +end + +desc 'Install Node package locally via npm link' +task :install do + Bazel.execute('build', [], '//javascript/selenium-webdriver') + Dir.chdir('bazel-bin/javascript/selenium-webdriver/selenium-webdriver') do + sh 'npm', 'link' + end +end + +desc 'Update JavaScript changelog' +task :changelogs do + header = "## #{node_version}\n" + SeleniumRake.update_changelog(node_version, 'javascript', 'javascript/selenium-webdriver/', + 'javascript/selenium-webdriver/CHANGES.md', header) +end + +desc 'Update Node version' +task :version, [:version] do |_task, arguments| + old_version = node_version + nightly = "-nightly#{Time.now.strftime('%Y%m%d%H%M')}" + new_version = SeleniumRake.updated_version(old_version, arguments[:version], nightly) + puts "Updating Node from #{old_version} to #{new_version}" + + %w[javascript/selenium-webdriver/package.json javascript/selenium-webdriver/BUILD.bazel].each do |file| + text = File.read(file).gsub(old_version, new_version) + File.open(file, 'w') { |f| f.puts text } + SeleniumRake.git.add(file) + end +end + +desc 'Run Node linter (prettier)' +task :lint do |_task, arguments| + args = arguments.to_a + node_dir = File.expand_path('javascript/selenium-webdriver') + prettier_config = File.join(node_dir, '.prettierrc') + puts ' Running prettier...' + Bazel.execute('run', args + ['--', node_dir, '--write', "--config=#{prettier_config}", '--log-level=warn'], + '//javascript:prettier') +end diff --git a/rake_tasks/python.rake b/rake_tasks/python.rake new file mode 100644 index 0000000000000..a0fe4e0c49a03 --- /dev/null +++ b/rake_tasks/python.rake @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +require 'find' + +def python_version + File.foreach('py/BUILD.bazel') do |line| + return line.split('=').last.strip.tr('"', '') if line.include?('SE_VERSION') + end +end + +def setup_pypirc + pypirc = File.join(Dir.home, '.pypirc') + return if File.exist?(pypirc) && File.read(pypirc).match?(/^\[pypi\]/m) + + token = ENV.fetch('TWINE_PASSWORD', nil) + raise 'Missing PyPI credentials: set TWINE_PASSWORD or configure ~/.pypirc' if token.nil? || token.empty? + + pypi_section = <<~PYPIRC + [pypi] + username = __token__ + password = #{token} + PYPIRC + + if File.exist?(pypirc) + File.open(pypirc, 'a') { |f| f.puts("\n#{pypi_section}") } + else + File.write(pypirc, pypi_section) + end + File.chmod(0o600, pypirc) +end + +desc 'Build Python wheel and sdist with optional arguments' +task :build do |_task, arguments| + args = arguments.to_a + Bazel.execute('build', args, '//py:selenium-wheel') + Bazel.execute('build', args, '//py:selenium-sdist') +end + +desc 'Validate Python release credentials' +task :check_credentials do |_task, arguments| + nightly = arguments.to_a.include?('nightly') + next if nightly + + pypirc = File.join(Dir.home, '.pypirc') + has_pypirc = File.exist?(pypirc) && File.read(pypirc).match?(/^\[pypi\]/m) + has_env = ENV.fetch('TWINE_PASSWORD', nil) && !ENV['TWINE_PASSWORD'].empty? + raise 'Missing PyPI credentials: set TWINE_PASSWORD or configure ~/.pypirc' unless has_pypirc || has_env +end + +desc 'Release Python wheel and sdist to pypi' +task :release do |_task, arguments| + nightly = arguments.to_a.include?('nightly') + Rake::Task['py:check_credentials'].invoke(*arguments.to_a) + setup_pypirc unless nightly + + if nightly + puts 'Updating Python version to nightly...' + Rake::Task['py:version'].invoke('nightly') + end + + command = nightly ? '//py:selenium-release-nightly' : '//py:selenium-release' + puts "Running Python release command: #{command}" + Bazel.execute('run', ['--config=release'], command) +end + +desc 'Verify Python package is published on PyPI' +task :verify do + SeleniumRake.verify_package_published("https://pypi.org/pypi/selenium/#{python_version}/json") +end + +desc 'Copy known generated files for local development (use `./go py:local_dev all` to copy everything)' +task :local_dev, [:all] do |_task, arguments| + Bazel.execute('build', [], '//py:selenium') + + bazel_bin = 'bazel-bin/py/selenium/webdriver' + lib_path = 'py/selenium/webdriver' + + copy_all = arguments[:all] == 'all' + if copy_all + FileUtils.rm_rf("#{lib_path}/common/devtools") + FileUtils.cp_r("#{bazel_bin}/.", lib_path, remove_destination: true) + else + %w[common/devtools common/linux common/mac common/windows].each do |dir| + src = "#{bazel_bin}/#{dir}" + dest = "#{lib_path}/#{dir}" + next unless Dir.exist?(src) + + FileUtils.rm_rf(dest) + FileUtils.cp_r(src, dest) + end + + %w[getAttribute.js isDisplayed.js findElements.js].each do |atom| + FileUtils.cp("#{bazel_bin}/remote/#{atom}", "#{lib_path}/remote/#{atom}") + end + end +end + +desc 'Generate Python documentation' +task :docs do |_task, arguments| + if python_version.match?(/^\d+\.\d+\.\d+\.\d+$/) && !arguments.to_a.include?('force') + abort('Aborting documentation update: nightly versions should not update docs.') + end + puts 'Generating Python documentation' + + FileUtils.rm_rf('build/docs/api/py/') + + # Generate API listing and stub files in source tree + Bazel.execute('run', [], '//py:generate-api-listing') + Bazel.execute('run', [], '//py:sphinx-autogen') + + # Build docs (outputs to bazel-bin) + Bazel.execute('build', [], '//py:docs') + + FileUtils.mkdir_p('build/docs/api') + FileUtils.cp_r('bazel-bin/py/docs/_build/html/.', 'build/docs/api/py') +end + +desc 'Install Python wheel locally' +task :install do + Bazel.execute('build', [], '//py:selenium-wheel') + sh 'pip install bazel-bin/py/selenium-*.whl' +end + +desc 'Update Python changelog' +task :changelogs do + header = "Selenium #{python_version}" + SeleniumRake.update_changelog(python_version, 'py', 'py/selenium/webdriver', 'py/CHANGES', header) +end + +desc 'Update Python version' +task :version, [:version] do |_task, arguments| + old_version = python_version + nightly = ".#{Time.now.strftime('%Y%m%d%H%M')}" + new_version = SeleniumRake.updated_version(old_version, arguments[:version], nightly) + puts "Updating Python from #{old_version} to #{new_version}" + + ['py/pyproject.toml', + 'py/BUILD.bazel', + 'py/selenium/__init__.py', + 'py/selenium/webdriver/__init__.py', + 'py/docs/source/conf.py'].each do |file| + text = File.read(file).gsub(old_version, new_version) + File.open(file, 'w') { |f| f.puts text } + SeleniumRake.git.add(file) + end + + old_short_version = old_version.split('.')[0..1].join('.') + new_short_version = new_version.split('.')[0..1].join('.') + + conf = 'py/docs/source/conf.py' + text = File.read(conf).gsub(old_short_version, new_short_version) + File.open(conf, 'w') { |f| f.puts text } + SeleniumRake.git.add(conf) +end + +desc 'Run Python linter (ruff check + format)' +task :lint do |_task, arguments| + args = arguments.to_a + puts ' Running ruff check...' + Bazel.execute('run', args + ['--', 'check', '--fix', 'py/'], '@multitool//tools/ruff:cwd') + puts ' Running ruff format...' + Bazel.execute('run', args + ['--', 'format', 'py/'], '@multitool//tools/ruff:cwd') +end diff --git a/rake_tasks/ruby.rake b/rake_tasks/ruby.rake new file mode 100644 index 0000000000000..d82a8619d7cfc --- /dev/null +++ b/rake_tasks/ruby.rake @@ -0,0 +1,209 @@ +# frozen_string_literal: true + +require 'digest' +require 'net/http' + +def ruby_version + File.foreach('rb/lib/selenium/webdriver/version.rb') do |line| + return line.split('=').last.strip.tr("'", '') if line.include?('VERSION') + end +end + +def setup_gem_credentials + gem_dir = File.join(Dir.home, '.gem') + credentials = File.join(gem_dir, 'credentials') + return if File.exist?(credentials) && File.read(credentials).include?(':rubygems_api_key:') + + token = ENV.fetch('GEM_HOST_API_KEY', nil) + if token.nil? || token.empty? + raise 'Missing RubyGems credentials: set GEM_HOST_API_KEY or configure ~/.gem/credentials' + end + + FileUtils.mkdir_p(gem_dir) + if File.exist?(credentials) + File.open(credentials, 'a') { |f| f.puts(":rubygems_api_key: #{token}") } + else + File.write(credentials, ":rubygems_api_key: #{token}\n") + end + File.chmod(0o600, credentials) +end + +desc 'Generate Ruby gems' +task :build do |_task, arguments| + args = arguments.to_a + webdriver = args.delete('webdriver') + devtools = args.delete('devtools') + + Bazel.execute('build', args, '//rb:selenium-webdriver') if webdriver || !devtools + Bazel.execute('build', args, '//rb:selenium-devtools') if devtools || !webdriver +end + +desc 'Update generated Ruby files for local development' +task :local_dev do + puts 'installing ruby, this may take a minute' + Bazel.execute('build', [], '@bundle//:bundle') + Rake::Task['rb:build'].invoke + Rake::Task['grid'].invoke + # A command like this is required to move ruby binary into working directory + Bazel.execute('build', %w[--test_arg --dry-run], '@bundle//bin:rubocop') +end + +desc 'Validate Ruby release credentials' +task :check_credentials do |_task, arguments| + nightly = arguments.to_a.include?('nightly') + next if nightly + + credentials = File.join(Dir.home, '.gem', 'credentials') + has_file = File.exist?(credentials) && File.read(credentials).include?(':rubygems_api_key:') + has_env = ENV.fetch('GEM_HOST_API_KEY', nil) && !ENV['GEM_HOST_API_KEY'].empty? + raise 'Missing RubyGems credentials: set GEM_HOST_API_KEY or configure ~/.gem/credentials' unless has_file || has_env +end + +desc 'Push Ruby gems to rubygems' +task :release do |_task, arguments| + nightly = arguments.to_a.include?('nightly') + Rake::Task['rb:check_credentials'].invoke(*arguments.to_a) + + if nightly + puts 'Bumping Ruby nightly version...' + Bazel.execute('run', [], '//rb:selenium-webdriver-bump-nightly-version') + + puts 'Releasing nightly WebDriver gem...' + Bazel.execute('run', ['--config=release'], '//rb:selenium-webdriver-release-nightly') + else + setup_gem_credentials + patch_release = ruby_version.split('.').fetch(2, '0').to_i.positive? + + puts 'Releasing Ruby gems...' + Bazel.execute('run', ['--config=release'], '//rb:selenium-webdriver-release') + Bazel.execute('run', ['--config=release'], '//rb:selenium-devtools-release') unless patch_release + end +end + +desc 'Verify Ruby packages are published on RubyGems' +task :verify do + patch_release = ruby_version.split('.').fetch(2, '0').to_i.positive? + + SeleniumRake.verify_package_published("https://rubygems.org/api/v2/rubygems/selenium-webdriver/versions/#{ruby_version}.json") + unless patch_release + SeleniumRake.verify_package_published("https://rubygems.org/api/v2/rubygems/selenium-devtools/versions/#{ruby_version}.json") + end +end + +desc 'Generate Ruby documentation' +task :docs do |_task, arguments| + if ruby_version.include?('nightly') && !arguments.to_a.include?('force') + abort('Aborting documentation update: nightly versions should not update docs.') + end + puts 'Generating Ruby documentation' + + FileUtils.rm_rf('build/docs/api/rb/') + Bazel.execute('run', [], '//rb:docs') + FileUtils.mkdir_p('build/docs/api') + FileUtils.cp_r('bazel-bin/rb/docs.sh.runfiles/_main/docs/api/rb/.', 'build/docs/api/rb') +end + +desc 'Install Ruby gem locally' +task :install do + Bazel.execute('build', [], '//rb:selenium-webdriver') + Dir.glob('bazel-bin/rb/selenium-webdriver-*.gem').each do |gem| + sh 'gem', 'install', gem + end +end + +desc 'Update Ruby changelog' +task :changelogs do + header = "#{ruby_version} (#{Time.now.strftime('%Y-%m-%d')})\n=========================" + SeleniumRake.update_changelog(ruby_version, 'rb', 'rb/lib/', 'rb/CHANGES', header) +end + +desc 'Update Ruby version' +task :version, [:version] do |_task, arguments| + old_version = ruby_version + new_version = SeleniumRake.updated_version(old_version, arguments[:version], '.nightly') + puts "Updating Ruby from #{old_version} to #{new_version}" + + file = 'rb/lib/selenium/webdriver/version.rb' + text = File.read(file).gsub(old_version, new_version) + File.open(file, 'w') { |f| f.puts text } + SeleniumRake.git.add(file) + + Rake::Task['rb:update'].invoke +end + +desc 'Run Ruby linting' +task :lint do |_task, arguments| + args = arguments.to_a + puts ' Running rubocop...' + Bazel.execute('run', args, '//rb:lint') + puts ' Running steep type checker...' + Bazel.execute('run', args, '//rb:steep') +end + +desc 'Sync gem checksums from Gemfile.lock to MODULE.bazel (use force to re-download all)' +task :pin, [:force] do |_task, arguments| + gemfile_lock = 'rb/Gemfile.lock' + module_bazel = 'MODULE.bazel' + force = arguments[:force] == 'force' + + lock_content = File.read(gemfile_lock) + gem_section = lock_content[/GEM\n\s+remote:.*?\n\s+specs:\n(.*?)(?=\n[A-Z]|\Z)/m, 1] + gems = gem_section.scan(/^ ([a-zA-Z0-9_-]+) \(([^)]+)\)$/) + needed_gems = gems.map { |name, version| "#{name}-#{version}" } + + # Parse existing checksums from MODULE.bazel + module_content = File.read(module_bazel) + existing = module_content.scan(/"([^"]+)":\s*"([a-f0-9]{64})"/).to_h + + # Keep existing checksums for gems still in Gemfile.lock (unless force) + checksums = force ? {} : existing.slice(*needed_gems) + to_download = needed_gems - checksums.keys + + puts "Found #{gems.size} gems: #{checksums.size} cached, #{to_download.size} to download..." + + failed = [] + to_download.each do |key| + uri = URI("https://rubygems.org/gems/#{key}.gem") + response = nil + + 5.times do + response = Net::HTTP.get_response(uri) + break unless response.is_a?(Net::HTTPRedirection) + + uri = URI(response['location']) + end + + unless response.is_a?(Net::HTTPSuccess) + puts " #{key}: failed (HTTP #{response.code})" + failed << key + next + end + + sha = Digest::SHA256.hexdigest(response.body) + checksums[key] = sha + puts " #{key}: #{sha[0, 16]}..." + rescue StandardError => e + puts " #{key}: failed (#{e.message})" + failed << key + end + + raise "Failed to download checksums for: #{failed.join(', ')}" if failed.any? + + checksums_lines = checksums.keys.sort.map { |k| " \"#{k}\": \"#{checksums[k]}\"," } + formatted = " gem_checksums = {\n#{checksums_lines.join("\n")}\n }," + + new_content = module_content.sub(/ gem_checksums = \{[^}]+\},/m, formatted) + File.write(module_bazel, new_content) + + SeleniumRake.git.add(module_bazel) +end + +desc 'Update Ruby dependencies and sync checksums to MODULE.bazel' +task :update do + puts 'updating and pinning gem versions' + Bazel.execute('run', [], '//rb:bundle-update') + SeleniumRake.git.add('rb/Gemfile.lock') + Bazel.execute('run', [], '//rb:rbs-update') + SeleniumRake.git.add('rb/rbs_collection.lock.yaml') + Rake::Task['rb:pin'].invoke +end diff --git a/rake_tasks/rust.rake b/rake_tasks/rust.rake new file mode 100644 index 0000000000000..6be0de90deaed --- /dev/null +++ b/rake_tasks/rust.rake @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +def rust_version + File.foreach('rust/BUILD.bazel') do |line| + return line.split('=').last.strip.tr('",', '') if line.include?('version =') + end +end + +desc 'Build Selenium Manager' +task :build do |_task, arguments| + args = arguments.to_a + Bazel.execute('build', args, '//rust:selenium-manager') +end + +desc 'Update the rust lock files' +task :update do + puts 'pinning cargo versions' + ENV['CARGO_BAZEL_REPIN'] = 'true' + Bazel.execute('fetch', [], '@crates//:all') +end + +desc 'Pin Rust dependencies' +task pin: :update + +desc 'Run Rust linting' +task :lint do |_task, arguments| + args = arguments.to_a + puts ' Running rustfmt...' + Bazel.execute('run', args, '@rules_rust//:rustfmt') +end + +desc 'Update Rust changelog' +task :changelogs do + header = "#{rust_version}\n======" + version = rust_version.split('.').tap(&:shift).join('.') + SeleniumRake.update_changelog(version, 'rust', 'rust/src', 'rust/CHANGELOG.md', header) +end + +# Rust versioning is currently difficult compared to the others because we are using the 0.4.x pattern +# until Selenium Manager comes out of beta +desc 'Update Rust version' +task :version, [:version] do |_task, arguments| + old_version = rust_version.dup + equivalent_version = if old_version.include?('nightly') + "#{old_version.split(/\.|-/)[0...-1].tap(&:shift).join('.')}.0-nightly" + else + old_version.split('.').tap(&:shift).append('0').join('.') + end + updated = SeleniumRake.updated_version(equivalent_version, arguments[:version], '-nightly') + new_version = updated.split(/\.|-/).tap { |v| v.delete_at(2) }.unshift('0').join('.').gsub('.nightly', '-nightly') + puts "Updating Rust from #{old_version} to #{new_version}" + + ['rust/Cargo.toml', 'rust/BUILD.bazel'].each do |file| + text = File.read(file).gsub(old_version, new_version) + File.open(file, 'w') { |f| f.puts text } + SeleniumRake.git.add(file) + end + + Rake::Task['rust:update'].invoke + SeleniumRake.git.add('rust/Cargo.Bazel.lock') + SeleniumRake.git.add('rust/Cargo.lock') +end diff --git a/rb/.rubocop.yml b/rb/.rubocop.yml index 2aa77666d25ab..952e6352a27be 100644 --- a/rb/.rubocop.yml +++ b/rb/.rubocop.yml @@ -16,7 +16,7 @@ Layout/LineLength: AllowedPatterns: - '^\s*#' Exclude: - - '../Rakefile*' + - '../rake_tasks/**/*' Layout/SpaceInsideHashLiteralBraces: EnforcedStyle: no_space @@ -29,13 +29,15 @@ Metrics/AbcSize: - 'lib/selenium/webdriver/remote/capabilities.rb' - 'lib/selenium/webdriver/remote/http/curb.rb' - 'lib/selenium/webdriver/support/color.rb' + - '../rake_tasks/**/*' Metrics/BlockLength: Max: 18 Exclude: - 'spec/**/*.rb' - 'selenium-*.gemspec' - - '../Rakefile*' + - '../rake_tasks/**/*' + - '../Rakefile' Metrics/ClassLength: CountComments: false @@ -52,6 +54,7 @@ Metrics/CyclomaticComplexity: Exclude: - 'lib/selenium/webdriver/support/color.rb' - 'lib/selenium/webdriver/common/logger.rb' + - '../rake_tasks/**/*' Metrics/MethodLength: CountComments: false @@ -62,6 +65,7 @@ Metrics/MethodLength: - 'lib/selenium/webdriver/common/driver.rb' - 'lib/selenium/webdriver/common/driver_finder.rb' - 'lib/selenium/webdriver/remote/http/default.rb' + - '../rake_tasks/**/*' Metrics/ModuleLength: CountComments: false @@ -76,6 +80,7 @@ Metrics/PerceivedComplexity: - 'lib/selenium/webdriver/common/options.rb' - 'lib/selenium/webdriver/common/local_driver.rb' - 'lib/selenium/webdriver/common/logger.rb' + - '../rake_tasks/**/*' Naming/BlockForwarding: EnforcedStyle: explicit @@ -83,7 +88,6 @@ Naming/BlockForwarding: Naming/FileName: Exclude: - 'lib/selenium-webdriver.rb' - - 'Rakefile' - '../Rakefile' Naming/MethodParameterName: @@ -162,7 +166,7 @@ Lint/UselessConstantScoping: Lint/RedundantRequireStatement: Exclude: - - '../Rakefile' + - '../rake_tasks/**/*' RSpec/Output: Exclude: diff --git a/rb/BUILD.bazel b/rb/BUILD.bazel index 1f46a7f421064..518c60708ea65 100644 --- a/rb/BUILD.bazel +++ b/rb/BUILD.bazel @@ -194,6 +194,7 @@ _LINT_ARGS = [ "--config=rb/.rubocop.yml", "rb/", "Rakefile", + "rake_tasks/", ] _LINT_DATA = [ diff --git a/scripts/update_cdp.py b/scripts/update_cdp.py index cb084af50e126..d87e2fe08b44c 100755 --- a/scripts/update_cdp.py +++ b/scripts/update_cdp.py @@ -170,7 +170,7 @@ def update_java(chrome_milestone): files = [ root_dir / "java/src/org/openqa/selenium/devtools/versions.bzl", - root_dir / "Rakefile", + root_dir / "rake_tasks/java.rake", ] for file in files: replace_in_file(file, old_chrome(chrome_milestone), new_chrome(chrome_milestone))