WIP
This commit is contained in:
parent
f32c78a94b
commit
d6ddd8aa45
@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
|
@ -29,23 +29,27 @@ export const GEMINI_MODEL = process.env.GEMINI_MODEL || 'gemini-1.5-pro';
|
||||
|
||||
// Function configuration
|
||||
export const DEBUG = process.env.DEBUG === 'true';
|
||||
export const USE_LOCAL_REPO = process.env.USE_LOCAL_REPO === 'true';
|
||||
|
||||
// Validate required configuration
|
||||
export function validateConfig(): void {
|
||||
const missingVars: string[] = [];
|
||||
|
||||
if (!MAIN_REPO_URL) {
|
||||
missingVars.push('MAIN_REPO_URL');
|
||||
|
||||
// Only check for main repo URL and credentials if not using local repo
|
||||
if (!USE_LOCAL_REPO) {
|
||||
if (!MAIN_REPO_URL) {
|
||||
missingVars.push('MAIN_REPO_URL');
|
||||
}
|
||||
|
||||
if (!MAIN_REPO_TOKEN && (!MAIN_REPO_USERNAME || !MAIN_REPO_PASSWORD)) {
|
||||
missingVars.push('MAIN_REPO_TOKEN or MAIN_REPO_USERNAME/MAIN_REPO_PASSWORD');
|
||||
}
|
||||
}
|
||||
|
||||
if (!MAIN_REPO_TOKEN && (!MAIN_REPO_USERNAME || !MAIN_REPO_PASSWORD)) {
|
||||
missingVars.push('MAIN_REPO_TOKEN or MAIN_REPO_USERNAME/MAIN_REPO_PASSWORD');
|
||||
}
|
||||
|
||||
|
||||
if (!GOOGLE_CLOUD_PROJECT_ID) {
|
||||
missingVars.push('GOOGLE_CLOUD_PROJECT_ID');
|
||||
}
|
||||
|
||||
|
||||
if (missingVars.length > 0) {
|
||||
throw new Error(`Missing required environment variables: ${missingVars.join(', ')}`);
|
||||
}
|
||||
@ -53,6 +57,14 @@ export function validateConfig(): void {
|
||||
|
||||
// Get repository credentials for the main repository
|
||||
export function getMainRepoCredentials(): { type: 'username-password' | 'token'; username?: string; password?: string; token?: string } {
|
||||
if (USE_LOCAL_REPO) {
|
||||
// Return dummy credentials when using local repo
|
||||
return {
|
||||
type: 'token',
|
||||
token: 'dummy-token-for-local-repo'
|
||||
};
|
||||
}
|
||||
|
||||
if (MAIN_REPO_TOKEN) {
|
||||
return {
|
||||
type: 'token',
|
||||
@ -65,7 +77,7 @@ export function getMainRepoCredentials(): { type: 'username-password' | 'token';
|
||||
password: MAIN_REPO_PASSWORD
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
throw new Error('No credentials available for the main repository');
|
||||
}
|
||||
|
||||
@ -83,7 +95,7 @@ export function getGithubCredentials(): { type: 'username-password' | 'token'; u
|
||||
password: GITHUB_PASSWORD
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -96,6 +108,6 @@ export function getGiteaCredentials(): { type: 'username-password'; username: st
|
||||
password: GITEA_PASSWORD
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import { validateConfig } from './config';
|
||||
try {
|
||||
validateConfig();
|
||||
} catch (error) {
|
||||
console.error('Configuration error:', error.message);
|
||||
console.error('Configuration error:', error instanceof Error ? error.message : String(error));
|
||||
// Don't throw here to allow the function to start, but it will fail when executed
|
||||
}
|
||||
|
||||
|
@ -8,17 +8,17 @@ jest.mock('path');
|
||||
|
||||
describe('ProjectService', () => {
|
||||
let projectService: ProjectService;
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
projectService = new ProjectService();
|
||||
|
||||
|
||||
// Reset all mocks
|
||||
jest.resetAllMocks();
|
||||
|
||||
|
||||
// Mock path.join to return predictable paths
|
||||
(path.join as jest.Mock).mockImplementation((...args) => args.join('/'));
|
||||
});
|
||||
|
||||
|
||||
describe('findProjects', () => {
|
||||
it('should find all projects in the prompts directory', async () => {
|
||||
// Mock fs.readdirSync to return project directories
|
||||
@ -28,12 +28,12 @@ describe('ProjectService', () => {
|
||||
{ name: 'not-a-project', isDirectory: () => true },
|
||||
{ name: 'README.md', isDirectory: () => false }
|
||||
]);
|
||||
|
||||
|
||||
// Mock fs.existsSync to return true for INFO.md files
|
||||
(fs.existsSync as jest.Mock).mockImplementation((path: string) => {
|
||||
return path.endsWith('project1/INFO.md') || path.endsWith('project2/INFO.md');
|
||||
});
|
||||
|
||||
|
||||
// Mock readProjectInfo
|
||||
jest.spyOn(projectService, 'readProjectInfo').mockImplementation(async (projectPath, projectName) => {
|
||||
return {
|
||||
@ -44,9 +44,9 @@ describe('ProjectService', () => {
|
||||
jiraComponent: projectName
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
const projects = await projectService.findProjects('prompts');
|
||||
|
||||
|
||||
expect(projects).toHaveLength(2);
|
||||
expect(projects[0].name).toBe('project1');
|
||||
expect(projects[1].name).toBe('project2');
|
||||
@ -56,21 +56,21 @@ describe('ProjectService', () => {
|
||||
expect(fs.existsSync).toHaveBeenCalledWith('prompts/not-a-project/INFO.md');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('readProjectInfo', () => {
|
||||
it('should read project information from INFO.md', async () => {
|
||||
const infoContent = `# Project Name
|
||||
|
||||
|
||||
- [x] Repo host: https://github.com
|
||||
- [x] Repo url: https://github.com/org/project.git
|
||||
- [x] Jira component: project-component
|
||||
`;
|
||||
|
||||
|
||||
// Mock fs.readFileSync to return INFO.md content
|
||||
(fs.readFileSync as jest.Mock).mockReturnValueOnce(infoContent);
|
||||
|
||||
|
||||
const project = await projectService.readProjectInfo('path/to/project', 'project');
|
||||
|
||||
|
||||
expect(project).toEqual({
|
||||
name: 'project',
|
||||
path: 'path/to/project',
|
||||
@ -80,20 +80,42 @@ describe('ProjectService', () => {
|
||||
});
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith('path/to/project/INFO.md', 'utf-8');
|
||||
});
|
||||
|
||||
it('should handle project information that does not follow the expected format', async () => {
|
||||
const infoContent = `# Project Name
|
||||
|
||||
This is a project description.
|
||||
Some other content that doesn't match the expected format.
|
||||
`;
|
||||
|
||||
// Mock fs.readFileSync to return malformed INFO.md content
|
||||
(fs.readFileSync as jest.Mock).mockReturnValueOnce(infoContent);
|
||||
|
||||
const project = await projectService.readProjectInfo('path/to/project', 'project');
|
||||
|
||||
expect(project).toEqual({
|
||||
name: 'project',
|
||||
path: 'path/to/project',
|
||||
repoHost: undefined,
|
||||
repoUrl: undefined,
|
||||
jiraComponent: undefined
|
||||
});
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith('path/to/project/INFO.md', 'utf-8');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('findWorkitems', () => {
|
||||
it('should find all workitems in a project', async () => {
|
||||
// Mock fs.existsSync to return true for workitems directory
|
||||
(fs.existsSync as jest.Mock).mockReturnValueOnce(true);
|
||||
|
||||
|
||||
// Mock fs.readdirSync to return workitem files
|
||||
(fs.readdirSync as jest.Mock).mockReturnValueOnce([
|
||||
'workitem1.md',
|
||||
'workitem2.md',
|
||||
'not-a-workitem.txt'
|
||||
]);
|
||||
|
||||
|
||||
// Mock readWorkitemInfo
|
||||
jest.spyOn(projectService, 'readWorkitemInfo').mockImplementation(async (workitemPath, fileName) => {
|
||||
return {
|
||||
@ -106,32 +128,32 @@ describe('ProjectService', () => {
|
||||
isActive: true
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
const workitems = await projectService.findWorkitems('path/to/project');
|
||||
|
||||
|
||||
expect(workitems).toHaveLength(2);
|
||||
expect(workitems[0].name).toBe('workitem1');
|
||||
expect(workitems[1].name).toBe('workitem2');
|
||||
expect(fs.existsSync).toHaveBeenCalledWith('path/to/project/workitems');
|
||||
expect(fs.readdirSync).toHaveBeenCalledWith('path/to/project/workitems');
|
||||
});
|
||||
|
||||
|
||||
it('should return empty array if workitems directory does not exist', async () => {
|
||||
// Mock fs.existsSync to return false for workitems directory
|
||||
(fs.existsSync as jest.Mock).mockReturnValueOnce(false);
|
||||
|
||||
|
||||
const workitems = await projectService.findWorkitems('path/to/project');
|
||||
|
||||
|
||||
expect(workitems).toHaveLength(0);
|
||||
expect(fs.existsSync).toHaveBeenCalledWith('path/to/project/workitems');
|
||||
expect(fs.readdirSync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('readWorkitemInfo', () => {
|
||||
it('should read workitem information from markdown file', async () => {
|
||||
const workitemContent = `## Workitem Title
|
||||
|
||||
|
||||
This is a description of the workitem.
|
||||
It has multiple lines.
|
||||
|
||||
@ -139,12 +161,12 @@ It has multiple lines.
|
||||
- [ ] Implementation:
|
||||
- [x] Active
|
||||
`;
|
||||
|
||||
|
||||
// Mock fs.readFileSync to return workitem content
|
||||
(fs.readFileSync as jest.Mock).mockReturnValueOnce(workitemContent);
|
||||
|
||||
|
||||
const workitem = await projectService.readWorkitemInfo('path/to/workitem.md', 'workitem.md');
|
||||
|
||||
|
||||
expect(workitem).toEqual({
|
||||
name: 'workitem',
|
||||
path: 'path/to/workitem.md',
|
||||
@ -156,48 +178,48 @@ It has multiple lines.
|
||||
});
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith('path/to/workitem.md', 'utf-8');
|
||||
});
|
||||
|
||||
|
||||
it('should handle workitem without Active checkbox', async () => {
|
||||
const workitemContent = `## Workitem Title
|
||||
|
||||
|
||||
This is a description of the workitem.
|
||||
|
||||
- [ ] Jira: JIRA-123
|
||||
- [ ] Implementation:
|
||||
`;
|
||||
|
||||
|
||||
// Mock fs.readFileSync to return workitem content
|
||||
(fs.readFileSync as jest.Mock).mockReturnValueOnce(workitemContent);
|
||||
|
||||
|
||||
const workitem = await projectService.readWorkitemInfo('path/to/workitem.md', 'workitem.md');
|
||||
|
||||
|
||||
expect(workitem.isActive).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('readProjectGuidelines', () => {
|
||||
it('should read AI guidelines for a project', async () => {
|
||||
const guidelinesContent = '## Guidelines\n\nThese are the guidelines.';
|
||||
|
||||
|
||||
// Mock fs.existsSync to return true for AI.md
|
||||
(fs.existsSync as jest.Mock).mockReturnValueOnce(true);
|
||||
|
||||
|
||||
// Mock fs.readFileSync to return guidelines content
|
||||
(fs.readFileSync as jest.Mock).mockReturnValueOnce(guidelinesContent);
|
||||
|
||||
|
||||
const guidelines = await projectService.readProjectGuidelines('path/to/project');
|
||||
|
||||
|
||||
expect(guidelines).toBe(guidelinesContent);
|
||||
expect(fs.existsSync).toHaveBeenCalledWith('path/to/project/AI.md');
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith('path/to/project/AI.md', 'utf-8');
|
||||
});
|
||||
|
||||
|
||||
it('should return empty string if AI.md does not exist', async () => {
|
||||
// Mock fs.existsSync to return false for AI.md
|
||||
(fs.existsSync as jest.Mock).mockReturnValueOnce(false);
|
||||
|
||||
|
||||
const guidelines = await projectService.readProjectGuidelines('path/to/project');
|
||||
|
||||
|
||||
expect(guidelines).toBe('');
|
||||
expect(fs.existsSync).toHaveBeenCalledWith('path/to/project/AI.md');
|
||||
expect(fs.readFileSync).not.toHaveBeenCalled();
|
||||
|
@ -126,21 +126,16 @@ export class GeminiService {
|
||||
|
||||
const currentDate = new Date().toISOString();
|
||||
|
||||
// Send the AI.md file directly to Gemini without hardcoded instructions
|
||||
const prompt = `
|
||||
You are tasked with creating a Cucumber feature file based on a workitem description.
|
||||
|
||||
Project Guidelines:
|
||||
${guidelines}
|
||||
|
||||
Workitem:
|
||||
${workitemContent}
|
||||
|
||||
Create a Cucumber feature file that implements this workitem according to the guidelines.
|
||||
Include the following comment at the top of the file:
|
||||
Include the following comment at the top of the generated file:
|
||||
# Generated by prompts-to-test-spec on ${currentDate}
|
||||
# Source: ${workitemName}
|
||||
|
||||
The feature file should be complete and ready to use in a Cucumber test suite.
|
||||
`;
|
||||
|
||||
const result = await generativeModel.generateContent({
|
||||
@ -148,7 +143,86 @@ The feature file should be complete and ready to use in a Cucumber test suite.
|
||||
});
|
||||
|
||||
const response = await result.response;
|
||||
const generatedText = response.candidates[0].content.parts[0].text;
|
||||
const generatedText = response.candidates[0]?.content?.parts[0]?.text || '';
|
||||
|
||||
return generatedText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a pull request description using Gemini API
|
||||
* @param processedWorkitems List of processed workitems
|
||||
* @returns Generated pull request description
|
||||
*/
|
||||
async generatePullRequestDescription(
|
||||
processedWorkitems: { workitem: Workitem; success: boolean; error?: string }[]
|
||||
): Promise<string> {
|
||||
const generativeModel = this.vertexAI.getGenerativeModel({
|
||||
model: this.model,
|
||||
});
|
||||
|
||||
// Prepare workitem data for the prompt
|
||||
const added: string[] = [];
|
||||
const updated: string[] = [];
|
||||
const deleted: string[] = [];
|
||||
const failed: string[] = [];
|
||||
|
||||
for (const item of processedWorkitems) {
|
||||
const { workitem, success, error } = item;
|
||||
|
||||
if (!success) {
|
||||
failed.push(`- ${workitem.name}: ${error}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!workitem.isActive) {
|
||||
deleted.push(`- ${workitem.name}`);
|
||||
} else if (workitem.implementation) {
|
||||
updated.push(`- ${workitem.name}`);
|
||||
} else {
|
||||
added.push(`- ${workitem.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a structured summary of changes for Gemini
|
||||
let workitemSummary = '';
|
||||
|
||||
if (added.length > 0) {
|
||||
workitemSummary += 'Added workitems:\n' + added.join('\n') + '\n\n';
|
||||
}
|
||||
|
||||
if (updated.length > 0) {
|
||||
workitemSummary += 'Updated workitems:\n' + updated.join('\n') + '\n\n';
|
||||
}
|
||||
|
||||
if (deleted.length > 0) {
|
||||
workitemSummary += 'Deleted workitems:\n' + deleted.join('\n') + '\n\n';
|
||||
}
|
||||
|
||||
if (failed.length > 0) {
|
||||
workitemSummary += 'Failed workitems:\n' + failed.join('\n') + '\n\n';
|
||||
}
|
||||
|
||||
const prompt = `
|
||||
You are tasked with creating a pull request description for changes to test specifications.
|
||||
|
||||
The following is a summary of the changes made:
|
||||
${workitemSummary}
|
||||
|
||||
Create a clear, professional pull request description that:
|
||||
1. Explains that this PR was automatically generated by the prompts-to-test-spec function
|
||||
2. Summarizes the changes (added, updated, deleted, and failed workitems)
|
||||
3. Uses markdown formatting for better readability
|
||||
4. Keeps the description concise but informative
|
||||
|
||||
The pull request description should be ready to use without further editing.
|
||||
`;
|
||||
|
||||
const result = await generativeModel.generateContent({
|
||||
contents: [{ role: 'user', parts: [{ text: prompt }] }],
|
||||
});
|
||||
|
||||
const response = await result.response;
|
||||
const generatedText = response.candidates[0]?.content?.parts[0]?.text || '';
|
||||
|
||||
return generatedText;
|
||||
}
|
||||
|
@ -15,7 +15,8 @@ import {
|
||||
getGiteaCredentials,
|
||||
GOOGLE_CLOUD_PROJECT_ID,
|
||||
GOOGLE_CLOUD_LOCATION,
|
||||
GEMINI_MODEL
|
||||
GEMINI_MODEL,
|
||||
USE_LOCAL_REPO
|
||||
} from '../config';
|
||||
|
||||
export class ProcessorService {
|
||||
@ -42,9 +43,15 @@ export class ProcessorService {
|
||||
);
|
||||
this.pullRequestService = new PullRequestService();
|
||||
|
||||
// Get main repository URL and credentials
|
||||
this.mainRepoUrl = MAIN_REPO_URL;
|
||||
this.mainRepoCredentials = getMainRepoCredentials();
|
||||
// Get main repository URL and credentials only if not using local repo
|
||||
if (!USE_LOCAL_REPO) {
|
||||
this.mainRepoUrl = MAIN_REPO_URL;
|
||||
this.mainRepoCredentials = getMainRepoCredentials();
|
||||
} else {
|
||||
// Set dummy values when using local repo
|
||||
this.mainRepoUrl = '';
|
||||
this.mainRepoCredentials = getMainRepoCredentials();
|
||||
}
|
||||
|
||||
// Initialize other credentials
|
||||
this.githubCredentials = getGithubCredentials();
|
||||
@ -84,12 +91,19 @@ export class ProcessorService {
|
||||
const results: ProcessResult[] = [];
|
||||
|
||||
try {
|
||||
// Clone the main repository
|
||||
console.log(`Cloning main repository: ${this.mainRepoUrl}`);
|
||||
const mainRepoPath = await this.repositoryService.cloneMainRepository(
|
||||
this.mainRepoUrl,
|
||||
this.mainRepoCredentials
|
||||
);
|
||||
// Use local repository or clone the main repository
|
||||
let mainRepoPath: string;
|
||||
if (USE_LOCAL_REPO) {
|
||||
console.log('Using local repository path');
|
||||
mainRepoPath = path.resolve(__dirname, '../../..');
|
||||
console.log(`Resolved local repository path: ${mainRepoPath}`);
|
||||
} else {
|
||||
console.log(`Cloning main repository: ${this.mainRepoUrl}`);
|
||||
mainRepoPath = await this.repositoryService.cloneMainRepository(
|
||||
this.mainRepoUrl,
|
||||
this.mainRepoCredentials
|
||||
);
|
||||
}
|
||||
|
||||
// Find all projects in the prompts directory
|
||||
const promptsDir = path.join(mainRepoPath, 'src', 'prompts');
|
||||
@ -98,10 +112,23 @@ export class ProcessorService {
|
||||
|
||||
console.log(`Found ${projects.length} projects`);
|
||||
|
||||
// Log details of each project
|
||||
if (projects.length > 0) {
|
||||
console.log('Projects found:');
|
||||
projects.forEach((project, index) => {
|
||||
console.log(` ${index + 1}. ${project.name} (${project.path})`);
|
||||
});
|
||||
} else {
|
||||
console.log('No projects found. Check if the prompts directory exists and contains project folders.');
|
||||
}
|
||||
|
||||
// Process each project
|
||||
console.log('Starting to process projects...');
|
||||
for (const project of projects) {
|
||||
try {
|
||||
console.log(`Starting processing of project: ${project.name}`);
|
||||
const result = await this.processProject(project);
|
||||
console.log(`Finished processing project: ${project.name}`);
|
||||
results.push(result);
|
||||
} catch (error) {
|
||||
console.error(`Error processing project ${project.name}:`, error);
|
||||
@ -112,6 +139,7 @@ export class ProcessorService {
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log(`Finished processing all ${projects.length} projects`);
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
|
@ -13,25 +13,41 @@ export class ProjectService {
|
||||
*/
|
||||
async findProjects(promptsDir: string): Promise<Project[]> {
|
||||
const projects: Project[] = [];
|
||||
|
||||
|
||||
console.log(`ProjectService: Searching for projects in ${promptsDir}`);
|
||||
|
||||
// Check if prompts directory exists
|
||||
if (!fs.existsSync(promptsDir)) {
|
||||
console.log(`ProjectService: Directory does not exist: ${promptsDir}`);
|
||||
return projects;
|
||||
}
|
||||
|
||||
// Get all directories in the prompts directory
|
||||
const entries = fs.readdirSync(promptsDir, { withFileTypes: true });
|
||||
const projectDirs = entries.filter(entry => entry.isDirectory());
|
||||
|
||||
|
||||
console.log(`ProjectService: Found ${projectDirs.length} potential project directories`);
|
||||
|
||||
for (const dir of projectDirs) {
|
||||
const projectPath = path.join(promptsDir, dir.name);
|
||||
const infoPath = path.join(projectPath, 'INFO.md');
|
||||
|
||||
|
||||
console.log(`ProjectService: Checking directory: ${dir.name}`);
|
||||
|
||||
// Skip directories without INFO.md
|
||||
if (!fs.existsSync(infoPath)) {
|
||||
console.log(`ProjectService: Skipping ${dir.name} - no INFO.md file found`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
console.log(`ProjectService: Found INFO.md in ${dir.name}, reading project info`);
|
||||
|
||||
// Read project info
|
||||
const project = await this.readProjectInfo(projectPath, dir.name);
|
||||
projects.push(project);
|
||||
console.log(`ProjectService: Added project: ${project.name}`);
|
||||
}
|
||||
|
||||
|
||||
return projects;
|
||||
}
|
||||
|
||||
@ -43,20 +59,28 @@ export class ProjectService {
|
||||
*/
|
||||
async readProjectInfo(projectPath: string, projectName: string): Promise<Project> {
|
||||
const infoPath = path.join(projectPath, 'INFO.md');
|
||||
console.log(`ProjectService: Reading project info from ${infoPath}`);
|
||||
const infoContent = fs.readFileSync(infoPath, 'utf-8');
|
||||
|
||||
|
||||
// Parse INFO.md content
|
||||
const repoHostMatch = infoContent.match(/- \[[ x]\] Repo host: (.*)/);
|
||||
const repoUrlMatch = infoContent.match(/- \[[ x]\] Repo url: (.*)/);
|
||||
const jiraComponentMatch = infoContent.match(/- \[[ x]\] Jira component: (.*)/);
|
||||
|
||||
return {
|
||||
|
||||
const project = {
|
||||
name: projectName,
|
||||
path: projectPath,
|
||||
repoHost: repoHostMatch ? repoHostMatch[1].trim() : undefined,
|
||||
repoUrl: repoUrlMatch ? repoUrlMatch[1].trim() : undefined,
|
||||
jiraComponent: jiraComponentMatch ? jiraComponentMatch[1].trim() : undefined
|
||||
};
|
||||
|
||||
console.log(`ProjectService: Project info for ${projectName}:`);
|
||||
console.log(` - Repository host: ${project.repoHost || 'Not found'}`);
|
||||
console.log(` - Repository URL: ${project.repoUrl || 'Not found'}`);
|
||||
console.log(` - Jira component: ${project.jiraComponent || 'Not found'}`);
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -67,22 +91,22 @@ export class ProjectService {
|
||||
async findWorkitems(projectPath: string): Promise<Workitem[]> {
|
||||
const workitems: Workitem[] = [];
|
||||
const workitemsDir = path.join(projectPath, 'workitems');
|
||||
|
||||
|
||||
// Skip if workitems directory doesn't exist
|
||||
if (!fs.existsSync(workitemsDir)) {
|
||||
return workitems;
|
||||
}
|
||||
|
||||
|
||||
// Get all markdown files in the workitems directory
|
||||
const files = fs.readdirSync(workitemsDir)
|
||||
.filter(file => file.endsWith('.md'));
|
||||
|
||||
|
||||
for (const file of files) {
|
||||
const workitemPath = path.join(workitemsDir, file);
|
||||
const workitem = await this.readWorkitemInfo(workitemPath, file);
|
||||
workitems.push(workitem);
|
||||
}
|
||||
|
||||
|
||||
return workitems;
|
||||
}
|
||||
|
||||
@ -94,19 +118,19 @@ export class ProjectService {
|
||||
*/
|
||||
async readWorkitemInfo(workitemPath: string, fileName: string): Promise<Workitem> {
|
||||
const content = fs.readFileSync(workitemPath, 'utf-8');
|
||||
|
||||
|
||||
// Parse workitem content
|
||||
const titleMatch = content.match(/## (.*)/);
|
||||
const jiraMatch = content.match(/- \[[ x]\] Jira: (.*)/);
|
||||
const implementationMatch = content.match(/- \[[ x]\] Implementation: (.*)/);
|
||||
const activeMatch = content.match(/- \[([x ])\] Active/);
|
||||
|
||||
|
||||
// Extract description (everything between title and first metadata line)
|
||||
let description = '';
|
||||
const lines = content.split('\n');
|
||||
let titleIndex = -1;
|
||||
let metadataIndex = -1;
|
||||
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (titleIndex === -1 && lines[i].startsWith('## ')) {
|
||||
titleIndex = i;
|
||||
@ -114,15 +138,15 @@ export class ProjectService {
|
||||
metadataIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (titleIndex !== -1 && metadataIndex !== -1) {
|
||||
description = lines.slice(titleIndex + 1, metadataIndex).join('\n').trim();
|
||||
}
|
||||
|
||||
|
||||
// Determine if workitem is active
|
||||
// If the Active checkbox is missing, assume it's active
|
||||
const isActive = activeMatch ? activeMatch[1] === 'x' : true;
|
||||
|
||||
|
||||
return {
|
||||
name: fileName.replace('.md', ''),
|
||||
path: workitemPath,
|
||||
@ -141,11 +165,11 @@ export class ProjectService {
|
||||
*/
|
||||
async readProjectGuidelines(projectPath: string): Promise<string> {
|
||||
const aiPath = path.join(projectPath, 'AI.md');
|
||||
|
||||
|
||||
if (!fs.existsSync(aiPath)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
return fs.readFileSync(aiPath, 'utf-8');
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,14 @@
|
||||
import axios from 'axios';
|
||||
import * as path from 'path';
|
||||
import { Project, RepoCredentials, Workitem } from '../types';
|
||||
import { GeminiService } from './gemini-service';
|
||||
|
||||
export class PullRequestService {
|
||||
private geminiService: GeminiService;
|
||||
|
||||
constructor() {
|
||||
this.geminiService = new GeminiService();
|
||||
}
|
||||
/**
|
||||
* Create a pull request for changes in a repository
|
||||
* @param project Project information
|
||||
@ -26,7 +32,7 @@ export class PullRequestService {
|
||||
|
||||
// Generate PR title and description
|
||||
const title = `Update workitems: ${new Date().toISOString().split('T')[0]}`;
|
||||
const description = this.generatePullRequestDescription(processedWorkitems);
|
||||
const description = await this.generatePullRequestDescription(processedWorkitems);
|
||||
|
||||
// Determine the repository host type and create PR accordingly
|
||||
if (project.repoHost.includes('github.com')) {
|
||||
@ -61,11 +67,11 @@ export class PullRequestService {
|
||||
|
||||
// Create the pull request
|
||||
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/pulls`;
|
||||
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
};
|
||||
|
||||
|
||||
if (credentials.type === 'token' && credentials.token) {
|
||||
headers['Authorization'] = `token ${credentials.token}`;
|
||||
} else if (credentials.type === 'username-password' && credentials.username && credentials.password) {
|
||||
@ -112,12 +118,12 @@ export class PullRequestService {
|
||||
|
||||
// Create the pull request
|
||||
const apiUrl = `${project.repoHost}/api/v1/repos/${owner}/${repo}/pulls`;
|
||||
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
|
||||
if (credentials.type === 'token' && credentials.token) {
|
||||
headers['Authorization'] = `token ${credentials.token}`;
|
||||
} else if (credentials.type === 'username-password' && credentials.username && credentials.password) {
|
||||
@ -142,53 +148,14 @@ export class PullRequestService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a description for the pull request
|
||||
* Generate a description for the pull request using Gemini
|
||||
* @param processedWorkitems List of processed workitems
|
||||
* @returns Pull request description
|
||||
*/
|
||||
private generatePullRequestDescription(
|
||||
private async generatePullRequestDescription(
|
||||
processedWorkitems: { workitem: Workitem; success: boolean; error?: string }[]
|
||||
): string {
|
||||
const added: string[] = [];
|
||||
const updated: string[] = [];
|
||||
const deleted: string[] = [];
|
||||
const failed: string[] = [];
|
||||
|
||||
for (const item of processedWorkitems) {
|
||||
const { workitem, success, error } = item;
|
||||
|
||||
if (!success) {
|
||||
failed.push(`- ${workitem.name}: ${error}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!workitem.isActive) {
|
||||
deleted.push(`- ${workitem.name}`);
|
||||
} else if (workitem.implementation) {
|
||||
updated.push(`- ${workitem.name}`);
|
||||
} else {
|
||||
added.push(`- ${workitem.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
let description = 'This PR was automatically generated by the prompts-to-test-spec function.\n\n';
|
||||
|
||||
if (added.length > 0) {
|
||||
description += '## Added\n' + added.join('\n') + '\n\n';
|
||||
}
|
||||
|
||||
if (updated.length > 0) {
|
||||
description += '## Updated\n' + updated.join('\n') + '\n\n';
|
||||
}
|
||||
|
||||
if (deleted.length > 0) {
|
||||
description += '## Deleted\n' + deleted.join('\n') + '\n\n';
|
||||
}
|
||||
|
||||
if (failed.length > 0) {
|
||||
description += '## Failed\n' + failed.join('\n') + '\n\n';
|
||||
}
|
||||
|
||||
return description;
|
||||
): Promise<string> {
|
||||
// Use Gemini to generate the pull request description
|
||||
return await this.geminiService.generatePullRequestDescription(processedWorkitems);
|
||||
}
|
||||
}
|
||||
|
@ -35,4 +35,5 @@ export interface ProcessResult {
|
||||
error?: string;
|
||||
}[];
|
||||
pullRequestUrl?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user