Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.2
with:
fetch-depth: 0 # Required for rebasing
- name: Rebase on dependency PR
uses: ./actions/rebase_on_dependency
- name: Free Disk Space (Ubuntu)
uses: ./actions/free_disk_space
- uses: bazel-contrib/setup-bazel@0.18.0
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/build_and_test_host.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.2
with:
fetch-depth: 0 # Required for rebasing
- name: Rebase on dependency PR
uses: ./actions/rebase_on_dependency
- name: Free Disk Space (Ubuntu)
uses: ./actions/free_disk_space
- uses: bazel-contrib/setup-bazel@0.18.0
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/build_and_test_qnx.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ jobs:
with:
ref: ${{ github.head_ref || github.event.pull_request.head.ref || github.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
fetch-depth: 0 # Required for rebasing
- name: Rebase on dependency PR
uses: ./actions/rebase_on_dependency
- name: Free Disk Space (Ubuntu)
uses: ./actions/free_disk_space
- name: Setup Bazel
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/coverage_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v4.2.2

with:
fetch-depth: 0 # Required for rebasing
- name: Rebase on dependency PR
uses: ./actions/rebase_on_dependency
- name: Free Disk Space (Ubuntu)
uses: ./actions/free_disk_space

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/thread_sanitizer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.2
with:
fetch-depth: 0 # Required for rebasing
- name: Rebase on dependency PR
uses: ./actions/rebase_on_dependency
- name: Free Disk Space (Ubuntu)
uses: ./actions/free_disk_space
- uses: bazel-contrib/setup-bazel@0.18.0
Expand Down
40 changes: 40 additions & 0 deletions actions/rebase_on_dependency/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

load("@rules_python//python:defs.bzl", "py_library", "py_test")

py_library(
name = "find_dependencies_lib",
srcs = ["scripts/find_dependencies.py"],
imports = ["scripts"],
)

py_library(
name = "rebase_on_prs_lib",
srcs = ["scripts/rebase_on_prs.py"],
imports = ["scripts"],
)

py_test(
name = "find_dependencies_py_test",
srcs = ["test/find_dependencies_test.py"],
main = "test/find_dependencies_test.py",
deps = [":find_dependencies_lib"],
)

py_test(
name = "rebase_on_prs_py_test",
srcs = ["test/rebase_on_prs_test.py"],
main = "test/rebase_on_prs_test.py",
deps = [":rebase_on_prs_lib"],
)
97 changes: 97 additions & 0 deletions actions/rebase_on_dependency/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

name: "Rebase on Dependency PR"
description: "Rebases the current PR onto another PR specified via 'Depends-On: #<PR_NUMBER>' in the PR description"

inputs:
token:
description: "GitHub token with permissions to read PR information"
required: false
default: ${{ github.token }}

outputs:
dependency_prs:
description: "Comma-separated list of PR numbers that this PR depends on (if found)"
value: ${{ steps.find-dependency.outputs.dependency_prs }}
dependency_count:
description: "Number of dependency PRs found"
value: ${{ steps.find-dependency.outputs.dependency_count }}
rebased:
description: "Whether a rebase was performed"
value: ${{ steps.rebase.outputs.rebased }}
rebased_count:
description: "Number of PRs successfully rebased onto"
value: ${{ steps.rebase.outputs.rebased_count }}

runs:
using: composite
steps:
- name: Find dependency PR from description
id: find-dependency
shell: bash
env:
GH_TOKEN: ${{ inputs.token }}
EVENT_NAME: ${{ github.event_name }}
PR_NUMBER: ${{ github.event.pull_request.number }}
ACTION_PATH: ${{ github.action_path }}
run: python3 "${ACTION_PATH}/scripts/find_dependencies.py"

- name: Rebase on dependency PR
id: rebase
if: steps.find-dependency.outputs.dependency_prs != ''
shell: bash
env:
GH_TOKEN: ${{ inputs.token }}
DEPENDENCY_PRS: ${{ steps.find-dependency.outputs.dependency_prs }}
ACTION_PATH: ${{ github.action_path }}
run: python3 "${ACTION_PATH}/scripts/rebase_on_prs.py" "${DEPENDENCY_PRS}"

- name: Summary
if: always()
shell: bash
env:
DEPENDENCY_PRS: ${{ steps.find-dependency.outputs.dependency_prs || 0 }}
DEPENDENCY_COUNT: ${{ steps.find-dependency.outputs.dependency_count || 0}}
REBASED: ${{ steps.rebase.outputs.rebased || 0 }}
REBASED_COUNT: ${{ steps.rebase.outputs.rebased_count || 0 }}
SKIPPED_PRS: ${{ steps.rebase.outputs.skipped_prs || 0}}
FAILED_PRS: ${{ steps.rebase.outputs.failed_prs || 0}}
run: |
echo "### Dependency Check" >> $GITHUB_STEP_SUMMARY

if [[ -z "$DEPENDENCY_PRS" ]]; then
echo "No dependency found in PR description." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "To declare dependencies, add \`Depends-On: #<PR_NUMBER>\` to your PR description." >> $GITHUB_STEP_SUMMARY
echo "You can add multiple \`Depends-On:\` lines for multiple dependencies." >> $GITHUB_STEP_SUMMARY
else
echo "Found **${DEPENDENCY_COUNT}** dependency PR(s): ${DEPENDENCY_PRS}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY

if [[ "${REBASED_COUNT:-0}" -gt 0 ]]; then
echo "✅ Successfully rebased onto **${REBASED_COUNT}** PR(s)" >> $GITHUB_STEP_SUMMARY
fi

if [[ -n "$SKIPPED_PRS" ]]; then
echo "ℹ️ Skipped PRs (already merged or closed): ${SKIPPED_PRS}" >> $GITHUB_STEP_SUMMARY
fi

if [[ -n "$FAILED_PRS" ]]; then
echo "❌ Failed to rebase onto PR(s): ${FAILED_PRS}" >> $GITHUB_STEP_SUMMARY
fi

if [[ "$REBASED" != "true" && -z "$FAILED_PRS" ]]; then
echo "ℹ️ No rebase was needed (all dependency PRs are merged or closed)" >> $GITHUB_STEP_SUMMARY
fi
fi
110 changes: 110 additions & 0 deletions actions/rebase_on_dependency/scripts/find_dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env python3

# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

"""
find_dependencies.py
Parses a PR description and extracts Depends-On: references.

Reads EVENT_NAME, PR_NUMBER, GH_TOKEN from environment variables.
Calls 'gh pr view' to fetch the PR body.
Writes dependency_prs= and dependency_count= directly to $GITHUB_OUTPUT.
"""

import os
import re
import subprocess
import sys


def parse_depends_on(pr_body: str) -> list[str]:
"""Parse Depends-On patterns from PR body text.

Supports formats: "Depends-On: #123", "Depends-On: 123", "Depends-On:#123"
Case insensitive. Returns a list of PR number strings.
"""
if not pr_body:
return []
return re.findall(r"(?i)depends-on:\s*#?(\d+)", pr_body)


def get_pr_body(pr_number: str) -> str:
"""Fetch the PR body text using the GitHub CLI.

Raises:
RuntimeError: if the gh CLI exits with a non-zero return code.
"""
result = subprocess.run(
["gh", "pr", "view", pr_number, "--json", "body", "--jq", ".body"],
capture_output=True,
text=True,
)
if result.returncode != 0:
raise RuntimeError(
f"gh CLI failed for PR #{pr_number} (exit {result.returncode}): {result.stderr.strip()}"
)
return result.stdout.strip()


def write_github_output(output_path: str, prs: list[str]) -> None:
"""Write dependency_prs and dependency_count to the GITHUB_OUTPUT file."""
with open(output_path, "a", encoding="utf-8") as f:
f.write(f"dependency_prs={','.join(prs)}\n")
f.write(f"dependency_count={len(prs)}\n")


def main() -> int:
event_name = os.environ.get("EVENT_NAME", "")
pr_number = os.environ.get("PR_NUMBER", "")
output_path = os.environ.get("GITHUB_OUTPUT", os.devnull)

if event_name not in ("pull_request", "pull_request_target", "workflow_run"):
print("Not a pull request event, skipping dependency check")
write_github_output(output_path, [])
return 0

if not pr_number:
print("Could not determine PR number")
write_github_output(output_path, [])
return 0

print(f"Checking PR #{pr_number} for dependencies...")

try:
pr_body = get_pr_body(pr_number)
except RuntimeError as exc:
print(f"::error::{exc}")
write_github_output(output_path, [])
return 1

if not pr_body:
print("Could not fetch PR description or description is empty")
write_github_output(output_path, [])
return 0

dependency_prs = parse_depends_on(pr_body)

if not dependency_prs:
print("No 'Depends-On:' keyword found in PR description")
write_github_output(output_path, [])
return 0

print(f"Found {len(dependency_prs)} dependency PR(s): {','.join(dependency_prs)}")
write_github_output(output_path, dependency_prs)
return 0


if __name__ == "__main__":
sys.exit(main())

Loading
Loading