From 75ccd88c9350ce382707612ed6230a82ceee214f Mon Sep 17 00:00:00 2001 From: kompre Date: Tue, 21 Oct 2025 15:25:18 +0200 Subject: [PATCH 1/2] chore: fix help message in cli, added version, added github url --- _todo/pending/new-release-workflow.md | 699 ------------------ pyproject.toml | 4 +- .../quarto_batch_convert.py | 60 +- src/quarto_batch_convert/version.py | 25 - tests/manual.py | 8 +- tests/test_quarto_batch_convert.py | 32 +- uv.lock | 2 +- 7 files changed, 79 insertions(+), 751 deletions(-) delete mode 100644 _todo/pending/new-release-workflow.md delete mode 100644 src/quarto_batch_convert/version.py diff --git a/_todo/pending/new-release-workflow.md b/_todo/pending/new-release-workflow.md deleted file mode 100644 index 5211d67..0000000 --- a/_todo/pending/new-release-workflow.md +++ /dev/null @@ -1,699 +0,0 @@ -# New Release Workflow - -**Status**: In Progress -**Branch**: `feature/automated-release-workflow` -**Started**: 2025-10-21 - -## Progress Log - -### 2025-10-21 - -#### Initial Setup -- Proposal approved and moved to pending phase -- Created feature branch `feature/automated-release-workflow` -- Decisions finalized: - - Manual PR approval (no auto-merge requirement) - - Multiple bump labels allowed (applied sequentially) - - Deprecate `python-publish.yml` entirely - - No additional environment approvals beyond PR - - Use PR descriptions for changelog (no automation) - - Continue with CalVer versioning - -#### Phase 1 & 2 Implementation (Completed) -- ✅ Verified all required labels exist in repository (11 bump labels + 2 release labels) -- ✅ Created `.github/workflows/version-bump.yml` - - Triggers on PR label events - - Applies version bumps sequentially - - Commits changes back to PR branch - - Posts version change comment -- ✅ Created `.github/workflows/release.yml` - - Checks for release labels on merged PRs - - Builds and tests package - - Publishes to PyPI or TestPyPI - - Creates git tag and GitHub Release -- ✅ Deleted deprecated `.github/workflows/python-publish.yml` -- ✅ Created `.github/RELEASE.md` documentation -- ✅ Updated `CLAUDE.md` with release workflow section - -#### Bug Fixes (Post-merge) -- 🐛 Fixed version bump workflow to apply multiple bumps in single command - - Issue: PR #11 had 3 workflow runs (one per label) all failing - - Root cause: Workflow triggered on `synchronize` + bumps applied sequentially - - Solution: Removed `synchronize` trigger, build single `uv version --bump x --bump y` command - - Commit: 025ada7 -- 🐛 Fixed release workflow missing checkout step - - Issue: `gh pr view` failed with "not a git repository" - - Solution: Added checkout step before checking labels - - Commit: 9fdef71 -- ✅ Both fixes merged to `dev` via PR #13 - -#### Next Steps -- Test version bump workflow on a test PR -- Configure GitHub Environments (pypi, testpypi) -- Configure PyPI Trusted Publishing -- Test release to TestPyPI -- Configure branch protection rules -- Test production release to PyPI - ---- - -## Original Objective -We should move toward branch protection and automate release. - -A PR should be able to start the release workflow after it's merged to main. - -The steps should be the following: - -1. PR gets created -2. CI test action starts running -3. If it's a release type, label gets applied, with 2 types: - 1. `release` -> publish to PyPI - 2. `test-release` -> publish to TestPyPI - 3. Also version `bump` label gets added (create a new label for each bump type `uv version --bump` supports; multiple bump labels can be added simultaneously): this needs to be its own action workflow, that will be a requirement before publishing the package -4. When CI and version bump workflow are completed, then PR can be merged -5. The release should also create a new tag with the same version number of the package just published - -## Analysis - -### Current State -- **Existing workflow**: `.github/workflows/python-publish.yml` publishes to PyPI on release/tag events -- **Test workflow**: `.github/workflows/test.yml` runs on PRs to main -- **No branch protection**: Main branch has no protection rules -- **No automated versioning**: Manual version updates required -- **No pre-merge version bumping**: Versions updated after merge - -### Requirements Analysis - -1. **Label-driven workflow**: Use labels to control release type and version bumping -2. **Version bump automation**: Automated workflow to update version based on labels -3. **Status checks**: Version bump must complete before merge is allowed -4. **Branch protection**: Enforce required checks before merging -5. **Post-merge publishing**: Automatic PyPI/TestPyPI publish after merge -6. **Tag creation**: Automatic git tag matching published version - -### `uv version --bump` Options -According to `uv version --help`, supported bump types: -- `major` - X.0.0 (breaking changes) -- `minor` - 0.X.0 (new features) -- `patch` - 0.0.X (bug fixes) -- `stable` - Remove pre-release suffix -- `alpha` - Add/increment alpha version -- `beta` - Add/increment beta version -- `rc` - Add/increment release candidate -- `post` - Add/increment post-release -- `dev` - Add/increment development version - -## Implementation Plan - -### Step 1: Create Version Bump Labels -All labels listed here should be created (see `add-label-for-pr` task): - -**Release Type Labels:** -- `release` - Publish to PyPI -- `test-release` - Publish to TestPyPI - -**Version Bump Labels** (multiple can be applied): -- `bump:major` -- `bump:minor` -- `bump:patch` -- `bump:stable` -- `bump:alpha` -- `bump:beta` -- `bump:rc` -- `bump:post` -- `bump:dev` - -### Step 2: Create Version Bump Workflow -**File**: `.github/workflows/version-bump.yml` - -This workflow: -- Triggers when bump labels are added to PR -- Runs `uv version --bump ` for each bump label -- Commits the updated `pyproject.toml` and `uv.lock` back to the PR branch -- Reports success/failure as a status check - -```yaml -name: Version Bump - -on: - pull_request: - types: [labeled, synchronize] - branches: [main] - -jobs: - bump-version: - # Only run if PR has at least one bump: label - if: | - contains(github.event.pull_request.labels.*.name, 'bump:major') || - contains(github.event.pull_request.labels.*.name, 'bump:minor') || - contains(github.event.pull_request.labels.*.name, 'bump:patch') || - contains(github.event.pull_request.labels.*.name, 'bump:stable') || - contains(github.event.pull_request.labels.*.name, 'bump:alpha') || - contains(github.event.pull_request.labels.*.name, 'bump:beta') || - contains(github.event.pull_request.labels.*.name, 'bump:rc') || - contains(github.event.pull_request.labels.*.name, 'bump:post') || - contains(github.event.pull_request.labels.*.name, 'bump:dev') - - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - - steps: - - name: Checkout PR branch - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version-file: ".python-version" - - - name: Install uv - uses: astral-sh/setup-uv@v6 - with: - version: "latest" - - - name: Get current version - id: current-version - run: | - CURRENT_VERSION=$(uv version --short) - echo "version=${CURRENT_VERSION}" >> $GITHUB_OUTPUT - echo "Current version: ${CURRENT_VERSION}" - - - name: Apply version bumps - id: bump - env: - GH_TOKEN: ${{ github.token }} - run: | - # Get all labels on the PR - LABELS=$(gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name') - - # Extract bump labels in order of precedence - BUMP_TYPES=() - - for label in major minor patch stable alpha beta rc post dev; do - if echo "$LABELS" | grep -q "^bump:${label}$"; then - BUMP_TYPES+=("$label") - fi - done - - if [ ${#BUMP_TYPES[@]} -eq 0 ]; then - echo "No bump labels found, skipping version bump" - exit 0 - fi - - echo "Applying bumps: ${BUMP_TYPES[*]}" - - # Apply each bump sequentially - for bump_type in "${BUMP_TYPES[@]}"; do - echo "Bumping: $bump_type" - uv version --bump "$bump_type" - done - - NEW_VERSION=$(uv version --short) - echo "new_version=${NEW_VERSION}" >> $GITHUB_OUTPUT - echo "New version: ${NEW_VERSION}" - - - name: Commit version bump - if: steps.bump.outputs.new_version != '' - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - git add pyproject.toml uv.lock - - if git diff --staged --quiet; then - echo "No changes to commit" - else - git commit -m "Bump version to ${{ steps.bump.outputs.new_version }}" - git push - fi - - - name: Comment on PR - if: steps.bump.outputs.new_version != '' - env: - GH_TOKEN: ${{ github.token }} - run: | - gh pr comment ${{ github.event.pull_request.number }} --body \ - "✅ Version bumped: \`${{ steps.current-version.outputs.version }}\` → \`${{ steps.bump.outputs.new_version }}\`" -``` - -**Key Features:** -- Applies bumps in order of labels -- Commits changes back to PR branch -- Posts comment with version change -- Acts as required status check - -### Step 3: Create Release Workflow -**File**: `.github/workflows/release.yml` - -This workflow: -- Triggers when PR with `release` or `test-release` label is merged to main -- Builds the package -- Publishes to PyPI or TestPyPI based on label -- Creates a git tag matching the published version -- Creates a GitHub Release - -```yaml -name: Release - -on: - pull_request: - types: [closed] - branches: [main] - -jobs: - check-release-label: - if: github.event.pull_request.merged == true - runs-on: ubuntu-latest - outputs: - should-release: ${{ steps.check.outputs.release }} - is-test: ${{ steps.check.outputs.test }} - steps: - - name: Check for release labels - id: check - env: - GH_TOKEN: ${{ github.token }} - run: | - LABELS=$(gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name') - - if echo "$LABELS" | grep -q "^release$"; then - echo "release=true" >> $GITHUB_OUTPUT - echo "test=false" >> $GITHUB_OUTPUT - elif echo "$LABELS" | grep -q "^test-release$"; then - echo "release=true" >> $GITHUB_OUTPUT - echo "test=true" >> $GITHUB_OUTPUT - else - echo "release=false" >> $GITHUB_OUTPUT - echo "test=false" >> $GITHUB_OUTPUT - fi - - build: - needs: check-release-label - if: needs.check-release-label.outputs.should-release == 'true' - runs-on: ubuntu-latest - steps: - - name: Checkout main branch - uses: actions/checkout@v4 - with: - ref: main - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version-file: ".python-version" - - - name: Install uv - uses: astral-sh/setup-uv@v6 - with: - version: "latest" - - - name: Install dependencies - run: uv sync --locked --all-extras --dev - - - name: Run tests - run: uv run pytest tests -v - - - name: Build package - run: uv build - - - name: Get version - id: version - run: | - VERSION=$(uv version --short) - echo "version=${VERSION}" >> $GITHUB_OUTPUT - echo "Package version: ${VERSION}" - - - name: Upload distributions - uses: actions/upload-artifact@v4 - with: - name: release-dists - path: dist/ - - outputs: - version: ${{ steps.version.outputs.version }} - - publish-pypi: - needs: [check-release-label, build] - if: needs.check-release-label.outputs.is-test == 'false' - runs-on: ubuntu-latest - permissions: - id-token: write - environment: - name: pypi - url: https://pypi.org/p/quarto-batch-convert - - steps: - - name: Download distributions - uses: actions/download-artifact@v4 - with: - name: release-dists - path: dist/ - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - packages-dir: dist/ - - publish-testpypi: - needs: [check-release-label, build] - if: needs.check-release-label.outputs.is-test == 'true' - runs-on: ubuntu-latest - permissions: - id-token: write - environment: - name: testpypi - url: https://test.pypi.org/p/quarto-batch-convert - - steps: - - name: Download distributions - uses: actions/download-artifact@v4 - with: - name: release-dists - path: dist/ - - - name: Publish to TestPyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - repository-url: https://test.pypi.org/legacy/ - packages-dir: dist/ - - create-tag-and-release: - needs: [check-release-label, build, publish-pypi] - if: | - always() && - needs.check-release-label.outputs.should-release == 'true' && - (needs.publish-pypi.result == 'success' || needs.publish-testpypi.result == 'success') - runs-on: ubuntu-latest - permissions: - contents: write - - steps: - - name: Checkout main branch - uses: actions/checkout@v4 - with: - ref: main - - - name: Create and push tag - env: - VERSION: ${{ needs.build.outputs.version }} - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - git tag -a "v${VERSION}" -m "Release v${VERSION}" - git push origin "v${VERSION}" - - - name: Create GitHub Release - env: - GH_TOKEN: ${{ github.token }} - VERSION: ${{ needs.build.outputs.version }} - run: | - RELEASE_TYPE="${{ needs.check-release-label.outputs.is-test == 'true' && 'test' || 'production' }}" - - gh release create "v${VERSION}" \ - --title "v${VERSION}" \ - --notes "Release v${VERSION} (${RELEASE_TYPE}) - - Published from PR #${{ github.event.pull_request.number }} - - See [CHANGELOG](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for details." \ - --draft=false \ - --prerelease=${{ needs.check-release-label.outputs.is-test == 'true' }} -``` - -**Key Features:** -- Only runs on merged PRs with release labels -- Separate publish jobs for PyPI vs TestPyPI -- Creates git tag after successful publish -- Creates GitHub Release with version tag -- Test releases marked as pre-release - -### Step 4: Configure Branch Protection -**Repository Settings** → **Branches** → **Branch protection rules** for `main`: - -Enable: -- ✅ Require a pull request before merging - - ✅ Require approvals: 1 (optional, adjust as needed) - - ✅ Dismiss stale pull request approvals when new commits are pushed -- ✅ Require status checks to pass before merging - - ✅ Require branches to be up to date before merging - - **Required checks:** - - `test` (from test.yml) - - `bump-version` (from version-bump.yml, if bump labels present) -- ✅ Do not allow bypassing the above settings - -**Notes:** -- The `bump-version` check should only be required if bump labels are present -- May need to use branch protection rules API or rulesets for conditional requirements - -### Step 5: Configure PyPI Environments -**GitHub Repository Settings** → **Environments** - -Create two environments: - -**Environment: `pypi`** -- Required reviewers: (optional, for production safety) -- Deployment branches: Only `main` - -**Environment: `testpypi`** -- Required reviewers: None -- Deployment branches: Only `main` - -**PyPI Trusted Publishing:** -Both PyPI and TestPyPI need to be configured for trusted publishing: - -1. Go to PyPI account settings → Publishing -2. Add publisher: - - Repository: `kompre/quarto_batch_convert` - - Workflow: `release.yml` - - Environment: `pypi` (or `testpypi`) - -### Step 6: Remove Existing python-publish.yml -**Decision**: Deprecate the existing `python-publish.yml` workflow entirely. - -**Action**: Delete `.github/workflows/python-publish.yml` to enforce PR-based release workflow only. - -**Rationale**: The new release workflow provides all necessary functionality. Keeping the old workflow could cause confusion or accidental releases. - -### Step 7: Documentation - -**Create `.github/RELEASE.md`:** -```markdown -# Release Process - -## Automated Release Workflow - -### 1. Create PR with Changes -Create a PR targeting the `main` branch with your changes. - -### 2. Add Version Bump Label -Add ONE OR MORE of these labels to indicate version change: -- `bump:major` - Breaking changes (2025.8.11 → 2026.0.0) -- `bump:minor` - New features (2025.8.11 → 2025.9.0) -- `bump:patch` - Bug fixes (2025.8.11 → 2025.8.12) -- `bump:stable` - Remove pre-release suffix -- `bump:alpha`, `bump:beta`, `bump:rc`, `bump:post`, `bump:dev` - Pre-release - -The version-bump workflow will automatically update `pyproject.toml` and commit to your PR. - -### 3. Add Release Type Label -Add ONE of these labels: -- `release` - Publish to PyPI (production) -- `test-release` - Publish to TestPyPI (testing) - -### 4. Wait for Checks -Required checks must pass: -- ✅ Tests (`test.yml`) -- ✅ Version bump (`version-bump.yml`, if bump labels present) - -### 5. Merge PR -Once approved and checks pass, merge the PR. - -### 6. Automatic Publishing -After merge: -- Package is built and tested -- Published to PyPI or TestPyPI -- Git tag created (e.g., `v2025.9.0`) -- GitHub Release created - -## Example: Patch Release to PyPI - -1. Create PR: "Fix bug in file collection" -2. Add labels: `bump:patch`, `release` -3. Version bump workflow: `2025.8.11` → `2025.8.12` -4. Merge PR -5. Release workflow publishes to PyPI and creates tag `v2025.8.12` - -## Example: Test Release - -1. Create PR: "Test new feature" -2. Add labels: `bump:minor`, `bump:beta`, `test-release` -3. Version bump workflow: `2025.8.11` → `2025.9.0b1` -4. Merge PR -5. Release workflow publishes to TestPyPI (not production PyPI) -``` - -**Update CLAUDE.md** with release workflow section. - -## Workflow Diagram - -``` -┌─────────────────┐ -│ Create PR │ -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ Add Labels: │ -│ - bump:* │──┐ -│ - release OR │ │ -│ test-release │ │ -└────────┬────────┘ │ - │ │ - │ ▼ - │ ┌──────────────────┐ - │ │ version-bump │ - │ │ workflow runs │ - │ │ (required check)│ - │ └────────┬─────────┘ - │ │ - ▼ ▼ -┌─────────────────────────────┐ -│ test workflow runs │ -│ (required check) │ -└────────┬────────────────────┘ - │ - ▼ -┌─────────────────┐ -│ PR Approved │ -│ All checks ✅ │ -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ Merge to main │ -└────────┬────────┘ - │ - ▼ -┌─────────────────────────────┐ -│ release workflow triggers │ -│ - Build package │ -│ - Run tests │ -│ - Publish to PyPI/TestPyPI │ -│ - Create git tag │ -│ - Create GitHub Release │ -└─────────────────────────────┘ -``` - -## Success Criteria - -- [ ] All version bump labels (`bump:*`) exist in repository -- [ ] Release type labels (`release`, `test-release`) exist in repository -- [ ] `version-bump.yml` workflow created and functional -- [ ] `release.yml` workflow created and functional -- [ ] Branch protection rules configured on `main` branch -- [ ] PyPI and TestPyPI environments configured -- [ ] PyPI trusted publishing configured for both environments -- [ ] Documentation created (`.github/RELEASE.md`) -- [ ] `CLAUDE.md` updated with release workflow guidance -- [ ] Successfully complete a test release to TestPyPI -- [ ] Successfully complete a production release to PyPI - -## Decisions - -1. **Branch Protection Strictness**: - - **Decision**: User will manually approve PRs or toggle auto-merge - - **Implementation**: Configure branch protection to require 1 approval (can be bypassed by repo admin) - -2. **Version Bump Conflict Resolution**: - - **Decision**: Allow multiple bump labels; they will be applied in sequence - - **Behavior**: Workflow triggers on label events, applies all present bump labels in order (major, minor, patch, stable, alpha, beta, rc, post, dev) - - **Note**: Order matters only if labels are added separately (triggering workflow multiple times). If added together, all bumps apply in single workflow run. - -3. **Existing python-publish.yml**: - - **Decision**: Deprecate the existing `python-publish.yml` workflow - - **Implementation**: Remove the file entirely to enforce PR-based release workflow only - -4. **TestPyPI vs PyPI Environment Protection**: - - **Decision**: No additional environment approvals beyond PR approval - - **Clarification**: GitHub Environments can require separate approval for deployment (independent of PR approval). We will NOT use this feature - PR approval is sufficient. - - **Implementation**: Create environments without required reviewers - -5. **Automated Changelog**: - - **Decision**: Use PR descriptions for release notes (no automated changelog generation) - - **Implementation**: Manual changelog updates in PR body, which can be referenced in GitHub Release notes - -6. **Version Scheme**: - - **Decision**: Continue using current CalVer (YYYY.M.D) scheme - - **Note**: All bump types still work with CalVer (patch increments day, minor increments month, etc.) - -## Estimated Complexity -**High** - Complex multi-workflow system with dependencies, branch protection, and external integrations (PyPI). Requires careful testing and likely multiple iterations. - -## Implementation Phases - -**Phase 1: Labels and Version Bump** (Can be done first) -- Create all labels (via `add-label-for-pr` task) -- Implement `version-bump.yml` -- Test on draft PR - -**Phase 2: Release Workflow** (Depends on Phase 1) -- Implement `release.yml` -- Configure TestPyPI environment -- Test release to TestPyPI - -**Phase 3: Branch Protection** (After Phase 1 & 2 working) -- Enable branch protection on main -- Add required status checks -- Test enforcement - -**Phase 4: Production Release** (Final validation) -- Configure PyPI environment -- Perform production release to PyPI -- Validate tag and GitHub Release creation - -## Related Tasks -- **Dependency**: `add-label-for-pr` - Must create labels before workflows can use them -- **Related**: `fix-test-workflow` - Test workflow must be working for branch protection - -## Risks and Mitigations - -| Risk | Mitigation | -|------|------------| -| Workflow publishes wrong version | Version bump must be required check; publish job reads version from built package | -| Multiple bump labels cause confusion | Apply in precedence order; document expected behavior; consider enforcing single label | -| PyPI publish fails but tag created | Tag creation happens AFTER successful publish | -| Branch protection blocks emergency fixes | Keep manual-release.yml as fallback; admin bypass available | -| Trusted publishing not configured | Detailed documentation; test with TestPyPI first | - -## Testing Plan - -1. **Test Version Bump Workflow**: - - Create test PR with `bump:patch` label - - Verify version updated in PR - - Verify commit pushed to PR branch - -2. **Test TestPyPI Release**: - - Create test PR with `bump:patch` and `test-release` labels - - Merge PR - - Verify package published to TestPyPI - - Verify tag created - - Verify GitHub Release created (marked as pre-release) - -3. **Test Production Release**: - - Create PR with `bump:minor` and `release` labels - - Merge PR - - Verify package published to PyPI - - Verify tag created - - Verify GitHub Release created - -4. **Test Branch Protection**: - - Create PR without labels - - Verify cannot merge without required checks - - Add bump label - - Verify version bump check required and passes - - Verify can now merge diff --git a/pyproject.toml b/pyproject.toml index 1d92c50..78154d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "quarto-batch-convert" -version = "2025.9.1rc1" +version = "2025.9.1" description = "Converts multiple Jupyter notebooks to Quarto documents at once" readme = "README.md" license = "MIT" @@ -23,7 +23,7 @@ dev = [ [project.scripts] quarto-batch-convert = "quarto_batch_convert.quarto_batch_convert:convert_files" -qbc = "quarto_batch_convert.quarto_batch_convert:convert_files" +qbc = "quarto_batch_convert.quarto_batch_convert:quarto_batch_convert" [build-system] requires = ["uv_build>=0.8.9,<0.9.0"] diff --git a/src/quarto_batch_convert/quarto_batch_convert.py b/src/quarto_batch_convert/quarto_batch_convert.py index 1a08001..7a6119f 100644 --- a/src/quarto_batch_convert/quarto_batch_convert.py +++ b/src/quarto_batch_convert/quarto_batch_convert.py @@ -7,8 +7,31 @@ import re from typing import List, Optional from pathlib import Path - -from .version import __version__ +from importlib.metadata import version, PackageNotFoundError + +def get_package_info() -> str: + """Get formatted package information for epilogue display.""" + from importlib.metadata import metadata + try: + meta = metadata("quarto-batch-convert") + pkg_version = meta.get("Version", "unknown") + author = meta.get("Author", "kompre") + return f"quarto-batch-convert v{pkg_version} | by {author} | https://github.com/kompre/quarto_batch_convert" + except PackageNotFoundError: + return "quarto-batch-convert | https://github.com/kompre/quarto_batch_convert" + +def get_epilog() -> str: + """Get formatted epilog with examples and package info.""" + return f"""Examples: qbc . -r -m "^__/_" + +{get_package_info()}""" + +def get_version() -> str: + """Get package version from metadata.""" + try: + return version("quarto-batch-convert") + except PackageNotFoundError: + return "unknown" def check_quarto_installation() -> None: @@ -131,7 +154,11 @@ def convert_file( subprocess.run(["quarto", "convert", file, "--output", new_file_path]) -@click.command(no_args_is_help=True) +@click.command( + no_args_is_help=True, + epilog=get_epilog(), + help="Batch quarto convert multiple .ipynb to .qmd files or vice versa", +) @click.argument( "input_paths", nargs=-1, @@ -169,8 +196,30 @@ def convert_file( is_flag=True, help="Search files recursively when input is a directory", ) -@click.version_option(version=__version__, prog_name="Quarto Batch Converter") +@click.version_option(version=get_version(), prog_name="Quarto Batch Converter") @click.pass_context +def quarto_batch_convert( + ctx: click.Context, + input_paths: tuple, + qmd_to_ipynb: bool, + match_replace_pattern: Optional[str], + prefix: str, + keep_extension: bool, + output_path: Optional[str], + recursive: bool, +) -> None: + """ CLI wrapper for convert_files - see help parameters @click.command decorator""" + return convert_files( + ctx, + input_paths, + qmd_to_ipynb, + match_replace_pattern, + prefix, + keep_extension, + output_path, + recursive, + ) + def convert_files( ctx: click.Context, input_paths: tuple, @@ -308,5 +357,8 @@ def convert_files( print("-" * len(text)) + + + if __name__ == "__main__": pass diff --git a/src/quarto_batch_convert/version.py b/src/quarto_batch_convert/version.py deleted file mode 100644 index 4bd29a2..0000000 --- a/src/quarto_batch_convert/version.py +++ /dev/null @@ -1,25 +0,0 @@ -# import tomllib -# from pathlib import Path - -# # def get_version(): -# # """Read version from pyproject.toml.""" -# # pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml" - -# # if not pyproject_path.exists(): -# # return "unknown" - -# # try: -# # with open(pyproject_path, "rb") as f: -# # pyproject = tomllib.load(f) -# # return pyproject["project"]["version"] -# # except (KeyError, FileNotFoundError, tomllib.TOMLDecodeError): -# # return "unknown" - -# # __version__ = get_version() - - -import importlib.metadata - -__version__ = importlib.metadata.version("quarto-batch-convert") - -# print(__version__) \ No newline at end of file diff --git a/tests/manual.py b/tests/manual.py index 731a549..72e5f1b 100644 --- a/tests/manual.py +++ b/tests/manual.py @@ -1,20 +1,20 @@ -from quarto_batch_convert.quarto_batch_convert import convert_files +from quarto_batch_convert.quarto_batch_convert import quarto_batch_convert import os import glob def change_dir(): os.chdir("tests/assets") - convert_files(["*"]) + quarto_batch_convert(["*"]) def is_recursive(): - convert_files( + quarto_batch_convert( ["tests/assets"] ) def simple_run(): # files = glob.glob("C:/Users/s.follador/Desktop/__canc__/qbc/**/*.ipynb", recursive=True) files = ["C:/Users/s.follador/Desktop/__canc__/qbc/**/*"] - convert_files( + quarto_batch_convert( [ *files ] diff --git a/tests/test_quarto_batch_convert.py b/tests/test_quarto_batch_convert.py index 259700c..58c351d 100644 --- a/tests/test_quarto_batch_convert.py +++ b/tests/test_quarto_batch_convert.py @@ -2,7 +2,7 @@ import shutil import pytest from click.testing import CliRunner -from quarto_batch_convert.quarto_batch_convert import convert_files +from quarto_batch_convert.quarto_batch_convert import quarto_batch_convert import glob from contextlib import contextmanager from typing import Generator @@ -78,7 +78,7 @@ def test_single_match_no_replace(setup_teardown_test_env: str) -> None: input_files = glob.glob(test_dir + "/**/*", recursive=True) - result = runner.invoke(convert_files, [*input_files, "-m", "test_2"]) + result = runner.invoke(quarto_batch_convert, [*input_files, "-m", "test_2"]) assert result.exit_code == 0 assert "test_2.ipynb" in result.output @@ -103,7 +103,7 @@ def test_match_and_replace_pattern(setup_teardown_test_env: str) -> None: input_file = os.path.join(test_dir, "notebooks/_test_1.ipynb") - result = runner.invoke(convert_files, [input_file, "-o", output_dir, "-m", "^_/REPLACED_"]) + result = runner.invoke(quarto_batch_convert, [input_file, "-o", output_dir, "-m", "^_/REPLACED_"]) assert result.exit_code == 0 assert os.path.exists(os.path.join(output_dir, os.path.dirname(input_file), "REPLACED_test_1.qmd")) @@ -120,7 +120,7 @@ def test_invalid_regex_pattern() -> None: an error code when an invalid pattern is provided. """ runner = CliRunner() - result = runner.invoke(convert_files, ["./", "-m", "[invalid"]) + result = runner.invoke(quarto_batch_convert, ["./", "-m", "[invalid"]) assert result.exit_code != 0 assert "Invalid regex pattern" in result.output @@ -139,7 +139,7 @@ def test_no_match_found(setup_teardown_test_env: str) -> None: input_files = glob.glob(test_dir + "/**/*", recursive=True) - result = runner.invoke(convert_files, [*input_files, "-m", "non_existent_pattern"]) + result = runner.invoke(quarto_batch_convert, [*input_files, "-m", "non_existent_pattern"]) assert result.exit_code != 0 assert "No files found matching the regex pattern" in result.output @@ -156,7 +156,7 @@ def test_prefix_option(setup_teardown_test_env: str) -> None: test_dir = setup_teardown_test_env input_file = os.path.join(test_dir, "notebooks/_test_1.ipynb") - result = runner.invoke(convert_files, [input_file, "-p", "prefix_"]) + result = runner.invoke(quarto_batch_convert, [input_file, "-p", "prefix_"]) assert result.exit_code == 0 assert os.path.exists(os.path.join(test_dir, "notebooks/prefix__test_1.qmd")) @@ -175,7 +175,7 @@ def test_keep_extension_option(setup_teardown_test_env: str) -> None: input_file = os.path.join(test_dir, "file_in_root.ipynb") - result = runner.invoke(convert_files, [input_file, "-k"]) + result = runner.invoke(quarto_batch_convert, [input_file, "-k"]) assert result.exit_code == 0 assert os.path.exists(os.path.join(os.path.dirname(input_file), "file_in_root.ipynb.qmd")) @@ -193,7 +193,7 @@ def test_convert_qmd_to_ipynb(setup_teardown_test_env: str) -> None: test_dir = setup_teardown_test_env input_file = os.path.join(test_dir, "notebooks", "TEST.qmd") - result = runner.invoke(convert_files, [input_file, "-q"]) + result = runner.invoke(quarto_batch_convert, [input_file, "-q"]) assert result.exit_code == 0 assert os.path.exists(os.path.join(os.path.dirname(input_file), "TEST.ipynb")) @@ -213,7 +213,7 @@ def test_file_in_cwd(setup_teardown_test_env: str) -> None: with change_dir(test_dir): input_files = glob.glob("*", recursive=True) - result = runner.invoke(convert_files, [*input_files]) + result = runner.invoke(quarto_batch_convert, [*input_files]) assert result.exit_code == 0 # assert "input_path cannot be empty" in result.output @@ -232,7 +232,7 @@ def test_prefix_to_new_dir(setup_teardown_test_env: str) -> None: input_file = os.path.join(test_dir, "notebooks/_test_1.ipynb") input_prefix = "PREFIX/" - result = runner.invoke(convert_files, [input_file, "-p", input_prefix]) + result = runner.invoke(quarto_batch_convert, [input_file, "-p", input_prefix]) file_name, _ = os.path.splitext(os.path.basename(input_file)) assert result.exit_code == 0 @@ -252,7 +252,7 @@ def test_prefix_to_nested_dir(setup_teardown_test_env: str) -> None: input_file = os.path.join(test_dir, "notebooks/_test_1.ipynb") input_prefix = "../PREFIX/" - result = runner.invoke(convert_files, [input_file, "-p", input_prefix]) + result = runner.invoke(quarto_batch_convert, [input_file, "-p", input_prefix]) file_name, _ = os.path.splitext(os.path.basename(input_file)) assert result.exit_code == 0 @@ -272,7 +272,7 @@ def test_output_path(setup_teardown_test_env: str) -> None: output_dir = os.path.join(test_dir, "output") input_file = os.path.join(test_dir, "notebooks/_test_1.ipynb") - result = runner.invoke(convert_files, [input_file, "-o", output_dir]) + result = runner.invoke(quarto_batch_convert, [input_file, "-o", output_dir]) file_name, _ = os.path.splitext(os.path.basename(input_file)) @@ -294,7 +294,7 @@ def test_recursive_option_with_nested_directory() -> None: try: # Run with recursive flag - result = runner.invoke(convert_files, [test_dir, "-r", "-o", output_dir]) + result = runner.invoke(quarto_batch_convert, [test_dir, "-r", "-o", output_dir]) assert result.exit_code == 0 @@ -327,7 +327,7 @@ def test_non_recursive_option_ignores_subdirectories() -> None: try: # Run WITHOUT recursive flag - result = runner.invoke(convert_files, [test_dir, "-o", output_dir]) + result = runner.invoke(quarto_batch_convert, [test_dir, "-o", output_dir]) assert result.exit_code == 0 @@ -358,7 +358,7 @@ def test_recursive_with_match_pattern() -> None: try: # Run with recursive flag and match pattern - result = runner.invoke(convert_files, [test_dir, "-r", "-o", output_dir, "-m", "__test3"]) + result = runner.invoke(quarto_batch_convert, [test_dir, "-r", "-o", output_dir, "-m", "__test3"]) assert result.exit_code == 0 @@ -389,7 +389,7 @@ def test_recursive_preserves_directory_structure() -> None: try: # Run with recursive flag - result = runner.invoke(convert_files, [test_dir, "-r", "-o", output_dir]) + result = runner.invoke(quarto_batch_convert, [test_dir, "-r", "-o", output_dir]) assert result.exit_code == 0 diff --git a/uv.lock b/uv.lock index 7b580bc..da994a5 100644 --- a/uv.lock +++ b/uv.lock @@ -81,7 +81,7 @@ wheels = [ [[package]] name = "quarto-batch-convert" -version = "2025.9.1rc1" +version = "2025.9.1" source = { editable = "." } dependencies = [ { name = "click" }, From b83f91ac68f1aad94855625fa294494ad35c5d20 Mon Sep 17 00:00:00 2001 From: kompre Date: Tue, 21 Oct 2025 15:35:53 +0200 Subject: [PATCH 2/2] new file: _todo/completed/2025-10-21/fix-workflow-status-reporting.md new file: _todo/completed/2025-10-21/new-release-workflow.md --- .../fix-workflow-status-reporting.md | 225 ++++++ .../2025-10-21/new-release-workflow.md | 717 ++++++++++++++++++ 2 files changed, 942 insertions(+) create mode 100644 _todo/completed/2025-10-21/fix-workflow-status-reporting.md create mode 100644 _todo/completed/2025-10-21/new-release-workflow.md diff --git a/_todo/completed/2025-10-21/fix-workflow-status-reporting.md b/_todo/completed/2025-10-21/fix-workflow-status-reporting.md new file mode 100644 index 0000000..a7be18b --- /dev/null +++ b/_todo/completed/2025-10-21/fix-workflow-status-reporting.md @@ -0,0 +1,225 @@ +# Fix Workflow Status Reporting and Auto-Merge + +## Problem Analysis + +### Current Issues + +1. **workflow_run doesn't report status to PR** + - Test workflow triggered via `workflow_run` runs in **main branch context** + - Status check not associated with PR commit + - Branch protection can't see the test result + - Auto-merge blocked indefinitely + +2. **Version bump triggers multiple times** + - Concurrency control working but cancellations happen after work starts + - First run (25s) commits version before being cancelled + - Second run commits again (different version) + - Wasteful and confusing + +3. **Root cause: GITHUB_TOKEN limitations** + - Commits made with `secrets.GITHUB_TOKEN` deliberately **don't trigger workflows** + - GitHub's safeguard against infinite workflow loops + - We're fighting against this design + +### Why workflow_run Doesn't Work + +From GitHub docs and common issues: +- `workflow_run` executes in the context of the **default branch** (main) +- It does NOT run "on behalf of" the PR +- Status checks created are for the wrong commit SHA +- Branch protection rules on PR don't see these checks + +## Best Practice Solutions + +### Option A: Use GitHub App or PAT (Recommended) + +**How it works:** +1. Create GitHub App or use Personal Access Token (PAT) +2. Store token as repository secret (e.g., `BOT_TOKEN`) +3. Use this token for version bump commits instead of `GITHUB_TOKEN` +4. Commits from this token **DO trigger workflows normally** +5. Test runs automatically, reports to PR, auto-merge works + +**Pros:** +- ✅ Clean, standard solution used by major projects +- ✅ No workflow_run hacks +- ✅ Status checks report correctly +- ✅ No manual API calls needed + +**Cons:** +- ⚠️ Requires creating GitHub App or PAT +- ⚠️ Additional secret to manage +- ⚠️ Security consideration (token has write access) + +**Implementation:** +```yaml +# version-bump.yml +- uses: actions/checkout@v4 + with: + token: ${{ secrets.BOT_TOKEN }} # Instead of GITHUB_TOKEN + +# Rest of workflow unchanged - commits will trigger test.yml normally +``` + +**Examples in the wild:** +- Renovate Bot (uses GitHub App) +- Dependabot (uses GitHub App) +- Many semantic-release setups (use PAT) + +### Option B: Manual Status Check via API + +**How it works:** +1. Version bump workflow commits changes +2. Version bump workflow manually creates passing "test" status via API +3. References the test run that already passed before version bump +4. Auto-merge sees the status and proceeds + +**Pros:** +- ✅ No additional tokens needed +- ✅ Uses existing test results + +**Cons:** +- ⚠️ Hacky - we're asserting tests pass without re-running them +- ⚠️ Tests don't actually run on bumped version +- ⚠️ Complex API calls in workflow + +**Implementation:** +```yaml +# version-bump.yml - after commit +- name: Create test status check + env: + GH_TOKEN: ${{ github.token }} + run: | + gh api repos/${{ github.repository }}/statuses/${{ github.event.pull_request.head.sha }} \ + -f state=success \ + -f context=test \ + -f description="Tests passed before version bump" +``` + +### Option C: Remove Auto-Merge (Simplest) + +**How it works:** +1. Version bump commits changes +2. User reviews the version bump +3. User manually merges (or clicks merge button) +4. Release workflow triggers + +**Pros:** +- ✅ Extremely simple +- ✅ No workflow complexity +- ✅ Human verification of version +- ✅ No token management + +**Cons:** +- ⚠️ Not fully automated +- ⚠️ Requires manual action + +**Implementation:** +- Remove auto-merge enable step from version-bump.yml +- User merges when ready + +### Option D: workflow_dispatch Chain + +**How it works:** +1. Version bump commits changes +2. Version bump triggers test via `workflow_dispatch` +3. Test workflow runs in PR context (because we pass the ref) +4. Status reported correctly + +**Pros:** +- ✅ No tokens needed +- ✅ Tests run on bumped version +- ✅ Status reports correctly + +**Cons:** +- ⚠️ More complex workflow coordination +- ⚠️ Need to pass PR context manually + +**Implementation:** +```yaml +# version-bump.yml - after commit +- name: Trigger test workflow + env: + GH_TOKEN: ${{ github.token }} + run: | + gh workflow run test.yml \ + -f pr_number=${{ github.event.pull_request.number }} \ + -f sha=${{ github.event.pull_request.head.sha }} + +# test.yml - add workflow_dispatch trigger +on: + pull_request: + workflow_dispatch: + inputs: + pr_number: + required: true + sha: + required: true +``` + +## Recommendation + +**Option A (GitHub App/PAT)** is the industry standard and cleanest solution, BUT requires setup. + +**Option C (Remove Auto-Merge)** is simplest if you don't mind one manual step. + +**For this project, I recommend:** +1. **Short-term: Option C** - Remove auto-merge, keep workflows simple +2. **Long-term: Option A** - If you want full automation, set up GitHub App + +## Proposed Implementation (Option C - Simple) + +### Changes: +1. **Remove auto-merge step** from version-bump.yml +2. **Remove workflow_run trigger** from test.yml (back to pull_request only) +3. **Update documentation** - user merges after reviewing version bump + +### Updated Flow: +1. Create PR with changes +2. Add `bump:*` labels +3. Version bump workflow commits new version +4. **User reviews version bump** +5. **User merges PR** (via UI or gh CLI) +6. Release workflow publishes package + +### Pros: +- ✅ Simple, no complex workflow orchestration +- ✅ Human verification of version before release +- ✅ No token management +- ✅ No status check issues +- ✅ Tests run normally on PR + +### Cons: +- One manual step (clicking merge button) + +## Alternative Recommendation (Option A - Fully Automated) + +If you want full automation: + +### Setup GitHub App (one-time): +1. Create GitHub App with repo write permissions +2. Install on repository +3. Store private key or create installation token +4. Add as repository secret: `BOT_TOKEN` + +### Changes: +1. version-bump.yml: Use `BOT_TOKEN` instead of `GITHUB_TOKEN` +2. test.yml: Back to simple `pull_request` trigger +3. Keep auto-merge step + +### Flow: +1. Add labels → version bump commits → test runs → auto-merge → release + +Fully automated, no manual steps. + +## Questions + +1. Do you want full automation (Option A - requires GitHub App setup)? +2. Or accept one manual step for simplicity (Option C - remove auto-merge)? +3. Or try the workflow_dispatch approach (Option D - no tokens, moderately complex)? + + \ No newline at end of file diff --git a/_todo/completed/2025-10-21/new-release-workflow.md b/_todo/completed/2025-10-21/new-release-workflow.md new file mode 100644 index 0000000..74bdb7a --- /dev/null +++ b/_todo/completed/2025-10-21/new-release-workflow.md @@ -0,0 +1,717 @@ +# New Release Workflow + +**Status**: ✅ Completed +**Started**: 2025-10-21 +**Completed**: 2025-10-21 +**Final Branch**: `test/workflow-fixes` (PR #26) + +## Completion Summary + +Successfully implemented automated release workflow with label-driven publishing to PyPI/TestPyPI. + +**Final Approach**: Manual version bumping with automated publishing +- User manually runs `uv version --bump ` and commits +- Release workflow automatically publishes when PR with release label is merged +- Simple, reliable, no complex workflow orchestration + +**Key Achievement**: Working release workflow that publishes to PyPI/TestPyPI with improved GitHub Release notes + +**Lessons Learned**: +- GITHUB_TOKEN commits don't trigger other workflows (GitHub safety feature) +- workflow_run executes in wrong context for PR status checks +- Sometimes simple manual steps are better than fighting automation limitations +- Archived automated version-bump workflow for future reference if GitHub App approach is needed + +## Progress Log + +### 2025-10-21 + +#### Initial Setup +- Proposal approved and moved to pending phase +- Created feature branch `feature/automated-release-workflow` +- Decisions finalized: + - Manual PR approval (no auto-merge requirement) + - Multiple bump labels allowed (applied sequentially) + - Deprecate `python-publish.yml` entirely + - No additional environment approvals beyond PR + - Use PR descriptions for changelog (no automation) + - Continue with CalVer versioning + +#### Phase 1 & 2 Implementation (Completed) +- ✅ Verified all required labels exist in repository (11 bump labels + 2 release labels) +- ✅ Created `.github/workflows/version-bump.yml` + - Triggers on PR label events + - Applies version bumps sequentially + - Commits changes back to PR branch + - Posts version change comment +- ✅ Created `.github/workflows/release.yml` + - Checks for release labels on merged PRs + - Builds and tests package + - Publishes to PyPI or TestPyPI + - Creates git tag and GitHub Release +- ✅ Deleted deprecated `.github/workflows/python-publish.yml` +- ✅ Created `.github/RELEASE.md` documentation +- ✅ Updated `CLAUDE.md` with release workflow section + +#### Bug Fixes (Post-merge) +- 🐛 Fixed version bump workflow to apply multiple bumps in single command + - Issue: PR #11 had 3 workflow runs (one per label) all failing + - Root cause: Workflow triggered on `synchronize` + bumps applied sequentially + - Solution: Removed `synchronize` trigger, build single `uv version --bump x --bump y` command + - Commit: 025ada7 +- 🐛 Fixed release workflow missing checkout step + - Issue: `gh pr view` failed with "not a git repository" + - Solution: Added checkout step before checking labels + - Commit: 9fdef71 +- ✅ Both fixes merged to `dev` via PR #13 + +#### Next Steps +- Test version bump workflow on a test PR +- Configure GitHub Environments (pypi, testpypi) +- Configure PyPI Trusted Publishing +- Test release to TestPyPI +- Configure branch protection rules +- Test production release to PyPI + +--- + +## Original Objective +We should move toward branch protection and automate release. + +A PR should be able to start the release workflow after it's merged to main. + +The steps should be the following: + +1. PR gets created +2. CI test action starts running +3. If it's a release type, label gets applied, with 2 types: + 1. `release` -> publish to PyPI + 2. `test-release` -> publish to TestPyPI + 3. Also version `bump` label gets added (create a new label for each bump type `uv version --bump` supports; multiple bump labels can be added simultaneously): this needs to be its own action workflow, that will be a requirement before publishing the package +4. When CI and version bump workflow are completed, then PR can be merged +5. The release should also create a new tag with the same version number of the package just published + +## Analysis + +### Current State +- **Existing workflow**: `.github/workflows/python-publish.yml` publishes to PyPI on release/tag events +- **Test workflow**: `.github/workflows/test.yml` runs on PRs to main +- **No branch protection**: Main branch has no protection rules +- **No automated versioning**: Manual version updates required +- **No pre-merge version bumping**: Versions updated after merge + +### Requirements Analysis + +1. **Label-driven workflow**: Use labels to control release type and version bumping +2. **Version bump automation**: Automated workflow to update version based on labels +3. **Status checks**: Version bump must complete before merge is allowed +4. **Branch protection**: Enforce required checks before merging +5. **Post-merge publishing**: Automatic PyPI/TestPyPI publish after merge +6. **Tag creation**: Automatic git tag matching published version + +### `uv version --bump` Options +According to `uv version --help`, supported bump types: +- `major` - X.0.0 (breaking changes) +- `minor` - 0.X.0 (new features) +- `patch` - 0.0.X (bug fixes) +- `stable` - Remove pre-release suffix +- `alpha` - Add/increment alpha version +- `beta` - Add/increment beta version +- `rc` - Add/increment release candidate +- `post` - Add/increment post-release +- `dev` - Add/increment development version + +## Implementation Plan + +### Step 1: Create Version Bump Labels +All labels listed here should be created (see `add-label-for-pr` task): + +**Release Type Labels:** +- `release` - Publish to PyPI +- `test-release` - Publish to TestPyPI + +**Version Bump Labels** (multiple can be applied): +- `bump:major` +- `bump:minor` +- `bump:patch` +- `bump:stable` +- `bump:alpha` +- `bump:beta` +- `bump:rc` +- `bump:post` +- `bump:dev` + +### Step 2: Create Version Bump Workflow +**File**: `.github/workflows/version-bump.yml` + +This workflow: +- Triggers when bump labels are added to PR +- Runs `uv version --bump ` for each bump label +- Commits the updated `pyproject.toml` and `uv.lock` back to the PR branch +- Reports success/failure as a status check + +```yaml +name: Version Bump + +on: + pull_request: + types: [labeled, synchronize] + branches: [main] + +jobs: + bump-version: + # Only run if PR has at least one bump: label + if: | + contains(github.event.pull_request.labels.*.name, 'bump:major') || + contains(github.event.pull_request.labels.*.name, 'bump:minor') || + contains(github.event.pull_request.labels.*.name, 'bump:patch') || + contains(github.event.pull_request.labels.*.name, 'bump:stable') || + contains(github.event.pull_request.labels.*.name, 'bump:alpha') || + contains(github.event.pull_request.labels.*.name, 'bump:beta') || + contains(github.event.pull_request.labels.*.name, 'bump:rc') || + contains(github.event.pull_request.labels.*.name, 'bump:post') || + contains(github.event.pull_request.labels.*.name, 'bump:dev') + + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: ".python-version" + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + version: "latest" + + - name: Get current version + id: current-version + run: | + CURRENT_VERSION=$(uv version --short) + echo "version=${CURRENT_VERSION}" >> $GITHUB_OUTPUT + echo "Current version: ${CURRENT_VERSION}" + + - name: Apply version bumps + id: bump + env: + GH_TOKEN: ${{ github.token }} + run: | + # Get all labels on the PR + LABELS=$(gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name') + + # Extract bump labels in order of precedence + BUMP_TYPES=() + + for label in major minor patch stable alpha beta rc post dev; do + if echo "$LABELS" | grep -q "^bump:${label}$"; then + BUMP_TYPES+=("$label") + fi + done + + if [ ${#BUMP_TYPES[@]} -eq 0 ]; then + echo "No bump labels found, skipping version bump" + exit 0 + fi + + echo "Applying bumps: ${BUMP_TYPES[*]}" + + # Apply each bump sequentially + for bump_type in "${BUMP_TYPES[@]}"; do + echo "Bumping: $bump_type" + uv version --bump "$bump_type" + done + + NEW_VERSION=$(uv version --short) + echo "new_version=${NEW_VERSION}" >> $GITHUB_OUTPUT + echo "New version: ${NEW_VERSION}" + + - name: Commit version bump + if: steps.bump.outputs.new_version != '' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add pyproject.toml uv.lock + + if git diff --staged --quiet; then + echo "No changes to commit" + else + git commit -m "Bump version to ${{ steps.bump.outputs.new_version }}" + git push + fi + + - name: Comment on PR + if: steps.bump.outputs.new_version != '' + env: + GH_TOKEN: ${{ github.token }} + run: | + gh pr comment ${{ github.event.pull_request.number }} --body \ + "✅ Version bumped: \`${{ steps.current-version.outputs.version }}\` → \`${{ steps.bump.outputs.new_version }}\`" +``` + +**Key Features:** +- Applies bumps in order of labels +- Commits changes back to PR branch +- Posts comment with version change +- Acts as required status check + +### Step 3: Create Release Workflow +**File**: `.github/workflows/release.yml` + +This workflow: +- Triggers when PR with `release` or `test-release` label is merged to main +- Builds the package +- Publishes to PyPI or TestPyPI based on label +- Creates a git tag matching the published version +- Creates a GitHub Release + +```yaml +name: Release + +on: + pull_request: + types: [closed] + branches: [main] + +jobs: + check-release-label: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + outputs: + should-release: ${{ steps.check.outputs.release }} + is-test: ${{ steps.check.outputs.test }} + steps: + - name: Check for release labels + id: check + env: + GH_TOKEN: ${{ github.token }} + run: | + LABELS=$(gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name') + + if echo "$LABELS" | grep -q "^release$"; then + echo "release=true" >> $GITHUB_OUTPUT + echo "test=false" >> $GITHUB_OUTPUT + elif echo "$LABELS" | grep -q "^test-release$"; then + echo "release=true" >> $GITHUB_OUTPUT + echo "test=true" >> $GITHUB_OUTPUT + else + echo "release=false" >> $GITHUB_OUTPUT + echo "test=false" >> $GITHUB_OUTPUT + fi + + build: + needs: check-release-label + if: needs.check-release-label.outputs.should-release == 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout main branch + uses: actions/checkout@v4 + with: + ref: main + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: ".python-version" + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + version: "latest" + + - name: Install dependencies + run: uv sync --locked --all-extras --dev + + - name: Run tests + run: uv run pytest tests -v + + - name: Build package + run: uv build + + - name: Get version + id: version + run: | + VERSION=$(uv version --short) + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "Package version: ${VERSION}" + + - name: Upload distributions + uses: actions/upload-artifact@v4 + with: + name: release-dists + path: dist/ + + outputs: + version: ${{ steps.version.outputs.version }} + + publish-pypi: + needs: [check-release-label, build] + if: needs.check-release-label.outputs.is-test == 'false' + runs-on: ubuntu-latest + permissions: + id-token: write + environment: + name: pypi + url: https://pypi.org/p/quarto-batch-convert + + steps: + - name: Download distributions + uses: actions/download-artifact@v4 + with: + name: release-dists + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ + + publish-testpypi: + needs: [check-release-label, build] + if: needs.check-release-label.outputs.is-test == 'true' + runs-on: ubuntu-latest + permissions: + id-token: write + environment: + name: testpypi + url: https://test.pypi.org/p/quarto-batch-convert + + steps: + - name: Download distributions + uses: actions/download-artifact@v4 + with: + name: release-dists + path: dist/ + + - name: Publish to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + packages-dir: dist/ + + create-tag-and-release: + needs: [check-release-label, build, publish-pypi] + if: | + always() && + needs.check-release-label.outputs.should-release == 'true' && + (needs.publish-pypi.result == 'success' || needs.publish-testpypi.result == 'success') + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout main branch + uses: actions/checkout@v4 + with: + ref: main + + - name: Create and push tag + env: + VERSION: ${{ needs.build.outputs.version }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git tag -a "v${VERSION}" -m "Release v${VERSION}" + git push origin "v${VERSION}" + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ needs.build.outputs.version }} + run: | + RELEASE_TYPE="${{ needs.check-release-label.outputs.is-test == 'true' && 'test' || 'production' }}" + + gh release create "v${VERSION}" \ + --title "v${VERSION}" \ + --notes "Release v${VERSION} (${RELEASE_TYPE}) + + Published from PR #${{ github.event.pull_request.number }} + + See [CHANGELOG](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for details." \ + --draft=false \ + --prerelease=${{ needs.check-release-label.outputs.is-test == 'true' }} +``` + +**Key Features:** +- Only runs on merged PRs with release labels +- Separate publish jobs for PyPI vs TestPyPI +- Creates git tag after successful publish +- Creates GitHub Release with version tag +- Test releases marked as pre-release + +### Step 4: Configure Branch Protection +**Repository Settings** → **Branches** → **Branch protection rules** for `main`: + +Enable: +- ✅ Require a pull request before merging + - ✅ Require approvals: 1 (optional, adjust as needed) + - ✅ Dismiss stale pull request approvals when new commits are pushed +- ✅ Require status checks to pass before merging + - ✅ Require branches to be up to date before merging + - **Required checks:** + - `test` (from test.yml) + - `bump-version` (from version-bump.yml, if bump labels present) +- ✅ Do not allow bypassing the above settings + +**Notes:** +- The `bump-version` check should only be required if bump labels are present +- May need to use branch protection rules API or rulesets for conditional requirements + +### Step 5: Configure PyPI Environments +**GitHub Repository Settings** → **Environments** + +Create two environments: + +**Environment: `pypi`** +- Required reviewers: (optional, for production safety) +- Deployment branches: Only `main` + +**Environment: `testpypi`** +- Required reviewers: None +- Deployment branches: Only `main` + +**PyPI Trusted Publishing:** +Both PyPI and TestPyPI need to be configured for trusted publishing: + +1. Go to PyPI account settings → Publishing +2. Add publisher: + - Repository: `kompre/quarto_batch_convert` + - Workflow: `release.yml` + - Environment: `pypi` (or `testpypi`) + +### Step 6: Remove Existing python-publish.yml +**Decision**: Deprecate the existing `python-publish.yml` workflow entirely. + +**Action**: Delete `.github/workflows/python-publish.yml` to enforce PR-based release workflow only. + +**Rationale**: The new release workflow provides all necessary functionality. Keeping the old workflow could cause confusion or accidental releases. + +### Step 7: Documentation + +**Create `.github/RELEASE.md`:** +```markdown +# Release Process + +## Automated Release Workflow + +### 1. Create PR with Changes +Create a PR targeting the `main` branch with your changes. + +### 2. Add Version Bump Label +Add ONE OR MORE of these labels to indicate version change: +- `bump:major` - Breaking changes (2025.8.11 → 2026.0.0) +- `bump:minor` - New features (2025.8.11 → 2025.9.0) +- `bump:patch` - Bug fixes (2025.8.11 → 2025.8.12) +- `bump:stable` - Remove pre-release suffix +- `bump:alpha`, `bump:beta`, `bump:rc`, `bump:post`, `bump:dev` - Pre-release + +The version-bump workflow will automatically update `pyproject.toml` and commit to your PR. + +### 3. Add Release Type Label +Add ONE of these labels: +- `release` - Publish to PyPI (production) +- `test-release` - Publish to TestPyPI (testing) + +### 4. Wait for Checks +Required checks must pass: +- ✅ Tests (`test.yml`) +- ✅ Version bump (`version-bump.yml`, if bump labels present) + +### 5. Merge PR +Once approved and checks pass, merge the PR. + +### 6. Automatic Publishing +After merge: +- Package is built and tested +- Published to PyPI or TestPyPI +- Git tag created (e.g., `v2025.9.0`) +- GitHub Release created + +## Example: Patch Release to PyPI + +1. Create PR: "Fix bug in file collection" +2. Add labels: `bump:patch`, `release` +3. Version bump workflow: `2025.8.11` → `2025.8.12` +4. Merge PR +5. Release workflow publishes to PyPI and creates tag `v2025.8.12` + +## Example: Test Release + +1. Create PR: "Test new feature" +2. Add labels: `bump:minor`, `bump:beta`, `test-release` +3. Version bump workflow: `2025.8.11` → `2025.9.0b1` +4. Merge PR +5. Release workflow publishes to TestPyPI (not production PyPI) +``` + +**Update CLAUDE.md** with release workflow section. + +## Workflow Diagram + +``` +┌─────────────────┐ +│ Create PR │ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ Add Labels: │ +│ - bump:* │──┐ +│ - release OR │ │ +│ test-release │ │ +└────────┬────────┘ │ + │ │ + │ ▼ + │ ┌──────────────────┐ + │ │ version-bump │ + │ │ workflow runs │ + │ │ (required check)│ + │ └────────┬─────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────┐ +│ test workflow runs │ +│ (required check) │ +└────────┬────────────────────┘ + │ + ▼ +┌─────────────────┐ +│ PR Approved │ +│ All checks ✅ │ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ Merge to main │ +└────────┬────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ release workflow triggers │ +│ - Build package │ +│ - Run tests │ +│ - Publish to PyPI/TestPyPI │ +│ - Create git tag │ +│ - Create GitHub Release │ +└─────────────────────────────┘ +``` + +## Success Criteria + +- [ ] All version bump labels (`bump:*`) exist in repository +- [ ] Release type labels (`release`, `test-release`) exist in repository +- [ ] `version-bump.yml` workflow created and functional +- [ ] `release.yml` workflow created and functional +- [ ] Branch protection rules configured on `main` branch +- [ ] PyPI and TestPyPI environments configured +- [ ] PyPI trusted publishing configured for both environments +- [ ] Documentation created (`.github/RELEASE.md`) +- [ ] `CLAUDE.md` updated with release workflow guidance +- [ ] Successfully complete a test release to TestPyPI +- [ ] Successfully complete a production release to PyPI + +## Decisions + +1. **Branch Protection Strictness**: + - **Decision**: User will manually approve PRs or toggle auto-merge + - **Implementation**: Configure branch protection to require 1 approval (can be bypassed by repo admin) + +2. **Version Bump Conflict Resolution**: + - **Decision**: Allow multiple bump labels; they will be applied in sequence + - **Behavior**: Workflow triggers on label events, applies all present bump labels in order (major, minor, patch, stable, alpha, beta, rc, post, dev) + - **Note**: Order matters only if labels are added separately (triggering workflow multiple times). If added together, all bumps apply in single workflow run. + +3. **Existing python-publish.yml**: + - **Decision**: Deprecate the existing `python-publish.yml` workflow + - **Implementation**: Remove the file entirely to enforce PR-based release workflow only + +4. **TestPyPI vs PyPI Environment Protection**: + - **Decision**: No additional environment approvals beyond PR approval + - **Clarification**: GitHub Environments can require separate approval for deployment (independent of PR approval). We will NOT use this feature - PR approval is sufficient. + - **Implementation**: Create environments without required reviewers + +5. **Automated Changelog**: + - **Decision**: Use PR descriptions for release notes (no automated changelog generation) + - **Implementation**: Manual changelog updates in PR body, which can be referenced in GitHub Release notes + +6. **Version Scheme**: + - **Decision**: Continue using current CalVer (YYYY.M.D) scheme + - **Note**: All bump types still work with CalVer (patch increments day, minor increments month, etc.) + +## Estimated Complexity +**High** - Complex multi-workflow system with dependencies, branch protection, and external integrations (PyPI). Requires careful testing and likely multiple iterations. + +## Implementation Phases + +**Phase 1: Labels and Version Bump** (Can be done first) +- Create all labels (via `add-label-for-pr` task) +- Implement `version-bump.yml` +- Test on draft PR + +**Phase 2: Release Workflow** (Depends on Phase 1) +- Implement `release.yml` +- Configure TestPyPI environment +- Test release to TestPyPI + +**Phase 3: Branch Protection** (After Phase 1 & 2 working) +- Enable branch protection on main +- Add required status checks +- Test enforcement + +**Phase 4: Production Release** (Final validation) +- Configure PyPI environment +- Perform production release to PyPI +- Validate tag and GitHub Release creation + +## Related Tasks +- **Dependency**: `add-label-for-pr` - Must create labels before workflows can use them +- **Related**: `fix-test-workflow` - Test workflow must be working for branch protection + +## Risks and Mitigations + +| Risk | Mitigation | +|------|------------| +| Workflow publishes wrong version | Version bump must be required check; publish job reads version from built package | +| Multiple bump labels cause confusion | Apply in precedence order; document expected behavior; consider enforcing single label | +| PyPI publish fails but tag created | Tag creation happens AFTER successful publish | +| Branch protection blocks emergency fixes | Keep manual-release.yml as fallback; admin bypass available | +| Trusted publishing not configured | Detailed documentation; test with TestPyPI first | + +## Testing Plan + +1. **Test Version Bump Workflow**: + - Create test PR with `bump:patch` label + - Verify version updated in PR + - Verify commit pushed to PR branch + +2. **Test TestPyPI Release**: + - Create test PR with `bump:patch` and `test-release` labels + - Merge PR + - Verify package published to TestPyPI + - Verify tag created + - Verify GitHub Release created (marked as pre-release) + +3. **Test Production Release**: + - Create PR with `bump:minor` and `release` labels + - Merge PR + - Verify package published to PyPI + - Verify tag created + - Verify GitHub Release created + +4. **Test Branch Protection**: + - Create PR without labels + - Verify cannot merge without required checks + - Add bump label + - Verify version bump check required and passes + - Verify can now merge