Ctrl K
ComunidadGuías…

build.gradle

Rama develop · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import net.e175.klaus.zip.ZipPrefixer

buildscript {
    repositories {
        mavenCentral()
    }

    dependencies {
        classpath "net.e175.klaus:zip-prefixer:0.4.0"
    }
}

plugins {
    // micronaut
    id "java"
    id 'java-library'
    id "idea"
    id "com.gradleup.shadow" version "9.6.1"
    id "application"

    // test
    id 'jacoco-report-aggregation'

    // release
    id 'net.researchgate.release' version '3.2.0'
    id "com.gorylenko.gradle-git-properties" version "4.0.1"
    id 'signing'
    id "com.vanniktech.maven.publish" version "0.37.0"

    // Kestra gradle plugin
    id "io.kestra.gradle.spotless-conventions" version '1.2.12'
    id 'io.kestra.gradle.repository-conventions' version '1.2.12'
}

idea {
    module {
        downloadJavadoc = true
        downloadSources = true
    }
}

/**********************************************************************************************************************\
 * Main
 **********************************************************************************************************************/
final mainClassName = "io.kestra.cli.Kestra"

application {
    mainClass = mainClassName
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(25)
    }
}

dependencies {
    implementation project(":cli")
    testImplementation project(":cli")
}

/**********************************************************************************************************************\
 * Dependencies
 **********************************************************************************************************************/
allprojects {

    tasks.withType(GenerateModuleMetadata).configureEach {
        suppressedValidationErrors.add('enforced-platform')
    }

    if (it.name != 'platform') {
        group = "io.kestra"

        // Formatting
        apply plugin: "io.kestra.gradle.spotless-conventions"

        java {
            toolchain {
                languageVersion = JavaLanguageVersion.of(25)
            }
        }

        repositories {
            mavenCentral()
            mavenLocal()
        }

        // micronaut
        apply plugin: "java"
        apply plugin: "java-library"
        apply plugin: "idea"
        apply plugin: "jacoco"

        configurations {
            developmentOnly // for dependencies that are needed for development only
            micronaut
        }

        // dependencies
        dependencies {
            // Platform
            annotationProcessor enforcedPlatform(project(":platform"))
            implementation enforcedPlatform(project(":platform"))
            api enforcedPlatform(project(":platform"))
            micronaut enforcedPlatform(project(":platform"))

            // lombok
            annotationProcessor "org.projectlombok:lombok"
            compileOnly 'org.projectlombok:lombok'

            // micronaut
            annotationProcessor "io.micronaut:micronaut-inject-java"
            annotationProcessor "io.micronaut.validation:micronaut-validation-processor"
            micronaut "io.micronaut:micronaut-inject"
            micronaut "io.micronaut.validation:micronaut-validation"
            micronaut "io.micronaut.beanvalidation:micronaut-hibernate-validator"
            micronaut "io.micronaut:micronaut-runtime"
            micronaut "io.micronaut:micronaut-retry"
            micronaut "io.micronaut:micronaut-jackson-databind"
            micronaut "io.micronaut.data:micronaut-data-model"
            micronaut "io.micronaut:micronaut-management"
            micronaut "io.micrometer:micrometer-core"
            micronaut "io.micronaut.micrometer:micronaut-micrometer-registry-prometheus"
            micronaut "io.micronaut:micronaut-http-client"
            micronaut "io.micronaut.reactor:micronaut-reactor-http-client"
            micronaut "io.micronaut.tracing:micronaut-tracing-opentelemetry-http"

            // logs
            implementation "org.slf4j:slf4j-api"
            implementation "ch.qos.logback:logback-classic"
            implementation "org.codehaus.janino:janino"
            implementation "org.apache.logging.log4j:log4j-to-slf4j"
            implementation "org.slf4j:jul-to-slf4j"
            implementation "org.slf4j:jcl-over-slf4j"
            implementation "org.fusesource.jansi:jansi"

            // OTEL
            implementation "io.opentelemetry:opentelemetry-exporter-otlp"

            // jackson 2 - Kestra's own serialization (JacksonMapper: storage, queue, YAML flow parsing)
            implementation "com.fasterxml.jackson.core:jackson-core"
            implementation "com.fasterxml.jackson.core:jackson-databind"
            implementation "com.fasterxml.jackson.core:jackson-annotations"
            implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml"
            implementation "com.fasterxml.jackson.module:jackson-module-parameter-names"
            implementation "com.fasterxml.jackson.datatype:jackson-datatype-guava"
            implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310"

            // jackson 3 (tools.jackson) - used by Micronaut 5's HTTP/Jackson integration.
            implementation "tools.jackson.core:jackson-core"
            implementation "tools.jackson.core:jackson-databind"

            implementation "com.fasterxml.uuid:java-uuid-generator"

            // kestra
            implementation ("net.thisptr:jackson-jq") {
                exclude group: 'com.fasterxml.jackson.core'
            }

            // exposed utils
            api "com.google.guava:guava"
            api "commons-io:commons-io"
            api "org.apache.commons:commons-lang3"
            api "io.swagger.core.v3:swagger-annotations"
            api "com.google.code.findbugs:jsr305" // brings @CheckReturnValue and already in path in core
        }
    }
}

/**********************************************************************************************************************\
 * Test
 **********************************************************************************************************************/

// Per-module test heap, sized from measured peak GC heap occupancy (dev-tools/analyze-test-heap.sh,
// run with -PprofileHeap) plus ~30% headroom. A single blanket value either wastes memory on small
// modules or starves large ones — re-profile and bump a module's entry here if it starts failing
// with OutOfMemoryError / GC overhead limit exceeded.
ext.testHeapSizes = [
    'processor'        : '256m',
    'executor'         : '768m',
    'scheduler'        : '768m',
    'worker'           : '768m',
    'jdbc-mysql'       : '768m',
    'jdbc-postgres'    : '1024m',
    'tests'            : '1024m',
    'cli'              : '1280m',
    'jdbc-h2'          : '1280m',
    'webserver'        : '1280m',
    'core'             : '2560m',
]

subprojects {subProj ->

    if (subProj.name != 'platform' && subProj.name != 'jmh-benchmarks') {

        apply plugin: 'jacoco'

        java {
            toolchain {
                languageVersion = JavaLanguageVersion.of(25)
            }
        }

        configurations {
            mockitoAgent
            // Exposes this project's compiled test classes/resources to other projects, replacing the
            // Gradle 9.5-incompatible `project(':x').sourceSets.test.output` cross-project script access.
            testArtifacts {
                canBeConsumed = true
                canBeResolved = false
            }
        }

        artifacts {
            sourceSets.test.output.files.each { dir ->
                testArtifacts(dir) { builtBy tasks.named('testClasses') }
            }
        }

        dependencies {
            // Platform
            testAnnotationProcessor enforcedPlatform(project(":platform"))
            testImplementation enforcedPlatform(project(":platform"))

            // lombok
            testAnnotationProcessor "org.projectlombok:lombok:"
            testCompileOnly 'org.projectlombok:lombok'

            // micronaut
            testAnnotationProcessor "io.micronaut:micronaut-inject-java"
            testAnnotationProcessor "io.micronaut.validation:micronaut-validation-processor"
            testImplementation "io.micronaut.test:micronaut-test-junit5"

            testImplementation "org.junit.jupiter:junit-jupiter-engine"
            testImplementation "org.junit.jupiter:junit-jupiter-params"
            testImplementation "org.junit-pioneer:junit-pioneer"
            testImplementation 'org.mockito:mockito-junit-jupiter'
            mockitoAgent("org.mockito:mockito-core:5.24.0") {
                transitive = false // just the core
            }

            // hamcrest
            testImplementation 'org.hamcrest:hamcrest'
            testImplementation 'org.hamcrest:hamcrest-library'
            testImplementation 'org.exparity:hamcrest-date'

            //assertj
            testImplementation 'org.assertj:assertj-core'

            // awaitility
            testImplementation 'org.awaitility:awaitility'

        }

        def commonTestConfig = { Test t ->
            t.timeout = java.time.Duration.ofMinutes(
                (System.getenv('GRADLE_TEST_TIMEOUT_MIN') ?: '30').toInteger()
            )
            // Bounds a single test method so it fails by itself, rather than a hung test consuming
            // the module's whole Gradle-level timeout above and discarding every test's JUnit XML.
            t.systemProperty 'junit.jupiter.execution.timeout.default',
                System.getenv('JUNIT_TEST_TIMEOUT_MIN') ? "${System.getenv('JUNIT_TEST_TIMEOUT_MIN')} m" : '5 m'
            t.finalizedBy jacocoTestReport


            // -PprofileHeap: raise the ceiling so no module is heap-starved, and log real GC
            // occupancy per module so per-module maxHeapSize can be set from measurements
            // instead of guessed. See dev-tools/analyze-test-heap.sh.
            if (project.hasProperty('profileHeap')) {
                t.maxHeapSize = '8g'
                def heapProfileDir = "${rootDir}/heap-profile"
                mkdir(heapProfileDir)
                t.jvmArgs += ["-Xlog:gc:file=${heapProfileDir}/${project.name}-gc.log:time,uptime,level,tags"]
            } else {
                t.maxHeapSize = rootProject.ext.testHeapSizes.getOrDefault(project.name, '512m')
            }

            // By default a thread that hits OutOfMemoryError just dies silently (JVM default
            // UncaughtExceptionHandler) while the rest of the JVM limps on in a degraded state.
            // Exit immediately instead.
            t.jvmArgs += ["-XX:+ExitOnOutOfMemoryError"]

            // configure en_US default locale for tests
            t.systemProperty 'user.language', 'en'
            t.systemProperty 'user.country', 'US'

            t.environment 'SECRET_MY_SECRET', "{\"secretKey\":\"secretValue\"}".bytes.encodeBase64().toString()
            t.environment 'SECRET_NEW_LINE', "cGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2\nZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZleXJsb25ncGFzc3dvcmR2ZXJ5dmVyeXZl\neXJsb25n"
            t.environment 'SECRET_WEBHOOK_KEY', "secretKey".bytes.encodeBase64().toString()
            t.environment 'SECRET_NON_B64_SECRET', "some secret value"
            t.environment 'SECRET_PASSWORD', "cGFzc3dvcmQ="
            t.environment 'ENV_TEST1', "true"
            t.environment 'ENV_TEST2', "Pass by env"
        }

        tasks.register('integrationTest', Test) { Test t ->
            description = 'Runs integration tests'
            group = 'verification'

            useJUnitPlatform {
                includeTags 'integration'
            }

            testClassesDirs = sourceSets.test.output.classesDirs
            classpath = sourceSets.test.runtimeClasspath

            reports {
                junitXml.required = true
                junitXml.outputPerTestCase = true
                junitXml.mergeReruns = true
                junitXml.includeSystemErrLog = true
                junitXml.outputLocation = layout.buildDirectory.dir("test-results/test")
            }

            // Integration tests typically not parallel (but you can enable)
            maxParallelForks = 1
            commonTestConfig(t)
        }

        tasks.register('unitTest', Test) { Test t ->
            description = 'Runs unit tests'
            group = 'verification'

            useJUnitPlatform {
                excludeTags 'flaky', 'integration'
            }

            testClassesDirs = sourceSets.test.output.classesDirs
            classpath = sourceSets.test.runtimeClasspath

            reports {
                junitXml.required = true
                junitXml.outputPerTestCase = true
                junitXml.mergeReruns = true
                junitXml.includeSystemErrLog = true
                junitXml.outputLocation = layout.buildDirectory.dir("test-results/test")
            }

            commonTestConfig(t)
        }

        tasks.register('flakyTest', Test) { Test t ->
            group = 'verification'
            description = 'Runs tests tagged @Flaky but does not fail the build.'

            useJUnitPlatform {
                includeTags 'flaky'
            }

            testClassesDirs = sourceSets.test.output.classesDirs
            classpath = sourceSets.test.runtimeClasspath

            reports {
                junitXml.required = true
                junitXml.outputPerTestCase = true
                junitXml.mergeReruns = true
                junitXml.includeSystemErrLog = true
                junitXml.outputLocation = layout.buildDirectory.dir("test-results/flakyTest")
            }
            commonTestConfig(t)
            t.ignoreFailures = true
            t.filter { failOnNoMatchingTests = false }
            t.outputs.doNotCacheIf("Flaky tests are non-deterministic") { true }
            t.mustRunAfter(tasks.named('test'))
        }

        // test task (default)
        tasks.named('test', Test) { Test t ->
            group = 'verification'
            description = 'Runs all non-flaky tests.'

            useJUnitPlatform {
                excludeTags 'flaky'
            }
            reports {
                junitXml.required = true
                junitXml.outputPerTestCase = true
                junitXml.mergeReruns = true
                junitXml.includeSystemErrLog = true
                junitXml.outputLocation = layout.buildDirectory.dir("test-results/test")
            }
            commonTestConfig(t)
            jvmArgs "-javaagent:${configurations.mockitoAgent.singleFile}"
        }

        tasks.named('check') {
            dependsOn(tasks.named('test'))// default behaviour
            dependsOn(tasks.named('flakyTest'))
        }
    }
}

// Collect test task outcomes as they execute (for test-metadata.json)
def testTaskOutcomes = new java.util.concurrent.ConcurrentHashMap<String, String>()
gradle.taskGraph.afterTask { Task task ->
    if (task instanceof Test) {
        def state = task.state
        String outcome
        if (state.noSource) {
            outcome = 'NO_SOURCE'
        } else if (state.failure != null) {
            outcome = 'FAILED'
        } else if (state.didWork) {
            outcome = 'SUCCESS'
        } else if (state.upToDate) {
            outcome = 'UP_TO_DATE'
        } else if (state.skipped) {
            outcome = 'SKIPPED'
        } else {
            outcome = 'FROM_CACHE'
        }
        testTaskOutcomes.put(task.project.name + ':' + task.name, outcome)
    }
}

// Workaround for https://github.com/gradle/gradle/issues/36699
// Gradle's test task `timeout` kills the JVM hard, so JUnit XML reports are never
// generated for timed-out modules. This task captures per-module task states so that
// kestra-devtools can detect missing results and report them in PR comments.
// Remove this task once Gradle supports graceful test task timeouts.
tasks.register('generateTestMetadata') {
    group = 'verification'
    description = 'Generates build/test-metadata.json with per-module test task states for CI reporting.'
    mustRunAfter(subprojects.collectMany { it.tasks.withType(Test) })
    def outputFile = layout.buildDirectory.file('test-metadata.json')
    outputs.file(outputFile)
    outputs.upToDateWhen { false }
    doLast {
        def timeoutMin = (System.getenv('GRADLE_TEST_TIMEOUT_MIN') ?: '30').toInteger()
        def modules = new LinkedHashMap<String, Object>()
        subprojects.each { sub ->
            def hasTestSources = sub.file('src/test/java').isDirectory() &&
                sub.fileTree('src/test/java').files.size() > 0
            if (!hasTestSources && sub.file('src/test/kotlin').isDirectory()) {
                hasTestSources = sub.fileTree('src/test/kotlin').files.size() > 0
            }
            def xmlDir = sub.file("build/test-results/test")
            def hasXmlResults = xmlDir.isDirectory() &&
                sub.fileTree(dir: xmlDir, includes: ['TEST-*.xml']).files.size() > 0
            def state = testTaskOutcomes.getOrDefault(sub.name + ':test', 'NOT_RUN')

            modules.put(sub.name, [
                state: state,
                hasTestSources: hasTestSources,
                hasXmlResults: hasXmlResults
            ])
        }
        def metadata = [
            modules: modules,
            timeoutMinutes: timeoutMin
        ]
        def json = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(metadata))
        outputFile.get().asFile.parentFile.mkdirs()
        outputFile.get().asFile.text = json
        logger.lifecycle("Test metadata written to ${outputFile.get().asFile.path}")
    }
}

// Finalize every Test task (not just `check`) with generateTestMetadata. When a module's
// test task exceeds its timeout, Gradle aborts the build before `check` runs, so a finalizer
// of `check` is skipped and the metadata file is never written — leaving kestra-devtools to
// silently report success for the timed-out module. A finalizer of the Test task itself still
// runs on timeout (same as jacocoTestReport above), guaranteeing test-metadata.json is written.
subprojects {
    tasks.withType(Test).configureEach {
        finalizedBy rootProject.tasks.named('generateTestMetadata')
    }
}

tasks.named('check') {
    dependsOn tasks.named('testCodeCoverageReport', JacocoReport)
    dependsOn(tasks.named('flakyTest'))
    finalizedBy jacocoTestReport
    finalizedBy tasks.named('generateTestMetadata')
}

tasks.register('unitTest') {
    // No jacocoTestReport here, because it depends by default on :test,
    // and that would make :test being run twice in our CI.
    // In practice the report will be generated later in the CI by :check.
}

tasks.register('integrationTest') {
    dependsOn tasks.named('testCodeCoverageReport', JacocoReport)
    finalizedBy jacocoTestReport
}

tasks.register('flakyTest') {
    dependsOn tasks.named('testCodeCoverageReport', JacocoReport)
    finalizedBy jacocoTestReport
}

tasks.named('testCodeCoverageReport') {
    dependsOn ':core:copyGradleProperties'
}

/**********************************************************************************************************************\
 * Allure Reports
 **********************************************************************************************************************/
subprojects {
    plugins.withId('java') {
        dependencies {
            testImplementation "io.qameta.allure:allure-junit5"
        }

        test {
            // allure-junit5 writes results as a side effect of the test JVM, invisible to Gradle's
            // dependency tracking. Without declaring it as a task output, a build-cache hit on :test
            // (org.gradle.caching=true) restores build/test-results/ but skips the JVM entirely, so
            // no allure-results are ever produced for that run.
            def allureResultsDir = layout.buildDirectory.dir("allure-results")
            systemProperty 'allure.results.directory', allureResultsDir.get().asFile.absolutePath
            outputs.dir(allureResultsDir)
        }
    }
}

/**********************************************************************************************************************\
 * Micronaut
 **********************************************************************************************************************/
allprojects {
    gradle.projectsEvaluated {
        tasks.withType(JavaCompile).configureEach {
            options.encoding = "UTF-8"
            options.compilerArgs.add("-parameters")
            options.compilerArgs.add("-Xlint:all")
            options.compilerArgs.add("-Xlint:-processing")
        }
    }
}

tasks.withType(JavaCompile).configureEach {
    options.encoding = "UTF-8"
    options.compilerArgs.add("-parameters")
}

run.classpath += configurations.developmentOnly
test.classpath += configurations.developmentOnly

run.jvmArgs(
    "-noverify",
    "-XX:TieredStopAtLevel=1",
    "-Dcom.sun.management.jmxremote",
    '-Dmicronaut.environments=dev,override',
    // Silence the sun.misc.Unsafe memory-access startup warning (JEP 498) triggered by
    // com.google.protobuf.UnsafeUtil (pulled in by gRPC), see https://github.com/kestra-io/kestra/issues/16513
    '--sun-misc-unsafe-memory-access=allow'
)

/**********************************************************************************************************************\
 * Jar
 **********************************************************************************************************************/
jar {
    manifest {
        attributes(
            "Main-Class": mainClassName,
            "X-Kestra-Name": project.name,
            "X-Kestra-Title": project.name,
            "X-Kestra-Group": project.group,
            "X-Kestra-Version": project.version
        )
    }
}

shadowJar {
    archiveClassifier.set(null)
    duplicatesStrategy = DuplicatesStrategy.INCLUDE
    mergeServiceFiles()
    zip64 = true
}

distZip.dependsOn shadowJar
distTar.dependsOn shadowJar
startScripts.dependsOn shadowJar
startShadowScripts.dependsOn jar
shadowJar.dependsOn jar

// sourcesJar is registered later by com.vanniktech.maven.publish and packs the resources generated by generateGitProperties, which registers no builtBy.
tasks.named { it == 'sourcesJar' }.configureEach {
    dependsOn tasks.named('generateGitProperties')
}

/**********************************************************************************************************************\
 * Executable Jar
 **********************************************************************************************************************/
def executableDir = layout.buildDirectory.dir("executable")
def executable = layout.buildDirectory.file("executable/${project.name}-${project.version}").get().asFile

tasks.register('writeExecutableJar') {
    group = "build"
    description = "Write an executable jar from shadow jar"
    dependsOn = [shadowJar]

    final shadowJarFile =  tasks.shadowJar.outputs.files.singleFile
    inputs.file shadowJarFile
    outputs.file executable
    outputs.cacheIf { true }

    doFirst {
        executableDir.get().asFile.mkdirs()
    }

    doLast {
        executable.setBytes(shadowJarFile.readBytes())
        ByteArrayOutputStream executableBytes = new ByteArrayOutputStream()
        executableBytes.write("\n: <<END_OF_KESTRA_SELFRUN\r\n".getBytes())
        executableBytes.write(file("gradle/jar/selfrun.bat").readBytes())
        executableBytes.write("\r\n".getBytes())
        executableBytes.write("END_OF_KESTRA_SELFRUN\r\n\n".getBytes())
        executableBytes.write(file("gradle/jar/selfrun.sh").readBytes())
        ZipPrefixer.applyPrefixBytesToZip(executable.toPath(), executableBytes.toByteArray())
        executable.setExecutable(true)
    }
}

tasks.register('executableJar', Zip) {
    group = "build"
    description = "Zip the executable jar"
    dependsOn = [writeExecutableJar]

    archiveFileName = "${project.name}-${project.version}.zip"
    destinationDirectory = layout.buildDirectory.dir('archives')

    from executableDir
    archiveClassifier.set(null)
}

/**********************************************************************************************************************\
 * Standalone
 **********************************************************************************************************************/
tasks.register('runLocal') {
    group = "application"
    description = "Run Kestra as server local"
    dependsOn ':cli:runLocal'
}

tasks.register('runStandalone') {
    group = "application"
    description = "Run Kestra as server standalone (plugin directory: -PstandalonePlugins=<dir>, default '../local/plugins')"
    dependsOn ':cli:runStandalone'
}

/**********************************************************************************************************************\
 * UI SBOM
 **********************************************************************************************************************/
tasks.register('uiSbom', Exec) {
    group = 'build'
    description = 'Generate the CycloneDX SBOM of the UI production dependencies (requires `npm ci` in ui/)'
    workingDir = file('ui')
    commandLine 'npm', 'run', 'generate:sbom'
    outputs.file('ui/build/sbom/bom.json')
    outputs.upToDateWhen { false }
}

tasks.register('sbom') {
    group = 'build'
    description = 'Generate the CycloneDX SBOM of the Kestra executable, merging the backend and the UI dependencies'
    dependsOn ':cli:cyclonedxDirectBom', 'uiSbom'

    def backendSbom = provider { project(':cli').tasks.named('cyclonedxDirectBom').get().jsonOutput.get().asFile }
    def uiSbom = file('ui/build/sbom/bom.json')
    def mergedSbom = layout.buildDirectory.file('sbom/kestra-sbom.json')
    inputs.files(backendSbom, uiSbom)
    outputs.file(mergedSbom)

    doLast {
        def slurper = new groovy.json.JsonSlurper()
        def bom = slurper.parse(backendSbom.get(), 'UTF-8')
        def ui = slurper.parse(uiSbom, 'UTF-8')

        def uiRoot = ui.metadata.component
        uiRoot.name = 'kestra-ui'
        def uiRootRef = uiRoot['bom-ref']

        def knownRefs = bom.components.collect { it['bom-ref'] } as Set
        bom.components = bom.components + [uiRoot] + ui.components.findAll { !knownRefs.contains(it['bom-ref']) }

        def rootRef = bom.metadata.component['bom-ref']
        def dependencies = bom.dependencies.collect { it }
        def rootDependency = dependencies.find { it.ref == rootRef }
        rootDependency.dependsOn = ((rootDependency.dependsOn ?: []) + [uiRootRef]).unique()
        def knownDependencies = dependencies.collect { it.ref } as Set
        dependencies = dependencies + ui.dependencies.findAll { !knownDependencies.contains(it.ref) }

        // cyclonedx-npm emits references to nested packages it does not list, which strict validators reject
        def knownComponents = (bom.components.collect { it['bom-ref'] } + [rootRef]) as Set
        bom.dependencies = dependencies.findAll { knownComponents.contains(it.ref) }.collect { dependency ->
            dependency.dependsOn = (dependency.dependsOn ?: []).findAll { knownComponents.contains(it) }
            dependency
        }

        def output = mergedSbom.get().asFile
        output.parentFile.mkdirs()
        output.setText(groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(bom)), 'UTF-8')
    }
}

/**********************************************************************************************************************\
 * Publish
 **********************************************************************************************************************/
subprojects {subProject ->

    if (subProject.name != 'jmh-benchmarks' && subProject.name != rootProject.name) {
        apply plugin: 'signing'
        apply plugin: "com.vanniktech.maven.publish"

        javadoc {
            options {
                locale = 'en_US'
                encoding = 'UTF-8'
                addStringOption("Xdoclint:none", "-quiet")
            }
        }

        tasks.register('sourcesJar', Jar) {
            dependsOn = [':core:copyGradleProperties']
            archiveClassifier.set('sources')
            from sourceSets.main.allSource
        }
        sourcesJar.dependsOn ':core:copyGradleProperties'

        tasks.register('javadocJar', Jar) {
            archiveClassifier.set('javadoc')
            from javadoc
        }

        tasks.register('testsJar', Jar) {
            group = 'build'
            description = 'Build the tests jar'

            archiveClassifier.set('tests')
            if (sourceSets.matching { it.name == 'test'}) {
                from sourceSets.named('test').get().output
            }
        }

        //These modules should not be published
        def unpublishedModules = ["jdbc-mysql", "jdbc-postgres", "webserver"]
        if (subProject.name in unpublishedModules){
            return
        }

        tasks.named('cyclonedxDirectBom') {
            buildSystemEnvironmentVariable = '${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}'
            if (subProject.name == 'cli') {
                projectType = org.cyclonedx.model.Component.Type.APPLICATION
            }

            def organization = new org.cyclonedx.model.OrganizationalEntity()
            organization.name = "Kestra Technology"
            organization.urls = ["https://kestra.io"]
            def contact = new org.cyclonedx.model.OrganizationalContact()
            contact.name = "Security Team"
            contact.email = "security@kestra.io"
            organization.addContact(contact)
            organizationalEntity = organization
        }

        // Test and benchmark helper modules do not ship runtime code, so their SBOM is not published
        def hasSbom = !(subProject.name in ["tests", "jmh-benchmarks"])

        mavenPublishing {
            publishToMavenCentral(true)
            signAllPublications()

            coordinates(
                "${rootProject.group}",
                subProject.name == "cli" ? rootProject.name : subProject.name,
                "${rootProject.version}"
            )

            pom {
                name        = project.name
                description = "${project.group}:${project.name}:${rootProject.version}"
                url         = "https://github.com/kestra-io/${rootProject.name}"

                licenses {
                    license {
                        name = "The Apache License, Version 2.0"
                        url  = "http://www.apache.org/licenses/LICENSE-2.0.txt"
                    }
                }
                developers {
                    developer {
                        id    = "tchiotludo"
                        name  = "Ludovic Dehon"
                        email = "ldehon@kestra.io"
                    }
                }
                scm {
                    connection = 'scm:git:'
                    url = "https://github.com/kestra-io/${rootProject.name}"
                }
            }
        }

        afterEvaluate {
            publishing {
                publications {
                    withType(MavenPublication).configureEach { publication ->

                        if (subProject.name == "platform") {
                            // Clear all artifacts except the BOM
                            publication.artifacts.clear()
                        } else if (hasSbom) {
                            publication.artifact(tasks.named('cyclonedxDirectBom').flatMap { it.jsonOutput }) {
                                classifier = "cyclonedx"
                                extension = "json"
                            }
                        }
                    }
                }
            }
        }

        if (subProject.name == 'cli') {

            /* Make sure the special publication is wired *after* every plugin */
            subProject.afterEvaluate {
                /* 1. Remove the default java component so Gradle stops expecting
                       the standard cli-*.jar, sources, javadoc, etc.           */
                components.removeAll { it.name == "java" }

                /* 2. Replace the publication’s artifacts with shadow + exec */
                publishing.publications.withType(MavenPublication).configureEach { pub ->
                    pub.artifacts.clear()

                    // main shadow JAR built at root
                    pub.artifact(rootProject.tasks.named("shadowJar").get()) {
                        extension = "jar"
                    }

                    // executable ZIP built at root
                    pub.artifact(rootProject.tasks.named("executableJar").get().archiveFile) {
                        classifier = "exec"
                        extension  = "zip"
                    }
                    pub.artifact(tasks.named("sourcesJar").get())
                    pub.artifact(tasks.named("javadocJar").get())
                    // The shadow jar bundles the UI, so its SBOM covers the backend and the UI dependencies
                    pub.artifact(rootProject.layout.buildDirectory.file('sbom/kestra-sbom.json')) {
                        builtBy rootProject.tasks.named('sbom')
                        classifier = "cyclonedx"
                        extension = "json"
                    }

                }

                /* 3. Disable Gradle-module metadata for this publication to
                       avoid the “artifact removed from java component” error. */
                tasks.withType(GenerateModuleMetadata).configureEach { it.enabled = false }

                /* 4. Make every publish task in :cli wait for the two artifacts */
                tasks.matching { it.name.startsWith("publish") }.configureEach {
                    dependsOn rootProject.tasks.named("shadowJar")
                    dependsOn rootProject.tasks.named("executableJar")
                }
            }
        }

        if (subProject.name != 'platform' && subProject.name != 'cli') {
            // only if a test source set actually exists (avoids empty artifacts)
            def hasTests = subProject.extensions.findByName('sourceSets')?.findByName('test') != null

            if (hasTests) {
                // wire the artifact onto every Maven publication of this subproject
                publishing {
                    publications {
                        withType(MavenPublication).configureEach { pub ->
                            // keep the normal java component + sources/javadoc already configured
                            pub.artifact(subProject.tasks.named('testsJar').get())
                        }
                    }
                }

                // make sure publish tasks build the tests jar first
                tasks.matching { it.name.startsWith('publish') }.configureEach {
                    dependsOn subProject.tasks.named('testsJar')
                }
            }
        }
    }
}



/**********************************************************************************************************************\
 * Version
 **********************************************************************************************************************/
release {
    preCommitText = 'chore(version):'
    preTagCommitMessage = 'update to version'
    tagCommitMessage = 'tag version'
    newVersionCommitMessage = 'update snapshot version'
    tagTemplate = 'v${version}'
    buildTasks = ['classes']

    git {
        requireBranch.set('develop')
    }

    // Dynamically set properties with default values
    failOnSnapshotDependencies = providers.gradleProperty("release.failOnSnapshotDependencies")
            .map(val -> Boolean.parseBoolean(val))
            .getOrElse(true)

    pushReleaseVersionBranch = providers.gradleProperty("release.pushReleaseVersionBranch")
            .getOrElse(null)
}