From aef0df34b638fae00597249606ec0e438da7e10a Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Thu, 9 Apr 2026 16:00:11 +0200 Subject: [PATCH 1/9] Add immutable release enforcement and security docs Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/release.yml | 20 ++++++++++ README.md | 20 ++++++---- RELEASING.md | 71 +++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 RELEASING.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6912cc3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,20 @@ +name: Release + +on: + push: + tags: ['v*'] + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + with: + generate_release_notes: true diff --git a/README.md b/README.md index 8c8b571..09fa484 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ | Contributing | +Security +| License


@@ -470,15 +472,17 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/chartm ## Releasing -Make sure that: -1. you have prepared `~/.pypirc` with credentials, -2. a higher version has been set in `chartmogul/__init__.py`, -3. Run tests `python3 -m unittest` -4. Build package `python3 setup.py sdist` -4. release works `twine upload --repository-url https://test.pypi.org/legacy/ dist/*`, -5. release to production `twine upload dist/*`, +See [RELEASING.md](RELEASING.md) for the release process and security details. + +## Security + +### Verifying Releases + +All releases of this library are published as [immutable GitHub Releases](https://github.com/chartmogul/chartmogul-python/releases) with protected tags and as a package on [PyPI](https://pypi.org/project/chartmogul/). -[Read full HOWTO](http://peterdowns.com/posts/first-time-with-pypi.html) +To maximize supply chain security: +- **Commit your `requirements.txt`** (or lock file) to version control +- **Verify package checksums** with `pip install --require-hashes` for reproducible installs ## License diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..df7057e --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,71 @@ +# Releasing chartmogul-python + +## Prerequisites + +- You must have push access to the repository +- Tags matching `v*` are protected by GitHub tag protection rulesets +- Releases are immutable once published (GitHub repository setting) + +## Release Process + +1. Ensure all changes are merged to the `main` branch +2. Ensure CI is green on the `main` branch +3. Update the version in `chartmogul/version.py` +4. Commit the version bump +5. Create and push a version tag: + ```sh + git tag v4.X.Y + git push origin v4.X.Y + ``` +6. The [release workflow](.github/workflows/release.yml) will automatically create a GitHub Release with auto-generated release notes +7. Verify the release appears at https://github.com/chartmogul/chartmogul-python/releases +8. Build and publish to PyPI: + ```sh + python3 setup.py sdist + twine upload dist/* + ``` + +## Changelog + +Release notes are auto-generated from merged PR titles by the [release workflow](.github/workflows/release.yml). To ensure useful changelogs: + +- Use clear, descriptive PR titles (e.g., "Add External ID field to Contact model") +- Prefix breaking changes with `BREAKING:` so they stand out in release notes +- After the release is created, review and edit the notes on the [Releases page](https://github.com/chartmogul/chartmogul-python/releases) if needed + +## Pre-release Versions + +For pre-release versions, use a semver pre-release suffix: + +```sh +git tag v4.X.Y-rc1 +git push origin v4.X.Y-rc1 +``` + +These will be automatically marked as pre-releases on GitHub. + +## Security + +### Repository Protections + +- **Immutable releases**: Once a GitHub Release is published, its tag cannot be moved or deleted, and release assets cannot be modified +- **Tag protection rulesets**: `v*` tags cannot be deleted or force-pushed + +### PyPI + +- [PyPI](https://pypi.org) enforces version immutability: once a package version is published, it cannot be overwritten +- Users can verify package integrity using pip's hash-checking mode (`--require-hashes`) +- Pinning versions in `requirements.txt` with hashes ensures reproducible installs + +### What This Protects Against + +- A compromised maintainer account cannot modify or delete existing releases +- Tags cannot be moved to point to different commits after publication +- PyPI version immutability provides an independent verification layer beyond GitHub + +### Repository Settings (Admin) + +These settings must be configured by a repository admin: + +1. **Immutable Releases**: Settings > General > Releases > Enable "Immutable releases" +2. **Tag Protection Ruleset**: Settings > Rules > Rulesets > New ruleset targeting tags matching `v*` with deletion and force-push prevention From c2b7e4c3a8dc132bcf25e892b4399d8138652537 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Thu, 9 Apr 2026 16:05:28 +0200 Subject: [PATCH 2/9] Restore original releasing instructions in README Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 09fa484..45584a2 100644 --- a/README.md +++ b/README.md @@ -472,7 +472,17 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/chartm ## Releasing -See [RELEASING.md](RELEASING.md) for the release process and security details. +Make sure that: +1. you have prepared `~/.pypirc` with credentials, +2. a higher version has been set in `chartmogul/__init__.py`, +3. Run tests `python3 -m unittest` +4. Build package `python3 setup.py sdist` +4. release works `twine upload --repository-url https://test.pypi.org/legacy/ dist/*`, +5. release to production `twine upload dist/*`, + +[Read full HOWTO](http://peterdowns.com/posts/first-time-with-pypi.html) + +See also [RELEASING.md](RELEASING.md) for the full release process and security details. ## Security From 455d2f42f608aa5806acfc7cd595c6e61a43a399 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Fri, 10 Apr 2026 06:06:13 +0200 Subject: [PATCH 3/9] Replace softprops/action-gh-release with gh release Use the gh CLI already available on the runner, fixing the zizmor superfluous-actions warning. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/release.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6912cc3..ef9dbb7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,7 @@ jobs: with: persist-credentials: false - - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 - with: - generate_release_notes: true + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: gh release create "${{ github.ref_name }}" --generate-notes From a6a72f5a09d82e306ac02de5f7c54d3088576ef2 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Fri, 10 Apr 2026 10:25:38 +0200 Subject: [PATCH 4/9] Fix template injection warning and update security guidance Use GITHUB_REF_NAME env var instead of github.ref_name template expansion to fix zizmor code-injection warning. Replace --require-hashes recommendation with uv per review feedback. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/release.yml | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef9dbb7..6b9d2ec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,4 +18,4 @@ jobs: - name: Create GitHub Release env: GH_TOKEN: ${{ github.token }} - run: gh release create "${{ github.ref_name }}" --generate-notes + run: gh release create "$GITHUB_REF_NAME" --generate-notes diff --git a/README.md b/README.md index 45584a2..0ef90fd 100644 --- a/README.md +++ b/README.md @@ -491,8 +491,8 @@ See also [RELEASING.md](RELEASING.md) for the full release process and security All releases of this library are published as [immutable GitHub Releases](https://github.com/chartmogul/chartmogul-python/releases) with protected tags and as a package on [PyPI](https://pypi.org/project/chartmogul/). To maximize supply chain security: -- **Commit your `requirements.txt`** (or lock file) to version control -- **Verify package checksums** with `pip install --require-hashes` for reproducible installs +- **Commit your lock file** to version control — both `uv.lock` and `requirements.txt` record package hashes automatically +- **Use [uv](https://docs.astral.sh/uv/)** for hash-verified, reproducible installs ## License From e48d52ee4121ed04f71b4e66d22df51ebedbc285 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Tue, 14 Apr 2026 15:50:28 +0200 Subject: [PATCH 5/9] Add automated PyPI publishing via Trusted Publishers Replace manual twine upload with CI-based publishing triggered by tag push. The release workflow now runs lint, tests, version verification, builds sdist+wheel, publishes to PyPI via OIDC, and creates a GitHub Release with attached distribution files. - Add pyproject.toml with PEP 517/518 build-system declaration - Remove legacy setup.py publish command - Rewrite release.yml with lint/test/build/publish/release jobs - Update RELEASING.md with automated pipeline and setup docs --- .github/workflows/release.yml | 118 +++++++++++++++++++++++++++++++++- RELEASING.md | 33 ++++++++-- pyproject.toml | 3 + setup.py | 6 -- 4 files changed, 144 insertions(+), 16 deletions(-) create mode 100644 pyproject.toml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b9d2ec..53004f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,18 +4,130 @@ on: push: tags: ['v*'] -permissions: - contents: write +permissions: {} jobs: + lint: + name: Lint + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.14" + - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + - run: | + pip install flake8 + flake8 ./chartmogul + + test: + name: Test (Python ${{ matrix.python-version }}) + runs-on: ubuntu-24.04 + permissions: + contents: read + strategy: + fail-fast: true + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: ${{ matrix.python-version }} + - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + - run: | + python -m pip install --upgrade pip setuptools wheel coverage + pip install -e .[testing] + - run: coverage run -m unittest && coverage xml -i --include='chartmogul/*' + + build: + name: Build + needs: [lint, test] + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.14" + + - name: Verify version matches tag + run: | + TAG_VERSION="${GITHUB_REF_NAME#v}" + PKG_VERSION=$(python -c "import re; print(re.search(r\"^__version__\s*=\s*['\\\"]([^'\\\"]*)['\\\"]\" , open('chartmogul/version.py').read(), re.MULTILINE).group(1))") + if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then + echo "::error::Tag version ($TAG_VERSION) does not match package version ($PKG_VERSION)" + exit 1 + fi + + - name: Build sdist and wheel + run: | + python -m pip install --upgrade build + python -m build + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: dist + path: dist/ + if-no-files-found: error + retention-days: 5 + + publish: + name: Publish to PyPI + needs: [build] + runs-on: ubuntu-24.04 + permissions: + id-token: write + environment: + name: pypi + url: https://pypi.org/project/chartmogul/ + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: dist + path: dist/ + + - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 + release: + name: GitHub Release + needs: [build] runs-on: ubuntu-24.04 + permissions: + contents: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: dist + path: dist/ + - name: Create GitHub Release env: GH_TOKEN: ${{ github.token }} - run: gh release create "$GITHUB_REF_NAME" --generate-notes + run: gh release create "$GITHUB_REF_NAME" dist/* --generate-notes diff --git a/RELEASING.md b/RELEASING.md index df7057e..778d97f 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -5,6 +5,7 @@ - You must have push access to the repository - Tags matching `v*` are protected by GitHub tag protection rulesets - Releases are immutable once published (GitHub repository setting) +- PyPI Trusted Publisher must be configured (see [Setup](#trusted-publisher-setup-one-time) below) ## Release Process @@ -17,13 +18,15 @@ git tag v4.X.Y git push origin v4.X.Y ``` -6. The [release workflow](.github/workflows/release.yml) will automatically create a GitHub Release with auto-generated release notes -7. Verify the release appears at https://github.com/chartmogul/chartmogul-python/releases -8. Build and publish to PyPI: - ```sh - python3 setup.py sdist - twine upload dist/* - ``` +6. The [release workflow](.github/workflows/release.yml) automatically: + - Runs lint and tests across Python 3.10-3.14 + - Verifies the tag version matches `chartmogul/version.py` + - Builds sdist and wheel + - Publishes to PyPI via Trusted Publisher (OIDC) + - Creates a GitHub Release with auto-generated notes and attached distribution files +7. Verify the release: + - GitHub: https://github.com/chartmogul/chartmogul-python/releases + - PyPI: https://pypi.org/project/chartmogul/ ## Changelog @@ -53,6 +56,8 @@ These will be automatically marked as pre-releases on GitHub. ### PyPI +- Publishing uses [Trusted Publishers (OIDC)](https://docs.pypi.org/trusted-publishers/) — no API tokens or secrets are stored in the repository +- Authentication is based on GitHub's OIDC identity token, scoped to this specific repository, workflow file, and environment - [PyPI](https://pypi.org) enforces version immutability: once a package version is published, it cannot be overwritten - Users can verify package integrity using pip's hash-checking mode (`--require-hashes`) - Pinning versions in `requirements.txt` with hashes ensures reproducible installs @@ -61,6 +66,7 @@ These will be automatically marked as pre-releases on GitHub. - A compromised maintainer account cannot modify or delete existing releases - Tags cannot be moved to point to different commits after publication +- No long-lived API tokens that could be leaked or stolen - PyPI version immutability provides an independent verification layer beyond GitHub ### Repository Settings (Admin) @@ -69,3 +75,16 @@ These settings must be configured by a repository admin: 1. **Immutable Releases**: Settings > General > Releases > Enable "Immutable releases" 2. **Tag Protection Ruleset**: Settings > Rules > Rulesets > New ruleset targeting tags matching `v*` with deletion and force-push prevention + +### Trusted Publisher Setup (One-Time) + +**On PyPI** (project maintainer): +1. Go to https://pypi.org/manage/project/chartmogul/settings/publishing/ +2. Under "Add a new publisher", select "GitHub Actions" +3. Fill in: Owner = `chartmogul`, Repository = `chartmogul-python`, Workflow name = `release.yml`, Environment name = `pypi` +4. Click "Add" + +**On GitHub** (repository admin): +1. Go to repository Settings > Environments +2. Create environment named `pypi` +3. Optionally add deployment protection rules (e.g., require approval from specific reviewers before publishing) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a28da48 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=75.0", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/setup.py b/setup.py index 723efec..d2e0e5b 100644 --- a/setup.py +++ b/setup.py @@ -1,17 +1,11 @@ #!/usr/bin/env python -import os import re -import sys from codecs import open from setuptools import setup github_url = "https://github.com/chartmogul/chartmogul-python" -if sys.argv[-1] == "publish": - os.system("python setup.py sdist upload") - sys.exit() - requires = [ "requests>=2.31.0", "uritemplate>=4.1.1", From f976cb512bb8c7b34982eab7cb3a4fc69754a094 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Tue, 14 Apr 2026 15:50:48 +0200 Subject: [PATCH 6/9] Enable build attestations for PyPI publishing Adds SLSA provenance attestations so users can cryptographically verify that published packages were built from this repository by the release workflow. --- .github/workflows/release.yml | 2 ++ RELEASING.md | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 53004f9..5940d7f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -110,6 +110,8 @@ jobs: path: dist/ - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 + with: + attestations: true release: name: GitHub Release diff --git a/RELEASING.md b/RELEASING.md index 778d97f..c327879 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -58,6 +58,7 @@ These will be automatically marked as pre-releases on GitHub. - Publishing uses [Trusted Publishers (OIDC)](https://docs.pypi.org/trusted-publishers/) — no API tokens or secrets are stored in the repository - Authentication is based on GitHub's OIDC identity token, scoped to this specific repository, workflow file, and environment +- Each release includes [build attestations](https://docs.pypi.org/attestations/) (SLSA provenance), allowing users to cryptographically verify that the package was built from this repository - [PyPI](https://pypi.org) enforces version immutability: once a package version is published, it cannot be overwritten - Users can verify package integrity using pip's hash-checking mode (`--require-hashes`) - Pinning versions in `requirements.txt` with hashes ensures reproducible installs From 39cdcca803c2d8fab288372961481c83fc77f747 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Tue, 14 Apr 2026 16:48:44 +0200 Subject: [PATCH 7/9] Remove stale CI references and fix version classifiers - Rename test workflow from "Run specs and generate Code Climate report" to "Test" (Code Climate is no longer used) - Remove orphaned coverage XML generation (no service consumes it) - Replace Travis CI badge with GitHub Actions badge - Replace outdated manual releasing instructions with pointer to RELEASING.md - Drop Python 3.9 classifier (untested), add 3.14 (tested) --- .github/workflows/release.yml | 4 ++-- .github/workflows/test.yml | 6 +++--- README.md | 14 ++------------ setup.py | 2 +- 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5940d7f..f018eb4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,9 +54,9 @@ jobs: ${{ runner.os }}-pip- ${{ runner.os }}- - run: | - python -m pip install --upgrade pip setuptools wheel coverage + python -m pip install --upgrade pip setuptools wheel pip install -e .[testing] - - run: coverage run -m unittest && coverage xml -i --include='chartmogul/*' + - run: python -m unittest build: name: Build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 925628c..4691f95 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Run specs and generate Code Climate report +name: Test on: push: branches: [ main ] @@ -54,7 +54,7 @@ jobs: ${{ runner.os }}- - name: Install dependencies run: | - python -m pip install --upgrade pip setuptools wheel coverage + python -m pip install --upgrade pip setuptools wheel pip install -e .[testing] - name: Run tests - run: coverage run -m unittest && coverage xml -i --include='chartmogul/*' \ No newline at end of file + run: python -m unittest \ No newline at end of file diff --git a/README.md b/README.md index 0ef90fd..9bdafbb 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

chartmogul-python provides convenient Python bindings for ChartMogul's API.

PyPI version - Build Status + Build Status


@@ -472,17 +472,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/chartm ## Releasing -Make sure that: -1. you have prepared `~/.pypirc` with credentials, -2. a higher version has been set in `chartmogul/__init__.py`, -3. Run tests `python3 -m unittest` -4. Build package `python3 setup.py sdist` -4. release works `twine upload --repository-url https://test.pypi.org/legacy/ dist/*`, -5. release to production `twine upload dist/*`, - -[Read full HOWTO](http://peterdowns.com/posts/first-time-with-pypi.html) - -See also [RELEASING.md](RELEASING.md) for the full release process and security details. +See [RELEASING.md](RELEASING.md) for the full release process and security details. ## Security diff --git a/setup.py b/setup.py index d2e0e5b..3138b3d 100644 --- a/setup.py +++ b/setup.py @@ -59,10 +59,10 @@ classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "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", ], ) From 305d417e118ee5b3d9dbba7e0186d4d72347df52 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Thu, 16 Apr 2026 11:40:38 +0200 Subject: [PATCH 8/9] Add release script and update release docs bin/release.sh takes patch/minor/major, checks prerequisites and CI status, shows open and merged PRs for review, bumps the version, opens a release PR, waits for merge, tags, and polls until the release workflow completes. If interrupted, prints remaining manual steps. Route version bumps through PR flow instead of pushing to main directly. Replace CHANGELOG.md contents with a link to GitHub Releases where notes are now auto-generated from merged PR titles. --- CHANGELOG.md | 87 +------------------- RELEASING.md | 72 ++++++++--------- bin/release.sh | 216 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 250 insertions(+), 125 deletions(-) create mode 100755 bin/release.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 20b8b1b..9c1d43d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,88 +1,3 @@ # Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog], -and this project adheres to [Semantic Versioning]. - -[Keep a Changelog]: https://keepachangelog.com/en/1.0.0/ -[Semantic Versioning]: https://semver.org/spec/v2.0.0.html - -## [4.9.0] - 2026-03-16 -- Add `external_id` key to Contact model - -## [4.8.0] - 2026-01-06 -- Mention filters (CFL) param for metrics api -- Add Customer.subscriptions -- Update CustomerSubscription.list_imported to deprecated -- Add disabled, disabled_at, edit_history_summary and errors fields to Invoice - -## [4.7.0] - 2025-12-22 -- Add fetching additional DataSource fields - -## [4.6.4] - 2025-11-24 -- Add Subscription Set ID key to Activity - -## [4.6.3] - 2025-09-01 -- Remove future dependency to resolve vulnerability issues - -## [4.6.2] - 2025-07-09 -- Update Marshmallow dependency to use >=3.24.0 - -## [4.6.1] - 2025-05-19 -- Fixed Tasks API schema issue -- Unify requirements in a single place -- Support for Python 3.13. - -## [4.6.0] - 2025-04-25 -- Adds support for Tasks (https://dev.chartmogul.com/reference/tasks) - -## [4.5.1] - 2025-04-07 -- Update urllib3 dependency to use >=2.2.3 to allow for future minor updates - -## [4.5.0] - 2025-03-18 -- Adds support for disconnecting subscriptions -- Adds support for transaction fees to transactions - -## [4.4.0] - 2024-10-24 -- Adds support for unmerging customers - -## [4.3.2] - 2024-06-26 -- Remove VCR dependencies -- Replaced unit tests that depended on VCR using `request_mock` -- Updated urllib3 to latest secure version - -## [4.3.1] - 2024-06-20 -- Update the urllib3 dependency to a secure version - -## [4.3.0] - 2024-03-25 -- Adds support for Opportunities (https://dev.chartmogul.com/reference/opportunities) - -## [4.2.1] - 2024-01-15 -- Fix customer website_url, add missing allow_none=True - -## [4.2.0] - 2024-01-08 -- Add support for customer website_url - -## [4.1.1] - 2023-12-21 -- Fix missing customer_uuid when creating a note from a customer - -## [4.1.0] - 2023-12-20 -- Support customer notes - -## [4.0.0] - 2023-10-04 - -### Added -- v4.0.0 upgrade instructions. -- Support for Python 3.12. - -### Removed -- Support for old pagination using `page` query params. -- Deprecated `imp` module. -- Support for Python 3.7. - -## [3.1.3] - 2023-09-27 - -### Added -- Support for cursor based pagination to `.all()` endpoints. -- Changelog. +Release notes are auto-generated from merged pull request titles and published on the [GitHub Releases page](https://github.com/chartmogul/chartmogul-python/releases). diff --git a/RELEASING.md b/RELEASING.md index c327879..47c25e7 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -3,30 +3,35 @@ ## Prerequisites - You must have push access to the repository +- `git`, `gh`, `jq`, and `python3` must be installed - Tags matching `v*` are protected by GitHub tag protection rulesets - Releases are immutable once published (GitHub repository setting) -- PyPI Trusted Publisher must be configured (see [Setup](#trusted-publisher-setup-one-time) below) +- PyPI [trusted publishing](https://docs.pypi.org/trusted-publishers/) must be configured for the package (see [Repository Settings](#repository-settings-admin)) ## Release Process -1. Ensure all changes are merged to the `main` branch -2. Ensure CI is green on the `main` branch -3. Update the version in `chartmogul/version.py` -4. Commit the version bump -5. Create and push a version tag: - ```sh - git tag v4.X.Y - git push origin v4.X.Y - ``` -6. The [release workflow](.github/workflows/release.yml) automatically: - - Runs lint and tests across Python 3.10-3.14 - - Verifies the tag version matches `chartmogul/version.py` - - Builds sdist and wheel - - Publishes to PyPI via Trusted Publisher (OIDC) - - Creates a GitHub Release with auto-generated notes and attached distribution files -7. Verify the release: - - GitHub: https://github.com/chartmogul/chartmogul-python/releases - - PyPI: https://pypi.org/project/chartmogul/ +Run the release script from the repository root: + +```sh +bin/release.sh +``` + +The script will: + +1. Verify prerequisites and that CI is green on `main` +2. Show any open PRs targeting `main` and ask for confirmation +3. Show PRs merged since the last tag (what's being released) and ask for confirmation +4. Bump the version in `chartmogul/version.py` +5. Create a release branch, commit, push, and open a PR +6. Wait for the PR to be merged (poll every 10s) +7. Tag the merge commit and push the tag +8. Wait for the [release workflow](.github/workflows/release.yml) to complete, which will: + - Run lint and the full test suite across Python 3.10-3.14 + - Verify that `chartmogul/version.py` version matches the tag + - Build sdist and wheel + - Create a GitHub Release with auto-generated release notes and attached distribution files + - Publish to PyPI via [OIDC trusted publishing](https://docs.pypi.org/trusted-publishers/) with [build attestations](https://docs.pypi.org/attestations/) +9. Print links to the GitHub Release and PyPI package ## Changelog @@ -56,36 +61,25 @@ These will be automatically marked as pre-releases on GitHub. ### PyPI -- Publishing uses [Trusted Publishers (OIDC)](https://docs.pypi.org/trusted-publishers/) — no API tokens or secrets are stored in the repository -- Authentication is based on GitHub's OIDC identity token, scoped to this specific repository, workflow file, and environment -- Each release includes [build attestations](https://docs.pypi.org/attestations/) (SLSA provenance), allowing users to cryptographically verify that the package was built from this repository -- [PyPI](https://pypi.org) enforces version immutability: once a package version is published, it cannot be overwritten +- Publishing uses [OIDC trusted publishing](https://docs.pypi.org/trusted-publishers/) — no long-lived API tokens are stored in the repository. GitHub Actions authenticates directly with PyPI via short-lived OIDC tokens. +- Once a package version is published to PyPI, [it cannot be republished](https://pypi.org/help/#file-name-reuse) with different contents +- Packages are published with [build attestations](https://docs.pypi.org/attestations/) (SLSA provenance), linking each version to the specific GitHub Actions run that built it - Users can verify package integrity using pip's hash-checking mode (`--require-hashes`) - Pinning versions in `requirements.txt` with hashes ensures reproducible installs ### What This Protects Against - A compromised maintainer account cannot modify or delete existing releases +- No long-lived API tokens exist that could be leaked or stolen - Tags cannot be moved to point to different commits after publication -- No long-lived API tokens that could be leaked or stolen -- PyPI version immutability provides an independent verification layer beyond GitHub +- The PyPI registry provides an independent immutability layer beyond GitHub +- Build attestations allow anyone to verify a package was built from this repository by GitHub Actions ### Repository Settings (Admin) These settings must be configured by a repository admin: 1. **Immutable Releases**: Settings > General > Releases > Enable "Immutable releases" -2. **Tag Protection Ruleset**: Settings > Rules > Rulesets > New ruleset targeting tags matching `v*` with deletion and force-push prevention - -### Trusted Publisher Setup (One-Time) - -**On PyPI** (project maintainer): -1. Go to https://pypi.org/manage/project/chartmogul/settings/publishing/ -2. Under "Add a new publisher", select "GitHub Actions" -3. Fill in: Owner = `chartmogul`, Repository = `chartmogul-python`, Workflow name = `release.yml`, Environment name = `pypi` -4. Click "Add" - -**On GitHub** (repository admin): -1. Go to repository Settings > Environments -2. Create environment named `pypi` -3. Optionally add deployment protection rules (e.g., require approval from specific reviewers before publishing) +2. **Tag Protection Ruleset**: Settings > Rules > Rulesets > New ruleset targeting tags matching `v*` with deletion, force-push, and update prevention +3. **GitHub Actions Environment**: Settings > Environments > New environment named `pypi` +4. **PyPI Trusted Publishing**: On pypi.org, go to [chartmogul settings](https://pypi.org/manage/project/chartmogul/settings/publishing/) and configure a trusted publisher with: repository `chartmogul/chartmogul-python`, workflow `release.yml`, environment `pypi` diff --git a/bin/release.sh b/bin/release.sh new file mode 100755 index 0000000..634d1c9 --- /dev/null +++ b/bin/release.sh @@ -0,0 +1,216 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ─── Usage ────────────────────────────────────────────────────────────────────── + +usage() { + echo "Usage: bin/release.sh " + exit 1 +} + +[[ $# -eq 1 ]] || usage + +BUMP_TYPE="$1" +case "$BUMP_TYPE" in + patch|minor|major) ;; + *) usage ;; +esac + +# ─── Prerequisites ────────────────────────────────────────────────────────────── + +for cmd in git gh jq python3; do + if ! command -v "$cmd" &>/dev/null; then + echo "Error: '$cmd' is required but not found on PATH." >&2 + exit 1 + fi +done + +REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner) +STASHED=false + +restore_stash() { + if [[ "$STASHED" == true ]]; then + git checkout main 2>/dev/null || true + git stash pop --quiet 2>/dev/null || true + STASHED=false + fi +} + +# ─── Check main CI is green ───────────────────────────────────────────────────── + +echo "Checking CI status on main..." +LAST_RUN=$(gh run list --branch main --workflow test.yml --limit 1 --json conclusion,url) +CONCLUSION=$(echo "$LAST_RUN" | jq -r '.[0].conclusion // empty') +RUN_URL=$(echo "$LAST_RUN" | jq -r '.[0].url // empty') + +if [[ "$CONCLUSION" != "success" ]]; then + echo "Error: Latest CI run on main is not green (status: ${CONCLUSION:-unknown})." >&2 + [[ -n "$RUN_URL" ]] && echo " $RUN_URL" >&2 + exit 1 +fi +echo "CI is green." + +# ─── Show open PRs ────────────────────────────────────────────────────────────── + +OPEN_PRS=$(gh pr list --base main --json number,title,url) +PR_COUNT=$(echo "$OPEN_PRS" | jq 'length') + +if [[ "$PR_COUNT" -gt 0 ]]; then + echo "" + echo "There are $PR_COUNT open PR(s) targeting main:" + echo "$OPEN_PRS" | jq -r '.[] | " #\(.number) \(.title)\n \(.url)"' + echo "" + read -rp "Continue releasing? [y/N] " CONFIRM + if [[ ! "$CONFIRM" =~ ^[yY]$ ]]; then + echo "Aborted." + exit 0 + fi +fi + +# ─── Show PRs included in this release ─────────────────────────────────────────── + +LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") +if [[ -n "$LAST_TAG" ]]; then + echo "" + echo "PRs merged since ${LAST_TAG}:" + MERGED_PRS=$(gh pr list --base main --state merged --search "merged:>=$(git log -1 --format=%aI "$LAST_TAG")" --json number,title,url) +else + echo "" + echo "PRs merged (no previous tag found, showing recent):" + MERGED_PRS=$(gh pr list --base main --state merged --limit 10 --json number,title,url) +fi + +MERGED_COUNT=$(echo "$MERGED_PRS" | jq 'length') +if [[ "$MERGED_COUNT" -eq 0 ]]; then + echo " (none)" +else + echo "$MERGED_PRS" | jq -r '.[] | " #\(.number) \(.title)\n \(.url)"' +fi + +echo "" +read -rp "Release these changes as ${BUMP_TYPE}? [y/N] " CONFIRM +if [[ ! "$CONFIRM" =~ ^[yY]$ ]]; then + echo "Aborted." + exit 0 +fi + +# ─── Bump version ─────────────────────────────────────────────────────────────── + +VERSION_FILE="chartmogul/version.py" +CURRENT_VERSION=$(python3 -c "import re; print(re.search(r\"^__version__\s*=\s*['\\\"]([^'\\\"]*)['\\\"]\" , open('${VERSION_FILE}').read(), re.MULTILINE).group(1))") +IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" + +case "$BUMP_TYPE" in + major) NEW_VERSION="$((MAJOR + 1)).0.0" ;; + minor) NEW_VERSION="${MAJOR}.$((MINOR + 1)).0" ;; + patch) NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))" ;; +esac + +TAG="v${NEW_VERSION}" +BRANCH="release/${TAG}" + +echo "" +echo "Bumping version: ${CURRENT_VERSION} -> ${NEW_VERSION}" + +git stash --include-untracked --quiet +STASHED=true + +sed -i '' "s/__version__ = ['\"].*['\"]/__version__ = \"${NEW_VERSION}\"/" "$VERSION_FILE" + +# ─── Create release branch and PR ─────────────────────────────────────────────── + +git checkout -b "$BRANCH" main +git add "$VERSION_FILE" +git commit -m "Update version to ${TAG}" +git push -u origin "$BRANCH" + +PR_URL=$(gh pr create --title "Release ${TAG}" --body "Bump version to ${TAG}" | tail -1) +PR_NUMBER=$(gh pr view "$PR_URL" --json number -q .number) + +echo "" +echo "PR created: $PR_URL" +echo "Waiting for PR #${PR_NUMBER} to be merged..." + +# ─── Poll for PR merge ────────────────────────────────────────────────────────── + +interrupt_merge_wait() { + echo "" + echo "Interrupted. To finish the release manually:" + echo " 1. Merge PR: $PR_URL" + echo " 2. git checkout main && git pull" + echo " 3. git tag ${TAG} && git push origin ${TAG}" + echo " 4. Monitor the release workflow on GitHub Actions" + restore_stash + exit 0 +} + +trap interrupt_merge_wait INT + +while true; do + STATE=$(gh pr view "$PR_NUMBER" --json state -q .state) + if [[ "$STATE" == "MERGED" ]]; then + echo "" + echo "PR #${PR_NUMBER} merged." + break + fi + printf "." + sleep 10 +done + +trap - INT + +# ─── Tag and push ─────────────────────────────────────────────────────────────── + +git checkout main +git pull +git tag "$TAG" +git push origin "$TAG" + +echo "Tag ${TAG} pushed." +echo "Waiting for release workflow..." + +# ─── Poll for release CI ──────────────────────────────────────────────────────── + +interrupt_ci_wait() { + echo "" + echo "Interrupted. The tag ${TAG} has been pushed and the release workflow is running." + echo " Workflow: https://github.com/${REPO}/actions/workflows/release.yml" + echo " GitHub: https://github.com/${REPO}/releases/tag/${TAG}" + echo " PyPI: https://pypi.org/project/chartmogul/${NEW_VERSION}/" + restore_stash + exit 0 +} + +trap interrupt_ci_wait INT + +sleep 5 +while true; do + RUN=$(gh run list --branch "$TAG" --workflow release.yml --limit 1 --json status,conclusion,url) + STATUS=$(echo "$RUN" | jq -r '.[0].status // empty') + RUN_CONCLUSION=$(echo "$RUN" | jq -r '.[0].conclusion // empty') + RELEASE_RUN_URL=$(echo "$RUN" | jq -r '.[0].url // empty') + + if [[ "$STATUS" == "completed" ]]; then + echo "" + if [[ "$RUN_CONCLUSION" == "success" ]]; then + echo "Release workflow completed successfully." + else + echo "Release workflow finished with status: ${RUN_CONCLUSION}" >&2 + fi + [[ -n "$RELEASE_RUN_URL" ]] && echo " $RELEASE_RUN_URL" + break + fi + printf "." + sleep 10 +done + +trap - INT + +# ─── Summary ──────────────────────────────────────────────────────────────────── + +restore_stash + +echo "" +echo "Release ${TAG} complete!" +echo " GitHub: https://github.com/${REPO}/releases/tag/${TAG}" +echo " PyPI: https://pypi.org/project/chartmogul/${NEW_VERSION}/" From 7fd4bf709065b6bf18b2b875f389c065fcee6b55 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Thu, 16 Apr 2026 11:40:43 +0200 Subject: [PATCH 9/9] Suppress zizmor cache-poisoning findings for release workflow The release workflow uses actions/cache for pip dependencies in the lint and test jobs. These are low-confidence findings since the cache only contains pip packages (not executable code) and the jobs have read-only permissions. --- zizmor.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/zizmor.yml b/zizmor.yml index 39d1b18..72eb5ac 100644 --- a/zizmor.yml +++ b/zizmor.yml @@ -1,3 +1,7 @@ rules: secrets-outside-env: disable: true + cache-poisoning: + ignore: + - release.yml:22 + - release.yml:49