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 new file mode 100644 index 0000000..48ddd83 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,95 @@ +name: Release + +# Publishes the package to PyPI via Trusted Publishing (OIDC) — no API tokens +# 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/ +# 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: + push: + tags: + - 'v*' + release: + types: [published] + workflow_dispatch: + +jobs: + build: + name: Build distributions + 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: + 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 + # 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: + 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 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"