From 931847c1048e5a2fc27bcd8a026a15725ddfb677 Mon Sep 17 00:00:00 2001 From: mariatta Date: Mon, 19 Oct 2026 14:42:53 -0700 Subject: [PATCH 1/2] Support for Cabotage deployments Add a Dockerfile so cabotage can build the image, bind the web process to the cabotage unix socket when /var/run/cabotage exists (falling back to PORT otherwise), and serve GET /health for the platform's probes. Replace the hard-coded herokuapp URL in the howto page with APP_URL, and replace the Heroku deploy button in the readme with cabotage notes. --- .dockerignore | 13 +++++++++++++ Dockerfile | 12 ++++++++++++ blurb_it/__main__.py | 18 ++++++++++++++---- readme.rst | 18 ++++++++++++++---- templates/howto.html | 2 +- tests/test_main.py | 11 +++++++++++ 6 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..47e1b81 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.git +.github +.venv +venv +ENV +.env +.idea +__pycache__ +*.pyc +.coverage +coverage.xml +htmlcov +tests diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..94359d2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.14-slim + +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY blurb_it blurb_it +COPY templates templates +COPY static static + +CMD ["python", "-m", "blurb_it"] diff --git a/blurb_it/__main__.py b/blurb_it/__main__.py index e36a06c..69d8a25 100644 --- a/blurb_it/__main__.py +++ b/blurb_it/__main__.py @@ -127,6 +127,11 @@ async def handle_add_blurb_get(request: Request) -> Response: return response +async def health_check(request: Request) -> Response: + """Health check endpoint for container orchestration.""" + return web.Response(status=200, text="OK") + + def get_access_token(token_str: str) -> str | None: for token in token_str.split("&"): token_split = token.split("=") @@ -237,9 +242,14 @@ async def handle_add_blurb_post(request: Request) -> Response: app, loader=jinja2.FileSystemLoader(os.path.join(os.getcwd(), "templates")) ) app["static_root_url"] = os.path.join(os.getcwd(), "static") - port = os.environ.get("PORT") - if port is not None: - port = int(port) app.router.add_routes(routes) + app.router.add_get("/health", health_check) app.add_routes([web.static("/static", os.path.join(os.getcwd(), "static"))]) - web.run_app(app, port=port) + + if os.path.isdir("/var/run/cabotage"): + # On cabotage the TLS sidecar proxies to this unix socket. + os.umask(0) + web.run_app(app, path="/var/run/cabotage/cabotage.sock") + else: + port = os.environ.get("PORT") + web.run_app(app, port=int(port) if port else None) diff --git a/readme.rst b/readme.rst index 6115d4a..7e3a8d0 100644 --- a/readme.rst +++ b/readme.rst @@ -44,10 +44,20 @@ Uninstall blurb-it Deploy ====== -|Deploy| - -.. |Deploy| image:: https://www.herokucdn.com/deploy/button.svg - :target: https://heroku.com/deploy?template=https://github.com/python/blurb_it +blurb-it is deployed to `cabotage `_, +the container platform managed by the PSF. cabotage builds the image from the +``Dockerfile`` and runs the ``web`` process from the ``Procfile``. The ``web`` +process listens on the unix socket ``/var/run/cabotage/cabotage.sock`` when +that directory exists, and on ``PORT`` otherwise. ``GET /health`` is the +health check path. + +Configuration variables, set in the cabotage UI: + +- ``GH_CLIENT_ID``, ``GH_CLIENT_SECRET``: the GitHub OAuth App credentials. +- ``GH_APP_ID``, ``GH_PRIVATE_KEY``: the GitHub App ID and private key. +- ``APP_URL``: the public hostname, without scheme. Must match the OAuth App + callback URL. +- ``SENTRY_DSN``: where errors are reported. Requirements and dependencies diff --git a/templates/howto.html b/templates/howto.html index 4312f2e..220859f 100644 --- a/templates/howto.html +++ b/templates/howto.html @@ -4,7 +4,7 @@

As part of CPython’s workflow, any non-trivial change to the codebase requires an accompanying Misc/News.d file in the pull request.

So far, the way you would add the news entry is by using blurb command line tool, that can be installed using PyPI.

-

But now, we’re going to defy the Zen of Python’s “there should be only one way to do it”, and present you with another way to do it: blurb-it. https://blurb-it.herokuapp.com/

+

But now, we’re going to defy the Zen of Python’s “there should be only one way to do it”, and present you with another way to do it: blurb-it. https://{{ app_url }}/

blurb-it provides a web interface for you to create news entry to your own CPython pull request. That means, you can do this from your phone, as long as you have a webbrowser and connected to the internet.

blurb-it is a GitHub App owned by The PSF. You’ll need to install it and give write permission to your own fork’s cpython repo (so it can create the news file). Since the app will have write access to the repo, you should not install this on any other repo.

Please give this a try and let me know of any issues by filing a bug in the blurb-it repo.

diff --git a/tests/test_main.py b/tests/test_main.py index 1d0712c..fd8978e 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -31,6 +31,17 @@ def create_app() -> web.Application: return app +async def test_health_check(aiohttp_client) -> None: + app = create_app() + app.router.add_get("/health", __main__.health_check) + client = await aiohttp_client(app) + + response = await client.get("/health") + + assert response.status == 200 + assert await response.text() == "OK" + + async def test_handle_get(aiohttp_client) -> None: # Arrange app = create_app() From e4a1df875b4d5df1dd863448f3e94276214e5835 Mon Sep 17 00:00:00 2001 From: mariatta Date: Mon, 19 Oct 2026 21:30:57 -0700 Subject: [PATCH 2/2] Register /health with the route table and test the howto URL Declare the health route with the module's RouteTableDef like the other handlers, use APP_PROTOCOL for the howto link so local http runs work, and assert the howto page renders APP_URL instead of the old Heroku host. --- blurb_it/__main__.py | 12 ++++++------ templates/howto.html | 2 +- tests/test_main.py | 6 +++++- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/blurb_it/__main__.py b/blurb_it/__main__.py index 69d8a25..47cf321 100644 --- a/blurb_it/__main__.py +++ b/blurb_it/__main__.py @@ -64,6 +64,12 @@ async def handle_install(request: Request) -> Response: return response +@routes.get("/health", name="health") +async def health_check(request: Request) -> Response: + """Health check endpoint for container orchestration.""" + return web.Response(status=200, text="OK") + + @routes.get("/add_blurb", name="add_blurb") async def handle_add_blurb_get(request: Request) -> Response: """Render a page with a textbox and submit button.""" @@ -127,11 +133,6 @@ async def handle_add_blurb_get(request: Request) -> Response: return response -async def health_check(request: Request) -> Response: - """Health check endpoint for container orchestration.""" - return web.Response(status=200, text="OK") - - def get_access_token(token_str: str) -> str | None: for token in token_str.split("&"): token_split = token.split("=") @@ -243,7 +244,6 @@ async def handle_add_blurb_post(request: Request) -> Response: ) app["static_root_url"] = os.path.join(os.getcwd(), "static") app.router.add_routes(routes) - app.router.add_get("/health", health_check) app.add_routes([web.static("/static", os.path.join(os.getcwd(), "static"))]) if os.path.isdir("/var/run/cabotage"): diff --git a/templates/howto.html b/templates/howto.html index 220859f..1da3622 100644 --- a/templates/howto.html +++ b/templates/howto.html @@ -4,7 +4,7 @@

As part of CPython’s workflow, any non-trivial change to the codebase requires an accompanying Misc/News.d file in the pull request.

So far, the way you would add the news entry is by using blurb command line tool, that can be installed using PyPI.

-

But now, we’re going to defy the Zen of Python’s “there should be only one way to do it”, and present you with another way to do it: blurb-it. https://{{ app_url }}/

+

But now, we’re going to defy the Zen of Python’s “there should be only one way to do it”, and present you with another way to do it: blurb-it. {{ app_protocol }}://{{ app_url }}/

blurb-it provides a web interface for you to create news entry to your own CPython pull request. That means, you can do this from your phone, as long as you have a webbrowser and connected to the internet.

blurb-it is a GitHub App owned by The PSF. You’ll need to install it and give write permission to your own fork’s cpython repo (so it can create the news file). Since the app will have write access to the repo, you should not install this on any other repo.

Please give this a try and let me know of any issues by filing a bug in the blurb-it repo.

diff --git a/tests/test_main.py b/tests/test_main.py index fd8978e..791b6fa 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -58,8 +58,10 @@ async def test_handle_get(aiohttp_client) -> None: assert "Sign in with GitHub" in text -async def test_handle_howto_get(aiohttp_client) -> None: +async def test_handle_howto_get(aiohttp_client, monkeypatch) -> None: # Arrange + monkeypatch.setenv("APP_URL", "blurb-it.example.org") + monkeypatch.delenv("APP_PROTOCOL", raising=False) app = create_app() # Act @@ -72,6 +74,8 @@ async def test_handle_howto_get(aiohttp_client) -> None: text = await resp.text() assert "📜🤖 Blurb It!" in text assert "As part of CPython’s workflow" in text + assert "https://blurb-it.example.org/" in text + assert "herokuapp" not in text async def test_handle_install(aiohttp_client) -> None: