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
4 changes: 4 additions & 0 deletions .github/workflows/on_comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ def _process_release_issue_comment(
_write_github_output("command", "process-backports")
return

if _match_command("sync-changelog", comment_body):
_write_github_output("command", "sync-changelog")
return

if m := _match_command(("backport", "backports"), comment_body):
raw_args = m.group(1) if m.group(1) else ""
items = [item for item in re.split(r"[\s,]+", raw_args) if item]
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/on_comment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@ jobs:
comment_id: "${{ github.event.comment.id }}"
secrets: inherit

call_sync_changelog:
needs: parse_comment
if: needs.parse_comment.outputs.command == 'sync-changelog'
uses: ./.github/workflows/release_sync_changelog.yaml
with:
issue: ${{ needs.parse_comment.outputs.issue_number }}
secrets: inherit

call_promote:
needs: parse_comment
if: needs.parse_comment.outputs.command == 'promote'
Expand Down
124 changes: 124 additions & 0 deletions .github/workflows/on_pr_closed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""Parses closed/merged PR events to dispatch release and backport workflows."""

import json
import os
import re
import subprocess
import sys


def _load_event_data() -> dict:
"""Loads event JSON payload from GITHUB_EVENT_PATH."""
event_path = os.environ.get("GITHUB_EVENT_PATH")
if not event_path or not os.path.isfile(event_path):
return {}
try:
with open(event_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return {}


def _write_github_output(key: str, value: str) -> None:
"""Appends key=value to $GITHUB_OUTPUT."""
path = os.environ.get("GITHUB_OUTPUT")
if path:
with open(path, "a", encoding="utf-8") as f:
f.write(f"{key}={value}\n")


def _check_active_release_issue(repo: str) -> bool:
"""Checks if there is any active release tracking issue open."""
cmd = [
"gh",
"issue",
"list",
"--label",
"type: release",
"--state",
"open",
"--json",
"number",
]
if repo:
cmd.extend(["--repo", repo])
res = subprocess.run(cmd, capture_output=True, text=True, check=False)
if res.returncode != 0:
return False
try:
issues = json.loads(res.stdout or "[]")
return bool(issues)
except Exception:
return False


def _check_pr_has_backport_comment(repo: str, pr_number: str) -> bool:
"""Checks if PR comments contain a /backport command."""
cmd = ["gh", "pr", "view", pr_number, "--json", "comments"]
if repo:
cmd.extend(["--repo", repo])
res = subprocess.run(cmd, capture_output=True, text=True, check=False)
if res.returncode != 0:
return False
try:
data = json.loads(res.stdout or "{}")
comments = data.get("comments", [])
return any(
re.search(r"^\s*/backport(?:\s|$)", c.get("body", ""), re.MULTILINE)
for c in comments
)
except Exception:
return False


def process_pr_closed() -> int:
"""Processes closed PR event and determines workflow to dispatch."""
event = _load_event_data()
pr_data = event.get("pull_request")
if not pr_data or not isinstance(pr_data, dict):
_write_github_output("command", "none")
return 0

is_merged = bool(pr_data.get("merged", False))
pr_number = str(pr_data.get("number") or event.get("number") or "")

if not is_merged or not pr_number:
_write_github_output("command", "none")
return 0

repo = event.get("repository", {}).get("full_name") or os.environ.get(
"GITHUB_REPOSITORY", ""
)

labels_data = pr_data.get("labels", [])
labels = []
if isinstance(labels_data, list):
for label in labels_data:
if isinstance(label, dict) and "name" in label:
labels.append(label["name"])
elif isinstance(label, str):
labels.append(label)

if "type: sync-changelog" in labels:
_write_github_output("command", "complete-sync-changelog")
_write_github_output("pr_number", pr_number)
return 0

if _check_active_release_issue(repo) and _check_pr_has_backport_comment(
repo, pr_number
):
_write_github_output("command", "process-backports")
_write_github_output("pr_number", pr_number)
return 0

_write_github_output("command", "none")
return 0


def _main() -> None:
sys.exit(process_pr_closed())


if __name__ == "__main__":
_main()
78 changes: 20 additions & 58 deletions .github/workflows/on_pr_closed.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ on:
types: [closed]

permissions:
contents: read
issues: read
contents: write
issues: write
pull-requests: read

jobs:
Expand All @@ -17,40 +17,26 @@ jobs:
steps:
- run: echo "No-op"

check_if_backport:
parse_pr:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
outputs:
should_process: ${{ steps.check.outputs.should_process }}
command: ${{ steps.parse.outputs.command }}
pr_number: ${{ steps.parse.outputs.pr_number }}
steps:
- name: Check if PR is a backport candidate
id: check
- uses: actions/checkout@v7
- uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: Parse PR
id: parse
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
# Check if there is any active release issue
ACTIVE_ISSUES=$(gh issue list --repo ${{ github.repository }} --label "type: release" --state open --json number)
if [ "$ACTIVE_ISSUES" = "[]" ] || [ -z "$ACTIVE_ISSUES" ]; then
echo "No active release tracking issue found. Skipping."
echo "should_process=false" >> "$GITHUB_OUTPUT"
exit 0
fi

# Check if PR has "/backport" in comments (only comments, not body)
PR_DATA=$(gh pr view "$PR_NUMBER" --repo ${{ github.repository }} --json comments)

if echo "$PR_DATA" | jq -r '.comments[].body' | grep -qE '^[[:space:]]*/backport([[:space:]]|$)'; then
echo "Found /backport comment. Proceeding."
echo "should_process=true" >> "$GITHUB_OUTPUT"
else
echo "No /backport comment found. Skipping."
echo "should_process=false" >> "$GITHUB_OUTPUT"
fi
run: .github/workflows/on_pr_closed.py

process_backports:
needs: check_if_backport
if: needs.check_if_backport.outputs.should_process == 'true'
needs: parse_pr
if: needs.parse_pr.outputs.command == 'process-backports'
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -75,39 +61,15 @@ jobs:
- name: Process Backports
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_NUMBER: ${{ needs.parse_pr.outputs.pr_number }}
run: |
bazel run //tools/private/release -- on-pr-merged \
"$PR_NUMBER" \
--remote origin \
--no-dry-run

complete_sync_changelog:
if: |
github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'type: sync-changelog')
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Setup Bazel
uses: bazel-contrib/setup-bazel@0.19.0
with:
bazelisk-version: 1.20.0

- name: Complete Sync Changelog
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
bazel run //tools/private/release -- complete-sync-changelog \
--pr "$PR_NUMBER"


call_complete_sync_changelog:
needs: parse_pr
if: needs.parse_pr.outputs.command == 'complete-sync-changelog'
uses: ./.github/workflows/release_sync_changelog_complete.yaml
secrets: inherit
9 changes: 9 additions & 0 deletions .github/workflows/release_process_backports.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,12 @@ jobs:
--remote origin \
--no-dry-run \
"${ARGS[@]}"

call_sync_changelog:
needs: process_backports
uses: ./.github/workflows/release_sync_changelog.yaml
with:
issue: ${{ inputs.issue }}
secrets: inherit


46 changes: 46 additions & 0 deletions .github/workflows/release_sync_changelog.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: "Release: Sync Changelog"

on:
workflow_dispatch:
inputs:
issue:
description: 'The Release Tracking Issue Number (e.g., 142)'
required: false
type: string
workflow_call:
inputs:
issue:
description: 'The Release Tracking Issue Number (e.g., 142)'
required: false
type: string

permissions:
contents: write
issues: write
pull-requests: write

jobs:
sync_changelog:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Setup Bazel
uses: bazel-contrib/setup-bazel@0.19.0
with:
bazelisk-version: 1.20.0

- name: Configure Git Identity
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Create Sync PR to Main
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
bazel run //tools/private/release -- sync-changelog \
--remote origin
45 changes: 45 additions & 0 deletions .github/workflows/release_sync_changelog_complete.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: "Release: Sync Changelog: Complete"

on:
workflow_dispatch:
inputs:
pr:
description: 'The merged sync-changelog PR number (optional; extracted from GITHUB_EVENT_PATH if omitted)'
required: false
type: string
workflow_call:
inputs:
pr:
description: 'The merged sync-changelog PR number (optional; extracted from GITHUB_EVENT_PATH if omitted)'
required: false
type: string

permissions:
contents: write
issues: write
pull-requests: read

jobs:
complete_sync_changelog:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Setup Bazel
uses: bazel-contrib/setup-bazel@0.19.0
with:
bazelisk-version: 1.20.0

- name: Configure Git Identity
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Complete Sync Changelog
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
bazel run //tools/private/release -- complete-sync-changelog
23 changes: 23 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,29 @@ bazel run //tools/private/release -- process-news 2.3.0 news/3997.added.md
bazel run //tools/private/release -- process-news 2.3.0 3997
```

### Syncing Changelog to Main

When backports are processed, a separate workflow and job creates a sync PR to
`main` to merge news entries into `CHANGELOG.md` and update `VERSION_NEXT_*`
placeholders.

You can also manually trigger changelog syncing using the GitHub CLI or Actions
UI:

```shell
gh workflow run release_sync_changelog.yaml \
--repo bazel-contrib/rules_python \
--raw-field issue=<ISSUE>
```

Or comment `/sync-changelog` on the release tracking issue, or run via the
release tool CLI:

```shell
bazel run //tools/private/release -- \
sync-changelog --issue <ISSUE> --remote origin
```

### Failure Behavior
If a backport fails to process (e.g., due to cherry-pick conflicts):
* The failed backport checklist item will remain unchecked with
Expand Down
Loading
Loading