Workflow:SeleniumHQ Selenium Contributor Development Workflow
| Knowledge Sources | |
|---|---|
| Domains | Development, Open_Source, Bazel_Build |
| Last Updated | 2026-02-11 23:00 GMT |
Overview
End-to-end process for contributing code changes to the Selenium project, from setting up a local development environment through building, testing, formatting, and submitting a pull request for the Bazel-based polyglot monorepo.
Description
This workflow covers the complete contributor journey for the Selenium monorepo. The project uses Bazel as its build system to manage the polyglot codebase (Java, Python, Ruby, JavaScript, .NET, Rust) with hermetic toolchains. Contributors fork the repository, set up a local development environment with Bazelisk, make targeted changes in the appropriate language binding, run tests and formatters, and submit pull requests that are squashed onto the trunk branch.
Key outputs:
- A tested, formatted code change ready for review
- A pull request targeting the trunk branch
- CI-passing build with no lint or formatting errors
Scope:
- Covers environment setup for all platforms (macOS, Windows, Linux)
- Applies to changes across any language binding or shared component
- Includes the Rake/Go wrapper tasks and Bazel-native commands
- Covers formatting, linting, and testing workflows
Usage
Execute this workflow when you want to contribute a bug fix, new feature, or improvement to any component of the Selenium project. This is the standard path for both first-time contributors and experienced committers working on the monorepo.
Execution Steps
Step 1: Set Up Development Environment
Install the required toolchain and clone the repository. Bazelisk is the only required build tool; it handles downloading the correct Bazel version automatically.
What happens:
- Install Bazelisk (Bazel wrapper) which reads .bazelversion for the correct Bazel version
- Install Java JDK 17+ and set the JAVA_HOME environment variable
- Platform-specific setup: Xcode CLI tools on macOS, MSYS2 and Visual Studio on Windows
- Clone the repository (use --depth 1 for faster initial clone)
- Set up remote tracking for the upstream repository
Key considerations:
- The project provides alternative environments: GitPod (cloud-based), Dev Containers (Docker-based), and Docker images
- Windows requires additional setup for long file paths, MSYS2 bash, and Visual C++ build tools
- Rosetta is required on Apple Silicon Macs (add build --host_platform=//:rosetta to .bazelrc.local)
- No language-specific local development tools are needed; Bazel provides hermetic toolchains
Step 2: Create Feature Branch
Create a feature branch from the latest trunk. The project uses HEAD-based development with all changes applied directly on top of trunk.
What happens:
- Fetch the latest upstream changes
- Create a new branch from upstream/trunk
- The branch name should describe the change (e.g., fix/stale-element-handling, feature/bidi-support)
Key considerations:
- PRs are squashed to trunk, so commit history on the branch is flexible
- One feature or fix per PR; avoid mixing unrelated changes
- Keep diffs small and reversible; avoid repo-wide refactors
Step 3: Make Code Changes
Edit source files in the appropriate language binding directory, following the language-specific conventions documented in each binding AGENTS.md.
What happens:
- Navigate to the relevant binding directory (java/, py/, rb/, javascript/selenium-webdriver/, dotnet/, rust/)
- Read the language-specific AGENTS.md for coding conventions and test patterns
- Make targeted changes that maintain API/ABI compatibility
- For cross-binding changes, compare behavior with at least one other binding
Key considerations:
- Maintain API/ABI compatibility so users can upgrade by changing only the version number
- Prefer small, reversible diffs over large sweeping changes
- Comments should explain why, not what; prefer well-named methods over comments
- Avoid over-engineering; only change what is directly needed
- High-risk areas (WebDriver/BiDi semantics, capability parsing, Grid routing, MODULE.bazel) require extra care
Step 4: Build and Verify
Build the changed components using Bazel to verify compilation succeeds. Use bazel query to discover build targets before building.
What happens:
- Use bazel query to locate the build targets for changed files
- Build the specific targets affected by the change
- The ./go wrapper provides shortcuts for common build commands (e.g., ./go java:build)
- For local Java development, ./go java:install deploys to the local Maven repository
Key considerations:
- Prefer targeted Bazel commands over building everything
- Each buildable module is defined in a BUILD.bazel file
- Targets are referenced with // prefix, relative path, colon, and target name
- Language-specific local development setup may be needed for IDE integration
Step 5: Run Tests
Execute the relevant test suite to verify the change does not break existing functionality and that new behavior is covered by tests.
What happens:
- Run unit tests first (small size filter) for fast feedback
- Run integration tests (medium) for deeper verification
- Run browser tests (large) with the appropriate browser tag for full validation
- Use --test_output=all to see console output and --cache_test_results=no to force re-runs
Key considerations:
- Prefer writing a test first before implementing the fix (test-first approach)
- Prefer small (unit) tests over browser tests for speed and reliability
- Avoid mocks; they can misrepresent API contracts
- Each language has specific test commands documented in README.md and AGENTS.md
- Use --test_size_filters and --test_tag_filters to run targeted test subsets
Step 6: Format and Lint
Run the project formatting and linting tools to ensure code style compliance before committing. The format script handles all languages in the monorepo.
What happens:
- Run ./scripts/format.sh to check and fix formatting for changed files
- The script detects which files changed relative to trunk and runs the appropriate formatters
- Formatters include: buildifier (BUILD files), google-java-format (Java), prettier (JavaScript), rubocop (Ruby), rustfmt (Rust), ruff (Python), dotnet format (.NET)
- Copyright headers are verified on all source files
- Optional --lint flag runs additional linters (shellcheck, actionlint)
Key considerations:
- Run formatting before committing to prevent CI failures
- Use --all flag to format everything (not just changed files) if needed
- The --pre-push mode checks committed changes relative to trunk
- CI will reject PRs with formatting violations
Step 7: Submit Pull Request
Commit the changes, push to the fork, and create a pull request targeting the trunk branch.
What happens:
- Stage and commit changes with a descriptive commit message
- The commit message should be ~50 characters on the first line, with a blank second line and details wrapped at 72 characters
- Push the feature branch to the fork on GitHub
- Create a pull request with a clear description of the change and any related issue numbers
- Automated CI runs tests, and PR review bots (Qodo Merge) provide initial feedback
Key considerations:
- PRs are squashed to trunk, so focus on a clean PR description rather than individual commit messages
- Reference issue numbers with "Fixes #N" in the commit body
- The PR template asks for a description and test coverage information
- Label-based workflow (R: awaiting reviewer, R: needs code changes, etc.) tracks review state
- Committers will integrate the PR by squash-merging to trunk