Skip to content

Commit 2b32d68

Browse files
authored
✨ 배포 알리미 구현 (#47)
1 parent ad7dfc5 commit 2b32d68

7 files changed

Lines changed: 112 additions & 7 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ jobs:
3737
--build-arg SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }} \
3838
--build-arg SLACK_AUTH_TOKEN=${{ secrets.SLACK_AUTH_TOKEN }} \
3939
--build-arg SLACK_WATCHER_CHANNEL_ID=C050TMDUSTA \
40+
--build-arg DEPLOY_WATCHER_CHANNEL_ID=C06H0PJPDNH \
4041
.
4142
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
4243
echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ FROM node:18.17.1-alpine
22
ARG SLACK_BOT_TOKEN
33
ARG SLACK_AUTH_TOKEN
44
ARG SLACK_WATCHER_CHANNEL_ID
5+
ARG DEPLOY_WATCHER_CHANNEL_ID
56
COPY . .
67
RUN echo "SLACK_BOT_TOKEN=${SLACK_BOT_TOKEN}" >> .env.local
78
RUN echo "SLACK_AUTH_TOKEN=${SLACK_AUTH_TOKEN}" >> .env.local
89
RUN echo "SLACK_WATCHER_CHANNEL_ID=${SLACK_WATCHER_CHANNEL_ID}" >> .env.local
10+
RUN echo "DEPLOY_WATCHER_CHANNEL_ID=${DEPLOY_WATCHER_CHANNEL_ID}" >> .env.local
911
RUN yarn install
1012
RUN yarn build:server
1113
CMD ["yarn", "start:server"]

src/clients/SlackClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export type SlackClient = {
2-
sendMessage: (text: string) => Promise<void>;
2+
sendMessage: (text: string, options?: { ts?: string }) => Promise<{ ts: string }>;
33
};
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { type SlackClient } from '../clients/SlackClient';
2+
import { type GithubDeploymentService } from '../services/GithubDeploymentService';
3+
4+
const identifierToSlackTs: Record<string, string> = {};
5+
6+
export const implementDeploymentService = ({ slackClient }: { slackClient: SlackClient }): GithubDeploymentService => {
7+
return {
8+
handleCreateRelease: async (body) => {
9+
const author: string = body.release.author.login;
10+
const releaseBody: string = body.release.body;
11+
const tag: string = body.release.tag_name;
12+
const releaseUrl: string = body.release.html_url;
13+
const repository = body.repository.name;
14+
15+
const changes = releaseBody.split('\n').reduce<{ content: string; contributor: string }[]>((acc, cur) => {
16+
const match = cur.match(/\* (.*) by @(.*) in (.*)/);
17+
if (!match) return acc;
18+
return [...acc, { content: match[1], contributor: match[2] }];
19+
}, []);
20+
21+
const { ts } = await slackClient.sendMessage(
22+
[
23+
`:rocket: *${repository}* by @${author} (<${releaseUrl}|${tag}>)`,
24+
...changes.map((c) => ` - ${c.content} @${c.contributor}`),
25+
].join('\n'),
26+
);
27+
28+
identifierToSlackTs[toIdentifier({ tag, repository })] = ts;
29+
},
30+
handleActionStart: async (body) => {
31+
const workflowName: string = body.workflow_run.name;
32+
const tag: string = body.workflow_run.head_branch;
33+
const repository: string = body.repository.name;
34+
const workflowId: number = body.workflow_run.id;
35+
const workflowUrl: string = body.workflow_run.html_url;
36+
37+
const isDeploy = workflowName.toLowerCase().includes('deploy');
38+
if (!isDeploy) return;
39+
40+
const ts = identifierToSlackTs[toIdentifier({ tag, repository })];
41+
if (!ts) return;
42+
43+
await slackClient.sendMessage(
44+
[`:wip: deployment started :point_right: <${workflowUrl}|${workflowId}>`].join('\n'),
45+
{ ts },
46+
);
47+
},
48+
handleActionComplete: async (body) => {
49+
const workflowName: string = body.workflow_run.name;
50+
const tag: string = body.workflow_run.head_branch;
51+
const repository: string = body.repository.name;
52+
const workflowId: number = body.workflow_run.id;
53+
const workflowUrl: string = body.workflow_run.html_url;
54+
55+
const isDeploy = workflowName.toLowerCase().includes('deploy');
56+
if (!isDeploy) return;
57+
58+
const ts = identifierToSlackTs[toIdentifier({ tag, repository })];
59+
if (!ts) return;
60+
61+
await slackClient.sendMessage([`:tada: deployment completed <${workflowUrl}|${workflowId}>`].join('\n'), {
62+
ts,
63+
});
64+
65+
delete identifierToSlackTs[toIdentifier({ tag, repository })];
66+
},
67+
};
68+
};
69+
70+
const toIdentifier = ({ tag, repository }: { repository: string; tag: string }) => `${repository}:${tag}`;

src/infrastructures/implementSlackHttpClient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ type Deps = {
66
};
77
export const implementSlackHttpClient = ({ external: { slackAuthToken }, channelId }: Deps): SlackClient => {
88
return {
9-
sendMessage: async (text: string) => {
9+
sendMessage: async (text: string, options) => {
1010
const response = await fetch('https://slack.com/api/chat.postMessage', {
1111
method: 'POST',
1212
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${slackAuthToken}` },
13-
body: JSON.stringify({ channel: channelId, text }),
13+
body: JSON.stringify({ channel: channelId, text, thread_ts: options?.ts }),
1414
});
1515
const data = await response.json();
1616
if (!data.ok) throw data;

src/server.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import dotenv from 'dotenv';
22
import express from 'express';
33

4+
import { implementDeploymentService } from './infrastructures/implementDeploymentService';
45
import { implementSlackEventService } from './infrastructures/implementSlackEventService';
56
import { implementSlackHttpClient } from './infrastructures/implementSlackHttpClient';
67

@@ -9,10 +10,12 @@ dotenv.config({ path: '.env.local' });
910
const slackAuthToken = process.env.SLACK_AUTH_TOKEN;
1011
const slackBotToken = process.env.SLACK_BOT_TOKEN;
1112
const slackWatcherChannelId = process.env.SLACK_WATCHER_CHANNEL_ID;
13+
const deployWatcherChannelId = process.env.DEPLOY_WATCHER_CHANNEL_ID;
1214

1315
if (!slackAuthToken) throw new Error('Missing Slack Auth Token');
1416
if (!slackBotToken) throw new Error('Missing Slack Bot Token');
1517
if (!slackWatcherChannelId) throw new Error('Missing Slack Watcher Channel ID');
18+
if (!deployWatcherChannelId) throw new Error('Missing Deploy Watcher Channel ID');
1619

1720
const PORT = 3000;
1821
const app = express();
@@ -25,11 +28,18 @@ const app = express();
2528
██████╔╝███████╗██║ ███████╗██║ ╚████║██████╔╝███████╗██║ ╚████║╚██████╗██║███████╗███████║
2629
╚═════╝ ╚══════╝╚═╝ ╚══════╝╚═╝ ╚═══╝╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═════╝╚═╝╚══════╝╚══════╝
2730
*/
28-
const slackClient = implementSlackHttpClient({
29-
external: { slackAuthToken },
30-
channelId: slackWatcherChannelId,
31+
const slackService = implementSlackEventService({
32+
slackClient: implementSlackHttpClient({
33+
external: { slackAuthToken },
34+
channelId: slackWatcherChannelId,
35+
}),
36+
});
37+
const deploymentService = implementDeploymentService({
38+
slackClient: implementSlackHttpClient({
39+
external: { slackAuthToken },
40+
channelId: deployWatcherChannelId,
41+
}),
3142
});
32-
const slackService = implementSlackEventService({ slackClient });
3343

3444
/**
3545
██████╗ ███████╗ ██████╗██╗ █████╗ ██████╗ ███████╗
@@ -56,6 +66,15 @@ app.post('/slack/action-endpoint', express.json(), (req, res) => {
5666
}
5767
});
5868

69+
app.post('/github/webhook-endpoint', express.json(), async (req, res) => {
70+
if ('release' in req.body && req.body.action === 'released') await deploymentService.handleCreateRelease(req.body);
71+
if ('workflow_run' in req.body && req.body.action === 'requested')
72+
await deploymentService.handleActionStart(req.body);
73+
if ('workflow_run' in req.body && req.body.action === 'completed')
74+
await deploymentService.handleActionComplete(req.body);
75+
res.sendStatus(200);
76+
});
77+
5978
/**
6079
███████╗████████╗ █████╗ ██████╗ ████████╗
6180
██╔════╝╚══██╔══╝██╔══██╗██╔══██╗╚══██╔══╝
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export type GithubDeploymentService = {
2+
handleCreateRelease: (body: {
3+
release: { author: { login: string }; body: string; tag_name: string; html_url: string };
4+
repository: { name: string };
5+
}) => Promise<void>;
6+
handleActionStart: (body: WorkflowRunBody) => Promise<void>;
7+
handleActionComplete: (body: WorkflowRunBody) => Promise<void>;
8+
};
9+
10+
type WorkflowRunBody = {
11+
workflow_run: { name: string; head_branch: string; id: number; html_url: string };
12+
repository: { name: string };
13+
};

0 commit comments

Comments
 (0)