From 7315af6d29c70fb6ff2e609de05be9b1fd96cd76 Mon Sep 17 00:00:00 2001 From: Rafid Aslam Date: Mon, 24 Aug 2026 18:18:36 +0700 Subject: [PATCH] build: publish to PyPI as tirith-iac-governance Until now the only way to install tirith was a git URL. That needs git and the network at build time, resolves no wheels, and put us one deleted branch away from an unbuildable image -- which already happened once. Every consumer, including three separate CI integrations, carried a comment explaining why `pip install tirith` is the wrong command. The distribution is `tirith-iac-governance`, matching the action, the wfstep, the GitLab component and the step template. The import and the console script are still `tirith`. `pip install tirith` still gets an unrelated project that has held the name on PyPI since 2016, so the README says so plainly rather than leaving people to find out. Packaging, in order of how badly it was needed: - pyproject.toml had no [build-system] at all, so builds fell back to legacy setup.py semantics against whatever setuptools was around. - The version lived in two files kept in step by hand, and nothing asserted they agreed. setup.py now reads src/tirith/__init__.py, which is what `tirith --version` already reported. - license="Apache" was not an SPDX identifier; LICENSE was never declared. - The classifiers advertised 3.8 and 3.9 while CI tested 3.8 through 3.12, and claimed Pre-Alpha on a release used in production pipelines. - MANIFEST.in listed four directories this repository does not have. - setup_requires=["pytest-runner"] is deprecated and forced a download on install for a runner nothing invokes. release-pypi.yml publishes over Trusted Publishing, so there is no API token anywhere. It triggers on bare semver, matching every tag since 1.0.0-beta.2 -- bump_version.py had been printing instructions for a `v` prefix that no tag has used since 2022. A guard job refuses to publish when the tag and the shipped version disagree, because a PyPI version can be yanked but never replaced, and a build job asserts the wheel carries the TUI stylesheet and examples: without them the interface still starts, unstyled and with an empty playground, which no import check would catch. Docs, README and the install hints printed at runtime all move to the PyPI command. The README's four screenshots and four relative links are now absolute -- PyPI renders that file as the project description and resolves neither. --- .github/workflows/release-pypi.yml | 126 ++++++++++++++++++ CHANGELOG.md | 18 ++- MANIFEST.in | 25 +--- README.md | 40 +++--- .../docs/getting-started-with-tirith.md | 2 +- .../tirith-installation/quick-intallation.md | 5 +- .../docs/tirith-usage/ci-integration.md | 13 +- .../tirith-usage/interactive-interface.md | 6 +- documentation/src/pages/index.js | 2 +- pyproject.toml | 9 +- setup.py | 57 ++++---- src/tirith/__init__.py | 2 +- src/tirith/tui/__init__.py | 4 +- src/tirith/tui/cli.py | 3 +- src/tirith/tui/examples.py | 2 +- tests/tui/test_app.py | 2 +- tests/tui/test_ui_cli.py | 2 +- tools/bump_version.py | 19 +-- 18 files changed, 233 insertions(+), 104 deletions(-) create mode 100644 .github/workflows/release-pypi.yml diff --git a/.github/workflows/release-pypi.yml b/.github/workflows/release-pypi.yml new file mode 100644 index 00000000..6ffcab59 --- /dev/null +++ b/.github/workflows/release-pypi.yml @@ -0,0 +1,126 @@ +name: Release to PyPI + +# Publishing is deliberately split from `Build & test`, which triggers on bare `push:` and so +# already fires on a tag -- but nothing gates on it, so a tag push would otherwise publish in +# parallel with the tests rather than after them. Here `publish` needs `build` needs `test`. +# +# Authentication is Trusted Publishing (OIDC): PyPI verifies a short-lived token bound to this +# repository, this workflow filename and the named environment. There is no API token to store, +# rotate, or leak -- which is why the `id-token: write` permission is granted to the publish job +# alone and nowhere else. + +on: + push: + # Bare semver, because that is what every tag since 1.0.0-beta.2 uses. The two `v`-prefixed + # tags are from 2022 and abandoned. The trailing `*` catches prereleases like 1.3.0-beta.1. + tags: ["[0-9]+.[0-9]+.[0-9]+*"] + workflow_dispatch: + inputs: + target: + description: "Where to publish. Rehearse on TestPyPI before tagging for real." + type: choice + options: [testpypi, pypi] + default: testpypi + +permissions: + contents: read + +jobs: + guard: + name: Check the tag matches the shipped version + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + # A PyPI version is immutable: a wrong one can be yanked but never replaced, and the fix is + # always a new number. Nothing else in this repository asserts that the tag, setup.py and + # `tirith --version` agree, so it is asserted here, before anything is built. + - name: Tag == tirith.__version__ + if: startsWith(github.ref, 'refs/tags/') + run: | + set -euo pipefail + shipped=$(python -c "import re;print(re.search(r'^__version__ = \"([^\"]+)\"', open('src/tirith/__init__.py').read(), re.M).group(1))") + tag="${GITHUB_REF_NAME}" + echo "tag=$tag shipped=$shipped" + if [ "$tag" != "$shipped" ]; then + echo "::error::tag $tag does not match src/tirith/__init__.py ($shipped)" + exit 1 + fi + + test: + name: Test before publishing + needs: guard + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: Pipfile.lock + - run: pip install . && pip install pipenv && pipenv install --system -d + - run: pytest + + build: + name: Build sdist and wheel + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install build twine + - run: python -m build + # `twine check` validates that the README renders as a PyPI description. Catching that here + # rather than after upload matters, because the description cannot be corrected in place. + - run: twine check dist/* + # The TUI ships a stylesheet and five bundled examples as package data. A wheel missing them + # still installs and still starts -- it renders unstyled with an empty playground, which is a + # worse failure than not starting, and one no import check would catch. + - name: The wheel carries its data files + run: | + set -euo pipefail + python - <<'PY' + import glob, sys, zipfile + names = zipfile.ZipFile(glob.glob("dist/*.whl")[0]).namelist() + css = [n for n in names if n.endswith("tui/app.css")] + examples = [n for n in names if "/tui/examples/" in n and n.endswith(("about.md", "policy.json", "input.json"))] + print(f"stylesheet={css} example files={len(examples)}") + if not css or len(examples) < 3: + sys.exit("wheel is missing TUI package data") + PY + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + name: Publish + needs: build + runs-on: ubuntu-latest + environment: ${{ github.event_name == 'workflow_dispatch' && inputs.target || 'pypi' }} + permissions: + id-token: write # the OIDC token PyPI verifies; nothing else needs it + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + # Two steps rather than one with a computed `repository-url`: the action defaults that input + # to the real PyPI endpoint, and passing an empty string to mean "use the default" is not a + # contract it documents. + - name: Publish to TestPyPI + if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi' + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + + - name: Publish to PyPI + if: github.event_name != 'workflow_dispatch' || inputs.target == 'pypi' + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 19330f0a..2f6e56f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 -## [Unreleased] + +## [1.3.0] - 2026-08-24 ### Added - `tirith ui`: an interactive interface with three tabs. @@ -24,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Playground** — edit a policy and an input side by side and watch the verdict move, with five worked examples that mostly fail on purpose and explain why. - `--serve` runs the same interface over HTTP for a browser. -- Optional extra: `pip install 'py-tirith[tui]'`. Not a hard dependency — the interface needs +- Optional extra: `pip install 'tirith-iac-governance[tui]'`. Not a hard dependency — the interface needs Python 3.9 while tirith supports 3.8, and using tirith as a CI gate should stay dependency-light. Without it, `tirith ui` prints how to install it and exits 1. - A policy validator behind the interface, reporting the mistakes that are otherwise silent: @@ -36,6 +37,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `platform`, so `--json` output remains byte-identical to the golden file. - No new runtime dependencies for anyone who does not install the extra. +### Packaging +- **Published to PyPI as `tirith-iac-governance`.** `pip install tirith-iac-governance`, + or `pip install 'tirith-iac-governance[tui]'` for the interface. Previously the only way in + was a git URL, which needed git and the network at build time and could not resolve wheels. + The import and the command are still `tirith`; `pip install tirith` remains an unrelated + project that has held that name since 2016. +- The version is now read from `src/tirith/__init__.py` alone, rather than being duplicated in + `setup.py` and kept in step by hand. +- `pyproject.toml` declares a `[build-system]`, so builds no longer depend on whatever + setuptools happens to be present. +- Metadata corrected: SPDX `Apache-2.0` with an explicit `license_files`, Python 3.10-3.12 + classifiers to match the tested matrix, and `Development Status :: 5 - Production/Stable`. + ## [1.2.0] - 2026-08-03 ### Added diff --git a/MANIFEST.in b/MANIFEST.in index 3074ec4d..c3685c24 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,28 +1,15 @@ include *.lock include *.md +include LICENSE include Pipfile -recursive-include ci *.gitkeep -recursive-include cli *.sh -recursive-include policies *.json -recursive-include resources *.JPG -recursive-include resources *.conf -recursive-include resources *.css -recursive-include resources *.dockerignore -recursive-include resources *.ico -recursive-include resources *.js -recursive-include resources *.json -recursive-include resources *.md -recursive-include resources *.svg -recursive-include resources Makefile + +# The TUI's stylesheet, and the about.md plus policy JSON beside each bundled playground example. +# Without these the installed interface loads unstyled and the playground has nothing to open. +recursive-include src *.css recursive-include src *.json recursive-include src *.md -# The TUI's stylesheet, and the about.md beside each bundled playground example. Without -# these the installed interface loads unstyled and the playground has nothing to open. -recursive-include src *.css -recursive-include src *.new -recursive-include src *.old recursive-include src *.py -recursive-include src *.rego + recursive-include tests *.json global-exclude *.py[cod] __pycache__/* *.so *.dylib diff --git a/README.md b/README.md index 49ad8c96..57810252 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) -[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://raw.githubusercontent.com/StackGuardian/tirith/main/LICENSE) +[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://raw.githubusercontent.com/StackGuardian/tirith/main/CODE_OF_CONDUCT.md) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=StackGuardian_policy-framework&metric=alert_status&token=4a4d06e73940505edb7fc9d27a7f03b35fbbf23d)](https://sonarcloud.io/summary/new_code?id=StackGuardian_policy-framework) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=StackGuardian_policy-framework&metric=sqale_rating&token=4a4d06e73940505edb7fc9d27a7f03b35fbbf23d)](https://sonarcloud.io/summary/new_code?id=StackGuardian_policy-framework) @@ -13,7 +13,7 @@ > > Explore a failing evaluation down to the resource that caused it, assemble policies from a > form, and experiment in a playground with worked examples. Try it with -> `pip install 'py-tirith[tui] @ git+https://github.com/StackGuardian/tirith.git'`, then +> `pip install 'tirith-iac-governance[tui]'`, then > `tirith ui` — see > [The interactive interface](#the-interactive-interface). > @@ -104,18 +104,18 @@ failed, and on which resource and value. ### For users ``` -pip install git+https://github.com/StackGuardian/tirith.git +pip install tirith-iac-governance ``` -Pin a tag rather than tracking the default branch, so a CI job cannot change behaviour underneath you: +Pin the version in CI, so a release cannot change behaviour underneath you: ``` -pip install "git+https://github.com/StackGuardian/tirith.git@1.0.5" +pip install "tirith-iac-governance==1.3.0" ``` -`1.0.5` is the newest tag; `git ls-remote --tags https://github.com/StackGuardian/tirith.git` lists -them. Tirith is not on PyPI — `pip install tirith` installs an unrelated project of the same name, so -install from git. Python 3.8 or newer. +The distribution is **`tirith-iac-governance`**; the import and the command are both `tirith`. Note that +`pip install tirith` installs an unrelated project of the same name — that name has belonged to +someone else on PyPI since 2016. Python 3.8 or newer. ### For developers @@ -178,7 +178,7 @@ pip install -e . ``` tirith --version -tirith 1.2.0 +tirith 1.3.0 ``` Congratulations! Tirith has been setup in your system @@ -236,11 +236,11 @@ nobody gating a pipeline should pay to install an interface they never open. It 3.9 or newer, while tirith itself still supports 3.8: ```bash -pip install 'py-tirith[tui] @ git+https://github.com/StackGuardian/tirith.git' +pip install 'tirith-iac-governance[tui]' ``` -Tirith is not on PyPI — `pip install py-tirith` finds nothing and `pip install tirith` installs an -unrelated project of the same name — so the extra is requested against the git URL. +The interface is an extra rather than a dependency, so a CI gate does not pay to install a UI it +never opens. ```bash tirith ui # playground, with worked examples @@ -374,7 +374,7 @@ policy: image: python:3.12 needs: [plan] script: - - pip install "git+https://github.com/StackGuardian/tirith.git@1.0.5" + - pip install "tirith-iac-governance==1.3.0" - tirith -policy-path .tirith/policies -input-path plan.json --fail-on-error ``` @@ -442,7 +442,7 @@ Common flags: | `--output-json` / `--output-markdown` | Write the verdict to files for a later CI step | `--api-url` overrides `--region` for a self-hosted or dedicated host. Every flag is in -[docs/platform-check.md](docs/platform-check.md) or `tirith platform check --help`. +[docs/platform-check.md](https://raw.githubusercontent.com/StackGuardian/tirith/main/docs/platform-check.md) or `tirith platform check --help`. Running this from GitHub Actions? Use [the action](#github-actions) instead — it wires up the plan discovery, the sticky pull-request comment, the check run and the exit codes for you. @@ -650,7 +650,7 @@ Input: Output: -![](docs/tf_plan_example.gif) +![](https://raw.githubusercontent.com/StackGuardian/tirith/main/docs/tf_plan_example.gif) JSON Output: ```json @@ -899,7 +899,7 @@ Input: ``` Output: -![](docs/infracost_example.gif) +![](https://raw.githubusercontent.com/StackGuardian/tirith/main/docs/infracost_example.gif) JSON Output: ```json @@ -1044,7 +1044,7 @@ Example Input: ``` Output: -![](docs/sg_workflow_example.gif) +![](https://raw.githubusercontent.com/StackGuardian/tirith/main/docs/sg_workflow_example.gif) JSON Output: @@ -1202,7 +1202,7 @@ Example Input ``` Output: -![](docs/json_example.gif) +![](https://raw.githubusercontent.com/StackGuardian/tirith/main/docs/json_example.gif) JSON Output ```json @@ -1482,7 +1482,7 @@ Final expression used: We are calling for contributors to help build out new features, review pull requests, fix bugs, and maintain overall code quality. Email us at team[at]stackguardian.io, or get started by reading -[contributing.md](./CONTRIBUTING.md). +[contributing.md](https://raw.githubusercontent.com/StackGuardian/tirith/main/CONTRIBUTING.md). ### Getting an issue assigned diff --git a/documentation/docs/getting-started-with-tirith.md b/documentation/docs/getting-started-with-tirith.md index f57a5de9..b9301f86 100644 --- a/documentation/docs/getting-started-with-tirith.md +++ b/documentation/docs/getting-started-with-tirith.md @@ -16,7 +16,7 @@ import TabItem from '@theme/TabItem'; Explore a failing evaluation down to the resource that caused it, assemble policies from a form, and experiment in a playground with worked examples. Install it with -`pip install 'py-tirith[tui] @ git+https://github.com/StackGuardian/tirith.git'` and run +`pip install 'tirith-iac-governance[tui]'` and run `tirith ui` — see [the interactive interface](tirith-usage/interactive-interface.md). diff --git a/documentation/docs/tirith-installation/quick-intallation.md b/documentation/docs/tirith-installation/quick-intallation.md index 25cec02d..b3d674c5 100644 --- a/documentation/docs/tirith-installation/quick-intallation.md +++ b/documentation/docs/tirith-installation/quick-intallation.md @@ -37,15 +37,14 @@ If you simply want to install and start using Tirith, this option provides a fas ## Prerequisite - Make sure your machine has [Python](https://www.python.org/downloads/) and [pip](https://pip.pypa.io/en/stable/installation/) installed. -- Install [Git](https://git-scm.com/downloads) on your machine. ## Steps to Install Tirith ### Step 1: Install using the `pip` command -Run the following command in your terminal to download Tirith directly from the GitHub repository and install it on your local system. This command ensures that you have the latest version. +Run the following command in your terminal to install Tirith from PyPI. This gives you the latest release. ```bash -pip install git+https://github.com/StackGuardian/tirith.git +pip install tirith-iac-governance ``` diff --git a/documentation/docs/tirith-usage/ci-integration.md b/documentation/docs/tirith-usage/ci-integration.md index 68cab83c..6ea13d5a 100644 --- a/documentation/docs/tirith-usage/ci-integration.md +++ b/documentation/docs/tirith-usage/ci-integration.md @@ -107,14 +107,13 @@ policy: image: python:3.12 needs: [plan] script: - - pip install "git+https://github.com/StackGuardian/tirith.git@1.0.5" + - pip install "tirith-iac-governance==1.3.0" - tirith -policy-path .tirith/policies -input-path plan.json --fail-on-error ``` -Tirith is **not on PyPI** — `pip install tirith` installs an unrelated project of the same name. -Install from git, and pin a tag rather than tracking the default branch so a CI job cannot change -behaviour underneath you. `1.0.5` is the newest tag; -`git ls-remote --tags https://github.com/StackGuardian/tirith.git` lists them. Python 3.8 or newer. +The distribution is **`tirith-iac-governance`**; the import and the command are both `tirith`. Pin the +version in CI so a release cannot change behaviour underneath you. Note that `pip install tirith` +installs an unrelated project of the same name. Python 3.8 or newer. To evaluate your organization's policies instead of the committed files, swap the last line for `tirith platform check` and supply credentials as CI variables: @@ -126,7 +125,7 @@ policy: variables: SG_ORG: my-org # SG_API_TOKEN comes from a masked CI/CD variable script: - - pip install "git+https://github.com/StackGuardian/tirith.git@1.0.5" + - pip install "tirith-iac-governance==1.3.0" - tirith platform check --workflow-id my-repo --input-path plan.json --fail-on-error ``` @@ -138,7 +137,7 @@ Nothing above is GitLab-specific: any runner that can execute a container and pr the same way. The recipe is always the same three steps — 1. produce the input document (`terraform show -json tfplan > plan.json`); -2. `pip install "git+https://github.com/StackGuardian/tirith.git@1.0.5"`; +2. `pip install "tirith-iac-governance==1.3.0"`; 3. `tirith -policy-path -input-path plan.json --fail-on-error` — and gate the job on the exit code, which every CI system does by default for a non-zero exit. diff --git a/documentation/docs/tirith-usage/interactive-interface.md b/documentation/docs/tirith-usage/interactive-interface.md index df9425b7..a1092cca 100644 --- a/documentation/docs/tirith-usage/interactive-interface.md +++ b/documentation/docs/tirith-usage/interactive-interface.md @@ -33,11 +33,11 @@ pipeline should pay to install an interface they never open. It needs Python 3.9 Tirith itself still supports 3.8. ```bash -pip install 'py-tirith[tui] @ git+https://github.com/StackGuardian/tirith.git' +pip install 'tirith-iac-governance[tui]' ``` -Tirith is not on PyPI — `pip install py-tirith` finds nothing and `pip install tirith` installs an -unrelated project of the same name — so the extra is requested against the git URL. +The interface is an extra rather than a dependency, so a CI gate does not pay to install a UI it +never opens. ## Opening it diff --git a/documentation/src/pages/index.js b/documentation/src/pages/index.js index 90b94fbc..424a5d08 100644 --- a/documentation/src/pages/index.js +++ b/documentation/src/pages/index.js @@ -27,7 +27,7 @@ const content = { body: 'Tirith reads the plan your pipeline already produces, checks it against your policies, and ' + 'exits non-zero so a violating change never reaches apply. Apache-2.0, and no account needed.', - install: 'pip install git+https://github.com/StackGuardian/tirith.git', + install: 'pip install tirith-iac-governance', announcement: { label: 'New', // No backticks: this is plain JSX text, not markdown, so they would render literally. diff --git a/pyproject.toml b/pyproject.toml index c18f6191..bb43833b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,10 @@ +# Without this table, pip falls back to legacy setup.py semantics and builds against whatever +# setuptools happens to be in the environment. Declaring it is what makes `python -m build` +# reproducible and PEP 517 compliant -- the package metadata itself still lives in setup.py. +[build-system] +requires = ["setuptools>=64", "wheel"] +build-backend = "setuptools.build_meta" + [tool.black] line-length = 120 target-version = ['py38'] @@ -6,4 +13,4 @@ target-version = ['py38'] markers = [ "failing: Evaluations should fail for these tests", "passing: Evaluations should pass for these tests", -] \ No newline at end of file +] diff --git a/setup.py b/setup.py index 6dc8be09..582cdf8b 100644 --- a/setup.py +++ b/setup.py @@ -5,11 +5,8 @@ import io import re -from glob import glob -from os.path import basename from os.path import dirname from os.path import join -from os.path import splitext from setuptools import find_packages from setuptools import setup @@ -20,10 +17,26 @@ def read(*names, **kwargs): return file_handle.read() +def read_version(): + """ + Single source of truth: `src/tirith/__init__.py`. + + Read rather than imported, because importing the package at build time would execute its + imports before its dependencies are installed. `tirith --version` reports this same value + (`cli.py` reads `tirith.__version__`), so a duplicate literal here is a version the CLI can + contradict -- which is exactly what a hardcoded copy invites, and nothing asserted otherwise. + """ + match = re.search(r'^__version__ = "([^"]+)"', read("src", "tirith", "__init__.py"), re.M) + if not match: + raise RuntimeError("could not find __version__ in src/tirith/__init__.py") + return match.group(1) + + setup( - name="py-tirith", - version="1.2.0", - license="Apache", + name="tirith-iac-governance", + version=read_version(), + license="Apache-2.0", + license_files=["LICENSE"], description="Tirith simplifies defining Policy as Code.", long_description_content_type="text/markdown", long_description="%s\n%s" @@ -36,7 +49,6 @@ def read(*names, **kwargs): url="https://github.com/stackguardian/tirith", packages=find_packages("src"), package_dir={"": "src"}, - py_modules=[splitext(basename(path))[0] for path in glob("src/*.py")], include_package_data=True, # Declared explicitly as well as in MANIFEST.in: MANIFEST governs the sdist, but a wheel # built straight from the tree takes its data files from here. Without this the TUI @@ -48,28 +60,26 @@ def read(*names, **kwargs): }, zip_safe=False, classifiers=[ - # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers - "Development Status :: 2 - Pre-Alpha", + # The matrix in .github/workflows/build_test.yml is the source of truth for what is + # actually supported; these must not claim less than it tests, which they did -- 3.10 + # through 3.12 were tested on every push and advertised nowhere. + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", + "Intended Audience :: System Administrators", "License :: OSI Approved :: Apache Software License", "Operating System :: Unix", "Operating System :: POSIX", - # 'Operating System :: Microsoft :: Windows', + "Operating System :: MacOS", "Programming Language :: Python", - # 'Programming Language :: Python :: 2.7', - # 'Programming Language :: Python :: 3', - # 'Programming Language :: Python :: 3.5', - # 'Programming Language :: Python :: 3.6', - # 'Programming Language :: Python :: 3.7', + "Programming Language :: Python :: 3", "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", "Programming Language :: Python :: Implementation :: CPython", - # 'Programming Language :: Python :: Implementation :: PyPy', - # uncomment if you test on these interpreters: - # 'Programming Language :: Python :: Implementation :: IronPython', - # 'Programming Language :: Python :: Implementation :: Jython', - # 'Programming Language :: Python :: Implementation :: Stackless', - "Topic :: System", + "Topic :: System :: Systems Administration", + "Topic :: Software Development :: Quality Assurance", ], project_urls={ "Changelog": "https://github.com/stackguardian/tirith/blob/main/CHANGELOG.md", @@ -79,7 +89,7 @@ def read(*names, **kwargs): python_requires=">=3.8", install_requires=["simplejson==3.17.2", "pydash==6.0.0", "PyYAML==6.0.1"], extras_require={ - # `pip install py-tirith[tui]` adds the interactive interface (`tirith ui`). + # `pip install tirith-iac-governance[tui]` adds the interactive interface (`tirith ui`). # # An extra rather than a dependency, for two reasons. The UI toolkit requires Python # >=3.9 while tirith supports >=3.8, so a hard dependency would drop 3.8 support for @@ -96,9 +106,6 @@ def read(*names, **kwargs): 'textual-serve>=1.0; python_version >= "3.9"', ], }, - setup_requires=[ - "pytest-runner", - ], entry_points={ "console_scripts": [ "tirith=tirith.__main__:main", diff --git a/src/tirith/__init__.py b/src/tirith/__init__.py index 4c2aac77..b3fd7cbc 100644 --- a/src/tirith/__init__.py +++ b/src/tirith/__init__.py @@ -2,6 +2,6 @@ tirith: Execute policies defined using Tirith (StackGuardian Policy Framework) """ -__version__ = "1.2.0" +__version__ = "1.3.0" __author__ = "StackGuardian" __license__ = "Apache" diff --git a/src/tirith/tui/__init__.py b/src/tirith/tui/__init__.py index bf4daaed..388b7910 100644 --- a/src/tirith/tui/__init__.py +++ b/src/tirith/tui/__init__.py @@ -7,13 +7,13 @@ * `import tirith.tui.schema` works without the extra installed -- which is what lets the schema drift-guard tests run on CI's Python 3.8 leg, where the toolkit cannot be installed at all (it requires >=3.9, tirith supports >=3.8); and - * a user who installed plain `py-tirith` gets an actionable message instead of an + * a user who installed plain `tirith-iac-governance` gets an actionable message instead of an ImportError traceback. """ TUI_EXTRA_HINT = ( "The Tirith interactive interface needs the optional 'tui' extra:\n" - " pip install 'py-tirith[tui] @ git+https://github.com/StackGuardian/tirith.git'\n" + " pip install 'tirith-iac-governance[tui]'\n" "It is optional so that using tirith as a CI gate stays dependency-light. It needs " "Python 3.9 or newer; tirith itself supports 3.8." ) diff --git a/src/tirith/tui/cli.py b/src/tirith/tui/cli.py index 78f2df06..597fc4fa 100644 --- a/src/tirith/tui/cli.py +++ b/src/tirith/tui/cli.py @@ -221,8 +221,7 @@ def _serve(opts, has_result): if not _is_missing_toolkit(e): raise print( - "Serving needs the optional 'tui' extra:\n" - " pip install 'py-tirith[tui] @ git+https://github.com/StackGuardian/tirith.git'", + "Serving needs the optional 'tui' extra:\n pip install 'tirith-iac-governance[tui]'", file=sys.stderr, ) return ExitStatus.ERROR diff --git a/src/tirith/tui/examples.py b/src/tirith/tui/examples.py index 75c30d40..212742bc 100644 --- a/src/tirith/tui/examples.py +++ b/src/tirith/tui/examples.py @@ -8,7 +8,7 @@ The examples live in `examples/` beside this module rather than being read out of `tests/`. Reusing the test fixtures was tempting since there are ~30 of them, but tests/ is not shipped in the wheel (MANIFEST.in packages `src/*.json`, and the test tree only reaches an sdist), so -an installed `pip install py-tirith[tui]` would have found an empty playground. They are also +an installed `pip install tirith-iac-governance[tui]` would have found an empty playground. They are also written to demonstrate the engine, not to teach it: several exist precisely because they are malformed, and `policy.json` uses a `&` the engine rejects outright. diff --git a/tests/tui/test_app.py b/tests/tui/test_app.py index c78bb731..b7d52511 100644 --- a/tests/tui/test_app.py +++ b/tests/tui/test_app.py @@ -18,7 +18,7 @@ from pytest import fixture, mark, importorskip -importorskip("textual", reason="the TUI is an optional extra (pip install 'py-tirith[tui]')") +importorskip("textual", reason="the TUI is an optional extra (pip install 'tirith-iac-governance[tui]')") def drives_the_app(test): diff --git a/tests/tui/test_ui_cli.py b/tests/tui/test_ui_cli.py index 9a02de6f..3acb29b2 100644 --- a/tests/tui/test_ui_cli.py +++ b/tests/tui/test_ui_cli.py @@ -147,7 +147,7 @@ def test_an_unreadable_file_is_reported_not_raised(capsys, monkeypatch): @mark.passing def test_the_missing_extra_is_reported_with_instructions(capsys, monkeypatch): """ - The expected failure for anyone who installed plain `py-tirith`: an instruction, not an + The expected failure for anyone who installed plain `tirith-iac-governance`: an instruction, not an ImportError traceback. """ diff --git a/tools/bump_version.py b/tools/bump_version.py index a8b33263..2b190b6f 100755 --- a/tools/bump_version.py +++ b/tools/bump_version.py @@ -27,17 +27,16 @@ class VersionBumper: def __init__(self, root_dir=None): # If no root_dir provided, use parent directory of the script (project root) self.root_dir = Path(root_dir) if root_dir else Path(__file__).parent.parent - self.setup_py = self.root_dir / "setup.py" self.init_py = self.root_dir / "src" / "tirith" / "__init__.py" self.changelog = self.root_dir / "CHANGELOG.md" def get_current_version(self): - """Extract current version from setup.py""" - content = self.setup_py.read_text() - match = re.search(r'version="([^"]+)"', content) + """Extract current version from src/tirith/__init__.py, the single source of truth.""" + content = self.init_py.read_text() + match = re.search(r'^__version__ = "([^"]+)"', content, re.M) if match: return match.group(1) - raise ValueError("Could not find version in setup.py") + raise ValueError("Could not find __version__ in src/tirith/__init__.py") def validate_version(self, version): """Validate version format (semantic versioning)""" @@ -46,13 +45,6 @@ def validate_version(self, version): raise ValueError(f"Invalid version format: {version}. Expected format: X.Y.Z or X.Y.Z-beta.N") return True - def update_setup_py(self, new_version): - """Update version in setup.py""" - content = self.setup_py.read_text() - updated = re.sub(r'version="[^"]+"', f'version="{new_version}"', content) - self.setup_py.write_text(updated) - print(f"✓ Updated {self.setup_py.relative_to(self.root_dir)}") - def update_init_py(self, new_version): """Update version in src/tirith/__init__.py""" content = self.init_py.read_text() @@ -94,7 +86,6 @@ def bump_version(self, new_version, change_type=None, description=None): print(f"New version: {new_version}\n") # Update all files - self.update_setup_py(new_version) self.update_init_py(new_version) self.update_changelog(new_version, change_type, description) @@ -102,7 +93,7 @@ def bump_version(self, new_version, change_type=None, description=None): print("\nNext steps:") print("1. Review the changes") print("2. Commit with: git add -A && git commit -m 'Bump version'") - print("3. Create a tag: git tag -a v{} -m 'Release v{}'".format(new_version, new_version)) + print("3. Create a tag: git tag -a {} -m 'Release {}'".format(new_version, new_version)) print("4. Push: git push && git push --tags")