@intlayer/backend 7.5.10 → 7.5.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/cli/ci.json +3080 -0
- package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/cli/list_projects.json +1 -0
- package/dist/esm/controllers/bitbucket.controller.mjs +77 -0
- package/dist/esm/controllers/bitbucket.controller.mjs.map +1 -0
- package/dist/esm/controllers/dictionary.controller.mjs +20 -0
- package/dist/esm/controllers/dictionary.controller.mjs.map +1 -1
- package/dist/esm/controllers/github.controller.mjs.map +1 -1
- package/dist/esm/controllers/gitlab.controller.mjs +77 -0
- package/dist/esm/controllers/gitlab.controller.mjs.map +1 -0
- package/dist/esm/controllers/project.controller.mjs +109 -2
- package/dist/esm/controllers/project.controller.mjs.map +1 -1
- package/dist/esm/export.mjs +3 -1
- package/dist/esm/index.mjs +5 -1
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/routes/bitbucket.routes.mjs +43 -0
- package/dist/esm/routes/bitbucket.routes.mjs.map +1 -0
- package/dist/esm/routes/gitlab.routes.mjs +43 -0
- package/dist/esm/routes/gitlab.routes.mjs.map +1 -0
- package/dist/esm/routes/project.routes.mjs +25 -1
- package/dist/esm/routes/project.routes.mjs.map +1 -1
- package/dist/esm/schemas/project.schema.mjs +39 -4
- package/dist/esm/schemas/project.schema.mjs.map +1 -1
- package/dist/esm/services/bitbucket.service.mjs +173 -0
- package/dist/esm/services/bitbucket.service.mjs.map +1 -0
- package/dist/esm/services/ci.service.mjs +134 -0
- package/dist/esm/services/ci.service.mjs.map +1 -0
- package/dist/esm/services/github.service.mjs +90 -2
- package/dist/esm/services/github.service.mjs.map +1 -1
- package/dist/esm/services/gitlab.service.mjs +217 -0
- package/dist/esm/services/gitlab.service.mjs.map +1 -0
- package/dist/esm/services/webhook.service.mjs +164 -0
- package/dist/esm/services/webhook.service.mjs.map +1 -0
- package/dist/esm/utils/auth/getAuth.mjs +15 -9
- package/dist/esm/utils/auth/getAuth.mjs.map +1 -1
- package/dist/esm/utils/errors/errorCodes.mjs +156 -0
- package/dist/esm/utils/errors/errorCodes.mjs.map +1 -1
- package/dist/types/controllers/ai.controller.d.ts.map +1 -1
- package/dist/types/controllers/bitbucket.controller.d.ts +62 -0
- package/dist/types/controllers/bitbucket.controller.d.ts.map +1 -0
- package/dist/types/controllers/dictionary.controller.d.ts.map +1 -1
- package/dist/types/controllers/github.controller.d.ts.map +1 -1
- package/dist/types/controllers/gitlab.controller.d.ts +67 -0
- package/dist/types/controllers/gitlab.controller.d.ts.map +1 -0
- package/dist/types/controllers/project.controller.d.ts +39 -1
- package/dist/types/controllers/project.controller.d.ts.map +1 -1
- package/dist/types/emails/InviteUserEmail.d.ts +4 -4
- package/dist/types/emails/MagicLinkEmail.d.ts +4 -4
- package/dist/types/emails/MagicLinkEmail.d.ts.map +1 -1
- package/dist/types/emails/OAuthTokenCreatedEmail.d.ts +4 -4
- package/dist/types/emails/OAuthTokenCreatedEmail.d.ts.map +1 -1
- package/dist/types/emails/PasswordChangeConfirmation.d.ts +4 -4
- package/dist/types/emails/PasswordChangeConfirmation.d.ts.map +1 -1
- package/dist/types/emails/ResetUserPassword.d.ts +4 -4
- package/dist/types/emails/ResetUserPassword.d.ts.map +1 -1
- package/dist/types/emails/SubscriptionPaymentCancellation.d.ts +4 -4
- package/dist/types/emails/SubscriptionPaymentCancellation.d.ts.map +1 -1
- package/dist/types/emails/SubscriptionPaymentError.d.ts +4 -4
- package/dist/types/emails/SubscriptionPaymentSuccess.d.ts +4 -4
- package/dist/types/emails/ValidateUserEmail.d.ts +4 -4
- package/dist/types/emails/ValidateUserEmail.d.ts.map +1 -1
- package/dist/types/emails/Welcome.d.ts +4 -4
- package/dist/types/export.d.ts +8 -4
- package/dist/types/models/dictionary.model.d.ts +4 -4
- package/dist/types/models/dictionary.model.d.ts.map +1 -1
- package/dist/types/models/discussion.model.d.ts +3 -3
- package/dist/types/models/discussion.model.d.ts.map +1 -1
- package/dist/types/models/oAuth2.model.d.ts +3 -3
- package/dist/types/models/oAuth2.model.d.ts.map +1 -1
- package/dist/types/routes/bitbucket.routes.d.ts +35 -0
- package/dist/types/routes/bitbucket.routes.d.ts.map +1 -0
- package/dist/types/routes/gitlab.routes.d.ts +35 -0
- package/dist/types/routes/gitlab.routes.d.ts.map +1 -0
- package/dist/types/routes/project.routes.d.ts +20 -0
- package/dist/types/routes/project.routes.d.ts.map +1 -1
- package/dist/types/schemas/dictionary.schema.d.ts +6 -6
- package/dist/types/schemas/discussion.schema.d.ts +6 -6
- package/dist/types/schemas/oAuth2.schema.d.ts +5 -5
- package/dist/types/schemas/oAuth2.schema.d.ts.map +1 -1
- package/dist/types/schemas/organization.schema.d.ts +6 -6
- package/dist/types/schemas/plans.schema.d.ts +6 -6
- package/dist/types/schemas/plans.schema.d.ts.map +1 -1
- package/dist/types/schemas/project.schema.d.ts +6 -6
- package/dist/types/schemas/project.schema.d.ts.map +1 -1
- package/dist/types/schemas/session.schema.d.ts +6 -6
- package/dist/types/schemas/tag.schema.d.ts +6 -6
- package/dist/types/schemas/user.schema.d.ts +6 -6
- package/dist/types/services/bitbucket.service.d.ts +71 -0
- package/dist/types/services/bitbucket.service.d.ts.map +1 -0
- package/dist/types/services/ci.service.d.ts +27 -0
- package/dist/types/services/ci.service.d.ts.map +1 -0
- package/dist/types/services/email.service.d.ts +11 -11
- package/dist/types/services/github.service.d.ts +20 -1
- package/dist/types/services/github.service.d.ts.map +1 -1
- package/dist/types/services/gitlab.service.d.ts +58 -0
- package/dist/types/services/gitlab.service.d.ts.map +1 -0
- package/dist/types/services/webhook.service.d.ts +19 -0
- package/dist/types/services/webhook.service.d.ts.map +1 -0
- package/dist/types/types/project.types.d.ts +32 -4
- package/dist/types/types/project.types.d.ts.map +1 -1
- package/dist/types/utils/errors/ErrorHandler.d.ts +3 -3
- package/dist/types/utils/errors/errorCodes.d.ts +156 -0
- package/dist/types/utils/errors/errorCodes.d.ts.map +1 -1
- package/dist/types/utils/filtersAndPagination/getDictionaryFiltersAndPagination.d.ts +2 -2
- package/dist/types/utils/filtersAndPagination/getDiscussionFiltersAndPagination.d.ts +2 -2
- package/dist/types/utils/filtersAndPagination/getOrganizationFiltersAndPagination.d.ts +2 -2
- package/dist/types/utils/filtersAndPagination/getProjectFiltersAndPagination.d.ts +2 -2
- package/dist/types/utils/filtersAndPagination/getTagFiltersAndPagination.d.ts +2 -2
- package/dist/types/utils/mergeFunctionTypes.d.ts.map +1 -1
- package/package.json +11 -11
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { logger } from "../logger/index.mjs";
|
|
2
|
+
import { getDBClient } from "../utils/mongoDB/connectDB.mjs";
|
|
3
|
+
import { configurationFilesCandidates } from "@intlayer/config";
|
|
4
|
+
import { ObjectId } from "mongodb";
|
|
5
|
+
|
|
6
|
+
//#region src/services/bitbucket.service.ts
|
|
7
|
+
const BITBUCKET_API_URL = "https://api.bitbucket.org/2.0";
|
|
8
|
+
const BITBUCKET_AUTH_URL = "https://bitbucket.org/site/oauth2";
|
|
9
|
+
/**
|
|
10
|
+
* Get Bitbucket (Atlassian) authorization URL for OAuth flow
|
|
11
|
+
*/
|
|
12
|
+
const getAuthorizationUrl = (redirectUri) => {
|
|
13
|
+
const clientId = process.env.ATLASSIAN_CLIENT_ID;
|
|
14
|
+
if (!clientId) throw new Error("Bitbucket/Atlassian Client ID is not configured");
|
|
15
|
+
return `${BITBUCKET_AUTH_URL}/authorize?${new URLSearchParams({
|
|
16
|
+
client_id: clientId,
|
|
17
|
+
response_type: "code",
|
|
18
|
+
state: "bitbucket_oauth"
|
|
19
|
+
}).toString()}`;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Exchange Bitbucket authorization code for access token
|
|
23
|
+
*/
|
|
24
|
+
const exchangeCodeForToken = async (code) => {
|
|
25
|
+
const clientId = process.env.ATLASSIAN_CLIENT_ID;
|
|
26
|
+
const clientSecret = process.env.ATLASSIAN_CLIENT_SECRET;
|
|
27
|
+
if (!clientId || !clientSecret) throw new Error("Bitbucket OAuth credentials are not configured");
|
|
28
|
+
try {
|
|
29
|
+
const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
|
|
30
|
+
const response = await fetch(`${BITBUCKET_AUTH_URL}/access_token`, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: {
|
|
33
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
34
|
+
Authorization: `Basic ${credentials}`
|
|
35
|
+
},
|
|
36
|
+
body: new URLSearchParams({
|
|
37
|
+
grant_type: "authorization_code",
|
|
38
|
+
code
|
|
39
|
+
})
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok) throw new Error(`Bitbucket token exchange failed: ${response.statusText}`);
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
if (data.error) throw new Error(`Bitbucket token error: ${data.error_description}`);
|
|
44
|
+
return data.access_token;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
logger.error("Error exchanging Bitbucket code for token:", error);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Get user's Bitbucket repositories
|
|
52
|
+
*/
|
|
53
|
+
const getUserRepositories = async (accessToken) => {
|
|
54
|
+
try {
|
|
55
|
+
const userResponse = await fetch(`${BITBUCKET_API_URL}/user`, { headers: {
|
|
56
|
+
Authorization: `Bearer ${accessToken}`,
|
|
57
|
+
Accept: "application/json"
|
|
58
|
+
} });
|
|
59
|
+
if (!userResponse.ok) throw new Error(`Failed to fetch Bitbucket user: ${userResponse.statusText}`);
|
|
60
|
+
const reposResponse = await fetch(`${BITBUCKET_API_URL}/repositories?role=member&sort=-updated_on&pagelen=100`, { headers: {
|
|
61
|
+
Authorization: `Bearer ${accessToken}`,
|
|
62
|
+
Accept: "application/json"
|
|
63
|
+
} });
|
|
64
|
+
if (!reposResponse.ok) throw new Error(`Failed to fetch Bitbucket repositories: ${reposResponse.statusText}`);
|
|
65
|
+
return (await reposResponse.json()).values || [];
|
|
66
|
+
} catch (error) {
|
|
67
|
+
logger.error("Error fetching Bitbucket repositories:", error);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Check if valid intlayer configuration files exist in a Bitbucket repository (Recursively).
|
|
73
|
+
* Returns an array of file paths found.
|
|
74
|
+
*/
|
|
75
|
+
const checkIntlayerConfig = async (accessToken, workspace, repoSlug, branch = "main") => {
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetch(`${BITBUCKET_API_URL}/repositories/${workspace}/${repoSlug}/src/${encodeURIComponent(branch)}/?max_depth=10&pagelen=10000`, { headers: {
|
|
78
|
+
Authorization: `Bearer ${accessToken}`,
|
|
79
|
+
Accept: "application/json"
|
|
80
|
+
} });
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
if (response.status === 404) return [];
|
|
83
|
+
throw new Error(`Failed to fetch repository tree: ${response.statusText}`);
|
|
84
|
+
}
|
|
85
|
+
return ((await response.json()).values || []).filter((item) => {
|
|
86
|
+
if (item.type !== "commit_file") return false;
|
|
87
|
+
return configurationFilesCandidates.some((candidate) => item.path.endsWith(candidate));
|
|
88
|
+
}).map((item) => item.path);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error.status === 404) return [];
|
|
91
|
+
logger.error("Error checking intlayer configuration on Bitbucket:", error);
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Get repository file contents from Bitbucket and decode it
|
|
97
|
+
*/
|
|
98
|
+
const getRepositoryFileContents = async (accessToken, workspace, repoSlug, path, branch = "main") => {
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetch(`${BITBUCKET_API_URL}/repositories/${workspace}/${repoSlug}/src/${encodeURIComponent(branch)}/${encodeURIComponent(path)}`, { headers: {
|
|
101
|
+
Authorization: `Bearer ${accessToken}`,
|
|
102
|
+
Accept: "application/json"
|
|
103
|
+
} });
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
if (response.status === 404) return null;
|
|
106
|
+
throw new Error(`Failed to fetch file contents: ${response.statusText}`);
|
|
107
|
+
}
|
|
108
|
+
return await response.text();
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error.status === 404) return null;
|
|
111
|
+
logger.error("Error fetching Bitbucket file contents:", error);
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Get Bitbucket access token from user's linked account (Atlassian)
|
|
117
|
+
*/
|
|
118
|
+
const getBitbucketTokenFromUser = async (userId) => {
|
|
119
|
+
try {
|
|
120
|
+
const db = getDBClient().db();
|
|
121
|
+
let account = await db.collection("account").findOne({
|
|
122
|
+
userId,
|
|
123
|
+
providerId: "atlassian"
|
|
124
|
+
});
|
|
125
|
+
if (!account && ObjectId.isValid(userId)) account = await db.collection("account").findOne({
|
|
126
|
+
userId: new ObjectId(userId),
|
|
127
|
+
providerId: "atlassian"
|
|
128
|
+
});
|
|
129
|
+
if (!account) account = await db.collection("accounts").findOne({
|
|
130
|
+
userId,
|
|
131
|
+
providerId: "atlassian"
|
|
132
|
+
});
|
|
133
|
+
if (!account && ObjectId.isValid(userId)) account = await db.collection("accounts").findOne({
|
|
134
|
+
userId: new ObjectId(userId),
|
|
135
|
+
providerId: "atlassian"
|
|
136
|
+
});
|
|
137
|
+
if (!account) return null;
|
|
138
|
+
return account.accessToken || account.access_token || null;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
logger.error("Error retrieving Bitbucket token from DB:", error);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Check if a Bitbucket pipeline file exists
|
|
146
|
+
*/
|
|
147
|
+
const checkPipelineFileExists = async (accessToken, workspace, repoSlug, filename = "bitbucket-pipelines.yml", branch = "main") => {
|
|
148
|
+
try {
|
|
149
|
+
const response = await fetch(`${BITBUCKET_API_URL}/repositories/${workspace}/${repoSlug}/src/${encodeURIComponent(branch)}/${encodeURIComponent(filename)}`, { headers: {
|
|
150
|
+
Authorization: `Bearer ${accessToken}`,
|
|
151
|
+
Accept: "application/json"
|
|
152
|
+
} });
|
|
153
|
+
if (response.status === 404) return false;
|
|
154
|
+
if (!response.ok) throw new Error(`Failed to check file existence: ${response.statusText}`);
|
|
155
|
+
return true;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error.status === 404) return false;
|
|
158
|
+
logger.error("Error checking pipeline file existence:", error);
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Create or update a Bitbucket pipeline file
|
|
164
|
+
* Note: Bitbucket API doesn't support direct file creation via API v2.0
|
|
165
|
+
* This function returns false for allowAutoPush, requiring manual installation
|
|
166
|
+
*/
|
|
167
|
+
const createPipelineFile = async (accessToken, workspace, repoSlug, filename = "bitbucket-pipelines.yml", content, branch = "main", message = "Add Intlayer CI pipeline") => {
|
|
168
|
+
throw new Error("Bitbucket API does not support automatic file creation. Please manually add the pipeline file to your repository.");
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
//#endregion
|
|
172
|
+
export { checkIntlayerConfig, checkPipelineFileExists, createPipelineFile, exchangeCodeForToken, getAuthorizationUrl, getBitbucketTokenFromUser, getRepositoryFileContents, getUserRepositories };
|
|
173
|
+
//# sourceMappingURL=bitbucket.service.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bitbucket.service.mjs","names":["error: any"],"sources":["../../../src/services/bitbucket.service.ts"],"sourcesContent":["import { configurationFilesCandidates } from '@intlayer/config';\nimport { logger } from '@logger';\nimport { getDBClient } from '@utils/mongoDB/connectDB';\nimport { ObjectId } from 'mongodb';\n\nconst BITBUCKET_API_URL = 'https://api.bitbucket.org/2.0';\nconst BITBUCKET_AUTH_URL = 'https://bitbucket.org/site/oauth2';\n\nexport type BitbucketRepository = {\n uuid: string;\n name: string;\n full_name: string;\n slug: string;\n mainbranch?: {\n name: string;\n type: string;\n };\n links: {\n html: {\n href: string;\n };\n };\n workspace: {\n slug: string;\n name: string;\n uuid: string;\n };\n owner: {\n display_name: string;\n username?: string;\n uuid: string;\n };\n updated_on: string;\n is_private: boolean;\n};\n\nexport type BitbucketTreeItem = {\n path: string;\n type: 'commit_file' | 'commit_directory';\n size?: number;\n};\n\n/**\n * Get Bitbucket (Atlassian) authorization URL for OAuth flow\n */\nexport const getAuthorizationUrl = (redirectUri: string): string => {\n const clientId = process.env.ATLASSIAN_CLIENT_ID;\n\n if (!clientId) {\n throw new Error('Bitbucket/Atlassian Client ID is not configured');\n }\n\n const params = new URLSearchParams({\n client_id: clientId,\n response_type: 'code',\n state: 'bitbucket_oauth',\n });\n\n return `${BITBUCKET_AUTH_URL}/authorize?${params.toString()}`;\n};\n\n/**\n * Exchange Bitbucket authorization code for access token\n */\nexport const exchangeCodeForToken = async (code: string): Promise<string> => {\n const clientId = process.env.ATLASSIAN_CLIENT_ID;\n const clientSecret = process.env.ATLASSIAN_CLIENT_SECRET;\n\n if (!clientId || !clientSecret) {\n throw new Error('Bitbucket OAuth credentials are not configured');\n }\n\n try {\n const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString(\n 'base64'\n );\n\n const response = await fetch(`${BITBUCKET_AUTH_URL}/access_token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Authorization: `Basic ${credentials}`,\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n }),\n });\n\n if (!response.ok) {\n throw new Error(\n `Bitbucket token exchange failed: ${response.statusText}`\n );\n }\n\n const data = await response.json();\n\n if (data.error) {\n throw new Error(`Bitbucket token error: ${data.error_description}`);\n }\n\n return data.access_token;\n } catch (error) {\n logger.error('Error exchanging Bitbucket code for token:', error);\n throw error;\n }\n};\n\n/**\n * Get user's Bitbucket repositories\n */\nexport const getUserRepositories = async (\n accessToken: string\n): Promise<BitbucketRepository[]> => {\n try {\n // First, get the current user to find their workspaces\n const userResponse = await fetch(`${BITBUCKET_API_URL}/user`, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n });\n\n if (!userResponse.ok) {\n throw new Error(\n `Failed to fetch Bitbucket user: ${userResponse.statusText}`\n );\n }\n\n // Get repositories the user has access to\n const reposResponse = await fetch(\n `${BITBUCKET_API_URL}/repositories?role=member&sort=-updated_on&pagelen=100`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!reposResponse.ok) {\n throw new Error(\n `Failed to fetch Bitbucket repositories: ${reposResponse.statusText}`\n );\n }\n\n const data = await reposResponse.json();\n return data.values || [];\n } catch (error) {\n logger.error('Error fetching Bitbucket repositories:', error);\n throw error;\n }\n};\n\n/**\n * Check if valid intlayer configuration files exist in a Bitbucket repository (Recursively).\n * Returns an array of file paths found.\n */\nexport const checkIntlayerConfig = async (\n accessToken: string,\n workspace: string,\n repoSlug: string,\n branch: string = 'main'\n): Promise<string[]> => {\n try {\n // Use Bitbucket's src API to list files recursively\n const response = await fetch(\n `${BITBUCKET_API_URL}/repositories/${workspace}/${repoSlug}/src/${encodeURIComponent(branch)}/?max_depth=10&pagelen=10000`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n if (response.status === 404) return [];\n throw new Error(\n `Failed to fetch repository tree: ${response.statusText}`\n );\n }\n\n const data = await response.json();\n const items: BitbucketTreeItem[] = data.values || [];\n\n // Filter files that match the configuration candidates\n const foundFiles = items\n .filter((item) => {\n if (item.type !== 'commit_file') return false;\n return (configurationFilesCandidates as readonly string[]).some(\n (candidate) => item.path.endsWith(candidate)\n );\n })\n .map((item) => item.path);\n\n return foundFiles;\n } catch (error: any) {\n if (error.status === 404) return [];\n logger.error('Error checking intlayer configuration on Bitbucket:', error);\n return [];\n }\n};\n\n/**\n * Get repository file contents from Bitbucket and decode it\n */\nexport const getRepositoryFileContents = async (\n accessToken: string,\n workspace: string,\n repoSlug: string,\n path: string,\n branch: string = 'main'\n): Promise<string | null> => {\n try {\n const response = await fetch(\n `${BITBUCKET_API_URL}/repositories/${workspace}/${repoSlug}/src/${encodeURIComponent(branch)}/${encodeURIComponent(path)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n if (response.status === 404) return null;\n throw new Error(`Failed to fetch file contents: ${response.statusText}`);\n }\n\n const content = await response.text();\n return content;\n } catch (error: any) {\n if (error.status === 404) return null;\n logger.error('Error fetching Bitbucket file contents:', error);\n throw error;\n }\n};\n\n/**\n * Get Bitbucket access token from user's linked account (Atlassian)\n */\nexport const getBitbucketTokenFromUser = async (\n userId: string\n): Promise<string | null> => {\n try {\n const client = getDBClient();\n const db = client.db();\n\n // Try with 'atlassian' provider ID (as it's linked through Better Auth's atlassian provider)\n let account = await db.collection('account').findOne({\n userId: userId,\n providerId: 'atlassian',\n });\n\n if (!account && ObjectId.isValid(userId)) {\n account = await db.collection('account').findOne({\n userId: new ObjectId(userId),\n providerId: 'atlassian',\n });\n }\n\n if (!account) {\n account = await db.collection('accounts').findOne({\n userId: userId,\n providerId: 'atlassian',\n });\n }\n\n if (!account && ObjectId.isValid(userId)) {\n account = await db.collection('accounts').findOne({\n userId: new ObjectId(userId),\n providerId: 'atlassian',\n });\n }\n\n if (!account) {\n return null;\n }\n\n const accessToken = account.accessToken || account.access_token;\n\n return accessToken || null;\n } catch (error) {\n logger.error('Error retrieving Bitbucket token from DB:', error);\n return null;\n }\n};\n\n/**\n * Check if a Bitbucket pipeline file exists\n */\nexport const checkPipelineFileExists = async (\n accessToken: string,\n workspace: string,\n repoSlug: string,\n filename: string = 'bitbucket-pipelines.yml',\n branch: string = 'main'\n): Promise<boolean> => {\n try {\n const response = await fetch(\n `${BITBUCKET_API_URL}/repositories/${workspace}/${repoSlug}/src/${encodeURIComponent(branch)}/${encodeURIComponent(filename)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (response.status === 404) return false;\n if (!response.ok) {\n throw new Error(`Failed to check file existence: ${response.statusText}`);\n }\n\n return true;\n } catch (error: any) {\n if (error.status === 404) return false;\n logger.error('Error checking pipeline file existence:', error);\n throw error;\n }\n};\n\n/**\n * Create or update a Bitbucket pipeline file\n * Note: Bitbucket API doesn't support direct file creation via API v2.0\n * This function returns false for allowAutoPush, requiring manual installation\n */\nexport const createPipelineFile = async (\n accessToken: string,\n workspace: string,\n repoSlug: string,\n filename: string = 'bitbucket-pipelines.yml',\n content: string,\n branch: string = 'main',\n message: string = 'Add Intlayer CI pipeline'\n): Promise<void> => {\n // Bitbucket API v2.0 doesn't support direct file creation/update\n // Users need to manually add the file or use the web interface\n // We'll throw an error indicating manual installation is required\n throw new Error(\n 'Bitbucket API does not support automatic file creation. Please manually add the pipeline file to your repository.'\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { logger } from "../logger/index.mjs";
|
|
2
|
+
import { checkPipelineFileExists } from "./bitbucket.service.mjs";
|
|
3
|
+
import { checkWorkflowFileExists, createWorkflowFile } from "./github.service.mjs";
|
|
4
|
+
import { checkPipelineFileExists as checkPipelineFileExists$1, createPipelineFile } from "./gitlab.service.mjs";
|
|
5
|
+
|
|
6
|
+
//#region src/services/ci.service.ts
|
|
7
|
+
const GITHUB_WORKFLOW_FILENAME = ".github/workflows/intlayer-cms.yml";
|
|
8
|
+
const GITLAB_PIPELINE_FILENAME = ".gitlab-ci.yml";
|
|
9
|
+
const BITBUCKET_PIPELINE_FILENAME = "bitbucket-pipelines.yml";
|
|
10
|
+
const GITHUB_TEMPLATE = `name: Intlayer CMS Update
|
|
11
|
+
on:
|
|
12
|
+
repository_dispatch:
|
|
13
|
+
types: [intlayer_cms_update]
|
|
14
|
+
jobs:
|
|
15
|
+
build:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- run: npm ci
|
|
20
|
+
- run: npm run build
|
|
21
|
+
`;
|
|
22
|
+
const GITLAB_TEMPLATE = `stages:
|
|
23
|
+
- build
|
|
24
|
+
|
|
25
|
+
intlayer_cms_update:
|
|
26
|
+
stage: build
|
|
27
|
+
only:
|
|
28
|
+
- triggers
|
|
29
|
+
script:
|
|
30
|
+
- npm ci
|
|
31
|
+
- npm run build
|
|
32
|
+
`;
|
|
33
|
+
const BITBUCKET_TEMPLATE = `pipelines:
|
|
34
|
+
custom:
|
|
35
|
+
intlayer-cms-update:
|
|
36
|
+
- step:
|
|
37
|
+
name: Build
|
|
38
|
+
script:
|
|
39
|
+
- npm ci
|
|
40
|
+
- npm run build
|
|
41
|
+
`;
|
|
42
|
+
/**
|
|
43
|
+
* Get access token from project's OAuth2 access
|
|
44
|
+
*/
|
|
45
|
+
const getAccessToken = (project) => {
|
|
46
|
+
return (project.oAuth2Access?.[0])?.accessToken?.[0] || null;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Get CI configuration status for a project
|
|
50
|
+
*/
|
|
51
|
+
const getCIStatus = async (project) => {
|
|
52
|
+
const { repository } = project;
|
|
53
|
+
if (!repository) throw new Error("Project is not connected to a repository.");
|
|
54
|
+
const accessToken = getAccessToken(project);
|
|
55
|
+
if (!accessToken) throw new Error("No valid OAuth2 access token found.");
|
|
56
|
+
const branch = repository.branch || "main";
|
|
57
|
+
try {
|
|
58
|
+
switch (repository.provider) {
|
|
59
|
+
case "github": {
|
|
60
|
+
const { owner, repository: repoName } = repository;
|
|
61
|
+
const filename = GITHUB_WORKFLOW_FILENAME;
|
|
62
|
+
return {
|
|
63
|
+
exists: await checkWorkflowFileExists(accessToken, owner, repoName, filename, branch),
|
|
64
|
+
content: GITHUB_TEMPLATE,
|
|
65
|
+
path: filename,
|
|
66
|
+
fileUrl: `https://github.com/${owner}/${repoName}/blob/${branch}/${filename}`,
|
|
67
|
+
allowAutoPush: true
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
case "gitlab": {
|
|
71
|
+
const { projectId, instanceUrl } = repository;
|
|
72
|
+
if (!projectId) throw new Error("GitLab project ID is required.");
|
|
73
|
+
const filename = GITLAB_PIPELINE_FILENAME;
|
|
74
|
+
return {
|
|
75
|
+
exists: await checkPipelineFileExists$1(accessToken, projectId, filename, branch, instanceUrl),
|
|
76
|
+
content: GITLAB_TEMPLATE,
|
|
77
|
+
path: filename,
|
|
78
|
+
fileUrl: `${instanceUrl || "https://gitlab.com"}/${repository.owner}/${repository.repository}/-/blob/${branch}/${filename}`,
|
|
79
|
+
allowAutoPush: true
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
case "bitbucket": {
|
|
83
|
+
const { workspace, repository: repoSlug } = repository;
|
|
84
|
+
const filename = BITBUCKET_PIPELINE_FILENAME;
|
|
85
|
+
return {
|
|
86
|
+
exists: await checkPipelineFileExists(accessToken, workspace, repoSlug, filename, branch),
|
|
87
|
+
content: BITBUCKET_TEMPLATE,
|
|
88
|
+
path: filename,
|
|
89
|
+
fileUrl: `https://bitbucket.org/${workspace}/${repoSlug}/src/${branch}/${filename}`,
|
|
90
|
+
allowAutoPush: false
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
default: throw new Error(`Unsupported repository provider: ${repository.provider}`);
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
logger.error("Error getting CI status:", error);
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Install CI configuration file in the repository
|
|
102
|
+
*/
|
|
103
|
+
const installCI = async (project) => {
|
|
104
|
+
const { repository } = project;
|
|
105
|
+
if (!repository) throw new Error("Project is not connected to a repository.");
|
|
106
|
+
const accessToken = getAccessToken(project);
|
|
107
|
+
if (!accessToken) throw new Error("No valid OAuth2 access token found.");
|
|
108
|
+
const branch = repository.branch || "main";
|
|
109
|
+
try {
|
|
110
|
+
switch (repository.provider) {
|
|
111
|
+
case "github": {
|
|
112
|
+
const { owner, repository: repoName } = repository;
|
|
113
|
+
await createWorkflowFile(accessToken, owner, repoName, GITHUB_WORKFLOW_FILENAME, GITHUB_TEMPLATE, branch, "Add Intlayer CMS workflow");
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case "gitlab": {
|
|
117
|
+
const { projectId, instanceUrl } = repository;
|
|
118
|
+
if (!projectId) throw new Error("GitLab project ID is required.");
|
|
119
|
+
await createPipelineFile(accessToken, projectId, GITLAB_PIPELINE_FILENAME, GITLAB_TEMPLATE, branch, instanceUrl, "Add Intlayer CMS pipeline");
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case "bitbucket": throw new Error("Bitbucket does not support automatic CI file installation. Please add the file manually.");
|
|
123
|
+
default: throw new Error(`Unsupported repository provider: ${repository.provider}`);
|
|
124
|
+
}
|
|
125
|
+
logger.info(`Successfully installed CI configuration for project ${project.id}`);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
logger.error("Error installing CI configuration:", error);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
//#endregion
|
|
133
|
+
export { BITBUCKET_PIPELINE_FILENAME, BITBUCKET_TEMPLATE, GITHUB_TEMPLATE, GITHUB_WORKFLOW_FILENAME, GITLAB_PIPELINE_FILENAME, GITLAB_TEMPLATE, getCIStatus, installCI };
|
|
134
|
+
//# sourceMappingURL=ci.service.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ci.service.mjs","names":["githubService.checkWorkflowFileExists","gitlabService.checkPipelineFileExists","bitbucketService.checkPipelineFileExists","githubService.createWorkflowFile","gitlabService.createPipelineFile"],"sources":["../../../src/services/ci.service.ts"],"sourcesContent":["import { logger } from '@logger';\nimport type { Project } from '@/types/project.types';\nimport * as bitbucketService from './bitbucket.service';\nimport * as githubService from './github.service';\nimport * as gitlabService from './gitlab.service';\n\nexport const GITHUB_WORKFLOW_FILENAME = '.github/workflows/intlayer-cms.yml';\nexport const GITLAB_PIPELINE_FILENAME = '.gitlab-ci.yml';\nexport const BITBUCKET_PIPELINE_FILENAME = 'bitbucket-pipelines.yml';\n\nexport const GITHUB_TEMPLATE = `name: Intlayer CMS Update\non:\n repository_dispatch:\n types: [intlayer_cms_update]\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - run: npm ci\n - run: npm run build\n`;\n\nexport const GITLAB_TEMPLATE = `stages:\n - build\n\nintlayer_cms_update:\n stage: build\n only:\n - triggers\n script:\n - npm ci\n - npm run build\n`;\n\nexport const BITBUCKET_TEMPLATE = `pipelines:\n custom:\n intlayer-cms-update:\n - step:\n name: Build\n script:\n - npm ci\n - npm run build\n`;\n\nexport type CIStatus = {\n exists: boolean;\n content: string;\n path: string;\n fileUrl?: string;\n allowAutoPush: boolean;\n};\n\n/**\n * Get access token from project's OAuth2 access\n */\nconst getAccessToken = (project: Project): string | null => {\n const tokenData = project.oAuth2Access?.[0];\n const accessToken = tokenData?.accessToken?.[0];\n return accessToken || null;\n};\n\n/**\n * Get CI configuration status for a project\n */\nexport const getCIStatus = async (project: Project): Promise<CIStatus> => {\n const { repository } = project;\n\n if (!repository) {\n throw new Error('Project is not connected to a repository.');\n }\n\n const accessToken = getAccessToken(project);\n\n if (!accessToken) {\n throw new Error('No valid OAuth2 access token found.');\n }\n\n const branch = repository.branch || 'main';\n\n try {\n switch (repository.provider) {\n case 'github': {\n const { owner, repository: repoName } = repository;\n const filename = GITHUB_WORKFLOW_FILENAME;\n const exists = await githubService.checkWorkflowFileExists(\n accessToken,\n owner,\n repoName,\n filename,\n branch\n );\n\n const content = GITHUB_TEMPLATE;\n const fileUrl = `https://github.com/${owner}/${repoName}/blob/${branch}/${filename}`;\n\n return {\n exists,\n content,\n path: filename,\n fileUrl,\n allowAutoPush: true, // GitHub allows file creation via API\n };\n }\n\n case 'gitlab': {\n const { projectId, instanceUrl } = repository;\n if (!projectId) {\n throw new Error('GitLab project ID is required.');\n }\n\n const filename = GITLAB_PIPELINE_FILENAME;\n const exists = await gitlabService.checkPipelineFileExists(\n accessToken,\n projectId,\n filename,\n branch,\n instanceUrl\n );\n\n const content = GITLAB_TEMPLATE;\n const baseUrl = instanceUrl || 'https://gitlab.com';\n const fileUrl = `${baseUrl}/${repository.owner}/${repository.repository}/-/blob/${branch}/${filename}`;\n\n return {\n exists,\n content,\n path: filename,\n fileUrl,\n allowAutoPush: true, // GitLab allows file creation via API\n };\n }\n\n case 'bitbucket': {\n const { workspace, repository: repoSlug } = repository;\n const filename = BITBUCKET_PIPELINE_FILENAME;\n const exists = await bitbucketService.checkPipelineFileExists(\n accessToken,\n workspace,\n repoSlug,\n filename,\n branch\n );\n\n const content = BITBUCKET_TEMPLATE;\n const fileUrl = `https://bitbucket.org/${workspace}/${repoSlug}/src/${branch}/${filename}`;\n\n return {\n exists,\n content,\n path: filename,\n fileUrl,\n allowAutoPush: false, // Bitbucket API doesn't support automatic file creation\n };\n }\n\n default:\n throw new Error(\n `Unsupported repository provider: ${repository.provider}`\n );\n }\n } catch (error) {\n logger.error('Error getting CI status:', error);\n throw error;\n }\n};\n\n/**\n * Install CI configuration file in the repository\n */\nexport const installCI = async (project: Project): Promise<void> => {\n const { repository } = project;\n\n if (!repository) {\n throw new Error('Project is not connected to a repository.');\n }\n\n const accessToken = getAccessToken(project);\n\n if (!accessToken) {\n throw new Error('No valid OAuth2 access token found.');\n }\n\n const branch = repository.branch || 'main';\n\n try {\n switch (repository.provider) {\n case 'github': {\n const { owner, repository: repoName } = repository;\n await githubService.createWorkflowFile(\n accessToken,\n owner,\n repoName,\n GITHUB_WORKFLOW_FILENAME,\n GITHUB_TEMPLATE,\n branch,\n 'Add Intlayer CMS workflow'\n );\n break;\n }\n\n case 'gitlab': {\n const { projectId, instanceUrl } = repository;\n if (!projectId) {\n throw new Error('GitLab project ID is required.');\n }\n\n await gitlabService.createPipelineFile(\n accessToken,\n projectId,\n GITLAB_PIPELINE_FILENAME,\n GITLAB_TEMPLATE,\n branch,\n instanceUrl,\n 'Add Intlayer CMS pipeline'\n );\n break;\n }\n\n case 'bitbucket': {\n // Bitbucket API doesn't support automatic file creation\n // This should not be called if allowAutoPush is false\n throw new Error(\n 'Bitbucket does not support automatic CI file installation. Please add the file manually.'\n );\n }\n\n default:\n throw new Error(\n `Unsupported repository provider: ${repository.provider}`\n );\n }\n\n logger.info(\n `Successfully installed CI configuration for project ${project.id}`\n );\n } catch (error) {\n logger.error('Error installing CI configuration:', error);\n throw error;\n }\n};\n"],"mappings":";;;;;;AAMA,MAAa,2BAA2B;AACxC,MAAa,2BAA2B;AACxC,MAAa,8BAA8B;AAE3C,MAAa,kBAAkB;;;;;;;;;;;;AAa/B,MAAa,kBAAkB;;;;;;;;;;;AAY/B,MAAa,qBAAqB;;;;;;;;;;;;AAqBlC,MAAM,kBAAkB,YAAoC;AAG1D,SAFkB,QAAQ,eAAe,KACV,cAAc,MACvB;;;;;AAMxB,MAAa,cAAc,OAAO,YAAwC;CACxE,MAAM,EAAE,eAAe;AAEvB,KAAI,CAAC,WACH,OAAM,IAAI,MAAM,4CAA4C;CAG9D,MAAM,cAAc,eAAe,QAAQ;AAE3C,KAAI,CAAC,YACH,OAAM,IAAI,MAAM,sCAAsC;CAGxD,MAAM,SAAS,WAAW,UAAU;AAEpC,KAAI;AACF,UAAQ,WAAW,UAAnB;GACE,KAAK,UAAU;IACb,MAAM,EAAE,OAAO,YAAY,aAAa;IACxC,MAAM,WAAW;AAYjB,WAAO;KACL,QAZa,MAAMA,wBACnB,aACA,OACA,UACA,UACA,OACD;KAOC,SALc;KAMd,MAAM;KACN,SANc,sBAAsB,MAAM,GAAG,SAAS,QAAQ,OAAO,GAAG;KAOxE,eAAe;KAChB;;GAGH,KAAK,UAAU;IACb,MAAM,EAAE,WAAW,gBAAgB;AACnC,QAAI,CAAC,UACH,OAAM,IAAI,MAAM,iCAAiC;IAGnD,MAAM,WAAW;AAajB,WAAO;KACL,QAba,MAAMC,0BACnB,aACA,WACA,UACA,QACA,YACD;KAQC,SANc;KAOd,MAAM;KACN,SANc,GADA,eAAe,qBACJ,GAAG,WAAW,MAAM,GAAG,WAAW,WAAW,UAAU,OAAO,GAAG;KAO1F,eAAe;KAChB;;GAGH,KAAK,aAAa;IAChB,MAAM,EAAE,WAAW,YAAY,aAAa;IAC5C,MAAM,WAAW;AAYjB,WAAO;KACL,QAZa,MAAMC,wBACnB,aACA,WACA,UACA,UACA,OACD;KAOC,SALc;KAMd,MAAM;KACN,SANc,yBAAyB,UAAU,GAAG,SAAS,OAAO,OAAO,GAAG;KAO9E,eAAe;KAChB;;GAGH,QACE,OAAM,IAAI,MACR,oCAAoC,WAAW,WAChD;;UAEE,OAAO;AACd,SAAO,MAAM,4BAA4B,MAAM;AAC/C,QAAM;;;;;;AAOV,MAAa,YAAY,OAAO,YAAoC;CAClE,MAAM,EAAE,eAAe;AAEvB,KAAI,CAAC,WACH,OAAM,IAAI,MAAM,4CAA4C;CAG9D,MAAM,cAAc,eAAe,QAAQ;AAE3C,KAAI,CAAC,YACH,OAAM,IAAI,MAAM,sCAAsC;CAGxD,MAAM,SAAS,WAAW,UAAU;AAEpC,KAAI;AACF,UAAQ,WAAW,UAAnB;GACE,KAAK,UAAU;IACb,MAAM,EAAE,OAAO,YAAY,aAAa;AACxC,UAAMC,mBACJ,aACA,OACA,UACA,0BACA,iBACA,QACA,4BACD;AACD;;GAGF,KAAK,UAAU;IACb,MAAM,EAAE,WAAW,gBAAgB;AACnC,QAAI,CAAC,UACH,OAAM,IAAI,MAAM,iCAAiC;AAGnD,UAAMC,mBACJ,aACA,WACA,0BACA,iBACA,QACA,aACA,4BACD;AACD;;GAGF,KAAK,YAGH,OAAM,IAAI,MACR,2FACD;GAGH,QACE,OAAM,IAAI,MACR,oCAAoC,WAAW,WAChD;;AAGL,SAAO,KACL,uDAAuD,QAAQ,KAChE;UACM,OAAO;AACd,SAAO,MAAM,sCAAsC,MAAM;AACzD,QAAM"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { logger } from "../logger/index.mjs";
|
|
2
2
|
import { getDBClient } from "../utils/mongoDB/connectDB.mjs";
|
|
3
3
|
import { configurationFilesCandidates } from "@intlayer/config";
|
|
4
|
-
import { Octokit } from "@octokit/rest";
|
|
5
4
|
import { ObjectId } from "mongodb";
|
|
5
|
+
import { Octokit } from "@octokit/rest";
|
|
6
6
|
|
|
7
7
|
//#region src/services/github.service.ts
|
|
8
8
|
const getAuthorizationUrl = (redirectUri, login) => {
|
|
@@ -124,7 +124,95 @@ const getGitHubTokenFromUser = async (userId) => {
|
|
|
124
124
|
return null;
|
|
125
125
|
}
|
|
126
126
|
};
|
|
127
|
+
const triggerGithubDispatch = async ({ project, eventType = "intlayer_cms_update", payload = {} }) => {
|
|
128
|
+
const { repository, oAuth2Access } = project;
|
|
129
|
+
if (!repository || repository.provider !== "github") throw new Error("Project is not connected to a GitHub repository.");
|
|
130
|
+
const accessToken = (oAuth2Access?.[0])?.accessToken?.[0];
|
|
131
|
+
if (!accessToken) throw new Error("No valid OAuth2 access token found for GitHub.");
|
|
132
|
+
const { owner, repository: repoName } = repository;
|
|
133
|
+
const url = `https://api.github.com/repos/${owner}/${repoName}/dispatches`;
|
|
134
|
+
try {
|
|
135
|
+
const response = await fetch(url, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: {
|
|
138
|
+
Authorization: `Bearer ${accessToken}`,
|
|
139
|
+
Accept: "application/vnd.github+json",
|
|
140
|
+
"Content-Type": "application/json"
|
|
141
|
+
},
|
|
142
|
+
body: JSON.stringify({
|
|
143
|
+
event_type: eventType,
|
|
144
|
+
client_payload: {
|
|
145
|
+
...payload,
|
|
146
|
+
projectId: project.id,
|
|
147
|
+
timestamp: Date.now()
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
});
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
const errorText = await response.text();
|
|
153
|
+
throw new Error(`GitHub API Error: ${response.status} - ${errorText}`);
|
|
154
|
+
}
|
|
155
|
+
logger.info(`Successfully triggered GitHub Action '${eventType}' for ${owner}/${repoName}`);
|
|
156
|
+
return true;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
logger.error(error);
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Check if a GitHub workflow file exists
|
|
164
|
+
*/
|
|
165
|
+
const checkWorkflowFileExists = async (accessToken, owner, repo, filename, branch = "main") => {
|
|
166
|
+
try {
|
|
167
|
+
await new Octokit({ auth: accessToken }).rest.repos.getContent({
|
|
168
|
+
owner,
|
|
169
|
+
repo,
|
|
170
|
+
path: filename,
|
|
171
|
+
ref: branch
|
|
172
|
+
});
|
|
173
|
+
return true;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (error.status === 404) return false;
|
|
176
|
+
logger.error("Error checking workflow file existence:", error);
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Create or update a GitHub workflow file
|
|
182
|
+
*/
|
|
183
|
+
const createWorkflowFile = async (accessToken, owner, repo, filename, content, branch = "main", message = "Add Intlayer CI workflow") => {
|
|
184
|
+
try {
|
|
185
|
+
const octokit = new Octokit({ auth: accessToken });
|
|
186
|
+
let sha;
|
|
187
|
+
try {
|
|
188
|
+
const { data } = await octokit.rest.repos.getContent({
|
|
189
|
+
owner,
|
|
190
|
+
repo,
|
|
191
|
+
path: filename,
|
|
192
|
+
ref: branch
|
|
193
|
+
});
|
|
194
|
+
if (Array.isArray(data) || !("sha" in data)) throw new Error("Path points to a directory, not a file");
|
|
195
|
+
sha = data.sha;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (error.status !== 404) throw error;
|
|
198
|
+
}
|
|
199
|
+
const encodedContent = Buffer.from(content, "utf-8").toString("base64");
|
|
200
|
+
await octokit.rest.repos.createOrUpdateFileContents({
|
|
201
|
+
owner,
|
|
202
|
+
repo,
|
|
203
|
+
path: filename,
|
|
204
|
+
message,
|
|
205
|
+
content: encodedContent,
|
|
206
|
+
branch,
|
|
207
|
+
...sha && { sha }
|
|
208
|
+
});
|
|
209
|
+
logger.info(`Successfully ${sha ? "updated" : "created"} workflow file ${filename} for ${owner}/${repo}`);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
logger.error("Error creating/updating workflow file:", error);
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
127
215
|
|
|
128
216
|
//#endregion
|
|
129
|
-
export { checkIntlayerConfig, exchangeCodeForToken, getAuthorizationUrl, getGitHubTokenFromUser, getRepositoryFileContents, getUserRepos };
|
|
217
|
+
export { checkIntlayerConfig, checkWorkflowFileExists, createWorkflowFile, exchangeCodeForToken, getAuthorizationUrl, getGitHubTokenFromUser, getRepositoryFileContents, getUserRepos, triggerGithubDispatch };
|
|
130
218
|
//# sourceMappingURL=github.service.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"github.service.mjs","names":["error: any"],"sources":["../../../src/services/github.service.ts"],"sourcesContent":["import { configurationFilesCandidates } from '@intlayer/config';\nimport { logger } from '@logger';\nimport type { RestEndpointMethodTypes } from '@octokit/rest';\nimport { Octokit } from '@octokit/rest';\nimport { getDBClient } from '@utils/mongoDB/connectDB';\nimport { ObjectId } from 'mongodb';\n\nexport type GitHubRepository =\n RestEndpointMethodTypes['repos']['listForAuthenticatedUser']['response']['data'][0];\nexport type GitHubFileContent =\n RestEndpointMethodTypes['repos']['getContent']['response']['data'];\n\nexport const getAuthorizationUrl = (\n redirectUri: string,\n login?: string\n): string => {\n const clientId = process.env.GITHUB_CLIENT_ID;\n\n if (!clientId) {\n throw new Error('GitHub Client ID is not configured');\n }\n\n const params = new URLSearchParams({\n client_id: clientId,\n scope: 'repo',\n state: 'github_oauth',\n redirect_uri: redirectUri,\n });\n\n if (login) {\n params.append('login', login);\n }\n\n return `https://github.com/login/oauth/authorize?${params.toString()}`;\n};\n\nexport const exchangeCodeForToken = async (code: string): Promise<string> => {\n const clientId = process.env.GITHUB_CLIENT_ID;\n const clientSecret = process.env.GITHUB_CLIENT_SECRET;\n\n if (!clientId || !clientSecret) {\n throw new Error('GitHub OAuth credentials are not configured');\n }\n\n try {\n const response = await fetch(\n 'https://github.com/login/oauth/access_token',\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify({\n client_id: clientId,\n client_secret: clientSecret,\n code,\n }),\n }\n );\n\n if (!response.ok) {\n throw new Error(`GitHub token exchange failed: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n if (data.error) {\n throw new Error(`GitHub token error: ${data.error_description}`);\n }\n\n return data.access_token;\n } catch (error) {\n logger.error('Error exchanging GitHub code for token:', error);\n throw error;\n }\n};\n\nexport const getUserRepos = async (\n accessToken: string\n): Promise<GitHubRepository[]> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n\n const { data } = await octokit.rest.repos.listForAuthenticatedUser({\n sort: 'updated',\n per_page: 100,\n visibility: 'all',\n });\n\n return data;\n } catch (error) {\n logger.error('Error fetching GitHub repositories:', error);\n throw error;\n }\n};\n\n/**\n * Check if valid intlayer configuration files exist in a repository (Recursively).\n * Returns an array of file paths found (e.g. ['intlayer.config.ts', 'apps/web/intlayer.config.js']).\n */\nexport const checkIntlayerConfig = async (\n accessToken: string,\n owner: string,\n repo: string,\n branch: string = 'main'\n): Promise<string[]> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n\n // Use Git Tree API to get all files recursively\n // This allows finding configs in monorepos/subfolders\n const { data } = await octokit.rest.git.getTree({\n owner,\n repo,\n tree_sha: branch,\n recursive: 'true',\n });\n\n if (!data.tree || !Array.isArray(data.tree)) {\n return [];\n }\n\n // Filter files that match the configuration candidates\n // We check if the path ends with one of the candidate filenames\n const foundFiles = data.tree\n .filter((item) => {\n if (item.type !== 'blob' || !item.path) return false;\n return (configurationFilesCandidates as readonly string[]).some(\n (candidate) => item.path?.endsWith(candidate)\n );\n })\n .map((item) => item.path as string); // Return the full path (e.g., 'packages/app/intlayer.config.ts')\n\n return foundFiles;\n } catch (error: any) {\n // If branch doesn't exist or repo is empty\n if (error.status === 404 || error.status === 409) return [];\n\n logger.error('Error checking intlayer configuration:', error);\n return [];\n }\n};\n\n/**\n * Get repository file contents and decode it\n */\nexport const getRepositoryFileContents = async (\n accessToken: string,\n owner: string,\n repo: string,\n path: string,\n branch: string = 'main'\n): Promise<string | null> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n\n const { data } = await octokit.rest.repos.getContent({\n owner,\n repo,\n path,\n ref: branch,\n });\n\n // Octokit types are union types (file | dir | submodule), we need to check if it's a file\n if (Array.isArray(data) || !('content' in data)) {\n throw new Error('Path points to a directory, not a file');\n }\n\n // GitHub returns content in base64, we must decode it to read the actual code\n const decodedContent = Buffer.from(data.content, 'base64').toString(\n 'utf-8'\n );\n\n return decodedContent;\n } catch (error: any) {\n if (error.status === 404) return null;\n\n logger.error('Error fetching repository file contents:', error);\n throw error;\n }\n};\n\n// ... [Keep getGitHubTokenFromUser unchanged] ...\nexport const getGitHubTokenFromUser = async (\n userId: string\n): Promise<string | null> => {\n try {\n const client = getDBClient();\n const db = client.db();\n\n let account = await db.collection('account').findOne({\n userId: userId,\n providerId: 'github',\n });\n\n if (!account && ObjectId.isValid(userId)) {\n account = await db.collection('account').findOne({\n userId: new ObjectId(userId),\n providerId: 'github',\n });\n }\n\n if (!account) {\n account = await db.collection('accounts').findOne({\n userId: userId,\n providerId: 'github',\n });\n }\n\n if (!account && ObjectId.isValid(userId)) {\n account = await db.collection('accounts').findOne({\n userId: new ObjectId(userId),\n providerId: 'github',\n });\n }\n\n if (!account) {\n return null;\n }\n\n const accessToken = account.accessToken || account.access_token;\n\n return accessToken || null;\n } catch (error) {\n logger.error('Error retrieving GitHub token from DB:', error);\n return null;\n }\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"github.service.mjs","names":["error: any","sha: string | undefined"],"sources":["../../../src/services/github.service.ts"],"sourcesContent":["import { configurationFilesCandidates } from '@intlayer/config';\nimport { logger } from '@logger';\nimport type { RestEndpointMethodTypes } from '@octokit/rest';\nimport { Octokit } from '@octokit/rest';\nimport { getDBClient } from '@utils/mongoDB/connectDB';\nimport { ObjectId } from 'mongodb';\nimport type { Project } from '@/types/project.types';\n\nexport type GitHubRepository =\n RestEndpointMethodTypes['repos']['listForAuthenticatedUser']['response']['data'][0];\nexport type GitHubFileContent =\n RestEndpointMethodTypes['repos']['getContent']['response']['data'];\n\nexport const getAuthorizationUrl = (\n redirectUri: string,\n login?: string\n): string => {\n const clientId = process.env.GITHUB_CLIENT_ID;\n\n if (!clientId) {\n throw new Error('GitHub Client ID is not configured');\n }\n\n const params = new URLSearchParams({\n client_id: clientId,\n scope: 'repo',\n state: 'github_oauth',\n redirect_uri: redirectUri,\n });\n\n if (login) {\n params.append('login', login);\n }\n\n return `https://github.com/login/oauth/authorize?${params.toString()}`;\n};\n\nexport const exchangeCodeForToken = async (code: string): Promise<string> => {\n const clientId = process.env.GITHUB_CLIENT_ID;\n const clientSecret = process.env.GITHUB_CLIENT_SECRET;\n\n if (!clientId || !clientSecret) {\n throw new Error('GitHub OAuth credentials are not configured');\n }\n\n try {\n const response = await fetch(\n 'https://github.com/login/oauth/access_token',\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify({\n client_id: clientId,\n client_secret: clientSecret,\n code,\n }),\n }\n );\n\n if (!response.ok) {\n throw new Error(`GitHub token exchange failed: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n if (data.error) {\n throw new Error(`GitHub token error: ${data.error_description}`);\n }\n\n return data.access_token;\n } catch (error) {\n logger.error('Error exchanging GitHub code for token:', error);\n throw error;\n }\n};\n\nexport const getUserRepos = async (\n accessToken: string\n): Promise<GitHubRepository[]> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n\n const { data } = await octokit.rest.repos.listForAuthenticatedUser({\n sort: 'updated',\n per_page: 100,\n visibility: 'all',\n });\n\n return data;\n } catch (error) {\n logger.error('Error fetching GitHub repositories:', error);\n throw error;\n }\n};\n\n/**\n * Check if valid intlayer configuration files exist in a repository (Recursively).\n * Returns an array of file paths found (e.g. ['intlayer.config.ts', 'apps/web/intlayer.config.js']).\n */\nexport const checkIntlayerConfig = async (\n accessToken: string,\n owner: string,\n repo: string,\n branch: string = 'main'\n): Promise<string[]> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n\n // Use Git Tree API to get all files recursively\n // This allows finding configs in monorepos/subfolders\n const { data } = await octokit.rest.git.getTree({\n owner,\n repo,\n tree_sha: branch,\n recursive: 'true',\n });\n\n if (!data.tree || !Array.isArray(data.tree)) {\n return [];\n }\n\n // Filter files that match the configuration candidates\n // We check if the path ends with one of the candidate filenames\n const foundFiles = data.tree\n .filter((item) => {\n if (item.type !== 'blob' || !item.path) return false;\n return (configurationFilesCandidates as readonly string[]).some(\n (candidate) => item.path?.endsWith(candidate)\n );\n })\n .map((item) => item.path as string); // Return the full path (e.g., 'packages/app/intlayer.config.ts')\n\n return foundFiles;\n } catch (error: any) {\n // If branch doesn't exist or repo is empty\n if (error.status === 404 || error.status === 409) return [];\n\n logger.error('Error checking intlayer configuration:', error);\n return [];\n }\n};\n\n/**\n * Get repository file contents and decode it\n */\nexport const getRepositoryFileContents = async (\n accessToken: string,\n owner: string,\n repo: string,\n path: string,\n branch: string = 'main'\n): Promise<string | null> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n\n const { data } = await octokit.rest.repos.getContent({\n owner,\n repo,\n path,\n ref: branch,\n });\n\n // Octokit types are union types (file | dir | submodule), we need to check if it's a file\n if (Array.isArray(data) || !('content' in data)) {\n throw new Error('Path points to a directory, not a file');\n }\n\n // GitHub returns content in base64, we must decode it to read the actual code\n const decodedContent = Buffer.from(data.content, 'base64').toString(\n 'utf-8'\n );\n\n return decodedContent;\n } catch (error: any) {\n if (error.status === 404) return null;\n\n logger.error('Error fetching repository file contents:', error);\n throw error;\n }\n};\n\nexport const getGitHubTokenFromUser = async (\n userId: string\n): Promise<string | null> => {\n try {\n const client = getDBClient();\n const db = client.db();\n\n let account = await db.collection('account').findOne({\n userId: userId,\n providerId: 'github',\n });\n\n if (!account && ObjectId.isValid(userId)) {\n account = await db.collection('account').findOne({\n userId: new ObjectId(userId),\n providerId: 'github',\n });\n }\n\n if (!account) {\n account = await db.collection('accounts').findOne({\n userId: userId,\n providerId: 'github',\n });\n }\n\n if (!account && ObjectId.isValid(userId)) {\n account = await db.collection('accounts').findOne({\n userId: new ObjectId(userId),\n providerId: 'github',\n });\n }\n\n if (!account) {\n return null;\n }\n\n const accessToken = account.accessToken || account.access_token;\n\n return accessToken || null;\n } catch (error) {\n logger.error('Error retrieving GitHub token from DB:', error);\n return null;\n }\n};\n\ntype DispatchEventOptions = {\n project: Project;\n eventType?: string;\n payload?: Record<string, any>;\n};\n\nexport const triggerGithubDispatch = async ({\n project,\n eventType = 'intlayer_cms_update',\n payload = {},\n}: DispatchEventOptions) => {\n const { repository, oAuth2Access } = project;\n\n if (!repository || repository.provider !== 'github') {\n throw new Error('Project is not connected to a GitHub repository.');\n }\n\n // Get the valid Access Token\n // Assuming the first token is the active one, or implement logic to find the specific user's token\n const tokenData = oAuth2Access?.[0];\n const accessToken = tokenData?.accessToken?.[0]; // Assuming array of tokens\n\n if (!accessToken) {\n throw new Error('No valid OAuth2 access token found for GitHub.');\n }\n\n const { owner, repository: repoName } = repository;\n const url = `https://api.github.com/repos/${owner}/${repoName}/dispatches`;\n\n try {\n // 2. Send the Dispatch Event\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/vnd.github+json',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n event_type: eventType,\n client_payload: {\n ...payload,\n projectId: project.id,\n timestamp: Date.now(),\n },\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`GitHub API Error: ${response.status} - ${errorText}`);\n }\n\n logger.info(\n `Successfully triggered GitHub Action '${eventType}' for ${owner}/${repoName}`\n );\n return true;\n } catch (error) {\n logger.error(error);\n throw error;\n }\n};\n\n/**\n * Check if a GitHub workflow file exists\n */\nexport const checkWorkflowFileExists = async (\n accessToken: string,\n owner: string,\n repo: string,\n filename: string,\n branch: string = 'main'\n): Promise<boolean> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n await octokit.rest.repos.getContent({\n owner,\n repo,\n path: filename,\n ref: branch,\n });\n return true;\n } catch (error: any) {\n if (error.status === 404) return false;\n logger.error('Error checking workflow file existence:', error);\n throw error;\n }\n};\n\n/**\n * Create or update a GitHub workflow file\n */\nexport const createWorkflowFile = async (\n accessToken: string,\n owner: string,\n repo: string,\n filename: string,\n content: string,\n branch: string = 'main',\n message: string = 'Add Intlayer CI workflow'\n): Promise<void> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n\n // Check if file exists to get SHA for update\n let sha: string | undefined;\n try {\n const { data } = await octokit.rest.repos.getContent({\n owner,\n repo,\n path: filename,\n ref: branch,\n });\n\n if (Array.isArray(data) || !('sha' in data)) {\n throw new Error('Path points to a directory, not a file');\n }\n\n sha = data.sha;\n } catch (error: any) {\n if (error.status !== 404) {\n throw error;\n }\n // File doesn't exist, will create new one\n }\n\n // Encode content to base64\n const encodedContent = Buffer.from(content, 'utf-8').toString('base64');\n\n await octokit.rest.repos.createOrUpdateFileContents({\n owner,\n repo,\n path: filename,\n message,\n content: encodedContent,\n branch,\n ...(sha && { sha }), // Include SHA if updating existing file\n });\n\n logger.info(\n `Successfully ${sha ? 'updated' : 'created'} workflow file ${filename} for ${owner}/${repo}`\n );\n } catch (error) {\n logger.error('Error creating/updating workflow file:', error);\n throw error;\n }\n};\n"],"mappings":"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"}
|