#!/usr/bin/python3 # Copyright (C) 2023 Sofus Albert Høgsbro Rose # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import sys if not all([ sys.version_info.major == 3, sys.version_info.minor in [9, 10, 11, 12, 13], ]): sys.exit(1) from pathlib import Path import platform import shutil import subprocess import contextlib #################### # - Constants #################### SCRIPT_PATH = Path(__file__).resolve().parent CMD_DEPENDENCIES = [ 'podman', 'git', 'pre-commit' ] IMAGE_NAME = "site-support" IMAGE_VERSION = ("0", "0", "1") REGISTRY_HOST = "git.sofus.io" REGISTRY_USER = "so-rose" REGISTRY_PASSWORD = subprocess.check_output( ['pass', 'services/home/git.sofus.io/container-registry-token'] ).decode('ascii').strip() #################### # - Help Text #################### def action_help() -> None: print("""This script provides one-click development, CI, and deployment. Usage: echo -e "./run.py [OPTION] [EXTRAS]" The following commands must be available: podman => The project is developed and run in podman containers. git => This project uses git for versioning, and collaboration. pre-commit => Enforces that certain checks pass at each commit. Options: ./run.py => Equivilant to run.py help. ./run.py -h|h|help => Shows this text. Options, Run Locally: ./run.py app => Will run the app on port 8787, for local development. Options, Check: ./run.py check => Will run all checks, including static analysis and tests. ./run.py analyze-quality [OPTIONS] => Will run the static code-quality analysis suite. => OPTIONS will be passed directly to the tool (ruff). ./run.py analyze-types [OPTIONS] => Will run the static type checking suite. => OPTIONS will be passed directly to the tool (mypy). ./run.py analyze-security => Will run the static security analysis suite. ./run.py analyze-format [--overwrite] [OPTIONS] => Will run the code formatter in read-only omde. => '--overwrite' will cause the formater to reformat all files. => OPTIONS will be passed directly to the tool (tan). ./run.py test => Will run all Markdown Python snippets as tests. Options, Build & Deploy: ./run.py build => Will build, tag and upload a docker image appropriately. ./run.py build-release => Will build, tag and upload a docker image appropriate for release. Options, Housekeeping: ./run.py clean => Will delete all data caused by this project's presence on your system. """) #################### # - Utilities #################### @contextlib.contextmanager def cd_script_dir() -> None: cwd_orig = Path.cwd() os.chdir(SCRIPT_PATH) try: yield finally: os.chdir(cwd_orig) def get_git_revision_hash(short = True) -> str: commit_id = subprocess.check_output( ['git', 'rev-parse', 'HEAD'] ).decode('ascii').strip() return commit_id[:16] if short else commit_id def cmd_exists(cmd: str) -> bool: """Checks if a command exists. Supports Linux, Mac, Windows. """ return shutil.which(cmd) is not None #################### # - Actions - Build #################### def action_build(release = False) -> None: tag = get_git_revision_hash() if release else "dev" subprocess.run([ "podman", "build", ".", # : - Tag Commit ID "--tag", f"{IMAGE_NAME}:{tag}", ]) def action_publish() -> None: action_build(release = True) # Login to Registry subprocess.run([ "podman", "login", REGISTRY_HOST, "--username", REGISTRY_USER, "--password", REGISTRY_PASSWORD, ]) ## TODO: Ensure git tag matches. # Tag & Publish Image @ : subprocess.run([ "podman", "image", "push", f"{IMAGE_NAME}:dev", f"{REGISTRY_HOST}/{REGISTRY_USER}/{IMAGE_NAME}:{get_git_revision_hash()}", ]) # Publish Image @ :M, :M.m, :M.m.p for image_tag in [ f"{IMAGE_NAME}:{'.'.join(IMAGE_VERSION)}", f"{IMAGE_NAME}:{'.'.join(IMAGE_VERSION[:2])}", f"{IMAGE_NAME}:{IMAGE_VERSION[0]}", ]: # Tag Image subprocess.run([ "podman", "tag", f"{IMAGE_NAME}:dev", image_tag ]) # Publish Image subprocess.run([ "podman", "image", "push", image_tag, f"{REGISTRY_HOST}/{REGISTRY_USER}/{image_tag}", ]) #################### # - Actions - Run #################### def action_app() -> None: action_build() subprocess.run([ "podman", "run", "--rm", "-it", "--publish", "8787:8787", f"{IMAGE_NAME}:{get_git_revision_hash()}", ]) #################### # - Script #################### if __name__ == "__main__": # Check Available Commands for cmd in CMD_DEPENDENCIES: if not cmd_exists(cmd) : print("One or more dependencies are not installed. Please see --help.") sys.exit(1) with cd_script_dir(): if len(sys.argv) > 1: { "build": action_build, "publish": action_publish, "app": action_app, "dev": lambda: print("TBD"), "help": action_help, "-h": action_help, "--help": action_help, }[sys.argv[1]]() else: action_help()