feat: enhance deployment process with Gitea Actions integration and improved environment variable handling
This commit is contained in:
264
src/lib/env.ts
264
src/lib/env.ts
@@ -1,4 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { consola } from "consola";
|
||||
import { execa } from "execa";
|
||||
|
||||
const deployEnvSchema = z.object({
|
||||
REMOTE_HOST: z.string().min(1),
|
||||
@@ -11,8 +13,266 @@ const deployEnvSchema = z.object({
|
||||
WP_LOCALE: z.string().default("fr_CA"),
|
||||
});
|
||||
|
||||
const remoteEnvKeys = ["REMOTE_HOST", "REMOTE_PORT", "REMOTE_USER", "REMOTE_PATH"] as const;
|
||||
const requiredRemoteEnvKeys = ["REMOTE_HOST", "REMOTE_USER", "REMOTE_PATH"] as const;
|
||||
const giteaVariableKeys = [
|
||||
"REMOTE_HOST",
|
||||
"REMOTE_PORT",
|
||||
"REMOTE_USER",
|
||||
"REMOTE_PATH",
|
||||
"WP_SITE_URL",
|
||||
] as const;
|
||||
|
||||
export type DeployEnv = z.infer<typeof deployEnvSchema>;
|
||||
|
||||
export function readDeployEnv(env: NodeJS.ProcessEnv = process.env): DeployEnv {
|
||||
return deployEnvSchema.parse(env);
|
||||
type GiteaVariableKey = (typeof giteaVariableKeys)[number];
|
||||
|
||||
type GiteaRepo = {
|
||||
owner: string;
|
||||
repo: string;
|
||||
};
|
||||
|
||||
type GiteaVariable = {
|
||||
name?: string;
|
||||
data?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
export async function readDeployEnv(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
options: { cwd?: string } = {},
|
||||
): Promise<DeployEnv> {
|
||||
const normalizedEnv = normalizeEnv(env);
|
||||
const missingRemoteKeys = remoteEnvKeys.filter((key) => !hasValue(normalizedEnv[key]));
|
||||
|
||||
if (missingRemoteKeys.length > 0) {
|
||||
const giteaEnv = await readGiteaDeployEnv(normalizedEnv, options.cwd);
|
||||
for (const key of giteaVariableKeys) {
|
||||
if (!hasValue(normalizedEnv[key]) && hasValue(giteaEnv[key])) {
|
||||
normalizedEnv[key] = giteaEnv[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const missingRequiredKeys = requiredRemoteEnvKeys.filter((key) => !hasValue(normalizedEnv[key]));
|
||||
if (missingRequiredKeys.length > 0) {
|
||||
throw new Error(
|
||||
[
|
||||
`Missing deploy environment variable(s): ${missingRequiredKeys.join(", ")}`,
|
||||
"Set them directly or provide WEBSIMPLE_GITEA_API_TOKEN so wpop can read Gitea Actions variables.",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
return deployEnvSchema.parse(normalizedEnv);
|
||||
}
|
||||
|
||||
function normalizeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||
const normalized = { ...env };
|
||||
for (const key of [...remoteEnvKeys, "WP_SITE_URL"] as const) {
|
||||
if (typeof normalized[key] === "string" && normalized[key].trim() === "") {
|
||||
delete normalized[key];
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function readGiteaDeployEnv(
|
||||
env: NodeJS.ProcessEnv,
|
||||
cwd = process.cwd(),
|
||||
): Promise<Partial<Record<GiteaVariableKey, string>>> {
|
||||
const token = getGiteaToken(env);
|
||||
if (!token) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const repo = await resolveGiteaRepo(env, cwd);
|
||||
if (!repo) {
|
||||
consola.warn("Could not infer Gitea repository from git origin; skipping Gitea env lookup");
|
||||
return {};
|
||||
}
|
||||
|
||||
consola.info(
|
||||
`Reading deploy environment from Gitea Actions variables (${repo.owner}/${repo.repo})`,
|
||||
);
|
||||
|
||||
const baseUrl = getGiteaBaseUrl(env);
|
||||
const [orgVariables, repoVariables] = await Promise.all([
|
||||
fetchGiteaVariables(
|
||||
baseUrl,
|
||||
token,
|
||||
`/api/v1/orgs/${encodeURIComponent(repo.owner)}/actions/variables`,
|
||||
true,
|
||||
),
|
||||
fetchGiteaVariables(
|
||||
baseUrl,
|
||||
token,
|
||||
`/api/v1/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.repo)}/actions/variables`,
|
||||
false,
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
...variablesToEnv(orgVariables),
|
||||
...variablesToEnv(repoVariables),
|
||||
};
|
||||
}
|
||||
|
||||
function getGiteaToken(env: NodeJS.ProcessEnv): string | undefined {
|
||||
return firstValue(env.WPOP_GITEA_TOKEN, env.WEBSIMPLE_GITEA_API_TOKEN, env.GITEA_TOKEN);
|
||||
}
|
||||
|
||||
function getGiteaBaseUrl(env: NodeJS.ProcessEnv): string {
|
||||
return firstValue(env.WPOP_GITEA_BASE_URL, env.GITEA_BASE_URL) ?? "https://gitea.websimple.com";
|
||||
}
|
||||
|
||||
async function resolveGiteaRepo(
|
||||
env: NodeJS.ProcessEnv,
|
||||
cwd: string,
|
||||
): Promise<GiteaRepo | undefined> {
|
||||
const explicit = parseRepoSpec(env.WPOP_GITEA_REPO);
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
}
|
||||
|
||||
const explicitRepo = firstValue(env.WPOP_GITEA_REPO_NAME, env.GITEA_REPOSITORY);
|
||||
if (explicitRepo) {
|
||||
const owner = firstValue(env.WPOP_GITEA_OWNER, env.GITEA_REPOSITORY_OWNER) ?? "wp-sites";
|
||||
return parseRepoSpec(explicitRepo) ?? { owner, repo: trimGitSuffix(explicitRepo) };
|
||||
}
|
||||
|
||||
const originUrl = await readGitOriginUrl(cwd);
|
||||
if (!originUrl) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return parseGiteaRemoteUrl(originUrl);
|
||||
}
|
||||
|
||||
async function readGitOriginUrl(cwd: string): Promise<string | undefined> {
|
||||
const result = await execa("git", ["remote", "get-url", "origin"], {
|
||||
cwd,
|
||||
reject: false,
|
||||
});
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return result.stdout.trim() || undefined;
|
||||
}
|
||||
|
||||
function parseRepoSpec(spec: string | undefined): GiteaRepo | undefined {
|
||||
const value = firstValue(spec);
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [owner, repo] = value.split("/", 2);
|
||||
if (!owner || !repo) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { owner, repo: trimGitSuffix(repo) };
|
||||
}
|
||||
|
||||
function parseGiteaRemoteUrl(remoteUrl: string): GiteaRepo | undefined {
|
||||
try {
|
||||
const url = new URL(remoteUrl);
|
||||
if (url.hostname === "gitea.websimple.com") {
|
||||
return parseRepoSpec(url.pathname.replace(/^\/+/, ""));
|
||||
}
|
||||
} catch {
|
||||
// Not a URL-form remote; try scp-style below.
|
||||
}
|
||||
|
||||
const urlMatch = remoteUrl.match(
|
||||
/gitea\.websimple\.com[:/](?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/,
|
||||
);
|
||||
if (urlMatch?.groups) {
|
||||
return {
|
||||
owner: urlMatch.groups.owner,
|
||||
repo: trimGitSuffix(urlMatch.groups.repo),
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function fetchGiteaVariables(
|
||||
baseUrl: string,
|
||||
token: string,
|
||||
path: string,
|
||||
optional: boolean,
|
||||
): Promise<GiteaVariable[]> {
|
||||
const variables: GiteaVariable[] = [];
|
||||
|
||||
for (let page = 1; page <= 100; page += 1) {
|
||||
const url = new URL(path, baseUrl);
|
||||
url.searchParams.set("page", String(page));
|
||||
url.searchParams.set("limit", "100");
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `token ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 404 && optional) {
|
||||
return variables;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Gitea API request failed (${response.status}) while reading ${path}`);
|
||||
}
|
||||
|
||||
const pageVariables = (await response.json()) as GiteaVariable[];
|
||||
variables.push(...pageVariables);
|
||||
|
||||
if (pageVariables.length < 100) {
|
||||
return variables;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Gitea Actions variables pagination exceeded safety limit");
|
||||
}
|
||||
|
||||
function variablesToEnv(variables: GiteaVariable[]): Partial<Record<GiteaVariableKey, string>> {
|
||||
const env: Partial<Record<GiteaVariableKey, string>> = {};
|
||||
for (const variable of variables) {
|
||||
if (!isGiteaVariableKey(variable.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = firstValue(variable.data, variable.value);
|
||||
if (value) {
|
||||
env[variable.name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
function isGiteaVariableKey(value: string | undefined): value is GiteaVariableKey {
|
||||
return giteaVariableKeys.includes(value as GiteaVariableKey);
|
||||
}
|
||||
|
||||
function firstValue(...values: (string | undefined)[]): string | undefined {
|
||||
for (const value of values) {
|
||||
const trimmed = value?.trim();
|
||||
if (trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function hasValue(value: string | undefined): boolean {
|
||||
return Boolean(firstValue(value));
|
||||
}
|
||||
|
||||
function trimGitSuffix(value: string): string {
|
||||
return value.endsWith(".git") ? value.slice(0, -4) : value;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user