Skip to content

Commit d39a49c

Browse files
committed
ci: github activity slack notifications
1 parent 3c4f8a3 commit d39a49c

12 files changed

Lines changed: 4861 additions & 0 deletions

File tree

.github/actions/activity/dist/new-issues/index.js

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/actions/activity/dist/popular-issues/index.js

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"private": true,
3+
"license": "MIT",
4+
"scripts": {
5+
"build:all": "pnpm build:typecheck && pnpm build:popular-issues:ncc && pnpm build:new-issues:ncc",
6+
"build:new-issues:ncc": "ncc build src/new-issues.ts -m -o dist/new-issues",
7+
"build:popular-issues:ncc": "ncc build src/popular-issues.ts -m -o dist/popular-issues",
8+
"build:typecheck": "tsc",
9+
"clean": "rimraf dist"
10+
},
11+
"dependencies": {
12+
"@actions/core": "^1.3.0",
13+
"@actions/github": "^5.0.0",
14+
"@slack/web-api": "^7.10.0"
15+
},
16+
"devDependencies": {
17+
"@octokit/openapi-types": "^26.0.0",
18+
"@octokit/webhooks-types": "^7.5.1",
19+
"@types/node": "^20.16.5",
20+
"@vercel/ncc": "0.38.1",
21+
"typescript": "^4.9.5"
22+
}
23+
}

.github/actions/activity/pnpm-lock.yaml

Lines changed: 4241 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { run as newIssuesRun } from './new-issues'
2+
import { run as popularIssuesRun } from './popular-issues'
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Format date to <month|short> <day|numeric>, <year|numeric>
3+
*/
4+
export function formattedDate(createdAt: string): string {
5+
const date = new Date(createdAt)
6+
7+
return date.toLocaleDateString('en-US', {
8+
month: 'short',
9+
day: 'numeric',
10+
year: 'numeric',
11+
})
12+
}
13+
14+
/**
15+
* Get days ago in YYYY-MM-DD format
16+
*/
17+
export function daysAgo(days: number): string {
18+
const date = new Date()
19+
date.setDate(date.getDate() - days)
20+
return date.toISOString().split('T')[0]
21+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { context, getOctokit } from '@actions/github'
2+
import { info, setFailed } from '@actions/core'
3+
import { WebClient } from '@slack/web-api'
4+
import { formattedDate, daysAgo } from './lib/utils'
5+
import { SlimIssue } from './types'
6+
7+
function generateBlocks(issues: SlimIssue[]) {
8+
const blocks = [
9+
{
10+
type: 'section',
11+
text: {
12+
type: 'mrkdwn',
13+
text: '*A list of issues opened in the last 7 days.',
14+
},
15+
},
16+
{
17+
type: 'divider',
18+
},
19+
]
20+
21+
let text = ''
22+
23+
issues.forEach((issue, i) => {
24+
text += `${i + 1}. [<${issue.html_url}|#${issue.number}>, ${
25+
issue?.reactions?.total_count || 0
26+
} reactions, ${formattedDate(issue.created_at)}]: ${issue.title}\n`
27+
})
28+
29+
blocks.push({
30+
type: 'section',
31+
text: {
32+
type: 'mrkdwn',
33+
text: text,
34+
},
35+
})
36+
37+
return blocks
38+
}
39+
40+
export async function run() {
41+
try {
42+
if (!process.env.GITHUB_TOKEN) throw new TypeError('GITHUB_TOKEN not set')
43+
if (!process.env.SLACK_TOKEN) throw new TypeError('SLACK_TOKEN not set')
44+
45+
const octoClient = getOctokit(process.env.GITHUB_TOKEN)
46+
const slackClient = new WebClient(process.env.SLACK_TOKEN)
47+
48+
const { owner, repo } = context.repo
49+
const { data } = await octoClient.rest.search.issuesAndPullRequests({
50+
order: 'desc',
51+
per_page: 15,
52+
q: `repo:${owner}/${repo} is:issue is:open created:>=${daysAgo(7)}`,
53+
sort: 'created',
54+
})
55+
56+
if (!data.items.length) {
57+
info(`No popular issues`)
58+
return
59+
}
60+
61+
console.log(
62+
data.items
63+
.map(
64+
(i) =>
65+
`#${i.number}: ${i.title}, reactions: ${i.reactions?.total_count} - link: ${i.html_url}`,
66+
)
67+
.join('\n'),
68+
)
69+
70+
await slackClient.chat.postMessage({
71+
blocks: generateBlocks(data.items),
72+
channel: '#test-notifications',
73+
icon_emoji: ':github:',
74+
username: 'GitHub Notifier',
75+
})
76+
info(`Posted to Slack!`)
77+
} catch (error) {
78+
setFailed(error as Error)
79+
}
80+
}
81+
82+
run()
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { context, getOctokit } from '@actions/github'
2+
import { info, setFailed } from '@actions/core'
3+
import { WebClient } from '@slack/web-api'
4+
import { formattedDate, daysAgo } from './lib/utils'
5+
import { SlimIssue } from './types'
6+
7+
function generateBlocks(issues: SlimIssue[]) {
8+
const blocks = [
9+
{
10+
type: 'section',
11+
text: {
12+
type: 'mrkdwn',
13+
text: '*A list of the top 15 issues sorted by the most reactions over the last 30 days.',
14+
},
15+
},
16+
{
17+
type: 'divider',
18+
},
19+
]
20+
21+
let text = ''
22+
23+
issues.forEach((issue, i) => {
24+
text += `${i + 1}. [<${issue.html_url}|#${issue.number}>, ${
25+
issue?.reactions?.total_count || 0
26+
} reactions, ${formattedDate(issue.created_at)}]: ${issue.title}\n`
27+
})
28+
29+
blocks.push({
30+
type: 'section',
31+
text: {
32+
type: 'mrkdwn',
33+
text: text,
34+
},
35+
})
36+
37+
return blocks
38+
}
39+
40+
export async function run() {
41+
try {
42+
if (!process.env.GITHUB_TOKEN) throw new TypeError('GITHUB_TOKEN not set')
43+
if (!process.env.SLACK_TOKEN) throw new TypeError('SLACK_TOKEN not set')
44+
45+
const octoClient = getOctokit(process.env.GITHUB_TOKEN)
46+
const slackClient = new WebClient(process.env.SLACK_TOKEN)
47+
48+
const { owner, repo } = context.repo
49+
const { data } = await octoClient.rest.search.issuesAndPullRequests({
50+
order: 'desc',
51+
per_page: 15,
52+
q: `repo:${owner}/${repo} is:issue is:open created:>=${daysAgo(30)}`,
53+
sort: 'reactions',
54+
})
55+
56+
if (!data.items.length) {
57+
info(`No popular issues`)
58+
return
59+
}
60+
61+
console.log(
62+
data.items
63+
.map(
64+
(i) =>
65+
`#${i.number}: ${i.title}, reactions: ${i.reactions?.total_count} - link: ${i.html_url}`,
66+
)
67+
.join('\n'),
68+
)
69+
70+
await slackClient.chat.postMessage({
71+
blocks: generateBlocks(data.items),
72+
channel: '#test-notifications',
73+
icon_emoji: ':github:',
74+
username: 'GitHub Notifier',
75+
})
76+
77+
info(`Posted to Slack!`)
78+
} catch (error) {
79+
setFailed(error as Error)
80+
}
81+
}
82+
83+
run()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { components } from '@octokit/openapi-types'
2+
3+
export type SlimIssue = Pick<
4+
components['schemas']['issue-search-result-item'],
5+
'html_url' | 'number'
6+
> & {
7+
title: string
8+
created_at: string
9+
reactions?: components['schemas']['reaction-rollup']
10+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"compilerOptions": {
3+
"target": "esnext",
4+
"noEmit": true,
5+
"strict": true,
6+
"noUnusedLocals": false, // Undo this
7+
"noImplicitReturns": true,
8+
"noFallthroughCasesInSwitch": true,
9+
"forceConsistentCasingInFileNames": true,
10+
"downlevelIteration": true,
11+
"skipLibCheck": true,
12+
"moduleResolution": "node",
13+
},
14+
"exclude": [
15+
"src/**/*.test.ts"
16+
]
17+
}

0 commit comments

Comments
 (0)