Skip to content

Commit 64d8242

Browse files
committed
feat: support multi org #27
1 parent f1cad93 commit 64d8242

File tree

17 files changed

+560
-390
lines changed

17 files changed

+560
-390
lines changed

.github/workflows/tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
- name: Test Action
3030
uses: ./
3131
with:
32-
github-token: ${{ secrets.GH_TOKEN_GH_NOTIFIER }}
32+
github-tokens: test, ${{ secrets.GH_TOKEN_GH_NOTIFIER }}, ${{ secrets.GH_TOKEN_GH_NOTIFIER }}, ${{ secrets.GH_TOKEN_GH_NOTIFIER_ALT }}
3333
channels: C07L8EWB389
3434
slack-token: ${{ secrets.SLACK_TOKEN_GH_NOTIFIER }}
3535
with-test-data: true

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ This _GitHub Action_ will query the GitHub (or GitHub Enterprise) org for all re
4343

4444
1. (Optional) Start by creating a repository called `github-notifier`.
4545
1. Whatever repo you use to host this workflow needs to be able to use GitHub Runners.
46-
1. [Generate a GitHub token](https://github.com/settings/tokens?type=beta) – You’ll need a fine-grained GitHub token that allows access to either all your repositories or just the ones you want notifications about.
46+
1. [Generate a GitHub token](https://github.com/settings/tokens?type=beta) – You’ll need a fine-grained GitHub token with resource owner being an organization that allows access to either all your repositories or just the ones you want notifications about.
4747

4848
Here are the specific permissions the token needs:
4949

@@ -90,7 +90,7 @@ This _GitHub Action_ will query the GitHub (or GitHub Enterprise) org for all re
9090
steps:
9191
- uses: krauters/github-notifier@main
9292
with:
93-
github-token: ${{ secrets.GH_TOKEN_GH_NOTIFIER }}
93+
github-tokens: ${{ secrets.GH_TOKEN_GH_NOTIFIER }}, ${{ secrets.GH_TOKEN_GH_NOTIFIER_FOR_ANOTHER_ORG }}
9494
channels: C07L8EWB389
9595
slack-token: ${{ secrets.SLACK_TOKEN_GH_NOTIFIER }}
9696
```
@@ -103,7 +103,7 @@ See [action.yaml](./action.yaml) for more detailed information.
103103
104104
| Name | Description | Required | Default |
105105
|-----------------------|---------------------------------------------------------------------------------------------|----------|----------|
106-
| `github-token` | Fine-grained GitHub token with necessary scopes for administration, PR details, and members.| Yes | |
106+
| `github-tokens` | Comma Comma-separated list of fine grained Github tokens (one per GitHub organization) with scopes for administration, PR details, and members.| Yes | |
107107
| `slack-token` | Permissions to post to Slack and perform user lookups. | Yes | |
108108
| `channels` | Comma-separated list of Slack channel IDs to post to. | Yes | |
109109
| `with-archived` | Include PRs from archived repositories. | No | `false` |

action.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ branding:
99
license: ISC
1010

1111
inputs:
12-
github-token:
12+
github-tokens:
1313
description: |
14-
Fine grained Github token with scopes,
14+
Comma-separated list of fine grained Github tokens (one per GitHub organization) with scopes,
1515
- Administration:read (to list all repos in org)
1616
- Pull Requests:read (to get PR details for repos)
1717
- (Organzation) Members:read on all repos (to get GitHub email for Slack user matching)
@@ -27,7 +27,7 @@ inputs:
2727
- chat:write.customize (to allow the bot to customize the name and avatar)
2828
channels:
2929
required: true
30-
description: Comma separated list of Slack channel_ids to post to.
30+
description: Comma-separated list of Slack channel IDs to post to.
3131
with-test-data:
3232
description: Append some test data to the Slack post.
3333
required: false
@@ -52,7 +52,7 @@ inputs:
5252
default: true
5353
repository-filter:
5454
required: false
55-
description: Comma separated list of repositories to scan.
55+
description: Comma-separated list of repositories to scan.
5656
base-url:
5757
description: Point to different github instance such as https://api.github.com
5858
required: false

dist/index.js

Lines changed: 389 additions & 255 deletions
Large diffs are not rendered by default.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

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

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@krauters/github-notifier",
33
"description": "GitHub Notifier by Krauters – Post Open Pull Requests to Slack",
4-
"version": "0.14.1",
4+
"version": "0.15.0",
55
"author": "Colten Krauter <coltenkrauter>",
66
"type": "module",
77
"homepage": "https://buymeacoffee.com/coltenkrauter",
@@ -17,15 +17,15 @@
1717
"typescript"
1818
],
1919
"exports": {
20-
".": "./dist/index.js"
20+
".": "./dist/app.js"
2121
},
2222
"engines": {
2323
"node": ">=20"
2424
},
2525
"scripts": {
2626
"all": "npm run test && npm run package",
2727
"bundle:watch": "npm run bundle -- --watch",
28-
"bundle": "npx ncc build src/index.ts -o dist --source-map",
28+
"bundle": "npx ncc build src/app.ts -o dist --source-map",
2929
"fix": "npm run lint -- --fix",
3030
"lint": "npx eslint src/**",
3131
"prepare": "husky || true",

src/app.ts

Lines changed: 116 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -3,102 +3,138 @@
33
import type { KnownBlock } from '@slack/web-api'
44

55
import { context } from '@actions/github'
6-
import { formatStringList, plural, snapDate, SnapType } from '@krauters/utils'
7-
8-
import type { RunProps } from './structures.js'
6+
import { debug, error as logError } from '@actions/core'
7+
import { formatStringList, plural } from '@krauters/utils'
98

109
import pkg from '../package.json' with { type: 'json' }
11-
import { workflowLogsUrl, workflowUrl } from './defaults.js'
10+
import { workflowLogsUrl, workflowUrl } from './constants.js'
1211
import { GitHubClient } from './utils/github/github-client.js'
12+
import type { Pull } from './utils/github/structures.js'
1313
import { PullState, RepositoryType } from './utils/github/structures.js'
1414
import { getFirstBlocks, getLastBlocks, getPullBlocks } from './utils/slack/blocks.js'
1515
import { SlackClient } from './utils/slack/slack-client.js'
1616
import { getApprovedPullRequest } from './utils/test-data.js'
17+
import { parseInputs as getInputs } from './input-parser.js'
1718

1819
const { homepage, name, version } = pkg
1920

2021
/**
21-
* Runs the GitHub Notifier to query GitHub for open pull requests and then post messages to Slack channels.
22-
*
23-
* @param props Configurable properties of the GitHub Notifier.
22+
* The main function that gets executed when the action is run.
2423
*/
25-
export async function run({
26-
githubProps,
27-
repositoryFilter,
28-
slackProps,
29-
withArchived,
30-
withDrafts,
31-
withPublic,
32-
withPullReport,
33-
withTestData,
34-
withUserMentions,
35-
}: RunProps): Promise<void> {
36-
const slack = new SlackClient(slackProps)
37-
const gh = new GitHubClient(githubProps)
38-
39-
await slack.enforceAppNamePattern(/.*github[\s-_]?notifier$/i)
40-
41-
const repositories = await gh.getRepositories({
42-
repositoryFilter,
43-
type: RepositoryType.All,
44-
withArchived,
45-
withPublic,
46-
})
47-
const openPulls = await gh.getPulls({ repositories, state: PullState.Open, withDrafts })
48-
let blocks: KnownBlock[] = []
49-
for (const openPull of openPulls) {
50-
console.log(`Building Slack blocks from pull request [${openPull.number}]`)
51-
52-
blocks = [...blocks, ...(await getPullBlocks(openPull, slack, withUserMentions))]
53-
}
24+
async function main(): Promise<void> {
25+
try {
26+
debug('Starting main...')
27+
const {
28+
githubConfig,
29+
repositoryFilter,
30+
slackConfig,
31+
withArchived,
32+
withDrafts,
33+
withPublic,
34+
withTestData,
35+
withUserMentions,
36+
} = getInputs()
37+
38+
const slack = new SlackClient(slackConfig)
39+
const results = await githubConfig.tokens.reduce(
40+
async (accPromise, token) => {
41+
const acc = await accPromise
42+
try {
43+
// TODO - Consider making this thread safe so requests can be made in parallel
44+
const client = new GitHubClient({
45+
options: githubConfig.options,
46+
token,
47+
})
48+
49+
const repositories = await client.getRepositories({
50+
repositoryFilter,
51+
type: RepositoryType.All,
52+
withArchived,
53+
withPublic,
54+
})
55+
56+
const org = await client.getOrg()
57+
const pulls = await client.getPulls({ repositories, state: PullState.Open, withDrafts })
58+
console.log(`Found ${pulls.length} pulls for ${org.name}`)
59+
60+
return [...acc, { client, org: org.name, pulls }]
61+
} catch (error: unknown) {
62+
logError(
63+
`Failed to call to GitHub [${githubConfig.options?.baseUrl}] with last 4 chars of token [${token.slice(-4)}] with error [${error}]. Skipping this requests with this token.`,
64+
)
65+
66+
return acc
67+
}
68+
},
69+
Promise.resolve([] as { client: GitHubClient; org: string; pulls: Pull[] }[]),
70+
)
71+
72+
if (results.length === 0) {
73+
throw new Error('All GitHub tokens failed to process')
74+
}
75+
76+
console.log(`Successfully processed ${results.length} out of ${githubConfig.tokens.length} tokens`)
77+
78+
await slack.enforceAppNamePattern(/.*github[\s-_]?notifier$/i)
79+
80+
const pulls: Pull[] = results.flatMap((result) => result.pulls)
81+
console.log(`Found ${pulls.length} pulls`)
82+
console.log(pulls)
83+
84+
// Multiple tokens may have overlapping repository access, deduplicate PRs by org/repo/number
85+
const dedupedPulls = [...new Map(pulls.map((pull) => [`${pull.org}/${pull.repo}/${pull.number}`, pull]))].map(
86+
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars
87+
([_, pull]) => pull,
88+
)
89+
90+
let blocks: KnownBlock[] = []
91+
for (const pull of dedupedPulls) {
92+
console.log(`Building Slack blocks from pull request [${pull.number}]`)
5493

55-
if (withTestData) {
56-
console.log(`With test data: [${withTestData}]`)
57-
const testDataPullRequest = getApprovedPullRequest()
58-
for (let i = 1; i <= 2; i++) {
59-
blocks = [
60-
...blocks,
61-
...(await getPullBlocks({ ...testDataPullRequest, number: i }, slack, withUserMentions)),
62-
]
94+
blocks = [...blocks, ...(await getPullBlocks(pull, slack, withUserMentions))]
6395
}
64-
}
6596

66-
const total = openPulls.length
67-
let header = `You've got ${total} open pull ${plural('request', total)}.`
68-
if (total === 0) {
69-
header = 'There are no open pull requests! :tada:'
70-
}
97+
if (withTestData) {
98+
console.log(`With test data: [${withTestData}]`)
99+
const testDataPullRequest = getApprovedPullRequest()
100+
for (let i = 1; i <= 2; i++) {
101+
blocks = [
102+
...blocks,
103+
...(await getPullBlocks({ ...testDataPullRequest, number: i }, slack, withUserMentions)),
104+
]
105+
}
106+
}
71107

72-
let text
73-
if (repositoryFilter.length > 0) {
74-
text = `_<${workflowUrl}|Repository filter>: ${formatStringList(repositoryFilter)}_`
75-
}
108+
const total = dedupedPulls.length
109+
let header = `You've got ${total} open pull ${plural('request', total)}.`
110+
if (total === 0) {
111+
header = 'There are no open pull requests! :tada:'
112+
}
113+
114+
let text
115+
if (repositoryFilter.length > 0) {
116+
text = `_<${workflowUrl}|Repository filter>: ${formatStringList(repositoryFilter)}_`
117+
}
76118

77-
blocks = [...getFirstBlocks(gh.cacheOrganization.name, header, text), ...blocks]
78-
79-
blocks = [
80-
...blocks,
81-
...getLastBlocks(
82-
[
83-
`Run from <${workflowUrl}|${context.repo.owner}`,
84-
`/${context.payload.repository?.name}> (<${workflowLogsUrl}|logs>) using <${homepage}|${name}>@<${homepage}/releases/tag/${version}|${version}>`,
85-
].join(''),
86-
),
87-
]
88-
89-
await slack.postMessage(header, blocks)
90-
91-
if (withPullReport) {
92-
const pulls = await gh.getPulls({
93-
oldest: snapDate(new Date(), { months: -12, snap: SnapType.Month }),
94-
onlyGhReviews: true,
95-
repositories,
96-
state: PullState.All,
97-
withCommits: false,
98-
withDrafts: false,
99-
withFilesAndChanges: false,
100-
withUser: false,
101-
})
102-
console.log(gh.getPullReport(pulls).reportString)
119+
const orgs = [...new Set(dedupedPulls.map((pull) => pull.org))]
120+
blocks = [...getFirstBlocks(orgs, header, text), ...blocks]
121+
122+
blocks = [
123+
...blocks,
124+
...getLastBlocks(
125+
[
126+
`Run from <${workflowUrl}|${context.repo.owner}`,
127+
`/${context.payload.repository?.name}> (<${workflowLogsUrl}|logs>) using <${homepage}|${name}>@<${homepage}/releases/tag/${version}|${version}>`,
128+
].join(''),
129+
),
130+
]
131+
132+
await slack.postMessage(header, blocks)
133+
} catch (err) {
134+
console.error('Fatal error:', err)
135+
process.exit(1)
103136
}
104137
}
138+
139+
// This is required for the GitHub Action to execute main() when it invokes app.ts as specified in action.yaml
140+
await main()
File renamed without changes.

src/index.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.

0 commit comments

Comments
 (0)