diff --git a/.github/workflows/toyproject-21.5-three-days-dashboard.yml b/.github/workflows/toyproject-21.5-three-days-dashboard.yml new file mode 100644 index 0000000..d57b8e9 --- /dev/null +++ b/.github/workflows/toyproject-21.5-three-days-dashboard.yml @@ -0,0 +1,34 @@ +name: toyproject-21.5-send-three-days-dashboard + +on: + schedule: + - cron: '20 2 * * *' # 한국 시간대, 매일 11시 20분에 실행 + +jobs: + cron: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x] + + steps: + - name: Check out code + uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: Setup Node.js environment + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: 'yarn' + + # cron job을 실행합니다. + - name: run script + run: | + yarn install + SLACK_WEEKLY_CHANNEL_ID=C06BH14TZK6 \ + SLACK_AUTH_TOKEN=${{ secrets.SLACK_AUTH_TOKEN }} \ + GHP_ACCESS_TOKEN=${{ secrets.GHP_ACCESS_TOKEN }} \ + GITHUB_ORGANIZATION=wafflestudio21-5 \ + yarn send:toyproject-21.5-three-days-dashboard diff --git a/package.json b/package.json index 6522203..2e27fa2 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "start:server": "node dist/src/server.js", "dev:server": "ts-node src/server.ts", "send:weekly-dashboard": "ts-node src/weekly-dashboard.ts", + "send:toyproject-21.5-three-days-dashboard": "ts-node src/toyproject-21.5-three-days-dashboard.ts", "deploy-server": "scripts/deploy.sh", "test:unit": "jest", "lint": "eslint ./src" diff --git a/src/infrastructures/implementDashboardService.ts b/src/infrastructures/implementDashboardService.ts index 6f23696..867a55b 100644 --- a/src/infrastructures/implementDashboardService.ts +++ b/src/infrastructures/implementDashboardService.ts @@ -1,4 +1,5 @@ import { type SlackClient } from '../clients/SlackClient'; +import { type Repository } from '../entities/GitHub'; import { type GithubApiRepository } from '../repositories/GithubApiRepository'; import { type DashboardService } from '../services/DashboardService'; @@ -67,6 +68,128 @@ export const implementDashboardService = ({ .join('\n\n'); await slackClient.sendMessage([divider, title, divider, repositories].join('\n')); }, + sendGithubTopRepositoriesPerTeamLastThreeDays: async (organization: string) => { + const threeDaysAgo = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000); + const repos = await githubApiRepository.listOrganizationRepositories(organization, { + sort: 'pushed', + perPage: 30, + }); + + const reposGroupByTeam = new Map< + number, + { teamNumber: number; serverRepo: Repository | undefined; clientRepo: Repository | undefined } + >(); + repos.forEach((repo) => { + console.log(repo.name); + const extractedNumbers = repo.name.match(/\d+/); + if (extractedNumbers) { + const teamNumber = parseInt(extractedNumbers[0], 10); + const isServer = repo.name.endsWith('server'); + + const data = reposGroupByTeam.has(teamNumber) + ? reposGroupByTeam.get(teamNumber)! + : { teamNumber: teamNumber, serverRepo: undefined, clientRepo: undefined }; + if (isServer) { + data.serverRepo = repo; + } else { + data.clientRepo = repo; + } + reposGroupByTeam.set(teamNumber, data); + } + }); + + console.log(reposGroupByTeam.size); + + const repoWithDetails = await Promise.all( + Array.from(reposGroupByTeam.values()).map(async ({ teamNumber, serverRepo, clientRepo }) => { + if (!serverRepo || !clientRepo) return []; + + const recentMergedPullRequests = ( + await githubApiRepository.listRepositoryPullRequests(organization, serverRepo.name, { + perPage: 100, + sort: 'updated', + state: 'closed', + direction: 'desc', + }) + ) + .concat( + await githubApiRepository.listRepositoryPullRequests(organization, clientRepo.name, { + perPage: 100, + sort: 'updated', + state: 'closed', + direction: 'desc', + }), + ) + .filter((pr) => pr.merged_at && new Date(pr.merged_at) > threeDaysAgo); + + const recentCreatedComments = ( + await githubApiRepository.listRepositoryComments(organization, serverRepo.name, { + perPage: 100, + sort: 'created_at', + direction: 'desc', + }) + ) + .concat( + await githubApiRepository.listRepositoryComments(organization, clientRepo.name, { + perPage: 100, + sort: 'created_at', + direction: 'desc', + }), + ) + .filter((c) => new Date(c.created_at) > threeDaysAgo); + + const score = recentMergedPullRequests.length * 5 + recentCreatedComments.length * 1; + + if (score === 0) return []; + + return [ + { + serverRepo: serverRepo, + clientRepo: clientRepo, + teamName: `team${teamNumber}`, + score, + details: { + pullRequestCount: recentMergedPullRequests.length, + commentCount: recentCreatedComments.length, + }, + }, + ]; + }), + ); + + const topRepositories = repoWithDetails.flat().sort((a, b) => b.score - a.score); + const topRepositoriesLength = 3; + + const data = topRepositories.slice(0, topRepositoriesLength); + const divider = '---------------------------------------------'; + const title = `*:github: Top ${topRepositoriesLength} Teams Last 3 Days* :blob-clap:`; + const maxPointStringLength = `${Math.max(...data.map((item) => item.score))}`.length; + + const repositories = data + .map( + ( + { + teamName, + serverRepo: { html_url: server_html_url, name: server_name }, + clientRepo: { html_url: client_html_url, name: client_name }, + score, + details: { commentCount, pullRequestCount }, + }, + i, + ) => { + const scoreString = `${score}`.padStart(maxPointStringLength, ' '); + const serverEmoji = + server_name[4] == '2' || server_name[4] == '3' || server_name[4] == '5' ? ':spring:' : ':django:'; + let clientEmoji: string; + if (client_name[4] == '1') clientEmoji = ':apple_mac:'; + else if (client_name[4] == '2' || client_name[4] == '3' || client_name[4] == '4') clientEmoji = ':android:'; + else clientEmoji = ':react:'; + return `${rankEmojis[i]} [${scoreString}p] *${teamName}* (${pullRequestCount} pull requests, ${commentCount} comments)\n\n\t\t${serverEmoji} <${server_html_url}|*${server_name}*>\t${clientEmoji} <${client_html_url}|*${client_name}*>`; + }, + ) + .join('\n\n'); + await slackClient.sendMessage([divider, title, divider, repositories].join('\n')); + }, }; }; diff --git a/src/services/DashboardService.ts b/src/services/DashboardService.ts index e197ae6..c793812 100644 --- a/src/services/DashboardService.ts +++ b/src/services/DashboardService.ts @@ -1,3 +1,4 @@ export type DashboardService = { sendGithubTopRepositoriesLastWeek: (organization: string) => Promise; + sendGithubTopRepositoriesPerTeamLastThreeDays: (organization: string) => Promise; }; diff --git a/src/toyproject-21.5-three-days-dashboard.ts b/src/toyproject-21.5-three-days-dashboard.ts new file mode 100644 index 0000000..581bd9e --- /dev/null +++ b/src/toyproject-21.5-three-days-dashboard.ts @@ -0,0 +1,36 @@ +import dotenv from 'dotenv'; + +import { implementDashboardService } from './infrastructures/implementDashboardService'; +import { implementGithubApiRepository } from './infrastructures/implementGithubApiRepository'; +import { implementGithubHttpClient } from './infrastructures/implementGithubHttpClient'; +import { implementSlackHttpClient } from './infrastructures/implementSlackHttpClient'; + +dotenv.config({ path: '.env.local' }); + +const slackAuthToken = process.env.SLACK_AUTH_TOKEN; +const githubAccessToken = process.env.GHP_ACCESS_TOKEN; +const slackWeeklyChannelId = process.env.SLACK_WEEKLY_CHANNEL_ID; +const githubOrganization = process.env.GITHUB_ORGANIZATION; + +if (!slackAuthToken) throw new Error('Missing Slack Auth Token'); +if (!githubAccessToken) throw new Error('Missing Github Access Token'); +if (!slackWeeklyChannelId) throw new Error('Missing Slack Weekly Channel ID'); +if (!githubOrganization) throw new Error('Missing Github Organization'); + +/** +██████╗ ███████╗██████╗ ███████╗███╗ ██╗██████╗ ███████╗███╗ ██╗ ██████╗██╗███████╗███████╗ +██╔══██╗██╔════╝██╔══██╗██╔════╝████╗ ██║██╔══██╗██╔════╝████╗ ██║██╔════╝██║██╔════╝██╔════╝ +██║ ██║█████╗ ██████╔╝█████╗ ██╔██╗ ██║██║ ██║█████╗ ██╔██╗ ██║██║ ██║█████╗ ███████╗ +██║ ██║██╔══╝ ██╔═══╝ ██╔══╝ ██║╚██╗██║██║ ██║██╔══╝ ██║╚██╗██║██║ ██║██╔══╝ ╚════██║ +██████╔╝███████╗██║ ███████╗██║ ╚████║██████╔╝███████╗██║ ╚████║╚██████╗██║███████╗███████║ +╚═════╝ ╚══════╝╚═╝ ╚══════╝╚═╝ ╚═══╝╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═════╝╚═╝╚══════╝╚══════╝ + */ +const slackClient = implementSlackHttpClient({ + external: { slackAuthToken }, + channelId: slackWeeklyChannelId, +}); +const githubClient = implementGithubHttpClient({ githubAccessToken }); +const githubApiRepository = implementGithubApiRepository({ githubClient }); +const dashboardService = implementDashboardService({ githubApiRepository, slackClient }); + +dashboardService.sendGithubTopRepositoriesPerTeamLastThreeDays(githubOrganization).catch(console.error);