car-tracker/ops/gitops/request.py
Antoine Vandeplanque ccd2896c82
All checks were successful
CI / Lint, PostgreSQL tests, and package (push) Successful in 12s
CI / Build and publish tested image (push) Successful in 1m29s
publish tested images and queue manual GitOps app runs
2026-10-01 16:15:35 +02:00

98 lines
4.5 KiB
Python

"""Queue a digest-pinned, uniquely identified run on the gitops branch."""
import json
import os
from pathlib import Path
import re
import subprocess
import tempfile
import uuid
def command(*args, cwd=None, env=None):
return subprocess.run(args, cwd=cwd, env=env, check=True, text=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.strip()
def resolve_release(release):
if re.fullmatch(r"[0-9a-f]{40}", release):
ref = release
else:
command("git", "check-ref-format", f"refs/tags/{release}")
ref = f"refs/tags/{release}"
sha = command("git", "rev-parse", "--verify", f"{ref}^{{commit}}")
if not re.fullmatch(r"[0-9a-f]{40}", sha):
raise ValueError("Invalid revision")
# Only revisions in the trusted default branch's history may run.
command("git", "merge-base", "--is-ancestor", sha, "origin/master")
return sha
def image_digest(reference, sha):
command("docker", "pull", reference)
image = json.loads(command("docker", "image", "inspect", reference))[0]
if (image["Config"].get("Labels") or {}).get("org.opencontainers.image.revision") != sha:
raise ValueError("Image revision label does not match the selected release")
repository = reference.rsplit(":", 1)[0]
matches = [item for item in image.get("RepoDigests", [])
if item.startswith(repository + "@sha256:")]
if len(matches) != 1 or not re.fullmatch(re.escape(repository) + r"@sha256:[0-9a-f]{64}", matches[0]):
raise ValueError("No unambiguous registry digest")
return matches[0]
def queue(request):
user, token = os.environ["GITOPS_USER"], os.environ["GITOPS_TOKEN"]
if not user or not token:
raise ValueError("GitOps credentials are required")
url = os.environ["GITOPS_URL"]
if url != "https://gitea.vandeplanque.com/tonio/car-tracker.git":
raise ValueError("Unexpected GitOps repository")
branch = os.environ.get("GITOPS_BRANCH", "gitops")
if branch != "gitops":
raise ValueError("Unexpected state branch")
with tempfile.TemporaryDirectory(prefix="car-tracker-request-") as temporary:
root = Path(temporary)
askpass = root / "askpass.sh"
askpass.write_text('#!/bin/sh\ncase "$1" in\n*Username*) printf "%s\\n" "$GITOPS_USER" ;;\n*) printf "%s\\n" "$GITOPS_TOKEN" ;;\nesac\n')
askpass.chmod(0o700)
env = {**os.environ, "GIT_ASKPASS": str(askpass), "GIT_TERMINAL_PROMPT": "0"}
repo = root / "state"
repo.mkdir()
command("git", "init", cwd=repo, env=env)
command("git", "remote", "add", "origin", url, cwd=repo, env=env)
exists = command("git", "ls-remote", "--heads", "origin", f"refs/heads/{branch}", cwd=repo, env=env)
source = branch if exists else "master"
command("git", "fetch", "origin", source, cwd=repo, env=env)
command("git", "checkout", "-b", branch, "FETCH_HEAD", cwd=repo, env=env)
path = repo / "deploy/request.json"
path.parent.mkdir(exist_ok=True)
path.write_text(json.dumps(request, indent=2) + "\n")
command("git", "add", "deploy/request.json", cwd=repo, env=env)
command("git", "-c", "user.name=Car tracker Actions", "-c", "user.email=actions@localhost",
"commit", "-m", f"Queue app run {request['request_id']} ({request['revision'][:12]})", cwd=repo, env=env)
# Normal push is compare-and-swap: concurrent requests fail rather than
# silently overwriting each other. Never force-push the request history.
command("git", "push", "origin", f"HEAD:refs/heads/{branch}", cwd=repo, env=env)
def main():
try:
sha = resolve_release(os.environ["RELEASE"])
reference = f"{os.environ['REGISTRY']}/{os.environ['IMAGE_REPOSITORY']}:{sha}"
digest = image_digest(reference, sha)
request = {"schema_version": 1, "request_id": str(uuid.uuid4()), "revision": sha, "image": digest}
queue(request)
print(f"Queued request: {request['request_id']}")
print(f"Image: {digest}")
print("This confirms Git publication only. Inspect the production agent's outcome for collection/import status.")
return 0
except (KeyError, ValueError, OSError, subprocess.SubprocessError):
# Git/registry errors may contain authentication details: don't echo them.
print("Request failed: check the release, published image, credentials and state branch. No run was confirmed.")
return 1
if __name__ == "__main__":
raise SystemExit(main())