diff --git a/README.md b/README.md index 855027e..9ceef86 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,34 @@ Notes: - `GUNICORN_WORKERS` (default: 2) - `GUNICORN_BIND` (default: `0.0.0.0:8000`) +## Release helper (git + docker publish) + +This repo includes a small helper to: + +1) ask for a **commit message** and **version** +2) commit + push to the `openslide` git remote +3) build + push Docker images: + - `git.alphen.cloud/bramval/openslide:` + - `git.alphen.cloud/bramval/openslide:latest` + +Run (interactive): + +```bash +python scripts/release.py +``` + +Run (non-interactive): + +```bash +python scripts/release.py --version 1.2.3 --message "Release 1.2.3" +``` + +Dry-run (prints commands only): + +```bash +python scripts/release.py --version 1.2.3 --message "Release 1.2.3" --dry-run +``` + ## Notes - SQLite DB is stored at `instance/signage.sqlite`. @@ -232,5 +260,6 @@ If the reset email is not received: + diff --git a/app/templates/company/my_company.html b/app/templates/company/my_company.html index e33373d..4e32bc6 100644 --- a/app/templates/company/my_company.html +++ b/app/templates/company/my_company.html @@ -90,51 +90,7 @@ -
-
-

Overlay

-
-
-
- Upload a 16:9 PNG overlay. It will be rendered on top of the display content. - Transparent areas will show the content underneath. -
- - {% if overlay_url %} -
-
Current overlay:
-
- Company overlay -
-
- {% else %} -
No overlay uploaded.
- {% endif %} - -
-
- - -
Tip: export at 1920×1080 (or any 16:9 size).
-
-
- -
-
- - {% if overlay_url %} -
- -
- {% endif %} -
-
- -
+

Users

@@ -181,5 +137,51 @@
+ + +
+ +
+
+

Overlay

+
+
+
+ Upload a 16:9 PNG overlay. It will be rendered on top of the display content. + Transparent areas will show the content underneath. +
+ + {% if overlay_url %} +
+
Current overlay:
+
+ Company overlay +
+
+ {% else %} +
No overlay uploaded.
+ {% endif %} + +
+
+ + +
Tip: export at 1920×1080 (or any 16:9 size).
+
+
+ +
+
+ + {% if overlay_url %} +
+ +
+ {% endif %} +
{% endblock %} diff --git a/scripts/release.py b/scripts/release.py new file mode 100644 index 0000000..c476c85 --- /dev/null +++ b/scripts/release.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""Release helper. + +What it does (in order): +1) Ask/provide a commit message and version. +2) Commit & push to the `openslide` git remote. +3) Build + push Docker image tags: + - git.alphen.cloud/bramval/openslide: + - git.alphen.cloud/bramval/openslide:latest + +Usage examples: + python scripts/release.py --version 1.2.3 --message "Release 1.2.3" + python scripts/release.py # interactive prompts + +Notes: +- Assumes you are already authenticated for git + the Docker registry (docker login). +""" + +from __future__ import annotations + +import argparse +import re +import subprocess +import sys +from dataclasses import dataclass +from typing import Iterable, Sequence + + +DEFAULT_GIT_REMOTE = "openslide" +DEFAULT_IMAGE = "git.alphen.cloud/bramval/openslide" + + +@dataclass(frozen=True) +class ReleaseInfo: + version: str + message: str + + +_DOCKER_TAG_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$") + + +def _run(cmd: Sequence[str], *, dry_run: bool = False) -> None: + printable = " ".join(cmd) + print(f"> {printable}") + if dry_run: + return + subprocess.run(cmd, check=True) + + +def _capture(cmd: Sequence[str]) -> str: + return subprocess.check_output(cmd, text=True).strip() + + +def _require_tool(name: str) -> None: + try: + subprocess.run([name, "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False) + except FileNotFoundError as e: + raise SystemExit(f"Required tool not found in PATH: {name}") from e + + +def _validate_version(tag: str) -> str: + tag = tag.strip() + if not tag: + raise ValueError("Version may not be empty") + if not _DOCKER_TAG_RE.match(tag): + raise ValueError( + "Invalid docker tag for version. Use only letters, digits, '.', '_' or '-'. " + "(1..128 chars, must start with [A-Za-z0-9])" + ) + return tag + + +def get_release_info(*, version: str | None, message: str | None) -> ReleaseInfo: + """Collect commit message + version before doing the rest.""" + if version is None: + version = input("Version tag (e.g. 1.2.3): ").strip() + version = _validate_version(version) + + if message is None: + message = input(f"Commit message [Release {version}]: ").strip() or f"Release {version}" + + return ReleaseInfo(version=version, message=message) + + +def git_commit_and_push(*, remote: str, message: str, dry_run: bool = False) -> None: + # Stage all changes + _run(["git", "add", "-A"], dry_run=dry_run) + + # Only commit if there is something to commit + porcelain = _capture(["git", "status", "--porcelain"]) # empty => clean + if porcelain: + _run(["git", "commit", "-m", message], dry_run=dry_run) + else: + print("No working tree changes detected; skipping git commit.") + + branch = _capture(["git", "rev-parse", "--abbrev-ref", "HEAD"]) + _run(["git", "push", remote, branch], dry_run=dry_run) + + +def docker_build_and_push(*, image: str, version: str, dry_run: bool = False) -> None: + version_tag = f"{image}:{version}" + latest_tag = f"{image}:latest" + + _run(["docker", "build", "-t", version_tag, "-t", latest_tag, "."], dry_run=dry_run) + _run(["docker", "push", version_tag], dry_run=dry_run) + _run(["docker", "push", latest_tag], dry_run=dry_run) + + +def main(argv: Iterable[str]) -> int: + parser = argparse.ArgumentParser(description="Commit + push + docker publish helper.") + parser.add_argument("--version", "-v", help="Docker version tag (e.g. 1.2.3)") + parser.add_argument("--message", "-m", help="Git commit message") + parser.add_argument("--remote", default=DEFAULT_GIT_REMOTE, help=f"Git remote to push to (default: {DEFAULT_GIT_REMOTE})") + parser.add_argument("--image", default=DEFAULT_IMAGE, help=f"Docker image name (default: {DEFAULT_IMAGE})") + parser.add_argument("--dry-run", action="store_true", help="Print commands without executing") + args = parser.parse_args(list(argv)) + + _require_tool("git") + _require_tool("docker") + + info = get_release_info(version=args.version, message=args.message) + + print(f"\nReleasing version: {info.version}") + print(f"Commit message: {info.message}") + print(f"Git remote: {args.remote}") + print(f"Docker image: {args.image}\n") + + git_commit_and_push(remote=args.remote, message=info.message, dry_run=args.dry_run) + docker_build_and_push(image=args.image, version=info.version, dry_run=args.dry_run) + + print("\nDone.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:]))