Skip to content

[#23] Added '--cleanup-stale' option to prune stale remote branches.#337

Merged
AlexSkrypnyk merged 5 commits into
mainfrom
feature/23-cleanup-stale-branch
Jun 21, 2026
Merged

[#23] Added '--cleanup-stale' option to prune stale remote branches.#337
AlexSkrypnyk merged 5 commits into
mainfrom
feature/23-cleanup-stale-branch

Conversation

@AlexSkrypnyk

@AlexSkrypnyk AlexSkrypnyk commented Jun 20, 2026

Copy link
Copy Markdown
Member

Closes #23

Summary

Adds an opt-in --cleanup-stale flag to the artifact command that prunes stale branches from the destination repository after a successful push. A companion --cleanup-pattern glob (required) identifies which branches were created by the deployment workflow, and --cleanup-age (default 7 days) sets the staleness threshold. Cleanup is host-agnostic - it uses standard git push --delete, so it works with GitHub, GitLab, Bitbucket, and any other remote. The just-pushed branch and the remote's default branch are always protected, and cleanup fails closed: if the remote default branch cannot be resolved, pruning is skipped entirely rather than risk deleting it. Any fetch or delete failure is logged as a warning and never aborts the deployment.

Changes

New CLI options (src/Commands/ArtifactCommand.php):

  • --cleanup-stale - boolean gate that enables the feature; off by default so existing workflows are unaffected.
  • --cleanup-pattern - required when --cleanup-stale is set; a shell glob identifying the deployment branches (e.g. deployment/*).
  • --cleanup-age - positive integer number of days; branches older than this are eligible for deletion (default: 7).
  • Validation at option-resolve time: a missing pattern or invalid age throws RuntimeException before any Git work begins.
  • The summary banner printed at the start of a run shows the pattern and age when cleanup is enabled.
  • --dry-run is respected: stale branches are listed but not deleted.
  • Fail-closed safety: when the remote default branch cannot be determined, cleanup is skipped with a warning so the default branch can never be deleted.

Repository layer (src/Git/ArtifactGitRepository.php):

  • getRemoteBranchesInfo(string $remote): array<string, int> - shallow-fetches the remote (--depth=1) then reads refs/remotes/<remote> via for-each-ref to return branch-name-to-Unix-timestamp pairs.
  • getRemoteDefaultBranch(string $remote): ?string - resolves the remote's symbolic HEAD via ls-remote --symref; returns null on any failure.
  • deleteRemoteBranch(string $remote, string $branch): static - runs git push <remote> --delete <branch>.
  • static filterStaleBranches(...) - pure filtering logic: applies the glob, the age cutoff, and the protected-branch list, returning a sorted result. Static for easy unit-testing without a real repository.

Tests:

  • tests/Functional/CleanupStaleBranchesTest.php - seven functional scenarios: basic prune, dry-run passthrough, default-and-just-pushed branch protection, no-stale-branches no-op, unknown-remote returns null, missing-pattern validation, and invalid-age validation (data provider).
  • tests/Unit/ArtifactCommandTest.php - unit coverage for the three error-recovery paths in cleanupStaleBranches(): list failure, per-branch delete failure, and the fail-closed skip when the default branch is unknown.
  • tests/Unit/ArtifactGitRepositoryTest.php - eleven data-provider cases for filterStaleBranches() covering empty input, glob non-match, staleness boundary, protected exclusion, future timestamps, sorted output, numeric branch names, and wildcard-with-protected.
  • tests/Traits/GitTrait.php - new test helpers: gitCommitFileWithDate() (which now preserves and restores any pre-existing GIT_AUTHOR_DATE/GIT_COMMITTER_DATE), gitCreateBranchWithCommitDate(), gitGetBranchList(), gitAssertBranchesExist(), gitAssertBranchesNotExist().

Documentation (README.md):

  • New "Cleanup of stale branches" subsection with a usage example and prose explanation.
  • Three new rows in the CLI reference table for --cleanup-age, --cleanup-pattern, and --cleanup-stale.

Before / After

BEFORE
──────────────────────────────────────────────────────────
  Destination repo after N deployments (branch mode):

  refs/heads/deployment/1.0.0   (90 days old)
  refs/heads/deployment/1.1.0   (60 days old)
  refs/heads/deployment/1.2.0   (30 days old)
  refs/heads/deployment/1.3.0   (3 days old)   <-- just pushed
  refs/heads/main

  Branches accumulate indefinitely; no automatic pruning.

AFTER (with --cleanup-stale --cleanup-pattern="deployment/*" --cleanup-age=7)
──────────────────────────────────────────────────────────
  Push succeeds as before, then cleanup runs:

    Deleted stale branch "deployment/1.0.0"
    Deleted stale branch "deployment/1.1.0"
    Deleted stale branch "deployment/1.2.0"

  Destination repo after cleanup:

  refs/heads/deployment/1.3.0   (just pushed - always protected)
  refs/heads/main                (default branch - always protected)

  Safety: if the default branch can't be resolved, cleanup is skipped.
  Any delete failure logs a warning; the deployment exit code is unaffected.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds optional stale remote branch cleanup to ArtifactCommand (branch mode) via three new CLI options (--cleanup-stale, --cleanup-pattern, --cleanup-age). ArtifactGitRepository gains remote branch inspection (getRemoteBranchesInfo, getRemoteDefaultBranch, deleteRemoteBranch) and a static filterStaleBranches utility. Functional and unit tests are added alongside README documentation.

Changes

Stale Remote Branch Cleanup

Layer / File(s) Summary
Remote branch inspection and stale-filtering utilities
src/Git/ArtifactGitRepository.php, tests/Unit/ArtifactGitRepositoryTest.php
Adds getRemoteBranchesInfo (shallow-fetch + for-each-ref → branch/timestamp map), getRemoteDefaultBranch (ls-remote --symref), deleteRemoteBranch (push --delete), filterStaleBranches (protection list, glob match, age threshold, sorted output), and matchesGlob (glob-to-regex). Unit test data provider covers empty inputs, glob matching, stale/fresh boundaries, protected branch exclusion, future timestamps, sorted output, and wildcard behavior.
ArtifactCommand cleanup configuration, validation, and execution
src/Commands/ArtifactCommand.php, tests/Unit/ArtifactCommandTest.php
Adds CLEANUP_STALE_AGE_DEFAULT constant, three cleanup properties, --cleanup-stale/--cleanup-pattern/--cleanup-age CLI options, option validation requiring pattern when enabled and age as positive integer, a post-push call to cleanupStaleBranches(), the full cleanup method (early-exit, fetch remote branches, protect destination + default branch, filter stale, delete or dry-run, handle exceptions), and updated showInfo(). Unit tests cover GitException on list and delete, and unknown default branch.
Functional tests and GitTrait test helpers
tests/Traits/GitTrait.php, tests/Functional/CleanupStaleBranchesTest.php
GitTrait gains gitCommitFileWithDate, gitCreateBranchWithCommitDate, gitGetBranchList, gitAssertBranchesExist, and gitAssertBranchesNotExist. CleanupStaleBranchesTest covers pruning, dry-run, protection of default/pushed branches, no-stale-branches path, unknown-remote null return, missing-pattern validation error, and invalid-age rejection.
README documentation
README.md
Adds a "Cleanup of stale branches" subsection with usage example, deletion rules, protection rules, cross-remote notes, and --dry-run behaviour, plus --cleanup-age, --cleanup-pattern, and --cleanup-stale rows in the CLI options table.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and clearly summarizes the main feature added: a new '--cleanup-stale' option to prune stale remote branches, matching the primary objective of the PR.
Linked Issues check ✅ Passed The PR fully addresses issue #23 by implementing automated cleanup of stale remote branches with a configurable pattern and age threshold, working across all Git hosts.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the --cleanup-stale feature: new CLI options, repository methods, command logic, tests, documentation, and test helpers are all within scope.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/23-cleanup-stale-branch

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter

codecov-commenter commented Jun 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.53086% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.00%. Comparing base (4f42e64) to head (428d8f5).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/Git/ArtifactGitRepository.php 94.28% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #337      +/-   ##
==========================================
+ Coverage   96.91%   97.00%   +0.09%     
==========================================
  Files           6        6              
  Lines         421      501      +80     
==========================================
+ Hits          408      486      +78     
- Misses         13       15       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Line 177: The fenced code block containing the git-artifact command is missing
a language identifier on the opening fence, which triggers markdown linting
error MD040. To fix this, add bash as the language specifier to the opening
triple backticks of the code block that starts with the ./git-artifact
git@github.com:yourorg/your-repo-destination.git command. Change the opening
fence from triple backticks with no language to triple backticks followed by
bash.

In `@src/Commands/ArtifactCommand.php`:
- Around line 368-373: The code in the ArtifactCommand class fails to handle the
case when getRemoteDefaultBranch() returns NULL before proceeding with branch
cleanup. Currently, if the remote default branch cannot be resolved, the cleanup
continues without adding the default branch to the protected branches list,
risking deletion of the actual default branch. Add a check after calling
getRemoteDefaultBranch() that throws an exception or halts execution if the
method returns NULL, ensuring cleanup only proceeds when the default branch can
be safely protected.

In `@tests/Traits/GitTrait.php`:
- Around line 359-369: The finally block is unsetting GIT_AUTHOR_DATE and
GIT_COMMITTER_DATE environment variables without preserving their original
values, which causes test state leakage. Before setting these environment
variables (before the lines where putenv is called with the date values),
capture and store the original values of GIT_AUTHOR_DATE and GIT_COMMITTER_DATE
using getenv(). Then in the finally block, restore these original values by
calling putenv with the stored values instead of calling putenv with only the
variable names (which unsets them). Use conditional logic to handle cases where
the variables were not previously set.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1f07092e-37fd-442c-b49c-def9ca5e8d98

📥 Commits

Reviewing files that changed from the base of the PR and between 4f42e64 and 0985fc6.

📒 Files selected for processing (7)
  • README.md
  • src/Commands/ArtifactCommand.php
  • src/Git/ArtifactGitRepository.php
  • tests/Functional/CleanupStaleBranchesTest.php
  • tests/Traits/GitTrait.php
  • tests/Unit/ArtifactCommandTest.php
  • tests/Unit/ArtifactGitRepositoryTest.php

Comment thread README.md Outdated
Comment thread src/Commands/ArtifactCommand.php
Comment thread tests/Traits/GitTrait.php
Also specified the README example fence language and restored prior GIT_*_DATE values in the test commit helper.
@AlexSkrypnyk AlexSkrypnyk added the Needs review Pull request needs a review from assigned developers label Jun 21, 2026
@AlexSkrypnyk AlexSkrypnyk merged commit a353631 into main Jun 21, 2026
12 checks passed
@AlexSkrypnyk AlexSkrypnyk deleted the feature/23-cleanup-stale-branch branch June 21, 2026 02:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs review Pull request needs a review from assigned developers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cleanup stale remote branches

2 participants