Author SHA1 Message Date
vadimenovikau 009703f7fb feat(ci): forward MinIO runtime secrets
Nuvisphere/Platform-CI: Immutable QuickStack OCI deployment / Build once and deploy exact digest (push) Successful in 27s
Platform CI tests / QuickStack deploy action tests (pull_request) Successful in 28s
Nuvisphere/Platform-CI: Immutable QuickStack OCI deployment / Build once and deploy exact digest (pull_request) Successful in 27s
2026-08-21 13:28:19 +00:00
vadimenovikau 1ab428b949 test(ci): cover runtime secret mapping
Nuvisphere/Platform-CI: Immutable QuickStack OCI deployment / Build once and deploy exact digest (push) Successful in 32s
2026-08-21 13:28:18 +00:00
vadimenovikau 97bdc4692c feat(ci): support explicit runtime secret mapping
Nuvisphere/Platform-CI: Immutable QuickStack OCI deployment / Build once and deploy exact digest (push) Canceled after 0s
2026-08-21 13:28:17 +00:00
3 changed files with 59 additions and 3 deletions
+32 -2
View File
@@ -87,6 +87,33 @@ export function mergeEnvironment(source, overrides = {}) {
return rows.map((row) => row.raw ?? `${row.key}=${row.value}`).join("\n");
}
export function resolveSecretEnvironment(secretEnvironment = {}, environment = process.env) {
if (
secretEnvironment === null ||
typeof secretEnvironment !== "object" ||
Array.isArray(secretEnvironment)
) {
throw new Error("secretEnvironment must be an object.");
}
const resolved = {};
for (const [runtimeKey, rawSecretName] of Object.entries(secretEnvironment)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(runtimeKey)) {
throw new Error(`Invalid secret environment key ${runtimeKey}.`);
}
const secretName = String(rawSecretName);
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(secretName)) {
throw new Error(`Invalid Actions secret name for ${runtimeKey}.`);
}
const value = environment[secretName];
if (value === undefined || String(value).length === 0) {
throw new Error(`Missing Actions secret ${secretName} for runtime environment ${runtimeKey}.`);
}
resolved[runtimeKey] = String(value);
}
return resolved;
}
async function readBody(response) {
const text = await response.text();
if (!text) return null;
@@ -605,9 +632,12 @@ 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(
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,
@@ -6,6 +6,7 @@ import {
deployExactImage,
expandTokens,
mergeEnvironment,
resolveSecretEnvironment,
orderApplications,
resolveDeploymentBranch,
resolvePullRequestHeadBranch,
@@ -170,6 +171,24 @@ test("preserves response-only fields and existing environment secrets safely", (
assert.equal(mergeEnvironment(app().envVars, { ENVIRONMENT: "new" }), "SECRET=preserved\nENVIRONMENT=new");
});
test("maps only explicitly declared Actions secrets into runtime environment", () => {
assert.deepEqual(
resolveSecretEnvironment(
{ MINIO_ENDPOINT: "MINIO_ENDPOINT", MINIO_REGION: "MINIO_REGION" },
{ MINIO_ENDPOINT: "https://minio.example.test", MINIO_REGION: "us-east-1" },
),
{ MINIO_ENDPOINT: "https://minio.example.test", MINIO_REGION: "us-east-1" },
);
assert.throws(
() => resolveSecretEnvironment({ MINIO_ENDPOINT: "MINIO_ENDPOINT" }, {}),
/Missing Actions secret MINIO_ENDPOINT/,
);
assert.throws(
() => resolveSecretEnvironment({ "INVALID-KEY": "MINIO_ENDPOINT" }, { MINIO_ENDPOINT: "value" }),
/Invalid secret environment key/,
);
});
test("postflight waits for the exact expected identity", async () => {
let attempt = 0;
const result = await verifyEndpoint(
@@ -34,3 +34,10 @@ jobs:
QUICKSTACK_API_TOKEN: ${{ secrets.QUICKSTACK_API_TOKEN }}
QUICKSTACK_BASE_URL: https://server.nuvisphere.de
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
MINIO_ENDPOINT: ${{ secrets.MINIO_ENDPOINT }}
NEXT_PUBLIC_MINIO_ENDPOINT: ${{ secrets.NEXT_PUBLIC_MINIO_ENDPOINT }}
MINIO_REGION: ${{ secrets.MINIO_REGION }}
MINIO_ROOT_USER: ${{ secrets.MINIO_ROOT_USER }}
MINIO_ROOT_PASSWORD: ${{ secrets.MINIO_ROOT_PASSWORD }}
MINIO_TLS_REJECT_UNAUTHORIZED: ${{ secrets.MINIO_TLS_REJECT_UNAUTHORIZED }}
MINIO_AVATAR_BUCKET: ${{ secrets.MINIO_AVATAR_BUCKET }}