Skip to content

Commit 1862614

Browse files
authored
docs: publish the README screenshot with the demo, and write down the release rule (#38)
1 parent efc925a commit 1862614

8 files changed

Lines changed: 88 additions & 25 deletions

File tree

‎.github/workflows/demo.yml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
name: Demo
22

33
# Publishes the demo page to GitHub Pages: the CLI collects the made-up scenario in
4-
# demo/scenario.py, so the page and its report.json match the released code.
4+
# demo/scenario.py, so the page and its report.json match the released code. The
5+
# screenshot.png it publishes is the image the README shows.
56
on:
67
push:
78
tags:
@@ -22,7 +23,8 @@ jobs:
2223
- run: uv python install 3.13
2324
- run: uv python pin 3.13
2425
- run: just install
25-
- run: just demo
26+
- run: uv run --no-sync playwright install --with-deps chromium
27+
- run: just demo-screenshot
2628
- uses: actions/upload-pages-artifact@v5
2729
with:
2830
path: site/

‎AGENTS.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Besides the translation checks in `tests/test_page.py`, it has browser smoke tes
2222
`smoke` and run by `just test-smoke`, not `just test`; still check a page change in a browser against a report from
2323
the CLI tests, or against `just demo-serve`. `demo/` holds the made-up scenario behind the published demo page: the
2424
CLI collects it against respx routes, and `.github/workflows/demo.yml` publishes it to GitHub Pages on each release
25-
tag. A page change that adds a state belongs in the scenario too. Tests
25+
tag, with the `screenshot.png` the README shows. A page change that adds a state belongs in the scenario too. Tests
2626
mock GitLab and Jira only with respx routes (pytest-httpx2): the `gitlab` and `jira` fixtures in `tests/conftest.py`
2727
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,
2828
or stubs.
@@ -39,6 +39,9 @@ description, and PyPI does not rewrite relative links, so a relative one 404s on
3939
[`skills/release-scope/SKILL.md`](skills/release-scope/SKILL.md) describes the CLI flags, exit codes, and report fields
4040
for agents, and pins the minor version it describes. Update it, and its version range, in the same change as any of those.
4141

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

4447
### Issue tracker

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ copy the release post.
2525
It writes a static page with a `report.json`, built to run on a schedule and publish with GitLab Pages.
2626
[Try the demo](https://modern-python.github.io/release-scope/): a made-up shop with four services and six Jira issues.
2727

28-
[![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/)
28+
[![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/)
2929

3030
## Quickstart
3131

‎demo/screenshot.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import pathlib
2+
import sys
3+
import typing
4+
5+
from playwright.sync_api import FloatRect, Page, ViewportSize, sync_playwright
6+
7+
from demo.server import serve
8+
9+
10+
_VIEWPORT: typing.Final = ViewportSize(width=1200, height=760)
11+
_CLIP: typing.Final = FloatRect(x=0, y=0, width=1200, height=668)
12+
13+
14+
def capture(page: Page, url: str, output: pathlib.Path) -> None:
15+
page.set_viewport_size(_VIEWPORT)
16+
page.goto(f"{url}#issues=SHOP-38")
17+
page.locator("details.service .chip:visible").first.wait_for()
18+
page.screenshot(path=output, clip=_CLIP)
19+
20+
21+
def main(site: pathlib.Path) -> None:
22+
with serve(site) as url, sync_playwright() as playwright:
23+
browser: typing.Final = playwright.chromium.launch()
24+
page: typing.Final = browser.new_page(device_scale_factor=2, color_scheme="light", locale="en-US")
25+
capture(page, url, site / "screenshot.png")
26+
browser.close()
27+
28+
29+
if __name__ == "__main__":
30+
main(pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "site"))

‎demo/server.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import collections.abc
2+
import contextlib
3+
import functools
4+
import http.server
5+
import pathlib
6+
import threading
7+
import typing
8+
9+
10+
class _QuietHandler(http.server.SimpleHTTPRequestHandler):
11+
def log_message(self, format: str, *args: typing.Any) -> None: # noqa: A002, ANN401
12+
pass
13+
14+
15+
@contextlib.contextmanager
16+
def serve(directory: pathlib.Path) -> collections.abc.Generator[str, None, None]:
17+
server: typing.Final = http.server.ThreadingHTTPServer(
18+
("127.0.0.1", 0), functools.partial(_QuietHandler, directory=str(directory))
19+
)
20+
thread: typing.Final = threading.Thread(target=server.serve_forever, daemon=True)
21+
thread.start()
22+
try:
23+
yield f"http://127.0.0.1:{server.server_port}/"
24+
finally:
25+
server.shutdown()
26+
server.server_close()

‎docs/images/demo.png‎

-326 KB
Binary file not shown.

‎justfile‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ test-smoke *args:
4747
demo:
4848
uv run --no-sync python -m demo.build site
4949

50+
# Build the demo and save site/screenshot.png, the image the README shows; installs Chromium on first run.
51+
demo-screenshot: demo
52+
uv run --no-sync playwright install chromium
53+
uv run --no-sync python -m demo.screenshot site
54+
5055
# Build the demo and serve it at http://127.0.0.1:8000.
5156
demo-serve: demo
5257
uv run --no-sync python -m http.server 8000 --bind 127.0.0.1 --directory site

‎tests/test_browser.py‎

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import collections.abc
2-
import functools
3-
import http.server
42
import pathlib
53
import re
6-
import threading
74
import typing
85

96
import pytest
@@ -12,6 +9,8 @@
129
from typer.testing import CliRunner
1310

1411
from demo.build import build
12+
from demo.screenshot import capture
13+
from demo.server import serve
1514
from release_scope import ioc
1615
from release_scope.__main__ import MAIN_APP
1716
from tests.payloads import ENDPOINT, JIRA_ENDPOINT
@@ -20,11 +19,6 @@
2019
pytestmark = pytest.mark.smoke
2120

2221

23-
class _QuietHandler(http.server.SimpleHTTPRequestHandler):
24-
def log_message(self, format: str, *args: typing.Any) -> None: # noqa: A002, ANN401
25-
pass
26-
27-
2822
@pytest.fixture
2923
def site(jira: respx.Router, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> collections.abc.Iterator[str]:
3024
monkeypatch.setenv("GITLAB_TOKEN", "glpat-test")
@@ -37,25 +31,16 @@ def site(jira: respx.Router, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.
3731
result = CliRunner().invoke(MAIN_APP, ["collect", "-g", "team", "-o", str(output)])
3832
assert result.exit_code == 0, result.output
3933
assert jira["jira_search"].called
40-
yield from _serve(output)
34+
with serve(output) as url:
35+
yield url
4136

4237

4338
@pytest.fixture
4439
def demo_site(tmp_path: pathlib.Path) -> collections.abc.Iterator[str]:
4540
output: typing.Final = tmp_path / "demo"
4641
build(output)
47-
yield from _serve(output)
48-
49-
50-
def _serve(directory: pathlib.Path) -> collections.abc.Iterator[str]:
51-
server: typing.Final = http.server.ThreadingHTTPServer(
52-
("127.0.0.1", 0), functools.partial(_QuietHandler, directory=str(directory))
53-
)
54-
thread: typing.Final = threading.Thread(target=server.serve_forever, daemon=True)
55-
thread.start()
56-
yield f"http://127.0.0.1:{server.server_port}/"
57-
server.shutdown()
58-
server.server_close()
42+
with serve(output) as url:
43+
yield url
5944

6045

6146
@pytest.fixture
@@ -134,3 +119,15 @@ def test_demo_lists_its_services_and_picks_tags_for_an_issue(page: Page, demo_si
134119
_issue(page, "SHOP-38").check()
135120
expect(page).to_have_url(re.compile(r"#issues=SHOP-38&shop/api=2\.5\.0&shop/web=1\.9\.0$"))
136121
assert errors == []
122+
123+
124+
def test_demo_screenshot_shows_an_issue_with_its_picks(
125+
page: Page, demo_site: str, errors: list[str], tmp_path: pathlib.Path
126+
) -> None:
127+
output: typing.Final = tmp_path / "screenshot.png"
128+
129+
capture(page, demo_site, output)
130+
131+
assert output.read_bytes().startswith(b"\x89PNG")
132+
expect(page.locator("details.service .chip:visible > span")).to_have_text(["2.5.0", "1.9.0"])
133+
assert errors == []

0 commit comments

Comments
 (0)