Skip to content

Commit 90d1dbf

Browse files
committed
fix(nx-heroku): improve exec error handling
1 parent 8deb5d8 commit 90d1dbf

File tree

12 files changed

+130
-54
lines changed

12 files changed

+130
-54
lines changed

packages/nx-heroku/src/executors/common/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const HEROKU_BUILDPACK_APT = 'heroku-community/apt';
1414
export const HEROKU_ENV_VARIABLES_PREFIX = 'HD_';
1515
export const STATIC_JSON = 'static.json';
1616
export const APTFILE = 'Aptfile';
17+
export const PROCFILE = 'Procfile';
1718
export const DEFAULT_GIT_USERNAME = 'Heroku-Deploy';
1819
export const ASCII_COLORS_REGEX =
1920
// eslint-disable-next-line no-control-regex

packages/nx-heroku/src/executors/common/git.ts

+37-10
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,51 @@
11
import { DEFAULT_GIT_USERNAME } from './constants';
2+
import { ConsoleLogger } from './logger';
23
import { exec } from './utils';
34

4-
export async function getGitUserName(): Promise<string> {
5-
const { stdout, stderr } = await exec(`git config user.name`, {
6-
encoding: 'utf-8',
7-
});
8-
if (stderr) {
9-
throw new Error(stderr);
5+
export async function getGitUserParam(
6+
key: 'email' | 'name',
7+
defaultValue?: string
8+
): Promise<string> {
9+
try {
10+
const { stdout, stderr } = await exec(`git config user.${key}`, {
11+
encoding: 'utf-8',
12+
});
13+
if (stderr) {
14+
throw new Error(stderr);
15+
}
16+
return stdout?.trim() || defaultValue;
17+
} catch (e) {
18+
ConsoleLogger.error(e);
19+
return defaultValue;
1020
}
11-
return stdout?.trim() || DEFAULT_GIT_USERNAME;
1221
}
1322

14-
export async function getGitEmail(): Promise<string> {
15-
const { stdout, stderr } = await exec(`git config user.email`, {
23+
export function getGitUserName(): Promise<string> {
24+
return getGitUserParam('name', DEFAULT_GIT_USERNAME);
25+
}
26+
27+
export function getGitEmail(): Promise<string> {
28+
return getGitUserParam('email', '');
29+
}
30+
31+
export async function setGitUserParam(
32+
key: 'name' | 'email',
33+
value: string
34+
): Promise<void> {
35+
const { stderr } = await exec(`git config user.${key} "${value}"`, {
1636
encoding: 'utf-8',
1737
});
1838
if (stderr) {
1939
throw new Error(stderr);
2040
}
21-
return stdout?.trim() || '';
41+
}
42+
43+
export function setGitEmail(email: string): Promise<void> {
44+
return setGitUserParam('email', email);
45+
}
46+
47+
export function setGitUserName(name: string): Promise<void> {
48+
return setGitUserParam('name', name);
2249
}
2350

2451
export async function getGitLocalBranchName(): Promise<string> {

packages/nx-heroku/src/executors/common/heroku/apps.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { logger } from '@nrwl/devkit';
2+
import { ExecException } from 'child_process';
23

34
import {
45
Environment,
@@ -111,6 +112,8 @@ export async function appExists(options: {
111112
}
112113
return true;
113114
} catch (e) {
115+
const ex = e as ExecException;
116+
logger.warn(ex.message);
114117
return false;
115118
}
116119
}
@@ -141,7 +144,8 @@ export async function createAppRemote(options: {
141144
});
142145
} catch (error) {
143146
// TODO: catch error when gitconfig could not be locked that occurs during parallel deployment
144-
if (error.toString().includes("Couldn't find that app")) {
147+
const ex = error as ExecException;
148+
if (ex.toString().includes("Couldn't find that app")) {
145149
throw new HerokuError(`Couldn't find that app. ${error.toString()}`);
146150
}
147151
throw error;

packages/nx-heroku/src/executors/common/heroku/auth.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export async function removeCatFile(): Promise<void> {
3030
try {
3131
await rm(HEROKU_AUTH_FILE);
3232
} catch (error) {
33-
if (error.code === 'ENOENT') {
33+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
3434
return;
3535
}
3636
throw error;

packages/nx-heroku/src/executors/common/logger.ts

+34-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { logger } from '@nrwl/devkit';
2+
import type { ExecException } from 'child_process';
23
import Container, { Constructable } from 'typedi';
34

45
export interface LoggerInterface {
@@ -9,9 +10,35 @@ export interface LoggerInterface {
910
error(message: string | Error | unknown): void;
1011
}
1112

13+
function isExecException(error: unknown): error is ExecException {
14+
return (error as ExecException).code !== undefined;
15+
}
16+
1217
export class ConsoleLogger implements LoggerInterface {
1318
private _debug = false;
1419

20+
static log(message: string) {
21+
logger.log(message);
22+
}
23+
24+
static info(message: string) {
25+
logger.info(message);
26+
}
27+
28+
static warn(message: string | unknown) {
29+
logger.warn(message);
30+
}
31+
32+
static error(error: string | Error | unknown) {
33+
if (error instanceof Error) {
34+
logger.error(error.message);
35+
}
36+
if (isExecException(error)) {
37+
logger.error(`code: ${error.code}`);
38+
}
39+
logger.error(error);
40+
}
41+
1542
get debug() {
1643
return this._debug;
1744
}
@@ -21,19 +48,21 @@ export class ConsoleLogger implements LoggerInterface {
2148
}
2249

2350
log(message: string) {
24-
this.debug && logger.log(message);
51+
this.debug && ConsoleLogger.log(message);
2552
}
2653

2754
info(message: string) {
28-
this.debug && logger.info(message);
55+
this.debug && ConsoleLogger.info(message);
2956
}
3057

3158
warn(message: string | unknown) {
32-
this.debug && logger.warn(message);
59+
this.debug && ConsoleLogger.warn(message);
3360
}
3461

35-
error(message: string | Error | unknown) {
36-
this.debug && logger.error(message);
62+
error(error: string | Error | unknown) {
63+
if (this.debug) {
64+
ConsoleLogger.error(error);
65+
}
3766
}
3867
}
3968

packages/nx-heroku/src/executors/deploy/executor.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const options: DeployExecutorSchema = {
3636
},
3737
useForce: true,
3838
watchDelay: 0,
39-
debug: true,
39+
debug: false,
4040
};
4141

4242
class MockHerokuAppService extends HerokuAppService {

packages/nx-heroku/src/executors/deploy/executor.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import 'reflect-metadata';
22

3-
import { ExecutorContext, logger } from '@nrwl/devkit';
3+
import { type ExecutorContext } from '@nrwl/devkit';
44
import { Container } from 'typedi';
55

66
import { EXECUTOR_CONTEXT } from '../common/constants';
@@ -18,10 +18,10 @@ export default async function herokuDeployment(
1818
const herokuDeployService = Container.get(HerokuDeployService);
1919
try {
2020
await herokuDeployService.run();
21-
logger.info('Deployment successful.');
21+
herokuDeployService.logger.info('Deployment successful.');
2222
return { success: true };
23-
} catch (err) {
24-
logger.error(err);
23+
} catch (error) {
24+
herokuDeployService.logger.error(error);
2525
return { success: false };
2626
} finally {
2727
await herokuDeployService.close();

packages/nx-heroku/src/executors/deploy/services/heroku-app.service.ts

+29-18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
/* eslint-disable max-lines */
22
import axios from 'axios';
3-
import { ChildProcessWithoutNullStreams, spawn } from 'child_process';
3+
import {
4+
ChildProcessWithoutNullStreams,
5+
ExecException,
6+
spawn,
7+
} from 'child_process';
48
import { readFile, writeFile } from 'fs/promises';
59
import { toNumber } from 'lodash';
610
import { join } from 'path';
@@ -10,6 +14,7 @@ import {
1014
APTFILE,
1115
HEROKU_BUILDPACK_APT,
1216
HEROKU_BUILDPACK_STATIC,
17+
PROCFILE,
1318
STATIC_JSON,
1419
} from '../../common/constants';
1520
import { getGitLocalBranchName, getGitRemoteBranch } from '../../common/git';
@@ -95,23 +100,32 @@ class HerokuApp {
95100
public logger: LoggerInterface
96101
) {}
97102

103+
private async addAndCommit(
104+
projectName: string,
105+
pattern: string
106+
): Promise<void> {
107+
try {
108+
//? allow custom commit message
109+
await exec(
110+
`git add ${pattern} && git commit -m "ci(${projectName}): add ${pattern}" -n --no-gpg-sign`
111+
);
112+
this.logger.info(`Wrote ${pattern} with custom configuration.`);
113+
} catch (error) {
114+
const ex = error as ExecException;
115+
// there is (probably) nothing to commit
116+
this.logger.warn(ex.message);
117+
this.logger.warn(ex.code?.toString());
118+
}
119+
}
98120
/*
99121
* @description create Procfile from options
100122
*/
101123
private async createProcfile(): Promise<void> {
102124
const { procfile, projectName } = this.options;
103125
if (procfile) {
104-
const procfilePath = `apps/${projectName}/Procfile`;
126+
const procfilePath = `apps/${projectName}/${PROCFILE}`;
105127
await writeFile(join(process.cwd(), procfilePath), procfile);
106-
try {
107-
//? allow custom commit message
108-
await exec(
109-
`git add ${procfilePath} && git commit -m "ci(${projectName}): add Procfile" -n --no-gpg-sign`
110-
);
111-
this.logger.info('Written Procfile with custom configuration');
112-
} catch (err) {
113-
// there is nothing to commit
114-
}
128+
await this.addAndCommit(projectName, PROCFILE);
115129
}
116130
}
117131

@@ -121,6 +135,7 @@ class HerokuApp {
121135
): Promise<void> {
122136
const { buildPacks, projectName } = this.options;
123137
if (buildPacks.includes(buildPackName)) {
138+
// TODO: check nxConfig.appsDir
124139
const srcPath = join(
125140
process.cwd(),
126141
`apps/${projectName}/${buildPackFile}`
@@ -132,11 +147,7 @@ class HerokuApp {
132147
if (destBuildPackFile === srcBuildPackFile) return;
133148

134149
await writeFile(destPath, srcBuildPackFile);
135-
//? allow for custom commit message
136-
await exec(
137-
`git add ${buildPackFile} && git commit -m "ci(${projectName}): add ${buildPackFile}" -n --no-gpg-sign`
138-
);
139-
this.logger.info(`Written ${buildPackFile} with custom configuration`);
150+
await this.addAndCommit(projectName, buildPackFile);
140151
}
141152
}
142153

@@ -409,8 +420,8 @@ class HerokuApp {
409420
throw new Error('Failed to match the `checkString`');
410421
}
411422
this.logger.info(body);
412-
} catch (err) {
413-
this.logger.warn(err.message);
423+
} catch (error) {
424+
this.logger.warn(error.message);
414425
await this.healthcheckFailed();
415426
}
416427
}

packages/nx-heroku/src/executors/deploy/services/heroku-deploy.service.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import type { ExecutorContext } from '@nrwl/devkit';
22
import { Inject, Service } from 'typedi';
33

44
import { Environment, EXECUTOR_CONTEXT } from '../../common/constants';
5-
import { getGitEmail, getGitUserName } from '../../common/git';
5+
import {
6+
getGitEmail,
7+
getGitUserName,
8+
setGitEmail,
9+
setGitUserName,
10+
} from '../../common/git';
611
import { getAppName, getRemoteName } from '../../common/heroku';
712
import { HerokuBaseService } from '../../common/heroku/base.service';
813
import { Logger, LoggerInterface } from '../../common/logger';
@@ -100,12 +105,11 @@ export class HerokuDeployService extends HerokuBaseService<DeployExecutorSchema>
100105

101106
async close(): Promise<void> {
102107
try {
108+
await this.tearDownHerokuAuth();
103109
this.previousGitConfig.name &&
104-
(await exec(`git config user.name "${this.previousGitConfig.name}"`));
110+
(await setGitUserName(this.previousGitConfig.name));
105111
this.previousGitConfig.email &&
106-
(await exec(`git config user.email "${this.previousGitConfig.email}"`));
107-
108-
await this.tearDownHerokuAuth();
112+
(await setGitEmail(this.previousGitConfig.email));
109113
} catch (error) {
110114
this.logger.error(error);
111115
}

packages/nx-heroku/src/executors/dyno/executor.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const options: Omit<DynoExecutorSchema, 'command'> = {
1111
config: 'development',
1212
apiKey: 'heroku-user-api-key',
1313
email: 'heroku-user-email',
14-
debug: true,
14+
debug: false,
1515
};
1616

1717
class MockHerokuDynoService extends HerokuDynoService {

packages/nx-heroku/src/executors/dyno/executor.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import 'reflect-metadata';
22

3-
import { ExecutorContext, logger } from '@nrwl/devkit';
3+
import { type ExecutorContext } from '@nrwl/devkit';
44
import Container from 'typedi';
55

66
import { EXECUTOR_CONTEXT } from '../common/constants';
@@ -19,10 +19,10 @@ export default async function runExecutor(
1919

2020
try {
2121
await herokuDynoService.run();
22-
logger.info(`Dyno ${options.command} successful.`);
22+
herokuDynoService.logger.info(`Dyno ${options.command} successful.`);
2323
return { success: true };
24-
} catch (err) {
25-
logger.error(err);
24+
} catch (error) {
25+
herokuDynoService.logger.error(error);
2626
return { success: false };
2727
} finally {
2828
await herokuDynoService.close();

packages/nx-heroku/src/executors/promote/executor.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import 'reflect-metadata';
22

3-
import { ExecutorContext, logger } from '@nrwl/devkit';
3+
import type { ExecutorContext } from '@nrwl/devkit';
44
import Container from 'typedi';
55

66
import { EXECUTOR_CONTEXT } from '../common/constants';
@@ -18,10 +18,10 @@ export default async function runExecutor(
1818
const herokuPromoteService = Container.get(HerokuPromoteService);
1919
try {
2020
await herokuPromoteService.run();
21-
logger.info('Promotion successful.');
21+
herokuPromoteService.logger.info('Promotion successful.');
2222
return { success: true };
23-
} catch (err) {
24-
logger.error(err);
23+
} catch (error) {
24+
herokuPromoteService.logger.error(error);
2525
return { success: false };
2626
} finally {
2727
await herokuPromoteService.close();

0 commit comments

Comments
 (0)