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
27 changes: 27 additions & 0 deletions packages/targets/deploy-firebase/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,33 @@ describe('Firebase deployment target', () => {
});
});

it('rejects invalid Firebase config before plan or CLI work', async () => {
await expect(adapter.build(fakeBuildContext() as any, {
projectId: 'bad/project',
})).rejects.toThrow('projectId must contain only letters');

await expect(adapter.ship(fakeShipContext({
dryRun: true,
}) as any, {
projectId: 'my-firebase-project',
only: ['hosting,functions'],
})).rejects.toThrow('only[0] must not contain commas');

await expect(adapter.ship(fakeShipContext({
dryRun: true,
}) as any, {
projectId: 'my-firebase-project',
only: [' '],
})).rejects.toThrow('deploy-firebase requires only[0]');

await expect(adapter.ship(fakeShipContext({
dryRun: true,
}) as any, {
projectId: 'my-firebase-project',
message: ' ',
})).rejects.toThrow('deploy-firebase requires message');
});

it('requires a vault token for real deployments', async () => {
await expect(adapter.ship(fakeShipContext({
dryRun: false,
Expand Down
41 changes: 41 additions & 0 deletions packages/targets/deploy-firebase/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,50 @@ interface Config {
message?: string;
}

function requireText(value: string | undefined, field: string): string {
const text = value?.trim();
if (!text) throw new Error(`deploy-firebase requires ${field}`);
return text;
}

function optionalText(value: string | undefined, field: string): string | undefined {
return value === undefined ? undefined : requireText(value, field);
}

function projectId(value: string | undefined): string {
const id = requireText(value, 'projectId');
if (!/^[A-Za-z0-9._-]+$/.test(id)) {
throw new Error('deploy-firebase projectId must contain only letters, numbers, dots, underscores, or hyphens');
}
Comment on lines +24 to +26

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The projectId regex ^[A-Za-z0-9._-]+$ is more permissive than Firebase's actual constraints. Real Firebase project IDs allow only lowercase letters, digits, and hyphens (6–30 characters, must start with a letter). This means values like 'My_Project', 'proj.name', or 'AB' all pass this guard but are rejected by the Firebase CLI — potentially surfacing a confusing CLI error rather than the clear validation message added here.

Suggested change
if (!/^[A-Za-z0-9._-]+$/.test(id)) {
throw new Error('deploy-firebase projectId must contain only letters, numbers, dots, underscores, or hyphens');
}
if (!/^[a-z][a-z0-9-]{5,29}$/.test(id)) {
throw new Error('deploy-firebase projectId must be 6–30 lowercase letters, digits, or hyphens, and start with a letter');
}

return id;
}

function deployTargets(value: string[] | undefined): string[] | undefined {
return value?.map((target, index) => {
const text = requireText(target, `only[${index}]`);
if (text.includes(',')) throw new Error(`deploy-firebase only[${index}] must not contain commas`);
return text;
});
}

function normalizedConfig(config: Config): Config {
return {
...config,
projectId: projectId(config.projectId),
only: deployTargets(config.only),
config: optionalText(config.config, 'config'),
message: optionalText(config.message, 'message'),
};
}

function configPath(ctx: { projectDir: string }, config: Config): string | undefined {
config = normalizedConfig(config);
if (!config.config) return undefined;
return isAbsolute(config.config) ? config.config : join(ctx.projectDir, config.config);
}

function deployArgs(ctx: { projectDir: string }, config: Config, token?: string): string[] {
config = normalizedConfig(config);
const args = ['--yes', 'firebase-tools', 'deploy', '--project', config.projectId, '--json'];
if (config.only?.length) args.push('--only', config.only.join(','));
const firebaseConfig = configPath(ctx, config);
Expand All @@ -25,6 +63,7 @@ function deployArgs(ctx: { projectDir: string }, config: Config, token?: string)
}

function renderPlan(ctx: { projectDir: string; version: string }, config: Config): string {
config = normalizedConfig(config);
return `${JSON.stringify({
provider: 'firebase',
projectId: config.projectId,
Expand Down Expand Up @@ -52,13 +91,15 @@ export default defineTarget<Config>({
kind: 'web',
label: 'Firebase Hosting / Functions',
async build(ctx, config) {
config = normalizedConfig(config);
const planPath = join(ctx.outDir, 'firebase-deploy.json');
ctx.log('firebase emulators:exec --only hosting,functions');
await mkdir(ctx.outDir, { recursive: true });
await writeFile(planPath, renderPlan(ctx, config), 'utf-8');
return { artifact: planPath };
},
async ship(ctx, config) {
config = normalizedConfig(config);
const only = config.only?.length ? ` --only ${config.only.join(',')}` : '';
ctx.log(`firebase deploy --project ${config.projectId}${only}`);
if (ctx.dryRun) return { id: 'dry-run', meta: { command: ['npx', ...deployArgs(ctx, config)] } };
Expand Down
Loading