From a283673bcf0c0864cf78beda7197b7658897368b Mon Sep 17 00:00:00 2001 From: Andre Beging Date: Thu, 9 Oct 2025 11:35:25 +0200 Subject: [PATCH] feat: add script for building and pushing Docker images with tagging options --- README.md | 15 +++++++++ publish.py | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 publish.py diff --git a/README.md b/README.md index d448922..4af3380 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,21 @@ Alle Counter werden nach jeder Änderung in `/data/counters.json` gespeichert. D - Non-root User `appuser` - Kein Port-Expose (Long Polling) +## Docker Image veröffentlichen +Zum Bauen und Pushen des Images mit `latest`-Tag und einem datierten Tag (z. B. `2025-09-30`) steht das Skript `publish.py` bereit. Stelle vorher sicher, dass du in der Registry `git.beging.de` angemeldet bist. + +```bash +python publish.py --date 2025-09-30 +``` + +Ohne `--date` wird automatisch das heutige UTC-Datum verwendet. Mit `--dry-run` kannst du die Docker-Kommandos nur anzeigen lassen: + +```bash +python publish.py --dry-run +``` + +Falls du den `latest` Tag nicht veröffentlichen möchtest, verwende `--no-latest`. + ## Logging Standard: INFO. Anpassbar über `log_level` in der Config. diff --git a/publish.py b/publish.py new file mode 100644 index 0000000..11ac094 --- /dev/null +++ b/publish.py @@ -0,0 +1,93 @@ +import argparse +import datetime as dt +import shlex +import subprocess +import sys +from pathlib import Path + +REPOSITORY = "git.beging.de/troogs/gigalativbot" +DEFAULT_CONTEXT = Path(__file__).parent + + +def run_command(cmd: list[str], /, *, dry_run: bool) -> None: + """Print and optionally execute a shell command.""" + print("$", shlex.join(cmd)) + if dry_run: + return + subprocess.run(cmd, check=True) + + +def build_and_push(*, include_latest: bool, date_tag: str, context: Path, dockerfile: Path | None, dry_run: bool) -> None: + tags: list[str] = [] + if include_latest: + tags.append(f"{REPOSITORY}:latest") + tags.append(f"{REPOSITORY}:{date_tag}") + + build_cmd = ["docker", "build"] + for tag in tags: + build_cmd.extend(["-t", tag]) + if dockerfile is not None: + build_cmd.extend(["-f", str(dockerfile)]) + build_cmd.append(str(context)) + + run_command(build_cmd, dry_run=dry_run) + + for tag in tags: + run_command(["docker", "push", tag], dry_run=dry_run) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Build and push docker images with latest and YYYYMMDD tags.") + parser.add_argument("--context", default=DEFAULT_CONTEXT, type=Path, help="Build context directory (default: project root)") + parser.add_argument("--dockerfile", type=Path, default=None, help="Path to Dockerfile (defaults to /Dockerfile)") + parser.add_argument("--date", help="Custom date string for tag (YYYY-MM-DD). Defaults to today (UTC).") + parser.add_argument("--no-latest", action="store_true", help="Do not tag/push the 'latest' tag.") + parser.add_argument("--dry-run", action="store_true", help="Print the docker commands without executing them.") + return parser.parse_args() + + +def ensure_date_tag(raw: str | None) -> str: + if raw is None: + return dt.datetime.utcnow().strftime("%Y-%m-%d") + try: + parsed = dt.datetime.strptime(raw, "%Y-%m-%d") + except ValueError as exc: + raise ValueError("Date tag must be in YYYY-MM-DD format.") from exc + return parsed.strftime("%Y-%m-%d") + + +def main() -> int: + args = parse_args() + try: + date_tag = ensure_date_tag(args.date) + except ValueError as exc: + print(f"Error: {exc}", file=sys.stderr) + return 1 + + context = args.context.resolve() + dockerfile = args.dockerfile.resolve() if args.dockerfile else None + + if dockerfile and not dockerfile.exists(): + print(f"Error: Dockerfile {dockerfile} does not exist.", file=sys.stderr) + return 2 + + if not context.exists(): + print(f"Error: context directory {context} does not exist.", file=sys.stderr) + return 3 + + try: + build_and_push( + include_latest=not args.no_latest, + date_tag=date_tag, + context=context, + dockerfile=dockerfile, + dry_run=args.dry_run, + ) + except subprocess.CalledProcessError as exc: + print(f"Command failed with exit code {exc.returncode}: {exc.cmd}", file=sys.stderr) + return exc.returncode + return 0 + + +if __name__ == "__main__": + sys.exit(main())