Skip to content

Commit bb119d4

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

File tree

17 files changed

+552
-388
lines changed

17 files changed

+552
-388
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 }}
3333
channels: C07L8EWB389
3434
slack-token: ${{ secrets.SLACK_TOKEN_GH_NOTIFIER }}
3535
with-test-data: true

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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: 386 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: 113 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -3,102 +3,135 @@
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 pulls = await client.getPulls({ repositories, state: PullState.Open, withDrafts })
57+
const org = await client.getOrg()
58+
59+
return [...acc, { client, org: org.name, pulls }]
60+
} catch (error: unknown) {
61+
logError(
62+
`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.`,
63+
)
64+
65+
return acc
66+
}
67+
},
68+
Promise.resolve([] as { client: GitHubClient; org: string; pulls: Pull[] }[]),
69+
)
70+
71+
if (results.length === 0) {
72+
throw new Error('All GitHub tokens failed to process')
73+
}
74+
75+
console.log(`Successfully processed ${results.length} out of ${githubConfig.tokens.length} tokens`)
76+
77+
await slack.enforceAppNamePattern(/.*github[\s-_]?notifier$/i)
78+
79+
const pulls: Pull[] = results.flatMap((result) => result.pulls)
80+
81+
// Multiple tokens may have overlapping repository access, deduplicate PRs by org/repo/number
82+
const dedupedPulls = [...new Map(pulls.map((pull) => [`${pull.org}/${pull.repo}/${pull.number}`, pull]))].map(
83+
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars
84+
([_, pull]) => pull,
85+
)
86+
87+
let blocks: KnownBlock[] = []
88+
for (const pull of dedupedPulls) {
89+
console.log(`Building Slack blocks from pull request [${pull.number}]`)
5490

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-
]
91+
blocks = [...blocks, ...(await getPullBlocks(pull, slack, withUserMentions))]
6392
}
64-
}
6593

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-
}
94+
if (withTestData) {
95+
console.log(`With test data: [${withTestData}]`)
96+
const testDataPullRequest = getApprovedPullRequest()
97+
for (let i = 1; i <= 2; i++) {
98+
blocks = [
99+
...blocks,
100+
...(await getPullBlocks({ ...testDataPullRequest, number: i }, slack, withUserMentions)),
101+
]
102+
}
103+
}
71104

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

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)
116+
const orgs = [...new Set(dedupedPulls.map((pull) => pull.org))]
117+
blocks = [...getFirstBlocks(orgs, header, text), ...blocks]
118+
119+
blocks = [
120+
...blocks,
121+
...getLastBlocks(
122+
[
123+
`Run from <${workflowUrl}|${context.repo.owner}`,
124+
`/${context.payload.repository?.name}> (<${workflowLogsUrl}|logs>) using <${homepage}|${name}>@<${homepage}/releases/tag/${version}|${version}>`,
125+
].join(''),
126+
),
127+
]
128+
129+
await slack.postMessage(header, blocks)
130+
} catch (err) {
131+
console.error('Fatal error:', err)
132+
process.exit(1)
103133
}
104134
}
135+
136+
// This is required for the GitHub Action to execute main() when it invokes app.ts as specified in action.yaml
137+
await main()
File renamed without changes.

src/index.ts

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

0 commit comments

Comments
 (0)