@@ -13,23 +13,26 @@ import { Repository } from 'typeorm';
1313@Injectable ( )
1414export class GitHubService {
1515 private readonly logger = new Logger ( GitHubService . name ) ;
16-
16+
1717 private readonly appId : string ;
1818 private privateKey : string ;
1919 private ignored = [ 'node_modules' , '.git' , '.gitignore' , '.env' ] ;
2020
2121 constructor (
22- private configService : ConfigService ,
22+ private configService : ConfigService ,
2323 @InjectRepository ( Project )
24- private projectsRepository : Repository < Project > , )
25- {
26-
24+ private projectsRepository : Repository < Project > ,
25+ ) {
2726 this . appId = this . configService . get < string > ( 'GITHUB_APP_ID' ) ;
28-
29- const privateKeyPath = this . configService . get < string > ( 'GITHUB_PRIVATE_KEY_PATH' ) ;
27+
28+ const privateKeyPath = this . configService . get < string > (
29+ 'GITHUB_PRIVATE_KEY_PATH' ,
30+ ) ;
3031
3132 if ( ! privateKeyPath ) {
32- throw new Error ( 'GITHUB_PRIVATE_KEY_PATH is not set in environment variables' ) ;
33+ throw new Error (
34+ 'GITHUB_PRIVATE_KEY_PATH is not set in environment variables' ,
35+ ) ;
3336 }
3437
3538 this . logger . log ( `Reading GitHub private key from: ${ privateKeyPath } ` ) ;
@@ -50,9 +53,9 @@ export class GitHubService {
5053 // 1) Create a JWT (valid for ~10 minutes)
5154 const now = Math . floor ( Date . now ( ) / 1000 ) ;
5255 const payload = {
53- iat : now , // Issued at time
54- exp : now + 600 , // JWT expiration (10 minute maximum)
55- iss : this . appId , // Your GitHub App's App ID
56+ iat : now , // Issued at time
57+ exp : now + 600 , // JWT expiration (10 minute maximum)
58+ iss : this . appId , // Your GitHub App's App ID
5659 } ;
5760
5861 const gitHubAppJwt = jwt . sign ( payload , this . privateKey , {
@@ -77,13 +80,16 @@ export class GitHubService {
7780 return token ;
7881 }
7982
80-
8183 async exchangeOAuthCodeForToken ( code : string ) : Promise < string > {
8284 const clientId = this . configService . get < string > ( 'GITHUB_CLIENT_ID' ) ;
8385 const clientSecret = this . configService . get < string > ( 'GITHUB_CLIENT_SECRET' ) ;
84-
85- console . log ( 'Exchanging OAuth Code:' , { code, clientId, clientSecretExists : ! ! clientSecret } ) ;
86-
86+
87+ console . log ( 'Exchanging OAuth Code:' , {
88+ code,
89+ clientId,
90+ clientSecretExists : ! ! clientSecret ,
91+ } ) ;
92+
8793 try {
8894 const response = await axios . post (
8995 'https://github.com/login/oauth/access_token' ,
@@ -98,30 +104,37 @@ export class GitHubService {
98104 } ,
99105 } ,
100106 ) ;
101-
107+
102108 console . log ( 'GitHub Token Exchange Response:' , response . data ) ;
103-
109+
104110 if ( response . data . error ) {
105111 console . error ( 'GitHub OAuth error:' , response . data ) ;
106- throw new BadRequestException ( `GitHub OAuth error: ${ response . data . error_description } ` ) ;
112+ throw new BadRequestException (
113+ `GitHub OAuth error: ${ response . data . error_description } ` ,
114+ ) ;
107115 }
108-
116+
109117 const accessToken = response . data . access_token ;
110118 if ( ! accessToken ) {
111- throw new Error ( 'GitHub token exchange failed: No access token returned.' ) ;
119+ throw new Error (
120+ 'GitHub token exchange failed: No access token returned.' ,
121+ ) ;
112122 }
113-
123+
114124 return accessToken ;
115125 } catch ( error : any ) {
116- console . error ( 'OAuth exchange failed:' , error . response ?. data || error . message ) ;
126+ console . error (
127+ 'OAuth exchange failed:' ,
128+ error . response ?. data || error . message ,
129+ ) ;
117130 // throw new Error(`GitHub OAuth exchange failed: ${error.response?.data?.error_description || error.message}`);
118131 }
119132 }
120-
133+
121134 /**
122135 * Create a new repository under the *user's* account.
123136 * If you need an org-level repo, use POST /orgs/{org}/repos.
124- */
137+ */
125138 async createUserRepo (
126139 repoName : string ,
127140 isPublic : boolean ,
@@ -151,12 +164,17 @@ export class GitHubService {
151164 const data = response . data ;
152165 return {
153166 owner : data . owner . login , // e.g. "octocat"
154- repo : data . name , // e.g. "my-new-repo"
155- htmlUrl : data . html_url , // e.g. "https://github.com/octocat/my-new-repo"
167+ repo : data . name , // e.g. "my-new-repo"
168+ htmlUrl : data . html_url , // e.g. "https://github.com/octocat/my-new-repo"
156169 } ;
157170 }
158171
159- async pushMultipleFiles ( installationToken : string , owner : string , repo : string , files : string [ ] ) {
172+ async pushMultipleFiles (
173+ installationToken : string ,
174+ owner : string ,
175+ repo : string ,
176+ files : string [ ] ,
177+ ) {
160178 for ( const file of files ) {
161179 const fileName = path . basename ( file ) ;
162180 await this . pushFileContent (
@@ -165,88 +183,98 @@ export class GitHubService {
165183 repo ,
166184 file ,
167185 `myFolder/${ fileName } ` ,
168- 'Initial commit of file ' + fileName
186+ 'Initial commit of file ' + fileName ,
169187 ) ;
170188 }
171189 }
172190
173191 /**
174192 * Push a single file to the given path in the repo using GitHub Contents API.
175- *
193+ *
176194 * @param relativePathInRepo e.g. "backend/index.js" or "frontend/package.json"
177195 */
178- async pushFileContent (
179- installationToken : string ,
180- owner : string ,
181- repo : string ,
182- localFilePath : string ,
183- relativePathInRepo : string ,
184- commitMessage : string ,
185- ) {
186- const fileBuffer = fs . readFileSync ( localFilePath ) ;
187- const base64Content = fileBuffer . toString ( 'base64' ) ;
188-
189- const url = `https://api.github.com/repos/${ owner } /${ repo } /contents/${ relativePathInRepo } ` ;
190-
191- await axios . put (
192- url ,
193- {
194- message : commitMessage ,
195- content : base64Content ,
196- } ,
197- {
198- headers : {
199- Authorization : `token ${ installationToken } ` ,
200- Accept : 'application/vnd.github.v3+json' ,
201- } ,
196+ async pushFileContent (
197+ installationToken : string ,
198+ owner : string ,
199+ repo : string ,
200+ localFilePath : string ,
201+ relativePathInRepo : string ,
202+ commitMessage : string ,
203+ ) {
204+ const fileBuffer = fs . readFileSync ( localFilePath ) ;
205+ const base64Content = fileBuffer . toString ( 'base64' ) ;
206+
207+ const url = `https://api.github.com/repos/${ owner } /${ repo } /contents/${ relativePathInRepo } ` ;
208+
209+ await axios . put (
210+ url ,
211+ {
212+ message : commitMessage ,
213+ content : base64Content ,
214+ } ,
215+ {
216+ headers : {
217+ Authorization : `token ${ installationToken } ` ,
218+ Accept : 'application/vnd.github.v3+json' ,
202219 } ,
203- ) ;
204-
205- this . logger . log ( `Pushed file: ${ relativePathInRepo } -> https://github.com/${ owner } /${ repo } ` ) ;
206- }
207-
208- /**
209- * Recursively push all files in a local folder to the repo.
210- * Skips .git, node_modules, etc. (configurable)
211- */
212- async pushFolderContent (
213- installationToken : string ,
214- owner : string ,
215- repo : string ,
216- folderPath : string ,
217- basePathInRepo : string , // e.g. "" or "backend"
218- ) {
219- const entries = fs . readdirSync ( folderPath , { withFileTypes : true } ) ;
220-
221- for ( const entry of entries ) {
222-
223- // Skip unwanted files
224- if ( this . ignored . includes ( entry . name ) ) {
225- continue ;
226- }
220+ } ,
221+ ) ;
222+
223+ this . logger . log (
224+ `Pushed file: ${ relativePathInRepo } -> https://github.com/${ owner } /${ repo } ` ,
225+ ) ;
226+ }
227227
228- const entryPath = path . join ( folderPath , entry . name ) ;
229- if ( entry . isDirectory ( ) ) {
230- // Skip unwanted directories
231- if ( entry . name === '.git' || entry . name === 'node_modules' ) {
232- continue ;
233- }
234- // Recurse into subdirectory
235- const subDirInRepo = path . join ( basePathInRepo , entry . name ) . replace ( / \\ / g, '/' ) ;
236- await this . pushFolderContent ( installationToken , owner , repo , entryPath , subDirInRepo ) ;
237- } else {
238- // It's a file; push it
239- const fileInRepo = path . join ( basePathInRepo , entry . name ) . replace ( / \\ / g, '/' ) ;
240- await this . pushFileContent (
241- installationToken ,
242- owner ,
243- repo ,
244- entryPath ,
245- fileInRepo ,
246- `Add file: ${ fileInRepo } ` ,
247- ) ;
228+ /**
229+ * Recursively push all files in a local folder to the repo.
230+ * Skips .git, node_modules, etc. (configurable)
231+ */
232+ async pushFolderContent (
233+ installationToken : string ,
234+ owner : string ,
235+ repo : string ,
236+ folderPath : string ,
237+ basePathInRepo : string , // e.g. "" or "backend"
238+ ) {
239+ const entries = fs . readdirSync ( folderPath , { withFileTypes : true } ) ;
240+
241+ for ( const entry of entries ) {
242+ // Skip unwanted files
243+ if ( this . ignored . includes ( entry . name ) ) {
244+ continue ;
245+ }
246+
247+ const entryPath = path . join ( folderPath , entry . name ) ;
248+ if ( entry . isDirectory ( ) ) {
249+ // Skip unwanted directories
250+ if ( entry . name === '.git' || entry . name === 'node_modules' ) {
251+ continue ;
248252 }
253+ // Recurse into subdirectory
254+ const subDirInRepo = path
255+ . join ( basePathInRepo , entry . name )
256+ . replace ( / \\ / g, '/' ) ;
257+ await this . pushFolderContent (
258+ installationToken ,
259+ owner ,
260+ repo ,
261+ entryPath ,
262+ subDirInRepo ,
263+ ) ;
264+ } else {
265+ // It's a file; push it
266+ const fileInRepo = path
267+ . join ( basePathInRepo , entry . name )
268+ . replace ( / \\ / g, '/' ) ;
269+ await this . pushFileContent (
270+ installationToken ,
271+ owner ,
272+ repo ,
273+ entryPath ,
274+ fileInRepo ,
275+ `Add file: ${ fileInRepo } ` ,
276+ ) ;
249277 }
250278 }
251-
279+ }
252280}
0 commit comments