All skills
kotlin avatar

/kotlin-tooling-kotlin-toolchain-plugin-authoring

@0ad228e official
by kotlinkotlin/kotlin-agent-skills1.1k stars
42

Load when authoring, writing, or designing a Kotlin Toolchain local plugin to extend the declarative build with code generation, build-time processing, custom verification, or packaging that module.yaml cannot express, or when referencing @TaskAction, @Configurable, plugin.yaml, or jvm/amper-plugin. Skip for porting an existing Gradle plugin.

Use this Skill: https://skilld.dev/gh/kotlin/kotlin-agent-skills/kotlin-tooling-kotlin-toolchain-plugin-authoring

This session only. Nothing lands on disk.

referencesexamples.md

≈1.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Plugin authoring patterns

Snippets to adapt — rename packages and types to your domain. The running example is a release / version-stamping plugin, because it exercises every mechanism (settings, task actions, @Input/@Output, generated resources, env-var overrides) in one place.

1. project.yaml

modules:
  - <consumer-module>
  - plugins/<name>

plugins:
  - ./plugins/<name>

2. plugins/<name>/module.yaml

product: jvm/amper-plugin

dependencies:
  - <coordinate>:<version>
  - <coordinate>:<version>: runtime-only

pluginInfo:
  id: <plugin-id>
  settingsClass: com.example.<name>.Settings

settings:
  jvm:
    jdk:
      version: 21
  kotlin:
    languageVersion: 2.1

3. @Configurable interface Settings

package com.example.release

import org.jetbrains.amper.plugins.Configurable

@Configurable
interface Settings {
    val tagPrefix: String get() = "v"
    val initialVersion: String get() = "0.1.0"
    val ignoreUncommittedChanges: Boolean get() = false
    val releaseBranchPattern: String get() = "main|master"
    val checks: ChecksSettings
}

@Configurable
interface ChecksSettings {
    val uncommittedChanges: Boolean get() = true
    val aheadOfRemote: Boolean get() = true
    val snapshotDependencies: Boolean get() = true
}

Consumer module.yaml:

plugins:
  release:
    enabled: true
    tagPrefix: "v"
    initialVersion: "0.1.0"
    ignoreUncommittedChanges: false
    checks:
      aheadOfRemote: true

4. @TaskAction — one per file under src/tasks/

package com.example.release.tasks

import com.example.release.Settings
import com.example.release.git.GitRepo
import com.example.release.version.VersionPipeline
import org.jetbrains.amper.plugins.Input
import org.jetbrains.amper.plugins.TaskAction
import java.nio.file.Path

@TaskAction
fun currentVersion(
    @Input moduleRootDir: Path,
    settings: Settings,
) {
    val pipeline = VersionPipeline(settings)
    GitRepo.open(moduleRootDir, settings.repoDir).use { repo ->
        println(pipeline.infer(repo).version)
    }
}

5. File-based publication — the project.version analog

One task writes the value into its @Output; everything else reads it from there. Execution avoidance is disabled because the real input is Git state.

package com.example.release.tasks

import org.jetbrains.amper.plugins.ExecutionAvoidance
import org.jetbrains.amper.plugins.Input
import org.jetbrains.amper.plugins.Output
import org.jetbrains.amper.plugins.TaskAction
import java.nio.file.Path
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.createParentDirectories
import kotlin.io.path.deleteRecursively
import kotlin.io.path.div
import kotlin.io.path.writeText

@OptIn(ExperimentalPathApi::class)
@TaskAction(executionAvoidance = ExecutionAvoidance.Disabled)
fun writeVersion(
    @Input moduleRootDir: Path,
    @Output outputDir: Path,
    settings: Settings,
) {
    val pipeline = VersionPipeline(settings)
    val inferred = GitRepo.open(moduleRootDir, settings.repoDir).use { pipeline.infer(it) }

    outputDir.deleteRecursively()
    val versionFile = outputDir / "META-INF" / "release" / "version.txt"
    versionFile.createParentDirectories()
    versionFile.writeText(inferred.version + "\n")
}

Build-time consumer — declare @Input on a matching path and the dependency is inferred:

@TaskAction
fun packageWithVersion(@Input versionFile: Path) {
    val version = versionFile.readText().trim()
    // ...
}
tasks:
  packageWithVersion:
    action: !com.example.packageWithVersion
      versionFile: ${tasks.writeVersion.action.outputDir}/META-INF/release/version.txt

Runtime consumer — the directory is registered under generated.resources (§6), so the file is on the classpath:

fun version(): String? =
    object {}.javaClass.getResourceAsStream("/META-INF/release/version.txt")
        ?.bufferedReader()?.use { it.readText().trim() }

6. plugin.yaml

tasks:
  currentVersion:
    action: !com.example.release.tasks.currentVersion
      moduleRootDir: ${module.rootDir}
      settings: ${pluginSettings}

  writeVersion:
    action: !com.example.release.tasks.writeVersion
      moduleRootDir: ${module.rootDir}
      outputDir: ${taskOutputDir}
      settings: ${pluginSettings}

  release:
    action: !com.example.release.tasks.release
      moduleRootDir: ${module.rootDir}
      settings: ${pluginSettings}

generated:
  resources:
    - directory: ${tasks.writeVersion.action.outputDir}

# `writeVersion` stays out of commands: its @Output feeds generated.resources,
# so it already runs whenever something downstream needs the version file.
commands:
  - currentVersion
  - release

!com.example.release.tasks.currentVersion is the YAML tag form addressing a @TaskAction by fully-qualified name.

7. Environment-variable overrides

Take the env map as a constructor parameter so tests can pass a controlled one.

class VersionPipeline(
    private val settings: Settings,
    private val env: Map<String, String?> = System.getenv(),
) {
    fun infer(repo: GitRepo): InferredVersion {
        val forced = env["RELEASE_FORCE_VERSION"]?.takeIf { it.isNotBlank() }
        val forceSnapshot = env["RELEASE_FORCE_SNAPSHOT"].asBoolean()
        // ...
    }
}

private fun String?.asBoolean(): Boolean =
    this != null && this.equals("true", ignoreCase = true)

8. Consumer module as validation harness

# consumer-app/module.yaml
product: jvm/app

plugins:
  release:
    enabled: true
    tagPrefix: "v"
    initialVersion: "0.1.0"
    releaseBranchPattern: "main|master"

settings:
  jvm:
    mainClass: com.example.demo.MainKt
    jdk:
      version: 21
  kotlin:
    languageVersion: 2.1
// consumer-app/src/main.kt
package com.example.demo

private const val VERSION_RESOURCE = "/META-INF/release/version.txt"

fun main() {
    val version = readVersionFromClasspath() ?: "(version unavailable)"
    println("demo-app version: $version")
}

private fun readVersionFromClasspath(): String? =
    object {}.javaClass.getResourceAsStream(VERSION_RESOURCE)
        ?.bufferedReader()
        ?.use { it.readText().trim() }
        ?.takeIf { it.isNotEmpty() }
./kotlin run :consumer-app
./kotlin do <command-name>
./kotlin show commands

Source: SKILL.md on GitHub

No alerts20d3 checks · Risk SAFE
  • Gen Agent Trust Hub20d

    The skill provides guidance for authoring local plugins for the Kotlin Toolchain. It is generally safe for development purposes, but it documents a pattern where build-time tasks ingest untrusted configuration data from environment variables and YAML files, creating a potential attack surface for indirect prompt injection.

  • Socket20d

    No alerts

  • Snyk20d

    Risk: LOW · No issues

Signed by skilld at 0ad228e. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 weeks ago.

Activeupdated last month
metadata
{
  "author": "github:@singleton11",
  "version": "0.1.0"
}

README badge

README badge for kotlin/kotlin-agent-skills/kotlin-tooling-kotlin-toolchain-plugin-authoring