Release 1.5
This commit is contained in:
29
README.md
29
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:<version>`
|
||||
- `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:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -90,51 +90,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-elevated mt-4">
|
||||
<div class="card-header">
|
||||
<h2 class="h5 mb-0">Overlay</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-muted small mb-3">
|
||||
Upload a <strong>16:9 PNG</strong> overlay. It will be rendered on top of the display content.
|
||||
Transparent areas will show the content underneath.
|
||||
</div>
|
||||
|
||||
{% if overlay_url %}
|
||||
<div class="mb-3">
|
||||
<div class="text-muted small mb-2">Current overlay:</div>
|
||||
<div style="max-width: 520px; border: 1px solid rgba(0,0,0,0.15); border-radius: 8px; overflow: hidden;">
|
||||
<img
|
||||
src="{{ overlay_url }}"
|
||||
alt="Company overlay"
|
||||
style="display:block; width:100%; height:auto; background: repeating-linear-gradient(45deg, #eee 0 12px, #fff 12px 24px);"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-muted mb-3">No overlay uploaded.</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{{ url_for('company.upload_company_overlay') }}" enctype="multipart/form-data" class="d-flex gap-2 flex-wrap align-items-end">
|
||||
<div>
|
||||
<label class="form-label">Upload overlay (PNG)</label>
|
||||
<input class="form-control" type="file" name="overlay" accept="image/png" required />
|
||||
<div class="form-text">Tip: export at 1920×1080 (or any 16:9 size).</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-brand" type="submit">Save overlay</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if overlay_url %}
|
||||
<form method="post" action="{{ url_for('company.delete_company_overlay') }}" class="mt-3" onsubmit="return confirm('Remove the overlay?');">
|
||||
<button class="btn btn-outline-danger" type="submit">Remove overlay</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-elevated mt-4">
|
||||
<div class="card card-elevated mt-4">
|
||||
<div class="card-header">
|
||||
<h2 class="h5 mb-0">Users</h2>
|
||||
</div>
|
||||
@@ -181,5 +137,51 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="card card-elevated mt-4">
|
||||
<div class="card-header">
|
||||
<h2 class="h5 mb-0">Overlay</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-muted small mb-3">
|
||||
Upload a <strong>16:9 PNG</strong> overlay. It will be rendered on top of the display content.
|
||||
Transparent areas will show the content underneath.
|
||||
</div>
|
||||
|
||||
{% if overlay_url %}
|
||||
<div class="mb-3">
|
||||
<div class="text-muted small mb-2">Current overlay:</div>
|
||||
<div style="max-width: 520px; border: 1px solid rgba(0,0,0,0.15); border-radius: 8px; overflow: hidden;">
|
||||
<img
|
||||
src="{{ overlay_url }}"
|
||||
alt="Company overlay"
|
||||
style="display:block; width:100%; height:auto; background: repeating-linear-gradient(45deg, #eee 0 12px, #fff 12px 24px);"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-muted mb-3">No overlay uploaded.</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{{ url_for('company.upload_company_overlay') }}" enctype="multipart/form-data" class="d-flex gap-2 flex-wrap align-items-end">
|
||||
<div>
|
||||
<label class="form-label">Upload overlay (PNG)</label>
|
||||
<input class="form-control" type="file" name="overlay" accept="image/png" required />
|
||||
<div class="form-text">Tip: export at 1920×1080 (or any 16:9 size).</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-brand" type="submit">Save overlay</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if overlay_url %}
|
||||
<form method="post" action="{{ url_for('company.delete_company_overlay') }}" class="mt-3" onsubmit="return confirm('Remove the overlay?');">
|
||||
<button class="btn btn-outline-danger" type="submit">Remove overlay</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
136
scripts/release.py
Normal file
136
scripts/release.py
Normal file
@@ -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:<version>
|
||||
- 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:]))
|
||||
Reference in New Issue
Block a user