Skip to content

Commit b1aa641

Browse files
committed
refactor(deploy): orchestrator iterates over config.apps with per-app paths (sprint 2d)
1 parent 8f0b09b commit b1aa641

14 files changed

Lines changed: 236 additions & 283 deletions

src/cli/commands/deploy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ export async function cmdDeploy(cwd: string, options: { dryRun?: boolean; skipBu
2222
ui.banner();
2323
ui.step(`Deploying ${chalk.bold(getDeploymentName(config) ?? getActiveApp(config).appType)}${config.ssh.user}@${config.ssh.host}`);
2424

25-
const deployer = new DeployService(new LoggingExecutor(executor), config, cwd);
26-
await deployer.execute(options.skipBuild ?? false);
25+
const deployer = new DeployService(new LoggingExecutor(executor), config);
26+
await deployer.execute(cwd, options.skipBuild ?? false);
2727

2828
const lines = [
2929
`host ${config.ssh.user}@${config.ssh.host}`,

src/cli/commands/rollback.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ export async function cmdRollback(
1414
cwd,
1515
async ({ config, executor }) => {
1616
const app = getActiveApp(config);
17-
const releases = new ReleaseManager(executor, config.remotePath, app.keepReleases);
17+
const appPath = `${config.remotePath}/${app.name}`;
18+
const releases = new ReleaseManager(executor, appPath, app.keepReleases);
1819
const stepsBack = options.steps ?? 1;
1920

2021
ui.info('Fetching release history...');
@@ -43,7 +44,7 @@ export async function cmdRollback(
4344
return;
4445
}
4546

46-
const targetPath = `${config.remotePath}/releases/${target.timestamp}`;
47+
const targetPath = `${appPath}/releases/${target.timestamp}`;
4748
await releases.switchSymlink(targetPath);
4849
ui.success('Symlink switched');
4950

@@ -54,7 +55,7 @@ export async function cmdRollback(
5455
// Prefer reloading from the rolled-back release's ecosystem file (ADR-0001 — it
5556
// restores the exact process set that was active for that release). Fall back to
5657
// namespace reload if the target release predates per-release ecosystem files.
57-
const ecosystem = getEcosystemPath(config);
58+
const ecosystem = getEcosystemPath(config, app.name);
5859
await executor.execOrThrow(
5960
`${mise}; mise exec node@${nodeVersion} -- ` +
6061
`(pm2 reload "${ecosystem}" --update-env 2>/dev/null || pm2 reload ${namespace} --update-env)`,
@@ -65,7 +66,7 @@ export async function cmdRollback(
6566
if (app.appType === 'backend' && app.healthCheck.enabled) {
6667
ui.info('Running health check...');
6768
const health = new HealthCheckService(executor, config);
68-
await health.perform();
69+
await health.perform(app);
6970
ui.success('Health check passed');
7071
}
7172

src/domain/deploy/backend-strategy.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import { execa } from 'execa';
22
import { pathExists } from 'fs-extra';
33
import { resolve } from 'path';
44
import type { ShipnodeConfig, ShipnodeApp, Pm2App } from '../../shared/types.js';
5-
import { getDeploymentName, getPm2Name } from '../pm2/apps.js';
6-
import { getActiveApp } from '../workspace.js';
5+
import { getPm2Name } from '../pm2/apps.js';
76
import { getInstallCommand, getRunCommand, detectPkgManager } from '../framework/detector.js';
87
import { RSYNC_DEFAULT_EXCLUDES } from '../../shared/constants.js';
98
import { DeployError } from '../../shared/errors.js';
@@ -39,12 +38,13 @@ export class BackendStrategy implements DeploymentStrategy {
3938
readonly name = 'backend';
4039

4140
constructor(
42-
private config: ShipnodeConfig,
41+
private workspace: ShipnodeConfig,
42+
private app: ShipnodeApp,
4343
private cwd: string,
4444
) {}
4545

46-
private get app(): ShipnodeApp {
47-
return getActiveApp(this.config);
46+
private get appPath(): string {
47+
return `${this.workspace.remotePath}/${this.app.name}`;
4848
}
4949

5050
async stage(ctx: StrategyContext): Promise<void> {
@@ -55,25 +55,25 @@ export class BackendStrategy implements DeploymentStrategy {
5555
const args = [
5656
'-avz',
5757
'--progress',
58-
'-e', `ssh -p ${this.config.ssh.port}`,
58+
'-e', `ssh -p ${this.workspace.ssh.port}`,
5959
...excludes.flatMap((e) => ['--exclude', e]),
6060
...(hasIgnoreFile ? ['--exclude-from', ignoreFile] : []),
6161
`${this.cwd}/`,
62-
`${this.config.ssh.user}@${this.config.ssh.host}:${ctx.workDir}/`,
62+
`${this.workspace.ssh.user}@${this.workspace.ssh.host}:${ctx.workDir}/`,
6363
];
6464

6565
await execa('rsync', args, { stdio: 'inherit' });
6666
}
6767

6868
async setupEnvironment(ctx: StrategyContext): Promise<void> {
6969
const pkgManager = await this.resolvePkgManager();
70-
const installCmd = this.config.installCommand ?? getInstallCommand(pkgManager);
70+
const installCmd = this.workspace.installCommand ?? getInstallCommand(pkgManager);
7171
const runCmd = getRunCommand(pkgManager);
7272

7373
const commands = [
7474
`cd "${ctx.workDir}"`,
7575
`export PATH="$HOME/.local/bin:$HOME/.local/share/mise/shims:$PATH"`,
76-
`mise use -y "node@${this.config.nodeVersion}"`,
76+
`mise use -y "node@${this.workspace.nodeVersion}"`,
7777
`mise install -y`,
7878
];
7979

@@ -84,7 +84,7 @@ export class BackendStrategy implements DeploymentStrategy {
8484
if (this.app.envFile) {
8585
// Use the configured env filename in the shared path; the local workDir
8686
// alias stays `.env` (the well-known name framework loaders look for).
87-
commands.push(`ln -sf "${this.config.remotePath}/shared/${this.app.envFile}" .env`);
87+
commands.push(`ln -sf "${this.appPath}/shared/${this.app.envFile}" .env`);
8888
// Source it so install/build see env vars (private-registry tokens in
8989
// `.npmrc` via `${TOKEN}`, build-time secrets, etc.). Affects this shell
9090
// chain only; the PM2 wrapper sources independently at process start.
@@ -133,21 +133,21 @@ export class BackendStrategy implements DeploymentStrategy {
133133
// Ecosystem lives inside the release directory (per-release snapshot, ADR-0001).
134134
// PM2 references it via the `current` symlink so it always resolves to the active release.
135135
const ecosystemWritePath = `${ctx.workDir}/ecosystem.config.cjs`;
136-
const ecosystemRuntimePath = `${this.config.remotePath}/current/ecosystem.config.cjs`;
136+
const ecosystemRuntimePath = `${this.appPath}/current/ecosystem.config.cjs`;
137137

138138
const escaped = ecosystemContent.replace(/'/g, "'\"'\"'");
139139
await ctx.executor.execOrThrow(`echo '${escaped}' > "${ecosystemWritePath}"`);
140140

141-
const cdPath = `${this.config.remotePath}/current`;
141+
const cdPath = `${this.appPath}/current`;
142142
const mise = `export PATH="$HOME/.local/bin:$HOME/.local/share/mise/shims:$PATH"`;
143143

144144
// Re-run install from the final directory so the pkg manager's module
145145
// resolution state matches the path PM2 will use. Packages are already
146146
// in the local store so this is a fast offline relink, not a download.
147147
// If the user supplied a custom installCommand we use it verbatim — they've
148148
// chosen their flags and appending --prefer-offline would compose poorly.
149-
const baseInstall = this.config.installCommand ?? getInstallCommand(pkgManager);
150-
const relinkInstall = this.config.installCommand ? baseInstall : `${baseInstall} --prefer-offline`;
149+
const baseInstall = this.workspace.installCommand ?? getInstallCommand(pkgManager);
150+
const relinkInstall = this.workspace.installCommand ? baseInstall : `${baseInstall} --prefer-offline`;
151151
// Source env before relinking too — same reason as setupEnvironment:
152152
// private-registry tokens in `.npmrc` use env-var interpolation.
153153
const sourceCmd = sourceEnvCommand(this.app.envFile);
@@ -184,8 +184,8 @@ export class BackendStrategy implements DeploymentStrategy {
184184
private generateEcosystemFile(pkgManager: string): string {
185185
if (!this.app.pm2) return '';
186186

187-
const namespace = getDeploymentName(this.config) ?? this.app.pm2.apps[0].name;
188-
const envFilePath = `${this.config.remotePath}/shared/${this.app.envFile}`;
187+
const namespace = this.app.pm2.apps[0].name;
188+
const envFilePath = `${this.appPath}/shared/${this.app.envFile}`;
189189
const appBlocks = this.app.pm2.apps.map((app) => this.generateAppBlock(app, pkgManager, namespace, envFilePath));
190190

191191
return `module.exports = {
@@ -237,7 +237,7 @@ ${appBlocks.join(',\n')}
237237
// resolves require paths against the app, not the workspace root.
238238
// Install/build still run at the workspace root.
239239
const cwdLine = this.app.appRoot
240-
? `\n cwd: '${escapeSingleQuotes(`${this.config.remotePath}/current/${this.app.appRoot}`)}',`
240+
? `\n cwd: '${escapeSingleQuotes(`${this.appPath}/current/${this.app.appRoot}`)}',`
241241
: '';
242242

243243
return ` {
@@ -278,14 +278,14 @@ ${envLines}
278278

279279
if (this.app.sharedDirs) {
280280
for (const dir of this.app.sharedDirs) {
281-
commands.push(`mkdir -p "${this.config.remotePath}/shared/${dir}"`);
282-
commands.push(`ln -sfn "${this.config.remotePath}/shared/${dir}" "${workDir}/${dir}"`);
281+
commands.push(`mkdir -p "${this.appPath}/shared/${dir}"`);
282+
commands.push(`ln -sfn "${this.appPath}/shared/${dir}" "${workDir}/${dir}"`);
283283
}
284284
}
285285

286286
if (this.app.sharedFiles) {
287287
for (const file of this.app.sharedFiles) {
288-
commands.push(`ln -sf "${this.config.remotePath}/shared/${file}" "${workDir}/${file}"`);
288+
commands.push(`ln -sf "${this.appPath}/shared/${file}" "${workDir}/${file}"`);
289289
}
290290
}
291291

@@ -307,7 +307,7 @@ ${envLines}
307307
}
308308

309309
private async resolvePkgManager() {
310-
if (this.config.pkgManager) return this.config.pkgManager;
310+
if (this.workspace.pkgManager) return this.workspace.pkgManager;
311311
const detected = await detectPkgManager(this.cwd);
312312
return detected ?? 'npm';
313313
}

src/domain/deploy/frontend-strategy.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,18 @@ import { execa } from 'execa';
22
import { pathExists } from 'fs-extra';
33
import { resolve } from 'path';
44
import type { ShipnodeConfig, ShipnodeApp } from '../../shared/types.js';
5-
import { getActiveApp } from '../workspace.js';
65
import { getRunCommand, detectPkgManager } from '../framework/detector.js';
76
import type { DeploymentStrategy, StrategyContext } from './strategy.js';
87

98
export class FrontendStrategy implements DeploymentStrategy {
109
readonly name = 'frontend';
1110

1211
constructor(
13-
private config: ShipnodeConfig,
12+
private workspace: ShipnodeConfig,
13+
private app: ShipnodeApp,
1414
private cwd: string,
1515
) {}
1616

17-
private get app(): ShipnodeApp {
18-
return getActiveApp(this.config);
19-
}
20-
2117
async stage(ctx: StrategyContext): Promise<void> {
2218
if (!ctx.skipBuild) {
2319
await this.buildFrontend();
@@ -31,14 +27,14 @@ export class FrontendStrategy implements DeploymentStrategy {
3127
'-avz',
3228
'--progress',
3329
'--delete',
34-
'-e', `ssh -p ${this.config.ssh.port}`,
30+
'-e', `ssh -p ${this.workspace.ssh.port}`,
3531
'--exclude', 'shared/',
3632
'--exclude', '.shipnode/',
3733
'--exclude', 'releases/',
3834
'--exclude', 'current',
3935
...(hasIgnoreFile ? ['--exclude-from', ignoreFile] : []),
4036
`${this.cwd}/${buildDir}/`,
41-
`${this.config.ssh.user}@${this.config.ssh.host}:${ctx.workDir}/`,
37+
`${this.workspace.ssh.user}@${this.workspace.ssh.host}:${ctx.workDir}/`,
4238
], { stdio: 'inherit' });
4339
}
4440

@@ -64,7 +60,7 @@ export class FrontendStrategy implements DeploymentStrategy {
6460
}
6561

6662
private async resolvePkgManager() {
67-
if (this.config.pkgManager) return this.config.pkgManager;
63+
if (this.workspace.pkgManager) return this.workspace.pkgManager;
6864
const detected = await detectPkgManager(this.cwd);
6965
return detected ?? 'npm';
7066
}

0 commit comments

Comments
 (0)