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
13 changes: 13 additions & 0 deletions .agents/skills/conversation-done/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,16 @@ Fine-tune what gets cleaned up using selective leave flags:
* **Upstream Remote Protection**: Never deletes branches on `upstream`.
* **No Expunge Rule**: Never runs `bazel clean --expunge`; shuts down servers
gracefully and deletes the specific output base directory directly.

## Reporting Results

After running the cleanup script, always report to the user what resources were
deleted (or would be deleted in `--dry-run` mode), including:

* **Bazel Output Base(s)**: Paths and reclaimed disk space sizes.
* **Git Worktree**: Removed worktree directory path.
* **Local Git Branch**: Deleted local branch name.
* **Remote Git Branch**: Deleted remote branch (`<remote>/<branch>`).

If any resources were skipped, left intact via `--leave-*` flags, or not found,
note that briefly in the summary.
33 changes: 27 additions & 6 deletions .agents/skills/conversation-done/scripts/cleanup.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,23 +519,25 @@ async def execute_cleanup_target(
leave_branch: bool = False,
leave_remote: bool = False,
dry_run: bool = False,
) -> None:
"""Executes cleanup on a single target."""
) -> list[str]:
"""Executes cleanup on a single target and returns deleted resource descriptions."""
log(f"\nCleaning up target resources for: {target.worktree_path or target.branch}")
deleted: list[str] = []

if target.is_main_worktree:
log_error(
"Target is the main repository! Refusing to clean up main repository."
)
return
return deleted

# 1. Clean up Bazel output bases
if not leave_bazel_output_base and target.bazel_output_bases:
log("1. Cleaning up Bazel output bases:")
for ob in target.bazel_output_bases:
size_str = await get_path_size_human(ob)
log(f" - Output base: {ob} ({size_str})")
await shutdown_and_remove_bazel_output_base(ob, dry_run=dry_run)
if await shutdown_and_remove_bazel_output_base(ob, dry_run=dry_run):
deleted.append(f"Bazel output base: {ob} ({size_str})")
elif leave_bazel_output_base:
log("1. Leaving Bazel output base intact.")
else:
Expand All @@ -544,7 +546,8 @@ async def execute_cleanup_target(
# 2. Clean up Git worktree
if not leave_worktree and target.worktree_path:
log("2. Cleaning up Git worktree:")
await remove_git_worktree(main_repo, target.worktree_path, dry_run=dry_run)
if await remove_git_worktree(main_repo, target.worktree_path, dry_run=dry_run):
deleted.append(f"Git worktree: {target.worktree_path}")
elif leave_worktree:
log("2. Leaving Git worktree directory on disk.")
else:
Expand All @@ -558,6 +561,7 @@ async def execute_cleanup_target(
)
if success:
log(f" {msg}")
deleted.append(f"Local branch: {target.branch}")
else:
log_warn(f" {msg}")
elif leave_branch:
Expand All @@ -578,13 +582,16 @@ async def execute_cleanup_target(
)
if success:
log(f" {msg}")
deleted.append(f"Remote branch: {target.remote}/{target.branch}")
else:
log_warn(f" {msg}")
elif leave_remote:
log("4. Leaving remote branch intact.")
elif target.branch and target.remote:
log(f"4. Remote branch '{target.branch}' not present on '{target.remote}'.")

return deleted


async def list_resources(main_repo: str) -> None:
"""Lists all worktrees and their associated resources."""
Expand Down Expand Up @@ -759,8 +766,9 @@ async def async_main() -> None:
log("Aborted.")
return

deleted_resources: list[str] = []
for target in targets:
await execute_cleanup_target(
deleted = await execute_cleanup_target(
main_repo,
target,
leave_bazel_output_base=args.leave_bazel_output_base,
Expand All @@ -769,6 +777,19 @@ async def async_main() -> None:
leave_remote=args.leave_remote,
dry_run=args.dry_run,
)
deleted_resources.extend(deleted)

summary_header = (
"\nResources that would be deleted (dry run):"
if args.dry_run
else "\nDeleted resources:"
)
log(summary_header)
if deleted_resources:
for resource in deleted_resources:
log(f" - {resource}")
else:
log(" None")
log("\nCleanup completed.")


Expand Down
36 changes: 36 additions & 0 deletions .agents/skills/conversation-done/scripts/cleanup_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,42 @@ async def test_build_cleanup_target_branches(self):
self.assertEqual(target.remote, "origin")
self.assertTrue(target.remote_branch_exists)

async def test_execute_cleanup_target_reports_deleted_resources(self):
"""Verifies execute_cleanup_target returns the list of deleted resources."""
target = cleanup.CleanupTarget(
worktree_path="/repo/worktrees/feat1",
branch="feat1",
remote="origin",
remote_branch_exists=True,
bazel_output_bases=["/cache/bazel/ob1"],
is_main_worktree=False,
)
with mock.patch(
"cleanup.get_path_size_human", return_value="1.50 GB"
), mock.patch(
"cleanup.shutdown_and_remove_bazel_output_base", return_value=True
), mock.patch("cleanup.remove_git_worktree", return_value=True), mock.patch(
"cleanup.delete_local_branch",
return_value=(True, "Deleted local branch 'feat1'."),
), mock.patch(
"cleanup.delete_remote_branch",
return_value=(
True,
"Deleted remote branch 'feat1' from 'origin'.",
),
):
deleted = await cleanup.execute_cleanup_target("/repo/main", target)

self.assertEqual(
deleted,
[
"Bazel output base: /cache/bazel/ob1 (1.50 GB)",
"Git worktree: /repo/worktrees/feat1",
"Local branch: feat1",
"Remote branch: origin/feat1",
],
)


if __name__ == "__main__":
unittest.main()
31 changes: 0 additions & 31 deletions .github/workflows/check_version_markers.sh

This file was deleted.

3 changes: 0 additions & 3 deletions .github/workflows/create_archive_and_notes.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ if [ -z "$TAG" ]; then
fi
# If the workflow checks out one commit, but is releasing another
git fetch origin tag "$TAG"

# Update our local state so that check_version_markers searches what we expect
git checkout "$TAG"
$(dirname $0)/check_version_markers.sh

# A prefix is added to better match the GitHub generated archives.
PREFIX="rules_python-${TAG}"
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ changelog](https://rules-python.readthedocs.io/en/latest/changelog.html).
package directory, set it to `None` or `""` to set it to the root of the repo
([#4029](https://github.com/bazel-contrib/rules_python/issues/4029)).

{#v2-3-4}
## [2.3.4] - 2026-09-21

[2.3.4]: https://github.com/bazel-contrib/rules_python/releases/tag/2.3.4

{#v2-3-4-added}
### Added
* (toolchains) Added win32 (`i686-pc-windows-msvc`) Python runtimes for
versions 3.9.25, 3.10.20, 3.11.15, 3.12.13, 3.13.13, and 3.14.4
([#4156](https://github.com/bazel-contrib/rules_python/pull/4156)).

{#v2-3-3}
## [2.3.3] - 2026-09-04
Expand Down
19 changes: 16 additions & 3 deletions dev/release/create_rc.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
)
from dev.release.utils import (
REPO_URL,
find_version_markers,
get_latest_rc_tag,
set_github_output,
)
Expand Down Expand Up @@ -122,6 +123,21 @@ def _run_internal(self) -> int:
next_rc_num = rc_num + 1
next_rc = f"{version}-rc{next_rc_num}"

target_ref = f"{args.remote}/{branch_name}"
commit_sha = self.git.get_commit_sha(target_ref)
self.git.checkout(target_ref)

print("Checking for leftover VERSION_NEXT_ markers...")
markers = find_version_markers()
if markers:
for marker in markers:
print(marker)
print(
"Error: Found VERSION_NEXT markers indicating version needs to"
" be specified."
)
return 1

# Precheck: next RC number must exist and be unchecked in the checklist
rc_tags = state.get("rc_tags", {})
if next_rc_num not in rc_tags:
Expand All @@ -137,9 +153,6 @@ def _run_internal(self) -> int:
)
return 1

target_ref = f"{args.remote}/{branch_name}"
commit_sha = self.git.get_commit_sha(target_ref)

print(f"Tagging and pushing next RC: {next_rc}...")
self.git.tag(next_rc, target_ref)
self.git.push(args.remote, next_rc)
Expand Down
22 changes: 22 additions & 0 deletions dev/release/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def semver_type(value):
"./bazel-*/*",
"./CONTRIBUTING.md",
"./RELEASING.md",
"./docs/devguide.md",
"./dev/release/*",
"./tests/tools/private/release/*",
]
Expand Down Expand Up @@ -210,6 +211,27 @@ def replace_version_next(version: str) -> list[pathlib.Path]:
return replace_version_next_in_files(_iter_version_placeholder_files(), version)


def find_version_markers() -> list[str]:
"""Finds any remaining VERSION_NEXT_ markers in non-excluded files.

Returns:
A list of strings in the format 'filepath:line_number: line_content'
for each line containing 'VERSION_NEXT_'.
"""
matches: list[str] = []
for filepath in _iter_version_placeholder_files():
try:
content = filepath.read_text(encoding="utf-8")
except (IOError, UnicodeDecodeError):
continue

if "VERSION_NEXT_" in content:
for line_num, line in enumerate(content.splitlines(), start=1):
if "VERSION_NEXT_" in line:
matches.append(f"{filepath}:{line_num}: {line}")
return matches


def parse_pr_list(value: str) -> list[str]:
"""Parses a comma or space separated list of PR references.

Expand Down
44 changes: 42 additions & 2 deletions tests/tools/private/release/create_rc_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@
import tempfile
from unittest.mock import call

import pytest

from dev.release.create_rc import CreateRc

pytest_plugins = ["tests.tools.private.release.release_test_helper"]


@pytest.fixture(name="isolate_cwd", autouse=True)
def fixture_isolate_cwd(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)


def test_create_rc_success_first_rc(mocker, mock_git, mock_gh):
# Arrange
args = argparse.Namespace(
Expand Down Expand Up @@ -41,7 +48,7 @@ def test_create_rc_success_first_rc(mocker, mock_git, mock_gh):
mock_git.fetch.assert_has_calls(
[call("my-remote"), call("my-remote", tags=True, force=True)]
)
mock_git.checkout.assert_not_called()
mock_git.checkout.assert_called_once_with("my-remote/release/2.0")
mock_git.tag.assert_called_once_with("2.0.0-rc0", "my-remote/release/2.0")
mock_git.push.assert_called_once_with("my-remote", "2.0.0-rc0")
mock_git.get_commit_sha.assert_called_once_with("my-remote/release/2.0")
Expand Down Expand Up @@ -138,7 +145,7 @@ def test_create_rc_success_next_rc(mock_git, mock_gh):
mock_git.fetch.assert_has_calls(
[call("my-remote"), call("my-remote", tags=True, force=True)]
)
mock_git.checkout.assert_not_called()
mock_git.checkout.assert_called_once_with("my-remote/release/2.0")
mock_git.tag.assert_called_once_with("2.0.0-rc1", "my-remote/release/2.0")
mock_git.push.assert_called_once_with("my-remote", "2.0.0-rc1")
mock_git.get_commit_sha.assert_called_once_with("my-remote/release/2.0")
Expand Down Expand Up @@ -418,3 +425,36 @@ def test_create_rc_precondition_failure_reacts_to_comment(mocker, mock_git, mock
# Assert
assert result == 1
assert mock_gh.reactions.get(456) == ["-1"]


def test_create_rc_fails_on_version_markers(tmp_path, mock_git, mock_gh):
# Arrange
args = argparse.Namespace(
issue=123, remote="my-remote", triggering_comment=456, dry_run=False
)
initial_body = """
## Checklist
- [x] Prepare Release | status=done pr=#122 commit=abcdef12
- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12
- [ ] Tag RC0 | status=pending
"""
mock_gh.issues[123] = {
"title": "Release 2.0.0",
"body": initial_body,
"labels": ["type: release"],
}
mock_git.get_remote_tags.return_value = []
mock_git.get_commit_sha.return_value = "1234567890"
(tmp_path / "dirty.bzl").write_text(":::{versionadded} VERSION_NEXT_FEATURE\n")

# Act
result = CreateRc(args, mock_git, mock_gh).run()

# Assert
assert result == 1
mock_git.checkout.assert_called_once_with("my-remote/release/2.0")
mock_git.tag.assert_not_called()
mock_git.push.assert_not_called()
assert mock_gh.get_issue_body(123) == initial_body
assert 123 not in mock_gh.issue_comments
assert mock_gh.reactions.get(456) == ["-1"]
Loading
Loading