From 770ce9976a055eb74345bfcbcb6c8c392421629e Mon Sep 17 00:00:00 2001 From: Jason O'Neal Date: Sun, 30 Aug 2026 21:19:41 -0400 Subject: [PATCH 1/2] release: prepare GhostMCP v0.2.1 --- .github/workflows/release.yml | 41 ++++++++++++++++++++++++++-------- CHANGELOG.md | 4 +++- README.md | 2 +- SECURITY.md | 2 +- docs/README.md | 2 +- ghostmcp/__init__.py | 2 +- ghostmcp/dashboard.py | 2 +- pyproject.toml | 2 +- tests/test_release_metadata.py | 38 +++++++++++++++++++++++++++++++ 9 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 tests/test_release_metadata.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cbde082..063b35d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,9 +15,6 @@ jobs: attestations: write runs-on: ubuntu-latest - env: - PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }} - steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -54,6 +51,16 @@ jobs: dist/*.tar.gz dist/sbom.spdx.json + - name: Upload Python distributions for optional PyPI publication + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: python-distributions-${{ github.ref_name }} + path: | + dist/*.whl + dist/*.tar.gz + if-no-files-found: error + retention-days: 30 + - name: Publish GitHub Release uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 with: @@ -62,9 +69,25 @@ jobs: dist/*.tar.gz dist/sbom.spdx.json - - name: Publish to PyPI - if: ${{ env.PYPI_API_TOKEN != '' }} - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ env.PYPI_API_TOKEN }} - run: twine upload dist/*.whl dist/*.tar.gz + + publish-pypi: + if: github.repository == 'BlueDot-IT/GhostMCP' && vars.PYPI_PUBLISH_ENABLED == 'true' + needs: release + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/ghostmcp-server + permissions: + id-token: write + steps: + - name: Download Python distributions + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: python-distributions-${{ github.ref_name }} + path: dist + + - name: Publish with PyPI Trusted Publishing + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 + with: + packages-dir: dist/ + print-hash: true diff --git a/CHANGELOG.md b/CHANGELOG.md index a11a4fb..f8fc5f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,12 @@ # Changelog -## Unreleased +## 0.2.1 — 2026-08-30 - Required TLS 1.2 or newer unconditionally for certificate inspection. - Corrected repository badges and vulnerability-reporting links after the BlueDot migration. - Made CI and release dependency installation consume committed hash-locked requirements. +- Updated vulnerable dependency floors and audited lock files. +- Replaced token-based PyPI upload with an explicitly gated OIDC Trusted Publisher job. ## 0.2.0 — 2026-07-25 diff --git a/README.md b/README.md index b150af9..373af0d 100644 --- a/README.md +++ b/README.md @@ -250,7 +250,7 @@ CI validates Python 3.11 and 3.12, dependency locks, linting, typing, Bandit, de ## Release status -The package version is `0.2.0`. The runtime is beta-quality: policy enforcement is fail-closed, but operators must still layer network egress controls, least privilege, protected secrets, and written authorization around it. +The package version is `0.2.1`. The runtime is beta-quality: policy enforcement is fail-closed, but operators must still layer network egress controls, least privilege, protected secrets, and written authorization around it. ## License diff --git a/SECURITY.md b/SECURITY.md index 6714dd6..bc5db11 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ GhostMCP is currently a beta project. Security updates are provided for the latest `main` branch. Older commits, unmerged branches, locally modified copies, and prerelease artifacts may not receive fixes. -The current package version is `0.2.0`. Review configuration and deployment changes before upgrading between beta revisions. +The current package version is `0.2.1`. Review configuration and deployment changes before upgrading between beta revisions. ## Reporting a vulnerability diff --git a/docs/README.md b/docs/README.md index 3749e65..837261e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # GhostMCP documentation -GhostMCP is a beta security automation server for authorized assessment workflows. These documents describe the hardened runtime in version `0.2.0`. +GhostMCP is a beta security automation server for authorized assessment workflows. These documents describe the hardened runtime in version `0.2.1`. ## Start here diff --git a/ghostmcp/__init__.py b/ghostmcp/__init__.py index d3ec452..3ced358 100644 --- a/ghostmcp/__init__.py +++ b/ghostmcp/__init__.py @@ -1 +1 @@ -__version__ = "0.2.0" +__version__ = "0.2.1" diff --git a/ghostmcp/dashboard.py b/ghostmcp/dashboard.py index 65adcf3..19e9a9d 100644 --- a/ghostmcp/dashboard.py +++ b/ghostmcp/dashboard.py @@ -42,7 +42,7 @@ async def lifespan(_app: FastAPI): _worker = None -app = FastAPI(title="GhostMCP Dashboard", version="0.2.0", lifespan=lifespan) +app = FastAPI(title="GhostMCP Dashboard", version="0.2.1", lifespan=lifespan) # Templates template_dir = Path(__file__).parent / "templates" diff --git a/pyproject.toml b/pyproject.toml index ec8af0c..5d35b19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "ghostmcp-server" -version = "0.2.0" +version = "0.2.1" description = "Security-focused MCP server for authorized assessment workflows" readme = "README.md" requires-python = ">=3.11" diff --git a/tests/test_release_metadata.py b/tests/test_release_metadata.py new file mode 100644 index 0000000..3c7e8a0 --- /dev/null +++ b/tests/test_release_metadata.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import tomllib +from pathlib import Path + +import ghostmcp +from ghostmcp.dashboard import app + +ROOT = Path(__file__).resolve().parents[1] +EXPECTED_VERSION = "0.2.1" + + +def test_release_identity_is_consistent() -> None: + project = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))[ + "project" + ] + changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") + readme = (ROOT / "README.md").read_text(encoding="utf-8") + security = (ROOT / "SECURITY.md").read_text(encoding="utf-8") + + assert project["name"] == "ghostmcp-server" + assert project["version"] == EXPECTED_VERSION + assert ghostmcp.__version__ == EXPECTED_VERSION + assert app.version == EXPECTED_VERSION + assert f"## {EXPECTED_VERSION} — " in changelog + assert f"package version is `{EXPECTED_VERSION}`" in readme + assert f"package version is `{EXPECTED_VERSION}`" in security + + +def test_release_uses_explicitly_gated_trusted_publishing() -> None: + workflow = (ROOT / ".github/workflows/release.yml").read_text(encoding="utf-8") + + assert "vars.PYPI_PUBLISH_ENABLED == 'true'" in workflow + assert "environment:\n name: pypi" in workflow + assert "permissions:\n id-token: write" in workflow + assert "pypa/gh-action-pypi-publish@dc37677" in workflow + assert "PYPI_API_TOKEN" not in workflow + assert "TWINE_PASSWORD" not in workflow From e10c3d68e6791bde930a15bba4a53a9852da9662 Mon Sep 17 00:00:00 2001 From: Jason O'Neal Date: Sun, 30 Aug 2026 21:30:08 -0400 Subject: [PATCH 2/2] ci: bind releases to protected main --- .github/workflows/release.yml | 7 ++++ tests/test_release_metadata.py | 74 ++++++++++++++++++++++------------ 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 063b35d..f676e3d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,6 +19,7 @@ jobs: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + fetch-depth: 0 show-progress: false - name: Set up Python @@ -27,6 +28,12 @@ jobs: python-version: "3.12" cache: pip + - name: Verify release identity and protected-main ancestry + run: | + PACKAGE_VERSION="$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')" + test "v${PACKAGE_VERSION}" = "${GITHUB_REF_NAME}" + git merge-base --is-ancestor "${GITHUB_SHA}" refs/remotes/origin/main + - name: Install release tooling run: python -m pip install --require-hashes -r requirements-release.lock.txt diff --git a/tests/test_release_metadata.py b/tests/test_release_metadata.py index 3c7e8a0..3caa81a 100644 --- a/tests/test_release_metadata.py +++ b/tests/test_release_metadata.py @@ -1,6 +1,7 @@ from __future__ import annotations import tomllib +import unittest from pathlib import Path import ghostmcp @@ -10,29 +11,50 @@ EXPECTED_VERSION = "0.2.1" -def test_release_identity_is_consistent() -> None: - project = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))[ - "project" - ] - changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") - readme = (ROOT / "README.md").read_text(encoding="utf-8") - security = (ROOT / "SECURITY.md").read_text(encoding="utf-8") - - assert project["name"] == "ghostmcp-server" - assert project["version"] == EXPECTED_VERSION - assert ghostmcp.__version__ == EXPECTED_VERSION - assert app.version == EXPECTED_VERSION - assert f"## {EXPECTED_VERSION} — " in changelog - assert f"package version is `{EXPECTED_VERSION}`" in readme - assert f"package version is `{EXPECTED_VERSION}`" in security - - -def test_release_uses_explicitly_gated_trusted_publishing() -> None: - workflow = (ROOT / ".github/workflows/release.yml").read_text(encoding="utf-8") - - assert "vars.PYPI_PUBLISH_ENABLED == 'true'" in workflow - assert "environment:\n name: pypi" in workflow - assert "permissions:\n id-token: write" in workflow - assert "pypa/gh-action-pypi-publish@dc37677" in workflow - assert "PYPI_API_TOKEN" not in workflow - assert "TWINE_PASSWORD" not in workflow +class ReleaseMetadataTests(unittest.TestCase): + def test_release_identity_is_consistent(self) -> None: + project = tomllib.loads( + (ROOT / "pyproject.toml").read_text(encoding="utf-8") + )["project"] + changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") + readme = (ROOT / "README.md").read_text(encoding="utf-8") + security = (ROOT / "SECURITY.md").read_text(encoding="utf-8") + + self.assertEqual(project["name"], "ghostmcp-server") + self.assertEqual(project["version"], EXPECTED_VERSION) + self.assertEqual(ghostmcp.__version__, EXPECTED_VERSION) + self.assertEqual(app.version, EXPECTED_VERSION) + self.assertIn(f"## {EXPECTED_VERSION} — ", changelog) + self.assertIn(f"package version is `{EXPECTED_VERSION}`", readme) + self.assertIn(f"package version is `{EXPECTED_VERSION}`", security) + + def test_release_uses_explicitly_gated_trusted_publishing(self) -> None: + workflow = (ROOT / ".github/workflows/release.yml").read_text( + encoding="utf-8" + ) + release_job, publish_job = workflow.split( + "\n publish-pypi:\n", maxsplit=1 + ) + + self.assertIn("fetch-depth: 0", release_job) + self.assertIn( + 'test "v${PACKAGE_VERSION}" = "${GITHUB_REF_NAME}"', + release_job, + ) + self.assertIn( + 'git merge-base --is-ancestor "${GITHUB_SHA}" ' + "refs/remotes/origin/main", + release_job, + ) + self.assertIn("vars.PYPI_PUBLISH_ENABLED == 'true'", publish_job) + self.assertIn("environment:\n name: pypi", publish_job) + self.assertIn("permissions:\n id-token: write", publish_job) + self.assertIn( + "pypa/gh-action-pypi-publish@dc37677", publish_job + ) + self.assertNotIn("PYPI_API_TOKEN", workflow) + self.assertNotIn("TWINE_PASSWORD", workflow) + + +if __name__ == "__main__": + unittest.main()