mirror of
https://github.com/withastro/astro.git
synced 2025-04-14 23:51:49 -05:00
feat: astro:env validateSecrets (#11337)
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
75d118bf7f
commit
0a4b31ffeb
5 changed files with 150 additions and 70 deletions
.changeset
packages/astro
23
.changeset/slow-roses-call.md
Normal file
23
.changeset/slow-roses-call.md
Normal 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
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
23
packages/astro/src/env/vite-plugin-env.ts
vendored
23
packages/astro/src/env/vite-plugin-env.ts
vendored
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue