Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/demo.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
name: Demo

# Publishes the demo page to GitHub Pages: the CLI collects the made-up scenario in
# demo/scenario.py, so the page and its report.json match the released code.
# demo/scenario.py, so the page and its report.json match the released code. The
# screenshot.png it publishes is the image the README shows.
on:
push:
tags:
Expand All @@ -22,7 +23,8 @@ jobs:
- run: uv python install 3.13
- run: uv python pin 3.13
- run: just install
- run: just demo
- run: uv run --no-sync playwright install --with-deps chromium
- run: just demo-screenshot
- uses: actions/upload-pages-artifact@v5
with:
path: site/
Expand Down
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Besides the translation checks in `tests/test_page.py`, it has browser smoke tes
`smoke` and run by `just test-smoke`, not `just test`; still check a page change in a browser against a report from
the CLI tests, or against `just demo-serve`. `demo/` holds the made-up scenario behind the published demo page: the
CLI collects it against respx routes, and `.github/workflows/demo.yml` publishes it to GitHub Pages on each release
tag. A page change that adds a state belongs in the scenario too. Tests
tag, with the `screenshot.png` the README shows. A page change that adds a state belongs in the scenario too. Tests
mock GitLab and Jira only with respx routes (pytest-httpx2): the `gitlab` and `jira` fixtures in `tests/conftest.py`
declare one named static route per call of the scenario in `tests/payloads.py`. A test changes a response by re-mocking a named route; add no fakes, callbacks,
or stubs.
Expand All @@ -39,6 +39,9 @@ description, and PyPI does not rewrite relative links, so a relative one 404s on
[`skills/release-scope/SKILL.md`](skills/release-scope/SKILL.md) describes the CLI flags, exit codes, and report fields
for agents, and pins the minor version it describes. Update it, and its version range, in the same change as any of those.

A change to the report schema bumps `SCHEMA_VERSION` and ships in a new minor release; anything else is a patch.
Move the skill's version range and the README's `release-scope>=X.Y,<X.Z` pins to that minor in the same change.

## Agent skills

### Issue tracker
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ copy the release post.
It writes a static page with a `report.json`, built to run on a schedule and publish with GitLab Pages.
[Try the demo](https://modern-python.github.io/release-scope/): a made-up shop with four services and six Jira issues.

[![The release-scope page with one Jira issue chosen and the tags it needs picked in two services](https://raw.githubusercontent.com/modern-python/release-scope/main/docs/images/demo.png)](https://modern-python.github.io/release-scope/)
[![The release-scope page with one Jira issue chosen and the tags it needs picked in two services](https://modern-python.github.io/release-scope/screenshot.png)](https://modern-python.github.io/release-scope/)

## Quickstart

Expand Down
30 changes: 30 additions & 0 deletions demo/screenshot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import pathlib
import sys
import typing

from playwright.sync_api import FloatRect, Page, ViewportSize, sync_playwright

from demo.server import serve


_VIEWPORT: typing.Final = ViewportSize(width=1200, height=760)
_CLIP: typing.Final = FloatRect(x=0, y=0, width=1200, height=668)


def capture(page: Page, url: str, output: pathlib.Path) -> None:
page.set_viewport_size(_VIEWPORT)
page.goto(f"{url}#issues=SHOP-38")
page.locator("details.service .chip:visible").first.wait_for()
page.screenshot(path=output, clip=_CLIP)


def main(site: pathlib.Path) -> None:
with serve(site) as url, sync_playwright() as playwright:
browser: typing.Final = playwright.chromium.launch()
page: typing.Final = browser.new_page(device_scale_factor=2, color_scheme="light", locale="en-US")
capture(page, url, site / "screenshot.png")
browser.close()


if __name__ == "__main__":
main(pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "site"))
26 changes: 26 additions & 0 deletions demo/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import collections.abc
import contextlib
import functools
import http.server
import pathlib
import threading
import typing


class _QuietHandler(http.server.SimpleHTTPRequestHandler):
def log_message(self, format: str, *args: typing.Any) -> None: # noqa: A002, ANN401
pass


@contextlib.contextmanager
def serve(directory: pathlib.Path) -> collections.abc.Generator[str, None, None]:
server: typing.Final = http.server.ThreadingHTTPServer(
("127.0.0.1", 0), functools.partial(_QuietHandler, directory=str(directory))
)
thread: typing.Final = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
yield f"http://127.0.0.1:{server.server_port}/"
finally:
server.shutdown()
server.server_close()
Binary file removed docs/images/demo.png
Binary file not shown.
5 changes: 5 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ test-smoke *args:
demo:
uv run --no-sync python -m demo.build site

# Build the demo and save site/screenshot.png, the image the README shows; installs Chromium on first run.
demo-screenshot: demo
uv run --no-sync playwright install chromium
uv run --no-sync python -m demo.screenshot site

# Build the demo and serve it at http://127.0.0.1:8000.
demo-serve: demo
uv run --no-sync python -m http.server 8000 --bind 127.0.0.1 --directory site
Expand Down
39 changes: 18 additions & 21 deletions tests/test_browser.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import collections.abc
import functools
import http.server
import pathlib
import re
import threading
import typing

import pytest
Expand All @@ -12,6 +9,8 @@
from typer.testing import CliRunner

from demo.build import build
from demo.screenshot import capture
from demo.server import serve
from release_scope import ioc
from release_scope.__main__ import MAIN_APP
from tests.payloads import ENDPOINT, JIRA_ENDPOINT
Expand All @@ -20,11 +19,6 @@
pytestmark = pytest.mark.smoke


class _QuietHandler(http.server.SimpleHTTPRequestHandler):
def log_message(self, format: str, *args: typing.Any) -> None: # noqa: A002, ANN401
pass


@pytest.fixture
def site(jira: respx.Router, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> collections.abc.Iterator[str]:
monkeypatch.setenv("GITLAB_TOKEN", "glpat-test")
Expand All @@ -37,25 +31,16 @@ def site(jira: respx.Router, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.
result = CliRunner().invoke(MAIN_APP, ["collect", "-g", "team", "-o", str(output)])
assert result.exit_code == 0, result.output
assert jira["jira_search"].called
yield from _serve(output)
with serve(output) as url:
yield url


@pytest.fixture
def demo_site(tmp_path: pathlib.Path) -> collections.abc.Iterator[str]:
output: typing.Final = tmp_path / "demo"
build(output)
yield from _serve(output)


def _serve(directory: pathlib.Path) -> collections.abc.Iterator[str]:
server: typing.Final = http.server.ThreadingHTTPServer(
("127.0.0.1", 0), functools.partial(_QuietHandler, directory=str(directory))
)
thread: typing.Final = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
yield f"http://127.0.0.1:{server.server_port}/"
server.shutdown()
server.server_close()
with serve(output) as url:
yield url


@pytest.fixture
Expand Down Expand Up @@ -134,3 +119,15 @@ def test_demo_lists_its_services_and_picks_tags_for_an_issue(page: Page, demo_si
_issue(page, "SHOP-38").check()
expect(page).to_have_url(re.compile(r"#issues=SHOP-38&shop/api=2\.5\.0&shop/web=1\.9\.0$"))
assert errors == []


def test_demo_screenshot_shows_an_issue_with_its_picks(
page: Page, demo_site: str, errors: list[str], tmp_path: pathlib.Path
) -> None:
output: typing.Final = tmp_path / "screenshot.png"

capture(page, demo_site, output)

assert output.read_bytes().startswith(b"\x89PNG")
expect(page.locator("details.service .chip:visible > span")).to_have_text(["2.5.0", "1.9.0"])
assert errors == []
Loading