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
7 changes: 7 additions & 0 deletions .changelog/034.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: validate_no_changes
ts: 2026-03-01 07:26:09.172446+00:00
type: fix
author: EspenAlbert
changelog_message: 'fix: Allows using GITHUB_BASE_REF when validating no changes'
message: 'fix: Allows using GITHUB_BASE_REF when validating no changes'
short_sha: '257563'
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ By default, prompts before each git operation. See [Usage Scenarios](#usage-scen
uvx path-sync validate-no-changes -b main
```

In CI, when the workflow runs on `pull_request`, the comparison branch is the PR base. On `push` or locally, pass `-b` with the branch to compare against (e.g. `-b SDLC` for a branch based on SDLC).

Options:
- `-b, --branch` - Default branch to compare against (default: main)
- `-b, --branch` - Branch to compare against (default: main). When `GITHUB_BASE_REF` is set (e.g. in GitHub Actions), it overrides the default so CI can use the PR base without passing `-b`. If you set `GITHUB_BASE_REF`, use a non-empty branch name.
- `--skip-sections` - Comma-separated `path:section_id` pairs to skip (e.g., `justfile:coverage`)

## Usage Scenarios
Expand Down Expand Up @@ -358,7 +360,10 @@ jobs:
with:
fetch-depth: 0
- uses: astral-sh/setup-uv@v5
- run: uvx path-sync validate-no-changes -b main
- name: Validate no changes to synced files
env:
GITHUB_BASE_REF: ${{ github.base_ref || 'main' }}
run: uvx path-sync validate-no-changes
```

**Validation skips automatically when:**
Expand Down
9 changes: 7 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ By default, prompts before each git operation. See [Usage Scenarios](#usage-scen
uvx path-sync validate-no-changes -b main
```

In CI, when the workflow runs on `pull_request`, the comparison branch is the PR base. On `push` or locally, pass `-b` with the branch to compare against (e.g. `-b SDLC` for a branch based on SDLC).

Options:
- `-b, --branch` - Default branch to compare against (default: main)
- `-b, --branch` - Branch to compare against (default: main). When `GITHUB_BASE_REF` is set (e.g. in GitHub Actions), it overrides the default so CI can use the PR base without passing `-b`. If you set `GITHUB_BASE_REF`, use a non-empty branch name.
- `--skip-sections` - Comma-separated `path:section_id` pairs to skip (e.g., `justfile:coverage`)

## Usage Scenarios
Expand Down Expand Up @@ -358,7 +360,10 @@ jobs:
with:
fetch-depth: 0
- uses: astral-sh/setup-uv@v5
- run: uvx path-sync validate-no-changes -b main
- name: Validate no changes to synced files
env:
GITHUB_BASE_REF: ${{ github.base_ref || 'main' }}
run: uvx path-sync validate-no-changes
```

**Validation skips automatically when:**
Expand Down
11 changes: 6 additions & 5 deletions docs/validate_no_changes/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,16 @@ Validate no unauthorized changes to synced files.

**CLI Options:**

| Flag | Type | Default | Description |
|---|---|---|---|
| `-b`, `--branch` | `str` | `'main'` | Default branch to compare against |
| `--skip-sections` | `str` | `''` | Comma-separated path:section_id pairs to skip (e.g., 'justfile:coverage,pyproject.toml:default') |
| `--src-root` | `str` | `''` | Source repo root (default: find git root from cwd) |
| Flag | Type | Default | Env Var | Description |
|---|---|---|---|---|
| `-b`, `--branch` | `str` | `'main'` | `GITHUB_BASE_REF` | Branch to compare against (default: main; uses GITHUB_BASE_REF when set and -b not passed) |
| `--skip-sections` | `str` | `''` | - | Comma-separated path:section_id pairs to skip (e.g., 'justfile:coverage,pyproject.toml:default') |
| `--src-root` | `str` | `''` | - | Source repo root (default: find git root from cwd) |

### Changes

| Version | Change |
|---------|--------|
| unreleased | fix: Allows using GITHUB_BASE_REF when validating no changes |
| 0.4.1 | Made public |
<!-- === OK_EDIT: pkg-ext validate_no_changes_def === -->
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ vulture:

# === DO_NOT_EDIT: path-sync path-sync ===
path-sync-validate:
uv run path-sync validate-no-changes -n python-template
uv run path-sync validate-no-changes
# === OK_EDIT: path-sync path-sync ===

# === DO_NOT_EDIT: path-sync coverage ===
Expand Down
11 changes: 9 additions & 2 deletions path_sync/_internal/cmd_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@

@app.command("validate-no-changes")
def validate_no_changes(
branch: str = typer.Option("main", "-b", "--branch", help="Default branch to compare against"),
branch: str = typer.Option(
"main",
"-b",
"--branch",
help="Branch to compare against (default: main; uses GITHUB_BASE_REF when set and -b not passed)",
envvar="GITHUB_BASE_REF",
),
skip_sections_opt: str = typer.Option(
"",
"--skip-sections",
Expand All @@ -36,9 +42,10 @@ def validate_no_changes(
logger.info(f"On sync branch {current_branch}, validation skipped")
return
if current_branch == branch:
logger.info(f"On default branch {branch}, validation skipped")
logger.info(f"On base branch {branch}, validation skipped")
return

logger.info(f"Validating changes against branch {branch}")
skip_sections = parse_skip_sections(skip_sections_opt) if skip_sections_opt else None
unauthorized = validate_no_unauthorized_changes(repo_root, branch, skip_sections)

Expand Down
73 changes: 73 additions & 0 deletions path_sync/_internal/cmd_validate_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from __future__ import annotations

from pathlib import Path
from unittest.mock import MagicMock, patch

from path_sync._internal.cmd_validate import validate_no_changes
from path_sync._internal.validation import validate_no_unauthorized_changes

MODULE = validate_no_changes.__module__


def test_validate_no_changes_passes_branch_to_validation(tmp_path: Path):
mock_repo = MagicMock()
mock_repo.active_branch.name = "feature-branch"

with (
patch(f"{MODULE}.find_repo_root", return_value=tmp_path),
patch(f"{MODULE}.git_ops") as git_ops,
patch(f"{MODULE}.{validate_no_unauthorized_changes.__name__}") as validate_fn,
):
git_ops.get_repo.return_value = mock_repo
validate_fn.return_value = []

validate_no_changes(
branch="develop",
skip_sections_opt="",
src_root_opt=str(tmp_path),
)

validate_fn.assert_called_once()
call_kw = validate_fn.call_args
assert call_kw[0][0] == tmp_path
assert call_kw[0][1] == "develop"


def test_validate_no_changes_skips_when_on_sync_branch(tmp_path: Path):
mock_repo = MagicMock()
mock_repo.active_branch.name = "sync/python-template"

with (
patch(f"{MODULE}.find_repo_root", return_value=tmp_path),
patch(f"{MODULE}.git_ops") as git_ops,
patch(f"{MODULE}.{validate_no_unauthorized_changes.__name__}") as validate_fn,
):
git_ops.get_repo.return_value = mock_repo

validate_no_changes(
branch="main",
skip_sections_opt="",
src_root_opt=str(tmp_path),
)

validate_fn.assert_not_called()


def test_validate_no_changes_skips_when_on_base_branch(tmp_path: Path):
mock_repo = MagicMock()
mock_repo.active_branch.name = "main"

with (
patch(f"{MODULE}.find_repo_root", return_value=tmp_path),
patch(f"{MODULE}.git_ops") as git_ops,
patch(f"{MODULE}.{validate_no_unauthorized_changes.__name__}") as validate_fn,
):
git_ops.get_repo.return_value = mock_repo

validate_no_changes(
branch="main",
skip_sections_opt="",
src_root_opt=str(tmp_path),
)

validate_fn.assert_not_called()
36 changes: 36 additions & 0 deletions path_sync/_internal/validation_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from __future__ import annotations

import pytest

from path_sync._internal.validation import parse_skip_sections


def test_parse_skip_sections_empty_string_returns_empty():
assert parse_skip_sections("") == {}


def test_parse_skip_sections_single_entry():
assert parse_skip_sections("justfile:coverage") == {"justfile": {"coverage"}}


def test_parse_skip_sections_multiple_entries_same_path():
result = parse_skip_sections("justfile:coverage,justfile:default")
assert result == {"justfile": {"coverage", "default"}}


def test_parse_skip_sections_multiple_paths():
result = parse_skip_sections("justfile:coverage,pyproject.toml:default")
assert result == {"justfile": {"coverage"}, "pyproject.toml": {"default"}}


def test_parse_skip_sections_strips_whitespace():
assert parse_skip_sections(" a:b , c:d ") == {"a": {"b"}, "c": {"d"}}


def test_parse_skip_sections_rsplit_uses_last_colon():
assert parse_skip_sections("path:to:section_id") == {"path:to": {"section_id"}}


def test_parse_skip_sections_invalid_format_raises():
with pytest.raises(ValueError, match="Invalid format 'no_colon'"):
parse_skip_sections("no_colon")
Loading