cast/src/config.ts

26 lines
860 B
TypeScript
Raw Normal View History

feat: cast — the Coolify executor, extracted from the infra state repo Public tool, private state. cast holds no hostnames, no bindings, no secrets: it joins a product repo's .infra/ manifest with a state directory you point it at, and makes Coolify match. Extracted from heavy-duty/infra, which was half tool and half state — the inconsistency that made it impossible to say whether "infra" named a CLI or a runbook. rig builds the boxes; cast fills them; infra is what they are filled with. Two changes were required to make it genuinely stateless and publishable: - The implicit cwd contract (environments.yaml / secrets/ / .coolify.env resolved against the working directory, silently reading the wrong file from the wrong place) is now an explicit --state <dir> / $CAST_STATE. - BANNED_IN_PROD — a hardcoded list of one product's ALLOW_* flags, the only product knowledge in the executor — becomes the generic, operator- owned environments.<env>.forbidden_var_patterns. The guard now lives in private state, so a product-side change cannot lower its own guard, and it is a pattern rather than a list, so it catches unforeseen siblings. Age identities resolve as $CAST_AGE_KEY_FILE_<ENV> then ~/.config/cast/age-<env>.key — which is the entire attended-vs-unattended apply mechanism, with no environment names known to the tool. Instance identity (org names, the GitHub App name, founder domains) is out of the fixtures and out of register-github-app.sh, which took APP_NAME and ORG as arguments rather than baking them in. 69 tests green; bin/cast + curl installer mirror rig's shape. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 12:25:44 +00:00
import { readFileSync } from "node:fs";
// Coolify's base URL + API token, read from <state>/.coolify.env — the one
// file in the state repo that is never committed (it is a live credential).
export function loadCoolifyEnv(path: string): {
baseUrl: string;
token: string;
} {
const vars: Record<string, string> = {};
for (const raw of readFileSync(path, "utf8").split("\n")) {
const line = raw.trim();
if (line === "" || line.startsWith("#")) continue;
const eq = line.indexOf("=");
if (eq === -1) continue;
vars[line.slice(0, eq)] = line.slice(eq + 1).replace(/^"|"$/g, "");
}
const baseUrl = vars.COOLIFY_BASE_URL;
const token = vars.COOLIFY_ACCESS_TOKEN;
if (!baseUrl || !token) {
throw new Error(
`${path}: COOLIFY_BASE_URL and COOLIFY_ACCESS_TOKEN are required`,
);
}
return { baseUrl, token };
}