Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
27 changes: 27 additions & 0 deletions src/env.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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-'));
Expand Down
29 changes: 17 additions & 12 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>();
// 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<string>();

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 {
Expand All @@ -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.',
);
}

Expand All @@ -49,5 +54,5 @@ export function loadSecrets(options: LoadSecretsOptions): Secrets {

// Test-only escape hatch.
export function _resetDotenvLoaded(): void {
loadedDotenvPaths.clear();
loadedWorkspaceRoots.clear();
}