Skip to content

Commit bd242e2

Browse files
committed
Merge branch 'main' of https://github.com/CrowdDotDev/crowd.dev into LFX-1640
Signed-off-by: Sameh16 <[email protected]>
2 parents ee239b8 + 3dc9d5c commit bd242e2

File tree

339 files changed

+8148
-7444
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

339 files changed

+8148
-7444
lines changed

Diff for: backend/config/custom-environment-variables.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@
104104
"callbackUrl": "CROWD_GITHUB_CALLBACK_URL",
105105
"privateKey": "CROWD_GITHUB_PRIVATE_KEY",
106106
"webhookSecret": "CROWD_GITHUB_WEBHOOK_SECRET",
107-
"isCommitDataEnabled": "CROWD_GITHUB_IS_COMMIT_DATA_ENABLED"
107+
"isCommitDataEnabled": "CROWD_GITHUB_IS_COMMIT_DATA_ENABLED",
108+
"isSnowflakeEnabled": "CROWD_GITHUB_IS_SNOWFLAKE_ENABLED"
108109
},
109110
"githubIssueReporter": {
110111
"appId": "CROWD_GITHUB_ISSUE_REPORTER_APP_ID",

Diff for: backend/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
"script:purge-tenants-and-data": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/purge-tenants-and-data.ts",
3333
"script:import-lfx-memberships": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/import-lfx-memberships.ts",
3434
"script:fix-missing-org-displayName": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/fix-missing-org-displayName.ts",
35-
"script:syncActivities": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/syncActivities.ts"
35+
"script:syncActivities": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/syncActivities.ts",
36+
"script:refreshGithubRepoSettings": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/refresh-github-repo-settings.ts"
3637
},
3738
"dependencies": {
3839
"@aws-sdk/client-comprehend": "^3.159.0",
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Permissions from '../../../security/permissions'
2+
import IntegrationService from '../../../services/integrationService'
3+
import PermissionChecker from '../../../services/user/permissionChecker'
4+
5+
export default async (req, res) => {
6+
new PermissionChecker(req).validateHas(Permissions.values.tenantEdit)
7+
const payload = await new IntegrationService(req).connectGithub(
8+
req.params.code,
9+
req.body.installId,
10+
req.body.setupAction,
11+
)
12+
await req.responseHandler.success(req, res, payload)
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Permissions from '../../../security/permissions'
2+
import IntegrationService from '../../../services/integrationService'
3+
import PermissionChecker from '../../../services/user/permissionChecker'
4+
5+
export default async (req, res) => {
6+
new PermissionChecker(req).validateHas(Permissions.values.tenantEdit)
7+
const payload = await new IntegrationService(req).connectGithubInstallation(req.body.installId)
8+
await req.responseHandler.success(req, res, payload)
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Permissions from '../../../security/permissions'
2+
import IntegrationService from '../../../services/integrationService'
3+
import PermissionChecker from '../../../services/user/permissionChecker'
4+
5+
export default async (req, res) => {
6+
new PermissionChecker(req).validateHas(Permissions.values.tenantEdit)
7+
const payload = await new IntegrationService(req).getGithubInstallations()
8+
await req.responseHandler.success(req, res, payload)
9+
}
+19-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
1+
import { GITHUB_CONFIG } from '@/conf'
2+
13
import Permissions from '../../../security/permissions'
24
import IntegrationService from '../../../services/integrationService'
35
import PermissionChecker from '../../../services/user/permissionChecker'
46

7+
const isSnowflakeEnabled = GITHUB_CONFIG.isSnowflakeEnabled === 'true'
8+
59
export default async (req, res) => {
610
new PermissionChecker(req).validateHas(Permissions.values.tenantEdit)
7-
const payload = await new IntegrationService(req).mapGithubRepos(
8-
req.params.id,
9-
req.body.mapping,
10-
true,
11-
req.body?.isUpdateTransaction ?? false,
12-
)
11+
let payload
12+
if (isSnowflakeEnabled) {
13+
payload = await new IntegrationService(req).mapGithubReposSnowflake(
14+
req.params.id,
15+
req.body.mapping,
16+
true,
17+
req.body?.isUpdateTransaction ?? false,
18+
)
19+
} else {
20+
payload = await new IntegrationService(req).mapGithubRepos(
21+
req.params.id,
22+
req.body.mapping,
23+
true,
24+
)
25+
}
1326
await req.responseHandler.success(req, res, payload)
1427
}

Diff for: backend/src/api/integration/index.ts

+13
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,22 @@ export default (app) => {
2626
app.get(`/integration/autocomplete`, safeWrap(require('./integrationAutocomplete').default))
2727
app.get(`/integration/global`, safeWrap(require('./integrationGlobal').default))
2828
app.get(`/integration/global/status`, safeWrap(require('./integrationGlobalStatus').default))
29+
30+
app.get(
31+
'/integration/github-installations',
32+
safeWrap(require('./helpers/githubGetInstallations').default),
33+
)
34+
35+
app.post(
36+
'/integration/github-connect-installation',
37+
safeWrap(require('./helpers/githubConnectInstallation').default),
38+
)
39+
2940
app.get(`/integration`, safeWrap(require('./integrationList').default))
3041
app.get(`/integration/:id`, safeWrap(require('./integrationFind').default))
3142

43+
app.put(`/authenticate/:code`, safeWrap(require('./helpers/githubAuthenticate').default))
44+
3245
app.put(`/integration/:id/github/repos`, safeWrap(require('./helpers/githubMapRepos').default))
3346
app.get(`/integration/:id/github/repos`, safeWrap(require('./helpers/githubMapReposGet').default))
3447
app.get(

Diff for: backend/src/bin/jobs/refreshGithubRepoSettings.ts

+55-3
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ import cronGenerator from 'cron-time-generator'
44
import { timeout } from '@crowd/common'
55
import { getServiceChildLogger } from '@crowd/logging'
66

7+
import { GITHUB_CONFIG } from '../../conf'
78
import GithubReposRepository from '../../database/repositories/githubReposRepository'
89
import SequelizeRepository from '../../database/repositories/sequelizeRepository'
910
import GithubIntegrationService from '../../services/githubIntegrationService'
1011
import IntegrationService from '../../services/integrationService'
1112
import { CrowdJob } from '../../types/jobTypes'
1213

14+
const IS_GITHUB_SNOWFLAKE_ENABLED = GITHUB_CONFIG.isSnowflakeEnabled === 'true'
15+
1316
const log = getServiceChildLogger('refreshGithubRepoSettings')
1417

1518
interface Integration {
@@ -33,8 +36,7 @@ interface Integration {
3336
}
3437
}
3538

36-
export const refreshGithubRepoSettings = async () => {
37-
log.info('Updating Github repo settings.')
39+
const refreshForSnowflake = async () => {
3840
const dbOptions = await SequelizeRepository.getDefaultIRepositoryOptions()
3941

4042
const githubIntegrations = await dbOptions.database.sequelize.query(
@@ -124,7 +126,7 @@ export const refreshGithubRepoSettings = async () => {
124126
}
125127

126128
// Map new repos
127-
await integrationService.mapGithubRepos(
129+
await integrationService.mapGithubReposSnowflake(
128130
integration.id,
129131
newFullMapping,
130132
true,
@@ -150,6 +152,56 @@ export const refreshGithubRepoSettings = async () => {
150152
log.info('Finished updating Github repo settings.')
151153
}
152154

155+
const refreshForGitHub = async () => {
156+
log.info('Updating Github repo settings.')
157+
const dbOptions = await SequelizeRepository.getDefaultIRepositoryOptions()
158+
159+
interface Integration {
160+
id: string
161+
tenantId: string
162+
integrationIdentifier: string
163+
}
164+
165+
const githubIntegrations = await dbOptions.database.sequelize.query(
166+
`SELECT id, "tenantId", "integrationIdentifier" FROM integrations
167+
WHERE platform = 'github' AND "deletedAt" IS NULL
168+
AND "createdAt" < NOW() - INTERVAL '1 minute' AND "integrationIdentifier" IS NOT NULL`,
169+
)
170+
171+
for (const integration of githubIntegrations[0] as Integration[]) {
172+
log.info(`Updating repo settings for Github integration: ${integration.id}`)
173+
174+
try {
175+
const options = await SequelizeRepository.getDefaultIRepositoryOptions()
176+
options.currentTenant = { id: integration.tenantId }
177+
178+
const integrationService = new IntegrationService(options)
179+
// newly discovered repos will be mapped to default segment of the integration
180+
await integrationService.updateGithubIntegrationSettings(integration.integrationIdentifier)
181+
182+
log.info(`Successfully updated repo settings for Github integration: ${integration.id}`)
183+
} catch (err) {
184+
log.error(
185+
`Error updating repo settings for Github integration ${integration.id}: ${err.message}`,
186+
)
187+
} finally {
188+
await timeout(1000)
189+
}
190+
}
191+
192+
log.info('Finished updating Github repo settings.')
193+
}
194+
195+
export const refreshGithubRepoSettings = async () => {
196+
log.info('Updating Github repo settings.')
197+
198+
if (IS_GITHUB_SNOWFLAKE_ENABLED) {
199+
await refreshForSnowflake()
200+
} else {
201+
await refreshForGitHub()
202+
}
203+
}
204+
153205
const job: CrowdJob = {
154206
name: 'Refresh Github repo settings',
155207
// every day

Diff for: backend/src/conf/configTypes.ts

+1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export interface GithubConfiguration {
120120
privateKey: string
121121
webhookSecret: string
122122
isCommitDataEnabled: string
123+
isSnowflakeEnabled: string
123124
globalLimit?: number
124125
callbackUrl: string
125126
}

Diff for: backend/src/database/repositories/githubReposRepository.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,24 @@ export default class GithubReposRepository {
3838
)
3939
}
4040

41-
static async updateMapping(
41+
static async updateMapping(integrationId, mapping, options: IRepositoryOptions) {
42+
const tenantId = options.currentTenant.id
43+
44+
await GithubReposRepository.bulkInsert(
45+
'githubRepos',
46+
['tenantId', 'integrationId', 'segmentId', 'url'],
47+
(idx) => `(:tenantId_${idx}, :integrationId_${idx}, :segmentId_${idx}, :url_${idx})`,
48+
Object.entries(mapping).map(([url, segmentId], idx) => ({
49+
[`tenantId_${idx}`]: tenantId,
50+
[`integrationId_${idx}`]: integrationId,
51+
[`segmentId_${idx}`]: segmentId,
52+
[`url_${idx}`]: url,
53+
})),
54+
options,
55+
)
56+
}
57+
58+
static async updateMappingSnowflake(
4259
integrationId,
4360
newMapping: Record<string, string>,
4461
oldMapping: {

Diff for: backend/src/serverless/integrations/types/regularTypes.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import { PlatformType } from '@crowd/types'
33
export type Repo = {
44
url: string
55
name: string
6-
updatedAt: string
6+
updatedAt?: string
7+
createdAt?: string
8+
owner?: string
9+
available?: boolean
10+
fork?: boolean
11+
private?: boolean
12+
cloneUrl?: string
713
}
814

915
export type Repos = Array<Repo>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import axios, { AxiosRequestConfig } from 'axios'
2+
3+
import { getServiceChildLogger } from '@crowd/logging'
4+
5+
import { Repos } from '../../../types/regularTypes'
6+
7+
const log = getServiceChildLogger('getInstalledRepositories')
8+
9+
const getRepositoriesFromGH = async (page: number, installToken: string): Promise<any> => {
10+
const REPOS_PER_PAGE = 100
11+
12+
const requestConfig = {
13+
method: 'get',
14+
url: `https://api.github.com/installation/repositories?page=${page}&per_page=${REPOS_PER_PAGE}`,
15+
headers: {
16+
Authorization: `Bearer ${installToken}`,
17+
},
18+
} as AxiosRequestConfig
19+
20+
const response = await axios(requestConfig)
21+
return response.data
22+
}
23+
24+
const parseRepos = (repositories: any): Repos => {
25+
const repos: Repos = []
26+
27+
for (const repo of repositories) {
28+
repos.push({
29+
url: repo.html_url,
30+
owner: repo.owner.login,
31+
createdAt: repo.created_at,
32+
name: repo.name,
33+
fork: repo.fork,
34+
private: repo.private,
35+
cloneUrl: repo.clone_url,
36+
})
37+
}
38+
39+
return repos
40+
}
41+
42+
export const getInstalledRepositories = async (installToken: string): Promise<Repos> => {
43+
try {
44+
let page = 1
45+
let hasMorePages = true
46+
47+
const repos: Repos = []
48+
49+
while (hasMorePages) {
50+
const data = await getRepositoriesFromGH(page, installToken)
51+
52+
if (data.repositories) {
53+
repos.push(...parseRepos(data.repositories))
54+
}
55+
56+
hasMorePages = data.total_count && data.total_count > 0 && data.total_count > repos.length
57+
page += 1
58+
}
59+
return repos.filter((repo) => !repo.private && !repo.fork)
60+
} catch (err: any) {
61+
log.error(err, 'Error fetching installed repositories!')
62+
throw err
63+
}
64+
}

0 commit comments

Comments
 (0)