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..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.""" @@ -237,9 +243,13 @@ 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.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..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://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. {{ 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 1d0712c..791b6fa 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() @@ -47,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 @@ -61,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: