|
3 | 3 | import type { KnownBlock } from '@slack/web-api' |
4 | 4 |
|
5 | 5 | 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' |
9 | 8 |
|
10 | 9 | import pkg from '../package.json' with { type: 'json' } |
11 | | -import { workflowLogsUrl, workflowUrl } from './defaults.js' |
| 10 | +import { workflowLogsUrl, workflowUrl } from './constants.js' |
12 | 11 | import { GitHubClient } from './utils/github/github-client.js' |
| 12 | +import type { Pull } from './utils/github/structures.js' |
13 | 13 | import { PullState, RepositoryType } from './utils/github/structures.js' |
14 | 14 | import { getFirstBlocks, getLastBlocks, getPullBlocks } from './utils/slack/blocks.js' |
15 | 15 | import { SlackClient } from './utils/slack/slack-client.js' |
16 | 16 | import { getApprovedPullRequest } from './utils/test-data.js' |
| 17 | +import { parseInputs as getInputs } from './input-parser.js' |
17 | 18 |
|
18 | 19 | const { homepage, name, version } = pkg |
19 | 20 |
|
20 | 21 | /** |
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. |
24 | 23 | */ |
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}]`) |
54 | 93 |
|
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))] |
63 | 95 | } |
64 | | - } |
65 | 96 |
|
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 | + } |
71 | 107 |
|
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 | + } |
76 | 118 |
|
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) |
103 | 136 | } |
104 | 137 | } |
| 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() |
0 commit comments