commit 7f3a76c98480d90565d964d87ccd26b997cc97cc Author: amorozov <amorozov@tswf.io> Date: Tue Mar 4 15:35:36 2025 +0300 initial commit diff --git a/Example.Jenkinsfile b/Example.Jenkinsfile new file mode 100644 index 0000000..d709d16 --- /dev/null +++ b/Example.Jenkinsfile @@ -0,0 +1,115 @@ +def runGroovy(scriptName) { + sh "groovy -cp ./.ci/ ./.ci/script/${scriptName}.groovy" +} + +pipeline { + agent { + docker { + image 'git.tswf.io/docker-base-images/jdk14-alpine:0.1.4' + // Монтируем сокет для DooD. Так как сейчас все наши билд агенты запущены в SysBox, то это достаточно безопасно для хост системы. У Агента свой докер демон, его не жалко. + args '-v /var/run/docker.sock:/var/run/docker.sock' + } + } + stages { + // Настраиваем глобальные переменные окружения для сборки + stage('Prepare: Base envs') { + steps { + script { + env.CI_PROPERTIES_FILE_LOCATIONS = ".ci/ci.properties" + } + } + } + + // Убеждаемся, что тэг есть. В случае, если Jenkins не подсунул его автоматически (что обычная практика) - пробуем узнать сами. + stage('Prepare: Resolve build tag') { + steps { + script { + if (env.TAG_NAME == null) { + env.TAG_NAME = sh(returnStdout: true, script: "git tag --points-at HEAD").trim() + } + } + } + } + + // Простая сборка приложения "На каждый коммит", просто проверить что собирается + stage('Build: Regular') { + steps { + // TODO: Костыль. Надо скрипты адаптировать под сборку "На каждый коммит" и "Для релизов" + runGroovy 'docker_build' + } + } + + // Дополнительная сборка для релизов + stage('Build: Release Binaries And Deploy Image') { + when { + tag "release-*" + } + steps { + script { + // Пример смены набора докерфайлов, которые используются по-умолчанию + env.CI_DOCKER_FILES_PRESET="release" + } + runGroovy 'docker_build' + } + } + + // Пушим собранный образ в Docker Registry + stage('Publish: Publish a docker image') { + when { + tag "release-*" + } + steps { + // Переопределяем параметры скрипта из переменных окружения сборщика + script { + env.CI_DOCKER_REGISTRY_USERNAME = env.GITEA_USER + env.CI_DOCKER_REGISTRY_PASSWORD = env.GITEA_TOKEN + } + + runGroovy 'docker_push' + } + } + + // Создаем в Gitea релиз с бинарями + stage('Publish: Create gitea release') { + when { + tag "release-*" + } + steps { + // Переопределяем параметры скрипта из переменных окружения сборщика + script { + env.CI_DOCKER_REGISTRY_USERNAME = env.GITEA_USER + env.CI_DOCKER_REGISTRY_PASSWORD = env.GITEA_TOKEN + env.CI_GITEA_ORIGIN = env.GIT_URL + env.CI_GITEA_TOKEN = env.GITEA_TOKEN + } + + runGroovy 'docker_gitea_release_publish' + } + } + + // Добавляем SSH профиль для последующего деплоя и проверяем его + stage('Prepare: Configuring SSH profile') { + when { + tag "release-*" + } + steps { + // Переопределяем параметры скрипта из переменных окружения сборщика + script { + env.CI_DEPLOY_SSH_PROFILE_PRIVATE_KEY_BASE64 = env.SSH_KEY_BASE64 + } + + runGroovy 'deploy_ssh_profile_setup' + } + } + + // Подключаемся к серверу по SSH, обновляем тэг образа и перезапускаем + stage('Deploy: Update PROD docker-compose environment tag') { + when { + tag "release-*" + } + steps { + runGroovy 'deploy_compose_via_ssh' + } + } + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f7ea871 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Universal CI/CD Scripts + +CI/CD скрипты на груви для большинства проектов tswf. + +# Назначение + +Настройка всей сборки одним файлом - `ci.properties` + +# Как работает + +* Jenkins поднимает DooD контейнер (можно в целом заменить на DinD с SysBOX со временем), с груви и докером. + * CI скрипты на груви: + * Выполняют сборку проекта в Docker + * Если установлены специальные релизные тэги, то создает релиз в Gitea с нужными артефактами + * Если установлены специальные деплой-тэги, то деплоит через обновление тэга в docker-compose файле по SSH на целевом сервере нужный образ. + + +# Тэги +Понимание того, что не нужно ограничиваться простым билдом приходит из установленных тэгов. + +По-умолчанию тэги бывают двух видов: + +## Релизные тэги. + +Начинаются с префикса `release-`, или того, который пользователь переопределит в `.ci.properties` + +Если при сборке пайплайн видит, что установлен релизный тэг, то он постарается создать к собранному коммиту релиз в Gitea, а также запушить собранный образ Docker в указанную в `.ci.properties`registry. + +## Деплой тэги + +Начинаются с префикса `deploy-`, или переопределенных в `.ci.properties` + +Если при сборке коммита есть такой тэг - собранный докер образ будет опубликован и прописан в `docker-compose` через SSH файл согласно настройкам в `.ci.properies` \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..0b7000b --- /dev/null +++ b/build.gradle @@ -0,0 +1,48 @@ +plugins { + id 'java-library' + id 'groovy' + id 'maven-publish' +} + +group project.artifact_group +version project.artifact_version + +java { + withSourcesJar() + sourceCompatibility = targetCompatibility = project.java_min_version +} + +repositories { + mavenLocal() + mavenCentral() + jcenter() + maven { url 'https://jitpack.io' } + maven { url 'https://git.tswf.io/api/packages/public-repos/maven'} +} + +dependencies { + api group: "io.tswf.groovy.better-groovy", name: "better-groovy-scripting-shell", version: project.better_groovy_scripting_shell_version + api group: "io.tswf.groovy.better-groovy", name: "better-groovy-scripting-gitea", version: project.better_groovy_scripting_gitea_version +} + +// Apply to all groovy source sets +tasks.withType(GroovyCompile).each {task -> + task.doFirst { + // Append compile classpath to groovy classpath to access compile classpath dependencies in compiler configuration script + it.groovyClasspath += it.classpath + + // Transfers all groovyc options to compiler configuration script as system properties that can be accessed via System.getProperty("name") from the script file + for (def property : project.properties) { + if (property.key.startsWith("groovycOption")) { + task.groovyOptions.forkOptions.jvmArgs += "-D${property.key}=${property.value}" as String + } + } + } + + task.groovyOptions.javaAnnotationProcessing = true + task.options.encoding = "UTF-8" + task.groovyOptions.configurationScript = file('gradle/config/groovyc-static.groovy') +} + +compileJava.options.encoding = project.source_files_encoding + diff --git a/ci.example.properties b/ci.example.properties new file mode 100644 index 0000000..6c9baba --- /dev/null +++ b/ci.example.properties @@ -0,0 +1,35 @@ +# Git + ci.git.tag.deploy.prefixes=release-, deploy- + ci.git.tag.release.prefixes=release- + +# Docker + ci.docker.files.default=Deploy.Dockerfile + ci.docker.files.release=Binaries.Extra.Dockerfile, Deploy.Dockerfile + ci.docker.build.additional-args=--build-arg ARG1=${ENV1} --build-arg ARG2=${ENV2} + +## SSH + ci.deploy.ssh.profile=deploy-ssh-profile + ci.deploy.ssh.host=your.domain.or.ip + ci.deploy.ssh.port=22 + ci.deploy.ssh.username=your_login + +# Docker Push + ci.docker.image.push.files=Deploy.Dockerfile + ci.docker.image.base.name=your.registry/your-image + ci.docker.registry=your.registry + ci.docker.registry.username=your_registry_username + ci.docker.registry.password=your_registry_password + +## Docker-Compose Deploy + ci.deploy.docker-compose.base-command=docker compose + ci.deploy.docker-compose.dir=/path/to/your/docker-compose + ci.deploy.docker-compose.filename=docker-compose.yml + +# Gitea + ci.gitea.host=git.tswf.io + +## Releases + ci.release.artifacts.dockerfiles=Binaries.Extra.Dockerfile + ci.release.artifacts.is-required=true + ci.release.artifacts.binaries.libc.dockerfile.grab=/build/app.linux-386 + ci.release.artifact.app.linux-386.name=my-application-name.linux-386 diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..2a4dcc8 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,45 @@ +# Artifact Settings + artifact_group=io.tswf.ci + artifact_id=universal-ci-cd-scripts + artifact_version=1.0.0-SNAPSHOT + +# Dependencies + # Better Groovy + better_groovy_scripting_gitea_version=2.0.2-SNAPSHOT + better_groovy_scripting_shell_version=2.0.2-SNAPSHOT + +# Compilation settings + java_min_version=1.8 + source_files_encoding=UTF-8 + +# Groovy AST Settings + + ## Apply static compilation to all project + groovycOptionGlobalCompileStaticTransform=true + + ## Use GString as annotation members, needs to provide @Column(name = "some_name_${SOME_CONSTANT}) usage + groovycOptionGlobalGStringInAnnotationsTransform=true + + ## Call named method variants directly without map-parameterized generated proxy method + groovycOptionGlobalNamedVariantsDirectCallsTransform=true + + ## Use to add default paramertes cto named variant calls via map (for Groovy 3xx) + groovycOptionGlobalNamedVariantsDefaultsTransform=false + + ## Allow extensions in same sourceset (Exprerimental) + groovycOptionSameSourceExtensions=true + + ## Make @NullSafe annotations working + groovycOptionGlobalNullsafeChecks=true + + ## Make @CheckedMapConstructor annotation working + groovycOptionGlobalCheckMapConstructors=true + + ## Enable earler silent static type checking (may break static compilation!) + groovycOptionSilentTypeChecking=false + + ## Automatic checks slf4 log levels in runtime. E.g. log.debug("blablabla") will be if (log.isDebugEnabled()) { log.debug("blablabla") } + groovycOptionSLF4JLevels=true + + # Application settings + applicationMainClassName=none \ No newline at end of file diff --git a/gradle/config/groovyc-static.groovy b/gradle/config/groovyc-static.groovy new file mode 100644 index 0000000..c0ee0ab --- /dev/null +++ b/gradle/config/groovyc-static.groovy @@ -0,0 +1,63 @@ +import groovy.transform.CompileStatic +import io.tswf.groovy.bettergroovy.transforms.annotationgstrings.GStringsInAnnotations +import io.tswf.groovy.bettergroovy.transforms.directnamedargs.DirectNamedArgsCall +import io.tswf.groovy.bettergroovy.transforms.namedvariantdefaults.NamedVariantDefaults +import io.tswf.groovy.bettergroovy.transforms.scan.ScanSourceSet +import io.tswf.groovy.bettergroovy.transforms.extensions.SilentStaticTypeChecker +import io.tswf.groovy.bettergroovy.transforms.nullsafe.NullSafeVariables +import io.tswf.groovy.bettergroovy.transforms.validation.checkedmapconstructor.CheckMapConstructors +import io.tswf.groovy.bettergroovy.transforms.extensions.SameSourceSetExtensionMethods + +withConfig(configuration) { + ast(ScanSourceSet) + + if (getBooleanProperty("groovycOptionSilentTypeChecking")) { + ast(SilentStaticTypeChecker) + } + + + if (getBooleanProperty("groovycOptionSameSourceExtensions")) { + ast(SameSourceSetExtensionMethods) + } + + if (getBooleanProperty("groovycOptionGlobalGStringInAnnotationsTransform")) { + ast(GStringsInAnnotations) + } + + if (getBooleanProperty("groovycOptionGlobalNamedVariantsDirectCallsTransform")) { + ast(DirectNamedArgsCall) + } + + if (getBooleanProperty("groovycOptionGlobalNamedVariantsDefaultsTransform")) { + ast(NamedVariantDefaults) + } + + if (getBooleanProperty("groovycOptionGlobalNullsafeChecks")) { + ast(NullSafeVariables) + } + + if (getBooleanProperty("groovycOptionGlobalCheckMapConstructors")) { + ast(CheckMapConstructors) + } + + if (getBooleanProperty("groovycOptionSLF4JLevels")) { + ast(io.tswf.groovy.bettergroovy.transforms.slf4j.SLF4JLevels) + } + + if (getBooleanProperty("groovycOptionGlobalCompileStaticTransform")) { + ast(CompileStatic) + } +} + +boolean getBooleanProperty(String featureName) { + def stringProperty = getStringProperty(featureName) + if (stringProperty != null) { + return stringProperty.equalsIgnoreCase("true") + } + + return null +} + +String getStringProperty(String propertyName) { + return System.getProperty(propertyName) +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..01b8bf6 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..0e044e8 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Jul 23 20:37:40 CDT 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..c47844a --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = artifact_id + diff --git a/src/main/groovy/script/deploy_compose_via_ssh.groovy b/src/main/groovy/script/deploy_compose_via_ssh.groovy new file mode 100644 index 0000000..8b0696d --- /dev/null +++ b/src/main/groovy/script/deploy_compose_via_ssh.groovy @@ -0,0 +1,85 @@ +package script + +import util.CIProperties +import util.DockerImageNames +import util.DockerTags +import util.Dockerfiles + +/** + * Скрип для деплоя докер образа на сервер посредством обновления docker-compose файла и его перезапуска. + */ + +@GrabResolver(name='gitea', root='https://git.tswf.io/api/packages/public-repos/maven') +@Grapes([ + @Grab(group='io.tswf.groovy.better-groovy', module='better-groovy-scripting-shell', version='2.0.2-SNAPSHOT', changing = true), + @Grab(group='io.tswf.groovy.better-groovy', module='better-groovy-scripting-gitea', version='2.0.2-SNAPSHOT', changing = true) +]) +import util.GitTags +import util.GlobalProperties +import util.ScriptLog + +GlobalProperties.loadGlobalProperties() + +println """ +############################################################################# + DEPLOY via docker-compose script +############################################################################# +""" + +def deployGitTags = GitTags.getDeployTags() + +if (deployGitTags.isNullOrEmpty()) { + ScriptLog.printf "There is no tags to remote restart." + System.exit(1) +} + +def deployDockerfiles = Dockerfiles.getDeployDockerfiles() + +def dockerComposeBaseCommand = CIProperties.findProperty("deploy.docker-compose.base-command") + .orElse("docker compose") + +def dockerComposeDirectory = CIProperties.getProperty("deploy.docker-compose.dir") + +def dockerComposeFileName = CIProperties.findProperty("deploy.docker-compose.filename") + .orElse("docker-compose.yml") + +def sshDeployProfile = CIProperties.findProperty("deploy.ssh.profile") + .orElse("deploy") + +if (deployGitTags.size() > 1) { + throw new IllegalStateException("Can not deploy more than one git tag via docker-compose. Found multiple deploy tags: ${deployGitTags}") +} + +def deployGitTag = deployGitTags.first() + +ScriptLog.printf "Starting, total ${deployDockerfiles.size()} dockerfiles marked to be deployed... (tag: ${deployGitTag}, dockerfiles: ${deployDockerfiles})" + +for (def dockerfileName : deployDockerfiles) { + ScriptLog.printf "Deploying image for dockerfile named '${dockerfileName}' via ssh profile ${sshDeployProfile}" + + def dockerImageToUpdateTagBaseName = DockerImageNames.getImageName(dockerfileName) + + ScriptLog.printf "Docker image ${dockerImageToUpdateTagBaseName} base name will be used for dockerfile named '${dockerfileName}'" + + def dockerReleaseTagToSetup = DockerTags.getDockerTagPostfixForDockerfile( + GitTags.sanitizeTagFromPrefixes(deployGitTag, GitTags.deployPrefixes) + ) + + ScriptLog.printf "Updating docker-compose image tag to '${dockerReleaseTagToSetup}' in service '${dockerImageToUpdateTagBaseName}' in file '${dockerComposeFileName}' located in directory '${dockerComposeDirectory}'..." + + sh """ ssh -tt ${sshDeployProfile} "\\ + cd '${dockerComposeDirectory}' && \\ + sed -i 's|image: ${dockerImageToUpdateTagBaseName}:.*|image: ${dockerImageToUpdateTagBaseName}:${dockerReleaseTagToSetup}|g' '${dockerComposeFileName}'\\ + " + """ + + ScriptLog.printf "Service tag updated, restarting compose..." + + sh """ ssh -tt ${sshDeployProfile} "\\ + cd '${dockerComposeDirectory}' && \\ + ${dockerComposeBaseCommand} -f '${dockerComposeFileName}' up -d \\ + " + """ + + ScriptLog.printf "Deploying of docker tag '${dockerReleaseTagToSetup}' successfully completed!" +} \ No newline at end of file diff --git a/src/main/groovy/script/deploy_ssh_profile_setup.groovy b/src/main/groovy/script/deploy_ssh_profile_setup.groovy new file mode 100644 index 0000000..f3ea797 --- /dev/null +++ b/src/main/groovy/script/deploy_ssh_profile_setup.groovy @@ -0,0 +1,69 @@ +package script + +/** + * Скрипт для начальной конфигурации SSH профиля для деплоя + * Создает профиль для подключения к стенду, а также настраивает пользователя для этих целей. + */ + +@GrabResolver(name='gitea', root='https://git.tswf.io/api/packages/public-repos/maven') +@Grapes([ + @Grab(group='io.tswf.groovy.better-groovy', module='better-groovy-scripting-shell', version='2.0.2-SNAPSHOT', changing = true), + @Grab(group='io.tswf.groovy.better-groovy', module='better-groovy-scripting-gitea', version='2.0.2-SNAPSHOT', changing = true) +]) +import util.GlobalProperties +import util.ScriptLog + +println """ +############################################################################# + SSH Profile Setup Script +############################################################################# +""" + + +GlobalProperties.loadGlobalProperties() + +ScriptLog.printf "Creating .ssh directory..." + +def sshDir = new File("${System.getProperty("user.home")}/.ssh") +sshDir.mkdirsUnsafe() + +// Записываем приватный ключ + +ScriptLog.printf "Writing ssh private key..." + +def sshPrivateKey = sshDir.subFile("id_rsa") + +//TODO: может быть тут посолить? +def sshPrivateKeyBase64Content = System.getGlobalProperty("ci.deploy.ssh.profile.private-key-base64").removeAll(' ') +def sshPrivateKeyDecodedContent = Base64.decoder.decode(sshPrivateKeyBase64Content) + +sshPrivateKey.bytes = sshPrivateKeyDecodedContent + +sh 'chmod 400 ~/.ssh/id_rsa' + +// Записываем ssh config + +ScriptLog.printf "Writing ssh config..." + +def sshProfileName = System.getGlobalProperty("ci.deploy.ssh.profile") +def sshHost = System.getGlobalProperty("ci.deploy.ssh.host") +def sshPort = System.getGlobalProperty("ci.deploy.ssh.port") +def sshUsername = System.getGlobalProperty("ci.deploy.ssh.username") + +def sshConfigFile = sshDir.subFile("config") +sshConfigFile.text = """ +Host ${sshProfileName} + HostName ${sshHost} + User ${sshUsername} + Port ${sshPort} +""".stripIndent(false) + + +// Выполняем тестовое подключение к SSH, чтобы убедиться в корректности настройки. + +ScriptLog.printf "Validating ssh configuration..." + +sh "ssh ${sshProfileName} -o StrictHostKeyChecking=no -tt 'echo Testing connection!'" + +ScriptLog.printf "SSH profile configured successfully!" + diff --git a/src/main/groovy/script/docker_build.groovy b/src/main/groovy/script/docker_build.groovy new file mode 100644 index 0000000..bfe0385 --- /dev/null +++ b/src/main/groovy/script/docker_build.groovy @@ -0,0 +1,39 @@ +package script + +import util.Dockerfiles +import util.DockerBuildCommandFactory +import util.GlobalProperties +import util.ScriptLog + +/** + * Скрипт для тестового прогона сборки докер. Просто проверить, что собирается. + */ + +@GrabResolver(name='gitea', root='https://git.tswf.io/api/packages/public-repos/maven') +@Grapes([ + @Grab(group='io.tswf.groovy.better-groovy', module='better-groovy-scripting-shell', version='2.0.2-SNAPSHOT', changing = true), + @Grab(group='io.tswf.groovy.better-groovy', module='better-groovy-scripting-gitea', version='2.0.2-SNAPSHOT', changing = true) +]) +_ + +println """ +############################################################################# + Build Docker Image Script +############################################################################# +""" + +GlobalProperties.loadGlobalProperties() + +// Проверяем, что установлен docker +if (System.findExecutablesInPath(['docker']).isEmpty()) + throw new FileNotFoundException("Can't find installed docker-compose at that system!") + +ScriptLog.printf "Building docker image without tags..." + +for (def dockerfileName : Dockerfiles.getPresetDockerfiles()) { + def dockerCommand = DockerBuildCommandFactory.getBuildCommand(dockerfileName) + println "Using dockerfile '${dockerfileName}', full command: '$dockerCommand'" + sh dockerCommand +} + +ScriptLog.printf "Docker image successfully built!" diff --git a/src/main/groovy/script/docker_gitea_release_publish.groovy b/src/main/groovy/script/docker_gitea_release_publish.groovy new file mode 100644 index 0000000..bc4b916 --- /dev/null +++ b/src/main/groovy/script/docker_gitea_release_publish.groovy @@ -0,0 +1,143 @@ +package script + +/** + * + * Скрипт для создания или обновления релиза в gitea для всех релизных тегов текущего коммита. + * Ищет описание релиза в специальной папке, после чего добавляет его к релизу, если находит. + * + * Собирает докер образ, после чего из него копирует указанные как артефакты файлы и прикрепляет к релизу. + * + * Для работы требуется токен API Gitea и список нужных файлов, либо флаг того, что список может быть пустым. + */ + + +@GrabResolver(name='gitea', root='https://git.tswf.io/api/packages/public-repos/maven') +@Grapes([ + @Grab(group='io.tswf.groovy.better-groovy', module='better-groovy-scripting-shell', version='2.0.2-SNAPSHOT', changing = true), + @Grab(group='io.tswf.groovy.better-groovy', module='better-groovy-scripting-gitea', version='2.0.2-SNAPSHOT', changing = true) +]) +import io.tswf.gitea.parser.repository.GiteaRepositoryMetadataParser +import io.tswf.groovy.bettergroovy.scripting.v2.git.Git +import io.tswf.groovy.bettergroovy.scripting.v2.gitea.Gitea +import util.CIProperties +import util.ReleaseArtifactNames +import util.Dockerfiles +import util.DockerBuildCommandFactory +import util.GitTags +import util.GlobalProperties +import util.ReleaseArtifacts +import util.ReleaseDescriptionReader +import util.ScriptLog + +println """ +############################################################################# + Publish Release Artifacts Script +############################################################################# +""" + +GlobalProperties.loadGlobalProperties() + +def dockerFiles = Dockerfiles.getReleaseDockerfiles() + +def isAnyArtifactRequiredForRelease = CIProperties.findProperty("gitea.release.artifacts.is-required") + .map { it.toBoolean() } + .orElse(true) + +def releaseTags = GitTags.getReleaseTags() + +if (releaseTags.isNullOrEmpty()) { + ScriptLog.printf "There is no release tags to publish!" + System.exit(1) +} + +def giteaHost = CIProperties.findProperty("gitea.origin") // В рамках обратной совместимости + .orElseGet { + CIProperties.getProperty("gitea.host") // Актуальное название + } + +def giteaToken = CIProperties.getProperty("gitea.token") + +def giteaRepositoryParser = new GiteaRepositoryMetadataParser() +def giteaRepositoryMetadata = giteaRepositoryParser.parseRepositoryMetadataFromOrigin(giteaHost) + +def artifactLocalFiles = new ArrayList<File>() + +for (def dockerfileName : dockerFiles) { + ScriptLog.printf "Collecting artifacts from dockerfile named '${dockerfileName}'" + + // Собираем докер образ и поднимаем контейнер, чтобы далее извлечь из него нужные артефакты. + def temporaryDockerImage = Random.randomString(40) + def temporaryDockerContainerName = "build_in_docker_${Random.randomString(25)}" + + // Подчищаем за собою + addShutdownHook { + println "Removing temporary docker container '${temporaryDockerContainerName}' and image '${temporaryDockerImage}'" + sh "docker rm -f ${temporaryDockerContainerName}" + sh "docker image rm -f ${temporaryDockerImage}" + } + + def temporaryBuildDir = new File("./gitea-release-publication/tmp/build/${Random.randomString(10)}/") + temporaryBuildDir.mkdirsUnsafe() + temporaryBuildDir.deleteOnExit() + + def dockerBuildCommand = DockerBuildCommandFactory.getBuildCommand(dockerfileName, "-t ${temporaryDockerImage}") + sh dockerBuildCommand + sh "docker create -it --name ${temporaryDockerContainerName} ${temporaryDockerImage} sh" + + def artifactsToPublish = ReleaseArtifacts.getArtifacts(dockerfileName) + + if (artifactsToPublish.isEmpty()) { + ScriptLog.printf "There is no artifacts was configured for dockerfile named '${dockerfileName}'" + } else { + ScriptLog.printf "Copying artifacts for dockerfile named '${dockerfileName}': ${artifactsToPublish.join('\n')}" + } + + // Копируем все артефакты для публикации из временного докер контейнера + artifactsToPublish.stream() + .map { artifactToPublishAbsolutePath -> + def artifactSimpleName = artifactToPublishAbsolutePath.split("/").last() + + def artifactLocalFile = temporaryBuildDir.subFile(artifactSimpleName) + sh "docker cp ${temporaryDockerContainerName}:${artifactToPublishAbsolutePath} ${artifactLocalFile.path}" + + artifactLocalFile + } + + // Переименовываем файл, если это настроено + .map { artifactFile -> + ReleaseArtifactNames.getNewArtifactName(artifactFile, dockerfileName) + .map { newName -> + def newFile = artifactFile.parentFile.subFile(newName) + ScriptLog.printf "Renaming artifact '${artifactFile.absoluteCanonicalFile.path}' to '${newName}' for dockerfile named '${dockerfileName}'" + artifactFile.renameTo(newFile) + + newFile + } + .orElse(artifactFile) + } + + .forEach { + artifactLocalFiles << it + } +} + +ScriptLog.printf "Total ${artifactLocalFiles.size()} artifacts collected: ${artifactLocalFiles}" + +if (artifactLocalFiles.isEmpty() && isAnyArtifactRequiredForRelease) { + ScriptLog.printf "Release artifacts required, but no one found!" + System.exit(1) +} + +def giteaRepository = Gitea.getCIRepositoryUtilityV1(giteaToken, giteaRepositoryMetadata) + +// Создаем релизы в гитее +releaseTags.forEach { releaseTag -> + def releaseName = GitTags.sanitizeTagFromPrefixes(releaseTag, GitTags.getReleasePrefixes()) + def releaseCommitSha = System.findGlobalProperty("ci.gitea.release.commit.short-sha") + .orElse(Git.commitShortSha) + + def releaseDescription = ReleaseDescriptionReader.readDescription(releaseName) + .orElse("") + + giteaRepository.createOrUpdateRelease(releaseTag, releaseCommitSha, releaseName, releaseDescription, artifactLocalFiles) +} \ No newline at end of file diff --git a/src/main/groovy/script/docker_push.groovy b/src/main/groovy/script/docker_push.groovy new file mode 100644 index 0000000..9a9faf0 --- /dev/null +++ b/src/main/groovy/script/docker_push.groovy @@ -0,0 +1,74 @@ +package script + +import util.DockerImageNames + +/** + * Скрипт для публикации докер образа при сборке. + */ + +@GrabResolver(name='gitea', root='https://git.tswf.io/api/packages/public-repos/maven') +@Grapes([ + @Grab(group='io.tswf.groovy.better-groovy', module='better-groovy-scripting-shell', version='2.0.2-SNAPSHOT', changing = true), + @Grab(group='io.tswf.groovy.better-groovy', module='better-groovy-scripting-gitea', version='2.0.2-SNAPSHOT', changing = true) +]) +import util.DockerLogin +import util.DockerBuildCommandFactory +import util.Dockerfiles +import util.GitTags +import util.GlobalProperties +import util.ScriptLog + +println """ +############################################################################# + Push Docker Images Script +############################################################################# +""" + +GlobalProperties.loadGlobalProperties() + +def gitReleaseTags = GitTags.getDeployTags() + +if (gitReleaseTags.isNullOrEmpty()) { + ScriptLog.printf "There is no release tags found!" + System.exit(1) +} +else + ScriptLog.printf "Found git release tags ${gitReleaseTags}" + +ScriptLog.printf "Starting docker publishing..." + +DockerLogin.perform() + +def dockerfilesToPush = System.findGlobalPropertyList("ci.docker.image.push.files").orElse([]) + +gitReleaseTags.each { gitTag -> + Dockerfiles.getPresetDockerfiles().each { dockerfileName -> + if (!dockerfilesToPush.isEmpty() && dockerfileName !in dockerfilesToPush) { + ScriptLog.printf "Skipping push of dockerfile '${dockerfileName}' because it is not in push list: ${dockerfilesToPush}" + return + } + + ScriptLog.printf "Building docker tag: '${gitTag}' with dockerfile named '${dockerfileName}'" + + def dockerImageBaseName = DockerImageNames.getImageName(dockerfileName) + + // Для докер-тегов образов используем гит-теги с отброшенным префиксом + def imageBaseTag = gitTag.replace(GitTags.getPrefix(), "") + def imageTag = DockerImageNames.getImageTag(imageBaseTag, dockerfileName) + + def imageFullName = "${dockerImageBaseName}:${imageTag}" + + ScriptLog.printf "Full docker image name will be '${imageFullName}'" + + def dockerBuildCommand = DockerBuildCommandFactory.getBuildCommand(dockerfileName, "-t ${imageFullName}") + + ScriptLog.printf "Executing: '${dockerBuildCommand}'" + + sh dockerBuildCommand + sh "docker push '${imageFullName}'" + + ScriptLog.printf "Docker tag ${imageTag} build and push completed with full image name: '${imageFullName}'" + } +} + +ScriptLog.printf "Publishing done!" \ No newline at end of file diff --git a/src/main/groovy/util/CIProperties.groovy b/src/main/groovy/util/CIProperties.groovy new file mode 100644 index 0000000..78de74e --- /dev/null +++ b/src/main/groovy/util/CIProperties.groovy @@ -0,0 +1,25 @@ +package util + +import groovy.transform.CompileStatic + +@CompileStatic +class CIProperties { + + public static final String CI_PROPERTIES_PREFIX = "ci" + + static String getProperty(String propertyName) { + return findProperty(propertyName).orElseThrow { new IllegalStateException("Required CI property named '${propertyName}' was not found!") } + } + + static Optional<String> findProperty(String propertyName) { + return System.findGlobalProperty("${CI_PROPERTIES_PREFIX}.${propertyName}") + } + + static List<String> getListProperty(String propertyName) { + return findListProperty(propertyName).orElseThrow { new IllegalStateException("Required CI property named '${propertyName}' was not found!") } + } + + static Optional<List<String>> findListProperty(String propertyName) { + return System.findGlobalPropertyList("${CI_PROPERTIES_PREFIX}.${propertyName}") + } +} diff --git a/src/main/groovy/util/DockerBuildCommandFactory.groovy b/src/main/groovy/util/DockerBuildCommandFactory.groovy new file mode 100644 index 0000000..3257029 --- /dev/null +++ b/src/main/groovy/util/DockerBuildCommandFactory.groovy @@ -0,0 +1,26 @@ +package util + +import groovy.transform.CompileStatic + +@CompileStatic +class DockerBuildCommandFactory { + + static String getBuildAdditionalArgs(String dockerfileName) { + def propertyNames = [ + "docker.build.${dockerfileName.toLowerCase()}.additional-args", + "docker.build.additional-args" + ] + + return propertyNames.stream() + .map { CIProperties.findProperty("ci.docker.build.additional-args").orNull() } + .filter { it != null } + .findFirst() + .orElse("") + } + + static String getBuildCommand(String dockerfileName, String additionalArgs = "", String context = ".") { + def dockerCommand = "docker build ${context} -f '${dockerfileName}' ${additionalArgs} ${getBuildAdditionalArgs(dockerfileName)}" + return dockerCommand + } + +} \ No newline at end of file diff --git a/src/main/groovy/util/DockerImageNames.groovy b/src/main/groovy/util/DockerImageNames.groovy new file mode 100644 index 0000000..e8a0b20 --- /dev/null +++ b/src/main/groovy/util/DockerImageNames.groovy @@ -0,0 +1,29 @@ +package util + +import groovy.transform.CompileStatic + +@CompileStatic +class DockerImageNames { + + static String getImageTag(String baseName, String dockerfile) { + def imageTagPostfix = CIProperties.findProperty("docker.image.tag.${dockerfile.toLowerCase()}.postfix").orElse("") + return "${baseName}${imageTagPostfix}" + } + + static String getImageName(String dockerfileName) { + def propertyNames = [ + "docker.image.${dockerfileName.toLowerCase()}.name", + "docker.image.base.name" + ] + + return propertyNames.stream() + .map { it.toString() } + .map { + CIProperties.findProperty(it).orNull() + } + .filter { it != null } + .findFirst() + .orElseThrow { new NoSuchElementException("There is no one property set: ${propertyNames}") } + } + +} diff --git a/src/main/groovy/util/DockerLogin.groovy b/src/main/groovy/util/DockerLogin.groovy new file mode 100644 index 0000000..20b8e5a --- /dev/null +++ b/src/main/groovy/util/DockerLogin.groovy @@ -0,0 +1,29 @@ +package util + +import groovy.transform.CompileStatic +import groovy.transform.Memoized + +@CompileStatic +class DockerLogin { + + static void perform() { + loginDockerInternal() + } + + @Memoized + private static Object loginDockerInternal() { + // Проверяем на всякий случай, что докер вообще установлен + if (System.findExecutablesInPath(['docker']).isEmpty()) + throw new FileNotFoundException("Can't find installed docker-compose at that system!") + + // Логинимся в Registry + ScriptLog.printf "Performing login to registry..." + def registryName = System.getGlobalProperty("ci.docker.registry") + def registryUser = System.getGlobalProperty("ci.docker.registry.username") + def registryPassword = System.getGlobalProperty("ci.docker.registry.password") + + sh "docker login $registryName -u $registryUser -p $registryPassword" + + ScriptLog.printf "Login into docker registry '${registryName}' successful!" + } +} \ No newline at end of file diff --git a/src/main/groovy/util/DockerTags.groovy b/src/main/groovy/util/DockerTags.groovy new file mode 100644 index 0000000..d80f36b --- /dev/null +++ b/src/main/groovy/util/DockerTags.groovy @@ -0,0 +1,17 @@ +package util; + +import groovy.transform.CompileStatic + +@CompileStatic +class DockerTags { + + static String getDockerTagForDockerfile(String dockerfileName, String baseTag) { + return "${baseTag}${getDockerTagPostfixForDockerfile(dockerfileName)}" + } + + static String getDockerTagPostfixForDockerfile(String dockerfileName) { + return CIProperties.findProperty("docker.tag.${dockerfileName}.prefix") + .orElse("") + } + +} diff --git a/src/main/groovy/util/Dockerfiles.groovy b/src/main/groovy/util/Dockerfiles.groovy new file mode 100644 index 0000000..80b5232 --- /dev/null +++ b/src/main/groovy/util/Dockerfiles.groovy @@ -0,0 +1,38 @@ +package util + +import groovy.transform.CompileStatic + +@CompileStatic +class Dockerfiles { + + static List<String> getPresetDockerfiles() { + def preset = CIProperties.findProperty("docker.files.preset") + .orElse("default") + + def presetDockerfiles = CIProperties.findListProperty("docker.files.${preset}") + .orElse([]) + + if (presetDockerfiles.isEmpty()) + return CIProperties.findListProperty("docker.files") + .orElse(["Dockerfile"]) + else + return presetDockerfiles + } + + static List<String> getReleaseDockerfiles() { + return CIProperties.findListProperty("release.artifacts.dockerfiles") + .orElseGet { + CIProperties.findListProperty("gitea.release.artifacts.dockerfiles") // Обратная совместимость + .orElseGet { + getPresetDockerfiles() + } + } + } + + static List<String> getDeployDockerfiles() { + return CIProperties.findListProperty("deploy.dockerfile.names") + .orElseGet { + getPresetDockerfiles() + } + } +} diff --git a/src/main/groovy/util/GitTags.groovy b/src/main/groovy/util/GitTags.groovy new file mode 100644 index 0000000..8cca2ba --- /dev/null +++ b/src/main/groovy/util/GitTags.groovy @@ -0,0 +1,44 @@ +package util + +import groovy.transform.CompileStatic +import io.tswf.groovy.bettergroovy.scripting.v2.git.Git + +@CompileStatic +class GitTags { + public static final String DEFAULT_RELEASE_TAG_PREFIX = "release-" + public static final String DEFAULT_DEPLOY_TAG_PREFIX = DEFAULT_RELEASE_TAG_PREFIX + + static Set<String> getDeployPrefixes() { + return CIProperties.findListProperty("git.tag.deploy.prefixes") + .orElse([DEFAULT_DEPLOY_TAG_PREFIX]) + .toSet() + } + + static Set<String> getReleasePrefixes() { + return CIProperties.findListProperty("git.tag.release.prefixes") + .orElse([DEFAULT_RELEASE_TAG_PREFIX]) + .toSet() + } + + static List<String> getTagsWithPrefix(Collection<String> prefixes) { + def matchingTags = Git.tags.stream() + .filter {tag -> prefixes.any { tag.startsWith(it) }} + .toList() + + return matchingTags + } + + static List<String> getReleaseTags() { + def releasePrefixes = getReleasePrefixes() + return getTagsWithPrefix(releasePrefixes) + } + + static List<String> getDeployTags() { + def deployPrefixes = getDeployPrefixes() + return getTagsWithPrefix(deployPrefixes) + } + + static String sanitizeTagFromPrefixes(String tag, Collection<String> prefixes) { + return tag.removeAll(prefixes) + } +} diff --git a/src/main/groovy/util/GlobalProperties.groovy b/src/main/groovy/util/GlobalProperties.groovy new file mode 100644 index 0000000..55b3b66 --- /dev/null +++ b/src/main/groovy/util/GlobalProperties.groovy @@ -0,0 +1,39 @@ +package util + +import groovy.transform.Memoized + +class GlobalProperties { + + @Memoized + static loadGlobalProperties() { + def propertiesFiles = System.findGlobalPropertyList("${CIProperties.CI_PROPERTIES_PREFIX}.properties.file.locations") + .orElse([".ci/ci.properties"]) + .map { new File(it) } + + if (propertiesFiles.isNotNullOrEmpty()) { + propertiesFiles.forEach { propertiesFile -> + if (propertiesFile.exists()) { + loadPropertiesFile(propertiesFile) + } else { + ScriptLog.printf "Skipping non-existing properties file '${propertiesFile.absoluteCanonicalFile.path}'!" + } + } + } + } + + static void loadPropertiesFile(File propertiesFile) { + if (propertiesFile == null) { + return + } + + if (!propertiesFile.exists()) { + throw new IOException("Could not find properties file '${propertiesFile}' (with absolute path: '${propertiesFile.absoluteCanonicalFile.path}')!") + } + + if (propertiesFile.isDirectory()) { + throw new IOException("Directory found instead properties file '${propertiesFile}' (with absolute path: '${propertiesFile.absoluteCanonicalFile.path}')!") + } + + System.registerGlobalConfiguration(propertiesFile) + } +} diff --git a/src/main/groovy/util/ReleaseArtifactNames.groovy b/src/main/groovy/util/ReleaseArtifactNames.groovy new file mode 100644 index 0000000..aa180c2 --- /dev/null +++ b/src/main/groovy/util/ReleaseArtifactNames.groovy @@ -0,0 +1,67 @@ +package util + +import groovy.transform.CompileStatic + +@CompileStatic +class ReleaseArtifactNames { + + static List<ArtifactNameResolver> artifactFileNameResolvers = new ArrayList<>() + + static Optional<String> getNewArtifactName(File file, String dockerfileName) { + def newArtifactName = artifactFileNameResolvers.stream() + .map { it.resolveArtifactNewName(file, dockerfileName) } + .filter { it != null } + .firstOrNull() + + return Optional.ofNullable(newArtifactName) + } + + static void registerArtifactResolver(ArtifactNameResolver resolver) { + artifactFileNameResolvers << resolver + } + + static { + registerArtifactResolver { file, dockerfile -> + CIProperties.findProperty("release.artifact.${dockerfile}.${replacePathInvalidChars(file.absoluteCanonicalFile.path)}.name").orNull() + } + + registerArtifactResolver { file, dockerfile -> + CIProperties.findProperty("release.artifact.${dockerfile}.${file.name}.name").orNull() + } + + registerArtifactResolver { file, dockerfile -> + CIProperties.findProperty("release.artifact.${replacePathInvalidChars(file.absoluteCanonicalFile.path)}.name").orNull() + } + + registerArtifactResolver { file, dockerfile -> + CIProperties.findProperty("release.artifact.${file.name}.name").orNull() + } + + // В рамках обратной совместимости + registerArtifactResolver { file, dockerfile -> + CIProperties.findProperty("gitea.release.artifact.${dockerfile}.${replacePathInvalidChars(file.absoluteCanonicalFile.path)}.name").orNull() + } + + registerArtifactResolver { file, dockerfile -> + CIProperties.findProperty("gitea.release.artifact.${dockerfile}.${file.name}.name").orNull() + } + + registerArtifactResolver { file, dockerfile -> + CIProperties.findProperty("gitea.release.artifact.${replacePathInvalidChars(file.absoluteCanonicalFile.path)}.name").orNull() + } + + registerArtifactResolver { file, dockerfile -> + CIProperties.findProperty("gitea.release.artifact.${file.name}.name").orNull() + } + } + + static String replacePathInvalidChars(String fileFullName) { + return fileFullName + .replace('\\', '_') + .replace('/', '_') + } + + static interface ArtifactNameResolver { + String resolveArtifactNewName(File artifactFile, String dockerfileName) + } +} \ No newline at end of file diff --git a/src/main/groovy/util/ReleaseArtifacts.groovy b/src/main/groovy/util/ReleaseArtifacts.groovy new file mode 100644 index 0000000..04fda30 --- /dev/null +++ b/src/main/groovy/util/ReleaseArtifacts.groovy @@ -0,0 +1,23 @@ +package util + +import groovy.transform.CompileStatic + +@CompileStatic +class ReleaseArtifacts { + + static List<String> getArtifacts(String dockerFile) { + def propertyNames = [ + "gitea.release.${dockerFile}.artifacts", // В рамках обратной совместимости + "gitea.release.artifacts", // В рамках обратной совместимости + "release.artifacts.${dockerFile}.grab", + "release.artifacts.grab", + ] + + return propertyNames.stream() + .map { it.toString() } + .map { CIProperties.findListProperty(it).orNull() } + .filter { it != null } + .map { (List<String>) it } + .findFirst().orElse([]) + } +} diff --git a/src/main/groovy/util/ReleaseDescriptionReader.groovy b/src/main/groovy/util/ReleaseDescriptionReader.groovy new file mode 100644 index 0000000..efc5b70 --- /dev/null +++ b/src/main/groovy/util/ReleaseDescriptionReader.groovy @@ -0,0 +1,27 @@ +package util + +import groovy.transform.CompileStatic + +@CompileStatic +class ReleaseDescriptionReader { + + static Optional<String> readDescription(String releaseName) { + def releaseDescriptionsDir = CIProperties.findProperty("release.descriptions.dir") + .orElse(".releases/changelog/") + .with { + new File(it) + } + + def releaseDescriptionFile = releaseDescriptionsDir.subFile("${releaseName}-changelog.md") + + if (releaseDescriptionFile.exists()) { + def releaseAbsPath = releaseDescriptionFile.absoluteCanonicalFile.path + ScriptLog.printf "Found description file for '$releaseName' release in '$releaseAbsPath'! It will be used for release creation." + return Optional.of(releaseDescriptionFile.getText("UTF-8")) + } else { + ScriptLog.printf "Release description file not found in '${releaseDescriptionFile.absoluteCanonicalFile.path}', falling back to default value..." + } + + return Optional.empty() + } +} diff --git a/src/main/groovy/util/ScriptLog.groovy b/src/main/groovy/util/ScriptLog.groovy new file mode 100644 index 0000000..3c0feff --- /dev/null +++ b/src/main/groovy/util/ScriptLog.groovy @@ -0,0 +1,8 @@ +package util + +class ScriptLog { + static void printf(Object text, Object... args) { + def callerClassName = new Throwable().getStackTrace()[1].getClassName().split("\\.").last() + println "[${callerClassName}]: ${String.format(String.valueOf(text), args)}" + } +}