core/steps/setup.step.ts
core/steps/setup.step.ts is a file in Bane's Lab Deploy. 172 lines of code and 24 definitions.
import {
APPLY_EFFECTS,
PUBLISH_EFFECTS,
REVERT_EFFECTS,
STAGE_EFFECTS,
UPKEEP_EFFECTS,
runEffects,
} from "#core/pipelines/setup.pipeline";
import {
ARCHITECTURE_COMMAND,
CODENAME_COMMAND,
MODULE_PACKAGES,
NGINX_PACKAGE,
NGINX_SOURCE,
PACKAGE_SOURCES,
PACKAGE_STATUS_COMMAND,
SETUP_STAGING,
} from "#configuration/constants/nginx.constants";
import type {
IndexFetcher,
InstalledPackage,
Journal,
PackagePlan,
PackageStanza,
Platform,
SetupContext,
Shell,
} from "#types/deployment.types";
import {
PACKAGE_REVERT_FAILED,
PACKAGE_REVERT_UNAVAILABLE,
PACKAGE_UNAVAILABLE,
SERVER_CHECKING,
SERVER_CURRENT,
SERVER_READY,
} from "#configuration/strings/nginx.strings";
import {
codenameOf,
companionOf,
indexUrl,
installedOf,
isCurrent,
serverTarget,
stanzaAt,
stanzasOf,
} from "#core/converters/index.converter";
import { messageOf } from "#core/converters/failure.converter";
import { staleModules } from "#core/steps/build.step";
const required = function required<T>(value: T | null, message: string): T {
if (value === null) {
throw new Error(message);
}
return value;
};
const loadIndexes = async function loadIndexes(
fetchIndex: IndexFetcher,
codename: string,
architecture: string,
): Promise<readonly PackageStanza[]> {
const indexes = await Promise.all(
PACKAGE_SOURCES.map(async (source) =>
stanzasOf(await fetchIndex(indexUrl(source, codename, architecture)), source),
),
);
return indexes.flat();
};
const installedAll = async function installedAll(
shell: Shell,
names: readonly string[],
): Promise<readonly InstalledPackage[]> {
return Promise.all(
names.map(async (name) => installedOf(name, (await shell.run(`${PACKAGE_STATUS_COMMAND} ${name}`)).stdout)),
);
};
const revertStanzas = async function revertStanzas(
fetchIndex: IndexFetcher,
kept: readonly InstalledPackage[],
architecture: string,
): Promise<readonly PackageStanza[]> {
return Promise.all(
kept.map(async (installed) => {
const version = installed.version ?? "";
const stanzas = await loadIndexes(fetchIndex, codenameOf(version), architecture);
return required(
stanzaAt(stanzas, installed.name, version),
`${PACKAGE_REVERT_UNAVAILABLE}${installed.name} ${version}`,
);
}),
);
};
export const planPackages = async function planPackages(
shell: Shell,
fetchIndex: IndexFetcher,
platform: Platform,
): Promise<PackagePlan> {
const stanzas = await loadIndexes(fetchIndex, platform.codename, platform.architecture);
const target = required(
serverTarget(stanzas, NGINX_SOURCE.upstream),
`${PACKAGE_UNAVAILABLE}${NGINX_PACKAGE} ${NGINX_SOURCE.upstream} ${platform.codename}`,
);
const wanted = [
target,
...MODULE_PACKAGES.map((name) =>
required(companionOf(stanzas, name, target), `${PACKAGE_UNAVAILABLE}${name} ${target.version}`),
),
];
const installed = await installedAll(
shell,
wanted.map((stanza) => stanza.name),
);
const install = wanted.filter((stanza, at) => installed[at] === undefined || !isCurrent(installed[at], stanza));
const changing = installed.filter((entry) => install.some((stanza) => stanza.name === entry.name));
const kept = changing.filter((entry) => entry.configured && entry.version !== null);
const revert = await revertStanzas(fetchIndex, kept, platform.architecture);
const added = changing.filter((entry) => !entry.configured).map((entry) => entry.name);
return { added, install, revert };
};
const changeServer = async function changeServer(context: SetupContext): Promise<void> {
await runEffects(STAGE_EFFECTS, context);
try {
await runEffects(APPLY_EFFECTS, context);
} catch (error: unknown) {
try {
await runEffects(REVERT_EFFECTS, context);
} catch (revertError: unknown) {
throw new Error(`${messageOf(error)} ${PACKAGE_REVERT_FAILED}${messageOf(revertError)}`, {
cause: revertError,
});
}
throw error;
}
};
const contextOf = async function contextOf(
shell: Shell,
journal: Journal,
fetchIndex: IndexFetcher,
): Promise<SetupContext> {
const platform: Platform = {
architecture: (await shell.run(ARCHITECTURE_COMMAND)).stdout.trim(),
codename: (await shell.run(CODENAME_COMMAND)).stdout.trim(),
};
const plan = await planPackages(shell, fetchIndex, platform);
return { fetchIndex, journal, modules: await staleModules(shell), plan, shell };
};
export const ensureServer = async function ensureServer(
shell: Shell,
journal: Journal,
fetchIndex: IndexFetcher,
): Promise<SetupContext> {
journal.log(SERVER_CHECKING);
const context = await contextOf(shell, journal, fetchIndex);
const current = context.plan.install.length === 0 && context.modules.length === 0;
try {
if (current) {
journal.log(SERVER_CURRENT);
await runEffects(UPKEEP_EFFECTS, context);
} else {
await changeServer(context);
}
} finally {
await shell.run(`rm -rf ${SETUP_STAGING}`);
}
journal.log(SERVER_READY);
return context;
};
export const prepareServer = async function prepareServer(
shell: Shell,
journal: Journal,
fetchIndex: IndexFetcher,
): Promise<void> {
await runEffects(PUBLISH_EFFECTS, await ensureServer(shell, journal, fetchIndex));
};