Skip to content

Commit 8e3caa1

Browse files
Update GitLab webhooks
1 parent 23ba44b commit 8e3caa1

13 files changed

Lines changed: 384 additions & 130 deletions

File tree

‎package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@
9090
"@fastify/type-provider-typebox": "^4.0.0",
9191
"@firebase/rules-unit-testing": "^4.0.1",
9292
"@ghostery/adblocker-puppeteer": "2.11.3",
93-
"@gitbeaker/core": "43.4.0",
94-
"@gitbeaker/rest": "43.4.0",
93+
"@gitbeaker/core": "43.5.0",
94+
"@gitbeaker/rest": "43.5.0",
9595
"@google-cloud/aiplatform": "^5.7.0",
9696
"@google-cloud/bigquery": "^8.1.1",
9797
"@google-cloud/discoveryengine": "^2.4.0",

‎pnpm-lock.yaml‎

Lines changed: 20 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/agent/agentContextService/agentContextService.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export const testUser: User = {
5959
name: 'John Doe',
6060
email: 'test@example.com',
6161
enabled: true,
62+
admin: false,
6263
createdAt: new Date(Date.now() - 86400000), // Yesterday
6364
lastLoginAt: new Date(),
6465
hilBudget: 1.5,
@@ -73,6 +74,7 @@ export const otherUser: User = {
7374
name: 'John Doe',
7475
email: 'other@example.com',
7576
enabled: true,
77+
admin: false,
7678
createdAt: new Date(Date.now() - 172800000), // Day before yesterday
7779
lastLoginAt: new Date(Date.now() - 3600000), // Hour ago
7880
hilBudget: 0.5,

‎src/agent/autonomous/autonomousAgentRunner.ts‎

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { appContext } from '#app/applicationContext';
99
import { FUNC_SEP } from '#functionSchema/functions';
1010
import { Git } from '#functions/scm/git';
1111
import { GitHub } from '#functions/scm/github';
12+
import { GitLab } from '#functions/scm/gitlab';
1213
import { logger } from '#o11y/logger';
1314
import type { AgentContext } from '#shared/agent/agent.model';
1415
import type { FunctionCallResult } from '#shared/llm/llm.model';
@@ -53,15 +54,7 @@ async function _startAgent(agent: AgentContext): Promise<AgentExecution> {
5354

5455
await checkRepoHomeAndWorkingDirectory(agent);
5556

56-
const metadata = agent.metadata ?? {};
57-
const githubProject = metadata.github?.repository;
58-
if (githubProject) {
59-
runAsUser(agent.user, async () => {
60-
const repoPath = await new GitHub().cloneProject(githubProject);
61-
agent.fileSystem!.setWorkingDirectory(repoPath);
62-
if (metadata.github.branch) await new Git().switchToBranch(metadata.github.branch);
63-
});
64-
}
57+
await initialiseMetadataRepository(agent);
6558

6659
switch (agent.subtype) {
6760
case 'xml':
@@ -81,6 +74,48 @@ async function _startAgent(agent: AgentContext): Promise<AgentExecution> {
8174
return execution;
8275
}
8376

77+
async function initialiseMetadataRepository(agent: AgentContext) {
78+
const metadata = agent.metadata ?? {};
79+
let hasRepo = false;
80+
let branch: string | undefined;
81+
82+
let gitProject = metadata.github?.repository;
83+
if (gitProject) {
84+
hasRepo = true;
85+
await runAsUser(agent.user, async () => {
86+
branch = metadata.github.branch;
87+
const repoPath = await new GitHub().cloneProject(gitProject, branch);
88+
agent.fileSystem!.setWorkingDirectory(repoPath);
89+
});
90+
}
91+
gitProject = metadata.gitlab?.projectPath;
92+
if (gitProject) {
93+
hasRepo = true;
94+
await runAsUser(agent.user, async () => {
95+
branch = metadata.gitlab.branch;
96+
const repoPath = await new GitLab().cloneProject(gitProject, branch);
97+
agent.fileSystem!.setWorkingDirectory(repoPath);
98+
});
99+
}
100+
// If an agent has switched a shared repo from the main/master branch, then switch it back
101+
if (agent.useSharedRepos && hasRepo && !branch) {
102+
const git = new Git();
103+
const currentBranch = await git.getBranchName();
104+
if (currentBranch !== 'main' && currentBranch !== 'master') {
105+
logger.warn(`Shared repo ${gitProject} is not on branch main or master, switching back`);
106+
try {
107+
await git.switchToBranch('main');
108+
} catch (e) {
109+
try {
110+
await git.switchToBranch('master');
111+
} catch (e) {
112+
logger.warn({ metadata }, 'Couldnt restore branch to `main` or `master`', e);
113+
}
114+
}
115+
}
116+
}
117+
}
118+
84119
export async function startAgentAndWaitForCompletion(config: RunAgentConfig): Promise<string> {
85120
const agentExecution = await startAgent(config);
86121

‎src/chat/chatService.test.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import type { User } from '#shared/user/user.model';
66
import { runAsUser } from '#user/userContext';
77

88
export const SINGLE_USER: User = {
9-
enabled: false,
9+
enabled: true,
10+
admin: false,
1011
hilBudget: 0,
1112
hilCount: 0,
1213
llmConfig: {},
@@ -27,6 +28,7 @@ export const USER_A: User = {
2728
name: 'User A',
2829
email: 'usera@example.com',
2930
enabled: true,
31+
admin: false,
3032
hilBudget: 0,
3133
hilCount: 0,
3234
llmConfig: {},
@@ -44,6 +46,7 @@ export const USER_B: User = {
4446
name: 'User B',
4547
email: 'userb@example.com',
4648
enabled: true,
49+
admin: false,
4750
hilBudget: 0,
4851
hilCount: 0,
4952
llmConfig: {},
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export function buildJobUrl(job: JobResult): string {
2+
if (job.host.includes('github.com')) return `https://${job.host}/${job.project}/actions/runs/${job.buildId}`;
3+
4+
return `https://${job.host}/${job.project}/-/jobs/${job.buildId}`;
5+
}
6+
7+
export interface JobResult {
8+
buildId: number;
9+
project: string;
10+
status: string;
11+
jobName: string;
12+
stage: string;
13+
startedAt: string;
14+
duration: number;
15+
pipeline: number;
16+
host: string;
17+
/** build_failure_reason field */
18+
failureReason?: string;
19+
/** Our classification of the failure type */
20+
failureType?: string;
21+
}
22+
23+
export interface CICDStatsService {
24+
saveJobResult(jobResult: JobResult): Promise<void>;
25+
26+
getRecentSuccessfulJobs(project: string, jobName: string): Promise<JobResult[]>;
27+
}

‎src/functions/scm/gitlab.ts‎

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ export class GitLab extends AbstractSCM implements SourceControlManagement {
286286
* @param mergeRequestIId The merge request IID. Can be found in the URL to a pipeline
287287
*/
288288
@func()
289-
async getLatestMergeRequestPipeline(gitlabProjectId: string | number, mergeRequestIId: number): Promise<PipelineWithJobs | null> {
289+
async getMergeRequestLatestPipeline(gitlabProjectId: string | number, mergeRequestIId: number): Promise<PipelineWithJobs | null> {
290290
// allPipelines<E extends boolean = false>(projectId: string | number, mergerequestIId: number, options?: Sudo & ShowExpanded<E>): Promise<GitlabAPIResponse<Pick<PipelineSchema, 'id' | 'sha' | 'ref' | 'status'>[], C, E, void>>;
291291
const pipelines = await this.api().MergeRequests.allPipelines(gitlabProjectId, mergeRequestIId);
292292
if (pipelines.length === 0) return null;
@@ -330,28 +330,23 @@ export class GitLab extends AbstractSCM implements SourceControlManagement {
330330
/**
331331
* Gets the logs from the jobs which have failed in a pipeline
332332
* @param gitlabProjectId GitLab project full path or the numeric id
333-
* @param mergeRequestIId The merge request IID. Can get this from the URL of the merge request. https://<gitlab-host>/<group>/[<sub-group>/]<project>/-/merge_requests/<mergeRequestIId>
333+
* @param pipelineId The pipelineId. Can be determined from the URL of a pipeline. https://<gitlab-host>/<group>/[<sub-group>/]<project>/-/pipelines/<pipelineId>
334334
* @returns A Record with the job name as the key and the logs as the value.
335335
*/
336336
@func()
337-
async getMergeRequestPipelineFailedJobLogs(gitlabProjectId: string | number, mergeRequestIId: number): Promise<string> {
338-
logger.info({ gitlabProjectId, mergeRequestIId }, 'Getting pipelines');
339-
const pipelines = await this.api().MergeRequests.allPipelines(gitlabProjectId, mergeRequestIId);
340-
if (pipelines.length === 0) throw new Error('No pipelines for the merge request');
337+
async getPipelineFailedJobLogs(gitlabProjectId: string | number, pipelineId: number): Promise<string> {
338+
logger.info({ gitlabProjectId, pipelineId }, 'Getting pipeline');
339+
const pipeline = await this.api().Pipelines.show(gitlabProjectId, pipelineId);
341340

342-
// pipelines.sort((a, b) => (Date.parse(a.created_at) < Date.parse(b.created_at) ? 1 : -1));
343-
const latestPipeline = pipelines.at(0);
344-
if (!latestPipeline) throw new Error('No pipelines for the merge request');
345-
346-
if (latestPipeline.status !== 'failed' && latestPipeline.status !== 'blocked') throw new Error('Pipeline is not failed or blocked');
341+
if (pipeline.status !== 'failed' && pipeline.status !== 'blocked') throw new Error(`Pipeline status is not failed or blocked. Status: ${pipeline.status}`);
347342

348-
logger.info({ gitlabProjectId, pipelineId: latestPipeline.id }, 'Getting jobs');
349-
const jobs: JobSchema[] = await this.api().Jobs.all(gitlabProjectId, { pipelineId: latestPipeline.id });
343+
logger.info({ gitlabProjectId, pipelineId: pipeline.id }, 'Getting jobs');
344+
const jobs: JobSchema[] = await this.api().Jobs.all(gitlabProjectId, { pipelineId: pipeline.id });
350345
const failedJobs = jobs.filter((job) => job.status === 'failed' && job.allow_failure === false);
351346

352347
let jobLogs = '';
353348
for (const job of failedJobs) {
354-
logger.info({ gitlabProjectId, pipelineId: latestPipeline.id, jobId: job.id }, 'Getting job logs');
349+
logger.info({ gitlabProjectId, pipelineId: pipeline.id, jobId: job.id }, 'Getting job logs');
355350
let logs = await this.getJobLogs(gitlabProjectId, job.id.toString());
356351

357352
// If the logs are longer than ~12,000 tokens, truncate them.
@@ -365,6 +360,25 @@ export class GitLab extends AbstractSCM implements SourceControlManagement {
365360
return jobLogs;
366361
}
367362

363+
/**
364+
* Gets the logs from the jobs which have failed in the latest pipeline of a merge request
365+
* @param gitlabProjectId GitLab project full path or the numeric id
366+
* @param mergeRequestIId The merge request IID. Can get this from the URL of the merge request. https://<gitlab-host>/<group>/[<sub-group>/]<project>/-/merge_requests/<mergeRequestIId>
367+
* @returns A Record with the job name as the key and the logs as the value.
368+
*/
369+
@func()
370+
async getMergeRequestPipelineFailedJobLogs(gitlabProjectId: string | number, mergeRequestIId: number): Promise<string> {
371+
logger.info({ gitlabProjectId, mergeRequestIId }, 'Getting pipelines');
372+
const pipelines = await this.api().MergeRequests.allPipelines(gitlabProjectId, mergeRequestIId);
373+
if (pipelines.length === 0) throw new Error('No pipelines for the merge request');
374+
375+
// pipelines.sort((a, b) => (Date.parse(a.created_at) < Date.parse(b.created_at) ? 1 : -1));
376+
const latestPipeline = pipelines.at(0);
377+
if (!latestPipeline) throw new Error('No pipelines for the merge request');
378+
379+
return this.getPipelineFailedJobLogs(gitlabProjectId, latestPipeline.id);
380+
}
381+
368382
/**
369383
* @returns the diffs for a merge request
370384
*/
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { randomUUID } from 'node:crypto';
2+
import type { Firestore } from '@google-cloud/firestore';
3+
import { CICDStatsService, JobResult } from '#functions/scm/cicdStatsService';
4+
import { logger } from '#o11y/logger';
5+
import { span } from '#o11y/trace';
6+
import { currentUser } from '#user/userContext';
7+
import { firestoreDb } from './firestore';
8+
9+
export class FirestoreCICDStatsService implements CICDStatsService {
10+
private db: Firestore;
11+
12+
constructor() {
13+
this.db = firestoreDb();
14+
}
15+
16+
@span()
17+
async saveJobResult(jobResult: JobResult): Promise<void> {
18+
const docRef = this.db.collection('CICDStats').doc(randomUUID());
19+
await docRef.set(jobResult);
20+
}
21+
22+
@span()
23+
async getRecentSuccessfulJobs(project: string, jobName: string): Promise<JobResult[]> {
24+
const query = this.db
25+
.collection('CICDStats')
26+
.where('project', '==', project)
27+
.where('jobName', '==', jobName)
28+
.where('status', '==', 'success')
29+
.orderBy('startedAt', 'desc')
30+
.limit(20);
31+
return query.get().then((querySnapshot) => {
32+
const jobResults: JobResult[] = [];
33+
querySnapshot.forEach((doc) => {
34+
jobResults.push(doc.data() as JobResult);
35+
});
36+
return jobResults;
37+
});
38+
}
39+
}

0 commit comments

Comments
 (0)