Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save OleksandrKucherenko/58478be9d17de80617ccabb6977325ea to your computer and use it in GitHub Desktop.

Select an option

Save OleksandrKucherenko/58478be9d17de80617ccabb6977325ea to your computer and use it in GitHub Desktop.

Revisions

  1. OleksandrKucherenko created this gist Jul 7, 2025.
    307 changes: 307 additions & 0 deletions task-graph-publisher.gradle
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,307 @@
    /**
    * Task Graph Publisher - Generates Mermaid format report of Gradle task graph
    * This script logs each Gradle execution with command and task graph to execution.log.md
    */

    // Wait for the configuration phase to complete and task graph to be populated
    gradle.taskGraph.whenReady { taskGraph ->
    // Generate and log the Mermaid task graph report with execution info
    logTaskGraphExecution(taskGraph)
    }

    /**
    * Logs the Gradle execution with command and task graph to execution.log.md
    */
    def logTaskGraphExecution(taskGraph) {
    def timestamp = new Date().format("yyyy-MM-dd HH:mm:ss")
    def executionCommand = getGradleExecutionCommand()
    def logFile = new File(gradle.rootProject.projectDir, "execution.log.md")

    // Generate the Mermaid diagram content with execution info
    def mermaidContent = generateMermaidDiagram(taskGraph)

    // Prepare the log entry
    def logEntry = buildLogEntry(timestamp, executionCommand, mermaidContent)

    // Append to log file
    logFile.append(logEntry)

    def executedTasksCount = taskGraph.allTasks.size()
    println "\n" + "="*80
    println "EXECUTION LOGGED TO: ${logFile.absolutePath}"
    println "TIMESTAMP: ${timestamp}"
    println "COMMAND: ${executionCommand}"
    println "EXECUTED TASKS: ${executedTasksCount}"
    println "="*80 + "\n"
    }

    /**
    * Builds the complete log entry for the execution
    */
    def buildLogEntry(String timestamp, String command, String mermaidContent) {
    def separator = "\n" + "-"*80 + "\n"
    return """
    ${separator}
    # Gradle Execution - ${timestamp}
    ## Command
    ```bash
    ${command}
    ```
    ## Task Graph
    ${mermaidContent}
    ${separator}
    """
    }

    /**
    * Gets the Gradle execution command with all arguments
    */
    def getGradleExecutionCommand() {
    def startParameter = gradle.startParameter
    def command = "gradle"

    // Add task names
    if (startParameter.taskNames) {
    command += " " + startParameter.taskNames.join(" ")
    }

    // Add project properties
    if (startParameter.projectProperties) {
    startParameter.projectProperties.each { key, value ->
    command += " -P${key}=${value}"
    }
    }

    // Add system properties
    if (startParameter.systemPropertiesArgs) {
    startParameter.systemPropertiesArgs.each { key, value ->
    command += " -D${key}=${value}"
    }
    }

    // Add common flags
    if (startParameter.dryRun) command += " --dry-run"
    if (startParameter.refreshDependencies) command += " --refresh-dependencies"
    if (startParameter.rerunTasks) command += " --rerun-tasks"
    if (startParameter.continueOnFailure) command += " --continue"
    if (startParameter.offline) command += " --offline"
    if (startParameter.parallelProjectExecutionEnabled) command += " --parallel"
    if (startParameter.configureOnDemand) command += " --configure-on-demand"

    // Add log level
    switch (startParameter.logLevel) {
    case LogLevel.DEBUG:
    command += " --debug"
    break
    case LogLevel.INFO:
    command += " --info"
    break
    case LogLevel.WARN:
    command += " --warn"
    break
    case LogLevel.QUIET:
    command += " --quiet"
    break
    }

    // Add gradle user home if different from default
    if (startParameter.gradleUserHomeDir != gradle.gradleUserHomeDir) {
    command += " --gradle-user-home \"${startParameter.gradleUserHomeDir}\""
    }

    // Add project dir if different from current
    if (startParameter.currentDir != gradle.rootProject.projectDir) {
    command += " --project-dir \"${startParameter.currentDir}\""
    }

    return command
    }

    /**
    * Generates a Mermaid format diagram of the Gradle task graph with execution info
    */
    def generateMermaidDiagram(taskGraph) {
    def content = new StringBuilder()
    content.append("\n```mermaid\n")

    // Get executed tasks from the task graph first to determine orientation
    def executedTasks = taskGraph.allTasks.toList()

    // Use LR orientation for small graphs (< 5 tasks), TD for larger ones
    def graphOrientation = executedTasks.size() < 5 ? "LR" : "TD"
    content.append("graph ${graphOrientation}\n")

    // Collect all tasks from all projects
    def allTasks = []
    gradle.rootProject.allprojects { project ->
    project.tasks.each { task ->
    allTasks.add(task)
    }
    }

    // executedTasks already defined above for orientation decision
    def nonExecutedTasks = allTasks.findAll { !executedTasks.contains(it) }

    // Get requested tasks (the ones explicitly requested by user)
    def requestedTasks = gradle.startParameter.taskNames.collect { taskName ->
    // Find the actual task objects for the requested task names
    def foundTasks = executedTasks.findAll { task ->
    task.path == ":${taskName}" || task.path == taskName || task.name == taskName
    }
    return foundTasks
    }.flatten().toSet()

    // Sort tasks by name for consistent output
    allTasks.sort { it.path }

    // Track processed dependencies to avoid duplicates
    def processedDependencies = new HashSet()

    // Add start and stop nodes (compatible with Mermaid v10.9.1)
    content.append(" Start((Start))\n")
    content.append(" Stop(((Stop)))\n\n")

    // Create subgraph for executed tasks with execution order
    if (!executedTasks.isEmpty()) {
    content.append(" subgraph executed [\"🚀 Executed Tasks (Execution Order)\"]\n")
    content.append(" direction TB\n")

    executedTasks.eachWithIndex { task, index ->
    def taskId = sanitizeTaskId(task.path)
    def taskLabel = task.path
    def isRequested = requestedTasks.contains(task)

    // Mark requested tasks for styling (will be styled later)
    content.append(" ${taskId}[\"${taskLabel}\"]\n")
    }
    content.append(" end\n\n")

    // Connect start to first task and last task to stop with execution order
    if (!executedTasks.isEmpty()) {
    def firstTaskId = sanitizeTaskId(executedTasks[0].path)
    def lastTaskId = sanitizeTaskId(executedTasks[-1].path)

    content.append(" Start -->|\"Step 1\"| ${firstTaskId}\n")

    // Connect tasks in execution order
    for (int i = 0; i < executedTasks.size() - 1; i++) {
    def currentTaskId = sanitizeTaskId(executedTasks[i].path)
    def nextTaskId = sanitizeTaskId(executedTasks[i + 1].path)
    def stepNumber = i + 2
    content.append(" ${currentTaskId} -->|\"Step ${stepNumber}\"| ${nextTaskId}\n")
    }

    content.append(" ${lastTaskId} -->|\"Complete\"| Stop\n\n")
    }
    }

    // Create subgraph for non-executed tasks (if any and not too many)
    if (!nonExecutedTasks.isEmpty() && nonExecutedTasks.size() <= 15) {
    content.append(" subgraph available [\"📋 Available Tasks\"]\n")
    content.append(" direction TB\n")
    nonExecutedTasks.sort { it.path }.each { task ->
    def taskId = sanitizeTaskId(task.path)
    def taskLabel = task.path
    content.append(" ${taskId}[\"${taskLabel}\"]\n")
    }
    content.append(" end\n\n")
    }

    // Add dependency relationships (shown as dotted lines to not interfere with execution order)
    def tasksToProcess = executedTasks.isEmpty() ? allTasks : executedTasks
    tasksToProcess.each { task ->
    def taskId = sanitizeTaskId(task.path)

    // Process task dependencies (show as dotted dependency lines)
    task.dependsOn.each { dependency ->
    if (dependency instanceof Task && executedTasks.contains(dependency)) {
    def depId = sanitizeTaskId(dependency.path)
    def dependencyKey = "${depId} -.-> ${taskId}"

    if (!processedDependencies.contains(dependencyKey)) {
    content.append(" ${depId} -.->|\"depends on\"| ${taskId}\n")
    processedDependencies.add(dependencyKey)
    }
    } else if (dependency instanceof TaskDependency) {
    dependency.getDependencies(task).each { depTask ->
    if (executedTasks.contains(depTask)) {
    def depId = sanitizeTaskId(depTask.path)
    def dependencyKey = "${depId} -.-> ${taskId}"

    if (!processedDependencies.contains(dependencyKey)) {
    content.append(" ${depId} -.->|\"depends on\"| ${taskId}\n")
    processedDependencies.add(dependencyKey)
    }
    }
    }
    }
    }

    // Process finalizedBy dependencies
    task.finalizedBy.getDependencies(task).each { finalizer ->
    if (executedTasks.contains(finalizer)) {
    def finalizerId = sanitizeTaskId(finalizer.path)
    def dependencyKey = "${taskId} -.-> ${finalizerId}"

    if (!processedDependencies.contains(dependencyKey)) {
    content.append(" ${taskId} -.->|\"finalizes\"| ${finalizerId}\n")
    processedDependencies.add(dependencyKey)
    }
    }
    }
    }

    // Add CSS styling (compatible with Mermaid v10.9.1)
    content.append("\n %% Styling\n")
    content.append(" classDef requested fill:#90EE90,stroke:#006400,stroke-width:3px,color:#000000\n")
    content.append(" classDef available fill:#F0F0F0,stroke:#808080,stroke-width:1px,color:#666666\n")

    // Apply styling to requested tasks
    requestedTasks.each { task ->
    def taskId = sanitizeTaskId(task.path)
    content.append(" class ${taskId} requested\n")
    }

    // Apply styling to available tasks (if shown)
    if (!nonExecutedTasks.isEmpty() && nonExecutedTasks.size() <= 15) {
    nonExecutedTasks.each { task ->
    def taskId = sanitizeTaskId(task.path)
    content.append(" class ${taskId} available\n")
    }
    }

    content.append("```\n\n")
    content.append("**Legend:**\n")
    content.append("- 🚀 **Executed Tasks**: Tasks that will run in this execution (in execution order)\n")
    content.append("- 📋 **Available Tasks**: Other tasks available but not executed\n")
    content.append("- 🟢 **Green Background**: Tasks explicitly requested by user\n")
    content.append("- `-->|Step N|` : Execution order (solid arrows with step numbers)\n")
    content.append("- `-.->|depends on|` : Task dependencies (dashed arrows)\n")
    content.append("- ⭕ **Start/Stop**: Execution flow markers\n\n")

    // Add summary statistics
    def totalTasks = allTasks.size()
    def executedTasksCount = executedTasks.size()
    def totalDependencies = processedDependencies.size()

    content.append("**Summary:**\n")
    content.append("- **Executed Tasks**: ${executedTasksCount}\n")
    content.append("- **Available Tasks**: ${totalTasks}\n")
    content.append("- **Dependencies**: ${totalDependencies}\n")
    content.append("- **Projects**: ${gradle.rootProject.allprojects.size()}\n")

    return content.toString()
    }

    /**
    * Sanitizes task path to be valid Mermaid node ID
    * Replaces special characters with underscores
    */
    def sanitizeTaskId(String taskPath) {
    return taskPath.replaceAll('[^a-zA-Z0-9_]', '_')
    .replaceAll('^_+', '') // Remove leading underscores
    .replaceAll('_+$', '') // Remove trailing underscores
    .replaceAll('_+', '_') // Replace multiple underscores with single
    }