diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ace9ba1..c3c7136 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,6 @@ name: CI on: - push: pull_request: jobs: @@ -31,23 +30,3 @@ jobs: - name: Test run: uv run pytest tests/ -v --cov=ddogctl --cov-report=xml - - publish: - runs-on: ubuntu-latest - needs: test - if: startsWith(github.ref, 'refs/tags/v') - permissions: - id-token: write - steps: - - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v7 - with: - python-version: "3.12" - - - name: Build package - run: uv build - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index dab976d..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Publish to PyPI - -on: - push: - tags: - - 'v*' - -jobs: - publish: - runs-on: ubuntu-latest - permissions: - id-token: write # Required for PyPI trusted publishing - contents: read - - steps: - - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - enable-cache: true - - - name: Set up Python - run: uv python install 3.12 - - - name: Build package - run: uv build - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0a9ccb0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,73 @@ +name: Release + +on: + push: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + python-version: ${{ matrix.python-version }} + enable-cache: true + + - name: Install dependencies + run: uv sync --all-extras + + - name: Check formatting + run: uv run black --check ddogctl/ tests/ + + - name: Lint + run: uv run ruff check ddogctl/ tests/ + + - name: Test + run: uv run pytest tests/ -v --cov=ddogctl --cov-report=xml + + release: + runs-on: ubuntu-latest + needs: test + concurrency: release + permissions: + id-token: write + contents: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.RELEASE_TOKEN }} + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + + - name: Set up Python + run: uv python install 3.12 + + - name: Python Semantic Release + id: release + uses: python-semantic-release/python-semantic-release@v9.21.1 + with: + github_token: ${{ secrets.RELEASE_TOKEN }} + + - name: Publish to PyPI + if: steps.release.outputs.released == 'true' + uses: pypa/gh-action-pypi-publish@release/v1 + + - name: Publish to GitHub Release + if: steps.release.outputs.released == 'true' + uses: python-semantic-release/publish-action@v9.21.1 + with: + github_token: ${{ secrets.RELEASE_TOKEN }} + tag: ${{ steps.release.outputs.tag }} diff --git a/CLAUDE.md b/CLAUDE.md index 8ca5447..6dba78d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -119,7 +119,14 @@ Strict TDD (RED-GREEN-REFACTOR). Coverage target >90%. Reference implementation: ## Releasing -1. Bump version in `pyproject.toml` and `ddogctl/cli.py` (`@click.version_option`) -2. PR, merge, then tag: `git tag -a vX.Y.Z -m "vX.Y.Z"` and `git push origin vX.Y.Z` -3. CI publish job auto-triggers on `refs/tags/v*` via PyPI trusted publishing - - **First-time setup**: Configure PyPI Trusted Publishing at https://pypi.org/manage/project/ddogctl/settings/publishing/ (owner: `srgfrancisco`, repo: `ddogctl`, workflow: `publish.yml`) +Fully automated via `python-semantic-release`. On every merge to `main`: +1. CI tests run (Python 3.10-3.13) +2. Commits since last tag are analyzed for conventional commit prefixes +3. If `feat`/`fix`/`perf`/breaking changes found: version is bumped in `pyproject.toml`, tagged, published to PyPI, and a GitHub Release is created +4. If only `chore`/`docs`/`ci`/`refactor` commits: no release + +Version lives in `pyproject.toml` only. `cli.py` reads it at runtime via `importlib.metadata`. + +To force a major bump: use `feat!:` prefix or include `BREAKING CHANGE:` in commit body. + +- **Setup requirements**: `RELEASE_TOKEN` (fine-grained PAT with Contents: Read & Write) in repo secrets, PyPI trusted publisher configured for workflow `release.yml` diff --git a/ddogctl/__init__.py b/ddogctl/__init__.py index 5becc17..8b13789 100644 --- a/ddogctl/__init__.py +++ b/ddogctl/__init__.py @@ -1 +1 @@ -__version__ = "1.0.0" + diff --git a/ddogctl/cli.py b/ddogctl/cli.py index 3747493..3aca8b8 100644 --- a/ddogctl/cli.py +++ b/ddogctl/cli.py @@ -1,5 +1,7 @@ """Main CLI entry point for Datadog CLI.""" +from importlib.metadata import version as _get_version + import click from rich.console import Console @@ -36,7 +38,7 @@ def resolve_command(self, ctx, args): @click.group(cls=AliasGroup) -@click.version_option(version="2.0.3") +@click.version_option(version=_get_version("ddogctl")) @click.option( "--profile", default=None, diff --git a/pyproject.toml b/pyproject.toml index 4f264b6..944b833 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,3 +61,26 @@ line-length = 100 [tool.ruff] line-length = 100 + +[tool.semantic_release] +version_toml = ["pyproject.toml:project.version"] +commit_parser = "conventional" +tag_format = "v{version}" +commit_message = "chore(release): v{version} [skip ci]" +build_command = "uv build" + +[tool.semantic_release.branches.main] +match = "main" +prerelease = false + +[tool.semantic_release.commit_parser_options] +minor_tags = ["feat"] +patch_tags = ["fix", "perf"] + +[tool.semantic_release.changelog] +changelog_file = "CHANGELOG.md" +exclude_commit_patterns = ["chore\\(release\\):"] + +[tool.semantic_release.remote] +type = "github" +token = { env = "GH_TOKEN" } diff --git a/uv.lock b/uv.lock index 0147974..9329e70 100644 --- a/uv.lock +++ b/uv.lock @@ -309,7 +309,7 @@ wheels = [ [[package]] name = "ddogctl" -version = "2.0.0" +version = "2.0.3" source = { editable = "." } dependencies = [ { name = "click" },