feat(build): support optional BuildKit secrets
Platform CI tests / QuickStack deploy action tests (pull_request) Successful in 40s
Nuvisphere/Platform-CI: Immutable QuickStack OCI deployment / Build once and deploy exact digest (pull_request) Successful in 27s
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 29s
Platform CI tests / QuickStack deploy action tests (pull_request) Successful in 40s
Nuvisphere/Platform-CI: Immutable QuickStack OCI deployment / Build once and deploy exact digest (pull_request) Successful in 27s
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 29s
This commit was merged in pull request #4.
This commit is contained in:
@@ -374,6 +374,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 +422,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,
|
||||
};
|
||||
}
|
||||
@@ -566,6 +593,7 @@ 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));
|
||||
buildArgs.push(artifact.context);
|
||||
console.log(`Building ${artifact.name} from ${artifact.dockerfile} as ${taggedImage}.`);
|
||||
run("docker", buildArgs, { cwd: workspace, env: dockerEnv });
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
deployExactImage,
|
||||
expandTokens,
|
||||
mergeEnvironment,
|
||||
resolveBuildSecretArguments,
|
||||
resolveSecretEnvironment,
|
||||
orderApplications,
|
||||
resolveDeploymentBranch,
|
||||
@@ -151,6 +152,34 @@ test("version 2 validates promotion contracts and token expansion", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("passes declared Actions secrets to Docker only through BuildKit secret mounts", () => {
|
||||
const secretValue = "must-not-appear-in-docker-arguments";
|
||||
const artifact = validateArtifact({
|
||||
name: "web",
|
||||
image: "owner/web",
|
||||
buildSecrets: {
|
||||
"next-server-actions-encryption-key": "NEXT_SERVER_ACTIONS_ENCRYPTION_KEY",
|
||||
},
|
||||
});
|
||||
const args = resolveBuildSecretArguments(artifact.buildSecrets, {
|
||||
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: secretValue,
|
||||
});
|
||||
|
||||
assert.deepEqual(args, [
|
||||
"--secret",
|
||||
"id=next-server-actions-encryption-key,env=NEXT_SERVER_ACTIONS_ENCRYPTION_KEY",
|
||||
]);
|
||||
assert.doesNotMatch(JSON.stringify(args), new RegExp(secretValue));
|
||||
assert.throws(
|
||||
() => resolveBuildSecretArguments(artifact.buildSecrets, {}),
|
||||
/Missing Actions secret NEXT_SERVER_ACTIONS_ENCRYPTION_KEY/,
|
||||
);
|
||||
assert.throws(
|
||||
() => validateArtifact({ name: "web", image: "owner/web", buildSecrets: { "../invalid": "SECRET" } }),
|
||||
/Invalid BuildKit secret ID/,
|
||||
);
|
||||
});
|
||||
|
||||
test("application dependencies reject missing nodes and cycles", () => {
|
||||
assert.throws(
|
||||
() => orderApplications([{ name: "web", dependsOn: ["missing"] }]),
|
||||
|
||||
Reference in New Issue
Block a user