# type: ignore from invoke import task {%- if cookiecutter.project_type == "service" %} IMAGE = "{{ cookiecutter.project_slug }}" SERVICE = "{{ cookiecutter.project_slug }}" VPS = "vps" # ssh alias; edit to match your ~/.ssh/config REMOTE_DIR = "~/stack" # dir on the VPS holding docker-compose.yml {%- endif %} @task def venv(c): """Sync dependencies.""" c.run("uv sync --group dev") @task def format(c): """Format code.""" c.run("uv run ruff format src tests") @task def lint(c): """Run linters.""" c.run("uv run ruff check src tests") c.run("uv run ruff format --check src tests") c.run("uv run mypy src") @task def test(c): """Run tests with coverage.""" c.run("uv run pytest --cov=src --cov-report=term-missing") @task def ci(c): """Run lint and tests.""" lint(c) test(c) print("All checks passed!") def _latest_tag(c) -> str: result = c.run("git describe --tags --abbrev=0", hide=True, warn=True) return result.stdout.strip() if result.ok else "v0.0.0" @task(help={"part": "patch, minor, or major"}) def bump(c, part): """Tag a new semver release (git tag is the source of truth).""" if part not in ("patch", "minor", "major"): raise SystemExit("Usage: inv bump ") if c.run("git status --porcelain", hide=True).stdout.strip(): raise SystemExit("Working tree is dirty. Commit or stash changes first.") major, minor, patch = (int(x) for x in _latest_tag(c).lstrip("v").split(".")) if part == "major": major, minor, patch = major + 1, 0, 0 elif part == "minor": minor, patch = minor + 1, 0 else: patch += 1 tag = f"v{major}.{minor}.{patch}" c.run(f"git tag {tag}") print(f"Tagged {tag}. Push it with: git push origin {tag}") {%- if cookiecutter.project_type == "service" %} def _version(c) -> str: return _latest_tag(c).lstrip("v") @task def build_image(c): """Build the runtime image, tagged with the current version.""" c.run(f"docker build --network=host -t {IMAGE}:{_version(c)} .") @task def deploy(c): """Build image, copy to VPS over SSH, restart the compose service.""" version = _version(c) build_image(c) print(f"Transferring {IMAGE}:{version} to {VPS}...") c.run(f"docker save {IMAGE}:{version} | ssh {VPS} docker load", pty=True) print("Updating docker-compose and restarting service...") c.run( f'ssh {VPS} "cd {REMOTE_DIR} && ' f"sed -i 's|image: {IMAGE}:.*|image: {IMAGE}:{version}|' docker-compose.yml && " f'docker compose up -d {SERVICE}"' ) print(f"Deployed {IMAGE}:{version}") {%- endif %} @task def clean(c): """Preview files to delete (safe mode).""" c.run("git clean -nfdx") if input("Delete? [y/N] ").lower() == "y": c.run("git clean -fdx")