ci: 添加 GitLab CI/CD 配置并实现自动化流程- 新增 .gitlab-ci.main.yml、.gitlab-ci.other.yml 和 .gitlab-ci.test.yml 文件

- 实现了 build、tag、publish、reset 和 mirror等阶段的自动化流程
- 添加了 check 和 test 分支的构建和发布配置
- 更新了 build.gradle.kts 和 settings.gradle.kts 文件,调整了项目配置
- 新增 MdcContextElement 和 CoroutineMdcWebFilter 类,用于协程中的 MDC 上下文传播
This commit is contained in:
gewuyou 2025-07-16 12:16:32 +08:00
parent 5f3e078e0f
commit 3bfcf98e21
11 changed files with 369 additions and 53 deletions

2
.gitignore vendored
View File

@ -21,7 +21,7 @@ build/
out/ out/
!**/src/main/**/out/ !**/src/main/**/out/
!**/src/test/**/out/ !**/src/test/**/out/
/temp
### Kotlin ### ### Kotlin ###
.kotlin .kotlin

10
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,10 @@
include:
- local: '/.gitlab/workflows/.gitlab-ci.test.yml'
rules:
- if: '$CI_COMMIT_BRANCH == "test"'
- local: '/.gitlab/workflows/.gitlab-ci.main.yml'
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
- local: '/.gitlab/workflows/.gitlab-ci.other.yml'
rules:
- if: '$CI_COMMIT_BRANCH != "main" && $CI_COMMIT_BRANCH != "test"'

View File

@ -0,0 +1,131 @@
stages:
- build
- tag
- publish
- reset
- mirror
variables:
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
before_script:
- rm -rf $GRADLE_USER_HOME/.tmp || true
# ✅ Build 阶段
build:
stage: build
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
cache:
key:
files:
- gradle/libs.versions.toml
- "**/*.gradle.kts"
prefix: lab-agent
paths:
- .gradle/caches/
- .gradle/wrapper/
- .gradle/kotlin-profile/
- .kotlin/
policy: pull-push
script:
- ./gradlew clean build
tags:
- java
# 🏷️ 自动打标签
tag:
stage: tag
needs: [ "build" ]
image: alpine:latest
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
script:
- set -euo pipefail
- apk add --no-cache git
- git config --global user.email "pipeline@${GITLAB_URL}"
- git config --global user.name "Project Pipeline Bot"
- git fetch --tags
- git fetch origin main
- MAIN_COMMIT=$(git rev-parse origin/main)
- echo "🔗 main commit -> $MAIN_COMMIT"
- LATEST_TAG=$(git tag --list '*' --sort=-v:refname | head -n1 || true)
- if [ -z "$LATEST_TAG" ]; then LATEST_TAG="0.0.0"; fi
- echo "🔖 最新 tag -> $LATEST_TAG"
- VERSION=${LATEST_TAG#v}
- MAJOR=$(echo "$VERSION" | cut -d. -f1)
- MINOR=$(echo "$VERSION" | cut -d. -f2)
- PATCH=$(echo "$VERSION" | cut -d. -f3)
- PATCH=$((PATCH + 1))
- NEW_TAG="${MAJOR}.${MINOR}.${PATCH}"
- echo "🏷️ 新 tag -> $NEW_TAG"
- if git tag --points-at "$MAIN_COMMIT" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' > /dev/null; then
echo "⏭️ 已存在 tag跳过创建";
exit 0;
fi
- git tag $NEW_TAG $MAIN_COMMIT
- git push https://oauth2:${PIPELINE_BOT_TOKEN}@${GITLAB_URL}/${CI_PROJECT_PATH}.git $NEW_TAG
- echo "✅ tag $NEW_TAG 已推送"
tags:
- java
# 📦 发布至 GitLab Maven 仓库
publish:
stage: publish
needs: [ "tag" ]
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
script:
- ./gradlew publishMavenJavaPublicationToGitLabRepository
tags:
- java
# 🔄 重建 test 分支
reset:
stage: reset
needs: [ "build" ]
image: alpine:latest
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
script:
- set -euo pipefail
- apk add --no-cache git
- git config --global user.email "pipeline@${GITLAB_URL}"
- git config --global user.name "Project Pipeline Bot"
- git clone --depth=1 --branch main https://oauth2:${PIPELINE_BOT_TOKEN}@${GITLAB_URL}/${CI_PROJECT_PATH}.git repo
- cd repo
- git checkout -B test
- git push origin test --force
- echo "✅ test 分支已重建完成"
tags:
- java
# Mirror to GitHub
mirror-to-github:
stage: mirror
needs: [ "build" ]
image: alpine:latest
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
script:
- set -euo pipefail
- apk add --no-cache git openssh
- git config --global user.name "Project Pipeline Bot"
- git config --global user.email "pipeline@${GITLAB_URL}"
- echo "🔄 正在 clone 当前 GitLab 仓库..."
- git clone --depth=1 --branch main https://oauth2:${PIPELINE_BOT_TOKEN}@${GITLAB_URL}/${CI_PROJECT_PATH}.git repo
- cd repo
- echo "🔗 添加 GitHub 远程地址..."
- git remote add github https://x-access-token:${GITHUB_PUSH_TOKEN}@github.com/GeWuYou/forgeboot.git
- echo "🚀 推送 main 分支到 GitHub..."
- git push github main --force
- echo "✅ GitHub 同步完成"
tags:
- java

View File

@ -0,0 +1,44 @@
stages:
- check
before_script:
- export GRADLE_USER_HOME=$CI_PROJECT_DIR/.gradle
- rm -rf $CI_PROJECT_DIR/.gradle/.tmp || true
# ✅ check Job
check:
stage: check
cache:
key:
files:
- gradle/libs.versions.toml
- "**/build.gradle.kts"
prefix: lab-agent
paths:
- .gradle/caches/
- .gradle/wrapper/
- .gradle/kotlin-profile/
- .kotlin/
- buildSrc/.gradle/
- buildSrc/build/
policy: pull-push
script:
- echo "🔧 授予 gradlew 执行权限..."
- chmod +x gradlew
- echo "🧪 开始快速编译检查(跳过打包和测试)..."
- ./gradlew compileKotlin compileJava --stacktrace || (echo "❌ 编译失败!请检查错误日志" && exit 1)
- echo "📦 当前缓存目录:"
- ls -la .gradle/
- echo "📦 Gradle 缓存结构检查"
- ls -la .gradle/caches/modules-2/files-2.1 || true
- echo "📦 缓存文件数量:$(find .gradle/caches/modules-2/files-2.1 -type f | wc -l)"
- echo "🛑 停止 Gradle 守护进程..."
- ./gradlew --stop
- echo "🔍 当前 Java 进程:"
- ps aux | grep java || true
tags:
- docker
- java

View File

@ -0,0 +1,24 @@
stages:
- build
- publish
# 🧪 test 构建
build:
stage: build
rules:
- if: '$CI_COMMIT_BRANCH == "test"'
script:
- ./gradlew clean build
tags:
- java
# 🧪 test 发布 SNAPSHOT 包(允许覆盖)
publish:
stage: publish
needs: ["build"]
rules:
- if: '$CI_COMMIT_BRANCH == "test"'
script:
- ./gradlew publishMavenJavaPublicationToGitLabRepository
tags:
- java

View File

@ -11,13 +11,10 @@ plugins {
// Kotlin kapt 支持 // Kotlin kapt 支持
alias(libs.plugins.kotlin.kapt) alias(libs.plugins.kotlin.kapt)
id(libs.plugins.kotlin.jvm.get().pluginId) id(libs.plugins.kotlin.jvm.get().pluginId)
alias(libs.plugins.gradleMavenPublishPlugin) // alias(libs.plugins.gradleMavenPublishPlugin)
}
java {
withSourcesJar()
withJavadocJar()
} }
// 配置 SCM 版本插件 // 配置 SCM 版本插件
scmVersion { scmVersion {
tag { tag {
@ -93,27 +90,26 @@ subprojects {
tasks.named<Jar>("sourcesJar") { tasks.named<Jar>("sourcesJar") {
dependsOn("generateI18nKeys") dependsOn("generateI18nKeys")
} }
} }
version = rootProject.version version = rootProject.version
apply { apply {
plugin(libs.plugins.java.get().pluginId) // plugin(libs.plugins.java.get().pluginId)
plugin(libs.plugins.javaLibrary.get().pluginId) // plugin(libs.plugins.javaLibrary.get().pluginId)
plugin(libs.plugins.maven.publish.get().pluginId) plugin(libs.plugins.maven.publish.get().pluginId)
plugin(libs.plugins.kotlin.jvm.get().pluginId) plugin(libs.plugins.kotlin.jvm.get().pluginId)
plugin(libs.plugins.axionRelease.get().pluginId) plugin(libs.plugins.axionRelease.get().pluginId)
plugin(libs.plugins.kotlin.kapt.get().pluginId) plugin(libs.plugins.kotlin.kapt.get().pluginId)
plugin(libs.plugins.gradleMavenPublishPlugin.get().pluginId) // plugin(libs.plugins.gradleMavenPublishPlugin.get().pluginId)
// 导入仓库配置 // 导入仓库配置
from(file("$configDir/repositories.gradle.kts")) from(file("$configDir/repositories.gradle.kts"))
} }
// 中央仓库 // // 中央仓库
mavenPublishing { // mavenPublishing {
publishToMavenCentral("DEFAULT", true) // publishToMavenCentral("https://s01.oss.sonatype.org/", true)
signAllPublications() // signAllPublications()
} // }
// 发布配置 // 发布配置
publishing { publishing {
repositories { repositories {
@ -131,6 +127,17 @@ subprojects {
password = System.getenv("GITHUB_TOKEN") password = System.getenv("GITHUB_TOKEN")
} }
} }
maven {
name = "GitLab"
url = uri("https://gitlab.snow-lang.com/api/v4/projects/18/packages/maven")
credentials(HttpHeaderCredentials::class) {
name = "Private-Token"
value = System.getenv("PIPELINE_BOT_TOKEN") // 存储为 GitLab CI/CD Secret
}
authentication {
create("header", HttpHeaderAuthentication::class)
}
}
// Gitea 仓库 // Gitea 仓库
val host = System.getenv("GITEA_HOST") val host = System.getenv("GITEA_HOST")
host?.let { host?.let {

View File

@ -6,4 +6,5 @@ dependencies {
implementation(platform(libs.springBootDependencies.bom)) implementation(platform(libs.springBootDependencies.bom))
implementation(libs.springBoot.autoconfigure) implementation(libs.springBoot.autoconfigure)
implementation(libs.jackson.databind) implementation(libs.jackson.databind)
implementation(libs.kotlinxCoroutines.reactor)
} }

View File

@ -0,0 +1,53 @@
package com.gewuyou.forgeboot.context.impl.element
import kotlinx.coroutines.ThreadContextElement
import org.slf4j.MDC
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
/**
* MDC上下文元素用于在协程中传播MDCMapped Diagnostic Context上下文
*
* MDC SLF4J 提供的一个日志诊断工具允许将特定信息绑定到当前线程的上下文中
* 以便在日志输出时能够包含这些信息由于协程可能在线程间切换因此需要此实现来保证上下文一致性
*
* @param contextMap 包含 MDC 上下文键值对的不可变 Map用于保存和恢复诊断上下文
* @since 2025-07-16 11:05:47
* @author gewuyou
*/
class MdcContextElement(
private val contextMap: Map<String, String>
) : ThreadContextElement<Map<String, String>>,
AbstractCoroutineContextElement(Key) {
/**
* 协程上下文键对象用于标识此类上下文元素的唯一性
*/
companion object Key : CoroutineContext.Key<MdcContextElement>
/**
* 更新当前线程的 MDC 上下文为协程指定的上下文并返回旧的上下文状态
*
* 此方法会在协程切换至新线程时调用以确保目标线程的 MDC 上下文与协程一致
*
* @param context 当前协程的上下文不直接使用但保留用于扩展
* @return 返回更新前的 MDC 上下文状态用于后续恢复
*/
override fun updateThreadContext(context: CoroutineContext): Map<String, String> {
val oldState = MDC.getCopyOfContextMap() ?: emptyMap()
MDC.setContextMap(contextMap)
return oldState
}
/**
* 恢复当前线程的 MDC 上下文至先前保存的状态
*
* 此方法在协程完成执行并释放线程资源时调用确保线程可以还原其原始 MDC 上下文
*
* @param context 当前协程的上下文不直接使用但保留用于扩展
* @param oldState 需要恢复的先前 MDC 上下文状态
*/
override fun restoreThreadContext(context: CoroutineContext, oldState: Map<String, String>) {
MDC.setContextMap(oldState)
}
}

View File

@ -0,0 +1,46 @@
package com.gewuyou.forgeboot.context.impl.filter
import com.gewuyou.forgeboot.context.impl.element.MdcContextElement
import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactor.mono
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono
import java.util.stream.Collectors
/**
* 协程 MDC Web 过滤器
*
* 用于在响应式编程环境下将上下文 MDC Reactor Context 传递到 Kotlin 协程中
* 确保日志上下文信息能够正确传播
*
* @since 2025-07-16 11:07:44
* @author gewuyou
*/
class CoroutineMdcWebFilter : WebFilter {
/**
* 执行过滤操作的方法
*
* @param exchange 表示当前的服务器 Web 交换信息包含请求和响应
* @param chain 当前的过滤器链用于继续执行后续的过滤器或目标处理器
* @return 返回一个 Mono<Void>表示异步完成的过滤操作
*/
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
return Mono.deferContextual { ctxView ->
// 从 Reactor 上下文中提取键值对,筛选出 key 和 value 均为 String 类型的条目
val mdcMap = ctxView.stream()
.filter { it.key is String && it.value is String }
.collect(Collectors.toMap(
{ it.key as String },
{ it.value as String }
))
// 在带有 MDC 上下文的协程中执行过滤链
mono(MdcContextElement(mdcMap)) {
chain.filter(exchange).awaitFirstOrNull()
}
}
}
}

View File

@ -1,6 +1,6 @@
dependencies { dependencies {
compileOnly(libs.springBootStarter.jpa) compileOnly(libs.springBootStarter.jpa)
implementation(project(Modules.Webmvc.DTO)) api(project(Modules.Webmvc.DTO))
implementation(project(Modules.Core.EXTENSION)) api(project(Modules.Core.EXTENSION))
} }

View File

@ -136,42 +136,42 @@ project(":forgeboot-trace:forgeboot-trace-impl").name = "forgeboot-trace-impl"
project(":forgeboot-trace:forgeboot-trace-autoconfigure").name = "forgeboot-trace-autoconfigure" project(":forgeboot-trace:forgeboot-trace-autoconfigure").name = "forgeboot-trace-autoconfigure"
//endregion //endregion
//region module security ////region module security
/** ///**
* Includes and configures projects related to 'forgeboot-security' // * Includes and configures projects related to 'forgeboot-security'
* This module handles security-related functionality. // * This module handles security-related functionality.
*/ // */
include( //include(
"forgeboot-security", // "forgeboot-security",
":forgeboot-security:forgeboot-security-core", // ":forgeboot-security:forgeboot-security-core",
//
":forgeboot-security:forgeboot-security-authenticate", // ":forgeboot-security:forgeboot-security-authenticate",
":forgeboot-security:forgeboot-security-authenticate:api", // ":forgeboot-security:forgeboot-security-authenticate:api",
":forgeboot-security:forgeboot-security-authenticate:impl", // ":forgeboot-security:forgeboot-security-authenticate:impl",
":forgeboot-security:forgeboot-security-authenticate:autoconfigure", // ":forgeboot-security:forgeboot-security-authenticate:autoconfigure",
//
":forgeboot-security:forgeboot-security-authorize", // ":forgeboot-security:forgeboot-security-authorize",
":forgeboot-security:forgeboot-security-authorize:api", // ":forgeboot-security:forgeboot-security-authorize:api",
":forgeboot-security:forgeboot-security-authorize:impl", // ":forgeboot-security:forgeboot-security-authorize:impl",
":forgeboot-security:forgeboot-security-authorize:autoconfigure" // ":forgeboot-security:forgeboot-security-authorize:autoconfigure"
) //)
project(":forgeboot-security").name = "forgeboot-security-spring-boot-starter" //project(":forgeboot-security").name = "forgeboot-security-spring-boot-starter"
project(":forgeboot-security:forgeboot-security-core").name = "forgeboot-security-core" //project(":forgeboot-security:forgeboot-security-core").name = "forgeboot-security-core"
//
project(":forgeboot-security:forgeboot-security-authenticate").name = //project(":forgeboot-security:forgeboot-security-authenticate").name =
"forgeboot-security-authenticate-spring-boot-starter" // "forgeboot-security-authenticate-spring-boot-starter"
project(":forgeboot-security:forgeboot-security-authenticate:api").name = "forgeboot-security-authenticate-api" //project(":forgeboot-security:forgeboot-security-authenticate:api").name = "forgeboot-security-authenticate-api"
project(":forgeboot-security:forgeboot-security-authenticate:impl").name = "forgeboot-security-authenticate-impl" //project(":forgeboot-security:forgeboot-security-authenticate:impl").name = "forgeboot-security-authenticate-impl"
project(":forgeboot-security:forgeboot-security-authenticate:autoconfigure").name = //project(":forgeboot-security:forgeboot-security-authenticate:autoconfigure").name =
"forgeboot-security-authenticate-autoconfigure" // "forgeboot-security-authenticate-autoconfigure"
//
project(":forgeboot-security:forgeboot-security-authorize").name = "forgeboot-security-authorize-spring-boot-starter" //project(":forgeboot-security:forgeboot-security-authorize").name = "forgeboot-security-authorize-spring-boot-starter"
project(":forgeboot-security:forgeboot-security-authorize:api").name = "forgeboot-security-authorize-api" //project(":forgeboot-security:forgeboot-security-authorize:api").name = "forgeboot-security-authorize-api"
project(":forgeboot-security:forgeboot-security-authorize:impl").name = "forgeboot-security-authorize-impl" //project(":forgeboot-security:forgeboot-security-authorize:impl").name = "forgeboot-security-authorize-impl"
project(":forgeboot-security:forgeboot-security-authorize:autoconfigure").name = //project(":forgeboot-security:forgeboot-security-authorize:autoconfigure").name =
"forgeboot-security-authorize-autoconfigure" // "forgeboot-security-authorize-autoconfigure"
//
//endregion ////endregion
//region module cache //region module cache
include( include(