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))