Skip to content

Commit d8b6322

Browse files
committed
feat: enhanced user matcher
1 parent c0f2f69 commit d8b6322

File tree

14 files changed

+1067
-117
lines changed

14 files changed

+1067
-117
lines changed

.github/workflows/tests.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,19 @@ jobs:
3535
with-test-data: true
3636
- name: Print Output
3737
run: echo "${{ steps.test-action.outputs.response }}"
38+
test-action-2:
39+
runs-on: ubuntu-latest
40+
steps:
41+
- name: Checkout
42+
uses: actions/checkout@v4
43+
- name: Test Action
44+
uses: ./
45+
with:
46+
github-tokens: test, ${{ secrets.GH_TOKEN_GH_NOTIFIER }}
47+
channels: C07L8EWB389
48+
user-mappings: reviewer1:nassirougni
49+
slack-token: ${{ secrets.SLACK_TOKEN_GH_NOTIFIER }}
50+
with-test-data: true
51+
- name: Print Output
52+
run: echo "${{ steps.test-action.outputs.response }}"
53+

action.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ inputs:
2828
channels:
2929
required: true
3030
description: Comma-separated list of Slack channel IDs to post to.
31+
user-mappings:
32+
required: false
33+
description: |
34+
Comma-separated list of GitHub to Slack username mappings in the format "github:slack".
35+
These mappings are PRIORITIZED over automatic matching between GitHub and Slack users.
36+
If a mapping is defined but the Slack username doesn't exist, it will fall back to automatic matching.
37+
The automatic matching tries to match by email and username when custom mapping fails.
38+
Used specifically during Slack user lookup for @mentions and avatars in notifications.
39+
Example: "octocat:slackcat,user1:slack1" would map GitHub username "octocat" to Slack username "slackcat".
40+
default: ""
3141
with-test-data:
3242
description: Append some test data to the Slack post.
3343
required: false

dist/index.js

Lines changed: 258 additions & 53 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: 1 addition & 1 deletion
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": "1.2.1",
4+
"version": "1.3.0",
55
"author": "Colten Krauter <coltenkrauter>",
66
"type": "module",
77
"homepage": "https://buymeacoffee.com/coltenkrauter",

src/app.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ 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/client.js'
16-
import { getApprovedPullRequest } from './utils/test-data.js'
16+
import { getApprovedPullRequest, getChangesRequestedPullRequest } from './utils/test-data.js'
1717
import { parseInputs as getInputs } from './input-parser.js'
1818

1919
const { homepage, name, version } = pkg
@@ -73,13 +73,12 @@ async function main(): Promise<void> {
7373
throw new Error('All GitHub tokens failed to process')
7474
}
7575

76-
console.log(`Successfully processed ${results.length} out of ${githubConfig.tokens.length} tokens`)
76+
console.log(`Successfully processed [${results.length}] out of [${githubConfig.tokens.length}] tokens`)
7777

7878
await slack.enforceAppNamePattern(/.*github[\s-_]?notifier$/i)
7979

8080
const pulls: Pull[] = results.flatMap((result) => result.pulls)
81-
console.log(`Found ${pulls.length} pulls`)
82-
console.log(pulls)
81+
console.log(`Found [${pulls.length}] pulls`)
8382

8483
// Multiple tokens may have overlapping repository access, deduplicate PRs by org/repo/number
8584
const dedupedPulls = [...new Map(pulls.map((pull) => [`${pull.org}/${pull.repo}/${pull.number}`, pull]))].map(
@@ -95,13 +94,15 @@ async function main(): Promise<void> {
9594
}
9695

9796
if (withTestData) {
98-
console.log(`With test data: [${withTestData}]`)
99-
const testDataPullRequest = getApprovedPullRequest()
97+
console.log(`With test data [${withTestData}]`)
98+
const test1 = getApprovedPullRequest()
10099
for (let i = 1; i <= 2; i++) {
101-
blocks = [
102-
...blocks,
103-
...(await getPullBlocks({ ...testDataPullRequest, number: i }, slack, withUserMentions)),
104-
]
100+
blocks = [...blocks, ...(await getPullBlocks({ ...test1, number: i }, slack, withUserMentions))]
101+
}
102+
103+
const test2 = getChangesRequestedPullRequest()
104+
for (let i = 1; i <= 2; i++) {
105+
blocks = [...blocks, ...(await getPullBlocks({ ...test2, number: i }, slack, withUserMentions))]
105106
}
106107
}
107108

src/input-parser.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { debug, getBooleanInput, getInput } from '@actions/core'
22
import { stringToArray } from '@krauters/utils'
33

44
import type { InputProps } from './structures.js'
5+
import type { UserMapping } from './utils/slack/structures.js'
56

67
/**
78
* Parses and validates all inputs required for the GitHub Notifier.
@@ -21,6 +22,18 @@ export function parseInputs(): InputProps {
2122
const withUserMentions = getBooleanInput('with-user-mentions')
2223
const repositoryFilter = stringToArray(getInput('repository-filter'))
2324

25+
const userMappings = stringToArray(getInput('user-mappings'))
26+
.map((entry) => {
27+
const [github, slack] = entry.split(':').map((part) => part?.trim())
28+
29+
return github && slack ? { githubUsername: github, slackUsername: slack } : null
30+
})
31+
.filter((mapping): mapping is UserMapping => mapping !== null)
32+
33+
if (userMappings.length > 0) {
34+
debug(`Parsed [${userMappings.length}] GitHub to Slack user mappings`)
35+
}
36+
2437
// https://github.com/actions/github-script/issues/436
2538
const baseUrl = getInput('base-url') || process.env.GITHUB_API_URL
2639

@@ -35,6 +48,7 @@ export function parseInputs(): InputProps {
3548
slackConfig: {
3649
channels,
3750
token: slackToken,
51+
userMappings,
3852
},
3953
withArchived,
4054
withDrafts,

src/utils/slack/client.ts

Lines changed: 27 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,31 @@ import type { Bot } from '@slack/web-api/dist/types/response/BotsInfoResponse.js
44
import type { Member } from '@slack/web-api/dist/types/response/UsersListResponse.js'
55
import type { User } from '@slack/web-api/dist/types/response/UsersLookupByEmailResponse.js'
66

7+
import { debug } from '@actions/core'
78
import { getBatches } from '@krauters/utils'
89
import { WebClient } from '@slack/web-api'
910

10-
import { type GetUser, SlackAppUrl, type SlackConfig } from './structures.js'
11+
import { type GetUser, SlackAppUrl, type SlackConfig, type UserMapping } from './structures.js'
12+
import { createUserMatchers, logFailedMatches, type MatchParams } from './user-matchers.js'
1113

1214
export class SlackClient {
1315
public bot?: Bot
1416
private channels: string[]
1517
private client: WebClient
18+
private userMappings: UserMapping[]
1619
private users: undefined | User[]
1720

1821
/**
1922
* Slack client for interacting with the Slack API.
2023
*
2124
* @param token Slack token.
2225
* @param channels Slack channel IDs for posting messages in.
26+
* @param userMappings User mappings for the Slack client.
2327
*/
24-
constructor({ channels, token }: SlackConfig) {
28+
constructor({ channels, token, userMappings = [] }: SlackConfig) {
2529
this.client = new WebClient(token)
2630
this.channels = channels
31+
this.userMappings = userMappings
2732
}
2833

2934
/**
@@ -101,59 +106,35 @@ export class SlackClient {
101106
* @param [botId] The botId for the bot to find.
102107
*/
103108
async getUser({ email, userId, username }: GetUser): Promise<Member | undefined> {
104-
console.log(`Getting Slack UserId for email [${email}], username [${username}], and userId [${userId}]...`)
109+
debug(`Getting Slack UserId for email [${email}], username [${username}], and userId [${userId}]...`)
105110

106111
const users = this.users ?? (await this.getAllusers())
112+
const matchParams: MatchParams = { email, userId, userMappings: this.userMappings, username }
113+
const matchers = createUserMatchers(matchParams)
107114

108-
// Define matching functions for better readability and extensibility
109-
const matchById = (user: Member) => userId && user.id === userId
110-
const matchByEmail = (user: Member) => email && user.profile?.email === email
111-
const matchByEmailContainsUsername = (user: Member) =>
112-
username && String(user.profile?.email ?? '').includes(username)
113-
const matchByDisplayName = (user: Member) => username && user.profile?.display_name === username
114-
const matchByRealName = (user: Member) => username && user.profile?.real_name === username
115-
116-
const user = users.find((user: Member) => {
117-
const idMatch = matchById(user)
118-
const emailMatch = matchByEmail(user)
119-
const emailContainsUsernameMatch = matchByEmailContainsUsername(user)
120-
const displayNameMatch = matchByDisplayName(user)
121-
const realNameMatch = matchByRealName(user)
122-
123-
// Log the first match attempt that succeeds for debugging
124-
if (idMatch && userId) console.log(`Match found by userId [${userId}] with Slack userId [${user.id}]`)
125-
else if (emailMatch && email)
126-
console.log(`Match found by email [${email}] with Slack email [${user.profile?.email}]`)
127-
else if (emailContainsUsernameMatch && username)
128-
console.log(`Match found by username [${username}] contained in Slack email [${user.profile?.email}]`)
129-
else if (displayNameMatch && username)
130-
console.log(
131-
`Match found by username [${username}] matching Slack display_name [${user.profile?.display_name}]`,
132-
)
133-
else if (realNameMatch && username)
134-
console.log(
135-
`Match found by username [${username}] matching Slack real_name [${user.profile?.real_name}]`,
136-
)
115+
// Find the first user that matches any of our criteria
116+
const matchedUser = users.find((slackUser) => {
117+
// Find a matching criteria for this Slack user, if any
118+
const matchedCriteria = matchers.find((criteria) => criteria.check(slackUser))
119+
120+
// If we found a match, log it
121+
if (matchedCriteria) {
122+
matchedCriteria.log(slackUser)
137123

138-
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
139-
return idMatch || emailMatch || emailContainsUsernameMatch || displayNameMatch || realNameMatch
124+
return true
125+
}
126+
127+
return false
140128
})
141129

142-
if (user) {
143-
console.log(`User found with userId [${user.id}]`)
130+
if (matchedUser) {
131+
debug(`User found with userId [${matchedUser.id}]`)
144132

145-
return user
133+
return matchedUser
146134
}
147135

148-
console.log(`No user match found after checking against [${users.length}] users`)
149-
if (userId) console.log(`Tried to match userId [${userId}] against Slack user.id fields`)
150-
if (email) console.log(`Tried to match email [${email}] against Slack user.profile.email fields`)
151-
if (username)
152-
console.log(
153-
`Tried to match username [${username}] against Slack user.profile.email (contains), display_name and real_name fields`,
154-
)
155-
156-
console.log(`Since no Slack user match found, unable to @mention user or use their profile image`)
136+
// No match found, log the failure
137+
logFailedMatches(matchParams, users.length)
157138
}
158139

159140
/**

src/utils/slack/structures.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export enum SlackAppUrl {
22
Prefix = 'https://api.slack.com/apps',
3-
SuffixDisplayInfo = 'general#display_info_form',
3+
SuffixDisplayInfo = 'general',
44
}
55

66
export interface GetUser {
@@ -12,4 +12,10 @@ export interface GetUser {
1212
export interface SlackConfig {
1313
channels: string[]
1414
token: string
15+
userMappings?: UserMapping[]
16+
}
17+
18+
export interface UserMapping {
19+
githubUsername: string
20+
slackUsername: string
1521
}

0 commit comments

Comments
 (0)