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