Skip to content

Commit df11574

Browse files
committed
refactor jenkins client
1 parent a8ce19a commit df11574

File tree

18 files changed

+2743
-256
lines changed

18 files changed

+2743
-256
lines changed

src/api/ci/gitlabciClient.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -146,23 +146,24 @@ export class GitLabCIClient {
146146
* @param jobId The job ID for which to retrieve logs
147147
* @returns A promise that resolves to the job logs as a string
148148
*/
149-
// public async getPipelineLogs(projectPath: string, jobId: number): Promise<string> {
150-
// try {
151-
// // Access the raw REST API client to make a direct request for job logs
152-
// const gitlab = this.gitlabClient.getClient();
149+
//TODO: need to confirm if this is correct
150+
public async getPipelineLogs(projectPath: string, jobId: number): Promise<string> {
151+
try {
152+
// Access the raw REST API client to make a direct request for job logs
153+
const gitlab = this.gitlabClient.getClient();
153154

154-
// // GitLab API endpoint for job traces is GET /projects/:id/jobs/:job_id/trace
155-
// const encodedProjectPath = encodeURIComponent(projectPath);
156-
// const url = `projects/${encodedProjectPath}/jobs/${jobId}/trace`;
155+
// GitLab API endpoint for job traces is GET /projects/:id/jobs/:job_id/trace
156+
const encodedProjectPath = encodeURIComponent(projectPath);
157+
const url = `projects/${encodedProjectPath}/jobs/${jobId}/trace`;
157158

158-
// // Make the request using the underlying requester
159-
// const jobTrace = await gitlab.request.get(url);
160-
// return jobTrace as string;
161-
// } catch (error) {
162-
// console.error(`Failed to get logs for job ${jobId} in project ${projectPath}:`, error);
163-
// return 'Failed to retrieve job logs';
164-
// }
165-
// }
159+
// Make the request using the underlying requester
160+
const jobTrace = await gitlab.requester.get(url);
161+
return jobTrace as unknown as string;
162+
} catch (error) {
163+
console.error(`Failed to get logs for job ${jobId} in project ${projectPath}:`, error);
164+
return 'Failed to retrieve job logs';
165+
}
166+
}
166167

167168
/**
168169
* Maps GitLab pipeline status to our standardized PipelineStatus enum

src/api/ci/jenkins/README.md

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
# Jenkins Client - Refactored Architecture
2+
3+
This directory contains the refactored Jenkins client implementation following TypeScript best practices and design patterns.
4+
5+
## Architecture Overview
6+
7+
The Jenkins client has been refactored using a service-oriented architecture with the following benefits:
8+
9+
- **Single Responsibility Principle**: Each class has one clear purpose
10+
- **Type Safety**: Comprehensive TypeScript types throughout
11+
- **Testability**: Smaller, focused classes are easier to unit test
12+
- **Maintainability**: Changes to specific functionality affect fewer files
13+
- **Reusability**: Utility classes can be reused across the codebase
14+
- **Error Handling**: Consistent, typed error handling
15+
- **Configuration**: Centralized configuration management
16+
- **Extensibility**: Easy to add new credential types or job configurations
17+
18+
## Directory Structure
19+
20+
```
21+
jenkins/
22+
├── enums/ # Enums for Jenkins constants
23+
│ └── jenkins.enums.ts
24+
├── types/ # TypeScript type definitions
25+
│ └── jenkins.types.ts
26+
├── config/ # Configuration constants
27+
│ └── jenkins.config.ts
28+
├── errors/ # Custom error classes
29+
│ └── jenkins.errors.ts
30+
├── utils/ # Utility classes
31+
│ └── jenkins.utils.ts
32+
├── strategies/ # Strategy pattern implementations
33+
│ └── credential.strategy.ts
34+
├── http/ # HTTP client abstraction
35+
│ └── jenkins-http.client.ts
36+
├── services/ # Business logic services
37+
│ ├── jenkins-job.service.ts
38+
│ ├── jenkins-build.service.ts
39+
│ └── jenkins-credential.service.ts
40+
├── jenkins.client.ts # Main client facade
41+
├── index.ts # Module exports
42+
└── README.md # This file
43+
```
44+
45+
## Usage Examples
46+
47+
### Basic Usage (Backwards Compatible)
48+
49+
```typescript
50+
import { JenkinsClient } from './jenkins';
51+
52+
const client = new JenkinsClient({
53+
baseUrl: 'https://jenkins.example.com',
54+
username: 'your-username',
55+
token: 'your-api-token'
56+
});
57+
58+
// Create a job (legacy method signature)
59+
await client.createJob(
60+
'my-job',
61+
'https://github.com/user/repo.git',
62+
'my-folder',
63+
'main',
64+
'Jenkinsfile',
65+
'git-credentials'
66+
);
67+
68+
// Trigger a build
69+
await client.build('my-job', 'my-folder', { PARAM1: 'value1' });
70+
71+
// Get build information
72+
const build = await client.getBuild('my-job', 123, 'my-folder');
73+
```
74+
75+
### New Options-Based Usage
76+
77+
```typescript
78+
import { JenkinsClient, CreateJobOptions, BuildOptions } from './jenkins';
79+
80+
const client = new JenkinsClient({
81+
baseUrl: 'https://jenkins.example.com',
82+
username: 'your-username',
83+
token: 'your-api-token'
84+
});
85+
86+
// Create a job (new options signature)
87+
const jobOptions: CreateJobOptions = {
88+
jobName: 'my-job',
89+
repoUrl: 'https://github.com/user/repo.git',
90+
folderName: 'my-folder',
91+
branch: 'main',
92+
jenkinsfilePath: 'Jenkinsfile',
93+
credentialId: 'git-credentials'
94+
};
95+
await client.createJob(jobOptions);
96+
97+
// Trigger a build with options
98+
const buildOptions: BuildOptions = {
99+
jobName: 'my-job',
100+
folderName: 'my-folder',
101+
parameters: { PARAM1: 'value1' }
102+
};
103+
await client.build(buildOptions);
104+
```
105+
106+
### Direct Service Access
107+
108+
```typescript
109+
import { JenkinsClient } from './jenkins';
110+
111+
const client = new JenkinsClient(config);
112+
113+
// Access individual services for advanced operations
114+
const jobs = client.jobs;
115+
const builds = client.builds;
116+
const credentials = client.credentials;
117+
118+
// Use services directly
119+
const runningBuilds = await builds.getRunningBuilds('my-job', 'my-folder');
120+
const jobExists = await jobs.jobExists('my-job', 'my-folder');
121+
await credentials.createSecretTextCredential('my-folder', 'my-secret', 'secret-value');
122+
```
123+
124+
### Error Handling
125+
126+
```typescript
127+
import {
128+
JenkinsClient,
129+
JenkinsJobNotFoundError,
130+
JenkinsBuildTimeoutError,
131+
JenkinsAuthenticationError
132+
} from './jenkins';
133+
134+
try {
135+
const build = await client.getBuild('non-existent-job', 123);
136+
} catch (error) {
137+
if (error instanceof JenkinsJobNotFoundError) {
138+
console.log('Job not found:', error.message);
139+
} else if (error instanceof JenkinsAuthenticationError) {
140+
console.log('Authentication failed:', error.message);
141+
} else {
142+
console.log('Unexpected error:', error);
143+
}
144+
}
145+
```
146+
147+
## Design Patterns Used
148+
149+
### 1. Facade Pattern
150+
- `JenkinsClient` acts as a facade providing a simple interface to the complex subsystem
151+
152+
### 2. Strategy Pattern
153+
- `CredentialStrategy` and implementations for different credential types
154+
- Easy to add new credential types without modifying existing code
155+
156+
### 3. Service Layer Pattern
157+
- Business logic separated into focused service classes
158+
- Each service handles one domain (jobs, builds, credentials)
159+
160+
### 4. Builder Pattern
161+
- `JenkinsXmlBuilder` for constructing XML configurations
162+
- `JenkinsPathBuilder` for constructing API paths
163+
164+
### 5. Factory Pattern
165+
- `CredentialStrategyFactory` for creating credential strategies
166+
167+
### 6. Error Handling Pattern
168+
- Custom error hierarchy with specific error types
169+
- Consistent error handling across all services
170+
171+
## Configuration
172+
173+
All configuration constants are centralized in `JenkinsConfig`:
174+
175+
```typescript
176+
import { JenkinsConfig } from './jenkins';
177+
178+
// Access default values
179+
const timeout = JenkinsConfig.DEFAULT_TIMEOUT_MS;
180+
const headers = JenkinsConfig.HEADERS.JSON;
181+
const endpoint = JenkinsConfig.ENDPOINTS.API_JSON;
182+
```
183+
184+
## Extending the Client
185+
186+
### Adding New Credential Types
187+
188+
1. Add the new type to `CredentialType` enum
189+
2. Create a new strategy class implementing `CredentialStrategy`
190+
3. Register it in `CredentialStrategyFactory`
191+
192+
### Adding New Services
193+
194+
1. Create a new service class in `services/`
195+
2. Add it to the main `JenkinsClient` constructor
196+
3. Expose it through the facade if needed
197+
198+
### Adding New Error Types
199+
200+
1. Create new error classes extending `JenkinsError`
201+
2. Export them from `errors/jenkins.errors.ts`
202+
3. Use them in appropriate services
203+
204+
## Testing
205+
206+
The refactored architecture makes testing much easier:
207+
208+
```typescript
209+
// Mock individual services
210+
const mockJobService = {
211+
createJob: jest.fn(),
212+
getJob: jest.fn(),
213+
};
214+
215+
// Test services in isolation
216+
const jobService = new JenkinsJobService(mockHttpClient);
217+
```
218+
219+
## Performance Considerations
220+
221+
- Services are lightweight and share the same HTTP client instance
222+
- Path building and XML generation are optimized
223+
- Error handling is consistent and efficient
224+
- Configuration is loaded once and reused
225+
226+
## Security
227+
228+
- Credentials are handled through the strategy pattern
229+
- Sensitive data is not logged
230+
- XML escaping prevents injection attacks
231+
- Type-safe parameter handling
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Jenkins configuration constants and default values
3+
*/
4+
export class JenkinsConfig {
5+
public static readonly DEFAULT_BRANCH = 'main';
6+
public static readonly DEFAULT_JENKINSFILE_PATH = 'Jenkinsfile';
7+
public static readonly DEFAULT_CREDENTIAL_ID = 'GITOPS_AUTH_PASSWORD';
8+
public static readonly DEFAULT_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
9+
public static readonly DEFAULT_POLL_INTERVAL_MS = 5000; // 5 seconds
10+
public static readonly DEFAULT_MAX_BUILDS_TO_CHECK = 50;
11+
12+
/**
13+
* HTTP headers for different content types
14+
*/
15+
public static readonly HEADERS = {
16+
XML: { 'Content-Type': 'application/xml' },
17+
JSON: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
18+
PLAIN: { 'Accept': 'text/plain' },
19+
} as const;
20+
21+
/**
22+
* Jenkins plugin information
23+
*/
24+
public static readonly PLUGINS = {
25+
WORKFLOW_JOB: 'workflow-job@2.40',
26+
GITHUB: 'github@1.37.1',
27+
WORKFLOW_CPS: 'workflow-cps@2.89',
28+
GIT: 'git@4.4.5',
29+
PLAIN_CREDENTIALS: 'plain-credentials',
30+
} as const;
31+
32+
/**
33+
* Jenkins API endpoints
34+
*/
35+
public static readonly ENDPOINTS = {
36+
CREATE_ITEM: 'createItem',
37+
API_JSON: 'api/json',
38+
BUILD: 'build',
39+
BUILD_WITH_PARAMETERS: 'buildWithParameters',
40+
LOG_TEXT: 'logText/progressiveText',
41+
CREDENTIALS_STORE_SYSTEM: 'credentials/store/system/domain/_/createCredentials',
42+
CREDENTIALS_STORE_FOLDER: 'credentials/store/folder/domain/_/createCredentials',
43+
} as const;
44+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Jenkins build result status enum
3+
*/
4+
export enum JenkinsBuildResult {
5+
SUCCESS = 'SUCCESS',
6+
FAILURE = 'FAILURE',
7+
UNSTABLE = 'UNSTABLE',
8+
ABORTED = 'ABORTED',
9+
NOT_BUILT = 'NOT_BUILT',
10+
}
11+
12+
/**
13+
* Jenkins build trigger type enum
14+
*/
15+
export enum JenkinsBuildTrigger {
16+
UNKNOWN = 'UNKNOWN',
17+
PULL_REQUEST = 'PULL_REQUEST',
18+
PUSH = 'PUSH',
19+
MANUAL = 'MANUAL',
20+
SCHEDULED = 'SCHEDULED',
21+
API = 'API',
22+
}
23+
24+
/**
25+
* Jenkins credential types
26+
*/
27+
export enum CredentialType {
28+
SECRET_TEXT = 'Secret text',
29+
USERNAME_PASSWORD = 'Username with password',
30+
SSH_USERNAME_PRIVATE_KEY = 'SSH Username with private key',//TODO: need to confirm if this is correct
31+
}

0 commit comments

Comments
 (0)