import CommanderCommand from '../../CommanderCommand.js'; import ora from 'ora'; import SharedConfig from '../../../shared/config/SharedConfig'; import AccessesHelper from '../../../helpers/AccessesHelper.js'; import Assignment from '../../../sharedByClients/models/Assignment.js'; import DojoBackendManager from '../../../managers/DojoBackendManager.js'; import Toolbox from '../../../shared/helpers/Toolbox.js'; import * as Gitlab from '@gitbeaker/rest'; import TextStyle from '../../../types/TextStyle.js'; import Config from '../../../config/Config'; import SharedSonarManager from '../../../shared/managers/SharedSonarManager'; import { Option } from 'commander'; type CommandOptions = { name: string, language: string, template?: string, members_id?: Array<number>, members_username?: Array<string>, clone?: string | boolean, sonar?: boolean, sonarStrict: boolean, gate?: string, profile?: string[] } class AssignmentCreateCommand extends CommanderCommand { protected commandName: string = 'create'; private members!: Array<Gitlab.UserSchema> | undefined; private templateIdOrNamespace: string | null = null; private assignment!: Assignment; private sonar: boolean = false; private sonarGate: string | undefined = undefined; private sonarProfiles: string[] = []; protected defineCommand() { this.command .description('create a new repository for an assignment') .requiredOption('-n, --name <name>', 'name of the assignment') .requiredOption('-l, --language <string>', 'main programming language of the assignment') .option('-i, --members_id <ids...>', 'list of gitlab members ids (teaching staff) to add to the repository') .option('-u, --members_username <usernames...>', 'list of gitlab members username (teaching staff) to add to the repository') .option('-t, --template <string>', 'id or url of the template (http/s and ssh urls are possible)') .option('-c, --clone [string]', 'automatically clone the repository (SSH required) in the specified directory (this will create a subdirectory with the assignment name)') .action(this.commandAction.bind(this)); if ( SharedConfig.sonar.enabled ) { this.command.requiredOption('-s, --sonar', 'add sonar to the code checking process for assignment and exercises') .requiredOption('-d, --no-sonar', 'disable sonar for the code checking process for assignment and exercises') .addOption(new Option('--sonar-strict', 'force the sonar gate to pass to validate the exercise results').default(false).implies({ sonar: true })) .addOption(new Option('-g, --gate <gate>', 'quality gate for sonar').implies({ sonar: true })) .addOption(new Option('-p, --profile <profile...>', 'quality profiles for sonar').default([]).implies({ sonar: true })); } } private async dataRetrieval(options: CommandOptions) { console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...')); await AccessesHelper.checkTeachingStaff(); this.members = await Config.gitlabManager.fetchMembers(options); if ( !this.members ) { throw new Error(); } if ( options.sonar ) { const assignmentGetSonarSpinner: ora.Ora = ora('Checking server sonar status').start(); this.sonar = (SharedConfig.sonar.enabled ? options.sonar ?? false : false); if ( this.sonar && !(await DojoBackendManager.isSonarEnabled()) ) { assignmentGetSonarSpinner.fail(`Sonar is currently not supported by the server. Disable sonar integration or try again later.`); throw new Error(); } assignmentGetSonarSpinner.succeed(`Sonar is supported by the server`); this.sonarGate = options.gate; this.sonarProfiles = options.profile ?? []; } const assignmentGetSpinner: ora.Ora = ora('Checking assignment name availability').start(); if ( await DojoBackendManager.getAssignment(options.name) ) { assignmentGetSpinner.fail(`Assignment name "${ options.name }" is already taken. Please choose another one.`); throw new Error(); } assignmentGetSpinner.succeed(`Assignment name "${ options.name }" is available`); // Dojo languages const languagesSpinner: ora.Ora = ora('Checking language support').start(); const languages = await DojoBackendManager.getLanguages(); if ( !languages.includes(options.language) ) { languagesSpinner.fail(`Language "${ options.language }" is not supported. Choose a supported language or "other"`); console.log('List of supported languages:'); for ( const l of languages ) { console.log(` - ${ l }`); } throw new Error(); } languagesSpinner.succeed(`Language "${ options.language }" is supported`); if ( (this.sonarGate ?? '') !== '' || this.sonarProfiles.length > 0 ) { const qualitiesSpinner: ora.Ora = ora('Checking sonar qualities').start(); // SonarQube quality gate and profiles const result = await DojoBackendManager.testSonarQualities(this.sonarGate ?? '', this.sonarProfiles); if ( !result.valid ) { const invalid = (result.badGate == undefined ? result.badProfiles : [ result.badGate, ...result.badProfiles ]); qualitiesSpinner.fail(`Invalid quality gate or profiles : ${ invalid }`); throw new Error(); } qualitiesSpinner.succeed(`Quality gate and profiles are valid`); } // SonarQube languages if ( this.sonar ) { const sonarLang = await DojoBackendManager.getSonarLanguages(); if ( !sonarLang.includes(SharedSonarManager.mapLanguage(options.language)) ) { languagesSpinner.fail(`Language "${ options.language }" is not supported with Sonar. Choose a supported language or disable sonar`); throw new Error(); } } if ( options.template ) { this.templateIdOrNamespace = options.template; if ( Number.isNaN(Number(this.templateIdOrNamespace)) ) { this.templateIdOrNamespace = Toolbox.urlToPath(this.templateIdOrNamespace); } if ( !await DojoBackendManager.checkTemplateAccess(this.templateIdOrNamespace) ) { throw new Error(); } } } private async createAssignment(options: CommandOptions) { console.log(TextStyle.BLOCK('Please wait while we are creating the assignment (approximately 10 seconds)...')); this.assignment = await DojoBackendManager.createAssignment(options.name, options.language, this.members!, this.templateIdOrNamespace, this.sonar, !options.sonarStrict, this.sonarGate, this.sonarProfiles); const oraInfo = (message: string) => { ora({ text : message, indent: 4 }).start().info(); }; oraInfo(`${ TextStyle.LIST_ITEM_NAME('Name:') } ${ this.assignment.name }`); oraInfo(`${ TextStyle.LIST_ITEM_NAME('Web URL:') } ${ this.assignment.gitlabCreationInfo.web_url }`); oraInfo(`${ TextStyle.LIST_ITEM_NAME('HTTP Repo:') } ${ this.assignment.gitlabCreationInfo.http_url_to_repo }`); oraInfo(`${ TextStyle.LIST_ITEM_NAME('SSH Repo:') } ${ this.assignment.gitlabCreationInfo.ssh_url_to_repo }`); if ( this.assignment.useSonar ) { oraInfo(`${ TextStyle.LIST_ITEM_NAME('Sonar project:') } ${ SharedConfig.sonar.url }/dashboard?id=${ this.assignment.sonarKey }`); } } private async cloneRepository(options: CommandOptions) { if ( options.clone ) { console.log(TextStyle.BLOCK('Please wait while we are cloning the repository...')); await Config.gitlabManager.cloneRepository(options.clone, this.assignment.gitlabCreationInfo.ssh_url_to_repo, undefined, true, 0); } } protected async commandAction(options: CommandOptions): Promise<void> { try { await this.dataRetrieval(options); await this.createAssignment(options); await this.cloneRepository(options); } catch ( e ) { /* Do nothing */ } } } export default new AssignmentCreateCommand();