diff --git a/Readme.md b/Readme.md index 4e7c800..cb8db51 100644 --- a/Readme.md +++ b/Readme.md @@ -1,11 +1,22 @@ # Discord gcloud build bot - +## Generate Github token +Setting > Developer settings > Personal access tokens - +selects following scopes + +- [x] repo + - [x] repo:status + - [x] repo_deployment + - [x] public_repo + - [x] repo:invite + - [x] security_events +- [ ] user + - [x] read:user + - [x] user:email -To deploy to cloud function +## To deploy to cloud function @@ -21,7 +32,7 @@ and run following command, ``` -gcloud functions deploy subscribeDiscord --trigger-topic cloud-builds --runtime nodejs10 --env-vars-file .env.yaml +gcloud functions deploy subscribeDiscord --trigger-topic cloud-builds --runtime nodejs14 --env-vars-file .env.yaml ``` diff --git a/index.js b/index.js deleted file mode 100644 index 5d33fce..0000000 --- a/index.js +++ /dev/null @@ -1,122 +0,0 @@ -const axios = require("axios"); - -const GREY = 9545382; -// const BLUE = 39393 -const RED = 16333359; -const GREEN = 53606; -const YELLOW = 16302848; - -const INFO_IMG = "https://img.icons8.com/color/2x/info.png"; -const ERROR_IMG = "https://img.icons8.com/flat_round/2x/stop.png"; -const WARNING_IMG = "https://img.icons8.com/flat_round/2x/pause.png"; -const SUCCESS_IMG = "https://img.icons8.com/flat_round/2x/checkmark.png"; - -const addFeild = (inline = false) => - (name) => (value) => ({ inline, name, value }); - -module.exports.subscribeDiscord = function (event) { - const build = eventToBuild(event.data); - const status = [ - "WORKING", - "SUCCESS", - "FAILURE", - "INTERNAL_ERROR", - "TIMEOUT", - "CANCELLED", - ]; - - if (!status.includes(build.status)) return; - - const buildStatus = build.status; - const substitutions = build.substitutions; - const logUrl = build.logUrl; - const branch = substitutions.BRANCH_NAME; - const repo = substitutions.REPO_NAME; - const commit = substitutions.COMMIT_SHA; - const owner = process.env.OWNER_NAME; - const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL; - - const inlineField = addFeild(true); - - fetchGitMessage({ commit, repo, owner }).then((result) => { - const thumbnail = getThumbnail(buildStatus); - const color = getColor(buildStatus); - const title = `${repo}(${branch}) is ${capitalized(buildStatus)}` - axios.post(DISCORD_WEBHOOK_URL, { - embeds: [ - { - title, - color, - thumbnail, - fields: [ - inlineField("Repo")(repo), - inlineField("Branch")(branch), - inlineField("Status")(buildStatus), - addFeild()("See log")(logUrl), - addFeild()("Commit")(getMessage(result)), - ], - }, - ], - }) - .catch((e) => { - console.error(e.response.message); - }); - }); -}; - -const eventToBuild = (data) => { - return JSON.parse(Buffer.from(data, "base64").toString()); -}; - -const getColor = ( - statusIndex, -) => { - const colorSet = { SUCCESS: GREEN, WORKING: GREY, CANCELLED: YELLOW }; - return colorSet[statusIndex] || RED; -}; - -const getThumbnail = (status) => { - const imgSet = { - SUCCESS: SUCCESS_IMG, - WORKING: INFO_IMG, - CANCELLED: WARNING_IMG, - }; - return { - url: imgSet[status] || ERROR_IMG, - width: 64, - height: 64 - }; -}; - -const getMessage = (result) => { - try { - return result.data.data.repository.object.message; - } catch (error) { - return error; - } -}; - -const fetchGitMessage = ({ commit, repo, owner }) => - axios.post( - "https://api.github.com/graphql", - { - query: `{ - repository(owner:"${owner}",name:"${repo}") { - object(oid: "${commit}") { - ... on Commit { - message - } - } - } - }`, - }, - { - headers: { - Authorization: `bearer ${process.env.GITHUB_API_TOKEN}`, - }, - }, - ); - -const capitalized = (textString) => { - return `${textString[0].toUpperCase()}${textString.toLowerCase().slice(1)}` -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index dd01cd8..ebd47f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -807,6 +807,19 @@ "brace-expansion": "^1.1.7" } }, + "moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + }, + "moment-timezone": { + "version": "0.5.34", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz", + "integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==", + "requires": { + "moment": ">= 2.9.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index 613da74..64d9396 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,17 @@ "name": "nimble-build-notification", "version": "1.0.0", "description": "", - "main": "index.js", + "main": "dist/index.js", "scripts": { + "build": "tsc", "deploy": "gcloud functions deploy subscribeDiscord --env-vars-file .env.yaml" }, "author": "", "license": "ISC", "dependencies": { - "axios": "^0.21.2" + "axios": "^0.21.2", + "moment": "^2.29.1", + "moment-timezone": "^0.5.34" }, "devDependencies": { "@types/node": "^14.0.14", diff --git a/src/discord.ts b/src/discord.ts index dc7c710..6e785ec 100644 --- a/src/discord.ts +++ b/src/discord.ts @@ -42,10 +42,13 @@ const capitalized = (textString: string) => { export const getTitle = ({ repo, - branch, + target, status, }: { repo: string; - branch: string; + target: string; status: string; -}): string => `${repo}(${branch}) is ${capitalized(status)}`; +}): string => `${repo}(${target}) is ${capitalized(status)}`; + +export const getUrl = (message: string, url: string): string => + `[${message}](${url})`; diff --git a/src/github.ts b/src/github.ts index f7fb03b..a874053 100644 --- a/src/github.ts +++ b/src/github.ts @@ -1,9 +1,17 @@ import axios, { AxiosPromise, AxiosResponse } from 'axios'; -import { CommitResponse } from './interfaces'; +import { CommitResponse, ErrorResponse } from './interfaces'; -export const getMessage = (result: AxiosResponse): string => { +type Repo = CommitResponse['data']['repository']['object']; + +export const isError = ( + payload: AxiosResponse +): payload is AxiosResponse => { + return 'errors' in payload.data; +}; + +export const getRepository = (result: AxiosResponse): Repo => { try { - return result.data.data.repository.object.message; + return result.data.data.repository.object; } catch (error) { return error; } @@ -15,19 +23,26 @@ export const fetchGitMessage = ({ owner, }: { [k: string]: string; -}): AxiosPromise => { - return axios.post( - 'https://api.github.com/graphql', - { - query: `{ +}): AxiosPromise => { + const query = `{ repository(owner:"${owner}",name:"${repo}") { object(oid: "${commit}") { ... on Commit { message + committedDate + author { + avatarUrl + name + } } } } - }`, + }`; + console.log({ query }); + return axios.post( + 'https://api.github.com/graphql', + { + query, }, { headers: { diff --git a/src/index.ts b/src/index.ts index db47b2b..a48f017 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,22 +1,44 @@ -import axios from 'axios'; +import axios, { AxiosResponse } from 'axios'; +import * as moment from 'moment-timezone'; import * as discord from './discord'; -import { fetchGitMessage, getMessage } from './github'; -import { CloudFunction, CloudFunctionEvent } from './interfaces'; +import { fetchGitMessage, getRepository, isError } from './github'; +import { + CloudFunction, + CloudFunctionEvent, + CommitResponse, +} from './interfaces'; -const addFeild = +moment.locale('th'); + +interface AddFieldReturn { + inline?: boolean; + name: string; + value: string; +} + +const addField = (inline = false) => - (name: string) => - (value: string) => ({ + (nameOrValue: string, value?: string) => ({ inline, - name, - value, + name: value ? nameOrValue : undefined, + value: value || nameOrValue, }); const eventToBuild = (data: string): CloudFunctionEvent => { return JSON.parse(Buffer.from(data, 'base64').toString()); }; -export const subscribeDiscord: CloudFunction = (event): void => { +const sendDiscordMessage = async (embeds: { fields: AddFieldReturn[] }[]) => { + try { + await axios.post(process.env.DISCORD_WEBHOOK_URL, { + embeds, + }); + } catch (error) { + console.error(error); + } +}; + +export const subscribeDiscord: CloudFunction = async event => { const build = eventToBuild(event.data); const status = [ 'WORKING', @@ -29,40 +51,67 @@ export const subscribeDiscord: CloudFunction = (event): void => { if (!status.includes(build.status)) return; + const { substitutions, finishTime, startTime } = build; + const buildId = build.id.split('-')[0]; const buildStatus = build.status; - const substitutions = build.substitutions; const logUrl = build.logUrl; const branch = substitutions.BRANCH_NAME; + const tag = substitutions.TAG_NAME; const repo = substitutions.REPO_NAME; const commit = substitutions.COMMIT_SHA; const owner = process.env.OWNER_NAME; - const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL; - const inlineField = addFeild(true); + const inlineField = addField(true); + + const log = discord.getUrl('See log', logUrl); + + try { + const result = await fetchGitMessage({ commit, repo, owner }); + if (isError(result)) throw new Error(result.data.errors[0].message); + const { + author: githubAuthor, + message: description, + committedDate, + } = getRepository(result as AxiosResponse); + + const BKK_TIMEZONE = 'Asia/Bangkok'; + const mDate = moment(committedDate); + const committedAt = mDate.tz(BKK_TIMEZONE).calendar(); + const buildTime = finishTime ? moment(startTime).fromNow(true) : '-'; + const inlineBranch = inlineField('Branch', branch); + const inlineTag = inlineField('Tag', tag); + const target = branch || tag; + + const fields = [ + inlineField('Committed at', committedAt), + inlineField('Build time', buildTime), + inlineField('Build ID', buildId), + inlineField('Repo', repo), + branch ? inlineBranch : inlineTag, + inlineField('Status', buildStatus), + inlineField('Log', log), + ]; - fetchGitMessage({ commit, repo, owner }).then(result => { - const thumbnail = discord.getThumbnail(buildStatus); const color = discord.getColor(buildStatus); - const title = discord.getTitle({ repo, branch, status: buildStatus }); - axios - .post(DISCORD_WEBHOOK_URL, { - embeds: [ - { - title, - color, - thumbnail, - fields: [ - inlineField('Repo')(repo), - inlineField('Branch')(branch), - inlineField('Status')(buildStatus), - addFeild()('See log')(logUrl), - addFeild()('Commit')(getMessage(result)), - ], - }, - ], - }) - .catch(e => { - console.error(e.response.message); - }); - }); + const title = discord.getTitle({ repo, target, status: buildStatus }); + const thumbnail = discord.getThumbnail(buildStatus); + const author = { + name: githubAuthor.name, + icon_url: githubAuthor.avatarUrl, + }; + const embeds = [ + { + title, + description, + color, + thumbnail, + author, + fields, + }, + ]; + + await sendDiscordMessage(embeds); + } catch (error) { + console.error(error); + } }; diff --git a/src/interfaces.ts b/src/interfaces.ts index 39d76a8..1c82512 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,4 +1,4 @@ -enum CloudFunctionStatus { +export enum CloudFunctionStatus { STATUS_UNKNOWN = 'STATUS_UNKNOWN', QUEUED = 'QUEUED', WORKING = 'WORKING', @@ -78,6 +78,21 @@ export interface CommitResponse { }; } +interface Error { + message: string; +} + +export interface ErrorResponse { + errors: Error[]; +} + +export interface Author { + name: string; + avatarUrl: string; +} + export interface Commit { message: string; + author: Author; + committedDate: string; }