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", "name" : "dojo_exercise_checker",
"description" : "App that check an exercise of the Dojo project", "description" : "App that check an exercise of the Dojo project",
"version" : "3.5.0", "version" : "6.0.0",
"license" : "AGPLv3", "license" : "AGPLv3",
"author" : "Michaël Minelli <dojo@minelli.me>", "author" : "Michaël Minelli <dojo@minelli.me>",
"main" : "dist/app.js", "main" : "dist/app.js",
"bin" : { "bin" : {
"dirmanager": "./dist/app.js" "dirmanager": "./dist/app.js"
}, },
"pkg" : { "pkg": {
"scripts": [], "scripts": [],
"assets" : [ "assets": [
"node_modules/axios/dist/node/axios.cjs", "node_modules/axios/dist/node/axios.cjs",
".env", ".env",
"config.env",
"assets/**/*" "assets/**/*"
], ],
"targets": [ "targets": [
...@@ -21,41 +22,44 @@ ...@@ -21,41 +22,44 @@
] ]
}, },
"scripts" : { "scripts" : {
"dotenv:build": "npx dotenv-vault local build", "dotenv:build": "npx dotenvx encrypt",
"lint" : "npx eslint .", "lint" : "npx eslint .",
"genversion" : "npx genversion -s -e src/config/Version.ts", "genversion" : "npx genversion -s -e src/config/Version.ts",
"build" : "npm run genversion; npx tsc", "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" "test" : "echo \"Error: no test specified\" && exit 1"
}, },
"dependencies" : { "dependencies" : {
"axios" : "^1.6.5", "@dotenvx/dotenvx" : "^0.45.0",
"boxen" : "^5.1.2", "@gitbeaker/core" : "^42.1.0",
"chalk" : "^4.1.2", "@gitbeaker/requester-utils": "^42.1.0",
"dotenv" : "^16.3.1", "@gitbeaker/rest" : "^42.1.0",
"dotenv-expand" : "^10.0.0", "axios" : "^1.7.2",
"fs-extra" : "^11.2.0", "boxen" : "^5.1.2",
"http-status-codes" : "^2.3.0", "chalk" : "^4.1.2",
"json5" : "^2.2.3", "form-data" : "^4.0.2",
"ora" : "^5.4.1", "fs-extra" : "^11.3.0",
"tar-stream" : "^3.1.6", "http-status-codes" : "^2.3.0",
"winston" : "^3.11.0", "json5" : "^2.2.3",
"yaml" : "^2.3.4", "ora" : "^5.4.1",
"zod" : "^3.22.4", "tar-stream" : "^3.1.7",
"zod-validation-error": "^3.0.0" "winston" : "^3.17.0",
"winston-transport" : "^4.9.0",
"yaml" : "^2.7.0",
"zod" : "^3.24.2",
"zod-validation-error" : "^3.4.0"
}, },
"devDependencies": { "devDependencies": {
"@types/fs-extra" : "^11.0.4", "@types/fs-extra" : "^11.0.4",
"@types/js-yaml" : "^4.0.9", "@types/js-yaml" : "^4.0.9",
"@types/node" : "^18.19.8", "@types/node" : "^18.19.76",
"@types/tar-stream" : "^3.1.3", "@types/tar-stream" : "^3.1.3",
"@typescript-eslint/eslint-plugin": "^6.19.0", "genversion" : "^3.2.0",
"@typescript-eslint/parser" : "^6.19.0", "pkg" : "^5.8.1",
"dotenv-vault" : "^1.25.0", "tiny-typed-emitter": "^2.1.0",
"genversion" : "^3.2.0", "tsx" : "^4.19.3",
"pkg" : "^5.8.1", "ts-node" : "^10.9.2",
"tiny-typed-emitter" : "^2.1.0", "typescript" : "~5.5.4",
"ts-node" : "^10.9.2", "typescript-eslint" : "^7.18.0"
"typescript" : "^5.3.3"
} }
} }
// Read from the .env file // ATTENTION : This line MUST be the first of this file
// ATTENTION : These lines MUST be the first of this file (except for the path import) import './init.js';
import path = require('node:path'); import ClientsSharedConfig from './sharedByClients/config/ClientsSharedConfig.js';
import myEnv = require('dotenv'); import Styles from './types/Style.js';
import dotenvExpand = require('dotenv-expand'); import RecursiveFilesStats from './shared/helpers/recursiveFilesStats/RecursiveFilesStats.js';
import Toolbox from './shared/helpers/Toolbox.js';
import ExerciseCheckerError from './shared/types/Dojo/ExerciseCheckerError.js';
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';
import fs from 'fs-extra'; import fs from 'fs-extra';
import HttpManager from './managers/HttpManager'; import HttpManager from './managers/HttpManager.js';
import DojoBackendManager from './managers/DojoBackendManager'; import DojoBackendManager from './managers/DojoBackendManager.js';
import Config from './config/Config'; import Config from './config/Config.js';
import ArchiveHelper from './shared/helpers/ArchiveHelper'; import ArchiveHelper from './shared/helpers/ArchiveHelper.js';
import ExerciseDockerCompose from './sharedByClients/helpers/Dojo/ExerciseDockerCompose'; import ExerciseDockerCompose from './sharedByClients/helpers/Dojo/ExerciseDockerCompose.js';
import ExerciseResultsSanitizerAndValidator from './sharedByClients/helpers/Dojo/ExerciseResultsSanitizerAndValidator'; import ExerciseResultsSanitizerAndValidator from './sharedByClients/helpers/Dojo/ExerciseResultsSanitizerAndValidator.js';
import ExerciseAssignment from './sharedByClients/models/ExerciseAssignment'; import ExerciseAssignment from './sharedByClients/models/ExerciseAssignment.js';
import ClientsSharedExerciseHelper from './sharedByClients/helpers/Dojo/ClientsSharedExerciseHelper'; import ClientsSharedExerciseHelper from './sharedByClients/helpers/Dojo/ClientsSharedExerciseHelper.js';
import Icon from './shared/types/Icon'; import Icon from './shared/types/Icon.js';
import path from 'node:path';
import SharedAssignmentHelper from './shared/helpers/Dojo/SharedAssignmentHelper';
(async () => { import Exercise from './sharedByClients/models/Exercise';
HttpManager.registerAxiosInterceptor(); import SonarAnalyzer from './sharedByClients/helpers/Dojo/SonarAnalyzer';
import SharedConfig from './shared/config/SharedConfig';
console.log(Styles.APP_NAME(`${ Config.appName } (version {{VERSION}})`));
let exerciseAssignment: ExerciseAssignment | undefined; let exercise: Exercise | undefined;
let exerciseDockerCompose: ExerciseDockerCompose; let exerciseAssignment: ExerciseAssignment | undefined;
let exerciseResultsValidation: ExerciseResultsSanitizerAndValidator; let exerciseDockerCompose: ExerciseDockerCompose;
let exerciseResultsValidation: ExerciseResultsSanitizerAndValidator;
let haveResultsVolume: boolean;
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 ?) * 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(); async function downloadImmutablesFiles() {
if ( !exerciseAssignment ) { console.log(Styles.INFO(`${ Icon.INFO }️ Checking the exercise's assignment and his immutable files`));
console.error(Styles.ERROR(`${ Icon.ERROR } Error while getting the exercise's assignment`)); exercise = await DojoBackendManager.getExercise();
process.exit(ExerciseCheckerError.EXERCISE_ASSIGNMENT_GET_ERROR); 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); const filePath = path.join(Config.folders.project, immutableFile.file_path);
fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, immutableFile.content, { encoding: 'base64' }); 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);
}
/* if ( SonarAnalyzer.mustRunBuild(exerciseAssignment!.assignment.language, exerciseAssignment!.assignmentFile.buildLine) ) {
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 2: const buildSuccess = SonarAnalyzer.runBuildStep(exerciseAssignment!.assignmentFile.buildLine!);
- Get override of docker-compose file (for override the volume by a bind mount to the results folder shared between dind and the host) if ( !buildSuccess ) {
- Run docker-compose file console.error(Styles.ERROR(`${ Icon.ERROR } Error while compiling exercise files`));
- Get logs from linked services process.exit(ExerciseCheckerError.SONAR_BUILD_ERROR);
*/ }
{
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 ];
} }
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 { exerciseDockerCompose = new ExerciseDockerCompose(ClientsSharedConfig.dockerCompose.projectName, exerciseAssignment!.assignmentFile, Config.folders.project, composeFileOverride);
await new Promise<void>((resolve, reject) => {
exerciseDockerCompose.events.on('step', (_name: string, message: string) => {
console.log(Styles.INFO(`${ Icon.INFO } ${ message }`));
});
exerciseDockerCompose.events.on('endStep', (_stepName: string, message: string, error: boolean) => { try {
if ( error ) { await new Promise<void>((resolve, reject) => {
console.error(Styles.ERROR(`${ Icon.ERROR } ${ message }`)); exerciseDockerCompose.events.on('step', (_name: string, message: string) => {
} console.log(Styles.INFO(`${ Icon.INFO } ${ message }`));
}); });
exerciseDockerCompose.events.on('finished', (success: boolean) => { exerciseDockerCompose.events.on('endStep', (_stepName: string, message: string, error: boolean) => {
success ? resolve() : reject(); 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 }); exerciseDockerCompose.run();
fs.writeFileSync(path.join(Config.folders.resultsDojo, 'dockerComposeLogs.txt'), exerciseDockerCompose.allLogs); });
} catch ( error ) { /* empty */ }
if ( !exerciseDockerCompose.success ) { fs.rmSync(composeOverridePath, { force: true });
console.error(Styles.ERROR(`${ Icon.ERROR } Execution logs are available in artifacts`)); fs.writeFileSync(path.join(Config.folders.resultsDojo, 'dockerComposeLogs.txt'), exerciseDockerCompose.allLogs);
process.exit(exerciseDockerCompose.exitCode);
}
}
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) => { * Step 4:
exerciseResultsValidation.events.on('step', (_name: string, message: string) => { * - Check content requirements and content size
console.log(Styles.INFO(`${ Icon.INFO } ${ message }`)); */
}); async function checkExecutionContent() {
exerciseResultsValidation = new ExerciseResultsSanitizerAndValidator(Config.folders.resultsDojo, Config.folders.resultsExercise, exerciseDockerCompose.exitCode);
exerciseResultsValidation.events.on('endStep', (_stepName: string, message: string, error: boolean) => { try {
if ( error ) { await new Promise<void>(resolve => {
console.error(Styles.ERROR(`${ Icon.ERROR } ${ message }`)); exerciseResultsValidation.events.on('step', (_name: string, message: string) => {
} console.log(Styles.INFO(`${ Icon.INFO } ${ message }`));
}); });
exerciseResultsValidation.events.on('finished', (success: boolean, exitCode: number) => { exerciseResultsValidation.events.on('endStep', (_stepName: string, message: string, error: boolean) => {
if ( !success ) { if ( error ) {
process.exit(exitCode); 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 /**
{ * Step 6:
try { * - Display results
console.log(Styles.INFO(`${ Icon.INFO } Uploading results to the dojo server`)); * - Exit with container exit code
const commit: Record<string, string> = {}; */
Toolbox.getKeysWithPrefix(process.env, 'CI_COMMIT_').forEach(key => { async function displayResults() {
commit[Toolbox.snakeToCamel(key.replace('CI_COMMIT_', ''))] = process.env[key] as string; 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, { process.exit(exerciseDockerCompose.exitCode);
replacePathByRelativeOne: true, }
liteStats : true
});
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();
/* SharedAssignmentHelper.init(Config.gitlabManager);
//////////////////////////////////////////////////////////////////////////////////////////////////////////// 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`);
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 fs from 'fs-extra';
import path from 'path'; import path from 'path';
import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig';
import GitlabManager from '../managers/GitlabManager';
class Config { 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; project: string; resultsVolume: string; resultsDojo: string; resultsExercise: string;
}; };
public readonly exercise: { public exercise!: {
id: string; secret: string; id: string; secret: string;
}; };
public readonly dockerhub: { public dockerhub!: {
repositories: { repositories: {
exerciseChecker: string exerciseChecker: string
} }
}; };
constructor() { async init() {
this.appName = process.env.APP_NAME || ''; 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 = { this.folders = {
project : process.env.PROJECT_FOLDER?.convertWithEnvVars() ?? './', project : getEnvVar('PROJECT_FOLDER', './').convertWithEnvVars(),
resultsVolume : process.env.EXERCISE_RESULTS_VOLUME?.convertWithEnvVars() ?? '', resultsVolume : getEnvVar('EXERCISE_RESULTS_VOLUME', '').convertWithEnvVars(),
resultsDojo : path.join(process.env.EXERCISE_RESULTS_VOLUME?.convertWithEnvVars() ?? '', 'Dojo/'), resultsDojo : path.join(getEnvVar('EXERCISE_RESULTS_VOLUME', '').convertWithEnvVars(), 'Dojo/'),
resultsExercise: path.join(process.env.EXERCISE_RESULTS_VOLUME?.convertWithEnvVars() ?? '', 'Exercise/') resultsExercise: path.join(getEnvVar('EXERCISE_RESULTS_VOLUME', '').convertWithEnvVars(), 'Exercise/')
}; };
this.resetResultsVolume(); this.resetResultsVolume();
this.exercise = { this.exercise = {
id : process.env.DOJO_EXERCISE_ID || '', id : getEnvVar('DOJO_EXERCISE_ID', ''),
secret: process.env.DOJO_SECRET || '' secret: getEnvVar('DOJO_SECRET', '')
}; };
this.dockerhub = { this.dockerhub = {
repositories: { 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 axios from 'axios';
import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse'; import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse.js';
import ExerciseAssignment from '../sharedByClients/models/ExerciseAssignment'; import ExerciseAssignment from '../sharedByClients/models/ExerciseAssignment.js';
import Config from '../config/Config'; import Config from '../config/Config.js';
import ExerciseResultsFile from '../shared/types/Dojo/ExerciseResultsFile'; import ExerciseResultsFile from '../shared/types/Dojo/ExerciseResultsFile.js';
import ApiRoute from '../sharedByClients/types/Dojo/ApiRoute'; import ApiRoute from '../sharedByClients/types/Dojo/ApiRoute.js';
import { IFileDirStat } from '../shared/helpers/recursiveFilesStats/RecursiveFilesStats'; import { IFileDirStat } from '../shared/helpers/recursiveFilesStats/RecursiveFilesStats.js';
import DojoBackendHelper from '../sharedByClients/helpers/Dojo/DojoBackendHelper.js';
import Exercise from '../sharedByClients/models/Exercise';
class DojoBackendManager { class DojoBackendManager {
public getApiUrl(route: ApiRoute): string { public async getExercise(): Promise<Exercise | undefined> {
return `${ ClientsSharedConfig.apiURL }${ route }`; 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> { public async getExerciseAssignment(): Promise<ExerciseAssignment | undefined> {
try { 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 ) { } catch ( error ) {
return undefined; 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), { await axios.post(this.getApiUrl(ApiRoute.EXERCISE_RESULTS).replace('{{id}}', Config.exercise.id), {
exitCode : exitCode, exitCode : exitCode,
commit : JSON.stringify(commit), commit : JSON.stringify(commit),
results : JSON.stringify(results), results : JSON.stringify(results),
sonarGatePass: sonarGatePass?.toString() ?? "",
files : JSON.stringify(files), files : JSON.stringify(files),
archiveBase64: archiveBase64 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 axios, { AxiosRequestHeaders } from 'axios';
import FormData from 'form-data'; import FormData from 'form-data';
import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig'; import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig.js';
import Config from '../config/Config'; import Config from '../config/Config.js';
import { version } from '../config/Version'; import { version } from '../config/Version.js';
import boxen from 'boxen'; import boxen from 'boxen';
import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode.js';
import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse'; import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse.js';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
...@@ -31,15 +31,15 @@ class HttpManager { ...@@ -31,15 +31,15 @@ class HttpManager {
} }
private registerRequestInterceptor() { private registerRequestInterceptor() {
axios.interceptors.request.use((config) => { axios.interceptors.request.use(config => {
if ( config.data instanceof FormData ) { 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) ) { if ( config.url && (config.url.indexOf(ClientsSharedConfig.apiURL) !== -1) ) {
config.headers['Accept-Encoding'] = 'gzip'; 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'; config.headers['Content-Type'] = 'multipart/form-data';
} }
...@@ -54,18 +54,16 @@ class HttpManager { ...@@ -54,18 +54,16 @@ class HttpManager {
} }
private registerResponseInterceptor() { private registerResponseInterceptor() {
axios.interceptors.response.use((response) => { axios.interceptors.response.use(response => response, error => {
return response;
}, (error) => {
if ( error.response ) { if ( error.response ) {
if ( error.response.status === StatusCodes.METHOD_NOT_ALLOWED && error.response.data ) { if ( error.response.status === StatusCodes.METHOD_NOT_ALLOWED && error.response.data ) {
const data: DojoBackendResponse<void> = error.response.data; const data: DojoBackendResponse<void> = error.response.data;
switch ( data.code ) { 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.'); this.requestError('Client not recognized by the server. Please contact the administrator.');
break; 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.`); 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; break;
default: default:
......
Subproject commit 89f3579ca9009f793742170928d808ab4c35d931 Subproject commit 937081e68f6127b669daca30e57c43e73b9c96c9
Subproject commit 098c6d20f6ed84240c086b979b56afd598fdfea4 Subproject commit eedbe869a561f6e9a3b02fa9374cee425af27946
...@@ -6,11 +6,20 @@ ...@@ -6,11 +6,20 @@
"target" : "ES2022", "target" : "ES2022",
"module" : "commonjs", "module" : "commonjs",
"sourceMap" : true, "sourceMap" : true,
"noImplicitAny" : true,
"esModuleInterop" : true, "esModuleInterop" : true,
"moduleResolution": "node", "lib" : [
"noImplicitAny" : true "ES2022",
"DOM"
],
"types" : [
"node"
]
}, },
"exclude" : [ "exclude" : [
"node_modules" "node_modules"
],
"include" : [
"src/**/*.ts"
] ]
} }
\ No newline at end of file
# DojoExerciceRunner # Documentation of `The Dojo Exercise Checker`
\ No newline at end of file
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