ci: parallelize cached artifact delivery
Platform CI tests / QuickStack deploy action tests (push) Successful in 28s
Nuvisphere/Platform-CI: Immutable QuickStack OCI deployment / Build once and deploy exact digest (push) Successful in 31s

This commit is contained in:
2026-08-25 15:59:41 +02:00
parent 7625cab0d3
commit 9bc520ce64
+113 -47
View File
@@ -541,6 +541,27 @@ export function resolvePullRequestHeadBranch({ environment = process.env, eventP
return requiredString(payload?.pull_request?.head?.ref, "Pull request head branch"); 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) { export function classifyVersion2Execution(pipeline, eventName) {
if (eventName === "pull_request") { if (eventName === "pull_request") {
return pipeline.strategy === "promote" ? "validate-candidate" : "build-validation"; return pipeline.strategy === "promote" ? "validate-candidate" : "build-validation";
@@ -586,6 +607,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) { function exactDigestFromPush(pushOutput, taggedImage) {
const digest = [...pushOutput.matchAll(/digest:\s*(sha256:[0-9a-f]{64})/g)].at(-1)?.[1]; 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}.`); if (!digest) throw new Error(`Registry did not return a digest for ${taggedImage}.`);
@@ -598,11 +637,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 taggedImage = `${registry}/${artifact.image}:sha-${sha}`;
const cacheImage = `${registry}/${artifact.image}:buildcache`; const cacheImage = `${registry}/${artifact.image}:buildcache`;
const builder = ensureBuildxBuilder(dockerEnv);
const buildArgs = [ 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.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}`, "--label", `org.opencontainers.image.revision=${sha}`,
"--tag", taggedImage, "--tag", taggedImage,
@@ -611,18 +659,16 @@ function buildArtifact({ artifact, registry, sha, workspace, dockerEnv, validati
buildArgs.push("--build-arg", `${key}=${expandTokens(value, { sha })}`); buildArgs.push("--build-arg", `${key}=${expandTokens(value, { sha })}`);
} }
buildArgs.push(...resolveBuildSecretArguments(artifact.buildSecrets, dockerEnv)); buildArgs.push(...resolveBuildSecretArguments(artifact.buildSecrets, dockerEnv));
if (!validationOnly) { if (useRegistryCache) {
try { buildArgs.push("--cache-from", `type=registry,ref=${cacheImage}`);
run("docker", ["pull", cacheImage], { cwd: workspace, env: dockerEnv, capture: true }); if (!validationOnly) {
buildArgs.push("--cache-from", cacheImage); buildArgs.push(
console.log(`Using registry build cache ${cacheImage}.`); "--cache-to", `type=registry,ref=${cacheImage},mode=max,ignore-error=true`,
} catch { );
console.log(`No reusable registry build cache is available for ${artifact.name}.`); console.log(`Using read-write max-mode registry build cache ${cacheImage}.`);
} else {
console.log(`Using read-only registry build cache ${cacheImage}.`);
} }
buildArgs.push(
"--build-arg", "BUILDKIT_INLINE_CACHE=1",
"--tag", cacheImage,
);
} }
buildArgs.push(artifact.context); buildArgs.push(artifact.context);
console.log(`Building ${artifact.name} from ${artifact.dockerfile} as ${taggedImage}.`); console.log(`Building ${artifact.name} from ${artifact.dockerfile} as ${taggedImage}.`);
@@ -632,12 +678,6 @@ function buildArtifact({ artifact, registry, sha, workspace, dockerEnv, validati
const pushOutput = run("docker", ["push", taggedImage], { cwd: workspace, env: dockerEnv, capture: true }); const pushOutput = run("docker", ["push", taggedImage], { cwd: workspace, env: dockerEnv, capture: true });
process.stdout.write(pushOutput); process.stdout.write(pushOutput);
const digest = exactDigestFromPush(pushOutput, taggedImage); const digest = exactDigestFromPush(pushOutput, taggedImage);
try {
const cacheOutput = run("docker", ["push", cacheImage], { cwd: workspace, env: dockerEnv, capture: true });
process.stdout.write(cacheOutput);
} catch (error) {
console.log(`Registry cache publication failed for ${artifact.name}; the immutable image remains valid: ${error instanceof Error ? error.message : error}`);
}
return { artifact, taggedImage, digest, exactImage: `${registry}/${artifact.image}@${digest}` }; return { artifact, taggedImage, digest, exactImage: `${registry}/${artifact.image}@${digest}` };
} }
@@ -694,29 +734,52 @@ function promoteArtifactAliases(artifacts, releaseTag, options) {
} }
async function deployApplications({ applications, artifacts, sha, client }) { async function deployApplications({ applications, artifacts, sha, client }) {
for (const application of applications) { const pending = new Map(applications.map((application) => [application.name, application]));
const artifact = artifacts.get(application.artifact); const completed = new Set();
const environment = {
...Object.fromEntries( while (pending.size > 0) {
Object.entries(application.environment ?? {}).map(([key, value]) => [key, expandTokens(value, { sha })]), const wave = applications.filter(
), (application) =>
...resolveSecretEnvironment(application.secretEnvironment), pending.has(application.name) &&
}; application.dependsOn.every((dependency) => completed.has(dependency)),
const result = await deployExactImage({ );
client, if (wave.length === 0) {
appId: application.appId, throw new Error(
image: artifact.exactImage, `Unable to resolve deployment dependencies for: ${[...pending.keys()].join(", ")}`,
registryUsername: process.env.REGISTRY_USERNAME, );
registryToken: process.env.REGISTRY_TOKEN, }
environment,
healthCheckTcpPort: application.healthCheckTcpPort, console.log(`Deploying application wave: ${wave.map((application) => application.name).join(", ")}.`);
postflight: application.postflight, await Promise.all(
deploymentAttempts: application.deploymentAttempts, wave.map(async (application) => {
deploymentRetrySeconds: application.deploymentRetrySeconds, const artifact = artifacts.get(application.artifact);
sha, const environment = {
}); ...Object.fromEntries(
console.log(`Deployed ${application.name}: ${result.image}`); Object.entries(application.environment ?? {}).map(([key, value]) => [key, expandTokens(value, { sha })]),
appendSummary(`- ${application.name}: \`${result.image}\` (${result.status})`); ),
...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})`);
}),
);
for (const application of wave) {
pending.delete(application.name);
completed.add(application.name);
}
} }
} }
@@ -742,12 +805,13 @@ async function executeVersion2({
const dockerConfig = fs.mkdtempSync(path.join(os.tmpdir(), "quickstack-docker-")); const dockerConfig = fs.mkdtempSync(path.join(os.tmpdir(), "quickstack-docker-"));
const dockerEnv = { ...process.env, DOCKER_CONFIG: dockerConfig, DOCKER_BUILDKIT: "1" }; const dockerEnv = { ...process.env, DOCKER_CONFIG: dockerConfig, DOCKER_BUILDKIT: "1" };
const execution = classifyVersion2Execution(pipeline, eventName); const execution = classifyVersion2Execution(pipeline, eventName);
const useRegistryCache =
execution !== "build-validation" ||
isSameRepositoryPullRequest({ eventName, environment: process.env });
try { try {
const needsRegistryLogin = actionMode === "all" const needsRegistryLogin =
? execution !== "build-validation" !(actionMode === "coordinate" && execution === "build-validation") &&
: actionMode === "build" (execution !== "build-validation" || useRegistryCache);
? execution === "build-deploy"
: execution !== "build-validation";
if (needsRegistryLogin) dockerLogin(registry, dockerEnv); if (needsRegistryLogin) dockerLogin(registry, dockerEnv);
if (actionMode === "build") { if (actionMode === "build") {
@@ -767,6 +831,7 @@ async function executeVersion2({
workspace, workspace,
dockerEnv, dockerEnv,
validationOnly: execution === "build-validation", validationOnly: execution === "build-validation",
useRegistryCache,
}); });
appendSummary( appendSummary(
execution === "build-validation" execution === "build-validation"
@@ -810,6 +875,7 @@ async function executeVersion2({
workspace, workspace,
dockerEnv, dockerEnv,
validationOnly: execution === "build-validation", validationOnly: execution === "build-validation",
useRegistryCache,
})); }));
} }
if (execution === "build-validation") { if (execution === "build-validation") {