Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5a23839
Added plan
alelom Mar 2, 2026
030f275
Implement optional version resolution via semantic-release for subfol…
alelom Mar 2, 2026
03810ee
Enhance package configuration and script resolution
alelom Mar 2, 2026
9543377
Refactor script resolution and update package configuration
alelom Mar 2, 2026
5a0582c
Refactor script resolution logic in `resolve_version_via_semantic_rel…
alelom Mar 2, 2026
d4c28fa
Enhance backup logic in `get-next-version.cjs` script
alelom Mar 2, 2026
e200233
Refine subfolder build logic in `python_package_folder.py`
alelom Mar 2, 2026
d431e20
Update package configuration to force-include scripts directory in `p…
alelom Mar 2, 2026
14e67b6
Formatting fixed. src/python_package_folder/python_package_folder.py …
alelom Mar 2, 2026
44d3cf5
Update scripts/get-next-version.cjs
alelom Mar 2, 2026
76f76f9
Update src/python_package_folder/scripts/get-next-version.cjs
alelom Mar 2, 2026
2c52e69
Update src/python_package_folder/scripts/get-next-version.cjs
alelom Mar 2, 2026
b56d08e
Update scripts/get-next-version.cjs
alelom Mar 2, 2026
a45ba4c
Enhance cleanup logic in get-next-version.cjs script
alelom Mar 2, 2026
5bc03ab
The commitFilter variable was assigned but never used. The require() …
alelom Mar 2, 2026
8921197
Improve error handling in `resolve_version_via_semantic_release` func…
alelom Mar 2, 2026
70aed7e
Refactor script path resolution in `resolve_version_via_semantic_rele…
alelom Mar 2, 2026
edbd3f6
Enhance module resolution and error handling in `get-next-version.cjs…
alelom Mar 2, 2026
a291202
Merge branch 'semantic-versioning-implementation' of https://github.c…
alelom Mar 2, 2026
a2b3614
Refactor linting tests to skip format checks in CI/CD environments
alelom Mar 2, 2026
05bad29
Update script path in pyproject.toml to ensure proper resource inclusion
alelom Mar 2, 2026
eeaa156
Enhance backup and restoration logic in `get-next-version.cjs` script
alelom Mar 2, 2026
bb803c9
Implement argument validation and enhance backup handling in `get-nex…
alelom Mar 2, 2026
1797ccc
Add formatting check to CI workflow
alelom Mar 2, 2026
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
123 changes: 123 additions & 0 deletions .cursor/plans/optional_version_+_semantic-release_efed88a6.plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Optional --version with semantic-release (both workflows)

## Scope: two workflows

1. **Workflow 1 – Publishing a subfolder** (monorepo): Build and publish a subfolder of `src/` as its own package. Version today is required via `--version`.
2. **Workflow 2 – Building packages with shared code**: Build the main package (e.g. `src/my_package/`) that imports from `shared/`. Version today comes from `pyproject.toml` (dynamic or static) or `--version` when publishing.

Both workflows should support **optional `--version**`: when omitted, resolve the next version via semantic-release and use it for the build/publish.

## Current behavior

- **CLI** ([`python_package_folder.py`](src/python_package_folder/python_package_folder.py)): For subfolder builds, `--version` is required; the tool exits with an error if it is missing (lines 158–164). For main-package builds, `--version` is optional; version comes from `pyproject.toml` or user.
- **Manager** ([`manager.py`](src/python_package_folder/manager.py)): `prepare_build` defaults version to `"0.0.0"` with a warning when `version` is `None` for subfolders (lines 232–239). `build_and_publish` raises `ValueError` if `version` is missing for a subfolder build (lines 1254–1258). For main package, `version` can be None (no error); then publish uses whatever the build produced (dynamic versioning or static from pyproject).
- **Publisher** ([`publisher.py`](src/python_package_folder/publisher.py)): Filters dist files by `package_name` and `version`; both are required for reliable filtering.

## Target behavior

- `**--version` optional for both workflows**: If `--version` is not provided and a version is needed (subfolder build, or main-package publish), compute the next version using semantic-release, then proceed with that version. If provided, keep current behavior (explicit version).
- **Workflow 1 (subfolder)**: Per-package tags `{package-name}-v{version}` and commits filtered to the subfolder path.
- **Workflow 2 (main package)**: Repo-level tags (e.g. `v{version}`), no path filter; run semantic-release from project root.

## Architecture

```mermaid
flowchart LR
subgraph Workflows
W1[Workflow 1: Subfolder build]
W2[Workflow 2: Main package with shared code]
end
subgraph CLI
A[Build or publish without --version]
B[Resolve version via semantic-release]
C[Build and publish with resolved version]
end
W1 --> A
W2 --> A
A --> B --> C
B --> Node[Node: get-next-version script]
Node --> SR[semantic-release dry-run]
SR --> NextVer[Next version]
NextVer --> C
```

- **Version resolution**: When `--version` is missing and needed (subfolder build, or main-package publish), call a Node script that runs semantic-release in dry-run and prints the next version to stdout.
- **Workflow 1**: Script runs with subfolder path and package name → per-package tag format and path-filtered commits (semantic-release-commit-filter).
- **Workflow 2**: Script runs from project root, no path filter → default tag format `v{version}`; package name from `pyproject.toml` for Publisher filtering only.
- **Fallback**: If Node/semantic-release is unavailable or semantic-release decides there is no release, fail with a clear message and suggest installing semantic-release (and commit-filter for subfolders) or passing `--version` explicitly.

## Implementation options for “get next version”

- **Option A (recommended): Small Node script using semantic-release API**

Add a script (e.g. `scripts/get-next-version.cjs` or under `.release/`) that:

- Takes args: project root, subfolder path (relative or absolute), package name.
- Ensures a minimal `package.json` exists in the subfolder (or in a temp location with correct `name`) so that semantic-release-commit-filter can use `package.name` for `tagFormat` and filter commits by cwd.
- Requires semantic-release and semantic-release-commit-filter, runs semantic-release programmatically with `dryRun: true`, and prints `nextRelease.version` (or “none”) to stdout.

This avoids parsing human-oriented dry-run output and gives a single, stable contract.

- **Option B: Parse `npx semantic-release --dry-run` output**

Run the CLI in dry-run and parse stdout. Possible but brittle (format can change, localization, etc.). Not recommended.

## Key implementation details

1. **Where to run semantic-release**

Run from the **subfolder** directory so that semantic-release-commit-filter’s “current directory” is the subfolder and commits are filtered to that path. Tag format will be `{package.name}-v${version}` from the `package.json` in that directory.

2. **Temporary `package.json` in subfolder**

Python subfolders usually have no `package.json`. Create a temporary one for the version resolution only: `{"name": "<package_name>"}` (same name as used for the Python package). Run semantic-release from the subfolder, then remove the temp file (or overwrite only if we created it). Document that the script may create/remove `package.json` in the subfolder so users are not surprised.

3. **Dependencies**

- No new Python dependencies.
- Document that **Node.js** and **npm** (or **npx**) must be available when using auto-versioning.
- Document (and optionally script) install of semantic-release and semantic-release-commit-filter, e.g. `npm install -g semantic-release semantic-release-commit-filter` or per-repo `package.json` with these as devDependencies.

4. **CLI flow**

- If subfolder build and `args.version` is None:
- Call the version resolver (subprocess: `node scripts/get-next-version.cjs <project_root> <subfolder_path> <package_name>`).
- If resolver returns a version string: use it for the rest of the flow.
- If resolver returns “none” or fails (no release / semantic-release not found / Node error): exit with a clear error suggesting to pass `--version` or to install and configure semantic-release.
- Pass the resolved or explicit version into `build_and_publish` / `prepare_build` as today.

5. **Manager / Publisher**

No change to the contract: they still receive a concrete `version` (either from CLI or from the resolver). Only the CLI and the new resolution step change.

6. **Convention**

Rely on default Angular/conventional commit rules (e.g. `fix:` → patch, `feat:` → minor, `BREAKING CHANGE:` → major). Document that conventional commits are required for auto-versioning; no change to commit format inside this repo unless you add a config file for semantic-release.

## Files to add or touch

| Item | Action |

| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

| New script | Add `scripts/get-next-version.cjs` (or similar) that runs semantic-release in dry-run with commit-filter and prints next version. |

| CLI | In [`python_package_folder.py`](src/python_package_folder/python_package_folder.py): when `is_subfolder and not args.version`, call the resolver; on success set `args.version` (or a local variable) to the resolved version; on failure exit with error. Remove the “version required” error for this case. |

| Manager | In [`manager.py`](src/python_package_folder/manager.py): keep the `ValueError` when `version` is None for subfolder in `build_and_publish` (CLI will always pass a version after resolution). Optionally keep or adjust the “default 0.0.0” in `prepare_build` for programmatic callers who still omit version. |

| Docs | Update README (and any publishing doc) to describe: `--version` optional for subfolders when semantic-release is used, per-package tags, conventional commits, and Node/npm + semantic-release (and commit-filter) setup. |

| Tests | Add tests for: CLI with subfolder and no `--version` (mock or skip if Node/semantic-release missing), and for the resolver helper (or script) when given a fixture repo with tags and conventional commits. |

## Open decisions

- **Script location**: Ship `get-next-version.cjs` inside this repo under `scripts/` (or `.release/`) so that `python-package-folder` can invoke it without requiring the user to add it. The script will `require('semantic-release')` and `require('semantic-release-commit-filter')`; users must have these installed (globally or in a local `package.json` at project root or subfolder).
- **First release / no tag**: If there is no tag for this package yet, semantic-release will use an initial version (e.g. 1.0.0). Confirm desired behavior (e.g. configurable first version or always 1.0.0).
- **No release (no relevant commits)**: If semantic-release determines there is no release, the script should output something like “none” and the CLI should exit with a clear message rather than defaulting to 0.0.0.

## Summary

- Make `--version` optional for subfolder builds by resolving the next version via Node.js semantic-release with per-package tags and path-filtered commits.
- Add a small Node script that runs semantic-release in dry-run and prints the next version; wire it from the CLI when `--version` is omitted.
- Document Node/npm and semantic-release (and semantic-release-commit-filter) as requirements for this mode, and keep explicit `--version` as the fallback when auto-versioning is not available or not desired.
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ jobs:
- name: Run linting
run: uv run ruff check .

- name: Check formatting
continue-on-error: true
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

The formatting check is configured with continue-on-error: true, and tests/test_linting.py skips the formatter check in CI. Together this means formatting regressions won’t fail CI, so ruff format --check becomes advisory only. If formatting should be enforced, remove continue-on-error (or re-enable the test in CI).

Suggested change
continue-on-error: true

Copilot uses AI. Check for mistakes.
run: uv run ruff format --check .

- name: Run type checking
run: uv run basedpyright src/ || true

Expand Down
70 changes: 62 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ cd src/api_package
# Build and publish to TestPyPI with version 1.2.0
python-package-folder --publish testpypi --version 1.2.0

# Or publish to PyPI with automatic version resolution via semantic-release
python-package-folder --publish pypi

# Or publish to PyPI with a custom package name
python-package-folder --publish pypi --version 1.2.0 --package-name "my-api-package"

Expand Down Expand Up @@ -149,6 +152,13 @@ uv add twine

**For secure credential storage**: `keyring` is optional but recommended (install with `pip install keyring`)

**For automatic version resolution**: When using `--version` optional mode (automatic version resolution via semantic-release), you'll need:
- Node.js and npm (or npx)
- semantic-release: `npm install -g semantic-release`
- For subfolder builds: semantic-release-commit-filter: `npm install -g semantic-release-commit-filter`

Alternatively, install these as devDependencies in your project's `package.json`.


## Quick Start

Expand All @@ -162,9 +172,13 @@ Useful for monorepos containing many subfolders that may need publishing as stan
# First cd to the specific subfolder
cd src/subfolder_to_build_and_publish

# Build and publish any subdirectory of your repo to TestPyPi (https://test.pypi.org/)
# Build and publish any subdirectory of your repo to TestPyPi (https://test.pypi.org/)
# Version can be provided explicitly or resolved automatically via semantic-release
python-package-folder --publish testpypi --version 0.0.2

# Or let semantic-release determine the next version automatically (requires semantic-release setup)
python-package-folder --publish testpypi

# Only analyse (no building)
cd src/subfolder_to_build_and_publish
python-package-folder --analyze-only
Expand Down Expand Up @@ -437,33 +451,72 @@ The `--version` option:
**Version Format**: Versions must follow PEP 440 (e.g., `1.2.3`, `1.2.3a1`, `1.2.3.post1`, `1.2.3.dev1`)


### Automatic Version Resolution (semantic-release)

When `--version` is not provided, the tool can automatically determine the next version using semantic-release. This requires Node.js, npm, and semantic-release to be installed.

**For subfolder builds (Workflow 1):**
- Uses per-package tags: `{package-name}-v{version}` (e.g., `my-package-v1.2.3`)
- Filters commits to only those affecting the subfolder path
- Requires `semantic-release-commit-filter` plugin

**For main package builds (Workflow 2):**
- Uses repo-level tags: `v{version}` (e.g., `v1.2.3`)
- Analyzes all commits in the repository

**Setup:**
```bash
# Install semantic-release globally
npm install -g semantic-release

# For subfolder builds, also install semantic-release-commit-filter
npm install -g semantic-release-commit-filter
```

**Usage:**
```bash
# Subfolder build - version resolved automatically
cd src/my_subfolder
python-package-folder --publish pypi

# Main package - version resolved automatically
python-package-folder --publish pypi
```

**Requirements:**
- Conventional commits (e.g., `fix:`, `feat:`, `BREAKING CHANGE:`) are required for semantic-release to determine version bumps
- The tool will fall back to requiring `--version` explicitly if semantic-release is not available or determines no release is needed

### Subfolder Versioning

When building from a subdirectory (not the main `src/` directory), the tool automatically detects the subfolder and sets up the build configuration:

```bash
# Build a subfolder as a separate package (version recommended but not required)
# Build a subfolder as a separate package with explicit version
cd my_project/subfolder_to_build
python-package-folder --version "1.0.0" --publish pypi

# Or let semantic-release determine the version automatically
python-package-folder --publish pypi

# With custom package name
python-package-folder --version "1.0.0" --package-name "my-custom-name" --publish pypi

# Version defaults to "0.0.0" if not specified (with a warning)
python-package-folder --publish pypi
```

For subfolder builds:
- **Automatic detection**: The tool automatically detects subfolder builds
- **Version resolution**:
- If `--version` is provided: Uses the explicit version
- If `--version` is omitted: Attempts to resolve via semantic-release (requires setup)
- If semantic-release is unavailable or determines no release: Requires `--version` explicitly
- **pyproject.toml handling**:
- If `pyproject.toml` exists in subfolder: Uses that file (copied to project root temporarily)
- If no `pyproject.toml` in subfolder: Creates temporary one with correct package structure
- **Version**: Recommended but not required when creating temporary pyproject.toml. If not provided, defaults to `0.0.0` with a warning. Ignored if subfolder has its own `pyproject.toml`.
- **Package name**: Automatically derived from the subfolder name (e.g., `subfolder_to_build` → `subfolder-to-build`). Only used when creating temporary pyproject.toml.
- **Restoration**: Original `pyproject.toml` is restored after build
- **Temporary configuration**: Creates a temporary `pyproject.toml` with:
- Custom package name (from `--package-name` or derived)
- Specified version
- Specified or resolved version
- Correct package path for hatchling
- Dependency group from parent (if `--dependency-group` is specified)
- **Package initialization**: Automatically creates `__init__.py` if the subfolder doesn't have one (required for hatchling)
Expand Down Expand Up @@ -666,7 +719,8 @@ options:
--password PASSWORD Password/token for publishing (will prompt if not provided)
--skip-existing Skip files that already exist on the repository
--version VERSION Set a specific version before building (PEP 440 format).
Required for subfolder builds.
Optional: if omitted, version will be resolved via
semantic-release (requires Node.js and semantic-release setup).
--package-name PACKAGE_NAME
Package name for subfolder builds (default: derived from
source directory name)
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ bump = true
[tool.hatch.build.targets.wheel]
# The source location for the package.
packages = ["src/python_package_folder"]
# Force-include the scripts directory (non-Python files)
# Place scripts inside the package directory so importlib.resources can find them
force-include = { "src/python_package_folder/scripts" = "python_package_folder/scripts" }


# ---- Settings ----
Expand Down
Loading
Loading