diff --git a/.github/workflows/build-oscript.yml b/.github/workflows/build-oscript.yml new file mode 100644 index 0000000..ae6cf55 --- /dev/null +++ b/.github/workflows/build-oscript.yml @@ -0,0 +1,37 @@ +name: Build Oscript Docker Image + +on: + push: + tags: + - 'oscript_*' # реагировать на теги, начинающиеся с executor_ + +jobs: + build: + runs-on: ubuntu-latest + env: + DOCKER_LOGIN: ${{ secrets.DOCKER_LOGIN }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker registry + uses: docker/login-action@v3 + with: + registry: ${{ secrets.DOCKER_REGISTRY_URL }} + username: ${{ secrets.DOCKER_LOGIN }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Set environment variables + run: | + echo "DOCKER_REGISTRY_URL=${{ secrets.DOCKER_REGISTRY_URL }}" >> "$GITHUB_ENV" + + - name: Build and Push Docker image + run: | + export OSCRIPT_VERSION="${GITHUB_REF#refs/tags/oscript_}" + echo "Собираем oscript версии ${OSCRIPT_VERSION}" + ./src/build-oscript.sh diff --git a/scripts/cleanup.sh b/scripts/cleanup.sh new file mode 100755 index 0000000..341d4d2 --- /dev/null +++ b/scripts/cleanup.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Разлогинивание из Docker +if [ -n "$DOCKER_REGISTRY_URL" ]; then + docker logout "$DOCKER_REGISTRY_URL" +else + docker logout +fi + +# Очистка переменных среды из .env +if [ -f .env ]; then + while IFS='=' read -r var _; do + # Удаляем пробелы и префикс export, если есть + var=$(echo "$var" | sed -e 's/^export[[:space:]]*//') + if [[ $var != "" && $var != \#* ]]; then + unset "$var" + fi + done < .env +fi + +echo "Очистка завершена." \ No newline at end of file diff --git a/scripts/docker_login.sh b/scripts/docker_login.sh new file mode 100755 index 0000000..29d29fa --- /dev/null +++ b/scripts/docker_login.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Проверка наличия необходимых переменных среды +if [[ -z "$DOCKER_REGISTRY_URL" || -z "$DOCKER_LOGIN" || -z "$DOCKER_PASSWORD" ]]; then + echo "Ошибка: Необходимо установить переменные среды DOCKER_REGISTRY_URL, DOCKER_LOGIN и DOCKER_PASSWORD." + exit 1 +fi + +echo "$DOCKER_PASSWORD" | docker login "$DOCKER_REGISTRY_URL" -u "$DOCKER_LOGIN" --password-stdin + +if [[ $? -eq 0 ]]; then + echo "Успешная авторизация в $DOCKER_REGISTRY_URL" +else + echo "Ошибка авторизации в $DOCKER_REGISTRY_URL" + exit 1 +fi \ No newline at end of file diff --git a/scripts/load_env.sh b/scripts/load_env.sh new file mode 100755 index 0000000..de8fc2f --- /dev/null +++ b/scripts/load_env.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Путь к .env файлу (по умолчанию в текущей директории) +ENV_FILE=".env" + +# Проверяем, существует ли файл +if [ ! -f "$ENV_FILE" ]; then + echo "Файл $ENV_FILE не найден." + exit 1 +fi + +# Загружаем переменные окружения из .env файла +set -a +source "$ENV_FILE" +set +a + +echo "Переменные окружения загружены из $ENV_FILE" \ No newline at end of file diff --git a/src/build-oscript.sh b/src/build-oscript.sh new file mode 100755 index 0000000..e0e15f3 --- /dev/null +++ b/src/build-oscript.sh @@ -0,0 +1,64 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -z "${CI:-}" ]; then + echo "The script is not running in CI" + source "${SCRIPT_DIR}/../scripts/load_env.sh" +else + echo "The script is running in CI"; +fi + +source "${SCRIPT_DIR}/../scripts/docker_login.sh" +source "${SCRIPT_DIR}/../tools/assert.sh" + +if [[ "${DOCKER_SYSTEM_PRUNE:-}" = "true" ]] ; +then + docker system prune -af +fi + +last_arg="." +if [[ ${NO_CACHE:-} = "true" ]] ; then + last_arg="--no-cache ." +fi + +oscript_version="${OSCRIPT_VERSION}" + +docker build \ + --pull \ + --build-arg OSCRIPT_VERSION="${oscript_version}" \ + -t "${DOCKER_REGISTRY_URL}/${DOCKER_LOGIN}/oscript:${oscript_version}" \ + -f "${SCRIPT_DIR}/oscript/Dockerfile" \ + ${last_arg} + +if ./tests/test-oscript.sh; then + container_version=$(docker run --rm "${DOCKER_REGISTRY_URL}/${DOCKER_LOGIN}/oscript:${oscript_version}" -v | head -n1 | awk '{print $NF}') + + if [[ -n "${container_version}" ]]; then + docker push "${DOCKER_REGISTRY_URL}/${DOCKER_LOGIN}/oscript:${oscript_version}" + + docker tag "${DOCKER_REGISTRY_URL}/${DOCKER_LOGIN}/oscript:${oscript_version}" "${DOCKER_REGISTRY_URL}/${DOCKER_LOGIN}/oscript:${container_version}" + docker push "${DOCKER_REGISTRY_URL}/${DOCKER_LOGIN}/oscript:${container_version}" + + if ! [[ "${oscript_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && ! [[ "${container_version}" =~ rc ]]; then + semver_tag=$(echo "${container_version}" | awk -F. '{print $1"."$2"."$3}') + if [[ -n "${semver_tag}" ]]; then + docker tag "${DOCKER_REGISTRY_URL}/${DOCKER_LOGIN}/oscript:${oscript_version}" "${DOCKER_REGISTRY_URL}/${DOCKER_LOGIN}/oscript:${semver_tag}" + docker push "${DOCKER_REGISTRY_URL}/${DOCKER_LOGIN}/oscript:${semver_tag}" + else + echo "Не удалось получить корректную semver версию из контейнера" + exit 1 + fi + fi + + else + echo "Не удалось получить версию из контейнера" + exit 1 + fi + source "${SCRIPT_DIR}/../scripts/cleanup.sh" +else + log_failure "ERROR: Tests failed. Docker image will not be pushed." + source "${SCRIPT_DIR}/../scripts/cleanup.sh" + exit 1 +fi +exit 0 \ No newline at end of file diff --git a/src/oscript/Dockerfile b/src/oscript/Dockerfile new file mode 100644 index 0000000..cc32749 --- /dev/null +++ b/src/oscript/Dockerfile @@ -0,0 +1,55 @@ +ARG DOCKER_REGISTRY_URL=library +ARG BASE_IMAGE=ubuntu +ARG BASE_TAG=24.04 + +FROM ${DOCKER_REGISTRY_URL}/${BASE_IMAGE}:${BASE_TAG} +LABEL maintainer="Iosif Pravets " + +# mono and oscript dependencies +ARG MONO_VERSION=6.12.0.182 + +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates \ + gnupg \ + dirmngr \ + wget \ + locales \ + && apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF \ + && echo "deb http://download.mono-project.com/repo/debian stable-buster/snapshots/$MONO_VERSION main" > /etc/apt/sources.list.d/mono-official-stable.list \ + && apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \ + mono-runtime \ + ca-certificates-mono \ + libmono-i18n4.0-all \ + libmono-system-runtime-serialization4.0-cil \ + libicu-dev \ + && rm -rf /etc/apt/sources.list.d/mono-official-stable.list \ + && apt-get update \ + && cert-sync --user /etc/ssl/certs/ca-certificates.crt \ + && rm -rf \ + /var/lib/apt/lists/* \ + /var/cache/debconf \ +RUN locale-gen ru_RU.UTF-8 \ + && localedef -i ru_RU -c -f UTF-8 -A /usr/share/locale/locale.alias ru_RU.UTF-8 + +# locale +ENV LANG=ru_RU.UTF-8 +ENV LANGUAGE=ru_RU:ru +ENV LC_ALL=ru_RU.UTF-8 + +# oscript +ARG OVM_VERSION=1.6.1 +ARG OSCRIPT_VERSION + +RUN wget https://github.com/oscript-library/ovm/releases/download/v${OVM_VERSION}/ovm.exe \ + && mv ovm.exe /usr/local/bin/ \ + && echo 'mono /usr/local/bin/ovm.exe "$@"' | tee /usr/local/bin/ovm \ + && chmod +x /usr/local/bin/ovm \ + && ovm install ${OSCRIPT_VERSION} \ + && ovm use ${OSCRIPT_VERSION} + +ENV OSCRIPTBIN=/root/.local/share/ovm/current/bin +ENV PATH="$OSCRIPTBIN:$PATH" + +ENTRYPOINT ["oscript"] \ No newline at end of file diff --git a/tests/test-oscript.sh b/tests/test-oscript.sh new file mode 100755 index 0000000..e1f5dd2 --- /dev/null +++ b/tests/test-oscript.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -e + +if [ -z "${CI-}" ]; then + echo "The script is not running in CI" + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + source "${SCRIPT_DIR}/../.env" +else + echo "The script is running in CI" +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +source "${SCRIPT_DIR}/../tools/assert.sh" + + +test_oscript_is_running() { + log_header "Test :: oscript is running" + + local expected actual + expected="1Script Execution Engine" + actual=$(docker run --rm $DOCKER_REGISTRY_URL/${DOCKER_LOGIN}/oscript:$OSCRIPT_VERSION) + + if assert_contain "$actual" "$expected"; then + log_success "oscript is running test passed" + else + log_failure "oscript is running test failed" + fi +} + +# test calls +test_oscript_is_running \ No newline at end of file diff --git a/tools/assert.sh b/tools/assert.sh new file mode 100644 index 0000000..cf09ab4 --- /dev/null +++ b/tools/assert.sh @@ -0,0 +1,508 @@ +#!/usr/bin/env bash + +##################################################################### +## +## title: Assert Extension +## +## description: +## Assert extension of shell (bash, ...) +## with the common assert functions +## Function list based on: +## http://junit.sourceforge.net/javadoc/org/junit/Assert.html +## Log methods : inspired by +## - https://natelandau.com/bash-scripting-utilities/ +## author: Mark Torok +## +## date: 07. Dec. 2016 +## +## license: MIT +## +##################################################################### + +if command -v tput &>/dev/null && tty -s; then + RED=$(tput setaf 1) + GREEN=$(tput setaf 2) + MAGENTA=$(tput setaf 5) + NORMAL=$(tput sgr0) + BOLD=$(tput bold) +else + RED=$(echo -en "\e[31m") + GREEN=$(echo -en "\e[32m") + MAGENTA=$(echo -en "\e[35m") + NORMAL=$(echo -en "\e[00m") + BOLD=$(echo -en "\e[01m") +fi + +# Выводит заголовок в терминал жирным пурпурным цветом. +# +# Arguments: +# +# * Текст заголовка для отображения. +# +# Outputs: +# +# * Печатает форматированный заголовок в STDERR. +log_header() { + printf "\n${BOLD}${MAGENTA}========== %s ==========${NORMAL}\n" "$@" >&2 +} + +# Выводит сообщение об успешном выполнении с зелёной галочкой в стандартный поток ошибок. +# +# Arguments: +# +# * Сообщение об успехе (строка или несколько строк) +# +# Outputs: +# +# * Сообщение с зелёной галочкой в STDERR. +# +# Example: +# +# log_success "Тест успешно пройден" +log_success() { + printf "${GREEN}✔ %s${NORMAL}\n" "$@" >&2 +} + +# Выводит сообщение об ошибке с красным крестиком в стандартный поток ошибок. +# +# Arguments: +# +# * Сообщение об ошибке для отображения. +# +# Outputs: +# +# * Сообщение об ошибке с цветовым выделением в STDERR. +# +# Example: +# +# ```bash +# log_failure "Тест не пройден" +# ``` +log_failure() { + printf "${RED}✖ %s${NORMAL}\n" "$@" >&2 +} + + +# Проверяет, равны ли ожидаемое и фактическое значения. +# +# Arguments: +# +# * expected — ожидаемое значение. +# * actual — фактическое значение. +# * msg (необязательно) — сообщение, выводимое при ошибке. +# +# Returns: +# +# * 0, если значения равны; 1, если не равны (и при этом выводится сообщение об ошибке, если оно указано). +# +# Example: +# +# assert_eq "foo" "$result" "Результат не совпадает с ожидаемым" +assert_eq() { + local expected="$1" + local actual="$2" + local msg="${3-}" + + if [ "$expected" == "$actual" ]; then + return 0 + else + [ "${#msg}" -gt 0 ] && log_failure "$expected == $actual :: $msg" || true + return 1 + fi +} + +# Проверяет, что значения не равны друг другу. +# +# Возвращает 0, если значения различны; иначе выводит сообщение об ошибке (если указано) и возвращает 1. +# +# Аргументы: +# +# * Ожидаемое значение +# * Фактическое значение +# * Необязательное сообщение для вывода при ошибке +# +# Пример: +# +# ```bash +# assert_not_eq "foo" "bar" "Значения совпадают" +# ``` +assert_not_eq() { + local expected="$1" + local actual="$2" + local msg="${3-}" + + if [ ! "$expected" == "$actual" ]; then + return 0 + else + [ "${#msg}" -gt 0 ] && log_failure "$expected != $actual :: $msg" || true + return 1 + fi +} + +# Проверяет, равно ли значение строке "true". +# +# Arguments: +# +# * actual — проверяемое значение. +# * msg — необязательное сообщение, выводимое при ошибке. +# +# Returns: +# +# * 0, если значение равно "true"; иначе 1. +assert_true() { + local actual="$1" + local msg="${2-}" + + assert_eq true "$actual" "$msg" + return "$?" +} + +# Проверяет, что переданное значение равно "false". +# +# Arguments: +# +# * actual — проверяемое значение. +# * msg — необязательное сообщение, выводимое при ошибке. +# +# Returns: +# +# * 0, если значение равно "false"; иначе 1. +assert_false() { + local actual="$1" + local msg="${2-}" + + assert_eq false "$actual" "$msg" + return "$?" +} + +# Проверяет, что два массива равны по длине и содержимому. +# +# Arguments: +# +# * Имя массива с ожидаемыми значениями (передаётся по ссылке) +# * Имя массива с фактическими значениями (передаётся по ссылке) +# * Необязательное сообщение, выводимое при ошибке +# +# Returns: +# +# * 0, если массивы идентичны по длине и значениям; 1 в противном случае. +# +# Example: +# +# ```bash +# arr1=(a b c) +# arr2=(a b c) +# assert_array_eq arr1[@] arr2[@] "Массивы не совпадают" +# ``` +assert_array_eq() { + + declare -a expected=("${!1-}") + # echo "AAE ${expected[@]}" + + declare -a actual=("${!2}") + # echo "AAE ${actual[@]}" + + local msg="${3-}" + + local return_code=0 + if [ ! "${#expected[@]}" == "${#actual[@]}" ]; then + return_code=1 + fi + + local i + for (( i=1; i < ${#expected[@]} + 1; i+=1 )); do + if [ ! "${expected[$i-1]}" == "${actual[$i-1]}" ]; then + return_code=1 + break + fi + done + + if [ "$return_code" == 1 ]; then + [ "${#msg}" -gt 0 ] && log_failure "(${expected[*]}) != (${actual[*]}) :: $msg" || true + fi + + return "$return_code" +} + +# Проверяет, что два массива не равны по длине или содержимому. +# +# Arguments: +# +# * Имя переменной ожидаемого массива (по ссылке) +# * Имя переменной фактического массива (по ссылке) +# * Необязательное сообщение для вывода при неудаче +# +# Returns: +# +# * 0 — если массивы различаются по длине или хотя бы одному элементу +# * 1 — если массивы идентичны по длине и содержимому +# +# Example: +# +# ```bash +# arr1=(a b c) +# arr2=(a b d) +# assert_array_not_eq arr1[@] arr2[@] "Массивы не должны совпадать" +# ``` +assert_array_not_eq() { + + declare -a expected=("${!1-}") + declare -a actual=("${!2}") + + local msg="${3-}" + + local return_code=1 + if [ ! "${#expected[@]}" == "${#actual[@]}" ]; then + return_code=0 + fi + + local i + for (( i=1; i < ${#expected[@]} + 1; i+=1 )); do + if [ ! "${expected[$i-1]}" == "${actual[$i-1]}" ]; then + return_code=0 + break + fi + done + + if [ "$return_code" == 1 ]; then + [ "${#msg}" -gt 0 ] && log_failure "(${expected[*]}) == (${actual[*]}) :: $msg" || true + fi + + return "$return_code" +} + +# Проверяет, является ли переданная строка пустой. +# +# Arguments: +# +# * Строка для проверки. +# * Необязательное сообщение, выводимое при ошибке. +# +# Returns: +# +# * 0, если строка пуста; 1 в противном случае. +# +# Example: +# +# ```bash +# assert_empty "" "Строка должна быть пустой" +# ``` +assert_empty() { + local actual=$1 + local msg="${2-}" + + assert_eq "" "$actual" "$msg" + return "$?" +} + +# Проверяет, что переданная строка не пуста. +# +# Arguments: +# +# * actual — строка для проверки. +# * msg — необязательное сообщение, выводимое при ошибке. +# +# Returns: +# +# * 0, если строка не пуста; 1, если строка пуста. +# +# Example: +# +# ```bash +# assert_not_empty "hello" "Строка не должна быть пустой" +# ``` +assert_not_empty() { + local actual=$1 + local msg="${2-}" + + assert_not_eq "" "$actual" "$msg" + return "$?" +} + +# Проверяет, содержит ли строка подстроку. +# +# Arguments: +# +# * haystack — строка, в которой выполняется поиск. +# * needle — подстрока для поиска. +# * msg — необязательное сообщение, выводимое при неудаче. +# +# Returns: +# +# * 0, если подстрока найдена или needle пустая; 1 в противном случае. +# +# Example: +# +# ```bash +# assert_contain "hello world" "world" # вернёт 0 +# assert_contain "hello world" "foo" "Не найдено" # вернёт 1 и выведет сообщение об ошибке +# ``` +assert_contain() { + local haystack="$1" + local needle="${2-}" + local msg="${3-}" + + if [ -z "${needle:+x}" ]; then + return 0; + fi + + if [ -z "${haystack##*$needle*}" ]; then + return 0 + else + [ "${#msg}" -gt 0 ] && log_failure "$haystack doesn't contain $needle :: $msg" || true + return 1 + fi +} + +# Проверяет, что подстрока не содержится в строке. +# +# Arguments: +# +# * haystack — строка, в которой выполняется поиск. +# * needle — подстрока, отсутствие которой проверяется. +# * msg — необязательное сообщение, выводимое при неудаче. +# +# Returns: +# +# * 0, если needle не содержится в haystack или needle пуста. +# * 1, если needle содержится в haystack (и при этом выводится сообщение об ошибке, если оно указано). +# +# Example: +# +# ```bash +# assert_not_contain "abcdef" "gh" # вернёт 0 +# assert_not_contain "abcdef" "cd" # вернёт 1 +# ``` +assert_not_contain() { + local haystack="$1" + local needle="${2-}" + local msg="${3-}" + + if [ -z "${needle:+x}" ]; then + return 0; + fi + + if [ "${haystack##*$needle*}" ]; then + return 0 + else + [ "${#msg}" -gt 0 ] && log_failure "$haystack contains $needle :: $msg" || true + return 1 + fi +} + +# Проверяет, что первое число больше второго. +# +# Arguments: +# +# * first — первое сравниваемое число +# * second — второе сравниваемое число +# * msg (необязательно) — сообщение, выводимое при неудаче проверки +# +# Returns: +# +# * 0, если first больше second; иначе 1 +# +# Example: +# +# ```bash +# assert_gt 5 3 "5 должно быть больше 3" +# ``` +assert_gt() { + local first="$1" + local second="$2" + local msg="${3-}" + + if [[ "$first" -gt "$second" ]]; then + return 0 + else + [ "${#msg}" -gt 0 ] && log_failure "$first > $second :: $msg" || true + return 1 + fi +} + +# Проверяет, что первое число больше или равно второму. +# +# Arguments: +# +# * first — первое сравниваемое число. +# * second — второе сравниваемое число. +# * msg (необязательно) — сообщение, выводимое при ошибке. +# +# Returns: +# +# * 0, если first >= second; иначе 1 и сообщение об ошибке. +# +# Example: +# +# assert_ge 10 5 "Ожидалось, что 10 больше или равно 5" +assert_ge() { + local first="$1" + local second="$2" + local msg="${3-}" + + if [[ "$first" -ge "$second" ]]; then + return 0 + else + [ "${#msg}" -gt 0 ] && log_failure "$first >= $second :: $msg" || true + return 1 + fi +} + +# Проверяет, что первое число меньше второго. +# +# Arguments: +# +# * first — первое сравниваемое число. +# * second — второе сравниваемое число. +# * msg (необязательно) — сообщение, выводимое при ошибке. +# +# Returns: +# +# * 0, если first < second; иначе 1 и сообщение об ошибке. +# +# Example: +# +# ```bash +# assert_lt 3 5 "3 должно быть меньше 5" +# ``` +assert_lt() { + local first="$1" + local second="$2" + local msg="${3-}" + + if [[ "$first" -lt "$second" ]]; then + return 0 + else + [ "${#msg}" -gt 0 ] && log_failure "$first < $second :: $msg" || true + return 1 + fi +} + +# Проверяет, что первое число меньше или равно второму. +# +# Arguments: +# +# * first — первое сравниваемое число +# * second — второе сравниваемое число +# * msg (необязательно) — сообщение, выводимое при неудаче +# +# Returns: +# +# * 0, если first ≤ second; иначе 1 и сообщение об ошибке (если указано) +# +# Example: +# +# assert_le 3 5 "3 не больше 5" # успешно +# assert_le 7 2 "7 не меньше 2" # завершится с ошибкой и выведет сообщение +assert_le() { + local first="$1" + local second="$2" + local msg="${3-}" + + if [[ "$first" -le "$second" ]]; then + return 0 + else + [ "${#msg}" -gt 0 ] && log_failure "$first <= $second :: $msg" || true + return 1 + fi +} \ No newline at end of file