Skip to content

[build] use rulesets to restrict and unrestrict trunk during release window#16941

Merged
titusfortner merged 7 commits intotrunkfrom
ruleset
Jan 18, 2026
Merged

[build] use rulesets to restrict and unrestrict trunk during release window#16941
titusfortner merged 7 commits intotrunkfrom
ruleset

Conversation

@titusfortner
Copy link
Member

@titusfortner titusfortner commented Jan 18, 2026

User description

🔗 Related Issues

Addition to #16937 to prevent commits accidentally being added to release

💥 What does this PR do?

We want to make sure the PR that kicks off the automated release process was tested against trunk
instead of getting squash merged on top of it. It's also nice to be able to enforce no
changes to trunk when actively working on the release.

I've created 2 rulesets as part of this:

  1. Only Release Managers (currently TLC) can merge PRs or commit to trunk during the release process window
  2. No one can merge a PR that isn't up to date with trunk

The rulesets are automatically enabled when the release preparation PR comes out of Draft
and is marked "Ready for Review". They are automatically disabled when the release workflow
completes, or if the release preparation PR is closed without merging (abandoned release).

This can be manually toggled via the "Manage Trunk Restrictions" workflow with
production environment approval (currently TLC members) or directly in repo settings:
https://github.com/SeleniumHQ/selenium/settings/rules

🔧 Implementation Notes

A single consolidated manage-trunk job dynamically determines enforcement level:

  • Uses inputs.enforcement when manually triggered or called as reusable workflow
  • Uses active for ready_for_review PR events
  • Uses disabled for closed (unmerged) PR events
  • Unrestrict in release.yml runs with if: always() to ensure cleanup even on failure Unrestrict only runs if release is successful and version updates need to happen. If it is not successful, release managers will need to manually fix and run the unrestrict job. I'm adding a slack notification if it fails and needs to be fixed.

💡 Additional Considerations

  • Hardcoded ruleset IDs (11911909, 11912022) are specific to this repo - update if
    rulesets are recreated
  • All workflows skip on forks (no secrets, no rulesets)

🔄 Types of changes

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

PR Type

Enhancement


Description

  • Implement GitHub rulesets to restrict trunk during release window

  • Automatically enable restrictions when release PR marked ready

  • Automatically disable restrictions when release completes

  • Add manual workflow to toggle trunk restrictions via approval

  • Skip workflows on forked repositories


Diagram Walkthrough

flowchart LR
  A["Release Prep PR"] -->|ready_for_review| B["Enable Rulesets"]
  B -->|restrict trunk| C["Only Release Managers can merge"]
  D["Release Workflow"] -->|completes| E["Disable Rulesets"]
  F["Manual Trigger"] -->|with approval| E
  G["PR Closed Unmerged"] -->|abandoned| E
Loading

File Walkthrough

Relevant files
Enhancement
restrict-trunk.yml
New workflow to manage trunk restrictions                               

.github/workflows/restrict-trunk.yml

  • New workflow to manage trunk branch rulesets enforcement
  • Triggered on release-preparation PR ready_for_review and closed events
  • Supports manual workflow_dispatch and reusable workflow_call
    invocations
  • Dynamically determines enforcement level (active/disabled) based on
    event type
  • Updates two hardcoded rulesets (11911909, 11912022) via GitHub API
+49/-0   
pre-release.yml
Add fork check to pre-release workflow                                     

.github/workflows/pre-release.yml

  • Add fork check to skip workflow on forked repositories
  • Prevents execution of update-rust job on non-official forks
+1/-0     
release.yml
Add unrestrict-trunk job to release workflow                         

.github/workflows/release.yml

  • Add new unrestrict-trunk job that calls restrict-trunk workflow
  • Job runs with if: always() to ensure cleanup even on failure
  • Disables rulesets after github-release job completes
  • Update update-version job dependency to include unrestrict-trunk
+10/-1   

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

qodo-code-review bot commented Jan 18, 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: 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: Secure Error Handling

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

Status: Passed

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: 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:
Unvalidated enforcement input: The workflow_call input enforcement is typed as a free-form string and is used directly to
update ruleset enforcement without restricting it to allowed values (active/disabled).

Referred Code
workflow_call:
  inputs:
    enforcement:
      description: 'Ruleset enforcement level'
      required: true
      type: string

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

Generic: Comprehensive Audit Trails

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

Status:
Missing explicit audit log: The workflow changes repository ruleset enforcement via the GitHub API but does not add an
explicit log entry/summary capturing actor, ruleset IDs, enforcement value, and outcome
beyond default GitHub Actions logs.

Referred Code
manage-trunk:
  name: Manage Trunk Branch
  runs-on: ubuntu-latest
  if: |
    github.event.repository.fork == false &&
    (inputs.enforcement ||
     (startsWith(github.event.pull_request.head.ref, 'release-preparation-') &&
      (github.event.action == 'ready_for_review' ||
       (github.event.action == 'closed' && github.event.pull_request.merged == false))))
  strategy:
    matrix:
      ruleset_id: [11911909, 11912022]
  env:
    ENFORCEMENT: ${{ inputs.enforcement || (github.event.action == 'ready_for_review' && 'active') || 'disabled' }}
  steps:
    - name: Update ruleset enforcement
      uses: octokit/request-action@v2.x
      with:
        route: PUT /repos/{owner}/{repo}/rulesets/{ruleset_id}
        owner: ${{ github.repository_owner }}
        repo: ${{ github.event.repository.name }}


 ... (clipped 3 lines)

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:
No failure fallback: The ruleset update step relies on octokit/request-action failing the job but does not add
retries or a fallback path if the GitHub API call fails, which could leave trunk
restrictions in an unintended state.

Referred Code
strategy:
  matrix:
    ruleset_id: [11911909, 11912022]
env:
  ENFORCEMENT: ${{ inputs.enforcement || (github.event.action == 'ready_for_review' && 'active') || 'disabled' }}
steps:
  - name: Update ruleset enforcement
    uses: octokit/request-action@v2.x
    with:
      route: PUT /repos/{owner}/{repo}/rulesets/{ruleset_id}
      owner: ${{ github.repository_owner }}
      repo: ${{ github.event.repository.name }}
      ruleset_id: ${{ matrix.ruleset_id }}
      enforcement: ${{ env.ENFORCEMENT }}
    env:

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 18, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Guard and wrap multiline if
Suggestion Impact:The commit added a non-PR guard by checking `github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call'` before evaluating the pull_request-specific expression, which prevents null `github.event.pull_request` access on those triggers. However, it did not implement the suggested `${{ }}` wrapping and used an event_name-based guard rather than `github.event.pull_request && ...`.

code diff:

     if: |
       github.event.repository.fork == false &&
-      (inputs.enforcement ||
+      (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call' ||
        (startsWith(github.event.pull_request.head.ref, 'release-preparation-') &&
         (github.event.action == 'ready_for_review' ||
          (github.event.action == 'closed' && github.event.pull_request.merged == false))))

Wrap the multiline if condition in ${{ }} and add a check for
github.event.pull_request to prevent errors when the workflow is triggered by
events other than a pull request.

.github/workflows/restrict-trunk.yml [28-33]

-if: |
+if: ${{ 
   github.event.repository.fork == false &&
-  (inputs.enforcement ||
-   (startsWith(github.event.pull_request.head.ref, 'release-preparation-') &&
-    (github.event.action == 'ready_for_review' ||
-     (github.event.action == 'closed' && github.event.pull_request.merged == false))))
+  (
+    inputs.enforcement ||
+    (
+      github.event.pull_request &&
+      startsWith(github.event.pull_request.head.ref, 'release-preparation-') &&
+      (
+        github.event.action == 'ready_for_review' ||
+        (github.event.action == 'closed' && github.event.pull_request.merged == false)
+      )
+    )
+  )
+}}

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies a potential runtime error when the workflow is triggered by workflow_dispatch or workflow_call, as github.event.pull_request would be null. Adding a guard makes the workflow more robust.

Medium
Dynamically fetch ruleset IDs by name

Avoid hardcoding ruleset IDs by adding a new job to dynamically fetch them by
name at runtime. This will make the workflow more robust against changes to the
ruleset IDs.

.github/workflows/restrict-trunk.yml [34-36]

-strategy:
-  matrix:
-    ruleset_id: [11911909, 11912022]
+jobs:
+  get-ruleset-ids:
+    name: Get Ruleset IDs
+    runs-on: ubuntu-latest
+    outputs:
+      ruleset_ids: ${{ steps.get_ids.outputs.ruleset_ids }}
+    steps:
+      - name: Get ruleset IDs
+        id: get_ids
+        uses: octokit/request-action@v2.x
+        with:
+          route: GET /repos/{owner}/{repo}/rulesets
+          owner: ${{ github.repository_owner }}
+          repo: ${{ github.event.repository.name }}
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      - run: |
+          ruleset_ids=$(echo '${{ steps.get_ids.outputs.data }}' | jq '[.[] | select(.name=="Trunk" or .name=="Trunk PR").id]')
+          echo "ruleset_ids=$ruleset_ids" >> $GITHUB_OUTPUT
 
+  manage-trunk:
+    name: Manage Trunk Branch
+    needs: get-ruleset-ids
+    ...
+    strategy:
+      matrix:
+        ruleset_id: ${{ fromJson(needs.get-ruleset-ids.outputs.ruleset_ids) }}
+

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: This is a valuable suggestion that improves the workflow's robustness and maintainability by replacing hardcoded ruleset_id values with a dynamic lookup, preventing future breakages if the rulesets are recreated.

Medium
Learned
best practice
Validate enforcement input values
Suggestion Impact:The commit removed the free-form `enforcement` string input entirely and replaced it with a boolean `restrict` input (for both workflow_dispatch and workflow_call). It then derives the API `enforcement` value from that boolean (`active` if restricted, otherwise `disabled`), which effectively prevents invalid enforcement values without needing explicit string validation.

code diff:

     inputs:
-      enforcement:
-        description: 'Ruleset enforcement level'
+      restrict:
+        description: 'Restrict trunk branch'
         required: true
-        type: choice
-        options:
-          - active
-          - disabled
+        type: boolean
   workflow_call:
     inputs:
-      enforcement:
-        description: 'Ruleset enforcement level'
+      restrict:
+        description: 'Restrict trunk branch'
         required: true
-        type: string
+        type: boolean
 
 jobs:
   manage-trunk:
@@ -27,24 +30,26 @@
     runs-on: ubuntu-latest
     if: |
       github.event.repository.fork == false &&
-      (inputs.enforcement ||
+      (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call' ||
        (startsWith(github.event.pull_request.head.ref, 'release-preparation-') &&
         (github.event.action == 'ready_for_review' ||
          (github.event.action == 'closed' && github.event.pull_request.merged == false))))
     strategy:
       matrix:
-        ruleset_id: [11911909, 11912022]
+        ruleset_id:
+          - 11911909  # Release In Progress Access (restrict updates to trunk to release managers)
+          - 11912022  # Release In Progress Flow (requires branches to be up to date before merging)
     env:
-      ENFORCEMENT: ${{ inputs.enforcement || (github.event.action == 'ready_for_review' && 'active') || 'disabled' }}
+      TRUNK_RESTRICTED: ${{ inputs.restrict || github.event.action == 'ready_for_review' }}
     steps:
       - name: Update ruleset enforcement
-        uses: octokit/request-action@v2.x
+        uses: octokit/request-action@v2.4.0
         with:
           route: PUT /repos/{owner}/{repo}/rulesets/{ruleset_id}
           owner: ${{ github.repository_owner }}
           repo: ${{ github.event.repository.name }}
           ruleset_id: ${{ matrix.ruleset_id }}
-          enforcement: ${{ env.ENFORCEMENT }}
+          enforcement: ${{ env.TRUNK_RESTRICTED == 'true' && 'active' || 'disabled' }}

For workflow_call (string input), explicitly validate inputs.enforcement is one
of the allowed values before using it in the API request to prevent invalid
enforcement settings.

.github/workflows/restrict-trunk.yml [37-38]

 env:
-  ENFORCEMENT: ${{ inputs.enforcement || (github.event.action == 'ready_for_review' && 'active') || 'disabled' }}
+  ENFORCEMENT: ${{ (inputs.enforcement == 'active' || inputs.enforcement == 'disabled') && inputs.enforcement || (github.event.action == 'ready_for_review' && 'active') || 'disabled' }}

[Suggestion processed]

Suggestion importance[1-10]: 6

__

Why:
Relevant best practice - Validate and sanitize external inputs (workflow inputs/env) before using them to call external APIs.

Low
  • Update

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 pull request implements automated GitHub ruleset management to restrict trunk branch access during the Selenium release process. The restrictions ensure that only release managers can merge changes and that all PRs are up-to-date with trunk during the critical release window.

Changes:

  • Added a new reusable workflow to manage trunk branch rulesets that activates when release preparation PRs are marked ready and deactivates when they're closed without merging or when releases complete
  • Integrated automatic trunk unrestriction into the release workflow to restore normal branch access after successful releases
  • Added fork repository checks to skip workflows on forked repositories where secrets are unavailable

Reviewed changes

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

File Description
.github/workflows/restrict-trunk.yml New workflow that manages two trunk branch rulesets via GitHub API, supporting both automatic triggering from PR events and manual control via workflow dispatch
.github/workflows/release.yml Adds unrestrict-trunk job with if: always() condition to disable trunk restrictions after release completion, making it a dependency for the version update job
.github/workflows/pre-release.yml Adds fork check to update-rust job to prevent execution on forked repositories

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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 3 out of 3 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

titusfortner and others added 2 commits January 18, 2026 13:05
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
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 3 out of 3 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@titusfortner titusfortner merged commit bcd0976 into trunk Jan 18, 2026
20 checks passed
@titusfortner titusfortner deleted the ruleset branch January 18, 2026 20:19
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 2/5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants