Skip to content

[build] Java release improvements and build verification tasks#16952

Merged
titusfortner merged 9 commits intotrunkfrom
pr/rakefile-build-verification
Jan 19, 2026
Merged

[build] Java release improvements and build verification tasks#16952
titusfortner merged 9 commits intotrunkfrom
pr/rakefile-build-verification

Conversation

@titusfortner
Copy link
Member

@titusfortner titusfortner commented Jan 19, 2026

User description

Java release worked yesterday, but the new Sonatype validation step timed out

💥 What does this PR do?

  • Increase sonatype validation timeout
  • Verify that all assets uploaded at the end of the release workflow
  • Automatically publish the deployment
  • Add publication verification for each language

🔧 Implementation Notes

  • Added a lot of comments to make it clear where things are
    - Verification tasks can be run separately / manually

UPDATES:

  • Verifications Integrated later in the release workflow instead of in the release task
  • Verification tasks raise on failure instead of warning

💡 Additional Considerations

We can't know if the Java improvements will fix the problem I had yesterday until next release, unfortunately

🔄 Types of changes

  • New feature (non-breaking change which adds functionality)

PR Type

Enhancement, Tests


Description

  • Add package verification tasks for all language bindings

  • Improve Java release robustness with deployment validation

  • Increase Sonatype API timeout and add error handling

  • Support manual deployment publishing by ID


Diagram Walkthrough

flowchart LR
  Release["Language Release Tasks"]
  Verify["Verification Tasks"]
  JavaDeploy["Java Deployment Publishing"]
  
  Release -->|invoke| Verify
  Release -->|java| JavaDeploy
  JavaDeploy -->|validate| Verify
  Verify -->|check registries| Status["Package Status"]
Loading

File Walkthrough

Relevant files
Enhancement
Rakefile
Add verification tasks and improve Java release workflow 

Rakefile

  • Added package_published? helper to verify package availability on
    registries with retry logic
  • Added sonatype_api_post helper for Sonatype API interactions with
    Bearer token auth
  • Added :verify tasks for Node, Python, Ruby, .NET, and Java language
    bindings
  • Enhanced Java :publish task with improved timeout handling and
    deployment ID extraction
  • Added new :publish_deployment task to manually publish validated
    Sonatype deployments
  • Updated :check_credentials task to conditionally validate credentials
    based on release type
  • Added :verify task to all namespace to run verification for all
    languages
+157/-12

@selenium-ci selenium-ci added the B-build Includes scripting, bazel and CI integrations label Jan 19, 2026
@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Jan 19, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Leaks response body: The raised exception includes the full Sonatype HTTP response body, which may expose
internal details into CI/user-visible logs.

Referred Code
def sonatype_api_post(url, token)
  uri = URI(url)
  req = Net::HTTP::Post.new(uri)
  req['Authorization'] = "Bearer #{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)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Unhandled parse errors: sonatype_api_post can raise unhandled exceptions (e.g., JSON parse errors or unexpected
HTTP bodies) without adding actionable context about the request/operation that failed.

Referred Code
def sonatype_api_post(url, token)
  uri = URI(url)
  req = Net::HTTP::Post.new(uri)
  req['Authorization'] = "Bearer #{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.empty? ? {} : JSON.parse(res.body)
end

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Potential sensitive logging: Error paths may emit Sonatype response bodies (and other exception messages) to
stdout/stderr, which could inadvertently include sensitive operational details in build
logs.

Referred Code
def package_published?(url, max_attempts: 12, delay: 10)
  puts "Verifying #{url}..."
  uri = URI(url)
  max_attempts.times do |attempt|
    res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https',
                                                  open_timeout: 10, read_timeout: 10) { |http| http.request(Net::HTTP::Get.new(uri)) }
    if res.is_a?(Net::HTTPSuccess)
      puts 'Verified!'
      return true
    end
    puts "Not yet indexed, waiting... (#{attempt + 1}/#{max_attempts})"
    sleep(delay)
  rescue StandardError => e
    warn "Error: #{e.message}"
    sleep(delay) unless attempt == max_attempts - 1
  end
  warn "Not found after #{max_attempts * delay}s"
  false
end

def sonatype_api_post(url, token)


 ... (clipped 9 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Jan 19, 2026

PR Code Suggestions ✨

Latest suggestions up to 6cff022

CategorySuggestion                                                                                                                                    Impact
Possible issue
Retry registry verification requests

In the verify_package_published method, add a retry loop with a delay to handle
transient errors and eventual consistency from package registries, preventing
flaky pipeline failures.

Rakefile [399-407]

-def verify_package_published(url)
+def verify_package_published(url, attempts: 12, delay: 10)
   puts "Verifying #{url}..."
   uri = URI(url)
-  res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https',
-                                                open_timeout: 10, read_timeout: 10) { |http| http.request(Net::HTTP::Get.new(uri)) }
-  raise "Package not published: #{url}" unless res.is_a?(Net::HTTPSuccess)
 
-  puts 'Verified!'
+  attempts.times do |i|
+    res = Net::HTTP.start(uri.hostname, uri.port,
+                          use_ssl: uri.scheme == 'https',
+                          open_timeout: 10, read_timeout: 10) do |http|
+      http.request(Net::HTTP::Get.new(uri))
+    end
+
+    return puts('Verified!') if res.is_a?(Net::HTTPSuccess)
+
+    retryable = res.is_a?(Net::HTTPTooManyRequests) ||
+                res.is_a?(Net::HTTPServerError) ||
+                res.is_a?(Net::HTTPNotFound)
+
+    raise "Package not published: #{url} (HTTP #{res.code}): #{res.body}" unless retryable && i < attempts - 1
+
+    sleep(delay)
+  end
 end
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that package registries can have indexing delays, and the new verification step could be flaky without a retry mechanism. Implementing this makes the new workflow significantly more robust.

Medium
Prevent undefined state on timeout
Suggestion Impact:The commit ensures `state` is assigned after the polling loop (`state = status['deploymentState']`) before it is used in the timeout check, preventing a potential NameError when all API calls fail. This achieves the same goal as the suggestion (defined state on timeout) but via a different implementation (no pre-loop initialization or `final_state` fallback).

code diff:

-    return if status['deploymentState'] == 'PUBLISHED'
+    state = status['deploymentState']
+    return if state == 'PUBLISHED'
 
     raise "Timed out after #{(max_attempts * delay) / 60} minutes waiting for validation" unless state == 'VALIDATED'

Prevent a potential NameError in the java:publish_deployment task by
initializing the state variable to nil before the polling loop.

Rakefile [1222-1243]

 encoded_id = URI.encode_www_form_component(deployment_id.strip)
 status = {}
+state = nil
 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)
 rescue StandardError => e
   warn "API error (attempt #{attempt + 1}/#{max_attempts}): #{e.message}"
   sleep(delay) unless attempt == max_attempts - 1
 end
 
 return if status['deploymentState'] == 'PUBLISHED'
 
-raise "Timed out after #{(max_attempts * delay) / 60} minutes waiting for validation" unless state == 'VALIDATED'
+final_state = status['deploymentState'] || state
+raise "Timed out after #{(max_attempts * delay) / 60} minutes waiting for validation" unless final_state == 'VALIDATED'

[Suggestion processed]

Suggestion importance[1-10]: 7

__

Why: This suggestion correctly identifies a potential NameError if all API calls in the polling loop fail, as the state variable would be uninitialized. The fix is correct and prevents a crash in a plausible error scenario.

Medium
Validate deployment ID before publishing

In the java:publish task, validate that the deployment_id received from Sonatype
is not empty before invoking the java:publish_deployment task.

Rakefile [1202-1209]

 if res.is_a?(Net::HTTPSuccess)
-  deployment_id = res.body.strip
+  deployment_id = res.body.to_s.strip
+  if deployment_id.empty?
+    warn 'Central Portal returned an empty deployment ID; check the manual upload response and https://central.sonatype.com/publishing/deployments'
+    exit(1)
+  end
+
   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
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion adds a defensive check to ensure the deployment_id from Sonatype is not empty before proceeding. This improves error handling by failing fast with a clear message instead of causing a more obscure error later.

Low
Learned
best practice
Encode dynamic URL path segments

Trim and URL-encode node_version (and similarly other version values used in
verify URLs) before interpolating into a URL path to prevent invalid/unsafe
requests.

Rakefile [627-630]

 desc 'Verify Node package is published on npm'
 task :verify do
-  verify_package_published("https://registry.npmjs.org/selenium-webdriver/#{node_version}")
+  version = URI.encode_www_form_component(node_version.to_s.strip)
+  verify_package_published("https://registry.npmjs.org/selenium-webdriver/#{version}")
 end
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why:
Relevant best practice - Add explicit validation/encoding guards at integration boundaries before building URLs from dynamic inputs (e.g., versions) to avoid malformed requests or injection via special characters.

Low
  • Update

Previous suggestions

✅ Suggestions up to commit b11065d
CategorySuggestion                                                                                                                                    Impact
Possible issue
Add timeouts to network requests

Add open_timeout and read_timeout to the Net::HTTP.start call in the
sonatype_api_post function to prevent the script from hanging on unresponsive
network requests.

Rakefile [431-440]

 def sonatype_api_post(url, token)
   uri = URI(url)
   req = Net::HTTP::Post.new(uri)
   req['Authorization'] = "Bearer #{token}"
 
-  res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
+  res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout: 60, read_timeout: 60) { |http| http.request(req) }
   raise "Sonatype API error (#{res.code}): #{res.body}" unless res.is_a?(Net::HTTPSuccess)
 
   res.body.empty? ? {} : JSON.parse(res.body)
 end
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out the lack of timeouts in sonatype_api_post, which could cause the script to hang. Adding timeouts is a good practice for network requests to improve robustness.

Medium
Initialize variable to prevent NameError
Suggestion Impact:The commit adds `status = {}` before the polling loop in the Sonatype publish_deployment task, matching the intent of preventing errors when API calls fail. It also expands the logic with encoded deployment ID and retry/error handling, but the pre-initialization of `status` directly reflects the suggestion.

code diff:

-    60.times do
-      status = sonatype_api_post("https://central.sonatype.com/api/v1/publisher/status?id=#{deployment_id}", token)
+    encoded_id = URI.encode_www_form_component(deployment_id.strip)
+    status = {}
+    max_attempts = 60
+    delay = 5
+    max_attempts.times do |attempt|
+      status = sonatype_api_post("https://central.sonatype.com/api/v1/publisher/status?id=#{encoded_id}", token)
       state = status['deploymentState']
       puts "Deployment state: #{state}"
 
       case state
-      when 'VALIDATED' then break
-      when 'PUBLISHED' then exit(0)
+      when 'VALIDATED', 'PUBLISHED' then break
       when 'FAILED' then raise "Deployment failed: #{status['errors']}"
       end
-      sleep(5)
-    end
-    raise 'Timed out waiting for validation' unless status['deploymentState'] == 'VALIDATED'
+      sleep(delay)
+    rescue StandardError => e
+      warn "API error (attempt #{attempt + 1}/#{max_attempts}): #{e.message}"
+      sleep(delay) unless attempt == max_attempts - 1
+    end
+
+    return if status['deploymentState'] == 'PUBLISHED'
+
+    raise "Timed out after #{(max_attempts * delay) / 60} minutes waiting for validation" unless state == 'VALIDATED'
 

Initialize the status variable to an empty hash before the 60.times loop to
prevent a NameError if the sonatype_api_post call fails.

Rakefile [1191-1207]

+status = {}
 60.times do
   status = sonatype_api_post("https://central.sonatype.com/api/v1/publisher/status?id=#{deployment_id}", token)
   state = status['deploymentState']
   puts "Deployment state: #{state}"
 
   case state
   when 'VALIDATED' then break
   when 'PUBLISHED' then exit(0)
   when 'FAILED' then raise "Deployment failed: #{status['errors']}"
   end
   sleep(5)
 end
 raise 'Timed out waiting for validation' unless status['deploymentState'] == 'VALIDATED'
 
 expected = java_release_targets.size
 actual = status['purls']&.size || 0
 raise "Expected #{expected} packages but found #{actual}" if actual != expected

[Suggestion processed]

Suggestion importance[1-10]: 6

__

Why: This suggestion correctly identifies a potential NameError if the sonatype_api_post call fails within the loop, preventing status from being initialized. Initializing status before the loop is a valid fix for this edge case.

Low
Learned
best practice
Validate auth and harden HTTP calls
Suggestion Impact:The commit updated the Sonatype Authorization header from "Bearer" to "Basic" and changed response parsing to use `res.body.to_s.empty?` as suggested. However, it did not add the suggested token blank/strip guard nor add open/read timeouts to the Sonatype POST request.

code diff:

 def sonatype_api_post(url, token)
   uri = URI(url)
   req = Net::HTTP::Post.new(uri)
-  req['Authorization'] = "Bearer #{token}"
+  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.empty? ? {} : JSON.parse(res.body)
+  res.body.to_s.empty? ? {} : JSON.parse(res.body)
 end

Guard against blank tokens and add timeouts when calling the external API; also
ensure the authorization scheme matches the credential you’re passing (e.g.,
don’t label a Base64 basic token as Bearer).

Rakefile [431-440]

 def sonatype_api_post(url, token)
+  token = token&.strip
+  raise 'Sonatype token required' if token.nil? || token.empty?
+
   uri = URI(url)
   req = Net::HTTP::Post.new(uri)
-  req['Authorization'] = "Bearer #{token}"
+  req['Authorization'] = "Basic #{token}"
 
-  res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
+  res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true,
+                                                open_timeout: 10, read_timeout: 30) { |http| http.request(req) }
   raise "Sonatype API error (#{res.code}): #{res.body}" unless res.is_a?(Net::HTTPSuccess)
 
-  res.body.empty? ? {} : JSON.parse(res.body)
+  res.body.to_s.empty? ? {} : JSON.parse(res.body)
 end

[Suggestion processed]

Suggestion importance[1-10]: 6

__

Why:
Relevant best practice - Add explicit validation/guards for environment-based credentials and external API calls (auth scheme correctness and request robustness).

Low
Encode and validate URL parameters
Suggestion Impact:The commit now strips the deployment_id and URL-encodes it using URI.encode_www_form_component before interpolating it into Sonatype API URLs (status and deployment endpoints). It also improved the missing-ID error message, but did not adopt the exact URI.encode_www_form query construction shown in the suggestion.

code diff:

   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)
-    raise 'Deployment ID required' if deployment_id.nil? || deployment_id.empty?
+    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')}")
 
-    60.times do
-      status = sonatype_api_post("https://central.sonatype.com/api/v1/publisher/status?id=#{deployment_id}", token)
+    encoded_id = URI.encode_www_form_component(deployment_id.strip)
+    status = {}
+    max_attempts = 60
+    delay = 5
+    max_attempts.times do |attempt|
+      status = sonatype_api_post("https://central.sonatype.com/api/v1/publisher/status?id=#{encoded_id}", token)
       state = status['deploymentState']
       puts "Deployment state: #{state}"
 
       case state
-      when 'VALIDATED' then break
-      when 'PUBLISHED' then exit(0)
+      when 'VALIDATED', 'PUBLISHED' then break
       when 'FAILED' then raise "Deployment failed: #{status['errors']}"
       end
-      sleep(5)
-    end
-    raise 'Timed out waiting for validation' unless status['deploymentState'] == 'VALIDATED'
+      sleep(delay)
+    rescue StandardError => e
+      warn "API error (attempt #{attempt + 1}/#{max_attempts}): #{e.message}"
+      sleep(delay) unless attempt == max_attempts - 1
+    end
+
+    return if status['deploymentState'] == '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
-    raise "Expected #{expected} packages but found #{actual}" if actual != expected
+    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/#{deployment_id}", token)
+    sonatype_api_post("https://central.sonatype.com/api/v1/publisher/deployment/#{encoded_id}", token)
     puts "Published! Deployment ID: #{deployment_id}"

Strip and validate deployment_id, and URL-encode it (or use URI.encode_www_form)
when building the Sonatype status URL to avoid malformed requests or injection
via special characters.

Rakefile [1185-1192]

-deployment_id = arguments[:deployment_id] || ENV.fetch('DEPLOYMENT_ID', nil)
+deployment_id = (arguments[:deployment_id] || ENV.fetch('DEPLOYMENT_ID', nil))&.strip
 raise 'Deployment ID required' if deployment_id.nil? || deployment_id.empty?
 ...
 60.times do
-  status = sonatype_api_post("https://central.sonatype.com/api/v1/publisher/status?id=#{deployment_id}", token)
+  query = URI.encode_www_form(id: deployment_id)
+  status = sonatype_api_post("https://central.sonatype.com/api/v1/publisher/status?#{query}", token)
Suggestion importance[1-10]: 5

__

Why:
Relevant best practice - Add explicit validation and sanitization/encoding at integration boundaries (e.g., URL parameters) before use.

Low

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the Java release process with improved timeout handling, deployment validation, and automatic publishing capabilities. It also adds comprehensive package verification tasks for all language bindings (Node.js, Python, Ruby, .NET, and Java) to confirm successful publication to their respective package registries.

Changes:

  • Added package_published? and sonatype_api_post helper functions for deployment verification
  • Increased Sonatype API timeout from 60s to 180s and added better error handling
  • Created verification tasks for all five language bindings that check package availability on registries
  • Added java:publish_deployment task to handle manual deployment publishing by ID

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 5 comments.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 6 comments.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

@titusfortner titusfortner merged commit 895db10 into trunk Jan 19, 2026
20 checks passed
@titusfortner titusfortner deleted the pr/rakefile-build-verification branch January 19, 2026 22:33
This was referenced Feb 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

B-build Includes scripting, bazel and CI integrations Review effort 3/5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants