From 2575632e4a11804d12732fe0ca0a70220e2f0960 Mon Sep 17 00:00:00 2001 From: EspenAlbert Date: Sun, 1 Mar 2026 08:25:58 +0100 Subject: [PATCH 1/4] fix: Allows using GITHUB_BASE_REF when validating no changes --- README.md | 4 +++- path_sync/_internal/cmd_validate.py | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 53bda99..8df4e66 100644 --- a/README.md +++ b/README.md @@ -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`. - `--skip-sections` - Comma-separated `path:section_id` pairs to skip (e.g., `justfile:coverage`) ## Usage Scenarios diff --git a/path_sync/_internal/cmd_validate.py b/path_sync/_internal/cmd_validate.py index c8320af..dd28210 100644 --- a/path_sync/_internal/cmd_validate.py +++ b/path_sync/_internal/cmd_validate.py @@ -15,7 +15,11 @@ @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", @@ -36,7 +40,7 @@ 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 skip_sections = parse_skip_sections(skip_sections_opt) if skip_sections_opt else None From 370e22026ea7562d637e2aa88a8bc7ee1f061158 Mon Sep 17 00:00:00 2001 From: EspenAlbert Date: Sun, 1 Mar 2026 08:26:26 +0100 Subject: [PATCH 2/4] chore: regen --- .changelog/000.yaml | 7 +++++++ docs/index.md | 4 +++- docs/validate_no_changes/index.md | 11 ++++++----- 3 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 .changelog/000.yaml diff --git a/.changelog/000.yaml b/.changelog/000.yaml new file mode 100644 index 0000000..fb29285 --- /dev/null +++ b/.changelog/000.yaml @@ -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' diff --git a/docs/index.md b/docs/index.md index 53bda99..8df4e66 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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`. - `--skip-sections` - Comma-separated `path:section_id` pairs to skip (e.g., `justfile:coverage`) ## Usage Scenarios diff --git a/docs/validate_no_changes/index.md b/docs/validate_no_changes/index.md index 83acc13..f117953 100644 --- a/docs/validate_no_changes/index.md +++ b/docs/validate_no_changes/index.md @@ -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 | \ No newline at end of file From 81dfbb0f34c7884bbf17a501748ed71fa16b0df1 Mon Sep 17 00:00:00 2001 From: EspenAlbert Date: Sun, 1 Mar 2026 08:28:51 +0100 Subject: [PATCH 3/4] chore: updates changelog --- .changelog/{000.yaml => 034.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changelog/{000.yaml => 034.yaml} (100%) diff --git a/.changelog/000.yaml b/.changelog/034.yaml similarity index 100% rename from .changelog/000.yaml rename to .changelog/034.yaml From d68e2846a458ba32459bb22be3c420d09ea70ebf Mon Sep 17 00:00:00 2001 From: EspenAlbert Date: Sun, 1 Mar 2026 08:38:47 +0100 Subject: [PATCH 4/4] feat: enhance path-sync validation with branch handling and tests - Updated `path-sync validate-no-changes` command to accept branch names from the environment variable `GITHUB_BASE_REF`. - Modified documentation to clarify branch handling in CI. - Added tests for `validate_no_changes` and `parse_skip_sections` functions to ensure correct behavior. - Improved logging for validation process. --- README.md | 7 ++- docs/index.md | 7 ++- justfile | 2 +- path_sync/_internal/cmd_validate.py | 5 +- path_sync/_internal/cmd_validate_test.py | 73 ++++++++++++++++++++++++ path_sync/_internal/validation_test.py | 36 ++++++++++++ 6 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 path_sync/_internal/cmd_validate_test.py create mode 100644 path_sync/_internal/validation_test.py diff --git a/README.md b/README.md index 8df4e66..d6dd15a 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ 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` - 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`. +- `-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 @@ -360,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:** diff --git a/docs/index.md b/docs/index.md index 8df4e66..d6dd15a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -83,7 +83,7 @@ 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` - 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`. +- `-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 @@ -360,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:** diff --git a/justfile b/justfile index 3c03ebb..bf1b86c 100644 --- a/justfile +++ b/justfile @@ -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 === diff --git a/path_sync/_internal/cmd_validate.py b/path_sync/_internal/cmd_validate.py index dd28210..22b1ed2 100644 --- a/path_sync/_internal/cmd_validate.py +++ b/path_sync/_internal/cmd_validate.py @@ -16,7 +16,9 @@ @app.command("validate-no-changes") def validate_no_changes( branch: str = typer.Option( - "main", "-b", "--branch", + "main", + "-b", + "--branch", help="Branch to compare against (default: main; uses GITHUB_BASE_REF when set and -b not passed)", envvar="GITHUB_BASE_REF", ), @@ -43,6 +45,7 @@ def validate_no_changes( 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) diff --git a/path_sync/_internal/cmd_validate_test.py b/path_sync/_internal/cmd_validate_test.py new file mode 100644 index 0000000..a458ba5 --- /dev/null +++ b/path_sync/_internal/cmd_validate_test.py @@ -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() diff --git a/path_sync/_internal/validation_test.py b/path_sync/_internal/validation_test.py new file mode 100644 index 0000000..13e536b --- /dev/null +++ b/path_sync/_internal/validation_test.py @@ -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")