Template
CI reads Infisical staging as the read-only convexmonorepo-base-ci machine identity, runs the quality gate, and builds and pushes the image. The template is not deployed, so there is no Convex or VPS deploy step. The local stack moves to its own Compose name and ports so it runs beside the Payload template's, and it syncs the Convex runtime env. The scripts pick up Infisical account switching, dev-kill and a cache-safe CI install.
63 lines
2.1 KiB
JavaScript
Executable File
63 lines
2.1 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// ci-export-env.mjs [environment]
|
|
//
|
|
// CI's counterpart to scripts/export-env: prints an Infisical environment
|
|
// (default `staging`) as dotenv on stdout, in the same quoting export-env uses.
|
|
// It logs in as the `convexmonorepo-base-ci` machine identity through Universal Auth, using
|
|
// INFISICAL_CLIENT_ID and INFISICAL_CLIENT_SECRET from the job's Gitea secrets,
|
|
// and reads the project and host from .infisical.json. Plain Node, no CLI to
|
|
// install, so it runs in every job whether or not Bun is set up.
|
|
import { readFile } from 'node:fs/promises';
|
|
|
|
const environment = process.argv[2] ?? 'staging';
|
|
const { INFISICAL_CLIENT_ID: clientId, INFISICAL_CLIENT_SECRET: clientSecret } =
|
|
process.env;
|
|
|
|
const fail = (message) => {
|
|
console.error(`ci-export-env: ${message}`);
|
|
process.exit(1);
|
|
};
|
|
|
|
if (!clientId || !clientSecret)
|
|
fail('INFISICAL_CLIENT_ID and INFISICAL_CLIENT_SECRET must be set.');
|
|
|
|
const { workspaceId, domain } = JSON.parse(
|
|
await readFile(new URL('../.infisical.json', import.meta.url), 'utf8'),
|
|
);
|
|
|
|
const request = async (path, init) => {
|
|
const response = await fetch(`${domain}${path}`, init);
|
|
if (!response.ok)
|
|
fail(`${init?.method ?? 'GET'} ${path} returned ${response.status}.`);
|
|
return response.json();
|
|
};
|
|
|
|
const { accessToken } = await request('/v1/auth/universal-auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ clientId, clientSecret }),
|
|
});
|
|
|
|
const query = new URLSearchParams({
|
|
workspaceId,
|
|
environment,
|
|
secretPath: '/',
|
|
expandSecretReferences: 'true',
|
|
include_imports: 'true',
|
|
});
|
|
const { secrets, imports = [] } = await request(`/v3/secrets/raw?${query}`, {
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
});
|
|
|
|
// Imported secrets first so the environment's own values win.
|
|
const values = new Map();
|
|
for (const { secretKey, secretValue } of [
|
|
...imports.flatMap((folder) => folder.secrets),
|
|
...secrets,
|
|
])
|
|
values.set(secretKey, secretValue);
|
|
|
|
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
for (const [key, value] of [...values].sort(([a], [b]) => a.localeCompare(b)))
|
|
process.stdout.write(`${key}=${quote(value)}\n`);
|