@akash-chowdhury-24/deployhub 2.0.0 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,93 @@
1
+ import { execa } from 'execa';
1
2
  import { createSshProvider } from './ssh.js';
3
+ import { createLogger } from '../../logger/index.js';
2
4
 
3
- export function createEc2Provider(config, envName, env) {
4
- return createSshProvider(config, envName, env);
5
+ /**
6
+ * @param {import('../../core/config.js').DeployHubConfig} config
7
+ * @param {string} envName
8
+ * @param {Record<string, string>} [env]
9
+ */
10
+ export function createEc2Provider(config, envName, env = process.env) {
11
+ const log = createLogger('ec2');
12
+ const instanceId = env.EC2_INSTANCE_ID;
13
+ const region = env.AWS_REGION || 'us-east-1';
14
+
15
+ async function resolveHost() {
16
+ const environment = config.environments[envName];
17
+ if (environment?.host || env.SSH_HOST) {
18
+ return environment?.host || env.SSH_HOST;
19
+ }
20
+
21
+ if (!instanceId) {
22
+ throw new Error(
23
+ 'EC2 host unknown. Set SSH_HOST to your instance public IP, or set EC2_INSTANCE_ID with AWS credentials for auto lookup.'
24
+ );
25
+ }
26
+
27
+ log.info(`Looking up public IP for instance ${instanceId}...`);
28
+
29
+ /** @type {Record<string, string>} */
30
+ const awsEnv = { ...process.env };
31
+ if (env.AWS_ACCESS_KEY_ID) awsEnv.AWS_ACCESS_KEY_ID = env.AWS_ACCESS_KEY_ID;
32
+ if (env.AWS_SECRET_ACCESS_KEY) awsEnv.AWS_SECRET_ACCESS_KEY = env.AWS_SECRET_ACCESS_KEY;
33
+ awsEnv.AWS_DEFAULT_REGION = region;
34
+
35
+ try {
36
+ const { stdout } = await execa(
37
+ 'aws',
38
+ [
39
+ 'ec2',
40
+ 'describe-instances',
41
+ '--instance-ids',
42
+ instanceId,
43
+ '--query',
44
+ 'Reservations[0].Instances[0].PublicIpAddress',
45
+ '--output',
46
+ 'text',
47
+ '--region',
48
+ region,
49
+ ],
50
+ { stdio: 'pipe', env: awsEnv }
51
+ );
52
+ const publicIp = stdout.trim();
53
+ if (!publicIp || publicIp === 'None') {
54
+ throw new Error('No public IP returned');
55
+ }
56
+ log.info(`Resolved EC2 host: ${publicIp}`);
57
+ return publicIp;
58
+ } catch (err) {
59
+ const msg = err instanceof Error ? err.message : String(err);
60
+ throw new Error(
61
+ `Could not resolve public IP for ${instanceId} — ${msg}. Set SSH_HOST manually or install/configure AWS CLI with ec2:DescribeInstances access.`
62
+ );
63
+ }
64
+ }
65
+
66
+ const sshProvider = createSshProvider(config, envName, env);
67
+
68
+ async function connect() {
69
+ const host = await resolveHost();
70
+ const environment = config.environments[envName];
71
+ if (environment && !environment.host) {
72
+ environment.host = host;
73
+ }
74
+ if (!env.SSH_HOST) {
75
+ env.SSH_HOST = host;
76
+ }
77
+ return sshProvider.connect();
78
+ }
79
+
80
+ return {
81
+ ...sshProvider,
82
+ connect,
83
+ deploy: sshProvider.deploy.bind(sshProvider),
84
+ rollback: sshProvider.rollback.bind(sshProvider),
85
+ healthCheck: sshProvider.healthCheck.bind(sshProvider),
86
+ testConnection: async () => {
87
+ const ssh = await connect();
88
+ ssh.dispose();
89
+ },
90
+ };
5
91
  }
6
92
 
7
93
  export default { createEc2Provider };
@@ -1,7 +1,94 @@
1
+ import { execa } from 'execa';
1
2
  import { createSshProvider } from './ssh.js';
3
+ import { createLogger } from '../../logger/index.js';
2
4
 
3
- export function createGcpVmProvider(config, envName, env) {
4
- return createSshProvider(config, envName, env);
5
+ /**
6
+ * @param {import('../../core/config.js').DeployHubConfig} config
7
+ * @param {string} envName
8
+ * @param {Record<string, string>} [env]
9
+ */
10
+ export function createGcpVmProvider(config, envName, env = process.env) {
11
+ const log = createLogger('gcp-vm');
12
+ const projectId = env.GCP_PROJECT_ID;
13
+ const zone = env.GCP_ZONE;
14
+ const instanceName = env.GCP_INSTANCE_NAME;
15
+
16
+ async function resolveHost() {
17
+ const environment = config.environments[envName];
18
+ if (environment?.host || env.SSH_HOST) {
19
+ return environment?.host || env.SSH_HOST;
20
+ }
21
+
22
+ if (!projectId || !zone || !instanceName) {
23
+ throw new Error(
24
+ 'GCP VM host unknown. Set SSH_HOST to your instance external IP, or set GCP_PROJECT_ID, GCP_ZONE, and GCP_INSTANCE_NAME for auto lookup.'
25
+ );
26
+ }
27
+
28
+ log.info(`Looking up external IP for GCP instance ${instanceName}...`);
29
+
30
+ /** @type {Record<string, string>} */
31
+ const gcpEnv = { ...process.env };
32
+ if (env.GCP_KEY_FILE) {
33
+ gcpEnv.GOOGLE_APPLICATION_CREDENTIALS = env.GCP_KEY_FILE;
34
+ }
35
+
36
+ try {
37
+ const { stdout } = await execa(
38
+ 'gcloud',
39
+ [
40
+ 'compute',
41
+ 'instances',
42
+ 'describe',
43
+ instanceName,
44
+ '--zone',
45
+ zone,
46
+ '--project',
47
+ projectId,
48
+ '--format',
49
+ 'get(networkInterfaces[0].accessConfigs[0].natIP)',
50
+ ],
51
+ { stdio: 'pipe', env: gcpEnv }
52
+ );
53
+ const publicIp = stdout.trim();
54
+ if (!publicIp) {
55
+ throw new Error('No external IP returned');
56
+ }
57
+ log.info(`Resolved GCP VM host: ${publicIp}`);
58
+ return publicIp;
59
+ } catch (err) {
60
+ const msg = err instanceof Error ? err.message : String(err);
61
+ throw new Error(
62
+ `Could not resolve external IP for ${instanceName} — ${msg}. Set SSH_HOST manually or run gcloud auth login and verify project/zone/instance name.`
63
+ );
64
+ }
65
+ }
66
+
67
+ const sshProvider = createSshProvider(config, envName, env);
68
+
69
+ async function connect() {
70
+ const host = await resolveHost();
71
+ const environment = config.environments[envName];
72
+ if (environment && !environment.host) {
73
+ environment.host = host;
74
+ }
75
+ if (!env.SSH_HOST) {
76
+ env.SSH_HOST = host;
77
+ }
78
+ return sshProvider.connect();
79
+ }
80
+
81
+ return {
82
+ ...sshProvider,
83
+ connect,
84
+ deploy: sshProvider.deploy.bind(sshProvider),
85
+ rollback: sshProvider.rollback.bind(sshProvider),
86
+ healthCheck: sshProvider.healthCheck.bind(sshProvider),
87
+ testConnection: async () => {
88
+ const ssh = await connect();
89
+ ssh.dispose();
90
+ },
91
+ };
5
92
  }
6
93
 
7
94
  export default { createGcpVmProvider };
@@ -1,27 +1,124 @@
1
1
  import { execa } from 'execa';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import os from 'os';
2
5
  import { createLogger } from '../../logger/index.js';
3
6
 
4
- export function createKubernetesProvider(config, envName) {
7
+ /**
8
+ * @param {import('../../core/config.js').DeployHubConfig} config
9
+ * @param {string} envName
10
+ * @param {Record<string, string>} [env]
11
+ */
12
+ export function createKubernetesProvider(config, envName, env = process.env) {
5
13
  const log = createLogger('kubernetes');
6
14
 
15
+ const kubeconfig = env.KUBECONFIG || path.join(os.homedir(), '.kube', 'config');
16
+ const context = env.KUBE_CONTEXT || '';
17
+ const namespace = env.KUBE_NAMESPACE || config.project || 'default';
18
+
19
+ function getKubectlEnv() {
20
+ const expanded = kubeconfig.replace(/^~/, os.homedir());
21
+ return { ...process.env, KUBECONFIG: path.resolve(expanded) };
22
+ }
23
+
24
+ /**
25
+ * @param {string[]} baseArgs
26
+ */
27
+ function kubectlArgs(baseArgs) {
28
+ /** @type {string[]} */
29
+ const args = [...baseArgs];
30
+ if (context) args.push('--context', context);
31
+ if (namespace) args.push('--namespace', namespace);
32
+ return args;
33
+ }
34
+
35
+ /**
36
+ * @param {string} artifactDir
37
+ */
7
38
  async function deploy(artifactDir) {
8
- log.info('Deploying to Kubernetes...');
9
- await execa('kubectl', ['apply', '-f', '.'], {
10
- cwd: artifactDir,
39
+ log.info(`Deploying to Kubernetes (namespace: ${namespace}${context ? `, context: ${context}` : ''})...`);
40
+
41
+ const manifestDir = artifactDir;
42
+ const hasManifests =
43
+ (await fs.pathExists(path.join(manifestDir, 'k8s'))) ||
44
+ (await fs.readdir(manifestDir)).some((f) => /\.ya?ml$/.test(f));
45
+
46
+ if (!hasManifests) {
47
+ throw new Error(
48
+ 'No Kubernetes manifests found in artifact. Add .yaml files or a k8s/ directory to your project.'
49
+ );
50
+ }
51
+
52
+ const applyTarget = (await fs.pathExists(path.join(manifestDir, 'k8s')))
53
+ ? path.join(manifestDir, 'k8s')
54
+ : manifestDir;
55
+
56
+ await execa('kubectl', kubectlArgs(['apply', '-f', applyTarget]), {
11
57
  stdio: 'inherit',
58
+ env: getKubectlEnv(),
12
59
  });
60
+
61
+ const imageName = env.DOCKER_IMAGE_NAME;
62
+ const imageTag = env.DOCKER_IMAGE_TAG || config.version || 'latest';
63
+ if (imageName && config.project) {
64
+ await execa(
65
+ 'kubectl',
66
+ kubectlArgs([
67
+ 'set',
68
+ 'image',
69
+ `deployment/${config.project}`,
70
+ `${config.project}=${imageName}:${imageTag}`,
71
+ ]),
72
+ { stdio: 'pipe', env: getKubectlEnv() }
73
+ ).catch(() => {
74
+ log.warn('kubectl set image skipped (deployment name may differ from project name)');
75
+ });
76
+ }
77
+
78
+ log.success('Kubernetes deployment complete');
13
79
  }
14
80
 
15
81
  async function rollback(artifactDir) {
16
- await deploy(artifactDir);
82
+ log.info('Rolling back Kubernetes deployment...');
83
+ await execa('kubectl', kubectlArgs(['rollout', 'undo', `deployment/${config.project}`]), {
84
+ stdio: 'inherit',
85
+ env: getKubectlEnv(),
86
+ }).catch(async () => {
87
+ log.warn('kubectl rollout undo failed — redeploying previous artifact instead');
88
+ await deploy(artifactDir);
89
+ });
17
90
  }
18
91
 
19
92
  async function healthCheck() {
20
- return true;
93
+ const url = config.healthCheck?.url;
94
+ if (url) {
95
+ try {
96
+ const { stdout } = await execa('curl', ['-sf', '-o', '/dev/null', '-w', '%{http_code}', url], {
97
+ stdio: 'pipe',
98
+ });
99
+ return stdout.trim().startsWith('2');
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
104
+
105
+ try {
106
+ await execa(
107
+ 'kubectl',
108
+ kubectlArgs(['rollout', 'status', `deployment/${config.project}`, '--timeout=30s']),
109
+ { stdio: 'pipe', env: getKubectlEnv() }
110
+ );
111
+ return true;
112
+ } catch {
113
+ return false;
114
+ }
21
115
  }
22
116
 
23
117
  async function testConnection() {
24
- await execa('kubectl', ['cluster-info'], { stdio: 'pipe' });
118
+ await execa('kubectl', kubectlArgs(['cluster-info']), {
119
+ stdio: 'pipe',
120
+ env: getKubectlEnv(),
121
+ });
25
122
  }
26
123
 
27
124
  return { deploy, rollback, healthCheck, testConnection };
@@ -4,6 +4,7 @@ import path from 'path';
4
4
  import os from 'os';
5
5
  import { createLogger } from '../../logger/index.js';
6
6
  import { getNginxSitePath } from '../../utils/nginx.js';
7
+ import { shellQuote, formatRemoteCommandFailure } from '../../utils/shell-quote.js';
7
8
 
8
9
  /** @type {Set<string>} */
9
10
  const NODE_FRAMEWORKS = new Set(['express', 'nestjs', 'fastify', 'koa', 'nextjs', 'node']);
@@ -12,6 +13,8 @@ const PYTHON_FRAMEWORKS = new Set(['fastapi', 'django', 'flask', 'python']);
12
13
  /** @type {Set<string>} */
13
14
  const PHP_FRAMEWORKS = new Set(['laravel', 'symfony', 'php']);
14
15
 
16
+ const sh = shellQuote;
17
+
15
18
  /**
16
19
  * @param {import('../../core/config.js').DeployHubConfig} config
17
20
  * @param {string} envName
@@ -39,27 +42,44 @@ export function createSshProvider(config, envName, env = process.env) {
39
42
  const port = environment.port || config.port || Number(env.SSH_PORT) || 3000;
40
43
  const sshKey = env.SSH_KEY;
41
44
  const keyPath = environment.keyPath || env.SSH_KEY_PATH;
45
+ const sshPort = Number(env.SSH_SSH_PORT) || environment.sshPort || 22;
42
46
 
43
47
  const log = createLogger('ssh');
44
48
 
45
49
  async function connect() {
46
50
  if (!host || !user) {
47
- throw new Error('SSH host and user are required. Set SSH_HOST and SSH_USER in .env');
51
+ throw new Error(
52
+ 'SSH host and user are required. Set SSH_HOST and SSH_USER in .env (see .env.example comments).'
53
+ );
54
+ }
55
+
56
+ if (!sshKey && !keyPath) {
57
+ throw new Error(
58
+ 'SSH authentication required. Set SSH_KEY_PATH (local) or SSH_KEY (CI secret) in .env — see .env.example.'
59
+ );
48
60
  }
49
61
 
50
62
  const ssh = new NodeSSH();
51
63
  /** @type {import('node-ssh').SSHConnectOptions} */
52
- const connectOpts = { host, username: user };
64
+ const connectOpts = { host, username: user, port: sshPort };
53
65
 
54
66
  if (sshKey) {
55
67
  const tmpKeyPath = path.join(os.tmpdir(), 'deployhub-ssh-key');
56
68
  await fs.writeFile(tmpKeyPath, sshKey, { mode: 0o600 });
57
69
  connectOpts.privateKeyPath = tmpKeyPath;
58
70
  } else if (keyPath) {
59
- connectOpts.privateKeyPath = keyPath;
71
+ const expanded = keyPath.replace(/^~/, os.homedir());
72
+ connectOpts.privateKeyPath = path.resolve(expanded);
60
73
  }
61
74
 
62
- await ssh.connect(connectOpts);
75
+ try {
76
+ await ssh.connect(connectOpts);
77
+ } catch (err) {
78
+ const msg = err instanceof Error ? err.message : String(err);
79
+ throw new Error(
80
+ `SSH connection failed to ${user}@${host}:${sshPort} — ${msg}. Check SSH_HOST, SSH_USER, SSH_KEY_PATH, and that port ${sshPort} is open in your firewall/security group.`
81
+ );
82
+ }
63
83
  return ssh;
64
84
  }
65
85
 
@@ -71,7 +91,14 @@ export function createSshProvider(config, envName, env = process.env) {
71
91
  log.info(`$ ${command}`);
72
92
  const result = await ssh.execCommand(command);
73
93
  if (result.code !== 0 && result.code !== null) {
74
- log.warn(`Command exited with code ${result.code}: ${result.stderr || result.stdout}`);
94
+ const message = formatRemoteCommandFailure(
95
+ command,
96
+ result.code,
97
+ result.stderr,
98
+ result.stdout
99
+ );
100
+ log.error(message);
101
+ throw new Error(message);
75
102
  }
76
103
  return result;
77
104
  }
@@ -106,26 +133,27 @@ export function createSshProvider(config, envName, env = process.env) {
106
133
  async function runBackendStartSequence(ssh, targetPath) {
107
134
  const framework = resolveFramework();
108
135
  const startCommand = resolveStartCommand();
136
+ const dir = sh(targetPath);
109
137
 
110
138
  if (NODE_FRAMEWORKS.has(framework)) {
111
- await exec(ssh, `cd ${targetPath} && npm install --production`);
139
+ await exec(ssh, `cd ${dir} && npm install --production`);
112
140
  const start = startCommand || 'npm start';
113
141
  if (start === 'npm start') {
114
142
  await exec(
115
143
  ssh,
116
- `cd ${targetPath} && pm2 restart ${appName} || pm2 start npm --name "${appName}" -- start`
144
+ `cd ${dir} && pm2 restart ${sh(appName)} || pm2 start npm --name ${sh(appName)} -- start`
117
145
  );
118
146
  } else if (start.startsWith('npm run ')) {
119
147
  const script = start.replace('npm run ', '');
120
148
  await exec(
121
149
  ssh,
122
- `cd ${targetPath} && pm2 restart ${appName} || pm2 start npm --name "${appName}" -- run ${script}`
150
+ `cd ${dir} && pm2 restart ${sh(appName)} || pm2 start npm --name ${sh(appName)} -- run ${script}`
123
151
  );
124
152
  } else {
125
153
  const [cmd, ...args] = start.split(' ');
126
154
  await exec(
127
155
  ssh,
128
- `cd ${targetPath} && pm2 restart ${appName} || pm2 start ${cmd} --name "${appName}" -- ${args.join(' ')}`
156
+ `cd ${dir} && pm2 restart ${sh(appName)} || pm2 start ${cmd} --name ${sh(appName)} -- ${args.join(' ')}`
129
157
  );
130
158
  }
131
159
  await exec(ssh, 'pm2 save');
@@ -133,15 +161,15 @@ export function createSshProvider(config, envName, env = process.env) {
133
161
  }
134
162
 
135
163
  if (PYTHON_FRAMEWORKS.has(framework)) {
136
- await exec(ssh, `cd ${targetPath} && pip install -r requirements.txt`);
164
+ await exec(ssh, `cd ${dir} && pip install -r requirements.txt`);
137
165
  if (framework === 'django') {
138
- await exec(ssh, `cd ${targetPath} && python manage.py migrate`);
166
+ await exec(ssh, `cd ${dir} && python manage.py migrate`);
139
167
  }
140
168
  if (framework === 'fastapi') {
141
169
  await exec(ssh, 'pkill uvicorn || true');
142
170
  await exec(
143
171
  ssh,
144
- `cd ${targetPath} && nohup uvicorn main:app --host 0.0.0.0 --port ${port} > app.log 2>&1 &`
172
+ `cd ${dir} && nohup uvicorn main:app --host 0.0.0.0 --port ${port} > app.log 2>&1 &`
145
173
  );
146
174
  } else {
147
175
  await exec(ssh, 'pkill gunicorn || true');
@@ -149,17 +177,17 @@ export function createSshProvider(config, envName, env = process.env) {
149
177
  framework === 'django' ? 'config.wsgi:application' : 'app:app';
150
178
  await exec(
151
179
  ssh,
152
- `cd ${targetPath} && nohup gunicorn ${appTarget} --bind 0.0.0.0:${port} --daemon`
180
+ `cd ${dir} && nohup gunicorn ${appTarget} --bind 0.0.0.0:${port} --daemon`
153
181
  );
154
182
  }
155
183
  return;
156
184
  }
157
185
 
158
186
  if (PHP_FRAMEWORKS.has(framework)) {
159
- await exec(ssh, `cd ${targetPath} && composer install --no-dev`);
187
+ await exec(ssh, `cd ${dir} && composer install --no-dev`);
160
188
  if (framework === 'laravel') {
161
- await exec(ssh, `cd ${targetPath} && php artisan migrate --force`);
162
- await exec(ssh, `cd ${targetPath} && php artisan config:cache`);
189
+ await exec(ssh, `cd ${dir} && php artisan migrate --force`);
190
+ await exec(ssh, `cd ${dir} && php artisan config:cache`);
163
191
  }
164
192
  await exec(ssh, 'sudo systemctl restart php8.2-fpm');
165
193
  await exec(ssh, 'sudo systemctl reload nginx');
@@ -167,47 +195,47 @@ export function createSshProvider(config, envName, env = process.env) {
167
195
  }
168
196
 
169
197
  if (framework === 'spring' || framework === 'java') {
170
- await exec(ssh, `cd ${targetPath} && pkill -f "*.jar" || true`);
198
+ await exec(ssh, `cd ${dir} && pkill -f "*.jar" || true`);
171
199
  await exec(
172
200
  ssh,
173
- `cd ${targetPath} && nohup java -jar target/*.jar > app.log 2>&1 &`
201
+ `cd ${dir} && nohup java -jar target/*.jar > app.log 2>&1 &`
174
202
  );
175
203
  return;
176
204
  }
177
205
 
178
206
  if (framework === 'go') {
179
- await exec(ssh, `cd ${targetPath} && pkill ${appName} || true`);
207
+ await exec(ssh, `cd ${dir} && pkill ${sh(appName)} || true`);
180
208
  await exec(
181
209
  ssh,
182
- `cd ${targetPath} && nohup ./bin/app > app.log 2>&1 &`
210
+ `cd ${dir} && nohup ./bin/app > app.log 2>&1 &`
183
211
  );
184
212
  return;
185
213
  }
186
214
 
187
215
  if (framework === 'dotnet') {
188
- await exec(ssh, `cd ${targetPath} && pkill -f "dotnet" || true`);
216
+ await exec(ssh, `cd ${dir} && pkill -f "dotnet" || true`);
189
217
  const dll = startCommand?.replace('dotnet ', '') || 'App.dll';
190
218
  await exec(
191
219
  ssh,
192
- `cd ${targetPath} && nohup dotnet ${dll} > app.log 2>&1 &`
220
+ `cd ${dir} && nohup dotnet ${dll} > app.log 2>&1 &`
193
221
  );
194
222
  return;
195
223
  }
196
224
 
197
225
  if (framework === 'rails') {
198
- await exec(ssh, `cd ${targetPath} && bundle install --deployment`);
199
- await exec(ssh, `cd ${targetPath} && pkill puma || true`);
226
+ await exec(ssh, `cd ${dir} && bundle install --deployment`);
227
+ await exec(ssh, `cd ${dir} && pkill puma || true`);
200
228
  await exec(
201
229
  ssh,
202
- `cd ${targetPath} && nohup bundle exec puma -p ${port} > app.log 2>&1 &`
230
+ `cd ${dir} && nohup bundle exec puma -p ${port} > app.log 2>&1 &`
203
231
  );
204
232
  return;
205
233
  }
206
234
 
207
- await exec(ssh, `cd ${targetPath} && npm install --production`);
235
+ await exec(ssh, `cd ${dir} && npm install --production`);
208
236
  await exec(
209
237
  ssh,
210
- `cd ${targetPath} && pm2 restart ${appName} || pm2 start npm --name "${appName}" -- start`
238
+ `cd ${dir} && pm2 restart ${sh(appName)} || pm2 start npm --name ${sh(appName)} -- start`
211
239
  );
212
240
  await exec(ssh, 'pm2 save');
213
241
  }
@@ -222,11 +250,11 @@ export function createSshProvider(config, envName, env = process.env) {
222
250
 
223
251
  await exec(
224
252
  ssh,
225
- `sudo cp ${nginxConfRemote} ${sitePath} 2>/dev/null || sudo cp ${targetPath}/nginx.conf ${sitePath}`
253
+ `sudo cp ${sh(nginxConfRemote)} ${sh(sitePath)} 2>/dev/null || sudo cp ${sh(`${targetPath}/nginx.conf`)} ${sh(sitePath)}`
226
254
  );
227
255
  await exec(
228
256
  ssh,
229
- `sudo ln -sf ${sitePath} /etc/nginx/sites-enabled/${path.basename(sitePath)}`
257
+ `sudo ln -sf ${sh(sitePath)} ${sh(`/etc/nginx/sites-enabled/${path.basename(sitePath)}`)}`
230
258
  );
231
259
  await exec(ssh, 'sudo nginx -t');
232
260
  await exec(ssh, 'sudo systemctl reload nginx');
@@ -238,8 +266,8 @@ export function createSshProvider(config, envName, env = process.env) {
238
266
  * @param {string} targetPath
239
267
  */
240
268
  async function extractToPath(ssh, remoteZip, targetPath) {
241
- await exec(ssh, `mkdir -p ${targetPath}`);
242
- await exec(ssh, `unzip -o ${remoteZip} -d ${targetPath}`);
269
+ await exec(ssh, `mkdir -p ${sh(targetPath)}`);
270
+ await exec(ssh, `unzip -o ${sh(remoteZip)} -d ${sh(targetPath)}`);
243
271
  }
244
272
 
245
273
  /**
@@ -259,19 +287,19 @@ export function createSshProvider(config, envName, env = process.env) {
259
287
 
260
288
  if (projectType === 'both') {
261
289
  const remoteStaging = `/tmp/deployhub-staging-${Date.now()}`;
262
- await exec(ssh, `mkdir -p ${remoteStaging}`);
263
- await exec(ssh, `unzip -o ${remoteZip} -d ${remoteStaging}`);
290
+ await exec(ssh, `mkdir -p ${sh(remoteStaging)}`);
291
+ await exec(ssh, `unzip -o ${sh(remoteZip)} -d ${sh(remoteStaging)}`);
264
292
 
265
- await exec(ssh, `mkdir -p ${frontendDeployPath}`);
293
+ await exec(ssh, `mkdir -p ${sh(frontendDeployPath)}`);
266
294
  await exec(
267
295
  ssh,
268
- `rsync -a ${remoteStaging}/ ${frontendDeployPath}/ --exclude backend || cp -r ${remoteStaging}/* ${frontendDeployPath}/`
296
+ `rsync -a ${sh(remoteStaging)}/ ${sh(frontendDeployPath)}/ --exclude backend || cp -r ${sh(remoteStaging)}/* ${sh(frontendDeployPath)}/`
269
297
  );
270
298
 
271
- await exec(ssh, `mkdir -p ${backendDeployPath}`);
299
+ await exec(ssh, `mkdir -p ${sh(backendDeployPath)}`);
272
300
  await exec(
273
301
  ssh,
274
- `rsync -a ${remoteStaging}/backend/ ${backendDeployPath}/ || cp -r ${remoteStaging}/backend/* ${backendDeployPath}/`
302
+ `rsync -a ${sh(remoteStaging)}/backend/ ${sh(backendDeployPath)}/ || cp -r ${sh(remoteStaging)}/backend/* ${sh(backendDeployPath)}/`
275
303
  );
276
304
 
277
305
  if (await remoteFileExists(ssh, `${frontendDeployPath}/nginx.conf`)) {
@@ -279,7 +307,7 @@ export function createSshProvider(config, envName, env = process.env) {
279
307
  }
280
308
 
281
309
  await runBackendStartSequence(ssh, backendDeployPath);
282
- await exec(ssh, `rm -rf ${remoteStaging}`);
310
+ await exec(ssh, `rm -rf ${sh(remoteStaging)}`);
283
311
  } else if (projectType === 'backend') {
284
312
  log.info(`Backend deploy path: ${deployPath}`);
285
313
  await extractToPath(ssh, remoteZip, deployPath);
@@ -296,7 +324,7 @@ export function createSshProvider(config, envName, env = process.env) {
296
324
  }
297
325
  }
298
326
 
299
- await exec(ssh, `rm -f ${remoteZip}`);
327
+ await exec(ssh, `rm -f ${sh(remoteZip)}`);
300
328
  log.success('Deployment complete');
301
329
  } finally {
302
330
  ssh.dispose();
@@ -308,8 +336,8 @@ export function createSshProvider(config, envName, env = process.env) {
308
336
  * @param {string} remotePath
309
337
  */
310
338
  async function remoteFileExists(ssh, remotePath) {
311
- const result = await ssh.execCommand(`test -f ${remotePath} && echo yes`);
312
- return result.stdout.trim() === 'yes';
339
+ const result = await ssh.execCommand(`test -f ${sh(remotePath)} && echo yes`);
340
+ return result.code === 0 && result.stdout.trim() === 'yes';
313
341
  }
314
342
 
315
343
  async function rollback(artifactDir) {
@@ -322,7 +350,7 @@ export function createSshProvider(config, envName, env = process.env) {
322
350
 
323
351
  const ssh = await connect();
324
352
  try {
325
- const result = await ssh.execCommand(`curl -sf -o /dev/null -w "%{http_code}" "${url}"`);
353
+ const result = await ssh.execCommand(`curl -sf -o /dev/null -w "%{http_code}" ${sh(url)}`);
326
354
  return result.stdout.trim().startsWith('2');
327
355
  } finally {
328
356
  ssh.dispose();