From f76cab8545cecf6fa2d418108f264d59074c2e8d Mon Sep 17 00:00:00 2001 From: Tracewebstudio Dev Date: Fri, 1 May 2026 11:04:23 +0200 Subject: [PATCH] ci: update gitops with new SHA on each build (auto-deploy) --- .gitea/scripts/update-gitops.py | 145 ++++++++++++++++++++++++++++++++ .gitea/workflows/build.yaml | 26 ++++++ 2 files changed, 171 insertions(+) create mode 100644 .gitea/scripts/update-gitops.py diff --git a/.gitea/scripts/update-gitops.py b/.gitea/scripts/update-gitops.py new file mode 100644 index 0000000..03805a0 --- /dev/null +++ b/.gitea/scripts/update-gitops.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +""" +Update GitOps kustomization.yaml with new image SHA tags. + +Usage: + python3 update-gitops.py \ + --repo /path/to/nxtgauge-gitops \ + --service gateway \ + --sha abc123def456... + +This script: +1. Updates the newTag for the specified service to the SHA +2. Commits and pushes to the gitops repo +3. ArgoCD detects the change and deploys +""" + +import argparse +import os +import re +import subprocess +import sys + + +def run(cmd: list[str], cwd: str = None) -> tuple[int, str, str]: + """Run a command and return (returncode, stdout, stderr).""" + result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True) + return result.returncode, result.stdout, result.stderr + + +def update_kustomization(kustomization_path: str, service: str, sha: str) -> bool: + """Update the newTag for a service in kustomization.yaml.""" + with open(kustomization_path, "r") as f: + content = f.read() + + # Pattern to find image entry for the service + # Matches: - name: registry.nxtgauge.com/nxtgauge-rust-{service} + # newTag: something + pattern = rf'(\s+-\s+name:\s+registry\.nxtgauge\.com/nxtgauge-rust-{re.escape(service)}\n\s+newTag:\s+)[^\n]+' + + replacement = rf'\g<1>{sha}' + + new_content, count = re.subn(pattern, replacement, content) + + if count == 0: + # Try without the nxtgauge-rust- prefix (for frontend, admin, etc) + pattern = rf'(\s+-\s+name:\s+registry\.nxtgauge\.com/nxtgauge-{re.escape(service)}\n\s+newTag:\s+)[^\n]+' + new_content, count = re.subn(pattern, replacement, content) + + if count == 0: + print(f"[ERROR] Could not find image entry for service: {service}") + return False + + with open(kustomization_path, "w") as f: + f.write(new_content) + + print(f"[OK] Updated {service} to SHA {sha}") + return True + + +def main(): + parser = argparse.ArgumentParser(description="Update GitOps with new image SHA") + parser.add_argument("--repo", required=True, help="Path to gitops repo") + parser.add_argument("--service", required=True, help="Service name (e.g., gateway, users, frontend-solid)") + parser.add_argument("--sha", required=True, help="Git SHA to deploy") + parser.add_argument("--message", default=None, help="Commit message") + args = parser.parse_args() + + service_image_map = { + "gateway": "nxtgauge-rust-gateway", + "users": "nxtgauge-rust-users", + "companies": "nxtgauge-rust-companies", + "jobs": "nxtgauge-rust-jobs", + "leads": "nxtgauge-rust-leads", + "job-seekers": "nxtgauge-rust-job-seekers", + "customers": "nxtgauge-rust-customers", + "payments": "nxtgauge-rust-payments", + "employees": "nxtgauge-rust-employees", + "photographers": "nxtgauge-rust-photographers", + "makeup-artists": "nxtgauge-rust-makeup-artists", + "tutors": "nxtgauge-rust-tutors", + "developers": "nxtgauge-rust-developers", + "video-editors": "nxtgauge-rust-video-editors", + "graphic-designers": "nxtgauge-rust-graphic-designers", + "social-media-managers": "nxtgauge-rust-social-media-managers", + "fitness-trainers": "nxtgauge-rust-fitness-trainers", + "catering-services": "nxtgauge-rust-catering-services", + "ugc-content-creators": "nxtgauge-rust-ugc-content-creators", + "cron": "nxtgauge-rust-cron", + "frontend-solid": "nxtgauge-frontend-solid", + "admin-solid": "nxtgauge-admin-solid", + "ai-assistant": "nxtgauge-ai-assistant", + } + + # Determine which kustomization file to update + if service_image_map.get(args.service): + image_name = service_image_map[args.service] + else: + image_name = f"nxtgauge-{args.service}" + + # Find the right kustomization file based on service + if "frontend" in args.service or "admin" in args.service: + kustomization_path = os.path.join(args.repo, "apps/nxtgauge-frontend-solid/overlays/prod/kustomization.yaml") + if not os.path.exists(kustomization_path): + kustomization_path = os.path.join(args.repo, "apps/nxtgauge-frontend-solid/base/kustomization.yaml") + elif "ai-assistant" in args.service: + kustomization_path = os.path.join(args.repo, "apps/nxtgauge-ai-assistant/overlays/prod/kustomization.yaml") + if not os.path.exists(kustomization_path): + kustomization_path = os.path.join(args.repo, "apps/nxtgauge-ai-assistant/base/kustomization.yaml") + else: + kustomization_path = os.path.join(args.repo, "apps/nxtgauge-backend-rust/overlays/prod/kustomization.yaml") + + if not os.path.exists(kustomization_path): + print(f"[ERROR] Kustomization file not found: {kustomization_path}") + sys.exit(0) # Exit 0 per workflow requirement + + print(f"Updating {kustomization_path} for service {args.service}") + + if not update_kustomization(kustomization_path, args.service, args.sha): + sys.exit(0) # Exit 0 per workflow requirement + + # Git add, commit, push + commit_msg = args.message or f"chore: deploy {args.service}@{args.sha}" + + run(["git", "add", "-A"], cwd=args.repo) + code, stdout, stderr = run(["git", "diff", "--cached", "--stat"], cwd=args.repo) + + if not stdout.strip(): + print("[INFO] No changes to commit") + sys.exit(0) + + print(f"Changes to commit:\n{stdout}") + + run(["git", "commit", "-m", commit_msg], cwd=args.repo) + code, stdout, stderr = run(["git", "push"], cwd=args.repo) + + if code != 0: + print(f"[ERROR] Push failed: {stderr}") + else: + print(f"[OK] Pushed update to gitops repo") + + sys.exit(0) # Always exit 0 per workflow requirement + + +if __name__ == "__main__": + main() diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 8159b8d..2071b40 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -60,3 +60,29 @@ jobs: --username "$REGISTRY_USERNAME" \ --password "$REGISTRY_PASSWORD" \ --keep 1 + + - name: Update GitOps and trigger deployment + if: success() + continue-on-error: true + env: + GITEOPS_REPO: ${{ secrets.GITEOPS_REPO }} + GITEOPS_SSH_KEY: ${{ secrets.GITEOPS_SSH_KEY }} + run: | + set -euo pipefail + + GITEOPS_DIR=$(mktemp -d) + git clone "$GITEOPS_REPO" "$GITEOPS_DIR" + cd "$GITEOPS_DIR" + + mkdir -p ~/.ssh + echo "$GITEOPS_SSH_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null + + python3 .gitea/scripts/update-gitops.py \ + --repo "$GITEOPS_DIR" \ + --service "admin-solid" \ + --sha "${{ gitea.sha }}" \ + --message "chore: deploy admin-solid@${{ gitea.sha }}" + + rm -rf "$GITEOPS_DIR"