From af2f63f4a54d4117095d370e76ca5f8f176a87e9 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Fri, 7 Nov 2025 15:53:14 +0100 Subject: [PATCH] Migrate to GitHub Actions automated release process This commit modernizes the CI/CD pipeline to match the mxrepo workflow and implements automated releases via GitHub Actions. ## New Workflow Files - Add lint.yml: Automated code quality checks (ruff, isort) - Add typecheck.yml: Type checking with mypy - Add release.yml: Automated PyPI publishing workflow - Publishes to Test PyPI on every master push - Publishes to production PyPI on GitHub releases - Add dependabot.yml: Weekly GitHub Actions dependency updates ## Updated Workflow Files - Update test.yml: - Switch from actions/setup-python to astral-sh/setup-uv@v7 - Update Python versions from 3.7-3.12 to 3.10-3.14 - Add workflow_call and workflow_dispatch triggers - Implement coverage artifact collection and reporting - Add dedicated coverage job with HTML report generation - Update to latest action versions (checkout@v5) - Update docs.yml: - Switch to astral-sh/setup-uv@v7 for consistency - Update action versions (checkout@v5, setup-node@v4) - Update Node.js version from 16 to 20 - Add workflow_call trigger for reusability ## Build System Migration - Add pyproject.toml: - Migrate from setup.cfg to modern pyproject.toml format - Use hatch-vcs for automatic versioning from git tags - Use hatch-fancy-pypi-readme for combined readme - Configure ruff, isort, mypy, pytest, and coverage - Maintain backward compatibility with zest.releaser config ## Documentation - Add RELEASE.md: Comprehensive release process documentation - Automated GitHub release workflow - PyPI Trusted Publishing setup instructions - Version numbering guidelines - Troubleshooting guide - Migration notes from zest.releaser ## Related - Addresses #3: GitHub Actions for testing with coverage artifacts - Migrates release process from manual zest.releaser to automated GitHub releases - Follows same pattern as https://github.com/mxstack/mxrepo --- .github/dependabot.yml | 14 +++ .github/workflows/docs.yml | 22 +++- .github/workflows/lint.yml | 28 +++++ .github/workflows/release.yml | 85 +++++++++++++ .github/workflows/test.yml | 103 +++++++++++---- .github/workflows/typecheck.yml | 28 +++++ RELEASE.md | 213 ++++++++++++++++++++++++++++++++ pyproject.toml | 136 ++++++++++++++++++++ 8 files changed, 600 insertions(+), 29 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/typecheck.yml create mode 100644 RELEASE.md create mode 100644 pyproject.toml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..659de43 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + # Enable version updates for GitHub Actions + - package-ecosystem: "github-actions" + # Workflow files stored in the default location of `.github/workflows` + # You don't need to specify `/.github/workflows` for `directory`. You can use `directory: "/"`. + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d0e3c6f..981524f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,17 +1,29 @@ name: Docs -on: [push, workflow_dispatch] +on: + push: + workflow_call: + workflow_dispatch: jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v5 + + - name: Set up uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + cache-dependency-glob: "pyproject.toml" + + - name: Set up Python 3.13 + run: uv python install 3.13 + + - uses: actions/setup-node@v4 with: - node-version: 16 + node-version: 20 - run: npm install -g @mermaid-js/mermaid-cli diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..31bfc6e --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,28 @@ +name: Lint + +on: + push: + workflow_call: + workflow_dispatch: + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + + - name: Set up uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + cache-dependency-glob: "pyproject.toml" + + - name: Set up Python 3.10 + run: uv python install 3.10 + + - name: Install Project + run: make install + + - name: Run checks + run: make check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b3cc228 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,85 @@ +--- +name: Build & upload PyPI package + +on: + push: + branches: [master] + tags: ["*"] + release: + types: + - published + workflow_dispatch: + +jobs: + tests: + uses: "./.github/workflows/test.yml" + lint: + uses: "./.github/workflows/lint.yml" + typecheck: + uses: "./.github/workflows/typecheck.yml" + + # Always build & lint package. + build-package: + name: Build & verify package + needs: + - lint + - tests + - typecheck + runs-on: ubuntu-latest + permissions: + attestations: write + id-token: write + + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + persist-credentials: false + + - uses: hynek/build-and-inspect-python-package@v2 + with: + attest-build-provenance-github: 'true' + + # Upload to Test PyPI on every commit on master. + release-test-pypi: + name: Publish in-dev package to test.pypi.org + environment: release-test-pypi + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + needs: + - build-package + permissions: + id-token: write + + steps: + - name: Download packages built by build-and-inspect-python-package + uses: actions/download-artifact@v6 + with: + name: Packages + path: dist + + - name: Upload package to Test PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + + # Upload to real PyPI on GitHub Releases. + release-pypi: + name: Publish released package to pypi.org + environment: release-pypi + if: github.event.action == 'published' + runs-on: ubuntu-latest + needs: + - build-package + permissions: + id-token: write + + steps: + - name: Download packages built by build-and-inspect-python-package + uses: actions/download-artifact@v6 + with: + name: Packages + path: dist + + - name: Upload package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2ee1cf9..bd79b2f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,45 +1,100 @@ name: Tests -on: [push] +on: + push: + workflow_call: + workflow_dispatch: jobs: - test: - name: Test ${{ matrix.python }} - ${{ matrix.os }} + tests: + name: Test ${{ matrix.python-version }} - ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: + python-version: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" os: - ubuntu-latest - - windows-latest - macos-latest + - windows-latest - python: - - "3.7" - - "3.8" - - "3.9" - - "3.10" - - "3.11" - - "3.12" + steps: + - uses: actions/checkout@v5 + + - name: Set up uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + cache-dependency-glob: "pyproject.toml" + + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: Install Project (Windows) + if: runner.os == 'Windows' + run: make MAKESHELL='C:/Program Files/Git/usr/bin/bash' install + + - name: Install Project (Unix) + if: runner.os != 'Windows' + run: make install + + - name: Run Coverage (Windows) + if: runner.os == 'Windows' + run: make MAKESHELL='C:/Program Files/Git/usr/bin/bash' coverage + + - name: Run Coverage (Unix) + if: runner.os != 'Windows' + run: make coverage + + - name: Upload coverage data + uses: actions/upload-artifact@v4 + with: + name: coverage-data-${{ matrix.os }}-${{ matrix.python-version }} + path: .coverage.* + if-no-files-found: ignore + include-hidden-files: true + + coverage: + name: Combine & check coverage + needs: tests + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - name: Set up Python - uses: actions/setup-python@v5 + - name: Set up uv + uses: astral-sh/setup-uv@v7 with: - python-version: ${{ matrix.python }} + enable-cache: true + cache-dependency-glob: "pyproject.toml" - - name: Install - run: pip install -e .[test] + - name: Set up Python 3.13 + run: uv python install 3.13 - - name: Run tests - run: | - python --version - python -m webresource.tests + - name: Install Project + run: make install + + - name: Download coverage data + uses: actions/download-artifact@v4 + with: + pattern: coverage-data-* + merge-multiple: true - - name: Run coverage + - name: Combine coverage & fail if it's <100% run: | - coverage run --source webresource -m webresource.tests - coverage report --fail-under=100 + uv run coverage combine + uv run coverage html --skip-covered --skip-empty + uv run coverage report --fail-under=100 + + - name: Upload HTML report if check failed + uses: actions/upload-artifact@v4 + with: + name: html-report + path: htmlcov + if: ${{ failure() }} diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml new file mode 100644 index 0000000..3f924cb --- /dev/null +++ b/.github/workflows/typecheck.yml @@ -0,0 +1,28 @@ +name: Type checks + +on: + push: + workflow_call: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + + - name: Set up uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + cache-dependency-glob: "pyproject.toml" + + - name: Set up Python 3.13 + run: uv python install 3.13 + + - name: Install Project + run: make install + + - name: Run Typechecks + run: make typecheck diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..78c8801 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,213 @@ +# Release Process + +This document describes the automated release process for webresource. + +## Overview + +The project uses an automated GitHub Actions workflow for building, testing, and publishing packages to PyPI. Version numbers are automatically determined from git tags using `hatch-vcs`. + +## Prerequisites + +### GitHub Environment Configuration + +Before your first release, ensure the following GitHub environments are configured in the repository settings: + +1. **release-test-pypi** - For publishing to Test PyPI +2. **release-pypi** - For publishing to production PyPI + +### PyPI Trusted Publishing (Recommended) + +Configure Trusted Publishing on both PyPI and Test PyPI: + +1. Go to https://test.pypi.org/manage/account/publishing/ (for Test PyPI) +2. Go to https://pypi.org/manage/account/publishing/ (for production PyPI) +3. Add a new trusted publisher with: + - **PyPI Project Name**: `webresource` + - **Owner**: `conestack` + - **Repository name**: `webresource` + - **Workflow name**: `release.yml` + - **Environment name**: `release-test-pypi` (or `release-pypi` for production) + +This eliminates the need for API tokens and is more secure. + +## Release Types + +### Development Releases (Automatic) + +**Trigger**: Every push to the `master` branch + +**What happens**: +1. All tests, linting, and type checks run +2. Package is built and verified with attestation +3. Package is automatically published to **Test PyPI** + +**Version format**: `X.Y.devN+gCOMMITHASH` (e.g., `1.3.dev42+g1a2b3c4`) + +**Purpose**: Allows testing of the package installation process before making a production release + +**Test installation**: +```bash +pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ webresource +``` + +### Production Releases (Manual) + +**Trigger**: Creating a GitHub Release + +**What happens**: +1. All tests, linting, and type checks run +2. Package is built and verified with attestation +3. Package is automatically published to **production PyPI** + +**Version format**: `X.Y.Z` (e.g., `1.3.0`) + +## How to Create a Production Release + +### Step 1: Update CHANGES.rst + +Before creating a release, ensure `CHANGES.rst` has a section for the new version: + +```rst +1.3.0 (2025-01-15) +------------------ + +- Feature: Added support for new feature X +- Fix: Fixed bug Y +- Update: Improved documentation +``` + +Commit and push this change to master: + +```bash +git add CHANGES.rst +git commit -m "Prepare release 1.3.0" +git push origin master +``` + +Wait for the CI to pass and verify the dev package on Test PyPI if needed. + +### Step 2: Create a GitHub Release + +1. Go to https://github.com/conestack/webresource/releases/new +2. Click "Choose a tag" +3. Type the new version number with a `v` prefix (e.g., `v1.3.0`) +4. Click "Create new tag: v1.3.0 on publish" +5. Set the release title to the same version (e.g., `v1.3.0`) +6. In the description, add release notes (can copy from CHANGES.rst) +7. Click "Publish release" + +### Step 3: Automated Process + +Once you publish the release, GitHub Actions will automatically: + +1. Run all quality checks (tests, lint, typecheck) on Python 3.10-3.14 and all OS platforms +2. Build the package with build provenance attestation +3. Publish to production PyPI + +**Monitor the workflow**: https://github.com/conestack/webresource/actions + +### Step 4: Verify the Release + +After the workflow completes successfully: + +1. Check PyPI: https://pypi.org/project/webresource/ +2. Test installation: +```bash +pip install --upgrade webresource +python -c "import webresource; print(webresource.__version__)" +``` + +### Step 5: Update to Next Dev Version (Optional) + +If you want to explicitly mark the start of new development: + +```bash +git pull origin master # Get the tag +# Edit CHANGES.rst to add a new section like "1.4.0 (unreleased)" +git add CHANGES.rst +git commit -m "Back to development: 1.4.0" +git push origin master +``` + +Note: This step is optional since hatch-vcs automatically handles dev versions. + +## Version Numbering + +This project follows [Semantic Versioning](https://semver.org/): + +- **MAJOR version** (X.0.0): Incompatible API changes +- **MINOR version** (0.X.0): New functionality in a backwards compatible manner +- **PATCH version** (0.0.X): Backwards compatible bug fixes + +### Version Examples + +- `1.3.0` - Production release +- `1.3.dev42+g1a2b3c4` - Development version (42 commits after v1.2.0) +- `2.0.0b1` - Beta release (create tag like `v2.0.0b1`) +- `2.0.0rc1` - Release candidate (create tag like `v2.0.0rc1`) + +## Troubleshooting + +### Release Workflow Failed + +1. Check the GitHub Actions log: https://github.com/conestack/webresource/actions +2. Common issues: + - **Tests failed**: Fix the failing tests and push to master, then recreate the release + - **PyPI publish failed**: Check if the version already exists on PyPI (versions are immutable) + - **Permission denied**: Ensure Trusted Publishing is configured correctly + +### Version Not Updating + +If you see the old version after creating a release: + +1. Ensure the tag was created (check: https://github.com/conestack/webresource/tags) +2. Verify `hatch-vcs` is installed: `pip install hatch-vcs` +3. Check that `.git` directory exists (hatch-vcs reads from git) +4. For development installs, use: `pip install -e .` (not `pip install -e .[test]` from old setup.py) + +### Rollback a Release + +You **cannot** delete or modify a release on PyPI once published. If you need to fix a broken release: + +1. Fix the issue in master +2. Create a new patch release (e.g., if v1.3.0 is broken, release v1.3.1) + +## Migration from zest.releaser + +This project previously used `zest.releaser` for manual releases. Key differences: + +| Aspect | Old (zest.releaser) | New (GitHub Releases) | +|--------|---------------------|----------------------| +| Process | Manual command: `fullrelease` | Create GitHub Release | +| Version management | Hardcoded in setup.cfg | Automatic from git tags | +| PyPI upload | Manual or via zest.releaser | Automatic via GitHub Actions | +| Testing | Local only | Full CI/CD matrix (all Python versions & OS) | +| Verification | Manual | Automated with attestation | + +The `tool.zest-releaser` section in `pyproject.toml` is kept for backward compatibility but is no longer used. + +## Continuous Integration + +### On Every Push + +All pushes trigger: +- **Tests**: Python 3.10-3.14 on Ubuntu, macOS, and Windows +- **Lint**: Code quality checks with ruff and isort +- **Typecheck**: Static type checking with mypy +- **Docs**: Documentation build (deployed on manual trigger) +- **Coverage**: 100% coverage requirement with HTML report artifacts + +### On Master Branch Push + +Additionally publishes development version to Test PyPI. + +### On GitHub Release + +Additionally publishes production version to PyPI. + +## Additional Resources + +- [GitHub Actions Workflows](.github/workflows/) +- [hatch-vcs Documentation](https://github.com/ofek/hatch-vcs) +- [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/) +- [Semantic Versioning](https://semver.org/) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..69752ee --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,136 @@ +[project] +name = "webresource" +description = "A resource registry for web applications." +keywords = ["web", "resources", "dependencies", "javascript", "CSS"] +authors = [ + {name = "Conestack Contributors", email = "dev@conestack.org" } +] +requires-python = ">=3.10" +license = { text = "Simplified BSD" } +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", +] +dynamic = ["version", "readme"] + +[project.optional-dependencies] +docs = [ + "Sphinx", + "sphinx-conestack-theme", +] +test = [ + "coverage", + "pytest", + "pytest-cov", +] + +[project.urls] +Documentation = "https://webresource.readthedocs.io/" +ChangeLog = "https://github.com/conestack/webresource/blob/master/CHANGES.rst" +"Issue Tracker" = "https://github.com/conestack/webresource/issues" +"Source Code" = "https://github.com/conestack/webresource" + +[build-system] +requires = ["hatchling", "hatch-vcs", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[tool.hatch.version] +source = "vcs" + +[tool.hatch.build.hooks.vcs] +version-file = "webresource/_version.py" + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/x-rst" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "README.rst" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "CHANGES.rst" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "LICENSE.rst" + +[tool.hatch.build] +include = [ + "webresource/**", +] +exclude = [ + "webresource/_version.py", +] + +[tool.isort] +profile = "black" +force_alphabetical_sort = true +force_single_line = true +lines_after_imports = 2 + +[tool.mypy] +ignore_missing_imports = true +python_version = "3.10" + +[tool.ruff] +line-length = 88 +exclude = [ + ".git", + ".venv", + "venv", + "__pycache__", + "build", + "dist", + "webresource/_version.py", +] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "F", # pyflakes + "B", # flake8-bugbear + "UP", # pyupgrade + "S", # bandit (security) +] +ignore = [] + +[tool.ruff.lint.per-file-ignores] +"*/tests.py" = ["S101"] # Allow assert in tests + +[tool.pytest.ini_options] +testpaths = ["webresource"] +python_files = ["tests.py"] +addopts = [ + "--strict-markers", + "--strict-config", + "-ra", +] + +[tool.coverage.run] +source = ["webresource"] +omit = ["webresource/_version.py", "webresource/tests.py"] + +[tool.coverage.report] +fail_under = 100 +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", +] + +# Keep for backward compatibility, but can be removed later +[tool.zest-releaser] +create-wheel = true