blob: 52f0d33d68a564d75dfe2e7f6a88941336537595 [file] [log] [blame] [edit]
# frozen_string_literal: true
require 'English'
$LOAD_PATH.unshift File.expand_path('.')
require 'base64'
require 'json'
require 'rake'
require 'net/http'
require 'net/telnet'
require 'stringio'
require 'fileutils'
require 'open-uri'
require 'git'
require 'find'
Rake.application.instance_variable_set(:@name, 'go')
orig_verbose = verbose
verbose(false)
# The CrazyFun build grammar. There's no magic here, just ruby
require 'rake_tasks/crazy_fun/main'
require 'rake_tasks/selenium_rake/detonating_handler'
require 'rake_tasks/selenium_rake/crazy_fun'
# The CrazyFun builders - Most of these are either partially or fully obsolete
# Note the order here is important - The top 2 are used in inheritance chains
require 'rake_tasks/crazy_fun/mappings/file_copy_hack'
require 'rake_tasks/crazy_fun/mappings/tasks'
require 'rake_tasks/crazy_fun/mappings/rake_mappings'
# Location of all new (non-CrazyFun) methods
require 'rake_tasks/selenium_rake/browsers'
require 'rake_tasks/selenium_rake/checks'
require 'rake_tasks/selenium_rake/cpp_formatter'
require 'rake_tasks/selenium_rake/ie_generator'
require 'rake_tasks/selenium_rake/java_formatter'
require 'rake_tasks/selenium_rake/type_definitions_generator'
# Our modifications to the Rake / Bazel libraries
require 'rake/task'
require 'rake_tasks/rake/task'
require 'rake_tasks/rake/dsl'
require 'rake_tasks/bazel/task'
# These are the final items mixed into the global NS
# These need moving into correct namespaces, and not be globally included
require 'rake_tasks/bazel'
require 'rake_tasks/python'
$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
# The build system used by webdriver is layered on top of rake, and we call it
# "crazy fun" for no readily apparent reason.
# First off, create a new CrazyFun object.
crazy_fun = SeleniumRake::CrazyFun.new
# Secondly, we add the handlers, which are responsible for turning a build
# rule into a (series of) rake tasks. For example if we're looking at a file
# in subdirectory "subdir" contains the line:
#
# java_library(:name => "example", :srcs => ["foo.java"])
#
# we would generate a rake target of "//subdir:example" which would generate
# a Java JAR at "build/subdir/example.jar".
#
# If crazy fun doesn't know how to handle a particular output type ("java_library"
# in the example above) then it will throw an exception, stopping the build
CrazyFun::Mappings::RakeMappings.new.add_all(crazy_fun)
# Finally, find every file named "build.desc" in the project, and generate
# rake tasks from them. These tasks are normal rake tasks, and can be invoked
# from rake.
# FIXME: the rules for the targets were removed and build files won't load
# crazy_fun.create_tasks(Dir['**/build.desc'])
# If it looks like a bazel target, build it with bazel
rule(%r{//.*}) do |task|
task.out = Bazel.execute('build', %w[], task.name)
end
# Spoof tasks to get CI working with bazel
task '//java/test/org/openqa/selenium/environment/webserver:webserver:uber' => [
'//java/test/org/openqa/selenium/environment:webserver'
]
# 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]
# ./go update_browser stable
# ./go update_browser beta
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}"] : []
puts 'pinning updated browsers and drivers'
Bazel.execute('run', args, '//scripts:pinned_browsers')
@git.add('common/repositories.bzl')
end
desc 'Update Selenium Manager to latest release'
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 all: [
:'selenium-java',
'//java/test/org/openqa/selenium/environment:webserver'
]
task tests: [
'//java/test/org/openqa/selenium/htmlunit:htmlunit',
'//java/test/org/openqa/selenium/firefox:test-synthesized',
'//java/test/org/openqa/selenium/ie:ie',
'//java/test/org/openqa/selenium/chrome:chrome',
'//java/test/org/openqa/selenium/edge:edge',
'//java/test/org/openqa/selenium/support:small-tests',
'//java/test/org/openqa/selenium/support:large-tests',
'//java/test/org/openqa/selenium/remote:small-tests',
'//java/test/org/openqa/selenium/remote/server/log:test',
'//java/test/org/openqa/selenium/remote/server:small-tests'
]
task chrome: ['//java/src/org/openqa/selenium/chrome']
task grid: [:'selenium-server-standalone']
task ie: ['//java/src/org/openqa/selenium/ie']
task firefox: ['//java/src/org/openqa/selenium/firefox']
task remote: %i[remote_server remote_client]
task remote_client: ['//java/src/org/openqa/selenium/remote']
task remote_server: ['//java/src/org/openqa/selenium/remote/server']
task safari: ['//java/src/org/openqa/selenium/safari']
task selenium: ['//java/src/org/openqa/selenium:core']
task support: ['//java/src/org/openqa/selenium/support']
desc 'Build the standalone server'
task 'selenium-server-standalone' => '//java/src/org/openqa/selenium/grid:executable-grid'
task test_javascript: [
'//javascript/atoms:test-chrome:run',
'//javascript/webdriver:test-chrome:run',
'//javascript/selenium-atoms:test-chrome:run',
'//javascript/selenium-core:test-chrome:run'
]
task test_chrome: ['//java/test/org/openqa/selenium/chrome:chrome:run']
task test_edge: ['//java/test/org/openqa/selenium/edge:edge:run']
task test_chrome_atoms: [
'//javascript/atoms:test-chrome:run',
'//javascript/chrome-driver:test-chrome:run',
'//javascript/webdriver:test-chrome:run'
]
task test_htmlunit: [
'//java/test/org/openqa/selenium/htmlunit:htmlunit:run'
]
task test_grid: [
'//java/test/org/openqa/grid/common:common:run',
'//java/test/org/openqa/grid:grid:run',
'//java/test/org/openqa/grid/e2e:e2e:run',
'//java/test/org/openqa/selenium/remote:remote-driver-grid-tests:run'
]
task test_ie: [
'//cpp/iedriverserver:win32',
'//cpp/iedriverserver:x64',
'//java/test/org/openqa/selenium/ie:ie:run'
]
task test_jobbie: [:test_ie]
task test_firefox: ['//java/test/org/openqa/selenium/firefox:marionette:run']
task test_remote_server: [
'//java/test/org/openqa/selenium/remote/server:small-tests:run',
'//java/test/org/openqa/selenium/remote/server/log:test:run'
]
task test_remote: [
'//java/test/org/openqa/selenium/json:small-tests:run',
'//java/test/org/openqa/selenium/remote:common-tests:run',
'//java/test/org/openqa/selenium/remote:client-tests:run',
'//java/test/org/openqa/selenium/remote:remote-driver-tests:run',
:test_remote_server
]
task test_safari: ['//java/test/org/openqa/selenium/safari:safari:run']
task test_support: [
'//java/test/org/openqa/selenium/support:small-tests:run',
'//java/test/org/openqa/selenium/support:large-tests:run'
]
task :test_java_webdriver do
if SeleniumRake::Checks.windows?
Rake::Task['test_ie'].invoke
elsif SeleniumRake::Checks.chrome?
Rake::Task['test_chrome'].invoke
elsif SeleniumRake::Checks.edge?
Rake::Task['test_edge'].invoke
else
Rake::Task['test_htmlunit'].invoke
Rake::Task['test_firefox'].invoke
Rake::Task['test_remote_server'].invoke
end
end
task test_java: [
'//java/test/org/openqa/selenium/atoms:test:run',
:test_java_small_tests,
:test_support,
:test_java_webdriver,
:test_selenium,
'test_grid'
]
task test_java_small_tests: [
'//java/test/org/openqa/selenium:small-tests:run',
'//java/test/org/openqa/selenium/json:small-tests:run',
'//java/test/org/openqa/selenium/support:small-tests:run',
'//java/test/org/openqa/selenium/remote:common-tests:run',
'//java/test/org/openqa/selenium/remote:client-tests:run',
'//java/test/org/openqa/grid/selenium/node:node:run',
'//java/test/org/openqa/grid/selenium/proxy:proxy:run',
'//java/test/org/openqa/selenium/remote/server:small-tests:run',
'//java/test/org/openqa/selenium/remote/server/log:test:run'
]
task :test do
if SeleniumRake::Checks.python?
Rake::Task['test_py'].invoke
else
Rake::Task['test_javascript'].invoke
Rake::Task['test_java'].invoke
end
end
task test_py: [:py_prep_for_install_release, 'py:marionette_test']
task build: %i[all firefox remote selenium tests]
desc 'Clean build artifacts.'
task :clean do
rm_rf 'build/'
rm_rf 'java/build/'
rm_rf 'dist/'
end
# Create a new IEGenerator instance
ie_generator = SeleniumRake::IEGenerator.new
# Generate a C++ Header file for mapping between magic numbers and #defines
# in the C++ code.
ie_generator.generate_type_mapping(
name: 'ie_result_type_cpp',
src: 'cpp/iedriver/result_types.txt',
type: 'cpp',
out: 'cpp/iedriver/IEReturnTypes.h'
)
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)
cmd = cmd.tr('/', '\\').tr(':', ';') if SeleniumRake::Checks.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
file 'cpp/iedriver/sizzle.h' => ['//third_party/js/sizzle:sizzle:header'] do
cp 'build/third_party/js/sizzle/sizzle.h', 'cpp/iedriver/sizzle.h'
end
task sizzle_header: ['cpp/iedriver/sizzle.h']
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?('<id>central</id>') }
},
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?
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? '<id>central</id>'
elsif line.include?('<username>')
ENV['MAVEN_USER'] = line[%r{<username>(.*?)</username>}, 1]
elsif line.include?('<password>')
ENV['MAVEN_PASSWORD'] = line[%r{<password>(.*?)</password>}, 1]
end
break if ENV['MAVEN_PASSWORD'] && ENV['MAVEN_USER']
end
end
desc 'Publish all Java jars to Maven as stable release'
task 'publish-maven' do
Rake::Task['java:release'].invoke
end
desc 'Publish all Java jars to Maven as nightly release'
task 'publish-maven-snapshot' do
Rake::Task['java:release'].invoke('nightly')
end
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
end
desc 'Build the selenium client jars'
task 'selenium-java' => '//java/src/org/openqa/selenium:client-combined'
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
namespace :side do
task atoms: [
'//javascript/atoms/fragments:find-element'
] do
# TODO: move directly to IDE's directory once the repositories are merged
mkdir_p 'build/javascript/atoms'
atom = 'bazel-bin/javascript/atoms/fragments/find-element.js'
name = File.basename(atom)
puts "Generating #{atom} as #{name}"
File.open(File.join(baseDir, name), 'w') do |f|
f << "// GENERATED CODE - DO NOT EDIT\n"
f << 'module.exports = '
f << File.read(atom).strip
f << ";\n"
end
end
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
atom_list = %w[
//javascript/atoms/fragments:find-elements
//javascript/atoms/fragments:is-displayed
//javascript/webdriver/atoms:get-attribute
]
task atoms: atom_list do
base_dir = 'javascript/selenium-webdriver/lib/atoms'
mkdir_p base_dir
['bazel-bin/javascript/atoms/fragments/is-displayed.js',
'bazel-bin/javascript/webdriver/atoms/get-attribute.js',
'bazel-bin/javascript/atoms/fragments/find-elements.js'].each do |atom|
name = File.basename(atom)
puts "Generating #{atom} as #{name}"
File.open(File.join(base_dir, name), 'w') do |f|
f << "// GENERATED CODE - DO NOT EDIT\n"
f << 'module.exports = '
f << File.read(atom).strip
f << ";\n"
end
end
end
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')
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')
update_gh_pages unless arguments.to_a.include?('skip_update')
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')
update_gh_pages unless arguments.to_a.include?('skip_update')
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
task :atoms do
base_dir = 'rb/lib/selenium/webdriver/atoms'
mkdir_p base_dir
{
'//javascript/atoms/fragments:find-elements': 'findElements.js',
'//javascript/atoms/fragments:is-displayed': 'isDisplayed.js',
'//javascript/webdriver/atoms:get-attribute': 'getAttribute.js'
}.each do |target, name|
puts "Generating #{target} as #{name}"
atom = Bazel.execute('build', [], target.to_s)
File.open(File.join(base_dir, name), 'w') do |f|
f << File.read(atom).strip
end
end
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')
update_gh_pages unless arguments.to_a.include?('skip_update')
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
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')
update_gh_pages unless arguments.to_a.include?('skip_update')
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}"
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
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 <deployment_id>
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']
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 '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
update_gh_pages unless arguments.to_a.include?('skip_update')
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| @git.add(file) }
end
desc 'Update Java changelog'
task :changelog do
header = "v#{java_version}\n======"
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 = 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)
end
end
def rust_version
File.foreach('rust/BUILD.bazel') do |line|
return line.split('=').last.strip.tr('",', '') if line.include?('version =')
end
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')
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
end
namespace :all do
desc 'Pin dependencies for all languages'
task :pin do
Rake::Task['java:pin'].invoke
Rake::Task['rb:pin'].invoke
Rake::Task['rust:pin'].invoke
Rake::Task['node: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) }
end
desc 'Update all API Documentation'
task :docs do |_task, arguments|
args = arguments.to_a
Rake::Task['java:docs'].invoke(*(args + ['skip_update']))
Rake::Task['py:docs'].invoke(*(args + ['skip_update']))
Rake::Task['rb:docs'].invoke(*(args + ['skip_update']))
Rake::Task['dotnet:docs'].invoke(*(args + ['skip_update']))
Rake::Task['node:docs'].invoke(*(args + ['skip_update']))
update_gh_pages
end
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)
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)
end
desc 'Validate release credentials for all languages without releasing'
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
end
end
desc 'Verify all packages are published to their registries'
task :verify do
failures = []
%w[java py rb dotnet node].each do |lang|
Rake::Task["#{lang}:verify"].invoke
rescue StandardError => e
failures << "#{lang}: #{e.message}"
end
raise "Verification failed:\n#{failures.join("\n")}" unless failures.empty?
end
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)
Rake::Task['rb:release'].invoke(*args)
Rake::Task['dotnet:release'].invoke(*args)
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
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."
end
Bazel.execute('run', [], '//py:mypy')
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
end
desc 'Update all versions'
task :version, [:version] do |_task, arguments|
version = arguments[:version] || 'nightly'
puts "Updating all versions to #{version}"
Rake::Task['java:version'].invoke(version)
Rake::Task['rb:version'].invoke(version)
Rake::Task['node:version'].invoke(version)
Rake::Task['py:version'].invoke(version)
Rake::Task['dotnet:version'].invoke(version)
Rake::Task['rust:version'].invoke(version)
unless version == 'nightly'
major_minor = arguments[:version][/^\d+\.\d+/]
file = '.github/ISSUE_TEMPLATE/bug-report.yml'
old_version_pattern = /The latest released version of Selenium is (\d+\.\d+)/
text = File.read(file).gsub(old_version_pattern, "The latest released version of Selenium is #{major_minor}")
File.write(file, text)
@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
at_exit do
system 'sh', '.git-fixfiles' if File.exist?('.git') && SeleniumRake::Checks.linux?
rescue StandardError => e
puts "Do not exit execution when this errors... #{e.inspect}"
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 update_gh_pages(force: true)
puts 'Switching to gh-pages branch...'
@git.fetch('https://github.com/seleniumhq/selenium.git', {ref: 'gh-pages'})
unless force
puts 'Stash changes that are not docs...'
@git.lib.send(:command, 'stash', ['push', '-m', 'stash wip', '--', ':(exclude)build/docs/api/'])
end
@git.checkout('gh-pages', force: force)
updated = false
%w[java rb py dotnet javascript].each do |language|
source = "build/docs/api/#{language}"
destination = "docs/api/#{language}"
next unless Dir.exist?(source) && !Dir.empty?(source)
puts "Updating documentation for #{language}..."
FileUtils.rm_rf(destination)
FileUtils.mv(source, destination)
@git.add(destination)
updated = true
end
puts(updated ? 'Documentation staged. Ready for commit.' : 'No documentation changes found.')
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