Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨(deploy) slack notify authors of deployments #3101

Merged
merged 2 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions baker/BuildkiteTrigger.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { DeployMetadata } from "@ourworldindata/utils"
import {
BUILDKITE_API_ACCESS_TOKEN,
BUILDKITE_DEPLOY_CONTENT_PIPELINE_SLUG,
BUILDKITE_BRANCH,
BUILDKITE_DEPLOY_CONTENT_SLACK_CHANNEL,
} from "../settings/serverSettings.js"
import { defaultCommitMessage } from "./DeployUtils.js"

type BuildState =
| "running"
Expand Down Expand Up @@ -45,7 +48,7 @@ export class BuildkiteTrigger {
commit: "HEAD",
branch: this.branch,
message: message,
env: env,
env: { ...env, BUILDKITE_DEPLOY_CONTENT_SLACK_CHANNEL },
}

const response = await fetch(url, {
Expand Down Expand Up @@ -102,17 +105,36 @@ export class BuildkiteTrigger {
}

async runLightningBuild(
message: string,
gdocSlugs: string[]
gdocSlugs: string[],
{ title, changesSlackMentions }: DeployMetadata
): Promise<void> {
const message = `⚡️ ${title}${
gdocSlugs.length > 1
? ` and ${gdocSlugs.length - 1} more updates`
: ""
}`
const buildNumber = await this.triggerBuild(message, {
LIGHTNING_GDOC_SLUGS: gdocSlugs.join(" "),
CHANGES_SLACK_MENTIONS: changesSlackMentions.join("\n"),
})
await this.waitForBuildToFinish(buildNumber)
}

async runFullBuild(message: string): Promise<void> {
const buildNumber = await this.triggerBuild(message, {})
async runFullBuild({
title,
changesSlackMentions,
}: DeployMetadata): Promise<void> {
const message = changesSlackMentions.length
? `🚚 ${title}${
changesSlackMentions.length > 1
? ` and ${changesSlackMentions.length - 1} more updates`
: ""
} `
: await defaultCommitMessage()

const buildNumber = await this.triggerBuild(message, {
CHANGES_SLACK_MENTIONS: changesSlackMentions.join("\n"),
})
await this.waitForBuildToFinish(buildNumber)
}
}
111 changes: 88 additions & 23 deletions baker/DeployUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import {
BAKED_SITE_DIR,
BAKED_BASE_URL,
BUILDKITE_API_ACCESS_TOKEN,
SLACK_BOT_OAUTH_TOKEN,
} from "../settings/serverSettings.js"
import { DeployChange } from "@ourworldindata/utils"
import { SiteBaker } from "../baker/SiteBaker.js"
import { WebClient } from "@slack/web-api"
import { DeployChange, DeployMetadata } from "@ourworldindata/utils"

const deployQueueServer = new DeployQueueServer()

const defaultCommitMessage = async (): Promise<string> => {
export const defaultCommitMessage = async (): Promise<string> => {
let message = "Automated update"

// In the deploy.sh script, we write the current git rev to 'public/head.txt'
Expand All @@ -31,24 +33,22 @@ const defaultCommitMessage = async (): Promise<string> => {
* Initiate a deploy, without any checks. Throws error on failure.
*/
const triggerBakeAndDeploy = async (
message?: string,
deployMetadata: DeployMetadata,
lightningQueue?: DeployChange[]
) => {
message = message ?? (await defaultCommitMessage())

// deploy to Buildkite if we're on master and BUILDKITE_API_ACCESS_TOKEN is set
if (BUILDKITE_API_ACCESS_TOKEN) {
const buildkite = new BuildkiteTrigger()
if (lightningQueue?.length) {
await buildkite
.runLightningBuild(
message!,
lightningQueue.map((change) => change.slug!)
lightningQueue.map((change) => change.slug!),
deployMetadata
)
.catch(logErrorAndMaybeSendToBugsnag)
} else {
await buildkite
.runFullBuild(message)
.runFullBuild(deployMetadata)
.catch(logErrorAndMaybeSendToBugsnag)
}
} else {
Expand All @@ -65,22 +65,78 @@ const triggerBakeAndDeploy = async (
}
}

const generateCommitMsg = (queueItems: DeployChange[]) => {
const date: string = new Date().toISOString()
const getChangesAuthorNames = (queueItems: DeployChange[]): string[] => {
// Do not remove duplicates here, because we want to show the history of changes within a deploy
return queueItems
.map((item) => `${item.message} (by ${item.authorName})`)
.filter(Boolean)
}

const message: string = queueItems
.filter((item) => item.message)
.map((item) => item.message)
.join("\n")
const getChangesSlackMentions = async (
queueItems: DeployChange[]
): Promise<string[]> => {
const emailSlackMentionMap = await getEmailSlackMentionsMap(queueItems)

// Do not remove duplicates here, because we want to show the history of changes within a deploy
return queueItems.map(
(item) =>
`${item.message} (by ${
!item.authorEmail
? item.authorName
: emailSlackMentionMap.get(item.authorEmail) ??
item.authorName
})`
)
}

const coauthors: string = queueItems
.filter((item) => item.authorName)
.map((item) => {
return `Co-authored-by: ${item.authorName} <${item.authorEmail}>`
const getEmailSlackMentionsMap = async (
queueItems: DeployChange[]
): Promise<Map<string, string>> => {
const slackClient = new WebClient(SLACK_BOT_OAUTH_TOKEN)

// Get unique author emails
const uniqueAuthorEmails = [
...new Set(queueItems.map((item) => item.authorEmail)),
]

// Get a Map of email -> Slack mention (e.g. "<@U123456>")
const emailSlackMentionMap = new Map()
await Promise.all(
uniqueAuthorEmails.map(async (authorEmail) => {
if (authorEmail) {
const slackId = await getSlackMentionByEmail(
authorEmail,
slackClient
)
if (slackId) {
emailSlackMentionMap.set(authorEmail, slackId)
}
}
})
.join("\n")
)

return emailSlackMentionMap
}

return `Deploy ${date}\n${message}\n\n\n${coauthors}`
/**
*
* Get a Slack mention for a given email address. Format it according to the
* Slack API requirements to mention a user in a message
* (https://api.slack.com/reference/surfaces/formatting#mentioning-users).
*/
const getSlackMentionByEmail = async (
email: string | undefined,
slackClient: WebClient
): Promise<string | undefined> => {
if (!email) return

try {
const response = await slackClient.users.lookupByEmail({ email })
return response.user?.id ? `<@${response.user.id}>` : undefined
} catch (error) {
logErrorAndMaybeSendToBugsnag(error)
}
return
}

const MAX_SUCCESSIVE_FAILURES = 2
Expand Down Expand Up @@ -114,11 +170,20 @@ export const deployIfQueueIsNotEmpty = async () => {

const parsedQueue = deployQueueServer.parseQueueContent(deployContent)

const message = generateCommitMsg(parsedQueue)
console.log(`Deploying site...\n---\n${message}\n---`)
// Log the changes that are about to be deployed in a text format.
const dateStr: string = new Date().toISOString()
const changesAuthorNames = getChangesAuthorNames(parsedQueue)
console.log(
`Deploying site...\n---\n📆 ${dateStr}\n\n${changesAuthorNames.join(
"\n"
)}\n---`
)

try {
const changesSlackMentions =
await getChangesSlackMentions(parsedQueue)
await triggerBakeAndDeploy(
message,
{ title: changesAuthorNames[0], changesSlackMentions },
// If every DeployChange is a lightning change, then we can do a
// lightning deploy. In the future, we might want to separate
// lightning updates from regular deploys so we could prioritize
Expand Down
2 changes: 1 addition & 1 deletion ops/buildkite/deploy-content
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,4 @@ bake_gdoc_posts() {
)
}

deploy_content
deploy_content
15 changes: 15 additions & 0 deletions ops/buildkite/notify-slack
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/bin/bash
#
# notify-slack
#
# Notify authors of finished content deployments on Slack.
#

# see https://buildkite.com/docs/pipelines/writing-build-scripts#configuring-bash
set -euo pipefail

notify_slack() {
echo "$CHANGES_SLACK_MENTIONS" | slacktee -a good -t "$BUILDKITE_MESSAGE" -c "$BUILDKITE_DEPLOY_CONTENT_SLACK_CHANNEL" -u "Live deploy" --plain-text
}

notify_slack
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"@ourworldindata/utils": "workspace:^",
"@react-awesome-query-builder/antd": "^6.1.1",
"@sinclair/typebox": "^0.28.5",
"@slack/web-api": "^6.11.1",
"@tippyjs/react": "^4.2.6",
"@types/bcrypt": "^5.0.0",
"@types/chunk-text": "^1.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,8 @@ export interface Deploy {
status: DeployStatus
changes: DeployChange[]
}

export interface DeployMetadata {
title: string
changesSlackMentions: string[]
}
1 change: 1 addition & 0 deletions packages/@ourworldindata/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export {
type Deploy,
type DeployChange,
DeployStatus,
type DeployMetadata,
} from "./domainTypes/DeployStatus.js"
export { type Tag } from "./dbTypes/Tags.js"

Expand Down
5 changes: 5 additions & 0 deletions settings/serverSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,5 +181,10 @@ export const BUILDKITE_DEPLOY_CONTENT_PIPELINE_SLUG: string =
"owid-deploy-content-master"
export const BUILDKITE_BRANCH: string =
serverSettings.BUILDKITE_BRANCH || "master"
export const BUILDKITE_DEPLOY_CONTENT_SLACK_CHANNEL: string =
serverSettings.BUILDKITE_DEPLOY_CONTENT_SLACK_CHANNEL || "C03N1V9KXB9" // #article-and-data-updates

export const OPENAI_API_KEY: string = serverSettings.OPENAI_API_KEY ?? ""

export const SLACK_BOT_OAUTH_TOKEN: string =
serverSettings.SLACK_BOT_OAUTH_TOKEN ?? ""
Loading
Loading