From c03d135ae3edeb539465236d2be531df546cbb3a Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 18 Dec 2025 23:57:37 +0100 Subject: [PATCH] Upload files to "src" --- src/index.ts | 347 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 src/index.ts diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..eca8613 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,347 @@ +import { error, getInput, getMultilineInput, info } from "@actions/core"; + +enum Input { + CoolifyApiKey = "coolify-api-key", + CoolifyUrl = "coolify-url", + ProjectId = "project-id", + ServerId = "server-id", + Name = "name", + Domains = "domains", + EnvironmentId = "environment-id", + EnvironmentName = "environment-name", + DockerCompose = "docker-compose", + Env = "env", + ImageName = "image-name", + ImageTag = "image-tag", + Ports = "ports", + Override = "override", +} + +type CoolifyFetch = (path: string, init?: RequestInit) => Promise; + +type ParamResult

= P extends { list: true } ? string[] : string; + +type OptionalIfNotRequiredOrFallback = { + [K in keyof T as T[K] extends { required: true } + ? K + : T[K] extends { fallback: infer F } + ? F extends undefined + ? never + : K + : never]-?: ParamResult; +} & { + [K in keyof T as T[K] extends { required: true } + ? never + : T[K] extends { fallback: infer F } + ? F extends undefined + ? K + : never + : K]?: ParamResult; +}; + +function getParameters< + T extends { + [key: string]: { + name: string; + required?: boolean; + fallback?: string; + list?: boolean; + }; + } +>(parameters: T) { + return Object.fromEntries( + Object.entries(parameters).map(([name, _parameter]) => { + const parameter = _parameter as { + name: string; + required?: boolean; + fallback?: string; + list?: boolean; + }; + + return [ + name, + (parameter.list + ? getMultilineInput(parameter.name, parameter) + : getInput(parameter.name, parameter)) || + parameter.fallback || + undefined, + ]; + }) + ) as OptionalIfNotRequiredOrFallback; +} + +async function getUuid( + coolifyFetch: CoolifyFetch, + parameters: { + projectId: string; + environmentName?: string; + environmentId?: string; + serverId: string; + name: string; + domains: string; + } +) { + checkEnvironment(parameters); + + try { + const environment = await coolifyFetch( + `/projects/${parameters.projectId}/${ + parameters.environmentId ?? parameters.environmentName + }` + ).then((res) => res.json() as Promise<{ id: number }>); + + const response = await coolifyFetch("/applications").then( + (res) => + res.json() as Promise< + { + uuid: string; + name: string; + environment_id: number; + destination: { + server: { uuid: string }; + }; + fqdn: string; + }[] + > + ); + + const matches = response.filter((application) => { + if (application.name != parameters.name) { + return false; + } + + if (application.environment_id != environment.id) { + return false; + } + + if (application.destination.server.uuid != parameters.serverId) { + return false; + } + + if (application.fqdn != parameters.domains) { + return false; + } + + return true; + }); + + if (matches.length > 1) { + throw new Error("Multiple applications match the parameters"); + } + + return matches.length == 1 ? matches[0].uuid : null; + } catch (err) { + error(err as string | Error); + return null; + } +} + +async function deploy(coolifyFetch: CoolifyFetch, uuid: string) { + return await coolifyFetch(`/deploy?uuid=${uuid}&force=false`); +} + +const commonParameters = { + projectId: { name: Input.ProjectId, required: true }, + serverId: { name: Input.ServerId, required: true }, + name: { name: Input.Name, required: true }, + domains: { name: Input.Domains, required: true }, + environmentName: { name: Input.EnvironmentName }, + environmentId: { name: Input.EnvironmentId }, +} satisfies Parameters[0]; + +function checkEnvironment(parameters: { + environmentName?: string; + environmentId?: string; +}) { + if (!parameters.environmentName && !parameters.environmentId) { + throw new Error( + "'environment-name' or 'environment-id' must be defined" + ); + } +} + +async function createEnv( + coolifyFetch: CoolifyFetch, + uuid: string, + envs: string[] +) { + for (const env of envs) { + const [key, value] = env.split(/=(.*)/s).map((x) => x.trim()); + + await coolifyFetch(`/applications/${uuid}/envs`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + key, + value, + is_preview: false, + is_literal: true, + is_multiline: false, + is_shown_once: false, + }), + }).then((res) => res.json() as Promise<{ uuid: string }>); + } +} + +async function createDockerCompose(coolifyFetch: CoolifyFetch) { + const parameters = getParameters({ + ...commonParameters, + dockerCompose: { name: Input.DockerCompose, required: true }, + overrideString: { name: Input.Override, fallback: "{}" }, + env: { name: Input.Env, list: true }, + }); + + checkEnvironment(parameters); + + const override = JSON.parse(parameters.overrideString); + + const hasEnv = parameters.env && parameters.env.length > 0; + + const { uuid } = await coolifyFetch("/applications/dockercompose", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + project_uuid: parameters.projectId, + server_uuid: parameters.serverId, + environment_name: parameters.environmentName, + environment_uuid: parameters.environmentId, + docker_compose_raw: parameters.dockerCompose, + name: parameters.name, + domains: parameters.domains, + instant_deploy: false, + ...override, + }), + }).then((res) => res.json() as Promise<{ uuid: string }>); + + if (parameters.env && parameters.env.length > 0) { + createEnv(coolifyFetch, uuid, parameters.env); + } +} + +async function createDockerImage(coolifyFetch: CoolifyFetch) { + const parameters = getParameters({ + ...commonParameters, + imageName: { name: Input.ImageName, required: true }, + imageTag: { name: Input.ImageTag, required: true }, + ports: { name: Input.Ports, fallback: "80" }, + overrideString: { name: Input.Override, fallback: "{}" }, + env: { name: Input.Env, list: true }, + }); + + checkEnvironment(parameters); + + const override = JSON.parse(parameters.overrideString); + + const { uuid } = await coolifyFetch("/applications/dockerimage", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + project_uuid: parameters.projectId, + server_uuid: parameters.serverId, + environment_name: parameters.environmentName, + environment_uuid: parameters.environmentId, + docker_registry_image_name: parameters.imageName, + docker_registry_image_tag: parameters.imageTag, + name: parameters.name, + domains: parameters.domains, + ports_exposes: parameters.ports, + instant_deploy: false, + ...override, + }), + }).then((res) => res.json() as Promise<{ uuid: string }>); + + if (parameters.env && parameters.env.length > 0) { + createEnv(coolifyFetch, uuid, parameters.env); + } +} + +async function update(coolifyFetch: CoolifyFetch) { + const parameters = getParameters(commonParameters); + + checkEnvironment(parameters); + + const existingApplication = await getUuid(coolifyFetch, parameters); + + if (!existingApplication) { + throw new Error("Application doesn't exist"); + } + + return await deploy(coolifyFetch, existingApplication); +} + +async function deleteApplication(coolifyFetch: CoolifyFetch) { + const parameters = getParameters(commonParameters); + + checkEnvironment(parameters); + + const existingApplication = await getUuid(coolifyFetch, parameters); + + if (!existingApplication) { + throw new Error("No application found matching inputs"); + } + + return await coolifyFetch(`/applications/${existingApplication}`, { + method: "DELETE", + }); +} + +const actions: Record< + string, + { action: (coolifyFetch: CoolifyFetch) => Promise } +> = { + "create-docker-compose": { + action: createDockerCompose, + }, + "create-docker-image": { + action: createDockerImage, + }, + update: { action: update }, + delete: { action: deleteApplication }, +}; + +export async function run() { + const actionName = getInput("action", { required: true }); + const action = actions[actionName]; + + if (action == undefined) { + throw new Error( + `"${actionName} is not a valid action. Valid options are:\n\n\t${Object.keys( + actions + ).join("\n\t")}` + ); + } + + const apiKey = getInput("coolify-api-key", { required: true }); + const coolifyUrl = ( + getInput("coolify-url") || "https://coolify.skytechab.se" + ).replace(/\/+$/, ""); + + info(`Executing action '${actionName}' to url ${coolifyUrl}`); + + await action.action((input: string, init?: RequestInit) => + fetch(`${coolifyUrl}/api/v1/${input.replace(/^\//, "")}`, { + ...init, + headers: { + Authorization: `Bearer ${apiKey}`, + ...init?.headers, + }, + }).then(async (res) => { + if (!res.ok) { + const response = (await res.json()) as { message: string }; + throw new Error(response.message); + } + + return res; + }) + ); + + info("Action completed"); +} + +run();