Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
5ede7ae
restructure project, add korro-gradle-plugin and korro-analysis subpr…
devcrocod Apr 17, 2026
7db0256
Refactor `korro-gradle-plugin`: restructure tasks, enhance configurat…
devcrocod Apr 17, 2026
ea72e79
Add `integration-tests` module with Gradle TestKit-based golden-file …
devcrocod Apr 17, 2026
68a42f5
Set up GitHub Actions CI workflow using Java 17 and Gradle
devcrocod Apr 17, 2026
0fd142f
Refactor project structure, modularize functionality, and update depe…
devcrocod Apr 17, 2026
d8b63b3
Add integration tests for sample extraction and improve diagnostics s…
devcrocod Apr 17, 2026
10892a1
Add CLAUDE.md with detailed contributor guidelines and project archit…
devcrocod Apr 17, 2026
9e7225b
Refactor build configuration: centralize dependency versions using Gr…
devcrocod Apr 17, 2026
517e731
Refactor Korro tasks: replace `korroApply` with `korroGenerate` and `…
devcrocod Apr 18, 2026
6e62b8e
Add MDX directive support and extend integration tests
devcrocod Apr 18, 2026
65ce83d
Implement `korroCheckTask`: add diff-based validation for generated d…
devcrocod Apr 18, 2026
f39688e
Simplify CLAUDE.md: streamline overview, consolidate build commands, …
devcrocod Apr 18, 2026
9e3ce68
Migrate to `maven-publish` plugin alias and enhance publishing config…
devcrocod Apr 18, 2026
872bb86
Set up release workflow with version validation, Maven Central, and G…
devcrocod Apr 18, 2026
cec0096
Configure Renovate: add dependency grouping and monthly update schedule
devcrocod Apr 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true

[*.kt]
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL

[*.{yml,yaml,json}]
indent_size = 2

[*.md]
trim_trailing_whitespace = false
34 changes: 34 additions & 0 deletions .github/renovate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"schedule": ["on the first day of the month"],
"packageRules": [
{
"matchManagers": ["gradle"],
"groupName": "other dependencies"
},
{
"matchPackageNames": [
"org.jetbrains.kotlin:kotlin-stdlib",
"org.jetbrains.kotlin:kotlin-compiler",
"org.jetbrains.kotlin:analysis-api-for-ide",
"org.jetbrains.kotlin:analysis-api-impl-base-for-ide",
"org.jetbrains.kotlin:analysis-api-platform-interface-for-ide",
"org.jetbrains.kotlin:analysis-api-standalone-for-ide",
"org.jetbrains.kotlin:analysis-api-k2-for-ide",
"org.jetbrains.kotlin:low-level-api-fir-for-ide",
"org.jetbrains.kotlin:symbol-light-classes-for-ide",
"org.jetbrains.kotlin.jvm"
],
"groupName": "kotlin"
},
{
"matchManagers": ["github-actions"],
"groupName": "github actions"
},
{
"matchManagers": ["gradle-wrapper"],
"groupName": "gradle"
}
]
}
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: CI

on:
pull_request:
branches: [master]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'

- uses: gradle/actions/setup-gradle@v4

- run: ./gradlew build
95 changes: 95 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: Release

on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: "Version to release (must match gradle.properties, e.g. 0.2.0)"
required: true
type: string

permissions:
contents: read

jobs:
verify:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.resolve.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name || github.ref }}

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'

- uses: gradle/actions/setup-gradle@v4

- name: Resolve & validate version
id: resolve
run: |
RAW="${{ github.event.release.tag_name || inputs.version }}"
VERSION="${RAW#v}"
PROP=$(grep '^version=' gradle.properties | cut -d= -f2 | tr -d '[:space:]')
if [ "$VERSION" != "$PROP" ]; then
echo "::error::Release version ($VERSION) does not match gradle.properties ($PROP)"
exit 1
fi
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Version '$VERSION' is not X.Y.Z"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

- name: Build + integration tests
run: ./gradlew -Prelease build :integration-tests:test --stacktrace

publish-maven-central:
needs: verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name || github.ref }}

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'

- uses: gradle/actions/setup-gradle@v4

- name: Publish to Maven Central
run: ./gradlew -Prelease publishToMavenCentral --no-configuration-cache
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_IN_MEMORY_KEY_ID }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }}

publish-gradle-plugin-portal:
needs: verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name || github.ref }}

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'

- uses: gradle/actions/setup-gradle@v4

- name: Publish to Gradle Plugin Portal
run: ./gradlew -Prelease :korro-gradle-plugin:publishPlugins --no-configuration-cache
env:
GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }}
GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }}
68 changes: 64 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,66 @@
# Gradle
.gradle/
build/
out/
!gradle/wrapper/gradle-wrapper.jar
!**/src/**/build/
!**/src/**/out/

# Gradle local config (paths, SDK locations, credentials)
local.properties
gradle.properties.local

# Kotlin
.kotlin/
*.kotlin_metadata

# IntelliJ IDEA
.idea/
*.iml
*.ipr
*.iws

# Eclipse
.classpath
.project
.settings/
bin/

# VS Code
.vscode/

# JVM crash logs
hs_err_pid*
replay_pid*

# macOS
.DS_Store
.AppleDouble
.LSOverride

# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/

# Linux
*~
.directory
.Trash-*

# Editor temp files
*.swp
*.swo
*.bak
*.orig
*.rej

# Logs
*.log

.gradle
.idea
build
out
# Env / secrets
.env
.env.local
*.pem
*.key
67 changes: 67 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

Korro is a Gradle plugin (Kotlin/JVM), published as `io.github.devcrocod.korro`, that injects Kotlin function bodies into `.md`/`.mdx` docs via `<!---FUN ...-->` or `{/*---FUN ...--*/}` directives. Consumer-facing syntax and the DSL are in `README.md`; the 0.1.x→0.2.0 migration is in `MIGRATION.md`. **Read both before changing the directive parser or the extension DSL** — they are the downstream contract.

## Commands

Gradle wrapper (9.4.1):

- `./gradlew build` — compile and assemble both modules.
- `./gradlew :integration-tests:test` — GradleTestKit + golden-file tests under `integration-tests/fixtures/`. This is the only meaningful test suite in this repo.
- `./gradlew publishToMavenLocal` — install **both** artifacts to `~/.m2/` for consumer testing. Both must be installed together: `korroAnalysisRuntime` resolves `korro-analysis` at the plugin's own version at task-execution time.
- `./gradlew -Prelease build` — release-versioned artifact. Without `-Prelease`, `detectVersion()` in the root `build.gradle.kts` appends `-dev` (or `-dev-<build.number>`) to the version in `gradle.properties`.
- `./gradlew :korro-analysis:shadowJar` — build only the fat jar.
- `./gradlew publishPlugins` — publish to the Gradle Plugin Portal (requires credentials).

The `korroGenerate` / `korro` / `korroCheck` tasks the plugin registers are **not** runnable from this repo's root — only from a consumer project or an `integration-tests/fixtures/*` fixture.

## Architecture

Two modules separated by a Gradle worker boundary.

### `korro-gradle-plugin/` — Gradle-facing layer (thin)

Runs in the Gradle daemon classloader. No Analysis API imports at compile time — only `compileOnly(gradleApi())` + `implementation(kotlin("stdlib"))`. Contains `KorroPlugin`, `KorroExtension`, the three tasks, and the markdown directive parser (`Korro.kt`).

The parser lives here, not in `korro-analysis`, because `<!---…-->` / `{/*---…--*/}` parsing doesn't need the Analysis API. Per-file marker form is selected by extension through `DirectiveSyntax`: `.mdx` uses JSX-expression comments (required — Mintlify/Docusaurus reject raw HTML comments); everything else uses the HTML-comment form.

Analysis code is pulled in at task-execution time: `KorroPlugin` creates a detached `korroAnalysisRuntime` configuration with a dependency on `io.github.devcrocod:korro-analysis:<pluginVersion>`, and tasks submit work via `WorkerExecutor.classLoaderIsolation { classpath.from(korroRuntimeClasspath) }`.

**Task shape to preserve:**

- `korroGenerate` (`@CacheableTask`) writes out-of-place to `build/korro/docs/`.
- `korro` extends `Copy` (never `Sync`), depends on `korroGenerate`, and copies its output onto `docs.baseDir`. This is the only source-mutation point. **Must stay `Copy`:** `docs.baseDir` is typically the repo or project root and contains many files Korro does not manage — `Sync`'s delete-unknown semantics would wipe the working tree.
- `korroCheck` (`@CacheableTask`) regenerates into `build/korro/check/`, diffs against the source tree, and fails the build with the first differing line per file. CI entry point.
- Every task has an `@Input korroPluginVersion` so cached outputs invalidate on plugin bump (which is also the Analysis API bump).

### `korro-analysis/` — Analysis layer (shadowed fat jar)

Runs inside the worker's isolated classloader. Bundles the Kotlin Analysis API (K2 standalone), low-level FIR, and the IntelliJ platform. `com.intellij.*` and `org.jetbrains.kotlin.*` are **intentionally unrelocated** — the Analysis API is already uniquely namespaced, and relocating it breaks reflection lookups inside the platform.

- One `StandaloneAnalysisAPISession` per `KorroWorkAction.execute()` call, disposed in a `try/finally`. Do **not** call `disposeGlobalStandaloneApplicationServices()` — it's a one-shot that invalidates all future Analysis API use in the JVM. `classLoaderIsolation` gives a fresh classloader per task run, so singletons are reloaded naturally.
- FQN resolution is two-tier: a fast-path short-name index over `KtNamedFunction`s for unambiguous bare names, then a dummy-KDoc `/** [fqn] */` fallback for qualified/ambiguous names. First-import-wins on ambiguity.

### Worker boundary

`KorroWorkParameters` is serialized across the classloader boundary (even under `classLoaderIsolation`, Gradle serializes parameters). All fields must stay `Serializable` — `Set<File>`, primitives, strings, and the `SamplesGroup` DTO only. No `Project` / `Task` / `Logger` references.

## Version wiring

- Korro's own version lives in `gradle.properties` (`version=...`). Both subprojects inherit it via `subprojects { version = rootProject.version }` in the root `build.gradle.kts`. At runtime the plugin reads it from a generated `META-INF/korro-gradle-plugin.properties` resource (`KorroPlugin.readKorroPluginVersion`).
- Every other version lives in `gradle/libs.versions.toml`. The catalog is the single source of truth — do not hard-code versions in subproject scripts; add to the catalog and reference as `libs.*` / `libs.plugins.*`.
- `libs.versions.kotlin` — pinned Kotlin / Analysis API version. `libs.versions.kotlinLanguage` — Kotlin `languageVersion`/`apiVersion` used to compile Korro itself; unrelated to the bundled Analysis API. JVM target is hard-coded to `17` in the root `build.gradle.kts`.

## Invariants to preserve

These are contracts for every consumer's docs; breaking any of them silently breaks downstream projects.

- **Directives start at column 0 after `String.trim()`.** `parseDirective` returns `null` otherwise.
- **Three dashes to open, two to close.** `<!---NAME VALUE-->` for `.md` (and anything non-`.mdx`); `{/*---NAME VALUE--*/}` for `.mdx`. Do not collapse the open marker to two dashes — that becomes a standard HTML/MDX comment, and consumer docs rely on the distinction.
- **Directive name regex is `[_a-zA-Z.]+`.** Broadening it changes parsing for every consumer.
- **First `IMPORT` wins** on ambiguous short names (`firstNotNullOfOrNull` over the `imports` list).
- **Only `KtNamedFunction`** is a valid `FUN`/`FUNS` target. Properties, classes, top-level expressions, and `.kts` scripts must produce a diagnostic, not a silent empty snippet.
- **`behavior.ignoreMissing=false` is the strict-by-default contract.** Don't silently lower severity on unresolved references without an explicit opt-in.
27 changes: 26 additions & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
http://www.apache.org/licenses/

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

Expand Down Expand Up @@ -175,3 +175,28 @@
of your accepting any such warranty or additional liability.

END OF TERMS AND CONDITIONS

APPENDIX: How to apply the Apache License to your work.

To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright [yyyy] [name of copyright owner]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Loading
Loading