|
|
|
@@ -87,6 +87,54 @@ export function mergeEnvironment(source, overrides = {}) {
|
|
|
|
|
return rows.map((row) => row.raw ?? `${row.key}=${row.value}`).join("\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function parseEnvironment(source, { exclude = ["APP_DEPLOYMENT_ID"] } = {}) {
|
|
|
|
|
const excluded = new Set(exclude);
|
|
|
|
|
const environment = {};
|
|
|
|
|
for (const line of String(source ?? "").split(/\r?\n/)) {
|
|
|
|
|
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
|
|
|
if (match && !excluded.has(match[1])) environment[match[1]] = match[2];
|
|
|
|
|
}
|
|
|
|
|
return environment;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function normalizeVolumeMountPaths(volumeMountPaths = {}) {
|
|
|
|
|
if (
|
|
|
|
|
volumeMountPaths === null ||
|
|
|
|
|
typeof volumeMountPaths !== "object" ||
|
|
|
|
|
Array.isArray(volumeMountPaths)
|
|
|
|
|
) {
|
|
|
|
|
throw new Error("volumeMountPaths must be an object.");
|
|
|
|
|
}
|
|
|
|
|
return Object.fromEntries(
|
|
|
|
|
Object.entries(volumeMountPaths).map(([volumeId, rawPath]) => {
|
|
|
|
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(volumeId)) {
|
|
|
|
|
throw new Error(`Invalid QuickStack volume ID ${volumeId}.`);
|
|
|
|
|
}
|
|
|
|
|
const containerPath = String(rawPath).trim();
|
|
|
|
|
if (!/^\/[A-Za-z0-9._/-]+$/.test(containerPath) || containerPath.split("/").includes("..")) {
|
|
|
|
|
throw new Error(`Volume mount path for ${volumeId} must be a safe absolute path.`);
|
|
|
|
|
}
|
|
|
|
|
return [volumeId, containerPath];
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function applyVolumeMountPaths(appVolumes = [], volumeMountPaths = {}) {
|
|
|
|
|
const normalized = normalizeVolumeMountPaths(volumeMountPaths);
|
|
|
|
|
const matched = new Set();
|
|
|
|
|
const volumes = appVolumes.map((volume) => {
|
|
|
|
|
const key = [volume.id, volume.sharedVolumeId].find((candidate) => candidate && normalized[candidate]);
|
|
|
|
|
if (!key) return volume;
|
|
|
|
|
matched.add(key);
|
|
|
|
|
return { ...volume, containerMountPath: normalized[key] };
|
|
|
|
|
});
|
|
|
|
|
const missing = Object.keys(normalized).filter((key) => !matched.has(key));
|
|
|
|
|
if (missing.length) {
|
|
|
|
|
throw new Error(`QuickStack volume not found: ${missing.join(", ")}.`);
|
|
|
|
|
}
|
|
|
|
|
return volumes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function resolveSecretEnvironment(secretEnvironment = {}, environment = process.env) {
|
|
|
|
|
if (
|
|
|
|
|
secretEnvironment === null ||
|
|
|
|
@@ -237,6 +285,7 @@ export async function verifyEndpoint(postflight, { sha, fetchImpl = fetch, sleep
|
|
|
|
|
export async function deployExactImage({
|
|
|
|
|
client, appId, image, registryUsername, registryToken, environment = {},
|
|
|
|
|
healthCheckTcpPort, postflight, sha, deploymentAttempts = 1, deploymentRetrySeconds = 15,
|
|
|
|
|
volumeMountPaths = {},
|
|
|
|
|
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
|
|
|
fetchImpl = fetch,
|
|
|
|
|
}) {
|
|
|
|
@@ -256,6 +305,7 @@ export async function deployExactImage({
|
|
|
|
|
containerRegistryUsername: requiredString(registryUsername, "Registry username"),
|
|
|
|
|
containerRegistryPassword: requiredString(registryToken, "Registry token"),
|
|
|
|
|
envVars: mergeEnvironment(previous.envVars, environment),
|
|
|
|
|
appVolumes: applyVolumeMountPaths(previous.appVolumes, volumeMountPaths),
|
|
|
|
|
...(healthCheckTcpPort === undefined ? {} : { healthCheckTcpPort: Number(healthCheckTcpPort) }),
|
|
|
|
|
};
|
|
|
|
|
await client.saveApp(next);
|
|
|
|
@@ -374,6 +424,32 @@ function validateImagePath(value, name) {
|
|
|
|
|
return image;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeBuildSecrets(buildSecrets = {}) {
|
|
|
|
|
if (buildSecrets === null || typeof buildSecrets !== "object" || Array.isArray(buildSecrets)) {
|
|
|
|
|
throw new Error("buildSecrets must be an object.");
|
|
|
|
|
}
|
|
|
|
|
return Object.fromEntries(
|
|
|
|
|
Object.entries(buildSecrets).map(([id, rawEnvironmentName]) => {
|
|
|
|
|
if (!/^[A-Za-z0-9_.-]+$/.test(id)) throw new Error(`Invalid BuildKit secret ID ${id}.`);
|
|
|
|
|
const environmentName = String(rawEnvironmentName);
|
|
|
|
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(environmentName)) {
|
|
|
|
|
throw new Error(`Invalid Actions secret environment name for BuildKit secret ${id}.`);
|
|
|
|
|
}
|
|
|
|
|
return [id, environmentName];
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function resolveBuildSecretArguments(buildSecrets = {}, environment = process.env) {
|
|
|
|
|
return Object.entries(normalizeBuildSecrets(buildSecrets)).flatMap(([id, environmentName]) => {
|
|
|
|
|
const value = environment[environmentName];
|
|
|
|
|
if (value === undefined || String(value).length === 0) {
|
|
|
|
|
throw new Error(`Missing Actions secret ${environmentName} for BuildKit secret ${id}.`);
|
|
|
|
|
}
|
|
|
|
|
return ["--secret", `id=${id},env=${environmentName}`];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function validateArtifact(artifact) {
|
|
|
|
|
const name = requiredString(artifact.name, "Artifact name");
|
|
|
|
|
const requiredFiles = (artifact.requiredFiles ?? []).map((file) => {
|
|
|
|
@@ -396,6 +472,7 @@ export function validateArtifact(artifact) {
|
|
|
|
|
dockerfile: safeRelative(artifact.dockerfile ?? "Dockerfile", `Dockerfile for ${name}`),
|
|
|
|
|
context: safeRelative(artifact.context ?? ".", `Build context for ${name}`),
|
|
|
|
|
buildArgs,
|
|
|
|
|
buildSecrets: normalizeBuildSecrets(artifact.buildSecrets),
|
|
|
|
|
requiredFiles,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
@@ -417,12 +494,21 @@ export function validateApplication(application, artifactNames) {
|
|
|
|
|
if (deploymentRetrySeconds !== undefined && (!Number.isFinite(deploymentRetrySeconds) || deploymentRetrySeconds < 0)) {
|
|
|
|
|
throw new Error(`Deployment retry seconds for ${name} must be a non-negative number.`);
|
|
|
|
|
}
|
|
|
|
|
const appId = requiredString(application.appId, `QuickStack app ID for ${name}`);
|
|
|
|
|
const environmentFromAppId = application.environmentFromAppId === undefined
|
|
|
|
|
? undefined
|
|
|
|
|
: requiredString(application.environmentFromAppId, `Environment source app ID for ${name}`);
|
|
|
|
|
if (environmentFromAppId === appId) {
|
|
|
|
|
throw new Error(`Application ${name} cannot inherit environment from itself.`);
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
...application,
|
|
|
|
|
name,
|
|
|
|
|
artifact,
|
|
|
|
|
appId: requiredString(application.appId, `QuickStack app ID for ${name}`),
|
|
|
|
|
appId,
|
|
|
|
|
dependsOn,
|
|
|
|
|
volumeMountPaths: normalizeVolumeMountPaths(application.volumeMountPaths),
|
|
|
|
|
...(environmentFromAppId === undefined ? {} : { environmentFromAppId }),
|
|
|
|
|
...(deploymentAttempts === undefined ? {} : { deploymentAttempts }),
|
|
|
|
|
...(deploymentRetrySeconds === undefined ? {} : { deploymentRetrySeconds }),
|
|
|
|
|
};
|
|
|
|
@@ -514,6 +600,27 @@ export function resolvePullRequestHeadBranch({ environment = process.env, eventP
|
|
|
|
|
return requiredString(payload?.pull_request?.head?.ref, "Pull request head branch");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function isSameRepositoryPullRequest({ eventName, environment = process.env, eventPayload } = {}) {
|
|
|
|
|
if (eventName !== "pull_request") return false;
|
|
|
|
|
|
|
|
|
|
let payload = eventPayload;
|
|
|
|
|
if (payload === undefined) {
|
|
|
|
|
const eventPath = String(environment.GITHUB_EVENT_PATH || environment.GITEA_EVENT_PATH || "").trim();
|
|
|
|
|
if (eventPath) {
|
|
|
|
|
payload = JSON.parse(fs.readFileSync(eventPath, "utf8"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const baseRepository = String(
|
|
|
|
|
environment.GITHUB_REPOSITORY ||
|
|
|
|
|
environment.GITEA_REPOSITORY ||
|
|
|
|
|
payload?.repository?.full_name ||
|
|
|
|
|
payload?.pull_request?.base?.repo?.full_name ||
|
|
|
|
|
"",
|
|
|
|
|
).trim();
|
|
|
|
|
const headRepository = String(payload?.pull_request?.head?.repo?.full_name || "").trim();
|
|
|
|
|
return Boolean(baseRepository && headRepository && baseRepository === headRepository);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function classifyVersion2Execution(pipeline, eventName) {
|
|
|
|
|
if (eventName === "pull_request") {
|
|
|
|
|
return pipeline.strategy === "promote" ? "validate-candidate" : "build-validation";
|
|
|
|
@@ -521,6 +628,22 @@ export function classifyVersion2Execution(pipeline, eventName) {
|
|
|
|
|
return pipeline.strategy === "candidate" ? "build-deploy" : "promote";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function resolveActionMode(value = "all") {
|
|
|
|
|
const mode = String(value || "all").trim().toLowerCase();
|
|
|
|
|
if (!new Set(["all", "build", "coordinate"]).has(mode)) {
|
|
|
|
|
throw new Error(`Unsupported QuickStack action mode ${mode}.`);
|
|
|
|
|
}
|
|
|
|
|
return mode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function resolveArtifactIndex(value = "0") {
|
|
|
|
|
const index = Number(value);
|
|
|
|
|
if (!Number.isInteger(index) || index < 0) {
|
|
|
|
|
throw new Error("QuickStack artifact index must be a non-negative integer.");
|
|
|
|
|
}
|
|
|
|
|
return index;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function validatePromotionPullRequestSource(pipeline, headBranch) {
|
|
|
|
|
const sourceBranch = requiredString(pipeline.source?.branch, "Promotion source branch");
|
|
|
|
|
const actualHeadBranch = requiredString(headBranch, "Pull request head branch");
|
|
|
|
@@ -543,6 +666,24 @@ function dockerLogin(registry, dockerEnv) {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ensureBuildxBuilder(dockerEnv) {
|
|
|
|
|
const builder = "quickstack-registry-cache";
|
|
|
|
|
const existing = spawnSync(
|
|
|
|
|
"docker",
|
|
|
|
|
["buildx", "inspect", builder],
|
|
|
|
|
{ env: dockerEnv, encoding: "utf8", stdio: "ignore" },
|
|
|
|
|
);
|
|
|
|
|
if (existing.status !== 0) {
|
|
|
|
|
run(
|
|
|
|
|
"docker",
|
|
|
|
|
["buildx", "create", "--name", builder, "--driver", "docker-container"],
|
|
|
|
|
{ env: dockerEnv },
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
run("docker", ["buildx", "inspect", "--bootstrap", builder], { env: dockerEnv });
|
|
|
|
|
return builder;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function exactDigestFromPush(pushOutput, taggedImage) {
|
|
|
|
|
const digest = [...pushOutput.matchAll(/digest:\s*(sha256:[0-9a-f]{64})/g)].at(-1)?.[1];
|
|
|
|
|
if (!digest) throw new Error(`Registry did not return a digest for ${taggedImage}.`);
|
|
|
|
@@ -555,10 +696,20 @@ function verifyRequiredContainerFiles(taggedImage, requiredFiles, options) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildArtifact({ artifact, registry, sha, workspace, dockerEnv, validationOnly }) {
|
|
|
|
|
function buildArtifact({
|
|
|
|
|
artifact,
|
|
|
|
|
registry,
|
|
|
|
|
sha,
|
|
|
|
|
workspace,
|
|
|
|
|
dockerEnv,
|
|
|
|
|
validationOnly,
|
|
|
|
|
useRegistryCache = !validationOnly,
|
|
|
|
|
}) {
|
|
|
|
|
const taggedImage = `${registry}/${artifact.image}:sha-${sha}`;
|
|
|
|
|
const cacheImage = `${registry}/${artifact.image}:buildcache`;
|
|
|
|
|
const builder = ensureBuildxBuilder(dockerEnv);
|
|
|
|
|
const buildArgs = [
|
|
|
|
|
"build", "--progress=plain", "--file", artifact.dockerfile,
|
|
|
|
|
"buildx", "build", "--builder", builder, "--load", "--progress=plain", "--file", artifact.dockerfile,
|
|
|
|
|
"--label", `org.opencontainers.image.source=${process.env.GITHUB_SERVER_URL ?? process.env.GITEA_SERVER_URL}/${process.env.GITHUB_REPOSITORY ?? process.env.GITEA_REPOSITORY}`,
|
|
|
|
|
"--label", `org.opencontainers.image.revision=${sha}`,
|
|
|
|
|
"--tag", taggedImage,
|
|
|
|
@@ -566,6 +717,18 @@ function buildArtifact({ artifact, registry, sha, workspace, dockerEnv, validati
|
|
|
|
|
for (const [key, value] of Object.entries(artifact.buildArgs)) {
|
|
|
|
|
buildArgs.push("--build-arg", `${key}=${expandTokens(value, { sha })}`);
|
|
|
|
|
}
|
|
|
|
|
buildArgs.push(...resolveBuildSecretArguments(artifact.buildSecrets, dockerEnv));
|
|
|
|
|
if (useRegistryCache) {
|
|
|
|
|
buildArgs.push("--cache-from", `type=registry,ref=${cacheImage}`);
|
|
|
|
|
if (!validationOnly) {
|
|
|
|
|
buildArgs.push(
|
|
|
|
|
"--cache-to", `type=registry,ref=${cacheImage},mode=max,ignore-error=true`,
|
|
|
|
|
);
|
|
|
|
|
console.log(`Using read-write max-mode registry build cache ${cacheImage}.`);
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`Using read-only registry build cache ${cacheImage}.`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
buildArgs.push(artifact.context);
|
|
|
|
|
console.log(`Building ${artifact.name} from ${artifact.dockerfile} as ${taggedImage}.`);
|
|
|
|
|
run("docker", buildArgs, { cwd: workspace, env: dockerEnv });
|
|
|
|
@@ -630,33 +793,69 @@ function promoteArtifactAliases(artifacts, releaseTag, options) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function deployApplications({ applications, artifacts, sha, client }) {
|
|
|
|
|
for (const application of applications) {
|
|
|
|
|
const artifact = artifacts.get(application.artifact);
|
|
|
|
|
const environment = {
|
|
|
|
|
...Object.fromEntries(
|
|
|
|
|
Object.entries(application.environment ?? {}).map(([key, value]) => [key, expandTokens(value, { sha })]),
|
|
|
|
|
),
|
|
|
|
|
...resolveSecretEnvironment(application.secretEnvironment),
|
|
|
|
|
};
|
|
|
|
|
const result = await deployExactImage({
|
|
|
|
|
client,
|
|
|
|
|
appId: application.appId,
|
|
|
|
|
image: artifact.exactImage,
|
|
|
|
|
registryUsername: process.env.REGISTRY_USERNAME,
|
|
|
|
|
registryToken: process.env.REGISTRY_TOKEN,
|
|
|
|
|
environment,
|
|
|
|
|
healthCheckTcpPort: application.healthCheckTcpPort,
|
|
|
|
|
postflight: application.postflight,
|
|
|
|
|
deploymentAttempts: application.deploymentAttempts,
|
|
|
|
|
deploymentRetrySeconds: application.deploymentRetrySeconds,
|
|
|
|
|
sha,
|
|
|
|
|
});
|
|
|
|
|
console.log(`Deployed ${application.name}: ${result.image}`);
|
|
|
|
|
appendSummary(`- ${application.name}: \`${result.image}\` (${result.status})`);
|
|
|
|
|
const pending = new Map(applications.map((application) => [application.name, application]));
|
|
|
|
|
const completed = new Set();
|
|
|
|
|
|
|
|
|
|
while (pending.size > 0) {
|
|
|
|
|
const wave = applications.filter(
|
|
|
|
|
(application) =>
|
|
|
|
|
pending.has(application.name) &&
|
|
|
|
|
application.dependsOn.every((dependency) => completed.has(dependency)),
|
|
|
|
|
);
|
|
|
|
|
if (wave.length === 0) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Unable to resolve deployment dependencies for: ${[...pending.keys()].join(", ")}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`Deploying application wave: ${wave.map((application) => application.name).join(", ")}.`);
|
|
|
|
|
await Promise.all(
|
|
|
|
|
wave.map(async (application) => {
|
|
|
|
|
const artifact = artifacts.get(application.artifact);
|
|
|
|
|
const inheritedEnvironment = application.environmentFromAppId
|
|
|
|
|
? parseEnvironment((await client.getApp(application.environmentFromAppId)).envVars)
|
|
|
|
|
: {};
|
|
|
|
|
const environment = {
|
|
|
|
|
...inheritedEnvironment,
|
|
|
|
|
...Object.fromEntries(
|
|
|
|
|
Object.entries(application.environment ?? {}).map(([key, value]) => [key, expandTokens(value, { sha })]),
|
|
|
|
|
),
|
|
|
|
|
...resolveSecretEnvironment(application.secretEnvironment),
|
|
|
|
|
};
|
|
|
|
|
const result = await deployExactImage({
|
|
|
|
|
client,
|
|
|
|
|
appId: application.appId,
|
|
|
|
|
image: artifact.exactImage,
|
|
|
|
|
registryUsername: process.env.REGISTRY_USERNAME,
|
|
|
|
|
registryToken: process.env.REGISTRY_TOKEN,
|
|
|
|
|
environment,
|
|
|
|
|
healthCheckTcpPort: application.healthCheckTcpPort,
|
|
|
|
|
postflight: application.postflight,
|
|
|
|
|
deploymentAttempts: application.deploymentAttempts,
|
|
|
|
|
deploymentRetrySeconds: application.deploymentRetrySeconds,
|
|
|
|
|
volumeMountPaths: application.volumeMountPaths,
|
|
|
|
|
sha,
|
|
|
|
|
});
|
|
|
|
|
console.log(`Deployed ${application.name}: ${result.image}`);
|
|
|
|
|
appendSummary(`- ${application.name}: \`${result.image}\` (${result.status})`);
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
for (const application of wave) {
|
|
|
|
|
pending.delete(application.name);
|
|
|
|
|
completed.add(application.name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function executeVersion2({ config, pipeline, eventName, sha, workspace }) {
|
|
|
|
|
async function executeVersion2({
|
|
|
|
|
config,
|
|
|
|
|
pipeline,
|
|
|
|
|
eventName,
|
|
|
|
|
sha,
|
|
|
|
|
workspace,
|
|
|
|
|
actionMode = "all",
|
|
|
|
|
artifactIndex = 0,
|
|
|
|
|
}) {
|
|
|
|
|
const commitMessage = run("git", ["log", "-1", "--pretty=%B"], { cwd: workspace, capture: true });
|
|
|
|
|
const validationOnly = eventName === "pull_request";
|
|
|
|
|
const skipMarker = pipeline.strategy === "promote"
|
|
|
|
@@ -668,10 +867,68 @@ async function executeVersion2({ config, pipeline, eventName, sha, workspace })
|
|
|
|
|
}
|
|
|
|
|
const registry = requiredString(config.registry ?? "gitea.nuvisphere.de", "OCI registry").replace(/\/$/, "");
|
|
|
|
|
const dockerConfig = fs.mkdtempSync(path.join(os.tmpdir(), "quickstack-docker-"));
|
|
|
|
|
const dockerEnv = { ...process.env, DOCKER_CONFIG: dockerConfig };
|
|
|
|
|
const dockerEnv = { ...process.env, DOCKER_CONFIG: dockerConfig, DOCKER_BUILDKIT: "1" };
|
|
|
|
|
const execution = classifyVersion2Execution(pipeline, eventName);
|
|
|
|
|
const useRegistryCache =
|
|
|
|
|
execution !== "build-validation" ||
|
|
|
|
|
isSameRepositoryPullRequest({ eventName, environment: process.env });
|
|
|
|
|
try {
|
|
|
|
|
if (execution !== "build-validation") dockerLogin(registry, dockerEnv);
|
|
|
|
|
const needsRegistryLogin =
|
|
|
|
|
!(actionMode === "coordinate" && execution === "build-validation") &&
|
|
|
|
|
(execution !== "build-validation" || useRegistryCache);
|
|
|
|
|
if (needsRegistryLogin) dockerLogin(registry, dockerEnv);
|
|
|
|
|
|
|
|
|
|
if (actionMode === "build") {
|
|
|
|
|
if (execution !== "build-validation" && execution !== "build-deploy") {
|
|
|
|
|
console.log(`Artifact build slot ${artifactIndex} is not needed for ${execution}.`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const artifact = pipeline.artifacts[artifactIndex];
|
|
|
|
|
if (!artifact) {
|
|
|
|
|
console.log(`Artifact build slot ${artifactIndex} is unused for ${pipeline.name ?? pipeline.branch}.`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const built = buildArtifact({
|
|
|
|
|
artifact,
|
|
|
|
|
registry,
|
|
|
|
|
sha,
|
|
|
|
|
workspace,
|
|
|
|
|
dockerEnv,
|
|
|
|
|
validationOnly: execution === "build-validation",
|
|
|
|
|
useRegistryCache,
|
|
|
|
|
});
|
|
|
|
|
appendSummary(
|
|
|
|
|
execution === "build-validation"
|
|
|
|
|
? `Validated OCI artifact \`${artifact.name}\` for \`${pipeline.branch}\`.`
|
|
|
|
|
: `Built OCI artifact \`${artifact.name}\`: \`${built.exactImage}\`.`,
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (actionMode === "coordinate" && execution === "build-validation") {
|
|
|
|
|
appendSummary(`Validated ${pipeline.artifacts.length} immutable OCI artifact build(s) for \`${pipeline.branch}\`.`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (actionMode === "coordinate" && execution === "build-deploy") {
|
|
|
|
|
const artifacts = new Map();
|
|
|
|
|
for (const artifact of pipeline.artifacts) {
|
|
|
|
|
artifacts.set(artifact.name, pullCandidateArtifact({
|
|
|
|
|
artifact,
|
|
|
|
|
registry,
|
|
|
|
|
sourceSha: sha,
|
|
|
|
|
workspace,
|
|
|
|
|
dockerEnv,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
const client = createQuickStackClient({
|
|
|
|
|
baseUrl: process.env.QUICKSTACK_BASE_URL,
|
|
|
|
|
token: process.env.QUICKSTACK_API_TOKEN,
|
|
|
|
|
});
|
|
|
|
|
await deployApplications({ applications: pipeline.applications, artifacts, sha, client });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (execution === "build-validation" || execution === "build-deploy") {
|
|
|
|
|
const artifacts = new Map();
|
|
|
|
|
for (const artifact of pipeline.artifacts) {
|
|
|
|
@@ -682,6 +939,7 @@ async function executeVersion2({ config, pipeline, eventName, sha, workspace })
|
|
|
|
|
workspace,
|
|
|
|
|
dockerEnv,
|
|
|
|
|
validationOnly: execution === "build-validation",
|
|
|
|
|
useRegistryCache,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
if (execution === "build-validation") {
|
|
|
|
@@ -761,6 +1019,8 @@ async function main() {
|
|
|
|
|
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
|
|
|
const eventName = process.env.GITHUB_EVENT_NAME ?? process.env.GITEA_EVENT_NAME ?? "";
|
|
|
|
|
const branch = resolveDeploymentBranch({ eventName });
|
|
|
|
|
const actionMode = resolveActionMode(process.env.QUICKSTACK_ACTION_MODE);
|
|
|
|
|
const artifactIndex = resolveArtifactIndex(process.env.QUICKSTACK_ARTIFACT_INDEX);
|
|
|
|
|
if (config.version === 2) {
|
|
|
|
|
const pipeline = validateVersion2Pipeline(config, branch);
|
|
|
|
|
if (!pipeline) {
|
|
|
|
@@ -769,7 +1029,19 @@ async function main() {
|
|
|
|
|
}
|
|
|
|
|
const sha = requiredString(process.env.GITHUB_SHA ?? process.env.GITEA_SHA, "Git SHA").toLowerCase();
|
|
|
|
|
if (!/^[0-9a-f]{40,64}$/.test(sha)) throw new Error("Git SHA must be a full hexadecimal commit ID.");
|
|
|
|
|
await executeVersion2({ config, pipeline, eventName, sha, workspace });
|
|
|
|
|
await executeVersion2({
|
|
|
|
|
config,
|
|
|
|
|
pipeline,
|
|
|
|
|
eventName,
|
|
|
|
|
sha,
|
|
|
|
|
workspace,
|
|
|
|
|
actionMode,
|
|
|
|
|
artifactIndex,
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (actionMode === "build") {
|
|
|
|
|
console.log("Manifest version 1 remains on the coordinator's serial compatibility path.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const deployment = selectDeployment(config, branch);
|
|
|
|
|