From 659f342ab8a82e1a7e8b1a173c9ee81d766e704b Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Thu, 15 Feb 2024 08:46:59 -0500 Subject: [PATCH 01/12] feat: support ci workflow notifications --- src/utils/helpers.test.ts | 18 ++++++++------ src/utils/helpers.ts | 51 +++++++++++++++++++++++++++------------ 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/utils/helpers.test.ts b/src/utils/helpers.test.ts index f9a60b455..f8fe4b280 100644 --- a/src/utils/helpers.test.ts +++ b/src/utils/helpers.test.ts @@ -30,19 +30,15 @@ describe('utils/helpers.ts', () => { mockedUser.id, ); expect(referrerId).toBe( - 'notification_referrer_id=MDE4Ok5vdGlmaWNhdGlvblRocmVhZDEzODY2MTA5NjoxMjM0NTY3ODk=', + 'MDE4Ok5vdGlmaWNhdGlvblRocmVhZDEzODY2MTA5NjoxMjM0NTY3ODk=', ); }); }); describe('generateGitHubWebUrl', () => { - let notificationReferrerId; - beforeAll(() => { - notificationReferrerId = generateNotificationReferrerId( - mockedSingleNotification.id, - mockedUser.id, - ); - }); + let notificationReferrerId = + 'notification_referrer_id=MDE4Ok5vdGlmaWNhdGlvblRocmVhZDEzODY2MTA5NjoxMjM0NTY3ODk%253D'; + beforeAll(() => {}); it('should generate the GitHub url - non enterprise - (issue)', () => testGenerateUrl( @@ -110,6 +106,12 @@ describe('utils/helpers.ts', () => { ), )); + it('should generate the GitHub workflow url with encoded workflow name', () => + testGenerateUrl( + `${URL.normal.api}/actions?workflow=Some Workflow`, + `${URL.normal.default}/actions?workflow=Some+Workflow&${notificationReferrerId}`, + )); + function testGenerateUrl(apiUrl, ExpectedResult, comment?) { const notif = { ...mockedSingleNotification, subject: { url: apiUrl } }; expect( diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 708aac3af..7befbb2a3 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -29,7 +29,7 @@ export function generateNotificationReferrerId( const buffer = Buffer.from( `018:NotificationThread${notificationId}:${userId}`, ); - return `notification_referrer_id=${buffer.toString('base64')}`; + return buffer.toString('base64'); } export function generateGitHubWebUrl( @@ -38,28 +38,32 @@ export function generateGitHubWebUrl( userId?: number, comment: string = '', ) { - const { hostname } = new URL(url); + const newURL = new URL(url); + const hostname = newURL.hostname; + let params = new URLSearchParams(newURL.search); + const isEnterprise = hostname !== `api.${Constants.DEFAULT_AUTH_OPTIONS.hostname}`; - let newUrl: string = isEnterprise - ? url.replace(`${hostname}/api/v3/repos`, hostname) - : url.replace('api.github.com/repos', 'github.com'); - - if (newUrl.indexOf('/pulls/') !== -1) { - newUrl = newUrl.replace('/pulls/', '/pull/'); + if (isEnterprise) { + newURL.href = newURL.href.replace(`${hostname}/api/v3/repos`, hostname); + } else { + newURL.href = newURL.href.replace('api.github.com/repos', 'github.com'); } + newURL.href = newURL.href.replace('/pulls/', '/pull/'); + if (userId) { const notificationReferrerId = generateNotificationReferrerId( notificationId, userId, ); - return `${newUrl}?${notificationReferrerId}${comment}`; + params.append('notification_referrer_id', notificationReferrerId); + newURL.search = params.toString(); } - return newUrl + comment; + return encodeURI(newURL.href + comment); } const addHours = (date: string, hours: number) => @@ -161,12 +165,7 @@ export async function openInBrowser( if (notification.subject.type === 'Release') { getReleaseTagWebUrl(notification, accounts.token).then(({ url }) => openExternalLink( - generateGitHubWebUrl( - url, - notification.id, - accounts.user?.id, - undefined, - ), + generateGitHubWebUrl(url, notification.id, accounts.user?.id), ), ); } else if (notification.subject.type === 'Discussion') { @@ -195,5 +194,25 @@ export async function openInBrowser( latestCommentId ? '#issuecomment-' + latestCommentId : undefined, ), ); + } else if (notification.reason === 'ci_activity') { + const workflowName = notification.subject.title + .split('workflow run')[0] + .trim(); + + openExternalLink( + generateGitHubWebUrl( + `${notification.repository.html_url}/actions?workflow=${workflowName}`, + notification.id, + accounts.user?.id, + ), + ); + } else { + openExternalLink( + generateGitHubWebUrl( + notification.repository.html_url, + notification.id, + accounts.user?.id, + ), + ); } } From e8465d2207727167e4c25d97d0183e0977544f54 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Thu, 15 Feb 2024 09:04:16 -0500 Subject: [PATCH 02/12] feat: support ci workflow notifications --- src/utils/helpers.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 7befbb2a3..c7025fe52 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -182,28 +182,28 @@ export async function openInBrowser( ), ), ); - } else if (notification.subject.url) { - const latestCommentId = getCommentId( - notification.subject.latest_comment_url, - ); + } else if (notification.subject.type === 'CheckSuite') { + const workflowName = notification.subject.title + .split('workflow run')[0] + .trim(); + openExternalLink( generateGitHubWebUrl( - notification.subject.url, + `${notification.repository.html_url}/actions?workflow=${workflowName}`, notification.id, accounts.user?.id, - latestCommentId ? '#issuecomment-' + latestCommentId : undefined, ), ); - } else if (notification.reason === 'ci_activity') { - const workflowName = notification.subject.title - .split('workflow run')[0] - .trim(); - + } else if (notification.subject.url) { + const latestCommentId = getCommentId( + notification.subject.latest_comment_url, + ); openExternalLink( generateGitHubWebUrl( - `${notification.repository.html_url}/actions?workflow=${workflowName}`, + notification.subject.url, notification.id, accounts.user?.id, + latestCommentId ? '#issuecomment-' + latestCommentId : undefined, ), ); } else { From 820531bf1098e7b94a0612896b9ea953d3e43500 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Thu, 15 Feb 2024 11:37:40 -0500 Subject: [PATCH 03/12] feat: support ci workflow notifications --- src/typesGithub.ts | 15 ++++-- src/utils/github-api.test.ts | 45 +++++++++++++++++- src/utils/github-api.ts | 44 +++++++++++++----- src/utils/helpers.test.ts | 24 +++++++++- src/utils/helpers.ts | 89 ++++++++++++++++++++++++++++++++---- 5 files changed, 189 insertions(+), 28 deletions(-) diff --git a/src/typesGithub.ts b/src/typesGithub.ts index 3f4ac637f..84604a3a1 100644 --- a/src/typesGithub.ts +++ b/src/typesGithub.ts @@ -1,6 +1,8 @@ export type Reason = + | 'approval_requested' | 'assign' | 'author' + | 'ci_activity' | 'comment' | 'invitation' | 'manual' @@ -9,8 +11,7 @@ export type Reason = | 'security_alert' | 'state_change' | 'subscribed' - | 'team_mention' - | 'ci_activity'; + | 'team_mention'; export type SubjectType = | 'CheckSuite' @@ -20,7 +21,8 @@ export type SubjectType = | 'PullRequest' | 'Release' | 'RepositoryInvitation' - | 'RepositoryVulnerabilityAlert'; + | 'RepositoryVulnerabilityAlert' + | 'WorkflowRun'; export type IssueStateType = | 'closed' @@ -28,10 +30,17 @@ export type IssueStateType = | 'completed' | 'reopened' | 'not_planned'; + export type PullRequestStateType = 'closed' | 'open' | 'merged' | 'draft'; + export type StateType = IssueStateType | PullRequestStateType; + export type ViewerSubscription = 'IGNORED' | 'SUBSCRIBED' | 'UNSUBSCRIBED'; +// TODO - the following additional statuses are to be implemented #767 +// queued, in progress, neutral, action required, timed out, skipped, stale +export type WorkflowType = 'success' | 'failure' | 'cancelled' | 'waiting'; + export interface Notification { id: string; unread: boolean; diff --git a/src/utils/github-api.test.ts b/src/utils/github-api.test.ts index 73a7ef442..1bee47da0 100644 --- a/src/utils/github-api.test.ts +++ b/src/utils/github-api.test.ts @@ -1,4 +1,8 @@ -import { formatReason, getNotificationTypeIcon } from './github-api'; +import { + formatReason, + getNotificationTypeIcon, + getWorkflowTypeFromTitle, +} from './github-api'; import { Reason, SubjectType } from '../typesGithub'; describe('./utils/github-api.ts', () => { @@ -34,8 +38,47 @@ describe('./utils/github-api.ts', () => { expect( getNotificationTypeIcon('RepositoryVulnerabilityAlert').displayName, ).toBe('AlertIcon'); + expect(getNotificationTypeIcon('WorkflowRun').displayName).toBe( + 'RocketIcon', + ); expect(getNotificationTypeIcon('Unknown' as SubjectType).displayName).toBe( 'QuestionIcon', ); }); + + describe('should get the workflow status type from title', () => { + it('should infer failed workflow status from the title', () => { + expect( + getWorkflowTypeFromTitle('Demo workflow run failed for main branch'), + ).toBe('failure'); + }); + + it('should infer success workflow status from the title', () => { + expect( + getWorkflowTypeFromTitle('Demo workflow run succeeded for main branch'), + ).toBe('success'); + }); + + it('should infer cancelled workflow status from the title', () => { + expect( + getWorkflowTypeFromTitle('Demo workflow run cancelled for main branch'), + ).toBe('cancelled'); + }); + + it('should infer approval waiting status from the title', () => { + expect( + getWorkflowTypeFromTitle( + 'user requested your review to deploy to an environment', + ), + ).toBe('waiting'); + }); + + it('should return null for known workflow status', () => { + expect( + getWorkflowTypeFromTitle( + 'Demo workflow run has not status for main branch', + ), + ).toBeNull(); + }); + }); }); diff --git a/src/utils/github-api.ts b/src/utils/github-api.ts index 8a0a5d760..8540e0c88 100644 --- a/src/utils/github-api.ts +++ b/src/utils/github-api.ts @@ -7,15 +7,18 @@ import { MailIcon, OcticonProps, QuestionIcon, + RocketIcon, SyncIcon, TagIcon, } from '@primer/octicons-react'; -import { Reason, StateType, SubjectType } from '../typesGithub'; +import { Reason, StateType, SubjectType, WorkflowType } from '../typesGithub'; // prettier-ignore const DESCRIPTIONS = { + APPROVAL_REQUESTED: 'You were requested to review and approve a deployment workflow.', ASSIGN: 'You were assigned to the issue.', AUTHOR: 'You created the thread.', + CI_ACTIVITY: 'A GitHub Actions workflow run was triggered for your repository', COMMENT: 'You commented on the thread.', INVITATION: 'You accepted an invitation to contribute to the repository.', MANUAL: 'You subscribed to the thread (via an issue or pull request).', @@ -25,7 +28,6 @@ const DESCRIPTIONS = { STATE_CHANGE: 'You changed the thread state (for example, closing an issue or merging a pull request).', SUBSCRIBED: "You're watching the repository.", TEAM_MENTION: 'You were on a team that was mentioned.', - CI_ACTIVITY: 'A GitHub Actions workflow run was triggered for your repository', UNKNOWN: 'The reason for this notification is not supported by the app.', }; @@ -35,10 +37,14 @@ export function formatReason(reason: Reason): { } { // prettier-ignore switch (reason) { + case 'approval_requested': + return { type: 'Approval Requested', description: DESCRIPTIONS['APPROVAL_REQUESTED'] }; case 'assign': return { type: 'Assign', description: DESCRIPTIONS['ASSIGN'] }; case 'author': return { type: 'Author', description: DESCRIPTIONS['AUTHOR'] }; + case 'ci_activity': + return { type: 'Workflow Run', description: DESCRIPTIONS['CI_ACTIVITY'] }; case 'comment': return { type: 'Comment', description: DESCRIPTIONS['COMMENT'] }; case 'invitation': @@ -57,8 +63,6 @@ export function formatReason(reason: Reason): { return { type: 'Subscribed', description: DESCRIPTIONS['SUBSCRIBED'] }; case 'team_mention': return { type: 'Team Mention', description: DESCRIPTIONS['TEAM_MENTION'] }; - case 'ci_activity': - return { type: 'Workflow Run', description: DESCRIPTIONS['CI_ACTIVITY'] }; default: return { type: 'Unknown', description: DESCRIPTIONS['UNKNOWN'] }; } @@ -84,27 +88,43 @@ export function getNotificationTypeIcon( return MailIcon; case 'RepositoryVulnerabilityAlert': return AlertIcon; + case 'WorkflowRun': + return RocketIcon; default: return QuestionIcon; } } +export function getWorkflowTypeFromTitle(title: string): WorkflowType | null { + if (title.includes('succeeded')) { + return 'success'; + } else if (title.includes('failed')) { + return 'failure'; + } else if (title.includes('cancelled')) { + return 'cancelled'; + } else if (title.includes('requested your review')) { + return 'waiting'; + } + + return null; +} + export function getNotificationTypeIconColor(state: StateType): string { switch (state) { case 'closed': return 'text-red-500'; - case 'open': - return 'text-green-500'; - case 'merged': - return 'text-purple-500'; - case 'reopened': - return 'text-green-500'; - case 'not_planned': - return 'text-gray-300'; case 'completed': return 'text-purple-500'; case 'draft': return 'text-gray-600'; + case 'merged': + return 'text-purple-500'; + case 'not_planned': + return 'text-gray-300'; + case 'open': + return 'text-green-500'; + case 'reopened': + return 'text-green-500'; default: return 'text-gray-300'; } diff --git a/src/utils/helpers.test.ts b/src/utils/helpers.test.ts index f8fe4b280..838e44e4b 100644 --- a/src/utils/helpers.test.ts +++ b/src/utils/helpers.test.ts @@ -4,6 +4,7 @@ import { generateNotificationReferrerId, getCommentId, getLatestDiscussionCommentId, + inferWorkflowBranchFromTitle, } from './helpers'; import { mockedSingleNotification, @@ -37,8 +38,7 @@ describe('utils/helpers.ts', () => { describe('generateGitHubWebUrl', () => { let notificationReferrerId = - 'notification_referrer_id=MDE4Ok5vdGlmaWNhdGlvblRocmVhZDEzODY2MTA5NjoxMjM0NTY3ODk%253D'; - beforeAll(() => {}); + 'notification_referrer_id=MDE4Ok5vdGlmaWNhdGlvblRocmVhZDEzODY2MTA5NjoxMjM0NTY3ODk%3D'; it('should generate the GitHub url - non enterprise - (issue)', () => testGenerateUrl( @@ -136,4 +136,24 @@ describe('utils/helpers.ts', () => { expect(result).toBe('https://github.manos.im/api/v3/'); }); }); + + describe('inferWorkflowBranchFromTitle', () => { + let notification; + + beforeAll(() => { + notification = mockedSingleNotification; + }); + + it('should infer branch name from the title', () => { + notification.subject.title = + 'Demo workflow run succeeded for main branch'; + expect(inferWorkflowBranchFromTitle(notification)).toBe('main'); + }); + + it('should return null for known branch name', () => { + notification.subject.title = + 'Demo workflow run does not have branch name'; + expect(inferWorkflowBranchFromTitle(notification)).toBeNull(); + }); + }); }); diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index c7025fe52..8108c82dd 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -7,6 +7,7 @@ import { import { apiRequestAuth } from '../utils/api-requests'; import { openExternalLink } from '../utils/comms'; import { Constants } from './constants'; +import { getWorkflowTypeFromTitle } from './github-api'; export function getEnterpriseAccountToken( hostname: string, @@ -63,7 +64,7 @@ export function generateGitHubWebUrl( newURL.search = params.toString(); } - return encodeURI(newURL.href + comment); + return newURL.href + comment; } const addHours = (date: string, hours: number) => @@ -146,6 +147,74 @@ async function getDiscussionUrl( }; } +export function inferWorkflowStatusFilterFromTitle( + notification: Notification, +): string { + const title = notification.subject.title; + + // TODO - the following additional statuses are to be implemented #767 + // queued, in progress, neutral, action required, timed out, skipped, stale + if (title.includes('succeeded')) { + return 'is:success'; + } else if (title.includes('failed')) { + return 'is:failure'; + } else if (title.includes('cancelled')) { + return 'is:cancelled'; + } else if (title.includes('requested your review')) { + return 'is:waiting'; + } + + return null; +} + +export function inferWorkflowBranchFromTitle( + notification: Notification, +): string { + const title = notification.subject.title; + + const titleParts = title.split('for'); + + if (titleParts[1]) { + return titleParts[1].replace('branch', '').trim(); + } + + return null; +} + +async function getCheckSuiteUrl(notification: Notification) { + let titleParts = notification.subject.title.split('workflow run'); + const workflowName = titleParts[0].trim().replaceAll(' ', '+'); + + titleParts = titleParts[1].split('for'); + const workflowBranch = titleParts[1].replace('branch', '').trim(); + + const workflowStatus = getWorkflowTypeFromTitle(notification.subject.title); + + let url = `${notification.repository.html_url}/actions?workflow:"${workflowName}"`; + + if (workflowStatus) { + url += `+is:${workflowStatus}`; + } + + if (workflowBranch) { + url += `+branch:${workflowBranch}`; + } + + return url; +} + +async function getWorkflowRunUrl(notification: Notification) { + const workflowStatusQuery = inferWorkflowStatusFilterFromTitle(notification); + + let url = `${notification.repository.html_url}/actions`; + + if (workflowStatusQuery) { + url += `?${workflowStatusQuery}`; + + return url; + } +} + export const getLatestDiscussionCommentId = ( comments: DiscussionCommentEdge[], ) => @@ -183,15 +252,15 @@ export async function openInBrowser( ), ); } else if (notification.subject.type === 'CheckSuite') { - const workflowName = notification.subject.title - .split('workflow run')[0] - .trim(); - - openExternalLink( - generateGitHubWebUrl( - `${notification.repository.html_url}/actions?workflow=${workflowName}`, - notification.id, - accounts.user?.id, + getCheckSuiteUrl(notification).then((url) => + openExternalLink( + generateGitHubWebUrl(url, notification.id, accounts.user?.id), + ), + ); + } else if (notification.subject.type === 'WorkflowRun') { + getWorkflowRunUrl(notification).then((url) => + openExternalLink( + generateGitHubWebUrl(url, notification.id, accounts.user?.id), ), ); } else if (notification.subject.url) { From 695cbccbc8da0c0534134c7577d8a2bcd13ec9a3 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Thu, 15 Feb 2024 11:42:44 -0500 Subject: [PATCH 04/12] feat: support ci workflow notifications --- src/utils/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 8108c82dd..08531a869 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -190,7 +190,7 @@ async function getCheckSuiteUrl(notification: Notification) { const workflowStatus = getWorkflowTypeFromTitle(notification.subject.title); - let url = `${notification.repository.html_url}/actions?workflow:"${workflowName}"`; + let url = `${notification.repository.html_url}/actions?query=workflow:"${workflowName}"`; if (workflowStatus) { url += `+is:${workflowStatus}`; From 75c42483267f6b9d8e7a54396e6829d0cc9e2400 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Thu, 15 Feb 2024 11:44:39 -0500 Subject: [PATCH 05/12] feat: support ci workflow notifications --- src/utils/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 08531a869..17f544fb4 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -209,7 +209,7 @@ async function getWorkflowRunUrl(notification: Notification) { let url = `${notification.repository.html_url}/actions`; if (workflowStatusQuery) { - url += `?${workflowStatusQuery}`; + url += `?query=${workflowStatusQuery}`; return url; } From e3f2dd8438a5abb7df4922728e3352e32e52731b Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Thu, 15 Feb 2024 12:18:43 -0500 Subject: [PATCH 06/12] refactor: remove unused function --- src/utils/helpers.ts | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 17f544fb4..93e1e8cd9 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -147,26 +147,6 @@ async function getDiscussionUrl( }; } -export function inferWorkflowStatusFilterFromTitle( - notification: Notification, -): string { - const title = notification.subject.title; - - // TODO - the following additional statuses are to be implemented #767 - // queued, in progress, neutral, action required, timed out, skipped, stale - if (title.includes('succeeded')) { - return 'is:success'; - } else if (title.includes('failed')) { - return 'is:failure'; - } else if (title.includes('cancelled')) { - return 'is:cancelled'; - } else if (title.includes('requested your review')) { - return 'is:waiting'; - } - - return null; -} - export function inferWorkflowBranchFromTitle( notification: Notification, ): string { @@ -204,12 +184,12 @@ async function getCheckSuiteUrl(notification: Notification) { } async function getWorkflowRunUrl(notification: Notification) { - const workflowStatusQuery = inferWorkflowStatusFilterFromTitle(notification); + const workflowStatus = getWorkflowTypeFromTitle(notification.subject.title); let url = `${notification.repository.html_url}/actions`; - if (workflowStatusQuery) { - url += `?query=${workflowStatusQuery}`; + if (workflowStatus) { + url += `?query=${workflowStatus}`; return url; } From 999a979d531968aa5c365050a40c956f1ea0df6a Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Thu, 15 Feb 2024 12:23:08 -0500 Subject: [PATCH 07/12] test: add coverage --- .../__snapshots__/github-api.test.ts.snap | 43 +++++++++++-------- src/utils/github-api.test.ts | 3 +- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/utils/__snapshots__/github-api.test.ts.snap b/src/utils/__snapshots__/github-api.test.ts.snap index 8b1b1033a..654146aec 100644 --- a/src/utils/__snapshots__/github-api.test.ts.snap +++ b/src/utils/__snapshots__/github-api.test.ts.snap @@ -1,90 +1,97 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`./utils/github-api.ts should format the notification reason 1`] = ` +{ + "description": "You were requested to review and approve a deployment workflow.", + "type": "Approval Requested", +} +`; + +exports[`./utils/github-api.ts should format the notification reason 2`] = ` { "description": "You were assigned to the issue.", "type": "Assign", } `; -exports[`./utils/github-api.ts should format the notification reason 2`] = ` +exports[`./utils/github-api.ts should format the notification reason 3`] = ` { "description": "You created the thread.", "type": "Author", } `; -exports[`./utils/github-api.ts should format the notification reason 3`] = ` +exports[`./utils/github-api.ts should format the notification reason 4`] = ` +{ + "description": "A GitHub Actions workflow run was triggered for your repository", + "type": "Workflow Run", +} +`; + +exports[`./utils/github-api.ts should format the notification reason 5`] = ` { "description": "You commented on the thread.", "type": "Comment", } `; -exports[`./utils/github-api.ts should format the notification reason 4`] = ` +exports[`./utils/github-api.ts should format the notification reason 6`] = ` { "description": "You accepted an invitation to contribute to the repository.", "type": "Invitation", } `; -exports[`./utils/github-api.ts should format the notification reason 5`] = ` +exports[`./utils/github-api.ts should format the notification reason 7`] = ` { "description": "You subscribed to the thread (via an issue or pull request).", "type": "Manual", } `; -exports[`./utils/github-api.ts should format the notification reason 6`] = ` +exports[`./utils/github-api.ts should format the notification reason 8`] = ` { "description": "You were specifically @mentioned in the content.", "type": "Mention", } `; -exports[`./utils/github-api.ts should format the notification reason 7`] = ` +exports[`./utils/github-api.ts should format the notification reason 9`] = ` { "description": "You, or a team you're a member of, were requested to review a pull request.", "type": "Review Requested", } `; -exports[`./utils/github-api.ts should format the notification reason 8`] = ` +exports[`./utils/github-api.ts should format the notification reason 10`] = ` { "description": "GitHub discovered a security vulnerability in your repository.", "type": "Security Alert", } `; -exports[`./utils/github-api.ts should format the notification reason 9`] = ` +exports[`./utils/github-api.ts should format the notification reason 11`] = ` { "description": "You changed the thread state (for example, closing an issue or merging a pull request).", "type": "State Change", } `; -exports[`./utils/github-api.ts should format the notification reason 10`] = ` +exports[`./utils/github-api.ts should format the notification reason 12`] = ` { "description": "You're watching the repository.", "type": "Subscribed", } `; -exports[`./utils/github-api.ts should format the notification reason 11`] = ` +exports[`./utils/github-api.ts should format the notification reason 13`] = ` { "description": "You were on a team that was mentioned.", "type": "Team Mention", } `; -exports[`./utils/github-api.ts should format the notification reason 12`] = ` -{ - "description": "A GitHub Actions workflow run was triggered for your repository", - "type": "Workflow Run", -} -`; - -exports[`./utils/github-api.ts should format the notification reason 13`] = ` +exports[`./utils/github-api.ts should format the notification reason 14`] = ` { "description": "The reason for this notification is not supported by the app.", "type": "Unknown", diff --git a/src/utils/github-api.test.ts b/src/utils/github-api.test.ts index 1bee47da0..ae6845a0f 100644 --- a/src/utils/github-api.test.ts +++ b/src/utils/github-api.test.ts @@ -7,8 +7,10 @@ import { Reason, SubjectType } from '../typesGithub'; describe('./utils/github-api.ts', () => { it('should format the notification reason', () => { + expect(formatReason('approval_requested')).toMatchSnapshot(); expect(formatReason('assign')).toMatchSnapshot(); expect(formatReason('author')).toMatchSnapshot(); + expect(formatReason('ci_activity')).toMatchSnapshot(); expect(formatReason('comment')).toMatchSnapshot(); expect(formatReason('invitation')).toMatchSnapshot(); expect(formatReason('manual')).toMatchSnapshot(); @@ -18,7 +20,6 @@ describe('./utils/github-api.ts', () => { expect(formatReason('state_change')).toMatchSnapshot(); expect(formatReason('subscribed')).toMatchSnapshot(); expect(formatReason('team_mention')).toMatchSnapshot(); - expect(formatReason('ci_activity')).toMatchSnapshot(); expect(formatReason('something_else_unknown' as Reason)).toMatchSnapshot(); }); From 409177bf373322da6f60908dda2329134f508c0d Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Thu, 15 Feb 2024 12:56:44 -0500 Subject: [PATCH 08/12] refactor: use regex and add coverage --- src/utils/helpers.test.ts | 58 +++++++++++++++++++++++++++++++++++++- src/utils/helpers.ts | 59 ++++++++++++++++++++++++--------------- 2 files changed, 93 insertions(+), 24 deletions(-) diff --git a/src/utils/helpers.test.ts b/src/utils/helpers.test.ts index 838e44e4b..f3b0dfa2a 100644 --- a/src/utils/helpers.test.ts +++ b/src/utils/helpers.test.ts @@ -5,6 +5,8 @@ import { getCommentId, getLatestDiscussionCommentId, inferWorkflowBranchFromTitle, + getCheckSuiteUrl, + getWorkflowRunUrl, } from './helpers'; import { mockedSingleNotification, @@ -140,7 +142,7 @@ describe('utils/helpers.ts', () => { describe('inferWorkflowBranchFromTitle', () => { let notification; - beforeAll(() => { + beforeEach(() => { notification = mockedSingleNotification; }); @@ -156,4 +158,58 @@ describe('utils/helpers.ts', () => { expect(inferWorkflowBranchFromTitle(notification)).toBeNull(); }); }); + + describe('getCheckSuiteUrl', () => { + let notification; + + beforeEach(() => { + notification = mockedSingleNotification; + }); + + it('should generate a Github Action (CheckSuite) url', () => { + notification.subject.title = 'Demo workflow run'; + + const result = getCheckSuiteUrl(notification); + expect(result).toBe( + 'https://github.com/manosim/notifications-test/actions', + ); + }); + + it('should generate a Github Action (CheckSuite) url with filters', () => { + notification.subject.title = + 'Demo workflow run succeeded for main branch'; + + const result = getCheckSuiteUrl(notification); + expect(result).toBe( + 'https://github.com/manosim/notifications-test/actions?query=workflow:"Demo"+is:success+branch:main', + ); + }); + }); + + describe('getWorkflowRunUrl', () => { + let notification; + + beforeEach(() => { + notification = mockedSingleNotification; + }); + + it('should generate a Github Action (WorkflowRun) url', () => { + notification.subject.title = 'some title'; + + const result = getWorkflowRunUrl(notification); + expect(result).toBe( + 'https://github.com/manosim/notifications-test/actions', + ); + }); + + it('should generate a Github Action (WorkflowRun) url for deployment review', () => { + notification.subject.title = + 'userA requested your review to deploy to an environment'; + + const result = getWorkflowRunUrl(notification); + expect(result).toBe( + 'https://github.com/manosim/notifications-test/actions?query=is:waiting', + ); + }); + }); }); diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 93e1e8cd9..b43ac74fa 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -161,38 +161,51 @@ export function inferWorkflowBranchFromTitle( return null; } -async function getCheckSuiteUrl(notification: Notification) { - let titleParts = notification.subject.title.split('workflow run'); - const workflowName = titleParts[0].trim().replaceAll(' ', '+'); +export function getCheckSuiteUrl(notification: Notification) { + let url = `${notification.repository.html_url}/actions`; + let filters = []; - titleParts = titleParts[1].split('for'); - const workflowBranch = titleParts[1].replace('branch', '').trim(); + const regexPattern = + /^(?.*?) workflow run (?.*?) for (?.*?) branch$/; + const matches = regexPattern.exec(notification.subject.title); - const workflowStatus = getWorkflowTypeFromTitle(notification.subject.title); + if (matches) { + const { groups } = matches; - let url = `${notification.repository.html_url}/actions?query=workflow:"${workflowName}"`; + if (groups.workflowName) { + filters.push(`workflow:"${groups.workflowName.replaceAll(' ', '+')}"`); + } - if (workflowStatus) { - url += `+is:${workflowStatus}`; + if (groups.workflowStatus) { + const workflowStatus = getWorkflowTypeFromTitle( + notification.subject.title, + ); + + filters.push(`is:${workflowStatus}`); + } + + if (groups.branchName) { + filters.push(`branch:${groups.branchName}`); + } } - if (workflowBranch) { - url += `+branch:${workflowBranch}`; + if (filters.length === 3) { + url += `?query=${filters.join('+')}`; } return url; } -async function getWorkflowRunUrl(notification: Notification) { +export function getWorkflowRunUrl(notification: Notification) { const workflowStatus = getWorkflowTypeFromTitle(notification.subject.title); let url = `${notification.repository.html_url}/actions`; if (workflowStatus) { - url += `?query=${workflowStatus}`; - - return url; + url += `?query=is:${workflowStatus}`; } + + return url; } export const getLatestDiscussionCommentId = ( @@ -232,16 +245,16 @@ export async function openInBrowser( ), ); } else if (notification.subject.type === 'CheckSuite') { - getCheckSuiteUrl(notification).then((url) => - openExternalLink( - generateGitHubWebUrl(url, notification.id, accounts.user?.id), - ), + const url = getCheckSuiteUrl(notification); + + openExternalLink( + generateGitHubWebUrl(url, notification.id, accounts.user?.id), ); } else if (notification.subject.type === 'WorkflowRun') { - getWorkflowRunUrl(notification).then((url) => - openExternalLink( - generateGitHubWebUrl(url, notification.id, accounts.user?.id), - ), + const url = getWorkflowRunUrl(notification); + + openExternalLink( + generateGitHubWebUrl(url, notification.id, accounts.user?.id), ); } else if (notification.subject.url) { const latestCommentId = getCommentId( From 8287be76a35f9455d40de04f29c3985f0e64d071 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Thu, 15 Feb 2024 13:36:10 -0500 Subject: [PATCH 09/12] refactor: use regex and add coverage --- src/utils/helpers.test.ts | 12 +++++++++++- src/utils/helpers.ts | 6 ++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/utils/helpers.test.ts b/src/utils/helpers.test.ts index f3b0dfa2a..80031e843 100644 --- a/src/utils/helpers.test.ts +++ b/src/utils/helpers.test.ts @@ -175,7 +175,17 @@ describe('utils/helpers.ts', () => { ); }); - it('should generate a Github Action (CheckSuite) url with filters', () => { + it('should generate a Github Action (CheckSuite) url with workflow and branch filters', () => { + notification.subject.title = + 'Demo workflow run unhandled for main branch'; + + const result = getCheckSuiteUrl(notification); + expect(result).toBe( + 'https://github.com/manosim/notifications-test/actions?query=workflow:"Demo"+branch:main', + ); + }); + + it('should generate a Github Action (CheckSuite) url with all filters', () => { notification.subject.title = 'Demo workflow run succeeded for main branch'; diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index b43ac74fa..419e5f6e7 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -181,7 +181,9 @@ export function getCheckSuiteUrl(notification: Notification) { notification.subject.title, ); - filters.push(`is:${workflowStatus}`); + if (workflowStatus) { + filters.push(`is:${workflowStatus}`); + } } if (groups.branchName) { @@ -189,7 +191,7 @@ export function getCheckSuiteUrl(notification: Notification) { } } - if (filters.length === 3) { + if (filters.length > 0) { url += `?query=${filters.join('+')}`; } From 5b9ae4891c63b5b466c7ba8336d8b287681fc8d2 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Fri, 16 Feb 2024 04:59:32 -0500 Subject: [PATCH 10/12] refactor: update to match github mobile app terminology --- .../__snapshots__/github-api.test.ts.snap | 4 +-- src/utils/github-api.ts | 30 +++++++++---------- src/utils/helpers.test.ts | 20 ++++++------- src/utils/helpers.ts | 11 +++---- 4 files changed, 33 insertions(+), 32 deletions(-) diff --git a/src/utils/__snapshots__/github-api.test.ts.snap b/src/utils/__snapshots__/github-api.test.ts.snap index 654146aec..0c1928071 100644 --- a/src/utils/__snapshots__/github-api.test.ts.snap +++ b/src/utils/__snapshots__/github-api.test.ts.snap @@ -2,8 +2,8 @@ exports[`./utils/github-api.ts should format the notification reason 1`] = ` { - "description": "You were requested to review and approve a deployment workflow.", - "type": "Approval Requested", + "description": "You were requested to review and approve a deployment.", + "type": "Deployment Review", } `; diff --git a/src/utils/github-api.ts b/src/utils/github-api.ts index c74739ef4..7de0d34b6 100644 --- a/src/utils/github-api.ts +++ b/src/utils/github-api.ts @@ -21,20 +21,20 @@ import { Reason, StateType, SubjectType, WorkflowType } from '../typesGithub'; // prettier-ignore const DESCRIPTIONS = { - APPROVAL_REQUESTED: 'You were requested to review and approve a deployment workflow.', - ASSIGN: 'You were assigned to the issue.', - AUTHOR: 'You created the thread.', - CI_ACTIVITY: 'A GitHub Actions workflow run was triggered for your repository', - COMMENT: 'You commented on the thread.', - INVITATION: 'You accepted an invitation to contribute to the repository.', - MANUAL: 'You subscribed to the thread (via an issue or pull request).', - MENTION: 'You were specifically @mentioned in the content.', - REVIEW_REQUESTED: "You, or a team you're a member of, were requested to review a pull request.", - SECURITY_ALERT: 'GitHub discovered a security vulnerability in your repository.', - STATE_CHANGE: 'You changed the thread state (for example, closing an issue or merging a pull request).', - SUBSCRIBED: "You're watching the repository.", - TEAM_MENTION: 'You were on a team that was mentioned.', - UNKNOWN: 'The reason for this notification is not supported by the app.', + ASSIGN: 'You were assigned to the issue.', + AUTHOR: 'You created the thread.', + CI_ACTIVITY: 'A GitHub Actions workflow run was triggered for your repository', + COMMENT: 'You commented on the thread.', + DEPLOYMENT_REVIEW: 'You were requested to review and approve a deployment.', + INVITATION: 'You accepted an invitation to contribute to the repository.', + MANUAL: 'You subscribed to the thread (via an issue or pull request).', + MENTION: 'You were specifically @mentioned in the content.', + REVIEW_REQUESTED: "You, or a team you're a member of, were requested to review a pull request.", + SECURITY_ALERT: 'GitHub discovered a security vulnerability in your repository.', + STATE_CHANGE: 'You changed the thread state (for example, closing an issue or merging a pull request).', + SUBSCRIBED: "You're watching the repository.", + TEAM_MENTION: 'You were on a team that was mentioned.', + UNKNOWN: 'The reason for this notification is not supported by the app.', }; export function formatReason(reason: Reason): { @@ -44,7 +44,7 @@ export function formatReason(reason: Reason): { // prettier-ignore switch (reason) { case 'approval_requested': - return { type: 'Approval Requested', description: DESCRIPTIONS['APPROVAL_REQUESTED'] }; + return { type: 'Deployment Review', description: DESCRIPTIONS['DEPLOYMENT_REVIEW'] }; case 'assign': return { type: 'Assign', description: DESCRIPTIONS['ASSIGN'] }; case 'author': diff --git a/src/utils/helpers.test.ts b/src/utils/helpers.test.ts index 80031e843..526f76a1a 100644 --- a/src/utils/helpers.test.ts +++ b/src/utils/helpers.test.ts @@ -4,9 +4,9 @@ import { generateNotificationReferrerId, getCommentId, getLatestDiscussionCommentId, - inferWorkflowBranchFromTitle, - getCheckSuiteUrl, - getWorkflowRunUrl, + inferWorkflowRunBranchFromTitle, + getWorkflowRunsUrl, + getDeploymentReviewUrl, } from './helpers'; import { mockedSingleNotification, @@ -149,13 +149,13 @@ describe('utils/helpers.ts', () => { it('should infer branch name from the title', () => { notification.subject.title = 'Demo workflow run succeeded for main branch'; - expect(inferWorkflowBranchFromTitle(notification)).toBe('main'); + expect(inferWorkflowRunBranchFromTitle(notification)).toBe('main'); }); it('should return null for known branch name', () => { notification.subject.title = 'Demo workflow run does not have branch name'; - expect(inferWorkflowBranchFromTitle(notification)).toBeNull(); + expect(inferWorkflowRunBranchFromTitle(notification)).toBeNull(); }); }); @@ -169,7 +169,7 @@ describe('utils/helpers.ts', () => { it('should generate a Github Action (CheckSuite) url', () => { notification.subject.title = 'Demo workflow run'; - const result = getCheckSuiteUrl(notification); + const result = getWorkflowRunsUrl(notification); expect(result).toBe( 'https://github.com/manosim/notifications-test/actions', ); @@ -179,7 +179,7 @@ describe('utils/helpers.ts', () => { notification.subject.title = 'Demo workflow run unhandled for main branch'; - const result = getCheckSuiteUrl(notification); + const result = getWorkflowRunsUrl(notification); expect(result).toBe( 'https://github.com/manosim/notifications-test/actions?query=workflow:"Demo"+branch:main', ); @@ -189,7 +189,7 @@ describe('utils/helpers.ts', () => { notification.subject.title = 'Demo workflow run succeeded for main branch'; - const result = getCheckSuiteUrl(notification); + const result = getWorkflowRunsUrl(notification); expect(result).toBe( 'https://github.com/manosim/notifications-test/actions?query=workflow:"Demo"+is:success+branch:main', ); @@ -206,7 +206,7 @@ describe('utils/helpers.ts', () => { it('should generate a Github Action (WorkflowRun) url', () => { notification.subject.title = 'some title'; - const result = getWorkflowRunUrl(notification); + const result = getDeploymentReviewUrl(notification); expect(result).toBe( 'https://github.com/manosim/notifications-test/actions', ); @@ -216,7 +216,7 @@ describe('utils/helpers.ts', () => { notification.subject.title = 'userA requested your review to deploy to an environment'; - const result = getWorkflowRunUrl(notification); + const result = getDeploymentReviewUrl(notification); expect(result).toBe( 'https://github.com/manosim/notifications-test/actions?query=is:waiting', ); diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 419e5f6e7..f5adbfa6a 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -147,7 +147,7 @@ async function getDiscussionUrl( }; } -export function inferWorkflowBranchFromTitle( +export function inferWorkflowRunBranchFromTitle( notification: Notification, ): string { const title = notification.subject.title; @@ -161,7 +161,7 @@ export function inferWorkflowBranchFromTitle( return null; } -export function getCheckSuiteUrl(notification: Notification) { +export function getWorkflowRunsUrl(notification: Notification) { let url = `${notification.repository.html_url}/actions`; let filters = []; @@ -198,7 +198,7 @@ export function getCheckSuiteUrl(notification: Notification) { return url; } -export function getWorkflowRunUrl(notification: Notification) { +export function getDeploymentReviewUrl(notification: Notification) { const workflowStatus = getWorkflowTypeFromTitle(notification.subject.title); let url = `${notification.repository.html_url}/actions`; @@ -226,6 +226,7 @@ export async function openInBrowser( notification: Notification, accounts: AuthState, ) { + console.log('ADAM : ' + notification.subject.type); if (notification.subject.type === 'Release') { getReleaseTagWebUrl(notification, accounts.token).then(({ url }) => openExternalLink( @@ -247,13 +248,13 @@ export async function openInBrowser( ), ); } else if (notification.subject.type === 'CheckSuite') { - const url = getCheckSuiteUrl(notification); + const url = getWorkflowRunsUrl(notification); openExternalLink( generateGitHubWebUrl(url, notification.id, accounts.user?.id), ); } else if (notification.subject.type === 'WorkflowRun') { - const url = getWorkflowRunUrl(notification); + const url = getDeploymentReviewUrl(notification); openExternalLink( generateGitHubWebUrl(url, notification.id, accounts.user?.id), From 89f70ea3cb0d17de2a28414a6216bfee7fbc5288 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Fri, 16 Feb 2024 13:01:25 -0500 Subject: [PATCH 11/12] feat: ci_activity description with public docs --- src/utils/__snapshots__/github-api.test.ts.snap | 2 +- src/utils/github-api.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/__snapshots__/github-api.test.ts.snap b/src/utils/__snapshots__/github-api.test.ts.snap index 0c1928071..a7a563406 100644 --- a/src/utils/__snapshots__/github-api.test.ts.snap +++ b/src/utils/__snapshots__/github-api.test.ts.snap @@ -23,7 +23,7 @@ exports[`./utils/github-api.ts should format the notification reason 3`] = ` exports[`./utils/github-api.ts should format the notification reason 4`] = ` { - "description": "A GitHub Actions workflow run was triggered for your repository", + "description": "A GitHub workflow run that you triggered was completed.", "type": "Workflow Run", } `; diff --git a/src/utils/github-api.ts b/src/utils/github-api.ts index 7de0d34b6..e68650a97 100644 --- a/src/utils/github-api.ts +++ b/src/utils/github-api.ts @@ -23,7 +23,7 @@ import { Reason, StateType, SubjectType, WorkflowType } from '../typesGithub'; const DESCRIPTIONS = { ASSIGN: 'You were assigned to the issue.', AUTHOR: 'You created the thread.', - CI_ACTIVITY: 'A GitHub Actions workflow run was triggered for your repository', + CI_ACTIVITY: 'A GitHub workflow run that you triggered was completed.', COMMENT: 'You commented on the thread.', DEPLOYMENT_REVIEW: 'You were requested to review and approve a deployment.', INVITATION: 'You accepted an invitation to contribute to the repository.', From 88343a025203818e4998ef7fda9bde3c5d6ab5a1 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Fri, 16 Feb 2024 13:27:17 -0500 Subject: [PATCH 12/12] feat: align approval_requested description name --- src/utils/github-api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/github-api.ts b/src/utils/github-api.ts index e68650a97..c5fbfed61 100644 --- a/src/utils/github-api.ts +++ b/src/utils/github-api.ts @@ -21,11 +21,11 @@ import { Reason, StateType, SubjectType, WorkflowType } from '../typesGithub'; // prettier-ignore const DESCRIPTIONS = { + APPROVAL_REQUESTED: 'You were requested to review and approve a deployment.', ASSIGN: 'You were assigned to the issue.', AUTHOR: 'You created the thread.', CI_ACTIVITY: 'A GitHub workflow run that you triggered was completed.', COMMENT: 'You commented on the thread.', - DEPLOYMENT_REVIEW: 'You were requested to review and approve a deployment.', INVITATION: 'You accepted an invitation to contribute to the repository.', MANUAL: 'You subscribed to the thread (via an issue or pull request).', MENTION: 'You were specifically @mentioned in the content.', @@ -44,7 +44,7 @@ export function formatReason(reason: Reason): { // prettier-ignore switch (reason) { case 'approval_requested': - return { type: 'Deployment Review', description: DESCRIPTIONS['DEPLOYMENT_REVIEW'] }; + return { type: 'Deployment Review', description: DESCRIPTIONS['APPROVAL_REQUESTED'] }; case 'assign': return { type: 'Assign', description: DESCRIPTIONS['ASSIGN'] }; case 'author':