Skip to content
Snippets Groups Projects
Commit ecf618b9 authored by Sébastien Heurtematte's avatar Sébastien Heurtematte :speech_balloon:
Browse files

Initial commit

parents
No related branches found
No related tags found
No related merge requests found
GITHUB_TOKEN=""
GITLAB_TOKEN=""
GITLAB_HOST=""
GITLAB_REPO_ID=""
\ No newline at end of file
dist
.env
node_modules
# SPDX-FileCopyrightText: 2024 eclipse foundation
# SPDX-License-Identifier: EPL-2.0
include:
- project: 'eclipsefdn/it/releng/gitlab-runner-service/gitlab-ci-templates'
file: 'pipeline.gitlab-ci.yml'
default:
tags:
- origin:eclipse
variables:
CI_REGISTRY_IMAGE: docker.io/eclipsecbi/gitlab2github-sync-contribution
dco:
allow_failure: true
# GitHub to GitLab Sync Contribution
This project provides a solution to synchronize issues between GitHub and GitLab.
## Features
- synchronization of issues
- Conflict resolution
- Support for multiple repositories
## Installation
1. Clone the repository:
```sh
git clone https://gitlab.eclipse.org/eclipsefdn/it/releng/additional-tools/github2gitlab-sync-contribution.git
```
2. Navigate to the project directory:
```sh
cd github2gitlab-sync-contribution
```
3. Install the dependencies:
```sh
npm install
```
## Usage
1. Configure your GitHub and GitLab credentials in the `.env` file.
2. Run the synchronization script:
```sh
npm run start
```
## Issue to sync
Configure the issue in the `config.yaml`
E.g:
```yaml
issues:
- "https://github.com/eclipse-cbi/cbi-website/issues/2"
- "https://github.com/eclipse-cbi/cbi-website/issues/5"
```
\ No newline at end of file
issues:
- "https://github.com/heurtematte/eclipse-cbi-mkdocs/issues/2"
- "https://github.com/eclipse-cbi/cbi-website/issues/2"
- "https://github.com/eclipse-cbi/cbi-website/issues/5"
- "https://github.com/eclipse-apoapsis/ort-server/issues/2176"
This diff is collapsed.
{
"name": "occtet-github-contribution",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"build": "npm run clean && npm run lint && npx tsc",
"clean": "(rm -r ./dist || true)",
"clean:all": "npm run clean && (rm -r ./node_modules || true)",
"start": "node ./dist/index.js"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@gitbeaker/rest": "^42.1.0",
"@octokit/rest": "^21.1.1",
"config": "^3.3.12",
"dotenv": "^16.4.7",
"fs-extra": "^11.3.0",
"js-yaml": "^4.1.0",
"sitka": "^1.1.1"
},
"devDependencies": {
"@types/config": "^3.3.5",
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.13.5",
"typescript": "^5.7.3"
}
}
import fs from 'fs';
import yaml from 'js-yaml';
import { Octokit } from '@octokit/rest';
import { Gitlab } from '@gitbeaker/rest';
import dotenv from 'dotenv';
import { Logger } from 'sitka';
dotenv.config();
const log = Logger.getLogger({
name: 'sync-issue',
format: Logger.Format.TEXT_NO_TIME,
});
import { exit } from 'process';
interface Config {
issues: string[];
pr: string[];
}
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
const GITLAB_TOKEN = process.env.GITLAB_TOKEN;
const GITLAB_HOST = process.env.GITLAB_HOST;
const GITLAB_REPO_ID = process.env.GITLAB_REPO_ID || "";
if (!GITHUB_TOKEN || !GITLAB_TOKEN) {
log.error("Error: Missing GITHUB_TOKEN or GITLAB_TOKEN in config file or env.");
process.exit(1);
}
const CONFIG_FILE = 'config.yaml';
function loadConfig(): Config {
try {
const fileContents = fs.readFileSync(CONFIG_FILE, 'utf8');
return yaml.load(fileContents) as Config;
} catch (err) {
log.error("Error loading configuration:", err);
process.exit(1);
}
}
const config: Config = loadConfig();
const octokit = new Octokit({ auth: GITHUB_TOKEN });
const gitlab = new Gitlab({
token: GITLAB_TOKEN,
host: process.env.GITLAB_HOST
});
const issueUrls: string[] = config.issues;
// axios.interceptors.request.use(request => {
// console.log('Starting Request', JSON.stringify(request, null, 2))
// return request
// })
// axios.interceptors.response.use(response => {
// console.log('Response:', JSON.stringify(response, null, 2))
// return response
// })
async function getGitHubIssue(owner: string, repo: string, issueNumber: number) {
try {
const { data } = await octokit.issues.get({ owner, repo, issue_number: issueNumber });
return data;
} catch (error: any) {
log.error(`Error fetching GitHub issue #${issueNumber}:`, error.message);
return null;
}
}
async function getGitLabIssues() {
try {
return await gitlab.Issues.all({projectId:GITLAB_REPO_ID});
} catch (error: any) {
console.error("Error fetching GitLab issues:", error.message);
return [];
}
}
async function updateGitLabIssueStatus(issueId: number, githubState: string) {
const newState = githubState === "closed" ? "close" : "reopen";
try {
await gitlab.Issues.edit(GITLAB_REPO_ID, issueId, { stateEvent: newState });
console.log(`Updated GitLab issue #${issueId} to ${newState}`);
} catch (error: any) {
console.error(`Error updating GitLab issue #${issueId}:`, error.message);
}
}
async function getGitHubComments(owner: string, repo: string, issueNumber: number) {
try {
const { data } = await octokit.issues.listComments({ owner, repo, issue_number: issueNumber });
return data;
} catch (error: any) {
log.error(`Error fetching comments for GitHub issue #${issueNumber}:`, error.message);
return [];
}
}
async function getGitLabComments(issueId: number) {
try {
return await gitlab.IssueNotes.all(GITLAB_REPO_ID, issueId);
} catch (error: any) {
console.error(`Error fetching comments for GitLab issue #${issueId}:`, error.message);
return [];
}
}
function isCommentAlreadySynced(gitlabComments: any[], githubCommentId: number): boolean {
return gitlabComments.some(comment => comment.body.includes(`[GitHub Comment ID: ${githubCommentId}]`));
}
async function addGitLabComment(issueId: number, githubCommentId: number, owner: string, repo: string, issueNumber: number, author: string|undefined, body: string|undefined) {
try {
const githubCommentUrl = `https://github.com/${owner}/${repo}/issues/${issueNumber}#issuecomment-${githubCommentId}`;
const commentBody = `**${author}:** ${body}\n\n🔗 [Voir le commentaire sur GitHub](${githubCommentUrl})\n\n[GitHub Comment ID: ${githubCommentId}]`;
await gitlab.IssueNotes.create(GITLAB_REPO_ID, issueId, commentBody);
console.log(`Added comment to GitLab issue #${issueId} from GitHub comment #${githubCommentId}`);
} catch (error: any) {
console.error(`Error adding comment to GitLab issue #${issueId}:`, error.message);
}
}
// Synchroniser les issues spécifiques
async function syncIssues() {
log.info("Starting GitHub → GitLab issues sync...");
const gitlabIssues = await getGitLabIssues();
for (const issueUrl of issueUrls) {
const match = issueUrl.match(/https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
if (!match) {
log.error(`Invalid GitHub issue URL format: ${issueUrl}`);
continue;
}
const [, owner, repo, issueNumber] = match;
const issueData = await getGitHubIssue(owner, repo, parseInt(issueNumber));
if (!issueData) {
log.error(`Skipping issue #${issueNumber} (could not fetch details).`);
continue;
}
let existingGitLabIssue;
try {
existingGitLabIssue = gitlabIssues.find((issue: any) => issue.description? issue.description.includes(issueData?.html_url): false);
} catch (error: any) {
log.error(`Error with GitLab issue:`, error.message);
}
if (existingGitLabIssue) {
log.info(`Issue ${repo}#${issueNumber} already exists on GitLab, checking for updates...`);
const gitlabState = existingGitLabIssue.state;
const githubState = issueData.state;
if ((githubState === "closed" && gitlabState !== "closed") || (githubState === "open" && gitlabState !== "opened")) {
await updateGitLabIssueStatus(existingGitLabIssue.iid, githubState);
}
}else {
log.info(`Creating new issue on GitLab for GitHub issue ${repo}#${issueNumber}`);
existingGitLabIssue = await gitlab.Issues.create(GITLAB_REPO_ID, `[${repo}] ${issueData.title}`, {
description: `${issueData.body}\n\nOriginal Issue: [GitHub Issue #${issueNumber}](${issueData.html_url})`,
labels: issueData.labels.map((label: any) => label.name).join(',')
});
}
log.info(`Update comments for GitLab issue based on GitHub issue ${repo}#${issueNumber}:${existingGitLabIssue.iid}`);
const gitlabComments = await getGitLabComments(existingGitLabIssue.iid);
const githubComments = await getGitHubComments(owner, repo, parseInt(issueNumber));
for (const comment of githubComments) {
if (!isCommentAlreadySynced(gitlabComments, comment.id)) {
await addGitLabComment(existingGitLabIssue.iid, comment.id, owner, repo, parseInt(issueNumber), comment.user?.login, comment.body);
}
}
}
log.info("Sync completed!");
}
// Exécuter la synchronisation
syncIssues().catch(log.error("Error during sync:"));
\ No newline at end of file
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "node16", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */
"moduleResolution": "node16", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
"allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": false, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment