Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • jw_sonar
  • jw_sonar_backup
  • main
  • move-to-esm-only
  • update-dependencies
  • v5.0.0
  • v6.0.0
  • 2.0.0
  • 2.1.0
  • 2.2.0
  • 3.0.0
  • 3.0.1
  • 3.1.0
  • 3.2.0
  • 3.3.0
  • 3.4.0
  • 3.5.0
  • 4.0.0
  • 4.1.0
  • 4.1.1
  • 4.2.0
  • 5.0.0
  • 6.0.0-dev
  • v1.0.1
24 results

Target

Select target project
  • dojo_project/projects/pipelines/dojoexercisechecker
1 result
Select Git revision
  • jw_sonar
  • jw_sonar_backup
  • main
  • move-to-esm-only
  • update-dependencies
  • v5.0.0
  • v6.0.0
  • 2.0.0
  • 2.1.0
  • 2.2.0
  • 3.0.0
  • 3.0.1
  • 3.1.0
  • 3.2.0
  • 3.3.0
  • 3.4.0
  • 3.5.0
  • 4.0.0
  • 4.1.0
  • 4.1.1
  • 4.2.0
  • 5.0.0
  • 6.0.0-dev
  • v1.0.1
24 results
Show changes
Showing
with 3217 additions and 1668 deletions
// @ts-check
// @formatter:off
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config({
ignores: [ 'dist/*', 'node_modules/*', '.gitlab-ci.yml', 'eslint.config.mjs' ]
}, eslint.configs.recommended, ...tseslint.configs.recommendedTypeChecked, {
languageOptions: {
parserOptions: {
project: true, tsconfigRootDir: import.meta.dirname
}
}
}, {
plugins: {
'@typescript-eslint': tseslint.plugin
}, rules: {
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/require-await': 'off',
'@typescript-eslint/restrict-template-expressions': 'off',
'@typescript-eslint/no-floating-promises': 'off',
}
});
\ No newline at end of file
This diff is collapsed.
{
"name" : "dojo_exercise_checker",
"description" : "App that check an exercise of the Dojo project",
"version" : "3.5.0",
"version" : "6.0.0",
"license" : "AGPLv3",
"author" : "Michaël Minelli <dojo@minelli.me>",
"main" : "dist/app.js",
"bin" : {
"dirmanager": "./dist/app.js"
},
"pkg" : {
"pkg": {
"scripts": [],
"assets" : [
"assets": [
"node_modules/axios/dist/node/axios.cjs",
".env",
"config.env",
"assets/**/*"
],
"targets": [
......@@ -21,41 +22,44 @@
]
},
"scripts" : {
"dotenv:build": "npx dotenv-vault local build",
"dotenv:build": "npx dotenvx encrypt",
"lint" : "npx eslint .",
"genversion" : "npx genversion -s -e src/config/Version.ts",
"build" : "npm run genversion; npx tsc",
"start:dev" : "npm run genversion; npm run lint; npx ts-node src/app.ts",
"start:dev" : "npm run genversion; npm run lint; tsc --noEmit && npx tsx --no-warnings src/app.ts",
"test" : "echo \"Error: no test specified\" && exit 1"
},
"dependencies" : {
"axios" : "^1.6.5",
"boxen" : "^5.1.2",
"chalk" : "^4.1.2",
"dotenv" : "^16.3.1",
"dotenv-expand" : "^10.0.0",
"fs-extra" : "^11.2.0",
"http-status-codes" : "^2.3.0",
"json5" : "^2.2.3",
"ora" : "^5.4.1",
"tar-stream" : "^3.1.6",
"winston" : "^3.11.0",
"yaml" : "^2.3.4",
"zod" : "^3.22.4",
"zod-validation-error": "^3.0.0"
"@dotenvx/dotenvx" : "^0.45.0",
"@gitbeaker/core" : "^42.1.0",
"@gitbeaker/requester-utils": "^42.1.0",
"@gitbeaker/rest" : "^42.1.0",
"axios" : "^1.7.2",
"boxen" : "^5.1.2",
"chalk" : "^4.1.2",
"form-data" : "^4.0.2",
"fs-extra" : "^11.3.0",
"http-status-codes" : "^2.3.0",
"json5" : "^2.2.3",
"ora" : "^5.4.1",
"tar-stream" : "^3.1.7",
"winston" : "^3.17.0",
"winston-transport" : "^4.9.0",
"yaml" : "^2.7.0",
"zod" : "^3.24.2",
"zod-validation-error" : "^3.4.0"
},
"devDependencies": {
"@types/fs-extra" : "^11.0.4",
"@types/js-yaml" : "^4.0.9",
"@types/node" : "^18.19.8",
"@types/tar-stream" : "^3.1.3",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser" : "^6.19.0",
"dotenv-vault" : "^1.25.0",
"genversion" : "^3.2.0",
"pkg" : "^5.8.1",
"tiny-typed-emitter" : "^2.1.0",
"ts-node" : "^10.9.2",
"typescript" : "^5.3.3"
"@types/fs-extra" : "^11.0.4",
"@types/js-yaml" : "^4.0.9",
"@types/node" : "^18.19.76",
"@types/tar-stream" : "^3.1.3",
"genversion" : "^3.2.0",
"pkg" : "^5.8.1",
"tiny-typed-emitter": "^2.1.0",
"tsx" : "^4.19.3",
"ts-node" : "^10.9.2",
"typescript" : "~5.5.4",
"typescript-eslint" : "^7.18.0"
}
}
// Read from the .env file
// ATTENTION : These lines MUST be the first of this file (except for the path import)
import path = require('node:path');
import myEnv = require('dotenv');
import dotenvExpand = require('dotenv-expand');
dotenvExpand.expand(myEnv.config({
path : path.join(__dirname, '../.env'),
DOTENV_KEY: 'dotenv://:key_bebfddf18e3dd9a0bafafe0e383313f75add1da6fbe41ea5fde51f37ef1776aa@dotenv.local/vault/.env.vault?environment=development'
}));
require('./shared/helpers/TypeScriptExtensions'); // ATTENTION : This line MUST be the second of this file
import ClientsSharedConfig from './sharedByClients/config/ClientsSharedConfig';
import Styles from './types/Style';
import RecursiveFilesStats from './shared/helpers/recursiveFilesStats/RecursiveFilesStats';
import Toolbox from './shared/helpers/Toolbox';
import ExerciseCheckerError from './shared/types/Dojo/ExerciseCheckerError';
// ATTENTION : This line MUST be the first of this file
import './init.js';
import ClientsSharedConfig from './sharedByClients/config/ClientsSharedConfig.js';
import Styles from './types/Style.js';
import RecursiveFilesStats from './shared/helpers/recursiveFilesStats/RecursiveFilesStats.js';
import Toolbox from './shared/helpers/Toolbox.js';
import ExerciseCheckerError from './shared/types/Dojo/ExerciseCheckerError.js';
import fs from 'fs-extra';
import HttpManager from './managers/HttpManager';
import DojoBackendManager from './managers/DojoBackendManager';
import Config from './config/Config';
import ArchiveHelper from './shared/helpers/ArchiveHelper';
import ExerciseDockerCompose from './sharedByClients/helpers/Dojo/ExerciseDockerCompose';
import ExerciseResultsSanitizerAndValidator from './sharedByClients/helpers/Dojo/ExerciseResultsSanitizerAndValidator';
import ExerciseAssignment from './sharedByClients/models/ExerciseAssignment';
import ClientsSharedExerciseHelper from './sharedByClients/helpers/Dojo/ClientsSharedExerciseHelper';
import Icon from './shared/types/Icon';
(async () => {
HttpManager.registerAxiosInterceptor();
console.log(Styles.APP_NAME(`${ Config.appName } (version {{VERSION}})`));
let exerciseAssignment: ExerciseAssignment | undefined;
let exerciseDockerCompose: ExerciseDockerCompose;
let exerciseResultsValidation: ExerciseResultsSanitizerAndValidator;
let haveResultsVolume: boolean;
/*
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 1:
- Read the dojo assignment file from the assignment repository
- Download immutables files (maybe throw or show an error if the files have been modified ?)
*/
{
console.log(Styles.INFO(`${ Icon.INFO }️Checking the exercise's assignment and his immutable files`));
exerciseAssignment = await DojoBackendManager.getExerciseAssignment();
if ( !exerciseAssignment ) {
console.error(Styles.ERROR(`${ Icon.ERROR } Error while getting the exercise's assignment`));
process.exit(ExerciseCheckerError.EXERCISE_ASSIGNMENT_GET_ERROR);
}
import HttpManager from './managers/HttpManager.js';
import DojoBackendManager from './managers/DojoBackendManager.js';
import Config from './config/Config.js';
import ArchiveHelper from './shared/helpers/ArchiveHelper.js';
import ExerciseDockerCompose from './sharedByClients/helpers/Dojo/ExerciseDockerCompose.js';
import ExerciseResultsSanitizerAndValidator from './sharedByClients/helpers/Dojo/ExerciseResultsSanitizerAndValidator.js';
import ExerciseAssignment from './sharedByClients/models/ExerciseAssignment.js';
import ClientsSharedExerciseHelper from './sharedByClients/helpers/Dojo/ClientsSharedExerciseHelper.js';
import Icon from './shared/types/Icon.js';
import path from 'node:path';
import SharedAssignmentHelper from './shared/helpers/Dojo/SharedAssignmentHelper';
import Exercise from './sharedByClients/models/Exercise';
import SonarAnalyzer from './sharedByClients/helpers/Dojo/SonarAnalyzer';
import SharedConfig from './shared/config/SharedConfig';
let exercise: Exercise | undefined;
let exerciseAssignment: ExerciseAssignment | undefined;
let exerciseDockerCompose: ExerciseDockerCompose;
let exerciseResultsValidation: ExerciseResultsSanitizerAndValidator;
let haveResultsVolume: boolean;
let sonarSuccess: boolean | undefined = undefined;
/**
* Step 1:
* - Read the dojo assignment file from the assignment repository
* - Download immutables files (maybe throw or show an error if the files have been modified ?)
*/
async function downloadImmutablesFiles() {
console.log(Styles.INFO(`${ Icon.INFO }️ Checking the exercise's assignment and his immutable files`));
exercise = await DojoBackendManager.getExercise();
exerciseAssignment = await DojoBackendManager.getExerciseAssignment();
if ( !exerciseAssignment || !exercise ) {
console.error(Styles.ERROR(`${ Icon.ERROR } Error while getting the exercise or exercise's assignment`));
process.exit(ExerciseCheckerError.EXERCISE_ASSIGNMENT_GET_ERROR);
}
exerciseAssignment.immutable.forEach(immutableFile => {
exerciseAssignment.immutable.forEach(immutableFile => {
if ( typeof immutableFile.content === 'string' ) {
const filePath = path.join(Config.folders.project, immutableFile.file_path);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, immutableFile.content, { encoding: 'base64' });
});
haveResultsVolume = exerciseAssignment.assignmentFile.result.volume !== undefined;
}
}
});
haveResultsVolume = exerciseAssignment.assignmentFile.result.volume !== undefined;
}
/**
* Step 2:
* - Run sonar analysis
*/
async function runSonarAnalysis() {
if ( SharedConfig.sonar.enabled && exerciseAssignment!.assignment.useSonar ) {
console.log(Styles.INFO(`${ Icon.INFO } Running Sonar analysis on the exercise`));
const buildSuccess = SonarAnalyzer.buildDocker();
if ( !buildSuccess ) {
console.error(Styles.ERROR(`${ Icon.ERROR } Error while building the Docker image`));
process.exit(ExerciseCheckerError.SONAR_DOCKER_ERROR);
}
/*
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 2:
- Get override of docker-compose file (for override the volume by a bind mount to the results folder shared between dind and the host)
- Run docker-compose file
- Get logs from linked services
*/
{
let composeFileOverride: string[] = [];
const composeOverridePath: string = path.join(Config.folders.project, 'docker-compose-override.yml');
if ( haveResultsVolume ) {
const composeOverride = fs.readFileSync(path.join(__dirname, '../assets/docker-compose-override.yml'), 'utf8').replace('{{VOLUME_NAME}}', exerciseAssignment.assignmentFile.result.volume!).replace('{{MOUNT_PATH}}', Config.folders.resultsExercise);
fs.writeFileSync(composeOverridePath, composeOverride);
composeFileOverride = [ composeOverridePath ];
if ( SonarAnalyzer.mustRunBuild(exerciseAssignment!.assignment.language, exerciseAssignment!.assignmentFile.buildLine) ) {
const buildSuccess = SonarAnalyzer.runBuildStep(exerciseAssignment!.assignmentFile.buildLine!);
if ( !buildSuccess ) {
console.error(Styles.ERROR(`${ Icon.ERROR } Error while compiling exercise files`));
process.exit(ExerciseCheckerError.SONAR_BUILD_ERROR);
}
}
exerciseDockerCompose = new ExerciseDockerCompose(ClientsSharedConfig.dockerCompose.projectName, exerciseAssignment.assignmentFile, Config.folders.project, composeFileOverride);
sonarSuccess = SonarAnalyzer.runAnalysis(exercise!.sonarKey, exerciseAssignment!.assignment.language, exerciseAssignment!.assignmentFile.buildLine);
if ( !sonarSuccess && !exerciseAssignment!.assignment.allowSonarFailure ) {
console.error(Styles.ERROR(`${ Icon.ERROR } Sonar gate failed`));
process.exit(ExerciseCheckerError.SONAR_GATE_FAILED);
}
}
}
/**
* Step 3:
* - Get override of docker-compose file (for override the volume by a bind mount to the results folder shared between dind and the host)
* - Run docker-compose file
* - Get logs from linked services
*/
async function runDockerCompose() {
let composeFileOverride: string[] = [];
const composeOverridePath: string = path.join(Config.folders.project, 'docker-compose-override.yml');
if ( haveResultsVolume ) {
const composeOverride = fs.readFileSync(path.join(__dirname, '../assets/docker-compose-override.yml'), 'utf8').replace('{{VOLUME_NAME}}', exerciseAssignment!.assignmentFile.result.volume!).replace('{{MOUNT_PATH}}', Config.folders.resultsExercise);
fs.writeFileSync(composeOverridePath, composeOverride);
composeFileOverride = [ composeOverridePath ];
}
try {
await new Promise<void>((resolve, reject) => {
exerciseDockerCompose.events.on('step', (_name: string, message: string) => {
console.log(Styles.INFO(`${ Icon.INFO } ${ message }`));
});
exerciseDockerCompose = new ExerciseDockerCompose(ClientsSharedConfig.dockerCompose.projectName, exerciseAssignment!.assignmentFile, Config.folders.project, composeFileOverride);
exerciseDockerCompose.events.on('endStep', (_stepName: string, message: string, error: boolean) => {
if ( error ) {
console.error(Styles.ERROR(`${ Icon.ERROR } ${ message }`));
}
});
try {
await new Promise<void>((resolve, reject) => {
exerciseDockerCompose.events.on('step', (_name: string, message: string) => {
console.log(Styles.INFO(`${ Icon.INFO } ${ message }`));
});
exerciseDockerCompose.events.on('finished', (success: boolean) => {
success ? resolve() : reject();
});
exerciseDockerCompose.events.on('endStep', (_stepName: string, message: string, error: boolean) => {
if ( error ) {
console.error(Styles.ERROR(`${ Icon.ERROR } ${ message }`));
}
});
exerciseDockerCompose.run();
exerciseDockerCompose.events.on('finished', (success: boolean) => {
success ? resolve() : reject();
});
} catch ( error ) { /* empty */ }
fs.rmSync(composeOverridePath, { force: true });
fs.writeFileSync(path.join(Config.folders.resultsDojo, 'dockerComposeLogs.txt'), exerciseDockerCompose.allLogs);
exerciseDockerCompose.run();
});
} catch ( error ) { /* empty */ }
if ( !exerciseDockerCompose.success ) {
console.error(Styles.ERROR(`${ Icon.ERROR } Execution logs are available in artifacts`));
process.exit(exerciseDockerCompose.exitCode);
}
}
fs.rmSync(composeOverridePath, { force: true });
fs.writeFileSync(path.join(Config.folders.resultsDojo, 'dockerComposeLogs.txt'), exerciseDockerCompose.allLogs);
if ( !exerciseDockerCompose.success ) {
console.error(Styles.ERROR(`${ Icon.ERROR } Execution logs are available in artifacts`));
process.exit(exerciseDockerCompose.exitCode);
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 3: Check content requirements and content size
{
exerciseResultsValidation = new ExerciseResultsSanitizerAndValidator(Config.folders.resultsDojo, Config.folders.resultsExercise, exerciseDockerCompose.exitCode);
try {
await new Promise<void>((resolve) => {
exerciseResultsValidation.events.on('step', (_name: string, message: string) => {
console.log(Styles.INFO(`${ Icon.INFO } ${ message }`));
});
/**
* Step 4:
* - Check content requirements and content size
*/
async function checkExecutionContent() {
exerciseResultsValidation = new ExerciseResultsSanitizerAndValidator(Config.folders.resultsDojo, Config.folders.resultsExercise, exerciseDockerCompose.exitCode);
exerciseResultsValidation.events.on('endStep', (_stepName: string, message: string, error: boolean) => {
if ( error ) {
console.error(Styles.ERROR(`${ Icon.ERROR } ${ message }`));
}
});
try {
await new Promise<void>(resolve => {
exerciseResultsValidation.events.on('step', (_name: string, message: string) => {
console.log(Styles.INFO(`${ Icon.INFO } ${ message }`));
});
exerciseResultsValidation.events.on('finished', (success: boolean, exitCode: number) => {
if ( !success ) {
process.exit(exitCode);
}
exerciseResultsValidation.events.on('endStep', (_stepName: string, message: string, error: boolean) => {
if ( error ) {
console.error(Styles.ERROR(`${ Icon.ERROR } ${ message }`));
}
});
resolve();
});
exerciseResultsValidation.events.on('finished', (success: boolean, exitCode: number) => {
if ( !success ) {
process.exit(exitCode);
}
exerciseResultsValidation.run();
resolve();
});
} catch ( error ) { /* empty */ }
exerciseResultsValidation.run();
});
} catch ( error ) { /* empty */ }
}
/**
* Step 5:
* - Upload results
*/
async function uploadResults() {
try {
console.log(Styles.INFO(`${ Icon.INFO } Uploading results to the dojo server`));
const commit: Record<string, string> = {};
Toolbox.getKeysWithPrefix(process.env, 'CI_COMMIT_').forEach(key => {
commit[Toolbox.snakeToCamel(key.replace('CI_COMMIT_', ''))] = process.env[key] as string;
});
const files = await RecursiveFilesStats.explore(Config.folders.resultsVolume, {
replacePathByRelativeOne: true,
liteStats : true
});
await DojoBackendManager.sendResults(exerciseDockerCompose.exitCode, commit, exerciseResultsValidation.exerciseResults, sonarSuccess, files, await ArchiveHelper.getBase64(Config.folders.resultsVolume));
} catch ( error ) {
console.error(Styles.ERROR(`${ Icon.ERROR } Error while uploading the results`));
console.error(JSON.stringify(error));
process.exit(ExerciseCheckerError.UPLOAD);
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 4: Upload results
{
try {
console.log(Styles.INFO(`${ Icon.INFO } Uploading results to the dojo server`));
const commit: Record<string, string> = {};
Toolbox.getKeysWithPrefix(process.env, 'CI_COMMIT_').forEach(key => {
commit[Toolbox.snakeToCamel(key.replace('CI_COMMIT_', ''))] = process.env[key] as string;
});
/**
* Step 6:
* - Display results
* - Exit with container exit code
*/
async function displayResults() {
ClientsSharedExerciseHelper.displayExecutionResults(exerciseResultsValidation.exerciseResults, exerciseDockerCompose.exitCode, Styles, `\n\n${ Icon.INFO }️ More detailed logs and resources may be available in artifacts`);
const files = await RecursiveFilesStats.explore(Config.folders.resultsVolume, {
replacePathByRelativeOne: true,
liteStats : true
});
process.exit(exerciseDockerCompose.exitCode);
}
await DojoBackendManager.sendResults(exerciseDockerCompose.exitCode, commit, exerciseResultsValidation.exerciseResults, files, await ArchiveHelper.getBase64(Config.folders.resultsVolume));
} catch ( error ) {
console.error(Styles.ERROR(`${ Icon.ERROR } Error while uploading the results`));
console.error(JSON.stringify(error));
process.exit(ExerciseCheckerError.UPLOAD);
}
}
(async () => {
await Config.init();
/*
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 5:
- Display results
- Exit with container exit code
*/
{
ClientsSharedExerciseHelper.displayExecutionResults(exerciseResultsValidation.exerciseResults, exerciseDockerCompose.exitCode, Styles, `\n\n${ Icon.INFO }️ More detailed logs and resources may be available in artifacts`);
SharedAssignmentHelper.init(Config.gitlabManager);
process.exit(exerciseDockerCompose.exitCode);
}
HttpManager.registerAxiosInterceptor();
console.log(Styles.APP_NAME(`${ Config.appName } (version {{VERSION}})`));
await downloadImmutablesFiles();
await runSonarAnalysis();
await runDockerCompose();
await checkExecutionContent();
await uploadResults();
await displayResults();
})();
\ No newline at end of file
import fs from 'fs-extra';
import path from 'path';
import fs from 'fs-extra';
import path from 'path';
import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig';
import GitlabManager from '../managers/GitlabManager';
class Config {
public readonly appName: string;
public gitlabManager!: GitlabManager;
public readonly folders: {
public appName!: string;
public folders!: {
project: string; resultsVolume: string; resultsDojo: string; resultsExercise: string;
};
public readonly exercise: {
public exercise!: {
id: string; secret: string;
};
public readonly dockerhub: {
public dockerhub!: {
repositories: {
exerciseChecker: string
}
};
constructor() {
this.appName = process.env.APP_NAME || '';
async init() {
const apiUrl = process.env.API_URL ?? '';
await ClientsSharedConfig.init(apiUrl);
const getEnvVar = ClientsSharedConfig.envVarGetter();
this.gitlabManager = new GitlabManager(ClientsSharedConfig.gitlab.URL, ClientsSharedConfig.login.gitlab.client.id, ClientsSharedConfig.login.gitlab.url.redirect, ClientsSharedConfig.login.gitlab.url.token);
this.appName = getEnvVar('APP_NAME', '');
this.folders = {
project : process.env.PROJECT_FOLDER?.convertWithEnvVars() ?? './',
resultsVolume : process.env.EXERCISE_RESULTS_VOLUME?.convertWithEnvVars() ?? '',
resultsDojo : path.join(process.env.EXERCISE_RESULTS_VOLUME?.convertWithEnvVars() ?? '', 'Dojo/'),
resultsExercise: path.join(process.env.EXERCISE_RESULTS_VOLUME?.convertWithEnvVars() ?? '', 'Exercise/')
project : getEnvVar('PROJECT_FOLDER', './').convertWithEnvVars(),
resultsVolume : getEnvVar('EXERCISE_RESULTS_VOLUME', '').convertWithEnvVars(),
resultsDojo : path.join(getEnvVar('EXERCISE_RESULTS_VOLUME', '').convertWithEnvVars(), 'Dojo/'),
resultsExercise: path.join(getEnvVar('EXERCISE_RESULTS_VOLUME', '').convertWithEnvVars(), 'Exercise/')
};
this.resetResultsVolume();
this.exercise = {
id : process.env.DOJO_EXERCISE_ID || '',
secret: process.env.DOJO_SECRET || ''
id : getEnvVar('DOJO_EXERCISE_ID', ''),
secret: getEnvVar('DOJO_SECRET', '')
};
this.dockerhub = {
repositories: {
exerciseChecker: process.env.DOCKERHUB_EXERCISE_CHECKER_REPOSITORY || ''
exerciseChecker: getEnvVar('DOCKERHUB_EXERCISE_CHECKER_REPOSITORY', '')
}
};
}
......
import path from 'node:path';
import dotenv from 'dotenv';
import dotenvExpand from 'dotenv-expand';
import './shared/helpers/TypeScriptExtensions.js';
dotenvExpand.expand(dotenv.config({
path : path.join(__dirname, '../.env'),
DOTENV_KEY: 'dotenv://:key_bebfddf18e3dd9a0bafafe0e383313f75add1da6fbe41ea5fde51f37ef1776aa@dotenv.local/vault/.env.vault?environment=development'
}));
dotenvExpand.expand(dotenv.config({ path: path.join(__dirname, '../config.env') }));
\ No newline at end of file
import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig';
import axios from 'axios';
import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse';
import ExerciseAssignment from '../sharedByClients/models/ExerciseAssignment';
import Config from '../config/Config';
import ExerciseResultsFile from '../shared/types/Dojo/ExerciseResultsFile';
import ApiRoute from '../sharedByClients/types/Dojo/ApiRoute';
import { IFileDirStat } from '../shared/helpers/recursiveFilesStats/RecursiveFilesStats';
import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse.js';
import ExerciseAssignment from '../sharedByClients/models/ExerciseAssignment.js';
import Config from '../config/Config.js';
import ExerciseResultsFile from '../shared/types/Dojo/ExerciseResultsFile.js';
import ApiRoute from '../sharedByClients/types/Dojo/ApiRoute.js';
import { IFileDirStat } from '../shared/helpers/recursiveFilesStats/RecursiveFilesStats.js';
import DojoBackendHelper from '../sharedByClients/helpers/Dojo/DojoBackendHelper.js';
import Exercise from '../sharedByClients/models/Exercise';
class DojoBackendManager {
public getApiUrl(route: ApiRoute): string {
return `${ ClientsSharedConfig.apiURL }${ route }`;
public async getExercise(): Promise<Exercise | undefined> {
try {
return (await axios.get<DojoBackendResponse<Exercise>>(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_GET_DELETE).replace('{{id}}', Config.exercise.id))).data.data;
} catch ( error ) {
return undefined;
}
}
public async getExerciseAssignment(): Promise<ExerciseAssignment | undefined> {
try {
return (await axios.get<DojoBackendResponse<ExerciseAssignment>>(this.getApiUrl(ApiRoute.EXERCISE_ASSIGNMENT).replace('{{id}}', Config.exercise.id))).data.data;
return (await axios.get<DojoBackendResponse<ExerciseAssignment>>(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_ASSIGNMENT, { exerciseIdOrUrl: Config.exercise.id }))).data.data;
} catch ( error ) {
return undefined;
}
}
public async sendResults(exitCode: number, commit: Record<string, string>, results: ExerciseResultsFile, files: Array<IFileDirStat>, archiveBase64: string): Promise<void> {
public async sendResults(exitCode: number, commit: Record<string, string>, results: ExerciseResultsFile, sonarGatePass: boolean | undefined, files: Array<IFileDirStat>, archiveBase64: string): Promise<void> {
await axios.post(this.getApiUrl(ApiRoute.EXERCISE_RESULTS).replace('{{id}}', Config.exercise.id), {
exitCode : exitCode,
commit : JSON.stringify(commit),
results : JSON.stringify(results),
sonarGatePass: sonarGatePass?.toString() ?? "",
files : JSON.stringify(files),
archiveBase64: archiveBase64
});
......
import SharedGitlabManager from '../shared/managers/SharedGitlabManager.js';
// NOT USED
// File present only for prevent errors from shared submodules
class GitlabManager extends SharedGitlabManager {
constructor(public gitlabUrl: string, clientId?: string, urlRedirect?: string, urlToken?: string) {
super(gitlabUrl, '', clientId, urlRedirect, urlToken);
}
}
export default GitlabManager;
\ No newline at end of file
import axios, { AxiosRequestHeaders } from 'axios';
import FormData from 'form-data';
import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig';
import Config from '../config/Config';
import { version } from '../config/Version';
import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig.js';
import Config from '../config/Config.js';
import { version } from '../config/Version.js';
import boxen from 'boxen';
import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode';
import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse';
import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode.js';
import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse.js';
import { StatusCodes } from 'http-status-codes';
......@@ -31,15 +31,15 @@ class HttpManager {
}
private registerRequestInterceptor() {
axios.interceptors.request.use((config) => {
axios.interceptors.request.use(config => {
if ( config.data instanceof FormData ) {
config.headers = { ...config.headers, ...(config.data as FormData).getHeaders() } as AxiosRequestHeaders;
config.headers = { ...config.headers, ...config.data.getHeaders() } as AxiosRequestHeaders;
}
if ( config.url && (config.url.indexOf(ClientsSharedConfig.apiURL) !== -1) ) {
config.headers['Accept-Encoding'] = 'gzip';
if ( config.data && Object.keys(config.data).length > 0 ) {
if ( config.data && Object.keys(config.data as { [key: string]: unknown }).length > 0 ) {
config.headers['Content-Type'] = 'multipart/form-data';
}
......@@ -54,18 +54,16 @@ class HttpManager {
}
private registerResponseInterceptor() {
axios.interceptors.response.use((response) => {
return response;
}, (error) => {
axios.interceptors.response.use(response => response, error => {
if ( error.response ) {
if ( error.response.status === StatusCodes.METHOD_NOT_ALLOWED && error.response.data ) {
const data: DojoBackendResponse<void> = error.response.data;
switch ( data.code ) {
case DojoStatusCode.CLIENT_NOT_SUPPORTED:
case DojoStatusCode.CLIENT_NOT_SUPPORTED.valueOf():
this.requestError('Client not recognized by the server. Please contact the administrator.');
break;
case DojoStatusCode.CLIENT_VERSION_NOT_SUPPORTED:
case DojoStatusCode.CLIENT_VERSION_NOT_SUPPORTED.valueOf():
this.requestError(`ExerciseChecker version not supported by the server.\nPlease check that the CI/CD pipeline use the "${ Config.dockerhub.repositories.exerciseChecker }:latest" image.\nIf yes, try again later and if the problem persists, please contact the administrator.`);
break;
default:
......
Subproject commit 89f3579ca9009f793742170928d808ab4c35d931
Subproject commit 937081e68f6127b669daca30e57c43e73b9c96c9
Subproject commit 098c6d20f6ed84240c086b979b56afd598fdfea4
Subproject commit eedbe869a561f6e9a3b02fa9374cee425af27946
......@@ -6,11 +6,20 @@
"target" : "ES2022",
"module" : "commonjs",
"sourceMap" : true,
"noImplicitAny" : true,
"esModuleInterop" : true,
"moduleResolution": "node",
"noImplicitAny" : true
"lib" : [
"ES2022",
"DOM"
],
"types" : [
"node"
]
},
"exclude" : [
"node_modules"
],
"include" : [
"src/**/*.ts"
]
}
\ No newline at end of file
# DojoExerciceRunner
\ No newline at end of file
# Documentation of `The Dojo Exercise Checker`
All documentations are available on the [Dojo documentation website](https://www.hepiapp.ch/) : https://www.hepiapp.ch/.
\ No newline at end of file
# Documentation of `The Dojo Exercise Checker`
All documentations are available on the [Dojo documentation website](https://www.hepiapp.ch/) : https://www.hepiapp.ch/.
\ No newline at end of file
#!/bin/bash
# Define color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
DOCKERFILE=Dockerfile_ExerciseChecker
PROJECT_FOLDER=ExerciseChecker
test_if_command_unsuccessful() {
local status=$1
local title=$2
local text_if_failed=$3
if [ "$status" -ne 0 ]; then
echo -e "${title}: ${RED}${text_if_failed}: $status${NC}\n"
exit 1
fi
}
##########################################################
# Function to prompt for input if not provided
prompt_if_empty() {
local var_name=$1
local prompt_message=$2
local var_value=${!var_name}
if [ -z "$var_value" ]; then
read -p "$prompt_message: " var_value
export "$var_name"="$var_value"
fi
}
# Parse parameters
while [ "$1" != "" ]; do
case $1 in
--dockerhub-user ) shift
DOCKER_REGISTRY_USER=$1
;;
--dockerhub-password ) shift
DOCKER_REGISTRY_PASSWORD=$1
;;
--dockerhub-repo ) shift
DOCKER_REGISTRY_IMAGE=$1
;;
--backend-url ) shift
API_URL=$1
;;
* ) echo -e "${RED}Invalid parameter: $1${NC}"
exit 1
esac
shift
done
# Prompt for values if not provided
prompt_if_empty "DOCKER_REGISTRY_USER" "Docker Hub user"
prompt_if_empty "DOCKER_REGISTRY_PASSWORD" "Docker Hub password"
prompt_if_empty "DOCKER_REGISTRY_IMAGE" "Docker Hub repository"
prompt_if_empty "API_URL" "Backend url"
##########################################################
# Function to check if a command exists
check_dependency() {
local cmd=$1
if ! command -v $cmd &> /dev/null; then
echo -e "${RED}Error: $cmd is not installed.${NC}"
exit 1
fi
}
# Check for dependencies
echo -e "Dependencies: ${YELLOW}Checking...${NC}"
check_dependency jq
check_dependency docker
check_dependency sed
echo -e "Dependencies: ${GREEN}OK${NC}\n"
VERSION=$(jq -r .version $PROJECT_FOLDER/package.json)
CONTAINER_IMAGE=$DOCKER_REGISTRY_IMAGE:$VERSION
##########################################################
# Login to Docker Hub
echo -e "Docker Hub: ${YELLOW}Login in...${NC}"
echo "$DOCKER_REGISTRY_PASSWORD" | docker login -u "$DOCKER_REGISTRY_USER" --password-stdin "$DOCKER_REGISTRY" > /dev/null
test_if_command_unsuccessful $? "Docker Hub" "FAILED to login in"
echo -e "Docker Hub: ${GREEN}Logged in${NC}\n"
##########################################################
echo -e "Project files: ${YELLOW}Configuring...${NC}"
(
cd $PROJECT_FOLDER || exit 1
sed -i -r "s/\{\{VERSION\}\}/${VERSION}/g" src/app.ts 2> /dev/null
if [ $? -ne 0 ]; then # If on macOS
sed -i -r "s/{{VERSION}}/${VERSION}/g" src/app.ts 2> /dev/null
fi
sed -i -r "s/(DOTENV_KEY[ ]*:[ ]*[\'\"\`])[^'\"\`]*([\'\"\`])([ ]*\,)?//g" src/init.ts
sed -i -r "s/,[\ \n]*\}/\}/g" src/init.ts
# Write the content to the file
cat <<EOL > ".env"
##################### App env vars
API_URL=${API_URL}
DOCKERHUB_EXERCISE_CHECKER_REPOSITORY=${DOCKER_REGISTRY_IMAGE}
EOL
)
echo -e "Project files: ${GREEN}Configured${NC}\n"
##########################################################
echo -e "Docker: ${YELLOW}Building/Uploading to Docker Hub...${NC}"
docker buildx create --use --platform=linux/amd64 > /dev/null 2> /dev/null
test_if_command_unsuccessful $? "Docker" "FAILED to initialize builder"
docker buildx build --pull --platform=linux/amd64 --file $DOCKERFILE --push --tag "$CONTAINER_IMAGE" . > /dev/null 2> /dev/null
test_if_command_unsuccessful $? "Docker" "FAILED to build"
docker buildx imagetools create "$CONTAINER_IMAGE" --tag "$DOCKER_REGISTRY_IMAGE":latest > /dev/null 2> /dev/null
test_if_command_unsuccessful $? "Docker" "FAILED to set latest tag"
echo -e "Docker: ${GREEN}Uploaded to Docker Hub${NC}\n"
\ No newline at end of file
sonar.projectKey=DojoExerciseChecker
sonar.qualitygate.wait=true
# Node needed to analyze JS/TS files
FROM node:18-slim AS node_base
FROM gcc:14
ARG SONAR_HOST_URL=https://isc-sonar.edu.hesge.ch
RUN apt update && apt install -y curl unzip build-essential make g++ clang git-core openssl libssl-dev && apt clean
# Download sonar tools
RUN mkdir -p /sonar && \
curl -sSLo sonar-scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-5.0.1.3006-linux.zip && \
unzip -o sonar-scanner.zip -d /sonar && \
mv /sonar/sonar-scanner-5.0.1.3006-linux/* /sonar/ && \
ln -s /sonar/bin/sonar-scanner /usr/local/bin/sonar-scanner && \
curl --insecure -sSLo build-wrapper-linux-x86.zip "$SONAR_HOST_URL/static/cpp/build-wrapper-linux-x86.zip" && \
unzip -o build-wrapper-linux-x86.zip -d /tmp && \
mv /tmp/build-wrapper-linux-x86/* /usr/local/bin/ && \
rm build-wrapper-linux-x86.zip sonar-scanner.zip
COPY ./cacerts /tmp/cacerts
ENV SONAR_SCANNER_OPTS="-Djavax.net.ssl.trustStore=/tmp/cacerts"
RUN mkdir -p /usr/src && \
useradd -m sonar && \
chown sonar:sonar /usr/src && \
chmod 744 /tmp/cacerts
USER sonar
WORKDIR /usr/src
COPY --from=node_base /usr/local/bin /usr/local/bin
COPY --from=node_base /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/npm
File added