Files
lunarfront-manager/src/services/git.ts
ryan 49f19d1758
Some checks failed
Build & Release / build (push) Has been cancelled
fix: query backend image tags for latest version instead of helm chart
DO registry API is unreliable for OCI Helm chart tags. Since the CI
pushes images and chart with the same version, use lunarfront-app
image tags which are always indexed correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:37:43 +00:00

124 lines
4.7 KiB
TypeScript

import { execSync } from "child_process";
import { writeFileSync, mkdirSync, rmSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { config } from "../lib/config";
export async function getLatestChartVersion(): Promise<string> {
// Query backend image tags instead of Helm chart tags — DO API is unreliable for OCI helm artifacts
const res = await fetch("https://api.digitalocean.com/v2/registry/lunarfront/repositories/lunarfront-app/tags?page=1&per_page=100", {
headers: { Authorization: `Bearer ${config.doToken}` },
});
const data = await res.json() as { tags: { tag: string }[] };
const versions = (data.tags ?? [])
.map(t => t.tag)
.filter(t => /^\d+\.\d+\.\d+$/.test(t))
.sort((a, b) => {
const [aMaj, aMin, aPat] = a.split(".").map(Number);
const [bMaj, bMin, bPat] = b.split(".").map(Number);
return bMaj - aMaj || bMin - aMin || bPat - aPat;
});
if (!versions.length) throw new Error("No chart versions found in DOCR");
return versions[0];
}
function withRepo<T>(fn: (dir: string, env: NodeJS.ProcessEnv) => T): T {
const keyPath = join(tmpdir(), `manager-ssh-key-${Date.now()}`);
const dir = join(tmpdir(), `lunarfront-charts-${Date.now()}`);
const keyContent = config.gitSshKey.endsWith("\n") ? config.gitSshKey : config.gitSshKey + "\n";
writeFileSync(keyPath, keyContent, { mode: 0o600 });
const env = {
...process.env,
GIT_SSH_COMMAND: `ssh -i ${keyPath} -o StrictHostKeyChecking=no`,
};
try {
execSync(`git clone ${config.gitRepoUrl} ${dir}`, { env, stdio: "pipe" });
execSync(`git -C ${dir} config user.email "manager@lunarfront.tech"`, { env });
execSync(`git -C ${dir} config user.name "lunarfront-manager"`, { env });
const result = fn(dir, env);
const unpushed = execSync(`git -C ${dir} log origin/main..HEAD --oneline`, { env }).toString().trim();
if (unpushed) execSync(`git -C ${dir} push origin main`, { env, stdio: "pipe" });
return result;
} finally {
rmSync(dir, { recursive: true, force: true });
rmSync(keyPath, { force: true });
}
}
export async function addCustomerChart(slug: string, appVersion: string) {
const version = (appVersion === "*" || appVersion === "latest") ? await getLatestChartVersion() : appVersion;
withRepo((dir, env) => {
const manifest = buildArgoCDApp(slug, version);
writeFileSync(join(dir, "customers", `${slug}.yaml`), manifest);
execSync(`git -C ${dir} add customers/${slug}.yaml`, { env });
execSync(`git -C ${dir} commit -m "feat: provision customer ${slug}"`, { env });
});
}
export async function upgradeCustomerChart(slug: string, version: string) {
withRepo((dir, env) => {
const manifest = buildArgoCDApp(slug, version);
writeFileSync(join(dir, "customers", `${slug}.yaml`), manifest);
execSync(`git -C ${dir} add customers/${slug}.yaml`, { env });
const diff = execSync(`git -C ${dir} diff --cached --name-only`, { env }).toString().trim();
if (!diff) return;
execSync(`git -C ${dir} commit -m "chore: upgrade customer ${slug} to chart ${version}"`, { env });
});
}
export async function upgradeAllCustomerCharts(slugs: string[], version: string) {
withRepo((dir, env) => {
for (const slug of slugs) {
const manifest = buildArgoCDApp(slug, version);
writeFileSync(join(dir, "customers", `${slug}.yaml`), manifest);
execSync(`git -C ${dir} add customers/${slug}.yaml`, { env });
}
const diff = execSync(`git -C ${dir} diff --cached --name-only`, { env }).toString().trim();
if (!diff) return;
execSync(`git -C ${dir} commit -m "chore: upgrade all customers to chart ${version}"`, { env });
});
}
export function removeCustomerChart(slug: string) {
withRepo((dir, env) => {
const filePath = join(dir, "customers", `${slug}.yaml`);
const tracked = execSync(`git -C ${dir} ls-files customers/${slug}.yaml`, { env }).toString().trim();
if (!tracked) return;
rmSync(filePath, { force: true });
execSync(`git -C ${dir} add customers/${slug}.yaml`, { env });
execSync(`git -C ${dir} commit -m "chore: deprovision customer ${slug}"`, { env });
});
}
function buildArgoCDApp(slug: string, version: string): string {
const revision = version;
return `apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: customer-${slug}
namespace: argocd
spec:
project: default
source:
repoURL: registry.digitalocean.com/lunarfront
chart: lunarfront
targetRevision: "${revision}"
helm:
parameters:
- name: ingress.host
value: ${slug}.lunarfront.tech
destination:
server: https://kubernetes.default.svc
namespace: customer-${slug}
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
`;
}