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
55 changes: 55 additions & 0 deletions .cursor/rules/branch-naming.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
description: Topic branch naming and workflow for starting work on an issue
alwaysApply: true
---

# Topic Branch Naming and Workflow

When the user asks to create or start work on an issue (e.g. "create branch for issue 36", "start working on issue 36", or references `.github_data/issues/issue-36.md`), follow this workflow.

## Workflow: Create and link a development branch

1. **Verify no developer branch is linked yet**
- Run: `gh issue develop --list <issue_number>`
- If the issue already has a linked branch, tell the user and offer to checkout that branch locally (`git fetch origin && git checkout <branch_name>`) or stop. Do not create a second linked branch.

2. **Infer branch type**
- From issue labels or intent, pick one: `feature` | `bugfix` | `hotfix` | `release` | `docs` | `test` | `refactor`.
- Ask the user if labels and title are ambiguous.

3. **Set short summary**
- From the issue title or description, derive a kebab-case `short_summary` (a few words).
- Omit prefixes like "FEATURE", "BUG", "Add". Example: "Standardize and Enforce Commit Message Format" → `standardize-commit-messages`.

4. **Propose branch name and ask for validation**
- Propose: `<type>/<issue_number>-<short_summary>` (e.g. `feature/36-standardize-commit-messages`).
- Explicitly ask the user to confirm or give a different name before proceeding.

5. **Create and link the branch via GitHub**
- After user confirms: `gh issue develop <issue_number> --base dev --name <branch_name> --checkout`
- This creates the branch on the remote from `dev`, links it to the issue, and checks it out locally. If `gh` reports that the branch already exists on the remote, run `git fetch origin` and `git checkout <branch_name>` instead.

6. **Ensure local branch is up to date**
- After checkout: `git pull origin <branch_name>` (if the branch already had commits and you created it via another path, or to sync with remote).

## Branch name format (reference)

```
<type>/<issue_number>-<short_summary>
```

## Branch types (reference)

| Type | Use for |
|----------|-------------------------------------------------------------------------|
| feature | New functionality, enhancements |
| bugfix | Bug fixes (non-urgent) |
| hotfix | Urgent production fixes, often merged outside normal release |
| release | Release preparation, version bumps, release notes |
| docs | Documentation only |
| test | Tests, CI, or test infrastructure |
| refactor | Code refactoring, no new behavior |

## One-off branch name only

When the user only wants a branch name suggestion (no "create" or "start work"), propose the name in the format above and do not run the full workflow.
9 changes: 9 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
# Enforce topic branch naming: <type>/<issue_number>-<short_summary>
# Allows main, dev, and branches matching the convention.
- id: no-commit-to-branch
name: branch-name (enforce <type>/<issue>-<summary>)
args:
- --branch
- __none__ # override default so main/dev are not protected
- --pattern
- "^(?!main$)(?!dev$)(?!^(feature|bugfix|hotfix|release|docs|test|refactor)/[0-9]+-[a-z0-9]+(-[a-z0-9]+)*$).+$"
- id: check-added-large-files
- id: check-case-conflict
- id: check-json
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Branch name enforcement as a pre-commit hook**
- New `branch-name` hook enforcing `<type>/<issue>-<summary>` convention (e.g. `feature/38-standardize-branching-strategy-enforcement`)
- Pre-commit configuration updated in repo and in workspace assets (`.pre-commit-config.yaml`, `assets/workspace/.pre-commit-config.yaml`)
- Integration tests added for valid and invalid branch names
- **Cursor rules for branch naming and issue workflow**
- `.cursor/rules/branch-naming.mdc`: topic branch naming format, branch types, workflow for creating/linking branches via `gh issue develop`
- Guidelines for inferring branch type from issue labels and deriving short summary from issue title

### Changed

- **Updated pre-commit hook configuration in the devcontainer**
- Exclude issue and template docs from .github_data
- Autofix shellcheck
- Autofix pymarkdown
- Add license compliance check

### Deprecated

### Removed
Expand Down
55 changes: 55 additions & 0 deletions assets/workspace/.cursor/rules/branch-naming.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
description: Topic branch naming and workflow for starting work on an issue
alwaysApply: true
---

# Topic Branch Naming and Workflow

When the user asks to create or start work on an issue (e.g. "create branch for issue 36", "start working on issue 36", or references `.github_data/issues/issue-36.md`), follow this workflow.

## Workflow: Create and link a development branch

1. **Verify no developer branch is linked yet**
- Run: `gh issue develop --list <issue_number>`
- If the issue already has a linked branch, tell the user and offer to checkout that branch locally (`git fetch origin && git checkout <branch_name>`) or stop. Do not create a second linked branch.

2. **Infer branch type**
- From issue labels or intent, pick one: `feature` | `bugfix` | `hotfix` | `release` | `docs` | `test` | `refactor`.
- Ask the user if labels and title are ambiguous.

3. **Set short summary**
- From the issue title or description, derive a kebab-case `short_summary` (a few words).
- Omit prefixes like "FEATURE", "BUG", "Add". Example: "Standardize and Enforce Commit Message Format" → `standardize-commit-messages`.

4. **Propose branch name and ask for validation**
- Propose: `<type>/<issue_number>-<short_summary>` (e.g. `feature/36-standardize-commit-messages`).
- Explicitly ask the user to confirm or give a different name before proceeding.

5. **Create and link the branch via GitHub**
- After user confirms: `gh issue develop <issue_number> --base dev --name <branch_name> --checkout`
- This creates the branch on the remote from `dev`, links it to the issue, and checks it out locally. If `gh` reports that the branch already exists on the remote, run `git fetch origin` and `git checkout <branch_name>` instead.

6. **Ensure local branch is up to date**
- After checkout: `git pull origin <branch_name>` (if the branch already had commits and you created it via another path, or to sync with remote).

## Branch name format (reference)

```
<type>/<issue_number>-<short_summary>
```

## Branch types (reference)

| Type | Use for |
|----------|-------------------------------------------------------------------------|
| feature | New functionality, enhancements |
| bugfix | Bug fixes (non-urgent) |
| hotfix | Urgent production fixes, often merged outside normal release |
| release | Release preparation, version bumps, release notes |
| docs | Documentation only |
| test | Tests, CI, or test infrastructure |
| refactor | Code refactoring, no new behavior |

## One-off branch name only

When the user only wants a branch name suggestion (no "create" or "start work"), propose the name in the format above and do not run the full workflow.
29 changes: 20 additions & 9 deletions assets/workspace/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
---
# .pre-commit-config.yaml

exclude: ^\.github_data/

repos:
# General Pre-commit Hooks (Fixes formatting issues)
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
# Enforce topic branch naming: <type>/<issue_number>-<short_summary>
# Allows main, dev, and branches matching the convention.
- id: no-commit-to-branch
name: branch-name (enforce <type>/<issue>-<summary>)
args:
- --branch
- __none__ # override default so main/dev are not protected
- --pattern
- "^(?!main$)(?!dev$)(?!^(feature|bugfix|hotfix|release|docs|test|refactor)/[0-9]+-[a-z0-9]+(-[a-z0-9]+)*$).+$"
- id: check-added-large-files
- id: check-case-conflict
- id: check-json
Expand Down Expand Up @@ -41,14 +52,15 @@ repos:
hooks:
- id: shellcheck
name: shellcheck
args: ["-x"]

# Markdown Linting
# Markdown Linting (excludes auto-generated docs)
- repo: https://github.com/jackdewinter/pymarkdown
rev: v0.9.23
hooks:
- id: pymarkdown
name: pymarkdown
args: ["scan"]
args: ["-c", ".pymarkdown", "fix"]

# Justfile formatting
- repo: local
Expand All @@ -66,13 +78,12 @@ repos:
hooks:
- id: typos

# Typstyle Linting
# License Compliance Check (runs only when dependencies change)
- repo: local
hooks:
- id: typstyle
name: typstyle
entry: typstyle
- id: pip-licenses
name: pip-licenses (check dependency licenses)
entry: uv run pip-licenses --fail-on="GPL-3.0-only;GPL-3.0-or-later;AGPL-3.0-only;AGPL-3.0-or-later"
language: system
files: ^.*\.typ$
pass_filenames: true
args: [--inplace]
files: ^(pyproject\.toml|uv\.lock|requirements.*\.txt)$
pass_filenames: false
112 changes: 112 additions & 0 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1473,6 +1473,118 @@ def test_github_cli_authentication(self, devcontainer_up):
f"Expected 'Logged in to github.com' or similar in output"
)

def test_valid_branch_names_commit_succeeds(self, devcontainer_up):
"""Valid branch names (convention) allow commits; passes with or without branch-name hook."""
# Create dummy file to commit
workspace_path = devcontainer_up.resolve()
dummy_file = workspace_path / "dummy.txt"
dummy_file.write_text("dummy\n")

# Define valid branch names
valid_branch_names = [
"feature/123-test-branch",
"bugfix/123-test-branch",
"hotfix/123-test-branch",
"release/123-test-branch",
"docs/123-test-branch",
"test/123-test-branch",
"refactor/123-test-branch",
]

# Test valid branch names
for branch_name in valid_branch_names:
# Create branch and run pre-commit hook
exec_cmd = [
"devcontainer",
"exec",
"--workspace-folder",
str(workspace_path),
"--config",
f"{workspace_path}/.devcontainer/devcontainer.json",
"--docker-path",
"podman",
"bash",
"-c",
(
"cd /workspace/test_project"
" && printf 'dummy\\n' > dummy.txt"
f" && git checkout -b '{branch_name}'"
" && git add dummy.txt"
" && pre-commit run -a"
),
]
result = subprocess.run(
exec_cmd,
capture_output=True,
text=True,
cwd=str(workspace_path),
env=os.environ.copy(),
timeout=120,
)

assert result.returncode == 0, (
f"pre-commit on valid branch '{branch_name}' should succeed\n"
f"stdout: {result.stdout}\n"
f"stderr: {result.stderr}\n"
f"command: {' '.join(exec_cmd)}"
)

def test_invalid_branch_names_commit_fails(self, devcontainer_up):
"""Invalid branch names (convention) fail commits (branch-name pre-commit hook)."""
# Create dummy file to commit
workspace_path = devcontainer_up.resolve()
dummy_file = workspace_path / "dummy.txt"
dummy_file.write_text("dummy\n")

invalid_branch_names = [
"featur/123-typo",
"bugfix/missing-issue-number",
"hotfix/123",
"release123-missing-/",
"random-string",
]

for branch_name in invalid_branch_names:
exec_cmd = [
"devcontainer",
"exec",
"--workspace-folder",
str(workspace_path),
"--config",
f"{workspace_path}/.devcontainer/devcontainer.json",
"--docker-path",
"podman",
"bash",
"-c",
(
"cd /workspace/test_project"
" && printf 'dummy\\n' > dummy.txt"
f" && git checkout -b '{branch_name}'"
" && git add dummy.txt"
" && pre-commit run -a"
),
]
result = subprocess.run(
exec_cmd,
capture_output=True,
text=True,
cwd=str(workspace_path),
env=os.environ.copy(),
timeout=120,
)

assert result.returncode != 0, (
f"pre-commit on invalid branch '{branch_name}' should fail\n"
f"stdout: {result.stdout}\n"
f"stderr: {result.stderr}\n"
f"command: {' '.join(exec_cmd)}"
)
output = (result.stdout + result.stderr).lower()
assert "branch" in output or "no-commit-to-branch" in output, (
f"Expected branch-name hook failure in output\n"
f"stdout: {result.stdout}\nstderr: {result.stderr}"
)


class TestJustRecipes:
"""Test the just recipes."""
Expand Down