• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gitify-app / gitify / 7917138938

15 Feb 2024 02:04PM UTC coverage: 89.419% (-0.3%) from 89.688%
7917138938

Pull #766

github

web-flow
Merge e8465d220 into 4e1d038ed
Pull Request #766: feat: support ci workflow notifications

258 of 303 branches covered (85.15%)

Branch coverage included in aggregate %.

12 of 15 new or added lines in 1 file covered. (80.0%)

12 existing lines in 2 files now uncovered.

697 of 765 relevant lines covered (91.11%)

13.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.44
/src/utils/helpers.ts
1
import { EnterpriseAccount, AuthState } from '../types';
2
import {
3
  Notification,
4
  GraphQLSearch,
5
  DiscussionCommentEdge,
6
} from '../typesGithub';
7
import { apiRequestAuth } from '../utils/api-requests';
30✔
8
import { openExternalLink } from '../utils/comms';
30✔
9
import { Constants } from './constants';
30✔
10

11
export function getEnterpriseAccountToken(
30✔
12
  hostname: string,
13
  accounts: EnterpriseAccount[],
14
): string {
15
  return accounts.find((obj) => obj.hostname === hostname).token;
16✔
16
}
17

18
export function generateGitHubAPIUrl(hostname) {
30✔
19
  const isEnterprise = hostname !== Constants.DEFAULT_AUTH_OPTIONS.hostname;
86✔
20
  return isEnterprise
86✔
21
    ? `https://${hostname}/api/v3/`
22
    : `https://api.${hostname}/`;
23
}
24

25
export function generateNotificationReferrerId(
30✔
26
  notificationId: string,
27
  userId: number,
28
) {
29
  const buffer = Buffer.from(
32✔
30
    `018:NotificationThread${notificationId}:${userId}`,
31
  );
32
  return buffer.toString('base64');
32✔
33
}
34

35
export function generateGitHubWebUrl(
30✔
36
  url: string,
37
  notificationId: string,
38
  userId?: number,
39
  comment: string = '',
9✔
40
) {
41
  const newURL = new URL(url);
30✔
42
  const hostname = newURL.hostname;
30✔
43
  let params = new URLSearchParams(newURL.search);
30✔
44

45
  const isEnterprise =
46
    hostname !== `api.${Constants.DEFAULT_AUTH_OPTIONS.hostname}`;
30✔
47

48
  if (isEnterprise) {
30✔
49
    newURL.href = newURL.href.replace(`${hostname}/api/v3/repos`, hostname);
8✔
50
  } else {
51
    newURL.href = newURL.href.replace('api.github.com/repos', 'github.com');
22✔
52
  }
53

54
  newURL.href = newURL.href.replace('/pulls/', '/pull/');
30✔
55

56
  if (userId) {
30✔
57
    const notificationReferrerId = generateNotificationReferrerId(
30✔
58
      notificationId,
59
      userId,
60
    );
61

62
    params.append('notification_referrer_id', notificationReferrerId);
30✔
63
    newURL.search = params.toString();
30✔
64
  }
65

66
  return encodeURI(newURL.href + comment);
30✔
67
}
68

69
const addHours = (date: string, hours: number) =>
30✔
UNCOV
70
  new Date(new Date(date).getTime() + hours * 36e5).toISOString();
×
71

72
const queryString = (repo: string, title: string, lastUpdated: string) =>
30✔
UNCOV
73
  `${title} in:title repo:${repo} updated:>${addHours(lastUpdated, -2)}`;
×
74

75
async function getReleaseTagWebUrl(notification: Notification, token: string) {
UNCOV
76
  const response = await apiRequestAuth(notification.subject.url, 'GET', token);
×
77

UNCOV
78
  return {
×
79
    url: response.data.html_url,
80
  };
81
}
82

83
async function getDiscussionUrl(
84
  notification: Notification,
85
  token: string,
86
): Promise<{ url: string; latestCommentId: string | number }> {
UNCOV
87
  const response: GraphQLSearch = await apiRequestAuth(
×
88
    `https://api.github.com/graphql`,
89
    'POST',
90
    token,
91
    {
92
      query: `{
93
      search(query:"${queryString(
94
        notification.repository.full_name,
95
        notification.subject.title,
96
        notification.updated_at,
97
      )}", type: DISCUSSION, first: 10) {
98
          edges {
99
              node {
100
                  ... on Discussion {
101
                      viewerSubscription
102
                      title
103
                      url
104
                      comments(last: 100) {
105
                        edges {
106
                          node {
107
                            databaseId
108
                            createdAt
109
                            replies(last: 1) {
110
                              edges {
111
                                node {
112
                                  databaseId
113
                                  createdAt
114
                                }
115
                              }
116
                            }
117
                          }
118
                        }
119
                      }
120
                  }
121
              }
122
          }
123
      }
124
    }`,
125
    },
126
  );
127
  let edges =
UNCOV
128
    response?.data?.data?.search?.edges?.filter(
×
129
      (edge) => edge.node.title === notification.subject.title,
×
130
    ) || [];
UNCOV
131
  if (edges.length > 1)
×
132
    edges = edges.filter(
×
133
      (edge) => edge.node.viewerSubscription === 'SUBSCRIBED',
×
134
    );
135

UNCOV
136
  let comments = edges[0]?.node.comments.edges;
×
137

138
  let latestCommentId: string | number;
UNCOV
139
  if (comments?.length) {
×
140
    latestCommentId = getLatestDiscussionCommentId(comments);
×
141
  }
142

UNCOV
143
  return {
×
144
    url: edges[0]?.node.url,
145
    latestCommentId,
146
  };
147
}
148

149
export const getLatestDiscussionCommentId = (
30✔
150
  comments: DiscussionCommentEdge[],
151
) =>
152
  comments
2✔
153
    .flatMap((comment) => comment.node.replies.edges)
14✔
154
    .concat([comments.at(-1)])
155
    .reduce((a, b) => (a.node.createdAt > b.node.createdAt ? a : b))?.node
8✔
156
    .databaseId;
157

158
export const getCommentId = (url: string) =>
30✔
159
  /comments\/(?<id>\d+)/g.exec(url)?.groups?.id;
10✔
160

161
export async function openInBrowser(
30✔
162
  notification: Notification,
163
  accounts: AuthState,
164
) {
165
  if (notification.subject.type === 'Release') {
6✔
166
    getReleaseTagWebUrl(notification, accounts.token).then(({ url }) =>
×
167
      openExternalLink(
×
168
        generateGitHubWebUrl(url, notification.id, accounts.user?.id),
169
      ),
170
    );
171
  } else if (notification.subject.type === 'Discussion') {
6✔
172
    getDiscussionUrl(notification, accounts.token).then(
×
173
      ({ url, latestCommentId }) =>
174
        openExternalLink(
×
175
          generateGitHubWebUrl(
176
            url || `${notification.repository.url}/discussions`,
×
177
            notification.id,
178
            accounts.user?.id,
179
            latestCommentId
×
180
              ? '#discussioncomment-' + latestCommentId
181
              : undefined,
182
          ),
183
        ),
184
    );
185
  } else if (notification.subject.type === 'CheckSuite') {
6!
NEW
186
    const workflowName = notification.subject.title
×
187
      .split('workflow run')[0]
188
      .trim();
189

NEW
190
    openExternalLink(
×
191
      generateGitHubWebUrl(
192
        `${notification.repository.html_url}/actions?workflow=${workflowName}`,
193
        notification.id,
194
        accounts.user?.id,
195
      ),
196
    );
197
  } else if (notification.subject.url) {
6!
198
    const latestCommentId = getCommentId(
6✔
199
      notification.subject.latest_comment_url,
200
    );
201
    openExternalLink(
6✔
202
      generateGitHubWebUrl(
203
        notification.subject.url,
204
        notification.id,
205
        accounts.user?.id,
206
        latestCommentId ? '#issuecomment-' + latestCommentId : undefined,
3!
207
      ),
208
    );
209
  } else {
NEW
210
    openExternalLink(
×
211
      generateGitHubWebUrl(
212
        notification.repository.html_url,
213
        notification.id,
214
        accounts.user?.id,
215
      ),
216
    );
217
  }
218
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc