Skip to content

Commit a0d9580

Browse files
committed
Refactor movable tag logic to delete remote tags before pushing; update tests and documentation
1 parent a1e8e6d commit a0d9580

File tree

4 files changed

+203
-7
lines changed

4 files changed

+203
-7
lines changed

AGENTS.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Agent instructions
2+
3+
## When to Use What
4+
5+
| Your Need | Skill | Example Trigger |
6+
|-----------------------------------------------|--------------------------|---------------------------------------------------------------|
7+
| Local code search, structure, definitions | **Local Search** | "Find X in codebase", "Where is Y?", "Explore this dir" |
8+
| Full research (local + GitHub, PRs, packages) | **Research** | "How does X work?", "Who calls Z?", "Trace flow", "Review PR" |
9+
| Plan work before implementing | **Plan** | "Plan this feature", "Research & plan refactor" |
10+
| Review a pull request | **PR Reviewer** | "Review PR #123", "Is this PR safe to merge?" |
11+
| Brutal code criticism with fixes | **Roast** | "Roast my code", "Find code sins", "What's wrong with this?" |
12+
| Strengthen prompts / agent instructions | **Prompt Optimizer** | "Optimize this SKILL.md", "Agent skips steps" |
13+
| Generate repo documentation | **Documentation Writer** | "Document this project", "Create developer docs" |
14+
15+
---
16+
17+
## Skills Overview
18+
19+
### 1. OctoCode Local Search
20+
21+
**Location:** `octocode-local-search/`
22+
23+
Local codebase exploration using Octocode Local + LSP. Search, structure, find files, trace definitions/usages—no GitHub. Fast local discovery.
24+
25+
| When | Example |
26+
|-------------------|------------------------------------------|
27+
| Local search only | "Find auth logic", "Where is X defined?" |
28+
| Explore structure | "List src/ files", "Show package layout" |
29+
30+
---
31+
32+
### 2. OctoCode Research
33+
34+
**Location:** `octocode-research/`
35+
36+
Deep code exploration: LSP, local tools, GitHub API, packages, PRs. File:line citations and GitHub URLs. Full stack research.
37+
38+
| When | Example |
39+
|-----------------|-------------------------------------------------------|
40+
| Research code | "Research how auth works" |
41+
| GitHub/external | "How does library X work?", "Find PRs that changed Y" |
42+
43+
---
44+
45+
### 3. OctoCode Plan
46+
47+
**Location:** `octocode-plan/`
48+
49+
Evidence-based planning. Understand → Research (via Local Search/Research) → Plan → Implement. No guessing; validates with code.
50+
51+
| When | Example |
52+
|-------------------|-------------------------------------------------------|
53+
| Multi-step work | "Plan auth refactor", "Plan API v2" |
54+
| Non-trivial tasks | "Research & plan this feature" |
55+
56+
---
57+
58+
### 4. OctoCode Prompt Optimizer
59+
60+
**Location:** `octocode-prompt-optimizer/`
61+
62+
Turns weak prompts into enforceable protocols. Gates, FORBIDDEN lists, failure analysis. Preserves intent, adds reliability.
63+
64+
| When | Example |
65+
|-------------------|-------------------------------------------------------|
66+
| Prompts ignored | "Agent keeps skipping steps" |
67+
| New/weak instructions | "Optimize this SKILL.md", "Make prompt reliable" |
68+
69+
*Not for:* Short prompts (<50 lines), already-optimized docs.
70+
71+
---
72+
73+
### 5. OctoCode Documentation Writer
74+
**Location:** `octocode-documentation-writer/`
75+
76+
6-phase pipeline: Discovery → Questions → Research → Orchestration → Writing → QA. Produces 16+ docs with validation.
77+
78+
| When | Example |
79+
|-------------------|-----------------------------------------|
80+
| New/outdated docs | "Generate documentation", "Update docs" |
81+
| Onboarding | "Create docs for new devs" |
82+
83+
---
84+
85+
### 6. OctoCode Roast
86+
87+
**Location:** `octocode-roast/`
88+
89+
Brutal code critique with file:line citations. Severity: gentle → nuclear. Sin registry, user picks fixes. Cites or dies.
90+
91+
| When | Example |
92+
|-----------------|--------------------------------------|
93+
| Code critique | "Roast my code", "Find antipatterns" |
94+
| Honest feedback | "What's wrong with my code?" |
95+
96+
---
97+
98+
### 7. OctoCode Pull Request Reviewer
99+
100+
**Location:** `octocode-pull-request-reviewer/`
101+
102+
Holistic PR review via Octocode MCP: bugs, security, architecture, flow impact. 7 domains, evidence-backed, user checkpoint before deep dive.
103+
104+
| When | Example |
105+
|-----------------|-----------------------------------|
106+
| PR review | "Review PR #456", "Check this PR" |
107+
| Security/impact | "Is this safe to merge?" |

bumpversion/scm/git.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,10 @@ def moveable_tag(name: str) -> None:
263263
"""
264264
try:
265265
run_command(["git", "tag", "-f", name])
266-
push_remote("origin", name, force=True)
266+
if not has_remote("origin"):
267+
return
268+
delete_remote_tag("origin", name)
269+
push_remote("origin", name)
267270
except subprocess.CalledProcessError as e:
268271
format_and_raise_error(e)
269272

@@ -282,10 +285,40 @@ def assert_nondirty() -> None:
282285
def push_remote(remote_name: str, ref_name: str, force: bool = False) -> None:
283286
"""Push the `ref_name` to the `remote_name` repository, optionally forcing the push."""
284287
try:
285-
result = run_command(["git", "remote"])
286-
if remote_name not in result.stdout:
287-
logger.warning("Remote '%s' not found, skipping push.", remote_name)
288+
if not has_remote(remote_name):
288289
return
289-
run_command(["git", "push", remote_name, ref_name, "--force" if force else ""])
290+
command = ["git", "push", remote_name, ref_name]
291+
if force:
292+
command.append("--force")
293+
run_command(command)
290294
except subprocess.CalledProcessError as e:
291295
format_and_raise_error(e)
296+
297+
298+
def delete_remote_tag(remote_name: str, ref_name: str) -> None:
299+
"""Delete `ref_name` from `remote_name`, ignoring missing remote tag errors."""
300+
try:
301+
run_command(["git", "push", "--delete", remote_name, ref_name])
302+
except subprocess.CalledProcessError as e:
303+
if _is_missing_remote_tag_error(e):
304+
logger.debug("Remote tag '%s' does not exist in '%s', continuing.", ref_name, remote_name)
305+
return
306+
format_and_raise_error(e)
307+
308+
309+
def has_remote(remote_name: str) -> bool:
310+
"""Return True if `remote_name` exists in the repository remotes."""
311+
try:
312+
result = run_command(["git", "remote"])
313+
except subprocess.CalledProcessError as e:
314+
format_and_raise_error(e)
315+
if remote_name not in result.stdout:
316+
logger.warning("Remote '%s' not found, skipping push.", remote_name)
317+
return False
318+
return True
319+
320+
321+
def _is_missing_remote_tag_error(error: subprocess.CalledProcessError) -> bool:
322+
"""Return True if the error indicates the remote tag does not exist."""
323+
stderr = (error.stderr or "").lower()
324+
return "remote ref does not exist" in stderr

docs/reference/movable-version-tags.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Bump My Version's stance is to provide unique, immutable release tags when taggi
1212

1313
- The `moveable_tags` configuration is a list of serialization strings.
1414
- All strings are serialized
15-
- Each string is forcibly tagged (as a lightweight, non-annotated tag) and forcibly pushed to origin.
15+
- Each string is forcibly tagged locally (as a lightweight, non-annotated tag), deleted on origin, and then pushed to origin without force.
1616

1717

1818
## Configuration

tests/test_scm/test_git.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import subprocess
44
from pathlib import Path
5-
from unittest.mock import MagicMock, patch
5+
from unittest.mock import MagicMock, call, patch
66

77
import pytest
88
from pytest import param
@@ -284,6 +284,62 @@ def test_short_branch_name_logic(self, git_repo: Path, branch_name: str, expecte
284284
class TestMoveableTag:
285285
"""Tests for the moveable_tag function."""
286286

287+
@patch("bumpversion.scm.git.run_command")
288+
def test_deletes_remote_tag_before_push(self, mock_run_command) -> None:
289+
"""Moveable tags should delete remote tag and then push without force."""
290+
mock_run_command.return_value = MagicMock(stdout="origin\n")
291+
292+
moveable_tag("v1")
293+
294+
assert mock_run_command.call_args_list == [
295+
call(["git", "tag", "-f", "v1"]),
296+
call(["git", "remote"]),
297+
call(["git", "push", "--delete", "origin", "v1"]),
298+
call(["git", "remote"]),
299+
call(["git", "push", "origin", "v1"]),
300+
]
301+
302+
@patch("bumpversion.scm.git.run_command")
303+
def test_missing_remote_tag_delete_is_ignored(self, mock_run_command) -> None:
304+
"""Missing remote tag during delete should not block the subsequent push."""
305+
mock_run_command.side_effect = [
306+
MagicMock(stdout="origin\n"),
307+
MagicMock(stdout="origin\n"),
308+
subprocess.CalledProcessError(
309+
returncode=1,
310+
cmd="git push --delete origin v1",
311+
stderr="error: unable to delete 'v1': remote ref does not exist",
312+
),
313+
MagicMock(stdout="origin\n"),
314+
MagicMock(stdout=""),
315+
]
316+
317+
moveable_tag("v1")
318+
319+
assert mock_run_command.call_args_list == [
320+
call(["git", "tag", "-f", "v1"]),
321+
call(["git", "remote"]),
322+
call(["git", "push", "--delete", "origin", "v1"]),
323+
call(["git", "remote"]),
324+
call(["git", "push", "origin", "v1"]),
325+
]
326+
327+
@patch("bumpversion.scm.git.run_command")
328+
def test_delete_failure_raises_error(self, mock_run_command) -> None:
329+
"""Unexpected delete failures should raise BumpVersionError."""
330+
mock_run_command.side_effect = [
331+
MagicMock(stdout="origin\n"),
332+
MagicMock(stdout="origin\n"),
333+
subprocess.CalledProcessError(
334+
returncode=1,
335+
cmd="git push --delete origin v1",
336+
stderr="remote: permission denied",
337+
),
338+
]
339+
340+
with pytest.raises(BumpVersionError):
341+
moveable_tag("v1")
342+
287343
def test_moves_tags(self, git_repo: Path) -> None:
288344
"""Tag moves to subsequent commit."""
289345
# Arrange

0 commit comments

Comments
 (0)