blob: 33eab1d1e3425690a798ae41ce519ca46ae86afc [file] [edit]
# 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/v152:v152.publish
//java/src/org/openqa/selenium/devtools/v150:v150.publish
//java/src/org/openqa/selenium/devtools/v151:v151.publish
//java/src/org/openqa/selenium/devtools/latest:latest.publish
//java/src/org/openqa/selenium/edge:edge.publish
//java/src/org/openqa/selenium/firefox:firefox.publish
//java/src/org/openqa/selenium/grid/node/kubernetes:kubernetes.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
obsolete_targets = JAVA_RELEASE_TARGETS - current_targets
unlisted_targets = current_targets - JAVA_RELEASE_TARGETS
return if obsolete_targets.empty? && unlisted_targets.empty?
error_message = 'Java release targets are out of sync with Bazel query results.'
unless obsolete_targets.empty?
error_message += "\nObsolete targets (in list but not in Bazel): #{obsolete_targets.join(', ')}"
end
unless unlisted_targets.empty?
error_message += "\nMissing targets (in Bazel but not in list): #{unlisted_targets.join(', ')}"
end
raise error_message
end
module Sonatype
module_function
def load_credentials
ENV['MAVEN_USER'] ||= ENV.fetch('SEL_M2_USER', nil)
ENV['MAVEN_PASSWORD'] ||= ENV.fetch('SEL_M2_PASS', nil)
return if ENV['MAVEN_PASSWORD'] && ENV['MAVEN_USER']
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? '<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
def auth_token
load_credentials
Base64.strict_encode64("#{ENV.fetch('MAVEN_USER')}:#{ENV.fetch('MAVEN_PASSWORD')}")
end
def trigger_publish
puts 'Triggering Sonatype upload with automatic publishing...'
url = 'https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/' \
'org.seleniumhq?publishing_type=automatic'
uri = URI(url)
req = Net::HTTP::Post.new(uri)
req['Authorization'] = "Basic #{auth_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.
The deployment may still have been created on the server.
Check https://central.sonatype.com/publishing/deployments for status.
MSG
raise e
end
unless res.is_a?(Net::HTTPSuccess)
warn "Failed to trigger upload (HTTP #{res.code}): #{res.body}"
exit(1)
end
puts 'Upload triggered — Sonatype will automatically validate and publish.'
end
def request_json(request)
request['Accept'] = 'application/json'
res = SeleniumRake.get_request(request)
raise "#{request.uri.path} returned HTTP #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)
JSON.parse(res.body)
end
# Staging repositories are keyed by (user, IP, user agent) and the search defaults to the caller's
# IP, so ip=any is required to see one opened by a previous attempt on a different runner.
def staging_repositories
req = Net::HTTP::Get.new(URI('https://ossrh-staging-api.central.sonatype.com/manual/search/repositories?ip=any'))
req['Authorization'] = "Basic #{auth_token}"
request_json(req).fetch('repositories')
end
def published?(version)
query = "namespace=org.seleniumhq.selenium&name=selenium-java&version=#{version}"
req = Net::HTTP::Get.new(URI("https://central.sonatype.com/api/v1/publisher/published?#{query}"))
# The Portal documents Bearer, where the staging API shim above takes Basic. Same credential.
req['Authorization'] = "Bearer #{auth_token}"
request_json(req).fetch('published')
end
# A repository still open or closed is a previous attempt that has not landed — mid-publish, or
# rejected by the Portal. Both want a look at the deployments page before another deploy piles on.
def already_deployed?(version)
unfinished = %w[open closed]
if staging_repositories.any? { |repo| unfinished.include?(repo['state']) }
raise 'A previous attempt left a staging repository behind; check it at ' \
'https://central.sonatype.com/publishing/deployments before releasing again.'
end
return false unless published?(version)
puts "#{version} is already deployed — skipping the deploy."
true
end
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
# IDE test runners supply their own JUnit Platform launcher/engine; a second copy clashes.
JAVA_LIBS_IDE_TEST_RUNTIME = %w[
junit-jupiter-engine junit-platform-engine junit-platform-launcher junit-platform-reporting
].freeze
# Third-party jars on the IntelliJ modules' resolved classpath. Scoping to this drops unused
# transitives (e.g. javacc's test-only junit-4) by construction. Reads two providers, duck-typed
# because cquery keys them by long Starlark ids: JavaInfo.transitive_runtime_jars for libraries, and
# JavaRuntimeClasspathInfo.runtime_classpath for the in-tree java_binary tools (CDP/module generators).
def resolved_third_party_jars
expr = '"\n".join(' \
'[f.path for p in providers(target).values() ' \
'if hasattr(p, "transitive_runtime_jars") for f in p.transitive_runtime_jars.to_list()] + ' \
'[f.path for p in providers(target).values() ' \
'if hasattr(p, "runtime_classpath") for f in p.runtime_classpath.to_list()])'
query = 'set(//java/src/... //java/test/... //java/src/dev/...)'
jars = []
Bazel.execute('cquery', ['--pin_browsers=false', '--output=starlark', "--starlark:expr=#{expr}"], query) do |out|
jars = out.lines.map(&:strip)
.grep(%r{\+\+maven\+maven/.*\.jar$})
.map { |path| File.basename(path).sub(/\A(?:processed|header)_/, '') }
end
jars.uniq.reject { |base| JAVA_LIBS_IDE_TEST_RUNTIME.any? { |name| base.start_with?(name) } }
end
desc 'Copy Bazel-built dependency jars to ./java-libs for local development'
task :local_dev do
# pin_browsers defaults true and pulls pinned browsers this task doesn't need. @maven has the
# third-party jars; the module builds materialize the generated jars (devtools) the IDE can't rebuild.
['@maven//...', '//java/test/...', '//java/src/dev/...'].each do |target|
Bazel.execute('build', ['--pin_browsers=false'], target)
end
needed = resolved_third_party_jars
raise 'Resolved classpath query returned no third-party jars' if needed.empty?
# Bazel.execute merges stdout/stderr, so keep only the path line.
execroot = nil
Bazel.execute('info', [], 'execution_root') do |out|
execroot = out.lines.map(&:strip).grep(%r{[\\/]execroot[\\/]}).last
end
raise 'Could not determine Bazel execution_root' unless execroot
# Skip the exec-configuration bazel-out tree (config dir ends in "-exec") to avoid build-tool dupes.
exec_config = %r{[\\/]bazel-out[\\/][^\\/]*-exec[\\/]}
bin = File.join(execroot, 'bazel-out', '*', 'bin')
maven_tree = 'external/rules_jvm_external++maven+maven'
jars = Dir.glob([
File.join(bin, maven_tree, '**/*.jar'),
File.join(bin, 'java/src/org/openqa/selenium/devtools/v*/libcdp.jar'),
File.join(bin, 'java/src/org/openqa/selenium/devtools/v*/libcdp-src.jar'),
File.join(bin, 'external/rules_java+/**/librunfiles.jar'),
File.join(bin, 'external/rules_jvm_external+/**/libzip.jar')
]).grep_v(exec_config)
lib = File.join(Dir.pwd, 'java-libs')
sources = File.join(lib, 'sources')
FileUtils.rm_rf(lib)
FileUtils.mkdir_p(sources)
jars.each do |path|
base = File.basename(path)
if path.include?(maven_tree)
# rules_jvm_external emits header_/processed_ compile variants beside the raw jar; skip those
# and copy the raw jar, whose clean name matches the stripped `needed` basenames.
next if base.start_with?('header_', 'processed_')
next unless needed.include?(base.sub(/-(?:sources|src)\.jar$/, '.jar'))
elsif base.start_with?('libcdp')
# Every CDP version emits libcdp.jar; prefix with the version dir so a flat dir holds them all.
base = "#{File.basename(File.dirname(path))}-#{base}"
end
dir = base.end_with?('-sources.jar', '-src.jar') ? sources : lib
dest = File.join(dir, base)
warn "warning: overwriting #{base} (duplicate basename)" if File.exist?(dest)
FileUtils.cp(path, dest)
end
copied = Dir.children(lib).select { |entry| entry.end_with?('.jar') }
raise "No dependency jars copied to #{lib}; expected Bazel outputs are missing" if copied.empty?
# Fail loudly rather than hand the IDE an incomplete classpath if a resolved jar wasn't materialized.
missing = needed - copied
raise "Resolved classpath jars missing from #{lib}: #{missing.join(', ')}" unless missing.empty?
puts "Copied #{copied.size} dependency jars to #{lib}"
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')
Sonatype.load_credentials
unless ENV['MAVEN_USER'] && ENV['MAVEN_PASSWORD']
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'
task :release do |_task, arguments|
args = arguments.to_a
nightly = args.delete('nightly')
Rake::Task['java:check_credentials'].invoke(*(nightly ? ['nightly'] : []))
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')
next if !nightly && Sonatype.already_deployed?(java_version)
puts "Deploying Java artifacts to '#{ENV.fetch('MAVEN_REPO', nil)}'"
java_release_targets.each { |target| Bazel.execute('run', ['--config=release'], target) }
next if nightly
Sonatype.trigger_publish
end
def maven_central_pom_url
base = 'https://repo1.maven.org/maven2/org/seleniumhq/selenium/selenium-java'
"#{base}/#{java_version}/selenium-java-#{java_version}.pom"
end
desc 'Verify Java packages are published on Maven Central'
task :verify do
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 600
begin
SeleniumRake.verify_package_published(maven_central_pom_url)
rescue StandardError => e
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
raise "#{e.class}: #{e.message}; check https://central.sonatype.com/publishing/deployments"
end
puts " #{e.class}: #{e.message}; Maven Central may still be indexing, retrying in 15s"
sleep 15
retry
end
end
desc 'Install jars to local m2 directory'
task :install do
java_release_targets.each do |p|
Bazel.execute('run',
['--define',
"maven_repo=file://#{Dir.home}/.m2/repository",
'--define',
'gpg_sign=false'],
p)
end
end
desc 'Generate and stage 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
Rake::Task['java:docs_generate'].invoke
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 'Generate Java documentation without staging'
task :docs_generate do
puts 'Generating Java documentation'
Bazel.execute('build', [], '//java/src/org/openqa/selenium/grid:all-javadocs')
end
desc 'Update Maven dependencies to latest versions'
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') && version.match?(/\A(\d{6}-|0\.0\.0-)/)
# Maven Central indexes non-stable date-based artifacts that sort as "latest" which breaks for graphql
# https://github.com/graphql-java/graphql-java/discussions/3187
# Fall back to calling maven directly for this
version = maven_stable_release(artifact)
next if version.nil?
end
if artifact.start_with?('net.bytebuddy:') && version.match?(/-jdk\d/)
# Byte Buddy publishes -jdkN compat variants alongside regular releases; Maven sorts those
# variants as newer than the regular release. Selenium targets Java 8+ so we want the
# regular release without the JDK suffix.
# https://github.com/SeleniumHQ/selenium/issues/17355
version = maven_stable_release(artifact)
next if version.nil?
end
content.sub!(/#{Regexp.escape(artifact)}:([\d.-]+(?:[-.]?[A-Za-z0-9]+)*)/, "#{artifact}:#{version}")
end
File.write(file_path, content)
Rake::Task['java:pin'].reenable
Rake::Task['java:pin'].invoke
end
def maven_stable_release(artifact)
require 'rexml/document'
group_id, artifact_id = artifact.split(':', 2)
group_path = group_id.tr('.', '/')
uri = URI("https://repo1.maven.org/maven2/#{group_path}/#{artifact_id}/maven-metadata.xml")
xml = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 5, read_timeout: 10) do |http|
http.get(uri.request_uri).body
end
doc = REXML::Document.new(xml)
versions = doc.elements.to_a('metadata/versioning/versions/version').map(&:text)
stable = versions.grep(/\A\d+\.\d+(\.\d+)*\z/)
stable.max_by { |v| Gem::Version.new(v) }
rescue StandardError => e
puts "WARNING — Failed to fetch stable release for #{artifact}: #{e.message}"
nil
end
desc 'Pin Maven dependencies'
task :pin do
args = ['--repo_env=RULES_JVM_EXTERNAL_REPIN=1']
Bazel.execute('run', args, '@maven//:pin')
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 }
end
desc 'Format Java code with google-java-format'
task :format do
puts ' Running google-java-format...'
java_files = Dir.glob(File.join(Dir.pwd, 'java', '**', '*.java'))
return if java_files.empty?
Tempfile.create('google-java-format-files') do |f|
java_files.each { |file| f.puts(file) }
f.flush
args = ['--', '--replace', "@#{f.path}"]
Bazel.execute('run', args, '//scripts:google-java-format')
end
end
# ErrorProne runs at build time, SpotBugs runs as test targets in RBE
desc 'Run Java linter (docs only, other linting happens during build/test)'
task :lint do
Rake::Task['java:docs_generate'].invoke
end