diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..e48e536 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: gradle + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 + + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..196bfdb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-test: + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + steps: + - uses: actions/checkout@v6 + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "21" + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v6 + + - name: Quality + build + test + run: ./gradlew ktlintCheck detekt test build --stacktrace + env: + # mirror-api resolves from GitHub Packages (see settings.gradle.kts). + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload reports + if: always() + uses: actions/upload-artifact@v7 + with: + name: build-reports + path: "**/build/reports/**" + if-no-files-found: ignore diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..a24e436 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,50 @@ +name: CodeQL + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + schedule: + - cron: "27 4 * * 1" # weekly, Monday 04:27 UTC + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + security-events: write + steps: + - uses: actions/checkout@v6 + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "21" + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: java-kotlin + build-mode: manual + + - name: Build + run: ./gradlew build -x test --stacktrace + env: + # mirror-api resolves from GitHub Packages (see settings.gradle.kts). + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:java-kotlin" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..498b3a3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,40 @@ +name: Release + +on: + push: + tags: ["v*"] + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + packages: read + steps: + - uses: actions/checkout@v6 + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "21" + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Build module JAR + run: ./gradlew jar --stacktrace + env: + # mirror-api resolves from GitHub Packages (see settings.gradle.kts). + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: build/libs/*.jar + generate_release_notes: true diff --git a/build.gradle.kts b/build.gradle.kts index 76fda51..7095b3e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,12 +4,19 @@ plugins { alias(libs.plugins.kotlinJvm) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) + alias(libs.plugins.ktlint) + alias(libs.plugins.detekt) } kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } } +detekt { + buildUponDefaultConfig = true + config.setFrom(files("config/detekt/detekt.yml")) +} + java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 @@ -37,6 +44,9 @@ val deployToMirror by tasks.registering(Copy::class) { into( providers.gradleProperty("speculum.pluginsDir").orNull ?: System.getenv("SPECULUM_PLUGINS_DIR") - ?: layout.buildDirectory.dir("plugins").get().asFile.path + ?: layout.buildDirectory + .dir("plugins") + .get() + .asFile.path, ) } diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml new file mode 100644 index 0000000..e3a2721 --- /dev/null +++ b/config/detekt/detekt.yml @@ -0,0 +1,16 @@ +# detekt configuration — layered on top of the bundled defaults +# (buildUponDefaultConfig = true in build.gradle.kts). Only the relaxations +# that suit a small Compose example module are listed here. + +style: + MagicNumber: + # Compose UI code is full of literal dp/sp/weight values; flagging them + # adds noise without improving this reference module. + active: false + MaxLineLength: + maxLineLength: 120 + +complexity: + LongMethod: + # Composable functions legitimately grow past the default threshold. + threshold: 80 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 62fd6fb..1a17d7b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,6 +5,9 @@ composeMaterial3 = "1.9.0" # material3 is versioned independently of the Compose coroutines = "1.11.0" # The published Speculum plugin API to compile against (release tag without "v"). mirrorApi = "0.5.7" +# Quality gates. +ktlint = "13.1.0" # gradle plugin org.jlleitschuh.gradle.ktlint +detekt = "1.23.8" [libraries] # Compose artifacts declared directly (the `compose.runtime`/`material3`/… DSL @@ -20,3 +23,5 @@ mirror-api = { module = "org.speculum:mirror-api", version.ref = "mirrorApi" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose" } composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } diff --git a/src/main/kotlin/org/speculum/modules/example/ExampleModule.kt b/src/main/kotlin/org/speculum/modules/example/ExampleModule.kt index fb88a32..08ab25d 100644 --- a/src/main/kotlin/org/speculum/modules/example/ExampleModule.kt +++ b/src/main/kotlin/org/speculum/modules/example/ExampleModule.kt @@ -23,8 +23,9 @@ import org.speculum.ui.MirrorColors * * Config keys: greeting (default "Hello, Speculum!"), tickStep (default 1). */ -class ExampleModule(config: ModuleConfig) : MirrorModule(config) { - +class ExampleModule( + config: ModuleConfig, +) : MirrorModule(config) { private val greeting = config.string("greeting", "Hello, Speculum!") private val tickStep = config.int("tickStep", 1) @@ -60,4 +61,4 @@ class ExampleModule(config: ModuleConfig) : MirrorModule(config) { } override fun stop() { /* nothing to release */ } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/speculum/modules/example/ExampleModuleFactory.kt b/src/main/kotlin/org/speculum/modules/example/ExampleModuleFactory.kt index aab8017..a9398fe 100644 --- a/src/main/kotlin/org/speculum/modules/example/ExampleModuleFactory.kt +++ b/src/main/kotlin/org/speculum/modules/example/ExampleModuleFactory.kt @@ -14,10 +14,11 @@ class ExampleModuleFactory : ModuleFactory { override fun create(config: ModuleConfig): MirrorModule = ExampleModule(config) - override fun defaultConfig(): ModuleConfig = ModuleConfig( - module = "example", - position = "top_center", - refreshIntervalMs = 3000, - config = mapOf("greeting" to "Loaded from JAR!", "tickStep" to "2") - ) -} \ No newline at end of file + override fun defaultConfig(): ModuleConfig = + ModuleConfig( + module = "example", + position = "top_center", + refreshIntervalMs = 3000, + config = mapOf("greeting" to "Loaded from JAR!", "tickStep" to "2"), + ) +}