Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/bazel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ on:
required: false
type: number
default: 1
cache-name:
description: Name for cache restore (restores {name}.gz with key {name}-)
required: false
type: string
default: ''

jobs:
bazel:
Expand Down Expand Up @@ -100,6 +105,13 @@ jobs:
run: git pull origin "$GIT_REF"
env:
GIT_REF: ${{ github.ref }}
- name: Restore cache
if: inputs.cache-name != ''
uses: actions/cache/restore@v4
with:
path: ${{ inputs.cache-name }}
key: ${{ inputs.cache-name }}-
Comment thread
titusfortner marked this conversation as resolved.
restore-keys: ${{ inputs.cache-name }}-
- name: Free space
if: inputs.os != 'windows'
run: ./scripts/github-actions/free-disk-space.sh
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/ci-build-index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
uses: ./.github/workflows/bazel.yml
with:
name: Build Test Index
os: macos
os: ubuntu
run: ./go bazel:build_test_index bazel-test-target-index
artifact-name: bazel-test-target-index
artifact-path: bazel-test-target-index
Expand All @@ -21,6 +21,7 @@ jobs:
name: Cache Index
needs: build
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Download index
uses: actions/download-artifact@v4
Expand Down
89 changes: 41 additions & 48 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ permissions:
jobs:
build-index:
name: Build Index
if: github.event_name == 'schedule'
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
uses: ./.github/workflows/ci-build-index.yml

check:
Expand All @@ -31,8 +31,22 @@ jobs:
uses: ./.github/workflows/bazel.yml
with:
name: Check Targets
run: COMMIT_RANGE="${{ github.event.pull_request.base.sha || github.event.before }}...${{ github.event.pull_request.head.sha || github.sha }}" ./scripts/github-actions/check-bazel-targets.sh
cache-name: bazel-test-target-index
fetch-depth: 50
run: |
if [ "${{ github.event_name }}" == "schedule" ] || \
[ "${{ github.event_name }}" == "workflow_call" ] || \
[ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "Running all targets for ${{ github.event_name }} event"
echo "//java/... //py:* //rb/... //dotnet/... //rust/..." > bazel-targets.txt
else
BASE_SHA="${{ github.event.pull_request.base.sha || github.event.before }}"
if [ -n "$BASE_SHA" ]; then
./go bazel:affected_targets "${BASE_SHA}..HEAD" bazel-test-target-index
else
./go bazel:affected_targets bazel-test-target-index
Comment thread
titusfortner marked this conversation as resolved.
fi
fi
artifact-name: check-targets
artifact-path: bazel-targets.txt

Expand All @@ -41,83 +55,62 @@ jobs:
needs: check
runs-on: ubuntu-latest
outputs:
targets: ${{ steps.read.outputs.targets }}
java: ${{ steps.read.outputs.java }}
py: ${{ steps.read.outputs.py }}
rb: ${{ steps.read.outputs.rb }}
dotnet: ${{ steps.read.outputs.dotnet }}
rust: ${{ steps.read.outputs.rust }}
steps:
- name: Download targets
uses: actions/download-artifact@v4
with:
name: check-targets
- name: Read targets
id: read
env:
COMMIT_MESSAGES: ${{ join(github.event.commits.*.message, ' ') }}
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
if [ -s bazel-targets.txt ]; then
{
echo "targets<<EOF"
cat bazel-targets.txt
echo "EOF"
} >> "$GITHUB_OUTPUT"
else
echo "targets=" >> "$GITHUB_OUTPUT"
fi

targets=$(cat bazel-targets.txt 2>/dev/null || echo "")
check_binding() {
local pattern=$1 tag=$2
if [[ "$targets" == *"$pattern"* ]] || [[ "$COMMIT_MESSAGES" == *"[$tag]"* ]] || [[ "$PR_TITLE" == *"[$tag]"* ]]; then
echo "$tag=true" >> "$GITHUB_OUTPUT"
fi
}
check_binding "//java/" "java"
check_binding "//py:" "py"
check_binding "//rb/" "rb"
check_binding "//dotnet/" "dotnet"
check_binding "//rust/" "rust"
Comment thread
titusfortner marked this conversation as resolved.
dotnet:
name: .NET
needs: read-targets
uses: ./.github/workflows/ci-dotnet.yml
if: >
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
github.event_name == 'workflow_call' ||
contains(needs.read-targets.outputs.targets, '//dotnet') ||
contains(join(github.event.commits.*.message), '[dotnet]') ||
contains(github.event.pull_request.title, '[dotnet]')
if: needs.read-targets.outputs.dotnet != ''

java:
name: Java
needs: read-targets
uses: ./.github/workflows/ci-java.yml
if: >
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
github.event_name == 'workflow_call' ||
contains(needs.read-targets.outputs.targets, '//java') ||
contains(join(github.event.commits.*.message), '[java]') ||
contains(github.event.pull_request.title, '[java]')
if: needs.read-targets.outputs.java != ''

python:
name: Python
needs: read-targets
uses: ./.github/workflows/ci-python.yml
if: >
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
github.event_name == 'workflow_call' ||
contains(needs.read-targets.outputs.targets, '//py') ||
contains(join(github.event.commits.*.message), '[py]') ||
contains(github.event.pull_request.title, '[py]')
if: needs.read-targets.outputs.py != ''

ruby:
name: Ruby
needs: read-targets
uses: ./.github/workflows/ci-ruby.yml
if: >
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
github.event_name == 'workflow_call' ||
contains(needs.read-targets.outputs.targets, '//rb') ||
contains(join(github.event.commits.*.message), '[rb]') ||
contains(github.event.pull_request.title, '[rb]')
if: needs.read-targets.outputs.rb != ''

rust:
name: Rust
needs: read-targets
uses: ./.github/workflows/ci-rust.yml
secrets:
SELENIUM_CI_TOKEN: ${{ secrets.SELENIUM_CI_TOKEN }}
if: >
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
github.event_name == 'workflow_call' ||
contains(needs.read-targets.outputs.targets, '//rust') ||
contains(join(github.event.commits.*.message), '[rust]') ||
contains(github.event.pull_request.title, '[rust]')
if: needs.read-targets.outputs.rust != ''
139 changes: 136 additions & 3 deletions rake_tasks/bazel.rake
Original file line number Diff line number Diff line change
@@ -1,12 +1,52 @@
# frozen_string_literal: true

require 'json'
require 'set'

# ./go bazel:build_test_index --> 'build/bazel-test-target-index.json'
# ./go bazel:build_test_index index.json --> 'index.json'
# ./go bazel:affected_targets --> HEAD^..HEAD with default index
# ./go bazel:affected_targets abc123..def456 --> explicit range
# ./go bazel:affected_targets abc123..def456 my-index --> explicit range with custom index
# ./go bazel:affected_targets my-index --> HEAD^..HEAD with custom index
desc 'Find test targets affected by changes between revisions'
task :affected_targets do |_task, args|
values = args.to_a
index_file = values.find { |value| File.exist?(value) }
range = (values - [index_file]).first || 'HEAD'
index_file ||= 'build/bazel-test-target-index'

base_rev, head_rev = if range.include?('..')
range.split('..', 2)
else
["#{range}^", range]
end

puts "Commit range: #{base_rev}..#{head_rev}"

changed_files = `git diff --name-only #{base_rev} #{head_rev}`.split("\n").map(&:strip).reject(&:empty?)
puts "Changed files: #{changed_files.size}"

targets = if File.exist?(index_file)
affected_targets_with_index(changed_files, index_file)
else
puts 'No index found, using directory-based fallback'
affected_targets_fallback(changed_files)
end

if targets.empty?
puts 'No test targets affected'
File.write('bazel-targets.txt', '')
else
puts "Found #{targets.size} affected test targets"
File.write('bazel-targets.txt', targets.sort.join(' '))
targets.sort.each { |t| puts t }
end
end

# ./go bazel:build_test_index --> 'build/bazel-test-target-index'
# ./go bazel:build_test_index my-index --> 'my-index'
desc 'Build test target index for faster affected target lookup'
task :build_test_index, [:index_file] do |_task, args|
output = args[:index_file] || 'build/bazel-test-target-index.json'
output = args[:index_file] || 'build/bazel-test-target-index'

index = {}
tests = []
Expand Down Expand Up @@ -53,3 +93,96 @@ def bazel_label_to_package(label)
label = label.sub(%r{^@selenium//}, '').sub(%r{^//}, '')
label.split(':').first
end

def find_bazel_package(filepath)
path = File.dirname(filepath)
until path.empty?
return path if File.exist?(File.join(path, 'BUILD.bazel')) || File.exist?(File.join(path, 'BUILD'))
return nil if path == '.'

path = File.dirname(path)
end
nil
end

def affected_targets_with_index(changed_files, index_file)
puts "Using index: #{index_file}"
begin
index = JSON.parse(File.read(index_file))
rescue JSON::ParserError => e
puts "Invalid JSON in index file: #{e.message}"
return affected_targets_fallback(changed_files)
end

test_files, lib_files = changed_files.partition { |f| f.match?(%r{[_-]test\.rb$|_test\.py$|Test\.java$|Tests?\.cs$|\.test\.[jt]s$|_spec\.rb$}) }

affected = Set.new
affected.merge(targets_from_tests(test_files))

lib_files.each do |filepath|
pkg = find_bazel_package(filepath)
affected.merge(targets_from_lookup(pkg, index, filepath))
end

affected.to_a
end

def targets_from_lookup(pkg, index, filepath)
# ignore files not associated with bazel package
return [] if pkg.nil?

# Root package is empty string, not '.'
pkg = '' if pkg == '.'

# generate targets if package not in the index
test_targets = index[pkg] || query_package_dep(pkg)

# dotnet tests depend on java server, but there are no remote tests, so safe to ignore
filepath.start_with?('java/') ? test_targets.reject { |t| t.start_with?('//dotnet/') } : test_targets
end

def query_package_dep(pkg)
# Root package is empty string, not '.'
pkg = '' if pkg == '.'
puts "Package not in index, querying deps: //#{pkg}"
targets = []
Bazel.execute('query', ['--output=label'], "kind('.*_test', deps(//#{pkg}:all))") do |out|
targets = out.lines.map(&:strip).select { |l| l.start_with?('//') }
end
targets
end

def targets_from_tests(test_files)
test_files.select! { |f| File.exist?(f) }
return [] if test_files.empty?

query = test_files.filter_map { |f|
pkg = find_bazel_package(f)
next if pkg.nil?

# Bazel srcs often use paths relative to the package, not basenames.
rel = f.sub(%r{^#{Regexp.escape(pkg)}/}, '')
"attr(srcs, '#{rel}', //#{pkg}:*)"
}.join(' + ')

return [] if query.empty?

targets = []
Bazel.execute('query', ['--output=label'], "kind('.*_test', #{query})") do |out|
targets = out.lines.map(&:strip).select { |l| l.start_with?('//') }
end
targets
end

def affected_targets_fallback(changed_files)
targets = Set.new
top_level_dirs = changed_files.map { |f| f.split('/').first }.uniq

return BINDING_TARGETS.values if top_level_dirs.intersect?(%w[common rust])

top_level_dirs.each do |dir|
targets << BINDING_TARGETS[dir] if BINDING_TARGETS[dir]
end

targets.to_a
end
Loading