From 2b8ddcd2996d72e4222009139d3f6ff729f273dc Mon Sep 17 00:00:00 2001 From: agarciar Date: Thu, 4 Jun 2026 17:58:00 +0200 Subject: [PATCH] feat: fall back to .env when .env.local is absent If .env.local does not exist at the workspace root, the credentials loader now tries .env before giving up. .env.local still wins when both files are present. Bumps to 0.1.2. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 10 +++++++++- package.json | 2 +- src/env.spec.ts | 27 +++++++++++++++++++++++++++ src/env.ts | 29 +++++++++++++++++------------ 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8c439e..ed4e255 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.1.2] — 2026-06-04 + +### Changed + +- Credentials loader now falls back to `.env` when `.env.local` is absent. + `.env.local` still takes precedence when both files exist. + ## [0.1.1] — 2026-05-25 ### Fixed @@ -31,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `dryRun` mode and a `purgeAfterUpload` toggle. - Credentials read from environment variables or a `.env.local` file. -[Unreleased]: https://github.com/lostium/angular-deploy-bunny/compare/v0.1.1...HEAD +[Unreleased]: https://github.com/lostium/angular-deploy-bunny/compare/v0.1.2...HEAD +[0.1.2]: https://github.com/lostium/angular-deploy-bunny/compare/v0.1.1...v0.1.2 [0.1.1]: https://github.com/lostium/angular-deploy-bunny/compare/v0.1.0...v0.1.1 [0.1.0]: https://github.com/lostium/angular-deploy-bunny/releases/tag/v0.1.0 diff --git a/package.json b/package.json index 468bd7e..22a9e18 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-deploy-bunny", - "version": "0.1.1", + "version": "0.1.2", "description": "Angular Architect builder (ng deploy) that syncs your build output to a Bunny.net CDN Storage Zone with SHA256-based incremental diffing, then purges the Pull Zone.", "keywords": [ "angular", diff --git a/src/env.spec.ts b/src/env.spec.ts index 564cca5..a1485bb 100644 --- a/src/env.spec.ts +++ b/src/env.spec.ts @@ -51,6 +51,33 @@ describe('loadSecrets', () => { ); }); + it('falls back to .env when .env.local does not exist', () => { + const dir = mkdtempSync(join(tmpdir(), 'bunny-env-fallback-')); + writeFileSync(join(dir, '.env'), 'BUNNY_STORAGE_PASSWORD=fromEnv\n'); + try { + expect(loadSecrets({ requireAccountApiKey: false, workspaceRoot: dir })).toEqual({ + storagePassword: 'fromEnv', + accountApiKey: null, + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it('.env.local takes precedence over .env when both exist', () => { + const dir = mkdtempSync(join(tmpdir(), 'bunny-env-precedence-')); + writeFileSync(join(dir, '.env'), 'BUNNY_STORAGE_PASSWORD=fromEnv\n'); + writeFileSync(join(dir, '.env.local'), 'BUNNY_STORAGE_PASSWORD=fromLocal\n'); + try { + expect(loadSecrets({ requireAccountApiKey: false, workspaceRoot: dir })).toEqual({ + storagePassword: 'fromLocal', + accountApiKey: null, + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + it('loads .env.local for each distinct workspaceRoot, not only the first', () => { const dirA = mkdtempSync(join(tmpdir(), 'bunny-env-a-')); // no .env.local const dirB = mkdtempSync(join(tmpdir(), 'bunny-env-b-')); diff --git a/src/env.ts b/src/env.ts index 891c2bf..89b2921 100644 --- a/src/env.ts +++ b/src/env.ts @@ -13,18 +13,23 @@ export interface LoadSecretsOptions { workspaceRoot?: string; } -// Keyed by the resolved .env.local path so each distinct workspace root loads -// its own file once. A single boolean would latch on the first root and skip -// every later one (breaks multi-project workspaces deploying in one process). -const loadedDotenvPaths = new Set(); +// Keyed by the resolved workspace root so each distinct root loads its own +// file once. A single boolean would latch on the first root and skip every +// later one (breaks multi-project workspaces deploying in one process). +const loadedWorkspaceRoots = new Set(); function ensureDotenv(workspaceRoot: string): void { - const path = resolve(workspaceRoot, '.env.local'); - if (loadedDotenvPaths.has(path)) return; - if (existsSync(path)) { - loadDotenv({ path }); + if (loadedWorkspaceRoots.has(workspaceRoot)) return; + const envLocal = resolve(workspaceRoot, '.env.local'); + if (existsSync(envLocal)) { + loadDotenv({ path: envLocal }); + } else { + const envFile = resolve(workspaceRoot, '.env'); + if (existsSync(envFile)) { + loadDotenv({ path: envFile }); + } } - loadedDotenvPaths.add(path); + loadedWorkspaceRoots.add(workspaceRoot); } export function loadSecrets(options: LoadSecretsOptions): Secrets { @@ -33,14 +38,14 @@ export function loadSecrets(options: LoadSecretsOptions): Secrets { const storagePassword = process.env['BUNNY_STORAGE_PASSWORD']; if (!storagePassword) { throw new Error( - 'Missing BUNNY_STORAGE_PASSWORD. Set it in your shell or in .env.local at the repo root.', + 'Missing BUNNY_STORAGE_PASSWORD. Set it in your shell, in .env.local, or in .env at the repo root.', ); } const accountApiKey = process.env['BUNNY_ACCOUNT_API_KEY'] ?? null; if (options.requireAccountApiKey && !accountApiKey) { throw new Error( - 'Missing BUNNY_ACCOUNT_API_KEY (required when purgeAfterUpload is true). Set it in your shell or in .env.local at the repo root.', + 'Missing BUNNY_ACCOUNT_API_KEY (required when purgeAfterUpload is true). Set it in your shell, in .env.local, or in .env at the repo root.', ); } @@ -49,5 +54,5 @@ export function loadSecrets(options: LoadSecretsOptions): Secrets { // Test-only escape hatch. export function _resetDotenvLoaded(): void { - loadedDotenvPaths.clear(); + loadedWorkspaceRoots.clear(); }