missions api2
This commit is contained in:
parent
a7785ffc86
commit
4ac46d2d3c
273
lib/services/gitea-service.ts
Normal file
273
lib/services/gitea-service.ts
Normal file
@ -0,0 +1,273 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export class GiteaService {
|
||||
private apiUrl: string;
|
||||
private apiToken: string;
|
||||
private owner: string;
|
||||
|
||||
constructor() {
|
||||
this.apiUrl = process.env.GITEA_API_URL || '';
|
||||
this.apiToken = process.env.GITEA_API_TOKEN || '';
|
||||
this.owner = process.env.GITEA_OWNER || '';
|
||||
console.log('GiteaService initialized with URL:', this.apiUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the properly formatted API endpoint
|
||||
*/
|
||||
private getApiEndpoint(path: string): string {
|
||||
const baseUrl = this.apiUrl.endsWith('/')
|
||||
? this.apiUrl.slice(0, -1)
|
||||
: this.apiUrl;
|
||||
|
||||
const pathWithoutLeadingSlash = path.startsWith('/')
|
||||
? path.slice(1)
|
||||
: path;
|
||||
|
||||
return `${baseUrl}/api/v1/${pathWithoutLeadingSlash}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new repository for a mission
|
||||
* @param mission The mission data
|
||||
* @returns Repository info or throws error
|
||||
*/
|
||||
async createRepository(mission: any): Promise<any> {
|
||||
try {
|
||||
// Create a repo name from the mission name
|
||||
// Replace spaces with dashes and remove special characters
|
||||
const repoName = mission.name
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^a-z0-9-]/g, '');
|
||||
|
||||
console.log(`Creating repository: ${repoName}`);
|
||||
|
||||
// Determine if we're creating under a user or an organization
|
||||
const endpoint = this.owner.startsWith('@')
|
||||
? this.getApiEndpoint(`org/${this.owner.substring(1)}/repos`)
|
||||
: this.getApiEndpoint('user/repos');
|
||||
|
||||
// Create the repository
|
||||
const response = await axios.post(
|
||||
endpoint,
|
||||
{
|
||||
name: repoName,
|
||||
description: mission.intention || mission.name,
|
||||
private: true,
|
||||
auto_init: true,
|
||||
gitignores: 'Node',
|
||||
license: 'MIT',
|
||||
readme: 'Default',
|
||||
default_branch: 'main'
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `token ${this.apiToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`Repository created: ${response.data.full_name}`);
|
||||
|
||||
// If the mission has users, add them as collaborators
|
||||
if (mission.missionUsers && mission.missionUsers.length > 0) {
|
||||
await this.addCollaborators(response.data.owner.login, repoName, mission.missionUsers);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
console.error('Gitea API Error:', {
|
||||
status: error.response.status,
|
||||
data: error.response.data
|
||||
});
|
||||
}
|
||||
console.error('Error creating Gitea repository:', error);
|
||||
throw new Error(`Gitea integration failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add collaborators to a repository
|
||||
* @param owner Repository owner
|
||||
* @param repo Repository name
|
||||
* @param missionUsers The mission users to add as collaborators
|
||||
*/
|
||||
async addCollaborators(owner: string, repo: string, missionUsers: any[]): Promise<void> {
|
||||
console.log(`Adding ${missionUsers.length} collaborators to ${owner}/${repo}`);
|
||||
|
||||
const addCollaboratorPromises = missionUsers.map(async (missionUser) => {
|
||||
try {
|
||||
// Skip if no username is provided
|
||||
if (!missionUser.user.username) {
|
||||
console.log(`No username for user ${missionUser.user.email}, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine permission level based on role
|
||||
// For Gitea: 'read', 'write', 'admin'
|
||||
let permission = 'write';
|
||||
if (missionUser.role === 'gardien-temps') {
|
||||
permission = 'admin';
|
||||
} else if (missionUser.role === 'observateur') {
|
||||
permission = 'read';
|
||||
}
|
||||
|
||||
console.log(`Adding ${missionUser.user.username} as ${permission}`);
|
||||
|
||||
await axios.put(
|
||||
this.getApiEndpoint(`repos/${owner}/${repo}/collaborators/${missionUser.user.username}`),
|
||||
{ permission },
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `token ${this.apiToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`Added ${missionUser.user.username} as collaborator`);
|
||||
} catch (error) {
|
||||
console.error(`Error adding collaborator ${missionUser.user.username}:`, error);
|
||||
// Continue with other collaborators even if one fails
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for all collaborator additions to complete
|
||||
await Promise.all(addCollaboratorPromises);
|
||||
console.log('Finished adding collaborators');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a repository
|
||||
* @param owner Repository owner
|
||||
* @param repo Repository name
|
||||
* @returns True if successful, false otherwise
|
||||
*/
|
||||
async deleteRepository(owner: string, repo: string): Promise<boolean> {
|
||||
try {
|
||||
await axios.delete(
|
||||
this.getApiEndpoint(`repos/${owner}/${repo}`),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `token ${this.apiToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`Repository ${owner}/${repo} deleted successfully`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Error deleting repository ${owner}/${repo}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new branch in a repository
|
||||
* @param owner Repository owner
|
||||
* @param repo Repository name
|
||||
* @param branchName New branch name
|
||||
* @param baseBranchName Base branch name (default: 'main')
|
||||
* @returns True if successful, false otherwise
|
||||
*/
|
||||
async createBranch(
|
||||
owner: string,
|
||||
repo: string,
|
||||
branchName: string,
|
||||
baseBranchName: string = 'main'
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// First, get the SHA of the base branch
|
||||
const refResponse = await axios.get(
|
||||
this.getApiEndpoint(`repos/${owner}/${repo}/git/refs/heads/${baseBranchName}`),
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `token ${this.apiToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const sha = refResponse.data.object.sha;
|
||||
|
||||
// Create the new branch
|
||||
await axios.post(
|
||||
this.getApiEndpoint(`repos/${owner}/${repo}/git/refs`),
|
||||
{
|
||||
ref: `refs/heads/${branchName}`,
|
||||
sha: sha
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `token ${this.apiToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`Branch ${branchName} created in ${owner}/${repo}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Error creating branch ${branchName} in ${owner}/${repo}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a file in a repository
|
||||
* @param owner Repository owner
|
||||
* @param repo Repository name
|
||||
* @param path File path
|
||||
* @param content File content (will be base64 encoded)
|
||||
* @param message Commit message
|
||||
* @param branch Branch name (default: 'main')
|
||||
* @returns True if successful, false otherwise
|
||||
*/
|
||||
async createFile(
|
||||
owner: string,
|
||||
repo: string,
|
||||
path: string,
|
||||
content: string,
|
||||
message: string,
|
||||
branch: string = 'main'
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// Convert content to base64
|
||||
const contentBase64 = Buffer.from(content).toString('base64');
|
||||
|
||||
await axios.post(
|
||||
this.getApiEndpoint(`repos/${owner}/${repo}/contents/${path}`),
|
||||
{
|
||||
message,
|
||||
content: contentBase64,
|
||||
branch
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `token ${this.apiToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`File ${path} created in ${owner}/${repo}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Error creating file ${path} in ${owner}/${repo}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get repository URL
|
||||
* @param repoName Repository name
|
||||
* @returns Full repository URL
|
||||
*/
|
||||
getRepositoryUrl(repoName: string): string {
|
||||
const baseUrl = this.apiUrl.replace('/api/v1', '');
|
||||
return `${baseUrl}/${this.owner}/${repoName}`;
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ import { prisma } from '@/lib/prisma';
|
||||
import { LeantimeService } from './leantime-service';
|
||||
import { OutlineService } from './outline-service';
|
||||
import { RocketChatService } from './rocketchat-service';
|
||||
import { GiteaService } from './gitea-service';
|
||||
import axios from 'axios';
|
||||
|
||||
interface IntegrationResult {
|
||||
@ -20,11 +21,13 @@ export class IntegrationService {
|
||||
private leantimeService: LeantimeService;
|
||||
private outlineService: OutlineService;
|
||||
private rocketChatService: RocketChatService;
|
||||
private giteaService: GiteaService;
|
||||
|
||||
constructor() {
|
||||
this.leantimeService = new LeantimeService();
|
||||
this.outlineService = new OutlineService();
|
||||
this.rocketChatService = new RocketChatService();
|
||||
this.giteaService = new GiteaService();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -55,12 +58,14 @@ export class IntegrationService {
|
||||
let leantimeProjectId: number | undefined;
|
||||
let outlineCollectionId: string | undefined;
|
||||
let rocketChatChannelId: string | undefined;
|
||||
let giteaRepositoryUrl: string | undefined;
|
||||
|
||||
// Track which integrations succeeded and which failed
|
||||
const integrationStatus = {
|
||||
leantime: { success: false, id: undefined as number | undefined, error: undefined as string | undefined },
|
||||
outline: { success: false, id: undefined as string | undefined, error: undefined as string | undefined },
|
||||
rocketchat: { success: false, id: undefined as string | undefined, error: undefined as string | undefined }
|
||||
rocketchat: { success: false, id: undefined as string | undefined, error: undefined as string | undefined },
|
||||
gitea: { success: false, url: undefined as string | undefined, error: undefined as string | undefined }
|
||||
};
|
||||
|
||||
// A flag to determine if we should consider this a success or failure overall
|
||||
@ -131,14 +136,43 @@ export class IntegrationService {
|
||||
// Don't set criticalFailure - Rocket.Chat is non-critical
|
||||
}
|
||||
|
||||
// Add a delay to avoid rate limits (extended to 3 seconds)
|
||||
console.log('Waiting 3 seconds before proceeding to Gitea integration...');
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
// Step 4: Create Gitea repository (Consider this non-critical)
|
||||
try {
|
||||
console.log('Starting Gitea repository creation...');
|
||||
const repoData = await this.giteaService.createRepository(mission);
|
||||
if (repoData && repoData.html_url) {
|
||||
giteaRepositoryUrl = repoData.html_url;
|
||||
} else {
|
||||
giteaRepositoryUrl = this.giteaService.getRepositoryUrl(
|
||||
mission.name
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^a-z0-9-]/g, '')
|
||||
);
|
||||
}
|
||||
console.log(`Gitea repository created at: ${giteaRepositoryUrl}`);
|
||||
integrationStatus.gitea.success = true;
|
||||
integrationStatus.gitea.url = giteaRepositoryUrl;
|
||||
} catch (giteaError) {
|
||||
console.error('Error creating Gitea repository:', giteaError);
|
||||
integrationStatus.gitea.success = false;
|
||||
integrationStatus.gitea.error = giteaError instanceof Error ? giteaError.message : String(giteaError);
|
||||
|
||||
// Don't set criticalFailure - Gitea is non-critical
|
||||
}
|
||||
|
||||
// Update the mission with the integration IDs (only for successful integrations)
|
||||
await prisma.mission.update({
|
||||
where: { id: missionId },
|
||||
data: {
|
||||
leantimeProjectId: integrationStatus.leantime.success ? leantimeProjectId?.toString() : undefined,
|
||||
outlineCollectionId: integrationStatus.outline.success ? outlineCollectionId : undefined,
|
||||
rocketChatChannelId: integrationStatus.rocketchat.success ? rocketChatChannelId : undefined
|
||||
// giteaRepositoryUrl and penpotProjectId will be added when implemented
|
||||
rocketChatChannelId: integrationStatus.rocketchat.success ? rocketChatChannelId : undefined,
|
||||
giteaRepositoryUrl: integrationStatus.gitea.success ? giteaRepositoryUrl : undefined
|
||||
}
|
||||
});
|
||||
|
||||
@ -152,14 +186,15 @@ export class IntegrationService {
|
||||
data: {
|
||||
leantimeProjectId,
|
||||
outlineCollectionId,
|
||||
rocketChatChannelId
|
||||
rocketChatChannelId,
|
||||
giteaRepositoryUrl
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error setting up integrations:', error);
|
||||
|
||||
// Rollback any created resources
|
||||
await this.rollbackIntegrations(leantimeProjectId, outlineCollectionId, rocketChatChannelId);
|
||||
await this.rollbackIntegrations(leantimeProjectId, outlineCollectionId, rocketChatChannelId, giteaRepositoryUrl);
|
||||
|
||||
// Delete the mission itself since a critical integration failed
|
||||
try {
|
||||
@ -190,11 +225,13 @@ export class IntegrationService {
|
||||
* @param leantimeProjectId The Leantime project ID to delete
|
||||
* @param outlineCollectionId The Outline collection ID to delete
|
||||
* @param rocketChatChannelId The Rocket.Chat channel ID to delete
|
||||
* @param giteaRepositoryUrl The Gitea repository URL to extract owner/repo from for deletion
|
||||
*/
|
||||
private async rollbackIntegrations(
|
||||
leantimeProjectId?: number,
|
||||
outlineCollectionId?: string,
|
||||
rocketChatChannelId?: string
|
||||
rocketChatChannelId?: string,
|
||||
giteaRepositoryUrl?: string
|
||||
): Promise<void> {
|
||||
console.log('⚠️ Rolling back integrations due to an error...');
|
||||
|
||||
@ -202,7 +239,8 @@ export class IntegrationService {
|
||||
const rollbackStatuses = {
|
||||
leantime: false,
|
||||
outline: false,
|
||||
rocketchat: false
|
||||
rocketchat: false,
|
||||
gitea: false
|
||||
};
|
||||
|
||||
try {
|
||||
@ -247,17 +285,39 @@ export class IntegrationService {
|
||||
const rocketChatSuccess = await this.rocketChatService.deleteChannel(rocketChatChannelId);
|
||||
rollbackStatuses.rocketchat = rocketChatSuccess;
|
||||
console.log(`Rocket.Chat channel deletion ${rocketChatSuccess ? 'successful' : 'failed'}: ${rocketChatChannelId}`);
|
||||
|
||||
// Add a longer delay to avoid rate limiting (3 seconds)
|
||||
console.log('Waiting 3 seconds before next rollback operation...');
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
} catch (rocketChatError) {
|
||||
console.error('Error during Rocket.Chat rollback:', rocketChatError);
|
||||
console.log(`⚠️ Note: Rocket.Chat channel ${rocketChatChannelId} may need to be deleted manually`);
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to delete Gitea repository
|
||||
if (giteaRepositoryUrl) {
|
||||
try {
|
||||
// Extract owner and repo from URL
|
||||
const urlParts = giteaRepositoryUrl.split('/');
|
||||
const owner = urlParts[urlParts.length - 2];
|
||||
const repo = urlParts[urlParts.length - 1];
|
||||
|
||||
console.log(`Attempting to delete Gitea repository: ${owner}/${repo}`);
|
||||
const giteaSuccess = await this.giteaService.deleteRepository(owner, repo);
|
||||
rollbackStatuses.gitea = giteaSuccess;
|
||||
console.log(`Gitea repository deletion ${giteaSuccess ? 'successful' : 'failed'}: ${owner}/${repo}`);
|
||||
} catch (giteaError) {
|
||||
console.error('Error during Gitea rollback:', giteaError);
|
||||
console.log(`⚠️ Note: Gitea repository at ${giteaRepositoryUrl} may need to be deleted manually`);
|
||||
}
|
||||
}
|
||||
|
||||
// Provide a summary of rollback operations
|
||||
console.log('Rollback summary:', JSON.stringify(rollbackStatuses));
|
||||
|
||||
// If any rollbacks failed, provide a note
|
||||
if (!rollbackStatuses.leantime || !rollbackStatuses.outline || !rollbackStatuses.rocketchat) {
|
||||
if (!rollbackStatuses.leantime || !rollbackStatuses.outline || !rollbackStatuses.rocketchat || !rollbackStatuses.gitea) {
|
||||
console.log('⚠️ Some resources may need to be deleted manually.');
|
||||
}
|
||||
} catch (error) {
|
||||
@ -266,6 +326,7 @@ export class IntegrationService {
|
||||
if (leantimeProjectId) console.log(`- Leantime project: ${leantimeProjectId}`);
|
||||
if (outlineCollectionId) console.log(`- Outline collection: ${outlineCollectionId}`);
|
||||
if (rocketChatChannelId) console.log(`- Rocket.Chat channel: ${rocketChatChannelId}`);
|
||||
if (giteaRepositoryUrl) console.log(`- Gitea repository: ${giteaRepositoryUrl}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user