ci: wait for concurrent candidate publication
Nuvisphere/Platform-CI: Immutable QuickStack OCI deployment / Build once and deploy exact digest (push) Successful in 23s
Nuvisphere/Platform-CI: Immutable QuickStack OCI deployment / Build once and deploy exact digest (push) Successful in 23s
This commit is contained in:
@@ -577,11 +577,46 @@ function buildArtifact({ artifact, registry, sha, workspace, dockerEnv, validati
|
||||
return { artifact, taggedImage, digest, exactImage: `${registry}/${artifact.image}@${digest}` };
|
||||
}
|
||||
|
||||
function pullCandidateArtifact({ artifact, registry, sourceSha, workspace, dockerEnv }) {
|
||||
const PROMOTION_PR_CANDIDATE_PULL_ATTEMPTS = 40;
|
||||
const PROMOTION_PR_CANDIDATE_PULL_RETRY_MS = 10_000;
|
||||
|
||||
export async function pullCandidateArtifact({
|
||||
artifact,
|
||||
registry,
|
||||
sourceSha,
|
||||
workspace,
|
||||
dockerEnv,
|
||||
pullAttempts = 1,
|
||||
pullRetryMs = 0,
|
||||
runCommand = run,
|
||||
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
||||
}) {
|
||||
const taggedImage = `${registry}/${artifact.image}:sha-${sourceSha}`;
|
||||
run("docker", ["pull", taggedImage], { cwd: workspace, env: dockerEnv });
|
||||
if (!Number.isInteger(pullAttempts) || pullAttempts < 1) {
|
||||
throw new Error("Candidate pull attempts must be a positive integer.");
|
||||
}
|
||||
if (!Number.isFinite(pullRetryMs) || pullRetryMs < 0) {
|
||||
throw new Error("Candidate pull retry delay must be a non-negative number.");
|
||||
}
|
||||
for (let attempt = 1; attempt <= pullAttempts; attempt += 1) {
|
||||
try {
|
||||
runCommand("docker", ["pull", taggedImage], { cwd: workspace, env: dockerEnv });
|
||||
break;
|
||||
} catch (error) {
|
||||
if (attempt === pullAttempts) {
|
||||
throw new Error(
|
||||
`Candidate ${taggedImage} did not become available after ${pullAttempts} attempt(s).`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`Candidate ${taggedImage} is not available yet (${attempt}/${pullAttempts}); retrying in ${pullRetryMs}ms.`,
|
||||
);
|
||||
await sleep(pullRetryMs);
|
||||
}
|
||||
}
|
||||
verifyRequiredContainerFiles(taggedImage, artifact.requiredFiles, { cwd: workspace, env: dockerEnv });
|
||||
const repoDigests = run(
|
||||
const repoDigests = runCommand(
|
||||
"docker",
|
||||
["image", "inspect", "--format", "{{range .RepoDigests}}{{println .}}{{end}}", taggedImage],
|
||||
{ cwd: workspace, env: dockerEnv, capture: true },
|
||||
@@ -701,12 +736,14 @@ async function executeVersion2({ config, pipeline, eventName, sha, workspace })
|
||||
const release = loadRelease(workspace, pipeline.release);
|
||||
const artifacts = new Map();
|
||||
for (const artifact of pipeline.artifacts) {
|
||||
artifacts.set(artifact.name, pullCandidateArtifact({
|
||||
artifacts.set(artifact.name, await pullCandidateArtifact({
|
||||
artifact,
|
||||
registry,
|
||||
sourceSha: sha,
|
||||
workspace,
|
||||
dockerEnv,
|
||||
pullAttempts: PROMOTION_PR_CANDIDATE_PULL_ATTEMPTS,
|
||||
pullRetryMs: PROMOTION_PR_CANDIDATE_PULL_RETRY_MS,
|
||||
}));
|
||||
}
|
||||
console.log(`Validated ${release.tag} against ${artifacts.size} tested candidate artifact(s) from ${sourceBranch} at ${sha}; no rebuild or deployment performed.`);
|
||||
@@ -718,7 +755,7 @@ async function executeVersion2({ config, pipeline, eventName, sha, workspace })
|
||||
const release = loadRelease(workspace, pipeline.release);
|
||||
const artifacts = new Map();
|
||||
for (const artifact of pipeline.artifacts) {
|
||||
artifacts.set(artifact.name, pullCandidateArtifact({
|
||||
artifacts.set(artifact.name, await pullCandidateArtifact({
|
||||
artifact,
|
||||
registry,
|
||||
sourceSha,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
deployExactImage,
|
||||
expandTokens,
|
||||
mergeEnvironment,
|
||||
pullCandidateArtifact,
|
||||
resolveSecretEnvironment,
|
||||
orderApplications,
|
||||
resolveDeploymentBranch,
|
||||
@@ -99,6 +100,46 @@ test("production pull requests validate tested candidates without rebuilding", (
|
||||
);
|
||||
});
|
||||
|
||||
test("production pull requests wait for a concurrently published candidate", async () => {
|
||||
const registry = "gitea.nuvisphere.de";
|
||||
const artifact = {
|
||||
name: "webapp",
|
||||
image: "innomieter/webapp",
|
||||
requiredFiles: [],
|
||||
};
|
||||
const sourceSha = "1234567890abcdef1234567890abcdef12345678";
|
||||
const digest = `sha256:${"a".repeat(64)}`;
|
||||
const sleeps = [];
|
||||
let pullCalls = 0;
|
||||
const runCommand = (_command, args) => {
|
||||
if (args[0] === "pull") {
|
||||
pullCalls += 1;
|
||||
if (pullCalls < 3) throw new Error("manifest unknown");
|
||||
return "";
|
||||
}
|
||||
if (args[0] === "image") {
|
||||
return `${registry}/${artifact.image}@${digest}\n`;
|
||||
}
|
||||
throw new Error(`Unexpected Docker command: ${args.join(" ")}`);
|
||||
};
|
||||
|
||||
const result = await pullCandidateArtifact({
|
||||
artifact,
|
||||
registry,
|
||||
sourceSha,
|
||||
workspace: "/workspace",
|
||||
dockerEnv: {},
|
||||
pullAttempts: 3,
|
||||
pullRetryMs: 25,
|
||||
runCommand,
|
||||
sleep: async (ms) => sleeps.push(ms),
|
||||
});
|
||||
|
||||
assert.equal(pullCalls, 3);
|
||||
assert.deepEqual(sleeps, [25, 25]);
|
||||
assert.equal(result.exactImage, `${registry}/${artifact.image}@${digest}`);
|
||||
});
|
||||
|
||||
test("validates OCI paths and repository-local build paths", () => {
|
||||
assert.equal(validateTarget({ name: "Web", image: "Owner/Web", appId: "app-1" }).image, "owner/web");
|
||||
assert.throws(() => validateTarget({ name: "Web", image: "owner/web", appId: "app-1", context: "../secret" }), /inside/);
|
||||
|
||||
@@ -13,7 +13,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build_and_deploy:
|
||||
name: Build once and deploy exact digest
|
||||
name: ${{ gitea.event_name == 'pull_request' && 'Validate existing candidate without deployment' || gitea.event_name == 'push' && 'Build, publish and deploy exact candidate' || 'Validate or redeploy declared pipeline' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
Reference in New Issue
Block a user