@@ -8,6 +8,7 @@ import { getInstallCommand, getRunCommand, detectPkgManager } from '../framework
88import { RSYNC_DEFAULT_EXCLUDES } from '../../shared/constants.js' ;
99import { DeployError } from '../../shared/errors.js' ;
1010import type { DeploymentStrategy , StrategyContext } from './strategy.js' ;
11+ import { runWithDotenv } from './dotenv.js' ;
1112
1213function escapeSingleQuotes ( s : string ) : string {
1314 return s . replace ( / ' / g, "\\'" ) ;
@@ -25,20 +26,6 @@ function parseCommand(command: string | undefined, pkgManager: string): { script
2526 return { script : parts [ 0 ] , args : parts . slice ( 1 ) . join ( ' ' ) } ;
2627}
2728
28- /**
29- * Shell snippet that sources the workDir-local `.env` into the current shell
30- * so subsequent && commands inherit the variables. Returns empty string when
31- * the project has no envFile so callers can interpolate it unconditionally.
32- *
33- * Used during install, build, and post-symlink relink. The PM2 wrapper takes
34- * the same approach (see generateAppBlock) but sources `<shared>/<envFile>`
35- * directly — it doesn't depend on the workDir symlink.
36- */
37- function sourceEnvCommand ( envFile : string | undefined ) : string {
38- if ( ! envFile ) return '' ;
39- return `set -a && . ./.env && set +a` ;
40- }
41-
4229export class BackendStrategy implements DeploymentStrategy {
4330 readonly name = 'backend' ;
4431
@@ -98,11 +85,6 @@ export class BackendStrategy implements DeploymentStrategy {
9885 `{ echo ${ shellSingleQuote ( missingEnvMessage ) } >&2; exit 1; }` ,
9986 ) ;
10087 commands . push ( `ln -sf "${ this . appPath } /shared/${ this . app . envFile } " .env` ) ;
101- // Source it so install/build see env vars (private-registry tokens in
102- // `.npmrc` via `${TOKEN}`, build-time secrets, etc.). Affects this shell
103- // chain only; the PM2 wrapper sources independently at process start.
104- const sourceCmd = sourceEnvCommand ( this . app . envFile ) ;
105- if ( sourceCmd ) commands . push ( sourceCmd ) ;
10688 }
10789
10890 // Ensure third-party package managers are available on the remote
@@ -114,10 +96,10 @@ export class BackendStrategy implements DeploymentStrategy {
11496 commands . push ( `command -v bun &>/dev/null || npm install -g bun` ) ;
11597 }
11698
117- commands . push ( installCmd ) ;
99+ const envDependentCommands = [ installCmd ] ;
118100
119101 if ( ! ctx . skipBuild ) {
120- commands . push ( `if [ -f package.json ] && jq -e '.scripts.build' package.json >/dev/null 2>&1; then ${ runCmd } build; fi` ) ;
102+ envDependentCommands . push ( `if [ -f package.json ] && jq -e '.scripts.build' package.json >/dev/null 2>&1; then ${ runCmd } build; fi` ) ;
121103 }
122104
123105 // Symlink `.env` into compiled-output directories so frameworks whose env
@@ -128,7 +110,9 @@ export class BackendStrategy implements DeploymentStrategy {
128110 // 3. obvious monorepo layouts: `apps/*/build`, `packages/*/build`, dist twins
129111 // We never traverse into node_modules and never overwrite an existing
130112 // `.env` file.
131- commands . push ( this . getEnvSymlinkCommand ( ctx . workDir ) ) ;
113+ envDependentCommands . push ( this . getEnvSymlinkCommand ( ctx . workDir ) ) ;
114+
115+ commands . push ( runWithDotenv ( this . app . envFile ? '.env' : undefined , envDependentCommands . join ( ' && ' ) ) ) ;
132116
133117 const installResult = await ctx . executor . exec ( commands . join ( ' && ' ) ) ;
134118 this . assertNoBuildScriptsIgnored ( pkgManager , installResult ) ;
@@ -311,12 +295,8 @@ export class BackendStrategy implements DeploymentStrategy {
311295 ) : Promise < void > {
312296 const baseInstall = this . workspace . installCommand ?? getInstallCommand ( pkgManager ) ;
313297 const relinkInstall = this . workspace . installCommand ? baseInstall : `${ baseInstall } --prefer-offline` ;
314- // Source env before relinking — private-registry tokens in `.npmrc` use
315- // env-var interpolation.
316- const sourceCmd = sourceEnvCommand ( this . app . envFile ) ;
317- const sourceStep = sourceCmd ? `${ sourceCmd } && ` : '' ;
318298 const installResult = await ctx . executor . exec (
319- `cd "${ cdPath } " && ${ mise } && ${ sourceStep } ${ relinkInstall } ` ,
299+ `cd "${ cdPath } " && ${ mise } && ${ runWithDotenv ( this . app . envFile ? '.env' : undefined , relinkInstall ) } ` ,
320300 ) ;
321301 this . assertNoBuildScriptsIgnored ( pkgManager , installResult ) ;
322302 if ( installResult . exitCode !== 0 ) {
@@ -393,16 +373,9 @@ ${appBlocks.join(',\n')}
393373
394374 if ( useWrapper ) {
395375 const tail = origArgs ? `${ origScript } ${ origArgs } ` : origScript ;
396- // Sourcing the dotenv file happens inside the PM2-launched shell, so it
397- // can overwrite inline PM2 values such as the blue-green port. Re-apply
398- // the ecosystem's explicit values afterwards to preserve their normal
399- // precedence over the shared environment.
400- const explicitEnv = Object . entries ( env )
401- . map ( ( [ key , value ] ) => `${ key } =${ shellSingleQuote ( String ( value ) ) } ` )
402- . join ( ' ' ) ;
403- const inner = `set -a && . '${ escapeSingleQuotes ( envFilePath ) } ' && set +a && export ${ explicitEnv } && exec ${ tail } ` ;
376+ const runner = runWithDotenv ( envFilePath , `exec ${ tail } ` , env ) ;
404377 scriptLine = `script: 'bash',` ;
405- argsLine = `\n args: ['-c', '${ escapeSingleQuotes ( inner ) } '],` ;
378+ argsLine = `\n args: ['-c', '${ escapeSingleQuotes ( runner ) } '],` ;
406379 } else {
407380 scriptLine = `script: '${ escapeSingleQuotes ( origScript ) } ',` ;
408381 argsLine = origArgs ? `\n args: '${ escapeSingleQuotes ( origArgs ) } ',` : '' ;
0 commit comments