diff --git a/CHANGELOG.md b/CHANGELOG.md index 95a811e..64f51fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.4.0] - 2023-03-05 +### Added + - Change default prefix to project_name+ instead of project_name- + +### Changed + - Separate iotests to self-standing file + ## [0.3.0] - 2022-09-22 ## [0.2.0] - 2022-08-31 @@ -15,6 +22,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Set name of distributed project as `original_project_name user_name` +[0.4.0]: https://github.com/internetguru/academy/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/internetguru/academy/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/internetguru/academy/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/internetguru/academy/compare/v0.0.0...v0.1.0 diff --git a/README.md b/README.md index b34b602..1db4d73 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,8 @@ Note: To execute individual commands, [trigger their pipeline](https://docs.gitl ## GitLab CI Variables +- `ACADEMY_DASHBOARD: "URL"` + - Dashboard URL, e.g. https://academy.internetguru.io - `ACADEMY_DEADLINE: "DATE"` - See `--deadline` option in `academy collect` documentation. - `ACADEMY_EDITABLE: "PATTERN"` diff --git a/VERSION b/VERSION index 0d91a54..1d0ba9e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.0 +0.4.0 diff --git a/academy b/academy index cf46297..e3ed1a5 100755 --- a/academy +++ b/academy @@ -1,7 +1,6 @@ -#!/bin/bash +#!/usr/bin/env bash set -o pipefail - declare -r SCRIPT='academy' # shellcheck disable=SC2155 declare -r DIR="$(dirname "${BASH_SOURCE[0]}")" @@ -9,11 +8,23 @@ declare -r DIR="$(dirname "${BASH_SOURCE[0]}")" declare -rA LANG_TO_EXTENSION=( [java]=java [oracle]=sql ) # shellcheck disable=SC2034 declare -r SRC_DIR='src' +declare CUSTOMIZE=1 # shellcheck disable=SC1091 # shellcheck disable=SC1090 -. "${DIR}/commons" +source "${DIR}/commons" +# shellcheck disable=SC1091 +source "${DIR}/badges" + +# disabled by LB +# check_lang "${ACADEMY_LANG}" -check_lang "${ACADEMY_LANG}" +customize_academy() { + (( CUSTOMIZE )) || return + local CUSTOM_ACADEMY='.academy' + [[ -d "${CUSTOM_ACADEMY}" ]] \ + || return + cp "${CUSTOM_ACADEMY}"/* "${DIR}" +} # shellcheck disable=SC2155 declare WORKING_DIR="$(readlink -f ".")" @@ -25,7 +36,12 @@ while (( $# > 0 )); do WORKING_DIR="$(readlink -f "${2}")" [[ -d "${WORKING_DIR}" ]] \ || exception 'WORKING_DIR not found' - shift; shift ;; + shift 2 + ;; + -C|--no-customize) + CUSTOMIZE=0 + shift + ;; help) print_usage exit @@ -33,18 +49,18 @@ while (( $# > 0 )); do collect|distribute|measure) # shellcheck disable=SC1091 # shellcheck disable=SC1090 - . "${DIR}/git_functions" - # shellcheck disable=SC1091 - # shellcheck disable=SC1090 - . "${DIR}/gitlab_api" + source "${DIR}/git_functions" # check requirements check_command git jq ;& - evaluate) + evaluate|execute) declare -r CMD="${1}" shift + # shellcheck disable=SC1091 + source "${DIR}/gitlab_api" + customize_academy # shellcheck disable=SC1090 - . "${DIR}/${CMD}" "$@" + source "${DIR}/${CMD}" "$@" exit $? ;; *) diff --git a/badges b/badges new file mode 100644 index 0000000..6d899c5 --- /dev/null +++ b/badges @@ -0,0 +1,61 @@ +#!/bin/bash + +declare SHIELDS='https://img.shields.io/static/v1' +declare DEFAULT_COLOR='inactive' + +# init pathnames to badges +badge_init() { + local badge_name="${1}" + local badge_default="${2}" #opt + badge_generate "${badge_name}" "${badge_default:--}" + # printf '' ... FIXME - Print badge URL +} + +# % to colors +percent_to_color() { + declare color='brightgreen' + declare perc="${1%\%}" + (( 10#$perc < 85 )) \ + && color='green' + (( 10#$perc < 70 )) \ + && color='yellow' + (( 10#$perc < 55 )) \ + && color='orange' + (( 10#$perc < 45 )) \ + && color='red' + printf -- '%s\n' "${color}" +} + +fraction_to_color() { + if [[ "${1}" == */0 ]]; then + printf '%s\n' "${DEFAULT_COLOR}" + return 1 + fi + percent_to_color "$(( 100 * $1 ))" +} + +# URL encoding +url_encode() { + jq -Rr @uri <<< "${1}" +} + +# generate badge +badge_generate() { + local badge_name="${1}" + local ci_job="${CI_JOB_NAME:-local}" + local badge_file="${badge_name// /-}" + badge_file="${RESULTS:-${WORKING_DIR}/.results}/badge-${ci_job}-${badge_file,,}.svg" + local badge_label + badge_label="$( url_encode "${badge_name}" )" + local badge_value + badge_value="$( url_encode "${2}" )" + local badge_color="${3}" + [[ -z "${badge_color}" ]] \ + && case "${2}" in + n/a) badge_color="${DEFAULT_COLOR}";; + */*) badge_color="$( fraction_to_color "${2}" )";; + *%) badge_color="$( percent_to_color "${2%\%}" )";; + *) badge_color="${DEFAULT_COLOR}" + esac + curl -so "${badge_file}" "${SHIELDS}?label=${badge_label}&message=${badge_value}&color=${badge_color}" +} diff --git a/build-docker.yml b/build-docker.yml new file mode 100644 index 0000000..3c10c80 --- /dev/null +++ b/build-docker.yml @@ -0,0 +1,21 @@ +build-docker-image: + image: docker:20.10.10 + stage: build + tags: + - docker + variables: + DOCKER_HOST: tcp://dockerhost:2375/ + DOCKER_DRIVER: overlay2 + DOCKER_TLS_CERTDIR: "" + services: + - name: docker:20.10.10-dind + alias: dockerhost + script: + - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin + - docker build --pull -t $CI_REGISTRY_IMAGE . + - docker push $CI_REGISTRY_IMAGE + rules: + - changes: + - Dockerfile + - when: manual + allow_failure: true diff --git a/collect b/collect index 2de71b6..81269fa 100644 --- a/collect +++ b/collect @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash get_last_commit() { [[ -z "${DEADLINE}" ]] \ @@ -7,13 +7,21 @@ get_last_commit() { get_last_push } sync_files() { + # shellcheck disable=SC2155 + local eg=$( shopt -p extglob ) + shopt -s extglob git_checkout "${user_projects_folder}/${user_project_ns}" "${last_commit}" # shellcheck disable=SC2086 rsync -a --relative --delete \ "${user_projects_folder}/${user_project_ns}/./"${EDITABLE} "${WORKING_DIR}" \ || exception 'Unable to rsync files' + $eg } -create_results() { +create_results_default() { + "${DIR}/${SCRIPT}" -w "${WORKING_DIR}" --no-customize evaluate + mv "${WORKING_DIR}/.results" "${user_projects_folder}/${user_project_ns}" +} +create_results_java() { "${DIR}/${SCRIPT}" -w "${WORKING_DIR}" evaluate mv "${WORKING_DIR}/.results" "${user_projects_folder}/${user_project_ns}" compile_txt="${user_projects_folder}/${user_project_ns}/.results/compile.txt" @@ -29,14 +37,24 @@ create_results() { echo git_reset_hard "${user_projects_folder}/${user_project_ns}" } +create_results() { + case "${ACADEMY_LANG}" in + java) create_results_"${ACADEMY_LANG}";; + *) create_results_default;; + esac +} collect() { # shellcheck disable=SC2155 [[ ! -t 0 ]] \ || exception 'Missing stdin' 2 - exec 3<&0 + local user_list=() + # shellcheck disable=SC2207 + user_list=( $( cat ) ) + local user_i=1 acquire_token [[ -z "${OUTPUT_DIR}" ]] \ && OUTPUT_DIR="$(mktemp -d)" + trap 'rm -rf "${OUTPUT_DIR}"' EXIT # shellcheck disable=SC2155 declare -r user_projects_folder="${OUTPUT_DIR}" declare group_id @@ -46,19 +64,28 @@ collect() { declare checkstyle_txt declare test_txt declare results + declare -x user_repo # shellcheck disable=SC2034 group_id="$(get_group_id "${NAMESPACE}" 2>/dev/null)" [[ -z "${group_id}" ]] \ && exception "Namespace '${NAMESPACE}' not found" 3 + + # Pre-collect script + # shellcheck disable=SC1090 + [[ -f "${DIR}/pre-collect_${ACADEMY_LANG}" ]] \ + && . "${DIR}/pre-collect_${ACADEMY_LANG}" + # shellcheck disable=SC2013 - for user_name in $(cat <&3); do + export user_name + for user_name in "${user_list[@]}"; do user_project_name="${PREFIX}${user_name}" user_project_ns="${NAMESPACE}/${user_project_name}" + echo "[$((user_i++))/${#user_list[@]}] ${user_project_ns}" get_project_id "${user_project_ns}" >/dev/null \ || continue - echo "$user_project_ns" [[ "${DRY_RUN}" == 'true' ]] \ && continue + user_repo="${user_projects_folder}/${user_project_ns}" mkdir -p "${user_projects_folder}/${user_project_ns}" git_clone "$(get_remote_url "${user_project_ns}")" "${user_projects_folder}/${user_project_ns}" last_commit="$(get_last_commit)" \ @@ -68,14 +95,21 @@ collect() { && continue sync_files create_results + rm -rf "${user_projects_folder:?}/${user_project_ns}" done + + # Post-collect script + # shellcheck disable=SC1090 + [[ -f "${DIR}/post-collect_${ACADEMY_LANG}" ]] \ + && . "${DIR}/post-collect_${ACADEMY_LANG}" + } declare DRY_RUN='false' declare PREFIX='' declare NAMESPACE='' -declare EDITABLE='src/main/*.java' -declare DEADLINE='' +declare EDITABLE="${ACADEMY_EDITABLE}" +declare DEADLINE="${ACADEMY_DEADLINE}" declare OUTPUT_DIR='' # get options @@ -95,12 +129,12 @@ while (( $# > 0 )); do DEADLINE="${2}"; [[ "${DEADLINE}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}(T[0-9]{2}:[0-9]{2}(:[0-9]{2}(Z|[+−][0-9]{2}:[0-9]{2})?)?)?$ ]] \ || exception "Invalid deadline option" - shift; shift ;; + shift 2 ;; -e|--editable) EDITABLE="${2}"; [[ "${EDITABLE}" == *".."* ]] \ && exception "Invalid editable option, contains '..'" - shift; shift ;; + shift 2 ;; -n|--dry-run) DRY_RUN='true'; shift ;; -o|--output-dir) OUTPUT_DIR="${2}"; @@ -108,14 +142,14 @@ while (( $# > 0 )); do || mkdir -p "${OUTPUT_DIR}" \ || exception "Unable to create '${OUTPUT_DIR}' directory" [[ -z "$(ls "${OUTPUT_DIR}/")" ]] \ - || exception "Output directory 'ls' is not empty" - shift; shift ;; - -p|--prefix) PREFIX="${2}"; shift; shift ;; + || exception "Output directory '${OUTPUT_DIR}' is not empty" + shift 2 ;; + -p|--prefix) PREFIX="${2}"; shift 2 ;; -s|--namespace) NAMESPACE="${2}"; [[ "${NAMESPACE}" =~ ^[a-z0-9-]{2,}(/[a-z0-9-]{2,})*$ ]] \ || exception "Unsupported NAMESPACE '${NAMESPACE}'" 2 - shift; shift ;; + shift 2 ;; --) shift; break ;; *) break ;; esac diff --git a/commons b/commons index 38a0406..3ca27a1 100755 --- a/commons +++ b/commons @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash declare MSG_OPENED='false' @@ -51,6 +51,14 @@ exception() { || printf -- '%s [ #%d ]\n' "${message}" "${code}" >&2 exit "${code}" } +warning() { + declare -r message="${1:-Unknown exception}" + declare -ri code=${2:-1} + (( code == 1 )) \ + && printf -- '%s in %s() [ #%d ]\n' "${message}" "${FUNCNAME[1]}" "${code}" >&2 \ + || printf -- '%s [ #%d ]\n' "${message}" "${code}" >&2 + return "${code}" +} print_usage() { declare out declare cmd="${CMD}" diff --git a/deploy-pages.yml b/deploy-pages.yml new file mode 100644 index 0000000..a2dac98 --- /dev/null +++ b/deploy-pages.yml @@ -0,0 +1,12 @@ +pages: + stage: post-process + script: + - mkdir .public + - cp -r ./* .public + - rm -rf public + - mv .public public + artifacts: + paths: + - public + rules: + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH diff --git a/distribute b/distribute index 191e749..9ac326b 100755 --- a/distribute +++ b/distribute @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash ## functions get_cache_folder() { @@ -33,6 +33,7 @@ update_links() { sed -i "s~${PROJECT_LINK}~${user_project_ns}~g" "${user_readme_path}" sed -i "s~/${PROJECT_BRANCH}/\(pipeline\|raw\|file\)~/${user_default_branch}/\1~g" "${user_readme_path}" sed -i "s~ref=${PROJECT_BRANCH}~ref=${user_default_branch}~g" "${user_readme_path}" + sed -i "s~\(https://[^/]*\)/${PROJECT_LINK#*/}~\1/${user_project_ns#*/}~g" "${user_readme_path}" } init_user_repo() { declare user_id='' @@ -64,6 +65,7 @@ init_user_repo() { remove_protected_branch set_protected_branch "${user_default_branch}" "${DEV_ACCESS_LEVEL}" set_protected_branch "${SOURCE_BRANCH}" "${MAINTAINER_ACCESS_LEVEL}" + set_public_pages "${project_id}" >/dev/null } status_empty () { get_cache @@ -80,19 +82,37 @@ update_user_repo() { || exit 1 request_exists \ || create_request + copy_issues + set_public_pages "${project_id}" >/dev/null } read_issues() { - ISSUES="$(gitlab_api "api/v4/projects/${PROJECT_ID}/issues?labels=${ISSUES_LABEL}")" \ + local pid="${1:?project id missing}" + local label="${2}" + ISSUES="$(gitlab_api "api/v4/projects/${pid}/issues?labels=${label}")" \ && ISSUES_COUNT="$(jq length <<< "${ISSUES}")" \ || exit 1 } copy_issues() { - declare i issue - for (( i=0; i < ISSUES_COUNT; i++ )); do - issue="$(jq ".[${i}] | {title, description, due_date}" <<< "${ISSUES}")" + declare i issue_src issues_dst issues_count_dst + declare -A description=() + read_issues "${project_id}" \ + && issues_dst="${ISSUES}" issues_count_dst="${ISSUES_COUNT}" + for (( i=0; i < issues_count_dst; i++ )); do + description+=( ["$(jq -r ".[${i}].title" <<< "${issues_dst}")"]="$(jq -r ".[${i}].description" <<< "${issues_dst}")" ) + done + for (( i=0; i < issues_count_src; i++ )); do + issue_src="$(jq ".[${i}] | {title, description, due_date}" <<< "${issues_src}")" + if [[ "${description["$(jq -r ".title" <<< "${issue_src}")"]}" == "$(jq -r ".description" <<< "${issue_src}")" ]]; then + #printf -- "[issues] skipping '%s'\n" "$(jq -r ".title" <<< "${issue_src}")'" >&2 + continue + fi + # Update existing issue + # ToDo + # Add new issue + #printf -- "[issues] adding '%s'\n" "${issue_src//$'\n'/|}" >&2 [[ -n "${user_id}" ]] \ - && issue="$(jq --arg a "${user_id}" '. + {assignee_ids:[$a]}' <<< "${issue}")" - gitlab_api "api/v4/projects/${project_id}/issues" "${issue}" >/dev/null + && issue_src="$(jq --arg a "${user_id}" '. + {assignee_ids:[$a]}' <<< "${issue_src}")" + gitlab_api "api/v4/projects/${project_id}/issues" "${issue_src}" >/dev/null done } read_project_info() { @@ -109,7 +129,8 @@ read_project_info() { || exit 1 [[ -z "${NAMESPACE}" ]] \ && NAMESPACE="$(dirname "${PROJECT_LINK}")" - read_issues + read_issues "${PROJECT_ID}" "${ISSUES_LABEL}" \ + && issues_src="${ISSUES}" issues_count_src="${ISSUES_COUNT}" msg_end } process_users() { @@ -190,7 +211,9 @@ declare PROJECT_ID='' declare PROJECT_NAME='' declare PROJECT_BRANCH='' declare ISSUES='' +declare issues_src="$ISSUES" declare -i ISSUES_COUNT=0 +declare -i issues_count_src="${ISSUES_COUNT}" # shellcheck disable=SC2034 declare ACADEMY_GITLAB_ACCESS_TOKEN @@ -209,35 +232,47 @@ while (( $# > 0 )); do ASSIGN="${2}" [[ "${ASSIGN}" =~ ^(${ALWAYS}|${NEVER}|${AUTO})$ ]] \ || exception 'Invalid option ASSIGN value' - shift; shift ;; - -h|--help) print_usage && exit 0 ;; + shift 2 + ;; + -h|--help) + print_usage \ + && exit 0 + ;; -i|--process-issues) ISSUES_LABEL="$2" [[ "${ISSUES_LABEL}" =~ ^[a-z0-9][a-z0-9_-]+$ ]] \ || exception 'Unsupported ISSUES_LABEL value' 2 - shift; shift ;; + shift 2 + ;; -l|--update-links) UPDATE_LINKS='true' [[ -f "${WORKING_DIR}/${README_FILE}" ]] \ || exception 'Readme file not found in WORKING_DIR' - shift ;; - -n|--dry-run) DRY_RUN='true'; shift ;; + shift + ;; + -n|--dry-run) + DRY_RUN='true' + shift + ;; -p|--prefix) PREFIX="${2}" - [[ "${PREFIX}" =~ ^[a-z0-9][a-z0-9-]+$ ]] \ + [[ "${PREFIX}" =~ ^[a-z0-9]+([a-z0-9._-]+)*$ ]] \ || exception 'Unsupported PREFIX value' 2 - shift; shift ;; + shift 2 + ;; -s|--namespace) NAMESPACE="${2}" [[ "${NAMESPACE}" =~ ^[a-z0-9-]{2,}(/[a-z0-9-]{2,})*$ ]] \ - || exception 'Unsupported REMOTE_NAMESPACE dirname' 2 - shift; shift ;; + || exception "Unsupported REMOTE_NAMESPACE '$NAMESPACE' dirname" 2 + shift 2 + ;; -o|--output-dir) CACHE_FOLDER="${2}"; [[ -d "${CACHE_FOLDER}" ]] \ || mkdir -p "${CACHE_FOLDER}" \ || exception "Unable to create '${CACHE_FOLDER}' directory" - shift; shift ;; + shift 2 + ;; --) shift; break ;; *) break ;; esac diff --git a/documentation/academy.md b/documentation/academy.md index 0c9549a..6175334 100644 --- a/documentation/academy.md +++ b/documentation/academy.md @@ -27,8 +27,11 @@ collect distribute Create or update user repositories with files from source project. +execute + Execute code to see output on the fly. + evaluate - Verify Java project and generate badges for README. + Verify project and generate badges for README. help Display this usage. diff --git a/evaluate b/evaluate index ba796f1..c7b7962 100755 --- a/evaluate +++ b/evaluate @@ -1,129 +1,53 @@ -#!/bin/bash +#!/usr/bin/env bash -get_test_color() { - declare color='brightgreen' - declare perc="${1}" - (( perc < 85 )) \ - && color='green' - (( perc < 70 )) \ - && color='yellow' - (( perc < 55 )) \ - && color='orange' - (( perc < 45 )) \ - && color='red' - echo "${color}" -} -pretty_diff() { - diff --old-line-format=$'- %l\n' --new-line-format=$'+ %l\n' --unchanged-line-format='' "$@" -} -io_test() { - declare -r test_folder="${1}" - # shellcheck disable=SC2155 - declare -r test_folder_name="$(basename "${test_folder}")" - declare -r test_name="${2}" - declare -r test_path="${test_folder}/${test_name}" - declare cmd="${3}" - # TODO check unsupported test extensions (how?) - echo -e "\nTest ${test_folder_name}/${test_name}" - # create cmd from template - # TODO: add reference to README - declare -A variables - # script varaibles - variables=( \ - ['FOLDER_NAME']="${test_folder_name%.*}"\ - ['RELATIVE_PATH']="${test_folder#"${IOTEST_FOLDER}/"}"\ - ['WORKING_DIR']="${WORKING_DIR}" \ - ) - # add filtered env variables - while read -r line; do - variables["$(cut -d= -f1 <<< "${line}")"]="$(cut -d= -f2- <<< "${line}")" - done <<< "$(printenv | grep '^\(ACADEMY_\|CI_\)')" - # replace variables in cmd - for key in "${!variables[@]}"; do - cmd="${cmd/@${key}@/${variables[${key}]}}" - done - # TODO use optarg file - declare out_file err_file status_code - out_file="$(mktemp)" - err_file="$(mktemp)" - # run cmd once - eval "cat '${test_path}.stdin' | ${cmd} >'${out_file}' 2>'${err_file}'" - status_code=$? - declare retrun_code=0 - # test status code - if [[ -f "${test_path}.sc" ]]; then - echo 'Status code diff' - pretty_diff <(echo "${status_code}") "${test_path}.sc" \ - || retrun_code=1 - fi - # test stdout - if [[ -f "${test_path}.stdout" ]]; then - pretty_diff "${out_file}" "${test_path}.stdout" \ - || retrun_code=1 - fi - # test stderr - if [[ -f "${test_path}.stderr" ]]; then - echo "Stderr diff" - pretty_diff "${err_file}" "${test_path}.stderr" \ - || retrun_code=1 - fi - rm "${out_file}" - rm "${err_file}" - return ${retrun_code} -} run_io_tests() { - [[ ! -d "${IOTEST_FOLDER}" ]] \ - && return - # defaults - curl -so "${IOTEST_SVG}" 'https://img.shields.io/badge/IO%20Tests-0/0-gray' - printf -- 'n/a' > "${IOTEST_TXT}" - printf -- '' > "${IOTEST_LOG}" - # run iotests - declare -r cmd="${1}" - declare -i passed=0 count=0 - # search deepest folders only by -links 2 (folder with 2 hardlinks) - while read -r -d '' folder; do - # shellcheck disable=SC2012 - for name in $(ls "${folder}/" | rev | cut -d. -f2- | rev | sort -u); do - count+=1 - io_test "${folder}" "${name}" "${cmd}" >> "${IOTEST_LOG}" \ - && passed+=1 - done - done < <(find "${IOTEST_FOLDER}" -type d -links 2 -print0) - # generate summary and badge - printf -- "Summary: %s/%s\n" "${passed}" "${count}" >> "${IOTEST_LOG}" + "${DIR}/iotest" "${IOTEST_FOLDER}" "${1}" | tee "${IOTEST_LOG}" + declare summary passed count + summary="$(grep 'Summary:' < "${IOTEST_LOG}" | cut -d ' ' -f2-)" + count="$(cut -d',' -f1 <<< "${summary}" | cut -d' ' -f2)" + passed="$(cut -d',' -f3 <<< "${summary}" | cut -d' ' -f3)" declare test_color='success' [[ ${passed} -lt ${count} ]] \ && test_color='critical' - printf -- '%s/%s' "${passed}" "${count}" > "${IOTEST_TXT}" - printf -- 'iotest: %s\n' "$(<"${IOTEST_TXT}")" >> "${BADGES_TXT}" - curl -so "${IOTEST_SVG}" "https://img.shields.io/badge/IO%20Tests-${passed}/${count}-${test_color}" + badge_generate 'IO Tests' "${passed}/${count}" "${test_color}" } main() { - declare -r IOTEST_FOLDER="${WORKING_DIR}/iotest" - declare -r RESULTS="${WORKING_DIR}/.results" - declare -r IOTEST_LOG="${RESULTS}/iotest.log" - declare -r IOTEST_TXT="${RESULTS}/iotest.txt" - declare -r IOTEST_SVG="${RESULTS}/iotest.svg" - declare -r GENERATED_TXT="${RESULTS}/generated.txt" + # deprecated declare -r BADGES_TXT="${RESULTS}/badges.txt" + declare -r GENERATED_TXT="${RESULTS}/generated.txt" + : > "${BADGES_TXT}" + declare -r RESULTS="${WORKING_DIR}/.results" + declare -r ACADEMY_CACHE="${HOME}/.academy" mkdir -p "${RESULTS}" - printf -- '' > "${BADGES_TXT}" - case ${ACADEMY_LANG} in - java) - # shellcheck disable=SC1090 - # shellcheck disable=SC1091 - . "${DIR}/evaluate_java" - ;; - oracle) - # shellcheck disable=SC1090 - # shellcheck disable=SC1091 - . "${DIR}/evaluate_oracle" - ;; - *) - exception "Unexpected exception: academy lang should be checked before" - esac + mkdir -p "${ACADEMY_CACHE}" + + # jenom pro CI a ten grep je spatne, ACADEMY_EDITABLE neni RE ale SP + # shellcheck disable=SC2034 + mapfile -t CHANGED_FILES < <( + git -C "${WORKING_DIR}" diff HEAD..HEAD~1 --name-only 2>/dev/null \ + | grep -E "${ACADEMY_EDITABLE:-.*}" + ) + # shellcheck disable=SC1090 + [[ -f "${DIR}/pre-evaluate_${ACADEMY_LANG}" ]] \ + && source "${DIR}/pre-evaluate_${ACADEMY_LANG}" + + if [[ -f "${DIR}/evaluate_${ACADEMY_LANG}" ]]; then + # shellcheck disable=SC1090 + source "${DIR}/evaluate_${ACADEMY_LANG}" + else + if [[ -z "${ACADEMY_LANG}" ]]; then + warning "Undefined ACADEMY_LANG variable. Evaluation skipped." + else + exception "Missing evaluate script 'evaluate_${ACADEMY_LANG}' for selected language." + fi + fi + + # shellcheck disable=SC1090 + [[ -f "${DIR}/post-evaluate_${ACADEMY_LANG}" ]] \ + && source "${DIR}/post-evaluate_${ACADEMY_LANG}" + + # deprecated date +%s > "${GENERATED_TXT}" printf -- 'age: %s\n' "$(<"${GENERATED_TXT}")" >> "${BADGES_TXT}" } diff --git a/evaluate_gallery b/evaluate_gallery new file mode 100644 index 0000000..b3b0ed5 --- /dev/null +++ b/evaluate_gallery @@ -0,0 +1,13 @@ +# Načtení velikostí výchozích souborů +declare -A file_size=() +while read file size; do + file_size["${file}"]="${size}" +done <<< "${file_size_data}" + +# Přidat studentské soubory do galerie, pokud se liší od výchozích +for filepath in ${file_pattern_data}; do + [[ $( stat -c '%s' "${filepath}" ) -eq ${file_size["$filepath"]} ]] \ + && continue + mkdir -p "${local_gallery}/${user_name}/${filepath%/*}" + cp -v "${filepath}" "${local_gallery}/${user_name}/${filepath}" +done diff --git a/evaluate_java b/evaluate_java index 536f724..67a45d0 100644 --- a/evaluate_java +++ b/evaluate_java @@ -1,118 +1,14 @@ -#!/bin/bash - -main() { - declare -r POM="${WORKING_DIR}/pom.xml" - declare -r COMPILE_LOG="${RESULTS}/compile.log" - declare -r CHECKSTYLE_LOG="${RESULTS}/checkstyle.log" - declare -r TEST_LOG="${RESULTS}/test.log" - declare -r COMPILE_TXT="${RESULTS}/compile.txt" - declare -r CHECKSTYLE_TXT="${RESULTS}/checkstyle.txt" - declare -r TEST_TXT="${RESULTS}/test.txt" - declare -r COMPILE_SVG="${RESULTS}/compile.svg" - declare -r CHECKSTYLE_SVG="${RESULTS}/checkstyle.svg" - declare -r TEST_SVG="${RESULTS}/test.svg" - - curl -so "${COMPILE_SVG}" 'https://img.shields.io/badge/Compile-failed-critical' - curl -so "${CHECKSTYLE_SVG}" 'https://img.shields.io/badge/Code%20Style-n/a-gray' - curl -so "${TEST_SVG}" 'https://img.shields.io/badge/CI%20Tests-0/0-gray' - - printf -- 'failed' > "${COMPILE_TXT}" - printf -- 'n/a' > "${CHECKSTYLE_TXT}" - printf -- 'n/a' > "${TEST_TXT}" - - if ! mvn -f "${POM}" compile > "${COMPILE_LOG}" 2>&1; then - printf -- 'compile: failed\n' >> "${BADGES_TXT}" - exit 0 - fi - printf -- 'success' > "${COMPILE_TXT}" - printf -- 'compile: success\n' >> "${BADGES_TXT}" - curl -so "${COMPILE_SVG}" 'https://img.shields.io/badge/Compile-passed-success' - - # shellcheck disable=SC2155 - declare -r tmppom="$(mktemp -d)/checkstyle-pom.xml" - declare -r src='src/main' - - cat << EOD > "${tmppom}" - - 4.0.0 - InternetGuru - java-checkstyle - 1 - - 1.8 - 1.8 - UTF-8 - - - ${WORKING_DIR}/${src} - ${WORKING_DIR}/src/test - - - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.1.1 - - google_checks.xml - UTF-8 - true - true - false - - - - validate - validate - - check - - - - - - - - -EOD - - # run checkstyle and save warnings and errors into log - mvn -f "${tmppom}" -T 1C clean install -Dmaven.test.skip -DskipTests - mvn -f "${tmppom}" checkstyle:check | grep '^\[\(WARN\|ERROR\)' > "${CHECKSTYLE_LOG}" 2>&1 - [[ ${PIPESTATUS[0]} != 0 ]] \ - && exit 1 +[[ -n "${IOTEST_FOLDER}" && -d "${IOTEST_FOLDER}" ]] \ + && run_io_tests "java ${WORKING_DIR}/@RELATIVE_PATH@" +if [[ -n "${ACADEMY_EDITABLE}" ]]; then + ${CHECKSTYLE_CMD} -c 'google_checks.xml' ${ACADEMY_EDITABLE} | grep '^\[\(WARN\|ERROR\)' > "${CHECKSTYLE_LOG}" # shellcheck disable=SC2155 declare -ri errs=$(cut -d" " -f2 < "${CHECKSTYLE_LOG}" | sort -ut: -k1,2 | wc -l) # shellcheck disable=SC2155 - declare -ri lines=$(find ${src} -name '*.java' -exec cat {} + | wc -l) + declare -ri lines=$(find "${WORKING_DIR}/$(dirname "${ACADEMY_EDITABLE}")" -name "$(basename "${ACADEMY_EDITABLE}")" -exec cat {} + | wc -l) declare -i perc=100 (( errs > 0 )) \ && (( perc = 99 - errs * 100 / lines )) - - color="$(get_test_color "${perc}")" - - curl -so "${CHECKSTYLE_SVG}" "https://img.shields.io/badge/Code%20Style-${perc}%20%25-${color}" - printf -- "%d%%" "${perc}" > "${CHECKSTYLE_TXT}" - printf -- 'checkstyle: %s\n' "$(<"${CHECKSTYLE_TXT}")" >> "${BADGES_TXT}" - - declare test_color='success' - mvn -Dstyle.color=never -f "${POM}" test > "${TEST_LOG}" 2>&1 \ - || test_color='critical' - # shellcheck disable=SC2155 - declare -r summary=$(grep 'Tests run:' "${TEST_LOG}" | head -n1) - # shellcheck disable=SC2155 - declare -ri runs=$(tr -d ',' <<< "${summary}" | cut -d' ' -f4) - # shellcheck disable=SC2155 - declare -ri failures=$(tr -d ',' <<< "${summary}" | cut -d' ' -f6) - # shellcheck disable=SC2155 - declare -ri skipped=$(tr -d ',' <<< "${summary}" | cut -d' ' -f10) - declare -ri total=$((runs - skipped)) - declare -ri passed=$((total - failures)) - curl -so "${TEST_SVG}" "https://img.shields.io/badge/CI%20Tests-${passed}/${total}-${test_color}" - printf -- "%s/%s" "${passed}" "${total}" > "${TEST_TXT}" - printf -- 'test: %s\n' "$(<"${TEST_TXT}")" >> "${BADGES_TXT}" - - run_io_tests "mvn -q -f ${POM} exec:java@@FOLDER_NAME@" -} - -main "$@" + badge_generate 'Code Style' "${perc} %" +fi diff --git a/evaluate_oracle b/evaluate_oracle index 11db247..012a717 100644 --- a/evaluate_oracle +++ b/evaluate_oracle @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash main() { [[ -n "${ORACLE_LOGON}" ]] \ diff --git a/evaluate_shell b/evaluate_shell new file mode 100644 index 0000000..47836d6 --- /dev/null +++ b/evaluate_shell @@ -0,0 +1,19 @@ +mapfile -t files < <( + git -C "${user_repo}" diff HEAD..HEAD~1 --name-only \ + | grep -Ev '^(.academy|README.md)' + ) + +info() { + printf '%s[info]: %s\n' "${0}" "$*" >&2 +} +check_x() { + [[ -x "${1}" ]] \ + || chmod +x "${1}" +} + +info "Using default shell evaluator" +for file in "${files[@]}"; do + info "Running '${file}' ..." + check_x "${file}" + "$file" +done diff --git a/execute b/execute new file mode 100644 index 0000000..4e7b607 --- /dev/null +++ b/execute @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +main() { + declare -r RESULTS="${WORKING_DIR}/.results" + declare -r BADGES_TXT="${RESULTS}/badges.txt" + declare -r GENERATED_TXT="${RESULTS}/generated.txt" + + mkdir -p "${RESULTS}" + + [[ -f "${DIR}/pre-execute_${ACADEMY_LANG}" ]] \ + && source "${DIR}/pre-execute_${ACADEMY_LANG}" + + if [[ -f "${DIR}/execute_${ACADEMY_LANG}" ]]; then + source "${DIR}/execute_${ACADEMY_LANG}" + else + if [[ -z "${ACADEMY_LANG}" ]]; then + warning "Undefined ACADEMY_LANG variable. Execution skipped." + else + exception "Missing execute script 'execute_${ACADEMY_LANG}' for selected language." + fi + fi + + [[ -f "${DIR}/post-execute_${ACADEMY_LANG}" ]] \ + && source "${DIR}/post-execute_${ACADEMY_LANG}" + + date +%s > "${GENERATED_TXT}" + printf -- 'age: %s\n' "$(<"${GENERATED_TXT}")" >> "${BADGES_TXT}" +} + +main "$@" diff --git a/execute_shell b/execute_shell new file mode 100644 index 0000000..c2fa3a0 --- /dev/null +++ b/execute_shell @@ -0,0 +1,19 @@ +mapfile -t files < <( + git -C "${user_repo}" diff HEAD..HEAD~1 --name-only \ + | grep -Ev '^(.academy|README.md)' + ) + +info() { + printf '%s[info]: %s\n' "${0}" "$*" >&2 +} +check_x() { + [[ -x "${1}" ]] \ + || chmod +x "${1}" +} + +info "Using default shell executor" +for file in "${files[@]}"; do + info "Running '${file}' ..." + check_x "${file}" + "$file" +done diff --git a/git_functions b/git_functions index 09786e5..492ecbd 100755 --- a/git_functions +++ b/git_functions @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash git_repo_exists() { declare -r dir="${1:-.}" diff --git a/gitlab-stages.yml b/gitlab-stages.yml index 8e5f31c..dedd5e9 100644 --- a/gitlab-stages.yml +++ b/gitlab-stages.yml @@ -1,22 +1,21 @@ +stages: + - build + - manage + - run + - post-process variables: - ACADEMY_LANG: "java" - BASE_RUNNER_TAG: "java" - -workflow: - rules: - - if: $ACADEMY_LANG == "java" - variables: - RUNNER_TAG: "java" - MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository" - - if: $ACADEMY_LANG == "oracle" - variables: - RUNNER_TAG: "oracle" + ACADEMY_DEFAULT_DOCKER_IMAGE: "internetguru/academy:latest" + ACADEMY_DOCKER_IMAGE: ${ACADEMY_DEFAULT_DOCKER_IMAGE} + ACADEMY_CACHE: ".academy-cache" cache: - key: mvn - paths: - - .m2/repository + - key: academy-cache + paths: + - $ACADEMY_CACHE + - key: artifacts + paths: + - .results/ .before_script_global: &global_init | # fix TERM @@ -26,23 +25,20 @@ cache: # set git user git config --global user.email "${GITLAB_USER_EMAIL}" git config --global user.name "Runner = ${CI_RUNNER_DESCRIPTION}" + mkdir -p ${ACADEMY_CACHE} export ACADEMY_DIR="$(mktemp -d)" - revision=$(grep -oP "[^/]+(?=/gitlab-stages)" .gitlab-ci.yml) + revision=$(grep -oP -m1 "[^/]+(?=/gitlab-stages)" .gitlab-ci.yml) git clone --single-branch --branch ${revision} https://github.com/InternetGuru/academy ${ACADEMY_DIR} # e.g. /umiami/george/csc220 - declare -r namespace="$(dirname "${PWD}" | cut -d/ -f3-)" + declare -r namespace="${CI_PROJECT_NAMESPACE}" declare -r project_name="$(basename "${PWD}")" + declare -r user_namespace="${namespace}/${CI_COMMIT_REF_NAME}/${project_name}" # simplify project remote url git remote set-url --push origin "${namespace}/${project_name}.git" # set permissions declare ACADEMY=${ACADEMY_DIR}/academy chmod +x ${ACADEMY_DIR}/* - declare PREFIX - if [[ -n "${ACADEMY_PREFIX}" ]]; then - PREFIX="${ACADEMY_PREFIX}" - else - PREFIX="${project_name}-" - fi + declare DASHBOARD_PROJECT_NAME="${project_name}" .before_script_token: &validate_token | # prepare acccess token for the distribution script @@ -59,6 +55,12 @@ cache: # commit move to keep git status empty (do not push) git commit -m "move users file" -- "ACADEMY_ASSIGN" fi + # do not distribute ACADEMY_ALLOW file + if [[ -f "ACADEMY_ALLOW" ]]; then + mv "ACADEMY_ALLOW" .. + # commit move to keep git status empty (do not push) + git commit -m "move allow file" -- "ACADEMY_ALLOW" + fi declare users if [[ -n "${ACADEMY_ASSIGN}" ]]; then users="${ACADEMY_ASSIGN}" @@ -70,46 +72,34 @@ cache: users="$(tr ',' ' ' <<< "${users}")" collect: + image: ${ACADEMY_DEFAULT_DOCKER_IMAGE} + stage: manage tags: - - ${RUNNER_TAG} - stage: build + - docker before_script: - *validate_token - *global_init script: # set token file (will be deprecated) - echo "${ACADEMY_GITLAB_ACCESS_TOKEN}" > ${HOME}/.ACADEMY_GITLAB_ACCESS_TOKEN - # process files distributed from current branch - - declare user_namespace="${namespace}/${CI_COMMIT_REF_NAME}" # checkout to ACADEMY_SOLUTION branch - | if [[ -z "${ACADEMY_SOLUTION}" ]]; then - ACADEMY_SOLUTION='master' + ACADEMY_SOLUTION="${CI_DEFAULT_BRANCH}" fi git fetch --all git checkout "${ACADEMY_SOLUTION}" - # prepare deadline and editable options - - declare deadline='' - - declare editable='' - - | - if [[ -n "${ACADEMY_DEADLINE}" ]]; then - deadline="--deadline '${ACADEMY_DEADLINE}'" - fi - if [[ -n "${ACADEMY_EDITABLE}" ]]; then - editable="--editable '${ACADEMY_EDITABLE}'" - fi # run collect for given or all branches - mkdir -p '.collect' - *get_users - | - ${ACADEMY} collect ${deadline} ${editable} \ + ${ACADEMY} collect \ --namespace "${user_namespace}" \ - --prefix "${PREFIX}" \ --output-dir '.collect' \ <<< "${users}" \ | tee 'summary.txt' # output variables and summary - - mv 'summary.txt' '.collect/' + #- mv 'summary.txt' '.collect/' - | echo '#######################' echo "ACADEMY_DEADLINE: '${ACADEMY_DEADLINE}'" @@ -117,70 +107,55 @@ collect: echo "ACADEMY_SOLUTION: '${ACADEMY_SOLUTION}'" echo "summary.txt: ${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/.collect/summary.txt" echo '#######################' - when: manual + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + when: always + - if: $CI_COMMIT_BRANCH == "source" + when: never + - exists: + - "ACADEMY_ASSIGN" + when: manual + - when: never artifacts: paths: - .collect/ expire_in: 1 year allow_failure: true -evaluate: - tags: - - ${RUNNER_TAG} - stage: build - before_script: - - *global_init - - | - # skip initial commits - if [[ $(git rev-list --all --count) -le 2 ]]; then - exit 0 - fi - # do not evaluate ACADEMY_ASSIGN changes only - if [[ "$(git diff HEAD..HEAD~1 --name-only)" == "ACADEMY_ASSIGN" ]]; then - echo "Project evaluation stoped due to changes in ACADEMY_ASSIGN only" - exit 1 - fi - script: - - ${ACADEMY} evaluate - artifacts: - paths: - - .results/ - expire_in: 1 year - rules: - - if: '$ACADEMY_EVALUATE == "always"' - when: always - - when: manual - distribute: + image: ${ACADEMY_DEFAULT_DOCKER_IMAGE} + stage: manage tags: - - ${BASE_RUNNER_TAG} - stage: build + - docker + cache: [] before_script: - *validate_token - *global_init script: - # e.g. fall20 - - declare -r branch="$(git rev-parse --abbrev-ref HEAD)" # set token file (will be deprecated) - echo "${ACADEMY_GITLAB_ACCESS_TOKEN}" > ${HOME}/.ACADEMY_GITLAB_ACCESS_TOKEN - *get_users # distribute current project among all users - | - ${ACADEMY} distribute --update-links --namespace "${namespace}/${branch}" \ + ${ACADEMY} distribute --update-links --namespace "${user_namespace}" \ --process-issues "${ACADEMY_ISSUES:-${CI_COMMIT_REF_NAME}}" \ - --prefix "${PREFIX}" \ <<< "${users}" - cache: [] rules: + - if: $CI_COMMIT_BRANCH == "source" + when: never - if: '$ACADEMY_DISTRIBUTE == "always"' when: always - - when: manual + - exists: + - "ACADEMY_ASSIGN" + when: manual + - when: never allow_failure: true measure: + image: ${ACADEMY_DEFAULT_DOCKER_IMAGE} + stage: manage tags: - - ${BASE_RUNNER_TAG} - stage: build + - docker before_script: - *validate_token - *global_init @@ -192,8 +167,6 @@ measure: fi # set token file (will be deprecated) - echo "${ACADEMY_GITLAB_ACCESS_TOKEN}" > ${HOME}/.ACADEMY_GITLAB_ACCESS_TOKEN - # process files distributed from current branch - - declare -r user_namespace="${namespace}/${CI_COMMIT_REF_NAME}" # download moss script - curl -o /usr/local/bin/moss "${ACADEMY_MOSSURL}" - chmod +x /usr/local/bin/moss @@ -204,7 +177,7 @@ measure: # copy files from ACADEMY_SOLUTION branch to measure - | if [[ -z "${ACADEMY_SOLUTION}" ]]; then - ACADEMY_SOLUTION='master' + ACADEMY_SOLUTION="${CI_DEFAULT_BRANCH}" fi git fetch --all git --work-tree="${measure_output}" checkout -f "origin/${ACADEMY_SOLUTION}" -- . @@ -212,9 +185,116 @@ measure: - *get_users - | ${ACADEMY} measure --output-dir "${measure_output}" \ - --namespace "${user_namespace}" \ - --prefix "${PREFIX}" \ + --namespace "${user_namespace}" <<< "${users}" - cache: [] - when: manual + rules: + - if: $CI_COMMIT_BRANCH == "source" + when: never + - exists: + - "ACADEMY_ASSIGN" + when: manual + - when: never allow_failure: true + +execute: + image: ${ACADEMY_DOCKER_IMAGE} + stage: run + tags: + - docker + before_script: + - *global_init + - | + # skip initial commits + if [[ $(git rev-list --all --count) -le 1 ]]; then + exit 0 + fi + # do not evaluate ACADEMY_ASSIGN changes only + if [[ "$(git diff HEAD..HEAD~1 --name-only)" == "ACADEMY_ASSIGN" ]]; then + echo "Project execution stoped due to changes in ACADEMY_ASSIGN only" + exit 1 + fi + script: + - echo '%% job start execute %%' + - ${ACADEMY} execute + - echo '%% job end execute %%' + artifacts: + paths: + - .results/ + expire_in: 1 year + rules: + - if: $CI_COMMIT_BRANCH == "source" + when: never + - if: '$ACADEMY_EXECUTE == "always"' + when: always + - when: manual + allow_failure: true + +evaluate: + image: ${ACADEMY_DOCKER_IMAGE} + stage: run + tags: + - docker + before_script: + - *global_init + - | + # skip initial commits + if [[ $(git rev-list --all --count) -le 1 ]]; then + exit 0 + fi + # do not evaluate ACADEMY_ASSIGN changes only + if [[ "$(git diff HEAD..HEAD~1 --name-only)" == "ACADEMY_ASSIGN" ]]; then + echo "Project evaluation stoped due to changes in ACADEMY_ASSIGN only" + exit 1 + fi + script: + - echo '%% job start evaluate %%' + - ${ACADEMY} evaluate + - echo '%% job end evaluate %%' + artifacts: + paths: + - .results/ + expire_in: 1 year + rules: + - if: $CI_COMMIT_BRANCH == "source" + when: never + - if: '$ACADEMY_EVALUATE == "always"' + when: always + - when: manual + allow_failure: true + +meta: + image: ${ACADEMY_DEFAULT_DOCKER_IMAGE} + stage: post-process + tags: + - docker + before_script: + - *global_init + script: + - | + echo "${ACADEMY_DASHBOARD}/${namespace}/${DASHBOARD_PROJECT_NAME}" + # clear dashboard cache + curl "${ACADEMY_DASHBOARD}/${namespace}/${DASHBOARD_PROJECT_NAME}" \ + -I -H "Cache-Control: no-cache" -H "Accept: application/json" + # create age badge + mkdir -p .results + source "${ACADEMY_DIR}/badges" + WORKING_DIR='.' + badge_generate 'Age' "$(date +"%Y-%m-%dT%H:%M:%S%z")" 'blue' + badge_generate 'Updates' 'unknown' + badge_generate 'Status' 'unknown' + - | + for badge in .results/badge-*.svg; do + [[ "${badge}" =~ ".results/badge-"([^-]+)-(.*)".svg" ]] + job="${BASH_REMATCH[1]}" + badge_name="${BASH_REMATCH[2]}" + printf -- '![%s](%s/builds/artifacts/%s/raw/%s?job=%s)\n' \ + "${badge_name}" "${CI_PROJECT_URL}" "${CI_COMMIT_REF_NAME}" "${badge}" "${job}" + done + rules: + - if: $CI_COMMIT_BRANCH == "source" + when: never + - when: always + artifacts: + paths: + - .results/ + expire_in: 1 year diff --git a/gitlab_api b/gitlab_api index d378eb5..ce6a13f 100755 --- a/gitlab_api +++ b/gitlab_api @@ -1,30 +1,73 @@ -#!/bin/bash +#!/usr/bin/env bash # shellcheck disable=SC2034 -declare -r GITLAB_URL="${CI_SERVER_HOST:-gitlab.com}" -declare -r TOKEN_FILE="${HOME}/.ACADEMY_GITLAB_ACCESS_TOKEN" +[[ -z "${GITLAB_URL}" ]] \ + && declare -r GITLAB_URL="${CI_SERVER_HOST:-gitlab.com}" +[[ -z "${TOKEN_FILE}" ]] \ + && declare -r TOKEN_FILE="${HOME}/.ACADEMY_GITLAB_ACCESS_TOKEN" gitlab_api() { declare -r url="${1}" declare data="${2}" - declare req='GET' - [[ -n "${data}" ]] \ - && req='POST' - [[ "${data}" == '-' ]] \ - && req='DELETE' \ - && data="{}" + declare method + declare req + declare form="" + case "${data}" in + '-') + method='DELETE' + ;; + 'PUT:'*) + method='PUT' + data="{${data#PUT:}}" + ;; + 'PUT-FILE:'*) + method='PUT' + declare form="${data#PUT-FILE:}" + ;; + '') + method='GET' + ;; + *) + method='POST' + esac # shellcheck disable=SC2155 - declare response="$(curl --silent --write-out '\n%{http_code}\n' \ - --header "Authorization: Bearer ${ACADEMY_GITLAB_ACCESS_TOKEN}" \ - --header 'Content-Type: application/json' \ - --request ${req} --data "${data:-{\}}" "https://${GITLAB_URL}/${url}")" + case "${method}" in + 'GET'|'DELETE') + declare response="$(curl --silent --write-out '\n%{http_code}\n' \ + --header "Authorization: Bearer ${ACADEMY_GITLAB_ACCESS_TOKEN}" \ + --request "${method}" \ + "https://${GITLAB_URL}/${url}" + )" + ;; + 'PUT'|'POST') + if [[ -z "${form}" ]]; then + declare response="$(curl --silent --write-out '\n%{http_code}\n' \ + --header "Authorization: Bearer ${ACADEMY_GITLAB_ACCESS_TOKEN}" \ + --header 'Content-Type: application/json' \ + --request "${method}" \ + --data "${data:-{\}}" \ + "https://${GITLAB_URL}/${url}" + )" + else + declare response="$(curl --silent --write-out '\n%{http_code}\n' \ + --header "Authorization: Bearer ${ACADEMY_GITLAB_ACCESS_TOKEN}" \ + --form "${form}" \ + --request "${method}" \ + "https://${GITLAB_URL}/${url}" + )" + fi + ;; + *) + exception "Unknown HTTP method '${method}'" 1 + esac # shellcheck disable=SC2155 declare status="$(sed -n '$p' <<< "${response}")" # shellcheck disable=SC2155 declare output="$(sed '$d' <<< "${response}")" - [[ "${status}" != 20* ]] \ + [[ "${status}" != 2?? ]] \ && printf -- '%s\n' "${output}" >&2 \ - && exception "Request status ${status}: ${url}" + && warning "Request status ${status}: ${url}" 0 \ + && return 1 printf -- '%s\n' "${output}" } get_token() { @@ -36,8 +79,13 @@ get_token() { } get_project_id() { declare -r ns="${1}" + declare project_id # shellcheck disable=SC2154 - gitlab_api "api/v4/projects/${ns//\//%2F}" | jq -r '.id' + project_id=$( gitlab_api "api/v4/projects/${ns//\//%2F}" | jq -r '.id' ) + [[ -n "${project_id}" ]] \ + || warning "Project '${ns}' does not exist" 1 \ + || return 1 + printf -- '%s\n' "${project_id}" } get_project_name() { declare -r ns="${1}" @@ -49,15 +97,29 @@ get_default_branch() { gitlab_api "api/v4/projects/${user_project_ns//\//%2F}" | jq -r '.default_branch' } project_exists() { + # Should get $1 instead of $user_project_ns + [[ -n "$1" ]] && local user_project_ns="${1}" get_project_id "${user_project_ns}" >/dev/null 2>&1 } +group_exists() { + get_group_id "${1}" >/dev/null 2>&1 +} get_group_id() { + local group_id declare -r group_name="${1//\//%2F}" - gitlab_api "api/v4/groups/${group_name}" | jq -r '.id' + group_id="$( gitlab_api "api/v4/groups/${group_name}" | jq -r '.id' )" + [[ -n "${group_id}" ]] \ + || warning "Group '${1}' does not exist" 1 \ + || return 1 + printf -- '%s\n' "${group_id}" +} +get_source_merge() { + local project_id="${1}" + gitlab_api "api/v4/projects/${project_id}/merge_requests?state=opened&source_branch=${SOURCE_BRANCH}" } request_exists() { # shellcheck disable=SC2154 - gitlab_api "api/v4/projects/${project_id}/merge_requests?state=opened&source_branch=${SOURCE_BRANCH}" \ + get_source_merge "${project_id}" \ | grep -qv "^\[\]$" } create_request() { @@ -72,8 +134,9 @@ create_user_project() { [[ -n "${user_id}" ]] \ && visibility='private' # shellcheck disable=SC2154 + # shellcheck disable=SC2153 gitlab_api 'api/v4/projects' \ - "{\"namespace_id\":\"${GROUP_ID}\", \"name\":\"${PROJECT_NAME} ${user_name}\", \"path\":\"${user_project_name}\", \"visibility\":\"${visibility}\"}" \ + "{\"namespace_id\":\"${GROUP_ID}\", \"name\":\"${user_name}\", \"path\":\"${user_project_name}\", \"visibility\":\"${visibility}\"}" \ | jq -r '.id' } get_role() { @@ -109,6 +172,11 @@ get_user_id() { gitlab_api "api/v4/users?username=${user_name}" \ | jq -r '.[] | .id' | sed 's/null//' } +get_user_name() { + # shellcheck disable=SC2154 + gitlab_api "api/v4/users?username=${user_name}" \ + | jq -r '.[] | .name' | sed 's/null//' +} create_ns() { declare -r ns="${1}" # shellcheck disable=SC2155 @@ -148,3 +216,137 @@ acquire_token() { # test ACADEMY_GITLAB_ACCESS_TOKEN gitlab_api "api/v4/projects" >/dev/null } +remove_project() { + declare -r project="${1}" + declare project_id + project_id="$( get_project_id "${project}" )" \ + || exception "Project '${project}' does not exist" 1 + gitlab_api "api/v4/projects/${project_id}" - +} +get_project_avatar() { + local project_id + local output_file + local avatar_url + declare -r ns="${1}" + project_id=$( get_project_id "$ns" ) \ + || warning "Source project '${ns}' does not exist" 1 \ + || return 1 + output_file=$( mktemp ) + avatar_url="$( gitlab_api "api/v4/projects/${project_id}" | jq -r .avatar_url )" + curl "${avatar_url}" > "${output_file}.${avatar_url##*.}" + printf '%s\n' "${output_file}.${avatar_url##*.}" +} +set_project_avatar() { + local project_id + declare -r ns="${1}" + local input_file="${2}" + project_id=$( get_project_id "${ns}" ) \ + || warning "Destination project '${ns}' does not exist" 1 \ + || return 1 + gitlab_api "api/v4/projects/${project_id}" "PUT-FILE:avatar=@${input_file}" +} +set_group_avatar() { + local group_id + declare -r ns="${1}" + local input_file="${2}" + group_id=$( get_group_id "${ns}" ) \ + || warning "Destination group '${ns}' does not exist" 1 \ + || return 1 + gitlab_api "api/v4/groups/${group_id}" "PUT-FILE:avatar=@${input_file}" +} +set_public_pages() { + local project_id="${1}" + gitlab_api "api/v4/projects/${project_id}" 'PUT:"pages_access_level":"public"' +} +copy_avatar() { + local type + case "${1}" in + # group avatar + '-g') + type='group' + shift + ;; + # project avatar (default) + *) + type='project' + esac + local avatar_file + local src="${1}" + local dst="${2}" + avatar_file="$( get_project_avatar "${src}" )" \ + || warning "Could not get source project '${src}' avatar" 1 \ + || return 1 + "set_${type}_avatar" "${dst}" "${avatar_file}" \ + || warning "Could not set target ${type} '${dst}' avatar" 1 \ + || return 1 + rm "${avatar_file}" +} +fork_project() { + local src_id + local data + local -A attr=( [visibility]="public" ) + local output + while (( $# > 0 )); do + case "${1}" in + -d) + attr[description]="${2}" + shift 2 + ;; + -n) + attr[name]="${2}" + if [[ -z "${attr[path]}" ]]; then + attr[path]="${attr[name],,}" + attr[path]="${attr[path]// /-}" + fi + shift 2 + ;; + -N) + attr[path]="${2}" + shift 2 + ;; + -p) + attr[namespace_path]="${2}" + shift 2 + ;; + -v) + attr[visibility]="${2}" + shift 2 + ;; + --) + shift + break + ;; + -*) + warninig "Unknown parameter '${1}'" + return 2 + ;; + *) + break + esac + done + [[ -n "${attr[name]}" ]] \ + || warning "Missing essential parameter 'name' (opt. -n)" 1 \ + || return 1 + [[ -n "${attr[namespace_path]}" ]] \ + || warning "Missing essential parameter 'namespace_path' (opt. -p)" 1 \ + || return 1 + group_exists "${attr[namespace_path]}" \ + || warning "Target group '${attr[namespace_path]}' does not exist" 1 \ + || return 1 + ! project_exists "${attr[namespace_path]}/${attr[path]}" \ + || warning "Project '${attr[namespace_path]}/${attr[path]}' already exists" 1 \ + || return 1 + [[ -n "${1}" ]] \ + || warning "Missing source repository to fork from" 1 \ + || return 1 + local src="${1}" + src_id="$( get_project_id "${src}" )" \ + || warning "Source repository '${src}' does not exist" \ + || return 1 + for key in "${!attr[@]}"; do + printf -v data -- '%s,"%s":"%s"' "$data" "${key}" "${attr[$key]}" + done + output=$( gitlab_api "api/v4/projects/${src_id}/fork" "{${data#,}}" ) \ + || return 1 + jq . <<< "${output}" +} diff --git a/iotest b/iotest new file mode 100755 index 0000000..62d2b42 --- /dev/null +++ b/iotest @@ -0,0 +1,143 @@ +#!/usr/bin/env bash + +io_test_exit() { + printf -- '%s' "${1}" >&2 + exit 1 +} +is_long() { + [[ $(wc -l < "${1}") -gt 1 || $(head -1 "${1}" | wc -m) -gt 15 ]] +} +assert() { + declare input output expected status + input="${stdin}" + output="${1}" + expected="${2}" + if [[ -n "${stdin_file}" ]]; then + iotest_folder_path="$(dirname "${IOTEST_FOLDER}")" + is_long "${stdin_file}" \ + && printf -- "Input file: %s\n" "${stdin_file#"${iotest_folder_path}/"}" \ + || printf -- "Input: %s\n" "${input}" + fi + printf -- "${3}\n" "${output}" + printf -- "${4}\n" "${expected}" + printf -- 'Status: ' + status=1 + [[ "${output}" == "${expected}" ]] \ + && printf -- 'success' \ + && status=0 \ + || printf -- 'failed' + printf -- "\n" + return $status +} +assert_status() { + assert "${1}" "$(< "${2}")" 'Status code: %s' 'Expected status code: %s' +} +assert_stdout() { + assert "$(< "${1}")" "$(< "${2}")" 'Output: %s' 'Expected output: %s' +} +assert_stderr() { + assert "$(< "${1}")" "$(< "${2}")" 'Error output: %s' 'Expected error output: %s' +} +io_test() { + declare -r test_folder="${1}" + # shellcheck disable=SC2155 + declare -r test_folder_name="$(basename "${test_folder}")" + declare -r test_name="${2}" + declare -r test_path="${test_folder}/${test_name}" + declare cmd="${IOTEST_CMD}" + # TODO check unsupported test extensions (how?) + echo "Test ${test_folder_name}/${test_name}" + # create cmd from template + # TODO: add reference to README + declare -A variables + # script varaibles + variables=( \ + ['FOLDER_NAME']="${test_folder_name%.*}"\ + ['RELATIVE_PATH']="${test_folder#"${IOTEST_FOLDER}/"}" + ) + # add filtered env variables + while read -r line; do + [[ -z "${line}" ]] \ + && continue + variables["$(cut -d= -f1 <<< "${line}")"]="$(cut -d= -f2- <<< "${line}")" + done <<< "$(printenv | grep '^\(ACADEMY_\|CI_\)')" + # replace variables in cmd + for key in "${!variables[@]}"; do + cmd="${cmd/@${key}@/${variables[${key}]}}" + done + # TODO use optarg file + declare out_file err_file status_code stdin stdin_file + out_file="$(mktemp)" + err_file="$(mktemp)" + # run cmd once + if [[ -f "${test_path}.stdin" ]]; then + stdin="$(< "${test_path}.stdin")" + stdin_file="${test_path}.stdin" + eval "cat '${test_path}.stdin' | ${cmd} >'${out_file}' 2>'${err_file}'" + status_code=$? + else + stdin='' + stdin_file='' + eval "${cmd} >'${out_file}' 2>'${err_file}'" + status_code=$? + fi + declare retrun_code=0 + # test status code + if [[ -f "${test_path}.sc" ]]; then + assert_status "${status_code}" "${test_path}.sc" \ + || retrun_code=1 + fi + # test stdout + if [[ -f "${test_path}.stdout" ]]; then + assert_stdout "${out_file}" "${test_path}.stdout" \ + || retrun_code=1 + fi + # test stderr + if [[ -f "${test_path}.stderr" ]]; then + echo "Stderr diff" + assert_stderr "${err_file}" "${test_path}.stderr" \ + || retrun_code=1 + fi + [[ "${status_code}" == 0 ]] \ + || cat "${err_file}" >&2 + rm "${out_file}" + rm "${err_file}" + return ${retrun_code} +} +run_io_tests() { + [[ ! -d "${IOTEST_FOLDER}" ]] \ + && return + # run iotests + declare -i passed=0 count=0 local_passed local_count + # search deepest folders only by -links 2 (folder with 2 hardlinks) + while read -r -d '' folder; do + local_passed=0 + local_count=0 + echo -e "%% file start ${folder#"${IOTEST_FOLDER}/"} %%" + # shellcheck disable=SC2012 + for name in $(ls "${folder}/" | rev | cut -d. -f2- | rev | sort -u); do + count+=1 + local_count+=1 + io_test "${folder}" "${name}" \ + && passed+=1 \ + && local_passed+=1 + printf -- "\n" + done + printf -- "%s summary: run %s, skipped 0, passed %s\n" "$(basename "${folder}")" \ + "${local_count}" "${local_passed}" + echo -e "%% file end ${folder#"${IOTEST_FOLDER}/"} %%\n" + done < <(find "${IOTEST_FOLDER}" -type d -links 2 -print0) + # generate summary and badge + printf -- "Summary: run %s, skipped 0, passed %s\n" "${count}" "${passed}" +} +main() { + [[ -n "${1}" ]] \ + || io_test_exit "Missing iotest folder" + [[ -n "${2}" ]] \ + || io_test_exit "Missing iotest cmd" + declare -r IOTEST_FOLDER="${1}" + declare -r IOTEST_CMD="${2}" + run_io_tests +} + +main "$@" \ No newline at end of file diff --git a/local-academy b/local-academy new file mode 100755 index 0000000..03ebb8f --- /dev/null +++ b/local-academy @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +command="${1}" + +DIR="$(dirname "${BASH_SOURCE[0]}")" +declare -r DIR +source "${DIR}/commons" + +[[ -n "${ACADEMY_GITLAB_ACCESS_TOKEN}" || -f "${HOME}/.ACADEMY_GITLAB_ACCESS_TOKEN" ]] \ + || exception "Undefined ACADEMY_GITLAB_ACCESS_TOKEN variable nor ${HOME}/.ACADEMY_GITLAB_ACCESS_TOKEN file" + +export ACADEMY_LANG="gallery" +export ACADEMY_EDITABLE="@([01][0-9]-*|showreel)/@(render[123].jpg|video.mp4)" +export CI_SERVER_HOST="gitlab.wr.cz" +PROJECT="fit/bi-ble/tasks" +GITLAB_PATH="https://oauth2:${ACADEMY_GITLAB_ACCESS_TOKEN}@${CI_SERVER_HOST}/${PROJECT}.git" + +[ -n "${ACADEMY_GITLAB_ACCESS_TOKEN}" ] \ + && printf '%s\n' "${ACADEMY_GITLAB_ACCESS_TOKEN}" > "${HOME}/.ACADEMY_GITLAB_ACCESS_TOKEN" + + +cleanup() { + rm -rf "${WORKING_DIR}" +} + +# Clone repository to WORKING_DIRECTORY (the same as at gitlab) +WORKING_DIR="$( mktemp -d )" +trap 'cleanup; exit' EXIT +git clone "${GITLAB_PATH}" "${WORKING_DIR}" + + +exec < "${WORKING_DIR}/ACADEMY_ASSIGN" + +case "${command}" in + collect) + cd "${WORKING_DIR}" || exception "Cannot acces '${WORKING_DIR}'" + "${DIR}/academy" collect --namespace="fit/bi-ble/main/tasks" --prefix="bi-ble-tasks-" --editable="${ACADEMY_EDITABLE}" + ;; + *) + "${DIR}/academy" help + exit 2 + ;; +esac diff --git a/local-academy-shell b/local-academy-shell new file mode 100755 index 0000000..0a4d2cf --- /dev/null +++ b/local-academy-shell @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +command="${1}" +#PROJECT="${2}" + +DIR="$(dirname "${BASH_SOURCE[0]}")" +declare -r DIR +source "${DIR}/commons" + +[[ -n "${ACADEMY_GITLAB_ACCESS_TOKEN}" || -f "${HOME}/.ACADEMY_GITLAB_ACCESS_TOKEN" ]] \ + || exception "Undefined ACADEMY_GITLAB_ACCESS_TOKEN variable nor ${HOME}/.ACADEMY_GITLAB_ACCESS_TOKEN file" + +#export ACADEMY_LANG="shell" +export ACADEMY_EDITABLE="*/run" +export CI_SERVER_HOST="gitlab.wr.cz" +export WORKING_DIR="/project" +#GITLAB_PATH="git@${CI_SERVER_HOST}:/${PROJECT}.git" + +[ -n "${ACADEMY_GITLAB_ACCESS_TOKEN}" ] \ + && printf '%s\n' "${ACADEMY_GITLAB_ACCESS_TOKEN}" > "${HOME}/.ACADEMY_GITLAB_ACCESS_TOKEN" + + +cleanup() { + rm -rf "${WORKING_DIR}" +} + +# Clone repository to WORKING_DIRECTORY (the same as at gitlab) +#WORKING_DIR="$( mktemp -d )" +#trap 'cleanup; exit' EXIT +#git clone "${GITLAB_PATH}" "${WORKING_DIR}" + +[[ -f "${WORKING_DIR}/ACADEMY_ASSIGN" ]] \ +&& exec < "${WORKING_DIR}/ACADEMY_ASSIGN" + +case "${command}" in + collect|execute|evaluate) + cd "${WORKING_DIR}" || exception "Cannot acces '${WORKING_DIR}'" + "${DIR}/academy" "${command}" --namespace="bi-skj/b222/shell-intro" --editable="${ACADEMY_EDITABLE}" + ;; + *) + "${DIR}/academy" help + exit 2 + ;; +esac diff --git a/measure b/measure index c6d96b3..63acd85 100755 --- a/measure +++ b/measure @@ -1,4 +1,5 @@ -#!/bin/bash +#!/usr/bin/env bash + measure() { # shellcheck disable=SC2155 [[ ! -t 0 ]] \ diff --git a/post-collect_gallery b/post-collect_gallery new file mode 100644 index 0000000..23412dc --- /dev/null +++ b/post-collect_gallery @@ -0,0 +1,38 @@ +#!/bin/bash + +echo "Post-collect: Gallery" + +shopt -s globstar nullglob + +declare -A themes=() + +for dir in "${local_gallery:?}"/*/*; do + dir="${dir#"${local_gallery}"/*/}" + themes["${dir}"]= +done + +themes_sorted=() +while read -r; do + themes_sorted+=( "$REPLY" ) +done < <( printf '%s\n' "${!themes[@]}" | sort ) + +for theme in "${themes_sorted[@]}"; do + printf '## %s\n\n' "${theme//-/ }" >> "${readme:?}" + for file in "${local_gallery}"/*/"${theme}"/*; do + printf '![%s](%s){width=19%%}\n' "${file#"${local_gallery}"}" "${file#"${local_gallery}"}" >> "${readme}" + done + printf '\n\n' >> "${readme}" +done + +cat "${readme}" >&2 + +# Add-commit-push galerii +git -C "${local_gallery}" remote -v +git -C "${local_gallery}" add . +git -C "${local_gallery}" commit -m "Update gallery" +git -C "${local_gallery}" push + +# Clean-up +rm -rf "${local_gallery}" +rm -rf .collect +mkdir .collect diff --git a/pre-collect_gallery b/pre-collect_gallery new file mode 100644 index 0000000..5bde3ba --- /dev/null +++ b/pre-collect_gallery @@ -0,0 +1,36 @@ +#!/bin/bash +echo "Pre-collect: Gallery" + +export gallery="git@gitlab.wr.cz:bi-ble/gallery.git" +export local_gallery="/tmp/gallery" +export readme="${local_gallery}/README.md" + +# Přidání klíče serveru a klienta +eval $( ssh-agent -s ) +ssh-keyscan gitlab.wr.cz >> ~/.ssh/known_hosts +base64 -d <<< "${SSH_PRIVATE_KEY}" | ssh-add - + +# První běh -> Naklonovat galerii, vytvořit README.md +rm -rf "${local_gallery}" 2>/dev/null +git clone "${gallery}" "${local_gallery}" +printf '%s\n\n' "# Galerie studentských prací BI-BLE" > "${readme}" +printf '%s\n' \ +"Problematika tvorby v Blenderu je rozdělena do jednotlivých témat." \ +"V rámci každého tématu je zadán rámcový úkol." \ +"Z každé úlohy by měly průběžně v semestru vznikat výstupy v podobě" \ +"statických obrázků a také krátká videa." \ +"" \ +>> "${readme}" + + +# Nastavit správný remote (proč?) +git -C "${local_gallery}" remote set-url origin "${gallery}" + +# Zjistit velikosti výchozích (placeholder) souborů +export file_pattern_data=" + [01][0-9]-*/render[123].jpg + [01][0-9]-*/video.mp4 + showreel/video.mp4 +" + +export file_size_data="$( stat -c '%n %s' ${file_pattern_data} )" diff --git a/pre-evaluate_java b/pre-evaluate_java new file mode 100644 index 0000000..903e16a --- /dev/null +++ b/pre-evaluate_java @@ -0,0 +1,15 @@ +declare -r IOTEST_FOLDER="${WORKING_DIR}/iotest" +declare -r IOTEST_LOG="${RESULTS}/iotest.log" +declare -r CHECKSTYLE_LOG="${RESULTS}/checkstyle.log" + +# deafults +badge_generate 'IO Tests' '0/0' +badge_generate 'Code Style' 'n/a' + +printf -- '' > "${IOTEST_LOG}" + +declare -r CHECKSTYLE_VERION="10.7.0" +declare -r CHECKSTYLE_FILE="${ACADEMY_CACHE}/checkstyle.jar" +declare -r CHECKSTYLE_CMD="java -jar ${CHECKSTYLE_FILE}" +[[ -f "${CHECKSTYLE_FILE}" ]] \ + || curl -sL "https://github.com/checkstyle/checkstyle/releases/download/checkstyle-${CHECKSTYLE_VERION}/checkstyle-10.7.0-all.jar" > "${CHECKSTYLE_FILE}" diff --git a/pre-evaluate_oracle b/pre-evaluate_oracle new file mode 100644 index 0000000..df11d3e --- /dev/null +++ b/pre-evaluate_oracle @@ -0,0 +1,10 @@ +declare -r IOTEST_FOLDER="${WORKING_DIR}/iotest" +declare -r IOTEST_LOG="${RESULTS}/iotest.log" +declare -r IOTEST_TXT="${RESULTS}/iotest.txt" +declare -r IOTEST_SVG="${RESULTS}/iotest.svg" + +# deafults for iotests +curl -so "${IOTEST_SVG}" 'https://img.shields.io/badge/IO%20Tests-0/0-gray' + +printf -- 'n/a' > "${IOTEST_TXT}" +printf -- '' > "${IOTEST_LOG}"