deploy-vir 0.0.2 → 0.1.0
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.
- package/README.md +1 -1
- package/bin.js +1 -1
- package/dist/cli/cli.js +1 -0
- package/dist/config.d.ts +1 -1
- package/dist/git.d.ts +1 -1
- package/dist/git.js +4 -3
- package/dist/notifications/slack.js +2 -2
- package/package.json +1 -1
- package/src/cli/cli.script.ts +3 -0
- package/src/cli/cli.ts +81 -0
- package/src/config.ts +110 -0
- package/src/git.ts +142 -0
- package/src/index.ts +7 -0
- package/src/known.error.ts +8 -0
- package/src/notifications/notification-params.ts +19 -0
- package/src/notifications/send-notifications.ts +32 -0
- package/src/notifications/slack.ts +120 -0
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ export default defineDeployVirConfig({
|
|
|
22
22
|
{
|
|
23
23
|
/** This does not need to match the actual repo name. */
|
|
24
24
|
name: 'whatever',
|
|
25
|
-
|
|
25
|
+
gitUrl: 'git@github.com:electrovir/deploy-vir.git',
|
|
26
26
|
deploys: [
|
|
27
27
|
{
|
|
28
28
|
deployName: 'staging',
|
package/bin.js
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
import {runCliScript} from '@augment-vir/node/dist/index.js';
|
|
4
4
|
import {join} from 'node:path';
|
|
5
5
|
|
|
6
|
-
const cliPath = join(import.meta.dirname, 'src', 'cli.script.ts');
|
|
6
|
+
const cliPath = join(import.meta.dirname, 'src', 'cli', 'cli.script.ts');
|
|
7
7
|
|
|
8
8
|
await runCliScript(cliPath, import.meta.filename, 'deploy-vir');
|
package/dist/cli/cli.js
CHANGED
|
@@ -27,6 +27,7 @@ export async function runDeployVir(args, config) {
|
|
|
27
27
|
const repoConfig = assertWrap.isDefined(config.repos.find((repo) => repo.name === args.repoName, `Failed to find a repo config by name '${args.repoName}'`));
|
|
28
28
|
const branchConfig = assertWrap.isDefined(repoConfig.deploys.find((deploy) => deploy.deployName === args.deployName, `Failed to find a deploy named '${args.deployName}' in repo '${args.repoName}'.`));
|
|
29
29
|
const remoteName = await getGitRemoteName(git, repoConfig);
|
|
30
|
+
log.faint(`Remote: ${remoteName}`);
|
|
30
31
|
const deployCommits = await pushDeploy(git, branchConfig, remoteName);
|
|
31
32
|
if (config.notifications?.length) {
|
|
32
33
|
await sendNotifications(config.notifications, {
|
package/dist/config.d.ts
CHANGED
|
@@ -71,7 +71,7 @@ export type DeployVirRepoConfig = {
|
|
|
71
71
|
* - 'git@github.com:electrovir/deploy-vir.git'
|
|
72
72
|
* - 'https://github.com/electrovir/deploy-vir.git'
|
|
73
73
|
*/
|
|
74
|
-
|
|
74
|
+
gitUrl: string;
|
|
75
75
|
/**
|
|
76
76
|
* A url to the base commit path for links. The commit hash will be appended to this for links
|
|
77
77
|
* in notifications. If this is omitted, there simply won't be any links.
|
package/dist/git.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { type DeployVirBranchConfig, type DeployVirRepoConfig } from './config.j
|
|
|
5
5
|
*
|
|
6
6
|
* @category Internal
|
|
7
7
|
*/
|
|
8
|
-
export declare function getGitRemoteName(git: Readonly<SimpleGit>, repoConfig: Readonly<Pick<DeployVirRepoConfig, 'name' | '
|
|
8
|
+
export declare function getGitRemoteName(git: Readonly<SimpleGit>, repoConfig: Readonly<Pick<DeployVirRepoConfig, 'name' | 'gitUrl'>>): Promise<string>;
|
|
9
9
|
/**
|
|
10
10
|
* A git commit.
|
|
11
11
|
*
|
package/dist/git.js
CHANGED
|
@@ -9,9 +9,9 @@ import { KnownError } from './known.error.js';
|
|
|
9
9
|
*/
|
|
10
10
|
export async function getGitRemoteName(git, repoConfig) {
|
|
11
11
|
assert.isTruthy(repoConfig.name, 'Repo config name cannot be empty.');
|
|
12
|
-
assert.isTruthy(repoConfig.
|
|
12
|
+
assert.isTruthy(repoConfig.gitUrl, 'Repo git URL cannot be empty.');
|
|
13
13
|
const remotes = await git.getRemotes(true);
|
|
14
|
-
const remoteMatchByUrl = remotes.find((remote) => remote.refs.fetch === repoConfig.
|
|
14
|
+
const remoteMatchByUrl = remotes.find((remote) => remote.refs.fetch === repoConfig.gitUrl || remote.refs.push === repoConfig.gitUrl);
|
|
15
15
|
if (remoteMatchByUrl) {
|
|
16
16
|
return remoteMatchByUrl.name;
|
|
17
17
|
}
|
|
@@ -22,7 +22,7 @@ export async function getGitRemoteName(git, repoConfig) {
|
|
|
22
22
|
]
|
|
23
23
|
.filter(check.isTruthy)
|
|
24
24
|
.join('-');
|
|
25
|
-
await git.addRemote(newRemoteName, repoConfig.
|
|
25
|
+
await git.addRemote(newRemoteName, repoConfig.gitUrl);
|
|
26
26
|
return newRemoteName;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
@@ -34,6 +34,7 @@ export async function pushDeploy(git, { deployName, fromBranch, toBranch }, remo
|
|
|
34
34
|
assert.isTruthy(fromBranch, `Deploy '${deployName}' fromBranch cannot be empty.`);
|
|
35
35
|
assert.isTruthy(toBranch, `Deploy '${deployName}' toBranch cannot be empty.`);
|
|
36
36
|
assert.isTruthy(remoteName, 'Remote name cannot be empty.');
|
|
37
|
+
log.faint(`Pushing ${remoteName}/${fromBranch} to ${toBranch}`);
|
|
37
38
|
await git.fetch(remoteName, fromBranch);
|
|
38
39
|
const pushString = `${remoteName}/${fromBranch}:${toBranch}`;
|
|
39
40
|
/** Get commits that are on {@link toBranch} but not on {@link fromBranch}. */
|
|
@@ -26,10 +26,10 @@ export async function sendNotificationToSlack({ branchConfig, deployCommits: { d
|
|
|
26
26
|
const deployedCommitBullets = deployedCommits.map((commit) => formatCommit(repoConfig.commitBaseUrl, commit));
|
|
27
27
|
const overwrittenCommitBullets = overwrittenCommits.map((commit) => formatCommit(repoConfig.commitBaseUrl, commit));
|
|
28
28
|
const deployedCommitBulletsText = deployedCommitBullets
|
|
29
|
-
.map((bullet) =>
|
|
29
|
+
.map((bullet) => `- ${bullet}`)
|
|
30
30
|
.join('\n');
|
|
31
31
|
const overwrittenCommitBulletsText = overwrittenCommitBullets.length
|
|
32
|
-
? overwrittenCommitBullets.map((bullet) =>
|
|
32
|
+
? overwrittenCommitBullets.map((bullet) => `- ${bullet}`).join('\n')
|
|
33
33
|
: undefined;
|
|
34
34
|
const blocks = [
|
|
35
35
|
{
|
package/package.json
CHANGED
package/src/cli/cli.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {assertWrap} from '@augment-vir/assert';
|
|
2
|
+
import {extractErrorMessage, log} from '@augment-vir/common';
|
|
3
|
+
import simpleGit from 'simple-git';
|
|
4
|
+
import {type DeployVirConfig} from '../config.js';
|
|
5
|
+
import {getGitRemoteName, pushDeploy} from '../git.js';
|
|
6
|
+
import {KnownError} from '../known.error.js';
|
|
7
|
+
import {sendNotifications} from '../notifications/send-notifications.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Run the deploy-vir CLI.
|
|
11
|
+
*
|
|
12
|
+
* @category Internal
|
|
13
|
+
*/
|
|
14
|
+
export async function runDeployVirCli(args: ReadonlyArray<string>, cwd = process.cwd()) {
|
|
15
|
+
const configPath = assertWrap.isTruthy(args[0], 'Missing config path.');
|
|
16
|
+
const repoName = assertWrap.isTruthy(args[1], 'Missing repo name.');
|
|
17
|
+
const deployName = assertWrap.isTruthy(args[2], 'Missing deploy name.');
|
|
18
|
+
|
|
19
|
+
const config = (await import(configPath)).default;
|
|
20
|
+
|
|
21
|
+
await runDeployVir({cwd, deployName, repoName}, config);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Args for {@link runDeployVir}.
|
|
26
|
+
*
|
|
27
|
+
* @category Internal
|
|
28
|
+
*/
|
|
29
|
+
export type DeployVirArgs = {
|
|
30
|
+
/** This must match a repo name in your config. */
|
|
31
|
+
repoName: string;
|
|
32
|
+
/** This much match a deploy name in your config. */
|
|
33
|
+
deployName: string;
|
|
34
|
+
cwd: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Run a deploy.
|
|
39
|
+
*
|
|
40
|
+
* @category Main
|
|
41
|
+
*/
|
|
42
|
+
export async function runDeployVir(
|
|
43
|
+
args: Readonly<DeployVirArgs>,
|
|
44
|
+
config: Readonly<DeployVirConfig>,
|
|
45
|
+
) {
|
|
46
|
+
try {
|
|
47
|
+
const git = simpleGit(args.cwd);
|
|
48
|
+
|
|
49
|
+
const repoConfig = assertWrap.isDefined(
|
|
50
|
+
config.repos.find(
|
|
51
|
+
(repo) => repo.name === args.repoName,
|
|
52
|
+
`Failed to find a repo config by name '${args.repoName}'`,
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
const branchConfig = assertWrap.isDefined(
|
|
56
|
+
repoConfig.deploys.find(
|
|
57
|
+
(deploy) => deploy.deployName === args.deployName,
|
|
58
|
+
`Failed to find a deploy named '${args.deployName}' in repo '${args.repoName}'.`,
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
const remoteName = await getGitRemoteName(git, repoConfig);
|
|
62
|
+
log.faint(`Remote: ${remoteName}`);
|
|
63
|
+
|
|
64
|
+
const deployCommits = await pushDeploy(git, branchConfig, remoteName);
|
|
65
|
+
|
|
66
|
+
if (config.notifications?.length) {
|
|
67
|
+
await sendNotifications(config.notifications, {
|
|
68
|
+
branchConfig,
|
|
69
|
+
deployCommits,
|
|
70
|
+
repoConfig,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error instanceof KnownError) {
|
|
75
|
+
/** If its a known error, don't log the whole stack trace. */
|
|
76
|
+
log.error(extractErrorMessage(error));
|
|
77
|
+
} else {
|
|
78
|
+
log.error(error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The full deploy-vir config object.
|
|
3
|
+
*
|
|
4
|
+
* @category Config
|
|
5
|
+
*/
|
|
6
|
+
export type DeployVirConfig = {
|
|
7
|
+
repos: DeployVirRepoConfig[];
|
|
8
|
+
notifications?: NotificationConfig[] | undefined;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* All available targets for sending notifications to.
|
|
13
|
+
*
|
|
14
|
+
* @category Internal
|
|
15
|
+
*/
|
|
16
|
+
export enum NotificationTarget {
|
|
17
|
+
Slack = 'slack',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Notification config for Slack.
|
|
22
|
+
*
|
|
23
|
+
* @category Internal
|
|
24
|
+
*/
|
|
25
|
+
export type SlackNotificationConfig = {
|
|
26
|
+
target: NotificationTarget;
|
|
27
|
+
/**
|
|
28
|
+
* Find or create this by going to:
|
|
29
|
+
*
|
|
30
|
+
* Apps and workflows > Custom Integrations > Incoming WebHooks
|
|
31
|
+
*/
|
|
32
|
+
webhookUrl: string;
|
|
33
|
+
/**
|
|
34
|
+
* An emoji string for the Webhook's message. If not provided, the Webhook integration's avatar
|
|
35
|
+
* that you set will be used.
|
|
36
|
+
*/
|
|
37
|
+
avatarEmoji?: string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* A username for the Webhook's message. If not provided, the Webhook integration's avatar that
|
|
40
|
+
* you set will be used.
|
|
41
|
+
*/
|
|
42
|
+
username?: string | undefined;
|
|
43
|
+
/** A channel for the Webhook's message. If not provided, the Webhook integration */
|
|
44
|
+
channelName: string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* By default the notification will list out all commits that are getting deployed. This will
|
|
47
|
+
* turn that off.
|
|
48
|
+
*/
|
|
49
|
+
hideAllCommits?: boolean | undefined;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Config for a notification when a deploy runs.
|
|
54
|
+
*
|
|
55
|
+
* @category Config
|
|
56
|
+
*/
|
|
57
|
+
// eslint-disable-next-line sonarjs/redundant-type-aliases
|
|
58
|
+
export type NotificationConfig = SlackNotificationConfig;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A repo config for deploying.
|
|
62
|
+
*
|
|
63
|
+
* @category Config
|
|
64
|
+
*/
|
|
65
|
+
export type DeployVirRepoConfig = {
|
|
66
|
+
/**
|
|
67
|
+
* Does not need to match the actual repo name in GitHub, it can just be whatever name you want
|
|
68
|
+
* to call it for deploy-vir.
|
|
69
|
+
*/
|
|
70
|
+
name: string;
|
|
71
|
+
/**
|
|
72
|
+
* A git URL to the repo.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
*
|
|
76
|
+
* - 'git@github.com:electrovir/deploy-vir.git'
|
|
77
|
+
* - 'https://github.com/electrovir/deploy-vir.git'
|
|
78
|
+
*/
|
|
79
|
+
gitUrl: string;
|
|
80
|
+
/**
|
|
81
|
+
* A url to the base commit path for links. The commit hash will be appended to this for links
|
|
82
|
+
* in notifications. If this is omitted, there simply won't be any links.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
*
|
|
86
|
+
* - 'https://github.com/electrovir/deploy-vir/commit'
|
|
87
|
+
*/
|
|
88
|
+
commitBaseUrl?: string | undefined;
|
|
89
|
+
deploys: DeployVirBranchConfig[];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A deploy configuration.
|
|
94
|
+
*
|
|
95
|
+
* @category Config
|
|
96
|
+
*/
|
|
97
|
+
export type DeployVirBranchConfig = {
|
|
98
|
+
deployName: string;
|
|
99
|
+
fromBranch: string;
|
|
100
|
+
toBranch: string;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Define a type safe deploy-vir config.
|
|
105
|
+
*
|
|
106
|
+
* @category Internal
|
|
107
|
+
*/
|
|
108
|
+
export function defineDeployVirConfig(config: Readonly<DeployVirConfig>) {
|
|
109
|
+
return config;
|
|
110
|
+
}
|
package/src/git.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {assert, check} from '@augment-vir/assert';
|
|
2
|
+
import {extractErrorMessage, log, logColors} from '@augment-vir/common';
|
|
3
|
+
import {askQuestion} from '@augment-vir/node';
|
|
4
|
+
import {type DefaultLogFields, type ListLogLine, type SimpleGit} from 'simple-git';
|
|
5
|
+
import {type DeployVirBranchConfig, type DeployVirRepoConfig} from './config.js';
|
|
6
|
+
import {KnownError} from './known.error.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Find the git remote's name, or create a new one.
|
|
10
|
+
*
|
|
11
|
+
* @category Internal
|
|
12
|
+
*/
|
|
13
|
+
export async function getGitRemoteName(
|
|
14
|
+
git: Readonly<SimpleGit>,
|
|
15
|
+
repoConfig: Readonly<Pick<DeployVirRepoConfig, 'name' | 'gitUrl'>>,
|
|
16
|
+
): Promise<string> {
|
|
17
|
+
assert.isTruthy(repoConfig.name, 'Repo config name cannot be empty.');
|
|
18
|
+
assert.isTruthy(repoConfig.gitUrl, 'Repo git URL cannot be empty.');
|
|
19
|
+
|
|
20
|
+
const remotes = await git.getRemotes(true);
|
|
21
|
+
|
|
22
|
+
const remoteMatchByUrl = remotes.find(
|
|
23
|
+
(remote) =>
|
|
24
|
+
remote.refs.fetch === repoConfig.gitUrl || remote.refs.push === repoConfig.gitUrl,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (remoteMatchByUrl) {
|
|
28
|
+
return remoteMatchByUrl.name;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const remoteMatchByNameCount: number = remotes.reduce(
|
|
32
|
+
(count, remote) => count + (remote.name === repoConfig.name ? 1 : 0),
|
|
33
|
+
0,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const newRemoteName = [
|
|
37
|
+
repoConfig.name,
|
|
38
|
+
remoteMatchByNameCount,
|
|
39
|
+
]
|
|
40
|
+
.filter(check.isTruthy)
|
|
41
|
+
.join('-');
|
|
42
|
+
|
|
43
|
+
await git.addRemote(newRemoteName, repoConfig.gitUrl);
|
|
44
|
+
|
|
45
|
+
return newRemoteName;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A git commit.
|
|
50
|
+
*
|
|
51
|
+
* @category Internal
|
|
52
|
+
*/
|
|
53
|
+
export type Commit = DefaultLogFields & ListLogLine;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* All commits involved in a deploy.
|
|
57
|
+
*
|
|
58
|
+
* @category Internal
|
|
59
|
+
*/
|
|
60
|
+
export type DeployCommits = {
|
|
61
|
+
deployedCommits: ReadonlyArray<Readonly<Commit>>;
|
|
62
|
+
overwrittenCommits: ReadonlyArray<Readonly<Commit>>;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Push the deploy via git.
|
|
67
|
+
*
|
|
68
|
+
* @category Internal
|
|
69
|
+
*/
|
|
70
|
+
export async function pushDeploy(
|
|
71
|
+
git: Readonly<SimpleGit>,
|
|
72
|
+
{deployName, fromBranch, toBranch}: Readonly<DeployVirBranchConfig>,
|
|
73
|
+
remoteName: string,
|
|
74
|
+
): Promise<DeployCommits> {
|
|
75
|
+
assert.isTruthy(fromBranch, `Deploy '${deployName}' fromBranch cannot be empty.`);
|
|
76
|
+
assert.isTruthy(toBranch, `Deploy '${deployName}' toBranch cannot be empty.`);
|
|
77
|
+
assert.isTruthy(remoteName, 'Remote name cannot be empty.');
|
|
78
|
+
|
|
79
|
+
log.faint(`Pushing ${remoteName}/${fromBranch} to ${toBranch}`);
|
|
80
|
+
|
|
81
|
+
await git.fetch(remoteName, fromBranch);
|
|
82
|
+
|
|
83
|
+
const pushString = `${remoteName}/${fromBranch}:${toBranch}`;
|
|
84
|
+
|
|
85
|
+
/** Get commits that are on {@link toBranch} but not on {@link fromBranch}. */
|
|
86
|
+
const commitsAhead = (
|
|
87
|
+
await git.log({
|
|
88
|
+
from: `${remoteName}/${toBranch}`,
|
|
89
|
+
to: `${remoteName}/${fromBranch}`,
|
|
90
|
+
})
|
|
91
|
+
).all;
|
|
92
|
+
|
|
93
|
+
if (!commitsAhead.length) {
|
|
94
|
+
throw new KnownError('No commit diff: nothing to deploy!');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await git.push(remoteName, pushString);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
deployedCommits: commitsAhead,
|
|
102
|
+
overwrittenCommits: [],
|
|
103
|
+
};
|
|
104
|
+
} catch (error) {
|
|
105
|
+
log.error(`Push failed: ${extractErrorMessage(error)}`);
|
|
106
|
+
|
|
107
|
+
/** Get commits that are on {@link toBranch} but not on {@link fromBranch}. */
|
|
108
|
+
const commitsBehind = await git.log({
|
|
109
|
+
from: `${remoteName}/${fromBranch}`,
|
|
110
|
+
to: `${remoteName}/${toBranch}`,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (commitsBehind.total > 0) {
|
|
114
|
+
log.info(
|
|
115
|
+
`\nThe following ${commitsBehind.total} commit${commitsBehind.total === 1 ? '' : 's'} are on ${logColors.bold}${toBranch}${logColors.reset} but not on ${logColors.bold}${fromBranch}${logColors.reset}:`,
|
|
116
|
+
);
|
|
117
|
+
commitsBehind.all.forEach((commit, index) => {
|
|
118
|
+
log.faint(
|
|
119
|
+
` ${index + 1}. ${commit.hash.slice(0, 7)} (${commit.author_name}) - ${commit.message}`,
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const shouldForcePush = await askQuestion(
|
|
125
|
+
`\nDo you want to force push ${logColors.bold}${fromBranch}${logColors.reset} to ${logColors.bold}${toBranch}${logColors.reset}?\n\n${logColors.warning}This will overwrite the commits listed above.${logColors.reset} (y/N): `,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (shouldForcePush.toLowerCase() === 'y' || shouldForcePush.toLowerCase() === 'yes') {
|
|
129
|
+
await git.push(remoteName, pushString, ['--force']);
|
|
130
|
+
log.warning(
|
|
131
|
+
`Force pushed ${logColors.bold}${fromBranch}${logColors.reset} to ${logColors.bold}${toBranch}${logColors.reset}.`,
|
|
132
|
+
);
|
|
133
|
+
} else {
|
|
134
|
+
throw new KnownError(`Deploy aborted.`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
deployedCommits: commitsAhead,
|
|
139
|
+
overwrittenCommits: commitsBehind.all,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './cli/cli.js';
|
|
2
|
+
export * from './config.js';
|
|
3
|
+
export * from './git.js';
|
|
4
|
+
export * from './known.error.js';
|
|
5
|
+
export * from './notifications/notification-params.js';
|
|
6
|
+
export * from './notifications/send-notifications.js';
|
|
7
|
+
export * from './notifications/slack.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type DeployVirBranchConfig,
|
|
3
|
+
type DeployVirRepoConfig,
|
|
4
|
+
type NotificationConfig,
|
|
5
|
+
type NotificationTarget,
|
|
6
|
+
} from '../config.js';
|
|
7
|
+
import {type DeployCommits} from '../git.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Params for sending a notification.
|
|
11
|
+
*
|
|
12
|
+
* @category Internal
|
|
13
|
+
*/
|
|
14
|
+
export type NotificationParams<Target extends NotificationTarget = NotificationTarget> = {
|
|
15
|
+
deployCommits: Readonly<DeployCommits>;
|
|
16
|
+
branchConfig: Readonly<DeployVirBranchConfig>;
|
|
17
|
+
repoConfig: Readonly<Pick<DeployVirRepoConfig, 'commitBaseUrl'>>;
|
|
18
|
+
notification: Readonly<Extract<NotificationConfig, {target: Target}>>;
|
|
19
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {type NotificationConfig, NotificationTarget} from '../config.js';
|
|
2
|
+
import {KnownError} from '../known.error.js';
|
|
3
|
+
import {type NotificationParams} from './notification-params.js';
|
|
4
|
+
import {sendNotificationToSlack} from './slack.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Send all configured notifications.
|
|
8
|
+
*
|
|
9
|
+
* @category Internal
|
|
10
|
+
*/
|
|
11
|
+
export async function sendNotifications(
|
|
12
|
+
notifications: ReadonlyArray<Readonly<NotificationConfig>>,
|
|
13
|
+
{branchConfig, deployCommits, repoConfig}: Readonly<Omit<NotificationParams, 'notification'>>,
|
|
14
|
+
) {
|
|
15
|
+
await Promise.all(
|
|
16
|
+
notifications.map(async (notification) => {
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
18
|
+
if (notification.target === NotificationTarget.Slack) {
|
|
19
|
+
await sendNotificationToSlack({
|
|
20
|
+
deployCommits,
|
|
21
|
+
branchConfig,
|
|
22
|
+
repoConfig,
|
|
23
|
+
notification,
|
|
24
|
+
});
|
|
25
|
+
} else {
|
|
26
|
+
throw new KnownError(
|
|
27
|
+
`Unexpected notification target: '${String(notification.target)}'`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {addPrefix} from '@augment-vir/common';
|
|
2
|
+
import {type ChatPostMessageArguments} from '@slack/web-api';
|
|
3
|
+
import {joinUrlPaths} from 'url-vir';
|
|
4
|
+
import {type SlackNotificationConfig} from '../config.js';
|
|
5
|
+
import {type Commit} from '../git.js';
|
|
6
|
+
import {type NotificationParams} from './notification-params.js';
|
|
7
|
+
|
|
8
|
+
export {type ChatPostMessageArguments} from '@slack/web-api';
|
|
9
|
+
|
|
10
|
+
function truncateString(value: string, truncateAt = 100): string {
|
|
11
|
+
if (value.length > truncateAt) {
|
|
12
|
+
return `${value.slice(0, Math.max(0, truncateAt))}...`;
|
|
13
|
+
} else {
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatCommit(baseCommitUrl: string | undefined, commit: Readonly<Commit>) {
|
|
19
|
+
const commitLine = `${commit.hash.slice(0, 7)} (${commit.author_name}) - ${truncateString(commit.message)}`;
|
|
20
|
+
|
|
21
|
+
if (baseCommitUrl) {
|
|
22
|
+
return `<${joinUrlPaths(baseCommitUrl, commit.hash)}|${commitLine}>`;
|
|
23
|
+
} else {
|
|
24
|
+
return commitLine;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Send a notification to Slack.
|
|
30
|
+
*
|
|
31
|
+
* @category Internal
|
|
32
|
+
*/
|
|
33
|
+
export async function sendNotificationToSlack({
|
|
34
|
+
branchConfig,
|
|
35
|
+
deployCommits: {deployedCommits, overwrittenCommits},
|
|
36
|
+
notification,
|
|
37
|
+
repoConfig,
|
|
38
|
+
}: Readonly<NotificationParams>) {
|
|
39
|
+
const deployedCommitBullets = deployedCommits.map((commit) =>
|
|
40
|
+
formatCommit(repoConfig.commitBaseUrl, commit),
|
|
41
|
+
);
|
|
42
|
+
const overwrittenCommitBullets = overwrittenCommits.map((commit) =>
|
|
43
|
+
formatCommit(repoConfig.commitBaseUrl, commit),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const deployedCommitBulletsText = deployedCommitBullets
|
|
47
|
+
.map((bullet) => `- ${bullet}`)
|
|
48
|
+
.join('\n');
|
|
49
|
+
|
|
50
|
+
const overwrittenCommitBulletsText = overwrittenCommitBullets.length
|
|
51
|
+
? overwrittenCommitBullets.map((bullet) => `- ${bullet}`).join('\n')
|
|
52
|
+
: undefined;
|
|
53
|
+
|
|
54
|
+
const blocks: any[] = [
|
|
55
|
+
{
|
|
56
|
+
type: 'section',
|
|
57
|
+
text: {
|
|
58
|
+
type: 'mrkdwn',
|
|
59
|
+
text: `*${branchConfig.deployName}* Deployed`,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
type: 'section',
|
|
64
|
+
text: {
|
|
65
|
+
type: 'mrkdwn',
|
|
66
|
+
text: `\n*Deployed Commits*\n${deployedCommitBulletsText}`,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
...(overwrittenCommitBulletsText
|
|
70
|
+
? [
|
|
71
|
+
{
|
|
72
|
+
type: 'section',
|
|
73
|
+
text: {
|
|
74
|
+
type: 'mrkdwn',
|
|
75
|
+
text: `\n*Overwritten Commits*\n${overwrittenCommitBulletsText}`,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
]
|
|
79
|
+
: []),
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
await sendSlackMessage(notification, {
|
|
83
|
+
attachments: [],
|
|
84
|
+
channel: addPrefix({value: notification.channelName, prefix: '#'}),
|
|
85
|
+
blocks,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function sendSlackMessage(
|
|
90
|
+
slackConfig: Readonly<Pick<SlackNotificationConfig, 'avatarEmoji' | 'webhookUrl' | 'username'>>,
|
|
91
|
+
body: Readonly<ChatPostMessageArguments>,
|
|
92
|
+
) {
|
|
93
|
+
try {
|
|
94
|
+
const response = await fetch(slackConfig.webhookUrl, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: {
|
|
97
|
+
'Content-Type': 'application/json',
|
|
98
|
+
},
|
|
99
|
+
body: JSON.stringify({
|
|
100
|
+
...body,
|
|
101
|
+
...(slackConfig.avatarEmoji
|
|
102
|
+
? {
|
|
103
|
+
icon_emoji: slackConfig.avatarEmoji,
|
|
104
|
+
}
|
|
105
|
+
: {}),
|
|
106
|
+
...(slackConfig.username
|
|
107
|
+
? {
|
|
108
|
+
username: slackConfig.username,
|
|
109
|
+
}
|
|
110
|
+
: {}),
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
throw new Error(`Failed to send Slack webhook: ${response.status}`);
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error('Error sending Slack message:', error);
|
|
119
|
+
}
|
|
120
|
+
}
|