0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-04-14 23:51:49 -05:00

feat: astro:env validateSecrets ()

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
Florian Lefebvre 2024-07-09 19:46:37 +02:00 committed by GitHub
parent 75d118bf7f
commit 0a4b31ffeb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 150 additions and 70 deletions
.changeset
packages/astro

View file

@ -0,0 +1,23 @@
---
'astro': patch
---
Adds a new property `experimental.env.validateSecrets` to allow validating private variables on the server.
By default, this is set to `false` and only public variables are checked on start. If enabled, secrets will also be checked on start (dev/build modes). This is useful for example in some CIs to make sure all your secrets are correctly set before deploying.
```js
// astro.config.mjs
import { defineConfig, envField } from "astro/config"
export default defineConfig({
experimental: {
env: {
schema: {
// ...
},
validateSecrets: true
}
}
})
```

View file

@ -2166,6 +2166,37 @@ export interface AstroUserConfig {
* ```
*/
schema?: EnvSchema;
/**
* @docs
* @name experimental.env.validateSecrets
* @kind h4
* @type {boolean}
* @default `false`
* @version 4.11.6
* @description
*
* Whether or not to validate secrets on the server when starting the dev server or running a build.
*
* By default, only public variables are validated on the server when starting the dev server or a build, and private variables are validated at runtime only. If enabled, private variables will also be checked on start. This is useful in some continuous integration (CI) pipelines to make sure all your secrets are correctly set before deploying.
*
* ```js
* // astro.config.mjs
* import { defineConfig, envField } from "astro/config"
*
* export default defineConfig({
* experimental: {
* env: {
* schema: {
* // ...
* },
* validateSecrets: true
* }
* }
* })
* ```
*/
validateSecrets?: boolean;
};
};
}

View file

@ -89,6 +89,9 @@ export const ASTRO_CONFIG_DEFAULTS = {
clientPrerender: false,
globalRoutePriority: false,
rewriting: false,
env: {
validateSecrets: false
}
},
} satisfies AstroUserConfig & { server: { open: boolean } };
@ -526,6 +529,7 @@ export const AstroConfigSchema = z.object({
env: z
.object({
schema: EnvSchema.optional(),
validateSecrets: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.env.validateSecrets)
})
.strict()
.optional(),

View file

@ -52,7 +52,11 @@ export function astroEnv({
}
}
const validatedVariables = validatePublicVariables({ schema, loadedEnv });
const validatedVariables = validatePublicVariables({
schema,
loadedEnv,
validateSecrets: settings.config.experimental.env?.validateSecrets ?? false,
});
templates = {
...getTemplates(schema, fs, validatedVariables),
@ -94,23 +98,28 @@ function resolveVirtualModuleId<T extends string>(id: T): `\0${T}` {
function validatePublicVariables({
schema,
loadedEnv,
validateSecrets,
}: {
schema: EnvSchema;
loadedEnv: Record<string, string>;
validateSecrets: boolean;
}) {
const valid: Array<{ key: string; value: any; type: string; context: 'server' | 'client' }> = [];
const invalid: Array<{ key: string; type: string }> = [];
for (const [key, options] of Object.entries(schema)) {
if (options.access !== 'public') {
const variable = loadedEnv[key] === '' ? undefined : loadedEnv[key];
if (options.access === 'secret' && !validateSecrets) {
continue;
}
const variable = loadedEnv[key];
const result = validateEnvVariable(variable === '' ? undefined : variable, options);
if (result.ok) {
valid.push({ key, value: result.value, type: result.type, context: options.context });
} else {
const result = validateEnvVariable(variable, options);
if (!result.ok) {
invalid.push({ key, type: result.type });
// We don't do anything with validated secrets so we don't store them
} else if (options.access === 'public') {
valid.push({ key, value: result.value, type: result.type, context: options.context });
}
}

View file

@ -1,77 +1,90 @@
import assert from 'node:assert/strict';
import { writeFileSync } from 'node:fs';
import { after, describe, it } from 'node:test';
import { afterEach, describe, it } from 'node:test';
import * as cheerio from 'cheerio';
import testAdapter from './test-adapter.js';
import { loadFixture } from './test-utils.js';
describe('astro:env public variables', () => {
describe('astro:env secret variables', () => {
/** @type {Awaited<ReturnType<typeof loadFixture>>} */
let fixture;
/** @type {Awaited<ReturnType<(typeof fixture)["loadTestAdapterApp"]>>} */
let app;
/** @type {Awaited<ReturnType<(typeof fixture)["startDevServer"]>>} */
/** @type {Awaited<ReturnType<(typeof fixture)["startDevServer"]>> | undefined} */
let devServer = undefined;
describe('Server variables', () => {
after(async () => {
await devServer?.stop();
afterEach(async () => {
await devServer?.stop();
if (process.env.KNOWN_SECRET) {
delete process.env.KNOWN_SECRET
}
});
it('works in dev', async () => {
process.env.KNOWN_SECRET = '5'
fixture = await loadFixture({
root: './fixtures/astro-env-server-secret/',
});
devServer = await fixture.startDevServer();
const response = await fixture.fetch('/');
assert.equal(response.status, 200);
});
it('builds without throwing', async () => {
fixture = await loadFixture({
root: './fixtures/astro-env-server-secret/',
output: 'server',
adapter: testAdapter({
env: {
KNOWN_SECRET: '123456',
UNKNOWN_SECRET: 'abc',
},
}),
});
await fixture.build();
assert.equal(true, true);
});
it('adapter can set how env is retrieved', async () => {
fixture = await loadFixture({
root: './fixtures/astro-env-server-secret/',
output: 'server',
adapter: testAdapter({
env: {
KNOWN_SECRET: '123456',
UNKNOWN_SECRET: 'abc',
},
}),
});
await fixture.build();
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/');
const response = await app.render(request);
assert.equal(response.status, 200);
const html = await response.text();
const $ = cheerio.load(html);
const data = JSON.parse($('#data').text());
assert.equal(data.KNOWN_SECRET, 123456);
assert.equal(data.UNKNOWN_SECRET, 'abc');
});
it('fails if validateSecrets is enabled and secret is not set', async () => {
fixture = await loadFixture({
root: './fixtures/astro-env-server-secret/',
experimental: {
env: {
validateSecrets: true,
},
},
});
it('works in dev', async () => {
writeFileSync(
new URL('./fixtures/astro-env-server-secret/.env', import.meta.url),
'KNOWN_SECRET=5',
'utf-8'
);
fixture = await loadFixture({
root: './fixtures/astro-env-server-secret/',
});
devServer = await fixture.startDevServer();
const response = await fixture.fetch('/');
assert.equal(response.status, 200);
});
it('builds without throwing', async () => {
fixture = await loadFixture({
root: './fixtures/astro-env-server-secret/',
output: 'server',
adapter: testAdapter({
env: {
KNOWN_SECRET: '123456',
UNKNOWN_SECRET: 'abc',
},
}),
});
try {
await fixture.build();
app = await fixture.loadTestAdapterApp();
assert.equal(true, true);
});
it('adapter can set how env is retrieved', async () => {
fixture = await loadFixture({
root: './fixtures/astro-env-server-secret/',
output: 'server',
adapter: testAdapter({
env: {
KNOWN_SECRET: '123456',
UNKNOWN_SECRET: 'abc',
},
}),
});
await fixture.build();
app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/');
const response = await app.render(request);
assert.equal(response.status, 200);
const html = await response.text();
const $ = cheerio.load(html);
const data = JSON.parse($('#data').text());
assert.equal(data.KNOWN_SECRET, 123456);
assert.equal(data.UNKNOWN_SECRET, 'abc');
});
assert.fail()
} catch (error) {
assert.equal(error instanceof Error, true);
assert.equal(error.title, 'Invalid Environment Variables');
assert.equal(error.message.includes('Variable KNOWN_SECRET is not of type: number.'), true);
}
});
});