From 3e7c0efe9f4431cedae699e3d3ac5c8373fca2f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 17:05:15 +0000 Subject: [PATCH 1/3] Add PyPI release workflow via Trusted Publishing (#14) Add .github/workflows/release.yml that builds and publishes the package using PyPI Trusted Publishing (OIDC), so no API tokens are stored in GitHub: - build job: `python -m build` + `twine check`, uploads dist artifacts. - publish-pypi: runs on a published GitHub Release -> uploads to PyPI (environment: pypi). - publish-testpypi: runs on manual dispatch -> uploads to TestPyPI for staging (environment: testpypi). Requires a one-time "pending publisher" configured on PyPI/TestPyPI; setup steps are documented in the workflow header. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XYvSCw1sjoG2AHbLPCsX7S --- .github/workflows/release.yml | 86 +++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5dbf1d4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,86 @@ +name: Release + +# Publishes the package to PyPI via Trusted Publishing (OIDC) — no API tokens +# are stored in GitHub. +# +# Triggers: +# * Publishing a GitHub Release -> builds and uploads to PyPI. +# * Manual "Run workflow" -> builds and uploads to TestPyPI (staging). +# +# One-time setup on the package index (no secrets needed afterwards): +# PyPI: https://pypi.org/manage/account/publishing/ +# TestPyPI: https://test.pypi.org/manage/account/publishing/ +# Add a "pending publisher" for: +# - PyPI project name: domain-security-analyzer +# - Owner: CallMarcus +# - Repository: domain-security-analyzer +# - Workflow name: release.yml +# - Environment: pypi (for PyPI) / testpypi (for TestPyPI) + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + build: + name: Build distributions + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Build and validate + run: | + python -m pip install --upgrade pip build twine + python -m build + twine check dist/* + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish-testpypi: + name: Publish to TestPyPI + # Manual runs go to TestPyPI for staging/verification. + if: github.event_name == 'workflow_dispatch' + needs: build + runs-on: ubuntu-latest + environment: + name: testpypi + url: https://test.pypi.org/project/domain-security-analyzer/ + permissions: + id-token: write # required for Trusted Publishing (OIDC) + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - name: Publish to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + + publish-pypi: + name: Publish to PyPI + # Only a published GitHub Release uploads to production PyPI. + if: github.event_name == 'release' + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/domain-security-analyzer/ + permissions: + id-token: write # required for Trusted Publishing (OIDC) + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 From 95b1615316fdcf8de0f86da4ca594c9e73f53ea5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 16:24:40 +0000 Subject: [PATCH 2/3] Derive version from git tags via setuptools-scm Make the git tag the single source of truth for the package version, so releasing no longer requires editing a hardcoded version string: - pyproject.toml: switch to setuptools-scm (build-system requires + [tool.setuptools_scm] writing domain_security_analyzer/_version.py); drop the static [tool.setuptools.dynamic] attr. - __version__.py: resolve the version at runtime from installed package metadata, falling back to the setuptools-scm _version.py in a source tree. - Bump requires-python to >=3.8 (3.7 is EOL; importlib.metadata is stdlib from 3.8) and refresh classifiers (drop 3.7, add 3.12). - CI/release: checkout with fetch-depth: 0 so setuptools-scm sees tags. - gitignore the generated _version.py. A clean checkout of tag `vX.Y.Z` now builds exactly version `X.Y.Z`. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XYvSCw1sjoG2AHbLPCsX7S --- .github/workflows/ci.yml | 4 ++++ .github/workflows/release.yml | 2 ++ .gitignore | 3 +++ domain_security_analyzer/__version__.py | 18 +++++++++++++++++- pyproject.toml | 13 ++++++++----- 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 239b0d8..4987d5d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,8 @@ jobs: python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history + tags for setuptools-scm - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: @@ -38,6 +40,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history + tags for setuptools-scm - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5dbf1d4..0ba8917 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,6 +28,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history + tags for setuptools-scm - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.gitignore b/.gitignore index f818088..50576e3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ build/ dist/ *.egg-info/ *.egg + +# Version file generated by setuptools-scm +domain_security_analyzer/_version.py diff --git a/domain_security_analyzer/__version__.py b/domain_security_analyzer/__version__.py index 5becc17..c48e6fe 100644 --- a/domain_security_analyzer/__version__.py +++ b/domain_security_analyzer/__version__.py @@ -1 +1,17 @@ -__version__ = "1.0.0" +"""Resolve the installed package version. + +The version is owned by git tags via setuptools-scm — there is no hardcoded +version string to keep in sync. At runtime we read it from the installed +package metadata, falling back to the file setuptools-scm writes at build time +(``_version.py``) when running from an unbuilt source tree. +""" + +from importlib.metadata import PackageNotFoundError, version as _pkg_version + +try: + __version__ = _pkg_version("domain-security-analyzer") +except PackageNotFoundError: # pragma: no cover - source tree without install + try: + from ._version import version as __version__ + except Exception: + __version__ = "0.0.0+unknown" diff --git a/pyproject.toml b/pyproject.toml index cc6fa49..5a14df6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=61.0", "wheel"] +requires = ["setuptools>=64", "setuptools-scm>=8"] build-backend = "setuptools.build_meta" [project] @@ -7,7 +7,7 @@ name = "domain-security-analyzer" dynamic = ["version"] description = "Analyze domain security configurations: DNS, email authentication (SPF/DKIM/DMARC), subdomain discovery, and Subresource Integrity (SRI) scanning." readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.8" license = { text = "MIT" } authors = [{ name = "CallMarcus" }] keywords = [ @@ -31,11 +31,11 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "dnspython>=2.4.0", @@ -65,5 +65,8 @@ py-modules = ["domain_analyzer"] [tool.setuptools.packages.find] include = ["domain_security_analyzer*"] -[tool.setuptools.dynamic] -version = { attr = "domain_security_analyzer.__version__.__version__" } +# Version is derived from git tags (single source of truth). Tag `v1.2.3` +# produces version `1.2.3`; commits after a tag get a dev/local suffix. +[tool.setuptools_scm] +version_file = "domain_security_analyzer/_version.py" +fallback_version = "0.0.0" From cb38252ccc24ac65a3a9ed69ed3920cc5273c6ac Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 16:55:37 +0000 Subject: [PATCH 3/3] Trigger PyPI release on pushed version tags Add a `push: tags: ['v*']` trigger to release.yml so a tag push publishes to PyPI directly (no GitHub Release required): `git tag v1.2.3 && git push origin v1.2.3`. The publish-pypi job now runs on either a tag push or a published GitHub Release; manual dispatch still targets TestPyPI. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XYvSCw1sjoG2AHbLPCsX7S --- .github/workflows/release.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0ba8917..48ddd83 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,9 +4,13 @@ name: Release # are stored in GitHub. # # Triggers: +# * Pushing a version tag (v*) -> builds and uploads to PyPI. # * Publishing a GitHub Release -> builds and uploads to PyPI. # * Manual "Run workflow" -> builds and uploads to TestPyPI (staging). # +# To cut a release: `git tag v1.2.3 && git push origin v1.2.3` +# (setuptools-scm stamps the build with the tag, so the version is 1.2.3). +# # One-time setup on the package index (no secrets needed afterwards): # PyPI: https://pypi.org/manage/account/publishing/ # TestPyPI: https://test.pypi.org/manage/account/publishing/ @@ -18,6 +22,9 @@ name: Release # - Environment: pypi (for PyPI) / testpypi (for TestPyPI) on: + push: + tags: + - 'v*' release: types: [published] workflow_dispatch: @@ -69,8 +76,8 @@ jobs: publish-pypi: name: Publish to PyPI - # Only a published GitHub Release uploads to production PyPI. - if: github.event_name == 'release' + # A pushed version tag or a published GitHub Release uploads to production PyPI. + if: github.event_name == 'push' || github.event_name == 'release' needs: build runs-on: ubuntu-latest environment: