push-guardian 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +15 -0
- package/.pushguardian-plugins.json +10 -0
- package/Dockerfile +41 -0
- package/Dockerfile.dev +20 -0
- package/README.md +386 -0
- package/TECHNO.md +139 -0
- package/babel.config.js +1 -0
- package/developper_utils.md +119 -0
- package/docker-compose.yml +41 -0
- package/docs/PLUGINS.md +223 -0
- package/docs/technical/architecture.md +298 -0
- package/docs/technical/performance-guide.md +390 -0
- package/docs/technical/plugin-persistence.md +169 -0
- package/docs/technical/plugins-guide.md +409 -0
- package/jest.config.js +22 -0
- package/package.json +53 -0
- package/plugins/example-plugin/index.js +55 -0
- package/plugins/example-plugin/plugin.json +8 -0
- package/scripts/coverage-report.js +75 -0
- package/src/cli/command/config.js +33 -0
- package/src/cli/command/install.js +137 -0
- package/src/cli/command/mirror.js +90 -0
- package/src/cli/command/performance.js +160 -0
- package/src/cli/command/plugin.js +171 -0
- package/src/cli/command/security.js +152 -0
- package/src/cli/command/shell.js +238 -0
- package/src/cli/command/validate.js +54 -0
- package/src/cli/index.js +23 -0
- package/src/cli/install/codeQualityTools.js +156 -0
- package/src/cli/install/hooks.js +89 -0
- package/src/cli/install/mirroring.js +299 -0
- package/src/core/codeQualityTools/configAnalyzer.js +216 -0
- package/src/core/codeQualityTools/configGenerator.js +381 -0
- package/src/core/codeQualityTools/configManager.js +65 -0
- package/src/core/codeQualityTools/fileDetector.js +62 -0
- package/src/core/codeQualityTools/languageTools.js +104 -0
- package/src/core/codeQualityTools/toolInstaller.js +53 -0
- package/src/core/configManager.js +43 -0
- package/src/core/errorCMD.js +9 -0
- package/src/core/interactiveMenu/interactiveMenu.js +73 -0
- package/src/core/mirroring/branchSynchronizer.js +59 -0
- package/src/core/mirroring/generate.js +114 -0
- package/src/core/mirroring/repoManager.js +112 -0
- package/src/core/mirroring/syncManager.js +176 -0
- package/src/core/module/env-loader.js +109 -0
- package/src/core/performance/metricsCollector.js +217 -0
- package/src/core/performance/performanceAnalyzer.js +182 -0
- package/src/core/plugins/basePlugin.js +89 -0
- package/src/core/plugins/pluginManager.js +123 -0
- package/src/core/plugins/pluginRegistry.js +215 -0
- package/src/core/validator.js +53 -0
- package/src/hooks/constrains/constrains.js +174 -0
- package/src/hooks/constrains/constraintEngine.js +140 -0
- package/src/utils/chalk-wrapper.js +26 -0
- package/src/utils/exec-wrapper.js +6 -0
- package/tests/fixtures/mock-eslint-config-array.js +8 -0
- package/tests/fixtures/mock-eslint-config-single.js +6 -0
- package/tests/fixtures/mockLoadedPlugin.js +11 -0
- package/tests/setup.js +28 -0
- package/tests/unit/basePlugin.test.js +355 -0
- package/tests/unit/branchSynchronizer.test.js +308 -0
- package/tests/unit/cli-commands.test.js +144 -0
- package/tests/unit/codeQualityConfigManager.test.js +233 -0
- package/tests/unit/codeQualityTools.test.js +36 -0
- package/tests/unit/command-install.test.js +247 -0
- package/tests/unit/command-mirror.test.js +179 -0
- package/tests/unit/command-performance.test.js +169 -0
- package/tests/unit/command-plugin.test.js +288 -0
- package/tests/unit/command-security.test.js +277 -0
- package/tests/unit/command-shell.test.js +325 -0
- package/tests/unit/configAnalyzer.test.js +593 -0
- package/tests/unit/configGenerator.test.js +808 -0
- package/tests/unit/configManager.test.js +195 -0
- package/tests/unit/constrains.test.js +463 -0
- package/tests/unit/constraint.test.js +554 -0
- package/tests/unit/env-loader.test.js +279 -0
- package/tests/unit/fileDetector.test.js +171 -0
- package/tests/unit/install-codeQualityTools.test.js +343 -0
- package/tests/unit/install-hooks.test.js +280 -0
- package/tests/unit/install-mirroring.test.js +731 -0
- package/tests/unit/install-modules.test.js +81 -0
- package/tests/unit/interactiveMenu.test.js +426 -0
- package/tests/unit/languageTools.test.js +244 -0
- package/tests/unit/metricsCollector.test.js +354 -0
- package/tests/unit/mirroring-generate.test.js +96 -0
- package/tests/unit/modules-exist.test.js +96 -0
- package/tests/unit/performanceAnalyzer.test.js +473 -0
- package/tests/unit/pluginManager.test.js +427 -0
- package/tests/unit/pluginRegistry.test.js +592 -0
- package/tests/unit/repoManager.test.js +469 -0
- package/tests/unit/reviewAppManager.test.js +5 -0
- package/tests/unit/security-command.test.js +43 -0
- package/tests/unit/syncManager.test.js +494 -0
- package/tests/unit/toolInstaller.test.js +240 -0
- package/tests/unit/utils.test.js +144 -0
- package/tests/unit/validator.test.js +215 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const { getChalk } = require('../utils/chalk-wrapper');
|
|
2
|
+
const chalk = getChalk();
|
|
3
|
+
|
|
4
|
+
module.exports = (error) => {
|
|
5
|
+
const stackLines = error.stack.split('\n');
|
|
6
|
+
const location = stackLines[1] ? stackLines[1].trim() : 'Emplacement inconnu';
|
|
7
|
+
console.log(chalk.red('💥 Error during validation:'), error.message, location);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const { getChalk } = require('../../utils/chalk-wrapper');
|
|
2
|
+
const chalk = getChalk();
|
|
3
|
+
|
|
4
|
+
module.exports = (message, choices, preselected = []) => {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
let currentIndex = 0;
|
|
7
|
+
let choiseMap = preselected;
|
|
8
|
+
|
|
9
|
+
const renderMenu = () => {
|
|
10
|
+
console.clear();
|
|
11
|
+
console.log(chalk.blue(message));
|
|
12
|
+
choices.forEach((choise, index) => {
|
|
13
|
+
if (index == currentIndex) console.log(chalk.yellow(`> ${choise}`));
|
|
14
|
+
else if (index != currentIndex && choiseMap.includes(index)) console.log(chalk.green(`* ${choise}`));
|
|
15
|
+
else console.log(` ${choise}`);
|
|
16
|
+
});
|
|
17
|
+
console.log('\n▲: Up\t▼: Down\t◀: Unselect\t►: Select (Enter to validate, Ctrl+C to quit)');
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const handleInput = (key) => {
|
|
21
|
+
if (key == '\u0003') {
|
|
22
|
+
// QUIT with Ctrl+C
|
|
23
|
+
process.exit();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (key == '\u001B[A') {
|
|
28
|
+
// UP Arrow
|
|
29
|
+
currentIndex = (currentIndex - 1 + choices.length) % choices.length;
|
|
30
|
+
renderMenu();
|
|
31
|
+
} else if (key == '\u001B[B') {
|
|
32
|
+
// DOWN Arrow
|
|
33
|
+
currentIndex = (currentIndex + 1) % choices.length;
|
|
34
|
+
renderMenu();
|
|
35
|
+
} else if (key == '\r') {
|
|
36
|
+
// ENTER
|
|
37
|
+
process.stdin.setRawMode(false);
|
|
38
|
+
process.stdin.pause();
|
|
39
|
+
executeChoise(choiseMap);
|
|
40
|
+
} else if (key == '\u001B[D') {
|
|
41
|
+
// Left Arrow
|
|
42
|
+
const index = choiseMap.indexOf(currentIndex);
|
|
43
|
+
if (index !== -1) {
|
|
44
|
+
choiseMap.splice(index, 1);
|
|
45
|
+
}
|
|
46
|
+
renderMenu();
|
|
47
|
+
} else if (key == '\u001B[C') {
|
|
48
|
+
// Rigth Arrow
|
|
49
|
+
choiseMap.push(currentIndex);
|
|
50
|
+
renderMenu();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const executeChoise = (choiseMap) => {
|
|
55
|
+
if (!choiseMap.length) {
|
|
56
|
+
resolve([choices[currentIndex]]);
|
|
57
|
+
} else {
|
|
58
|
+
const selectedChoices = choiseMap.map((index) => choices[index]);
|
|
59
|
+
resolve(selectedChoices);
|
|
60
|
+
}
|
|
61
|
+
process.stdin.removeListener('data', handleInput);
|
|
62
|
+
process.stdin.setRawMode(false);
|
|
63
|
+
process.stdin.pause();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
process.stdin.setRawMode(true);
|
|
67
|
+
process.stdin.resume();
|
|
68
|
+
process.stdin.setEncoding('utf8');
|
|
69
|
+
process.stdin.on('data', handleInput);
|
|
70
|
+
|
|
71
|
+
renderMenu();
|
|
72
|
+
});
|
|
73
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
class BranchSynchronizer {
|
|
2
|
+
constructor(clients) {
|
|
3
|
+
this.clients = clients;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
async syncBranches(srcPlatform, targetPlatform, sourceRepo, targetRepo, sourceOwner, targetOwner) {
|
|
7
|
+
try {
|
|
8
|
+
const branches = await this.getBranches(srcPlatform, sourceRepo, sourceOwner);
|
|
9
|
+
|
|
10
|
+
for (const branch of branches) {
|
|
11
|
+
try {
|
|
12
|
+
await this.createBranch(targetPlatform, targetRepo, branch, targetOwner);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.warn(`⚠️ Impossible de créer la branche ${branch.name}: ${error.message}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
} catch (error) {
|
|
18
|
+
throw new Error(`La synchronisation des branches a échoué: ${error.message}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async getBranches(platform, repoName, owner) {
|
|
22
|
+
const client = this.clients[platform];
|
|
23
|
+
|
|
24
|
+
if (!client) throw new Error(`Plateforme non prise en charge: ${platform}`);
|
|
25
|
+
if (platform === 'github') {
|
|
26
|
+
const response = await client.repos.listBranches({ owner, repo: repoName });
|
|
27
|
+
return response.data;
|
|
28
|
+
}
|
|
29
|
+
if (platform === 'gitlab') return await client.Branches.all(repoName);
|
|
30
|
+
if (platform === 'bitbucket')
|
|
31
|
+
return await client.repositories.listBranches({ workspace: owner || 'workspace', repo_slug: repoName });
|
|
32
|
+
throw new Error(`Liste des branches non implémentée pour ${platform}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async createBranch(platform, repoName, branchData, owner) {
|
|
36
|
+
const client = this.clients[platform];
|
|
37
|
+
|
|
38
|
+
if (!client) throw new Error(`Plateforme non prise en charge: ${platform}`);
|
|
39
|
+
if (platform === 'github')
|
|
40
|
+
return await client.git.createRef({
|
|
41
|
+
owner,
|
|
42
|
+
repo: repoName,
|
|
43
|
+
ref: `refs/heads/${branchData.name}`,
|
|
44
|
+
sha: branchData.commit.sha
|
|
45
|
+
});
|
|
46
|
+
if (platform === 'gitlab')
|
|
47
|
+
return await client.Branches.create(repoName, branchData.name, { ref: branchData.commit.sha });
|
|
48
|
+
if (platform === 'bitbucket')
|
|
49
|
+
return await client.repositories.createBranch({
|
|
50
|
+
workspace: owner || 'workspace',
|
|
51
|
+
repo_slug: repoName,
|
|
52
|
+
name: branchData.name,
|
|
53
|
+
target: { hash: branchData.commit.sha }
|
|
54
|
+
});
|
|
55
|
+
throw new Error(`La création de branche n'est pas implémentée pour ${platform}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { BranchSynchronizer };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { getChalk } = require('../../utils/chalk-wrapper');
|
|
4
|
+
const chalk = getChalk();
|
|
5
|
+
|
|
6
|
+
function generateWorkflow() {
|
|
7
|
+
try {
|
|
8
|
+
const workflowsDir = path.join(process.cwd(), '.github', 'workflows');
|
|
9
|
+
if (!fs.existsSync(workflowsDir)) {
|
|
10
|
+
fs.mkdirSync(workflowsDir, { recursive: true });
|
|
11
|
+
console.log(chalk.green('✅ Dossier .github/workflows créé'));
|
|
12
|
+
}
|
|
13
|
+
const workflowContent = `name: Mirror Repository
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
schedule:
|
|
18
|
+
- cron: '0 2 * * *' # Every day at 2 AM UTC
|
|
19
|
+
push:
|
|
20
|
+
branches: [ main, master ]
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
mirror:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
|
|
26
|
+
steps:
|
|
27
|
+
- name: Checkout current repository
|
|
28
|
+
uses: actions/checkout@v4
|
|
29
|
+
with:
|
|
30
|
+
path: .
|
|
31
|
+
|
|
32
|
+
steps:
|
|
33
|
+
- name: Checkout push-guardian
|
|
34
|
+
uses: actions/checkout@v4
|
|
35
|
+
with:
|
|
36
|
+
repository: lagie-marin/push-guardian
|
|
37
|
+
path: push-guardian
|
|
38
|
+
|
|
39
|
+
- name: Setup Node.js
|
|
40
|
+
uses: actions/setup-node@v4
|
|
41
|
+
with:
|
|
42
|
+
node-version: '22.20.0'
|
|
43
|
+
cache: 'npm'
|
|
44
|
+
cache-dependency-path: push-guardian/package-lock.json
|
|
45
|
+
|
|
46
|
+
- name: Install specific npm version
|
|
47
|
+
run: |
|
|
48
|
+
echo "Current npm version:"
|
|
49
|
+
npm --version
|
|
50
|
+
echo ""
|
|
51
|
+
echo "Installing npm 11.6.0..."
|
|
52
|
+
# Désinstaller la version actuelle et installer la version spécifique
|
|
53
|
+
npm install -g npm@11.6.0 --force
|
|
54
|
+
echo ""
|
|
55
|
+
echo "New npm version:"
|
|
56
|
+
npm --version
|
|
57
|
+
|
|
58
|
+
- name: Install dependencies
|
|
59
|
+
run: |
|
|
60
|
+
cd push-guardian
|
|
61
|
+
npm ci
|
|
62
|
+
|
|
63
|
+
- name: Link push-guardian globally
|
|
64
|
+
run: |
|
|
65
|
+
cd push-guardian
|
|
66
|
+
npm link
|
|
67
|
+
|
|
68
|
+
- name: Execute mirror command
|
|
69
|
+
run: |
|
|
70
|
+
CURRENT_REPO="\${{ github.event.repository.name }}"
|
|
71
|
+
CURRENT_OWNER="\${{ github.event.repository.owner.login }}"
|
|
72
|
+
|
|
73
|
+
mirror_cmd="npx push-guardian mirror"
|
|
74
|
+
mirror_cmd="$mirror_cmd --source \${{ vars.SOURCE_PLATFORM }}"
|
|
75
|
+
mirror_cmd="$mirror_cmd --target \${{ vars.TARGET_PLATFORM }}"
|
|
76
|
+
mirror_cmd="$mirror_cmd --source-repo $CURRENT_REPO"
|
|
77
|
+
mirror_cmd="$mirror_cmd --repo \${{ vars.TARGET_REPO }}"
|
|
78
|
+
mirror_cmd="$mirror_cmd --source-owner $CURRENT_OWNER"
|
|
79
|
+
mirror_cmd="$mirror_cmd --target-owner \${{ vars.TARGET_OWNER }}"
|
|
80
|
+
|
|
81
|
+
if [ "\${{ vars.SYNC_BRANCHES }}" = "true" ]; then
|
|
82
|
+
mirror_cmd="$mirror_cmd --sync-branches"
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
if [ "\${{ vars.PUBLIC_REPO }}" = "true" ]; then
|
|
86
|
+
mirror_cmd="$mirror_cmd --public-repo"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
echo "Executing: $mirror_cmd"
|
|
90
|
+
eval $mirror_cmd
|
|
91
|
+
env:
|
|
92
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
93
|
+
GIT_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
94
|
+
GITLAB_TOKEN: \${{ secrets.GITLAB_TOKEN }}
|
|
95
|
+
BIT_BUCKET: \${{ secrets.BIT_BUCKET }}
|
|
96
|
+
BB_WORKSPACE: \${{ vars.BB_WORKSPACE }}
|
|
97
|
+
AZURE_DEVOPS_URL: \${{ secrets.AZURE_DEVOPS_URL }}
|
|
98
|
+
AZURE_DEVOPS_TOKEN: \${{ secrets.AZURE_DEVOPS_TOKEN }}
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
const workflowPath = path.join(workflowsDir, 'mirror.yml');
|
|
102
|
+
fs.writeFileSync(workflowPath, workflowContent, 'utf8');
|
|
103
|
+
|
|
104
|
+
console.log(chalk.green('✅ Workflow GitHub Actions généré : .github/workflows/mirror.yml'));
|
|
105
|
+
console.log(chalk.blue('ℹ️ Pensez à configurer les variables et secrets dans GitHub Actions'));
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error(chalk.red(`❌ Erreur lors de la génération du workflow : ${error.message}`));
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
generateWorkflow
|
|
114
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
class RepoManager {
|
|
2
|
+
constructor(clients) {
|
|
3
|
+
this.clients = clients;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
async createOrUpdateRepo(
|
|
7
|
+
srcPlatform,
|
|
8
|
+
targetPlatform,
|
|
9
|
+
sourceRepo,
|
|
10
|
+
targetRepo,
|
|
11
|
+
sourceOwner,
|
|
12
|
+
targetOwner,
|
|
13
|
+
public_repo = false
|
|
14
|
+
) {
|
|
15
|
+
try {
|
|
16
|
+
const srcRepo = await this.getRepo(srcPlatform, sourceRepo, sourceOwner);
|
|
17
|
+
const normalizedSrcRepo = srcRepo && srcRepo.data ? srcRepo.data : srcRepo;
|
|
18
|
+
const repoData = {
|
|
19
|
+
name: targetRepo,
|
|
20
|
+
description: normalizedSrcRepo.description || '',
|
|
21
|
+
private: normalizedSrcRepo.private || false
|
|
22
|
+
};
|
|
23
|
+
await this.createRepo(targetPlatform, repoData, targetOwner, public_repo);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new Error(`Échec de la mise en miroir du dépôt: ${error.message}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async getRepo(platform, repoName, owner) {
|
|
30
|
+
const client = this.clients[platform];
|
|
31
|
+
if (!client) throw new Error(`Plateforme non prise en charge: ${platform}`);
|
|
32
|
+
if (platform === 'github') return await client.repos.get({ owner, repo: repoName });
|
|
33
|
+
if (platform === 'gitlab') return await client.Projects.show(repoName);
|
|
34
|
+
if (platform === 'bitbucket')
|
|
35
|
+
return await client.repositories.get({ workspace: owner || 'workspace', repo_slug: repoName });
|
|
36
|
+
throw new Error(`La récupération du dépôt n'est pas implémentée pour ${platform}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async createRepo(platform, repoData, owner, public_repo = false) {
|
|
40
|
+
const client = this.clients[platform];
|
|
41
|
+
if (!client) throw new Error(`Plateforme non prise en charge: ${platform}`);
|
|
42
|
+
if (platform === 'github') {
|
|
43
|
+
try {
|
|
44
|
+
const existingRepo = await client.repos.get({ owner, repo: repoData.name }).catch(() => null);
|
|
45
|
+
if (existingRepo) {
|
|
46
|
+
console.log(
|
|
47
|
+
`📁 Le dépôt ${repoData.name} existe déjà chez ${owner}, utilisation du dépôt existant.`
|
|
48
|
+
);
|
|
49
|
+
return existingRepo.data;
|
|
50
|
+
}
|
|
51
|
+
return await client.repos.createForAuthenticatedUser({ ...repoData, private: !public_repo });
|
|
52
|
+
} catch {
|
|
53
|
+
try {
|
|
54
|
+
return await client.repos.createInOrg({ org: owner, ...repoData, private: !public_repo });
|
|
55
|
+
} catch (orgError) {
|
|
56
|
+
throw new Error(`Impossible de créer le dépôt: ${orgError.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (platform === 'gitlab') {
|
|
61
|
+
const projectPath = `${owner}/${repoData.name}`;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const existingByPath = await client.Projects.show(projectPath);
|
|
65
|
+
if (existingByPath) {
|
|
66
|
+
console.log(
|
|
67
|
+
`📁 Le dépôt ${repoData.name} existe déjà chez ${owner}, utilisation du dépôt existant.`
|
|
68
|
+
);
|
|
69
|
+
return existingByPath;
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Le projet n'existe pas (ou n'est pas accessible), on tente la création juste après.
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const existing = await client.Projects.search(repoData.name).catch(() => []);
|
|
76
|
+
const existingRepo = existing.find((p) => p.name === repoData.name && p.namespace.name === owner);
|
|
77
|
+
if (existingRepo) {
|
|
78
|
+
console.log(`📁 Le dépôt ${repoData.name} existe déjà chez ${owner}, utilisation du dépôt existant.`);
|
|
79
|
+
return existingRepo;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
return await client.Projects.create({
|
|
84
|
+
...repoData,
|
|
85
|
+
visibility: public_repo ? 'public' : 'private'
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error && (error.message || '').toLowerCase().includes('forbidden')) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"GitLab a refusé l'opération (Forbidden). Vérifiez que GITLAB_TOKEN a le scope 'api' et les droits de création dans le namespace cible."
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (platform === 'bitbucket') {
|
|
97
|
+
const existing = await client.repositories.list({ workspace: owner || 'workspace' });
|
|
98
|
+
const existingRepo = existing.data.values.find((r) => r.name === repoData.name);
|
|
99
|
+
if (existingRepo) {
|
|
100
|
+
console.log(`📁 Le dépôt ${repoData.name} existe déjà chez ${owner}, utilisation du dépôt existant.`);
|
|
101
|
+
return existingRepo;
|
|
102
|
+
}
|
|
103
|
+
return await client.repositories.create({
|
|
104
|
+
workspace: owner || 'workspace',
|
|
105
|
+
repository: { ...repoData, is_private: !public_repo }
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
throw new Error(`La création de dépôt n'est pas implémentée pour ${platform}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { RepoManager };
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
const { Octokit } = require('@octokit/rest');
|
|
2
|
+
const { WebApi } = require('azure-devops-node-api');
|
|
3
|
+
const { Bitbucket } = require('bitbucket');
|
|
4
|
+
const { Gitlab } = require('gitlab');
|
|
5
|
+
const simpleGit = require('simple-git');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const { RepoManager } = require('./repoManager');
|
|
9
|
+
const { BranchSynchronizer } = require('./branchSynchronizer');
|
|
10
|
+
const { getEnv } = require('../module/env-loader');
|
|
11
|
+
|
|
12
|
+
class SyncManager {
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.clients = this.initClients();
|
|
16
|
+
this.repoManager = new RepoManager(this.clients);
|
|
17
|
+
this.branchSynchronizer = new BranchSynchronizer(this.clients);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
initClients() {
|
|
21
|
+
const clients = {};
|
|
22
|
+
|
|
23
|
+
if (this.config.github && this.config.github.enabled) {
|
|
24
|
+
try {
|
|
25
|
+
const token = getEnv('GITHUB_TOKEN');
|
|
26
|
+
clients.github = new Octokit({ auth: token });
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.warn(`⚠️ Impossible d'initialiser le client GitHub: ${error.message}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (this.config.gitlab && this.config.gitlab.enabled) {
|
|
33
|
+
try {
|
|
34
|
+
const token = getEnv('GITLAB_TOKEN');
|
|
35
|
+
clients.gitlab = new Gitlab({ token: token });
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.warn(`⚠️ Impossible d'initialiser le client GitLab: ${error.message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (this.config.bitbucket && this.config.bitbucket.enabled) {
|
|
42
|
+
try {
|
|
43
|
+
const username = getEnv('BITBUCKET_USERNAME');
|
|
44
|
+
const password = getEnv('BITBUCKET_PASSWORD');
|
|
45
|
+
clients.bitbucket = new Bitbucket({
|
|
46
|
+
auth: { username: username, password: password }
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.warn(`⚠️ Impossible d'initialiser le client BitBucket: ${error.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (this.config.azure && this.config.azure.enabled) {
|
|
54
|
+
try {
|
|
55
|
+
const url = getEnv('AZURE_DEVOPS_URL');
|
|
56
|
+
const token = getEnv('AZURE_DEVOPS_TOKEN');
|
|
57
|
+
clients.azure = new WebApi(url, token);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.warn(`⚠️ Impossible d'initialiser le client Azure DevOps: ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return clients;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async mirror(
|
|
67
|
+
sourcePlatform,
|
|
68
|
+
targetPlatform,
|
|
69
|
+
sourceRepo,
|
|
70
|
+
targetRepo,
|
|
71
|
+
sourceOwner,
|
|
72
|
+
targetOwner,
|
|
73
|
+
syncBranches = false,
|
|
74
|
+
public_repo = false
|
|
75
|
+
) {
|
|
76
|
+
try {
|
|
77
|
+
await this.repoManager.createOrUpdateRepo(
|
|
78
|
+
sourcePlatform,
|
|
79
|
+
targetPlatform,
|
|
80
|
+
sourceRepo,
|
|
81
|
+
targetRepo,
|
|
82
|
+
sourceOwner,
|
|
83
|
+
targetOwner,
|
|
84
|
+
public_repo
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
await this.pushCodeToTarget(
|
|
88
|
+
sourcePlatform,
|
|
89
|
+
targetPlatform,
|
|
90
|
+
sourceRepo,
|
|
91
|
+
targetRepo,
|
|
92
|
+
sourceOwner,
|
|
93
|
+
targetOwner
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (syncBranches) {
|
|
97
|
+
await this.branchSynchronizer.syncBranches(
|
|
98
|
+
sourcePlatform,
|
|
99
|
+
targetPlatform,
|
|
100
|
+
sourceRepo,
|
|
101
|
+
targetRepo,
|
|
102
|
+
sourceOwner,
|
|
103
|
+
targetOwner
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error(`❌ Échec de la mise en miroir: ${error.message}`);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async pushCodeToTarget(sourcePlatform, targetPlatform, sourceRepo, targetRepo, sourceOwner, targetOwner) {
|
|
113
|
+
if (sourcePlatform !== 'github' || targetPlatform !== 'github') {
|
|
114
|
+
console.log("⚠️ Le push du code n'est actuellement supporté que pour GitHub vers GitHub");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const sourceToken = getEnv('GITHUB_TOKEN');
|
|
119
|
+
const targetToken = sourceToken;
|
|
120
|
+
|
|
121
|
+
if (!sourceToken || !targetToken) {
|
|
122
|
+
console.log('⚠️ Tokens manquants pour pousser le code');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const tempDir = path.join(process.cwd(), 'temp-mirror-' + Date.now());
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
130
|
+
|
|
131
|
+
const git = simpleGit(tempDir);
|
|
132
|
+
|
|
133
|
+
const sourceUrl = `https://${sourceToken}@github.com/${sourceOwner}/${sourceRepo}.git`;
|
|
134
|
+
console.log('📥 Clonage du dépôt source...');
|
|
135
|
+
await git.clone(sourceUrl, '.');
|
|
136
|
+
|
|
137
|
+
const targetUrl = `https://${targetToken}@github.com/${targetOwner}/${targetRepo}.git`;
|
|
138
|
+
console.log('📤 Configuration du remote cible...');
|
|
139
|
+
await git.removeRemote('origin');
|
|
140
|
+
await git.addRemote('origin', targetUrl);
|
|
141
|
+
|
|
142
|
+
console.log('🚀 Push du code vers le dépôt cible...');
|
|
143
|
+
await git.push('origin', 'main', ['--force']);
|
|
144
|
+
|
|
145
|
+
const branches = await git.branch();
|
|
146
|
+
for (const branch of branches.all) {
|
|
147
|
+
if (branch !== 'main' && branch !== 'master') {
|
|
148
|
+
try {
|
|
149
|
+
await git.push('origin', branch, ['--force']);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.warn(`⚠️ Impossible de pousser la branche ${branch}: ${error.message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
await git.pushTags('origin');
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.warn(`⚠️ Impossible de pousser les tags: ${error.message}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log('✅ Code poussé avec succès vers le dépôt cible');
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error(`❌ Échec du push du code: ${error.message}`);
|
|
165
|
+
throw error;
|
|
166
|
+
} finally {
|
|
167
|
+
try {
|
|
168
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
169
|
+
} catch (cleanupError) {
|
|
170
|
+
console.warn(`⚠️ Impossible de nettoyer le dossier temporaire: ${cleanupError.message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = { SyncManager };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Charge les variables d'environnement depuis un fichier .env
|
|
6
|
+
* @param {string} envPath - Chemin vers le fichier .env (par défaut: '.env')
|
|
7
|
+
*/
|
|
8
|
+
function loadEnv(envPath = '.env') {
|
|
9
|
+
try {
|
|
10
|
+
const absolutePath = path.resolve(process.cwd(), envPath);
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(absolutePath)) {
|
|
13
|
+
console.warn('⚠️ Fichier .env non trouvé, utilisation des variables système');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const content = fs.readFileSync(absolutePath, 'utf8');
|
|
18
|
+
let lines = content.split('\n');
|
|
19
|
+
|
|
20
|
+
lines.forEach((line, index) => {
|
|
21
|
+
line = line.trim();
|
|
22
|
+
if (!line || line.startsWith('#')) return;
|
|
23
|
+
|
|
24
|
+
const match = line.match(/^\s*([\w.-]+)\s*=\s*(.*)?\s*$/);
|
|
25
|
+
if (!match) {
|
|
26
|
+
console.warn(`⚠️ Ligne ${index + 1} ignorée (format invalide): ${line}`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const key = match[1];
|
|
31
|
+
let value = match[2] || '';
|
|
32
|
+
|
|
33
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
34
|
+
value = value.slice(1, -1).replace(/\\n/g, '\n');
|
|
35
|
+
} else if (value.startsWith("'") && value.endsWith("'")) {
|
|
36
|
+
value = value.slice(1, -1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (process.env[key] === undefined) {
|
|
40
|
+
process.env[key] = value;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
console.log("✅ Variables d'environnement chargées depuis .env");
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('❌ Erreur lors du chargement du fichier .env:', error.message);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Récupère une variable d'environnement avec validation
|
|
52
|
+
* @param {string} key - Clé de la variable
|
|
53
|
+
* @param {*} defaultValue - Valeur par défaut si non trouvée
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
function getEnv(key, defaultValue = null, werror = false) {
|
|
57
|
+
const value = process.env[key];
|
|
58
|
+
if (value === undefined || value === '') {
|
|
59
|
+
if (defaultValue === null) {
|
|
60
|
+
if (werror) {
|
|
61
|
+
throw new Error(`Variable d'environnement manquante: ${key}`);
|
|
62
|
+
} else {
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return defaultValue;
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Sauvegarde une variable d'environnement dans le fichier .env
|
|
73
|
+
* @param {string} key - Clé de la variable
|
|
74
|
+
* @param {string} value - Valeur de la variable
|
|
75
|
+
* @param {string} envPath - Chemin vers le fichier .env (par défaut: '.env')
|
|
76
|
+
*/
|
|
77
|
+
function saveEnv(key, value, envPath = '.env') {
|
|
78
|
+
try {
|
|
79
|
+
const absolutePath = path.resolve(process.cwd(), envPath);
|
|
80
|
+
let content = '';
|
|
81
|
+
|
|
82
|
+
if (fs.existsSync(absolutePath)) {
|
|
83
|
+
content = fs.readFileSync(absolutePath, 'utf8');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const lines = content.split('\n');
|
|
87
|
+
let updated = false;
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < lines.length; i++) {
|
|
90
|
+
const line = lines[i].trim();
|
|
91
|
+
if (line.startsWith(`${key}=`) || line.startsWith(`${key} =`)) {
|
|
92
|
+
lines[i] = `${key}=${value}`;
|
|
93
|
+
updated = true;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!updated) {
|
|
99
|
+
lines.push(`${key}=${value}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fs.writeFileSync(absolutePath, lines.join('\n'), 'utf8');
|
|
103
|
+
console.log(`✅ Variable ${key} sauvegardée dans .env`);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('❌ Erreur lors de la sauvegarde de la variable:', error.message);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { loadEnv, getEnv, saveEnv };
|