Repository: smithy-lang/smithy
Pull request #2484 (2024-12-09 21:22:05)

Migrate build logic to conventions plugins
#### Background
* Overhauls our builds to use convention plugins, which is a far cleaner approach
* Spotless and formatting will be added/applied in a follow-up PR (separate to keep this PR from being several pages)

#### Testing
* `./gradlew clean build publish -c -Plog-tests`
* tested jreleaser configs (for main and cli release)
* checked shadowed publications, verified shadowed deps are not in the pom

---
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b8385815b..9a8e4bc27 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,16 +25,16 @@ jobs:
           java-version: ${{ matrix.java }}
           distribution: 'corretto'
 
-      - name: Clean, build and javadoc
+      - name: Clean, build, test, and javadoc
         run: ./gradlew clean build javadoc -Plog-tests --stacktrace
 
       - name: Allow long file names in git for windows
         if: matrix.os == 'windows-latest'
         run: git config --system core.longpaths true
 
-      - name: CLI integration tests
+      - name: Integration tests
         if: matrix.java == 17
-        run: ./gradlew :smithy-cli:integ -Plog-tests --stacktrace
+        run: ./gradlew integ -Plog-tests --stacktrace
 
       - uses: actions/upload-artifact@v4
         if: failure()
diff --git a/.gitignore b/.gitignore
index f39ff7525..7faf808e6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,7 +28,7 @@ target/
 **/dependency-reduced-pom.xml
 
 # Gradle
-/.gradle
+**/.gradle
 build/
 !smithy-build/src/main/java/software/amazon/smithy/build
 !smithy-build/src/main/resources/software/amazon/smithy/build
diff --git a/build.gradle.kts b/build.gradle.kts
index 5de3130ef..6c88af05c 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,3 +1,6 @@
+import org.jreleaser.gradle.plugin.JReleaserExtension
+import org.jreleaser.model.Active
+
 /*
  * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
  *
@@ -13,26 +16,13 @@
  * permissions and limitations under the License.
  */
 
-import com.github.spotbugs.snom.Effort
-import org.jreleaser.model.Active
-import org.gradle.api.tasks.testing.logging.TestExceptionFormat
-
 plugins {
-    `java-library`
-    `maven-publish`
-    signing
-    checkstyle
-    jacoco
-    id("com.github.spotbugs") version "6.0.8"
-    id("io.codearte.nexus-staging") version "0.30.0"
-    id("me.champeau.jmh") version "0.7.2"
-    id("com.github.johnrengelman.shadow") version "7.1.2"
-    id("org.jreleaser") version "1.12.0" apply false
+    java
+    alias(libs.plugins.jreleaser) apply false
 }
 
 // Load the Smithy version from VERSION.
 val libraryVersion = project.file("VERSION").readText().trim()
-
 println("Smithy version: '$libraryVersion'")
 
 allprojects {
@@ -40,236 +30,21 @@ allprojects {
     version = libraryVersion
 }
 
-// JReleaser publishes artifacts from a local staging repository, rather than maven local.
-// https://jreleaser.org/guide/latest/examples/maven/staging-artifacts.html#_gradle
-val stagingDirectory = rootProject.layout.buildDirectory.dir("staging")
-
-subprojects {
-    apply(plugin = "java-library")
-
-    java {
-        sourceCompatibility = JavaVersion.VERSION_1_8
-        targetCompatibility = JavaVersion.VERSION_1_8
-    }
-
-    repositories {
-        mavenLocal()
-        mavenCentral()
-    }
-
-    dependencies {
-        testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0")
-        testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0")
-        testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.0")
-        testImplementation("org.hamcrest:hamcrest:2.1")
-        testCompileOnly("org.apiguardian:apiguardian-api:1.1.2")
-    }
-
-    // Reusable license copySpec for building JARs
-    val licenseSpec = copySpec {
-        from("${project.rootDir}/LICENSE")
-        from("${project.rootDir}/NOTICE")
-    }
-
-    // Set up tasks that build source and javadoc jars.
-    tasks.register<Jar>("sourcesJar") {
-        metaInf.with(licenseSpec)
-        from(sourceSets.main.get().allSource)
-        archiveClassifier.set("sources")
-    }
-
-    // Build a javadoc JAR too.
-    tasks.register<Jar>("javadocJar") {
-        metaInf.with(licenseSpec)
-        from(tasks.javadoc)
-        archiveClassifier.set("javadoc")
-    }
-
-    // Include an Automatic-Module-Name in all JARs.
-    afterEvaluate {
-        tasks.jar {
-            metaInf.with(licenseSpec)
-            inputs.property("moduleName", project.extra.get("moduleName"))
-            manifest {
-                attributes("Automatic-Module-Name" to project.extra.get("moduleName"))
-            }
-        }
-    }
-
-    // Always run javadoc after build.
-    tasks["build"].dependsOn(tasks["javadoc"])
-
-    // ==== Tests ====
-    // https://docs.gradle.org/current/samples/sample_java_multi_project_with_junit5_tests.html
-    tasks.test {
-        useJUnitPlatform()
-    }
-
-    // Log on passed, skipped, and failed test events if the `-Plog-tests` property is set.
-    // https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/testing/logging/TestLoggingContainer.html
-    if (project.hasProperty("log-tests")) {
-        tasks.test {
-            testLogging {
-                events("passed", "skipped", "failed")
-                exceptionFormat = TestExceptionFormat.FULL
-            }
-        }
-    }
-
-    // ==== Maven ====
-    apply(plugin = "maven-publish")
-    apply(plugin = "signing")
-    apply(plugin = "com.github.johnrengelman.shadow")
-
-    // This is a little hacky, but currently needed to build a shadowed CLI JAR and smithy-syntax JAR with the same
-    // customizations as other JARs.
-    if (project.name != "smithy-cli" && project.name != "smithy-syntax") {
-        tasks.named<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") {
-            isEnabled = false
-        }
-    }
-
-    publishing {
-        repositories {
-            // JReleaser's `publish` task publishes to all repositories, so only configure one.
-            maven {
-                name = "localStaging"
-                url = uri(stagingDirectory)
-            }
-        }
-
-        publications {
-            create<MavenPublication>("mavenJava") {
-                if (tasks.findByName("shadowJar")?.enabled == true) {
-                    project.shadow.component(this)
-                } else {
-                    from(components["java"])
-                }
-
-                // Ship the source and javadoc jars.
-                artifact(tasks["sourcesJar"])
-                artifact(tasks["javadocJar"])
-
-                // Include extra information in the POMs.
-                afterEvaluate {
-                    pom {
-                        name.set(project.ext["displayName"].toString())
-                        description.set(project.description)
-                        url.set("https://github.com/smithy-lang/smithy")
-                        licenses {
-                            license {
-                                name.set("Apache License 2.0")
-                                url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
-                                distribution.set("repo")
-                            }
-                        }
-                        developers {
-                            developer {
-                                id.set("smithy")
-                                name.set("Smithy")
-                                organization.set("Amazon Web Services")
-                                organizationUrl.set("https://aws.amazon.com")
-                                roles.add("developer")
-                            }
-                        }
-                        scm {
-                            url.set("https://github.com/smithy-lang/smithy.git")
-                        }
-                    }
-                }
-            }
-        }
-
-        // Don't sign the artifacts if we didn't get a key and password to use.
-        if (project.hasProperty("signingKey") && project.hasProperty("signingPassword")) {
-            signing {
-                useInMemoryPgpKeys(
-                    project.property("signingKey").toString(),
-                    project.property("signingPassword").toString()
-                )
-                sign(publishing.publications["mavenJava"])
-            }
-        }
-    }
-
-    tasks.register<Copy>("copyMavenMetadataForDevelopment") {
-        from("build/tmp/publishMavenJavaPublicationToMavenLocal") {
-            rename("module-maven-metadata.xml", "maven-metadata.xml")
-        }
-        val wdir = "${System.getProperty("user.home")}/.m2/repository/software/amazon/smithy/${project.name}"
-        into(wdir)
-    }
-
-    tasks.publishToMavenLocal {
-        finalizedBy("copyMavenMetadataForDevelopment")
-    }
-
-    // ==== CheckStyle ====
-    // https://docs.gradle.org/current/userguide/checkstyle_plugin.html
-    apply(plugin = "checkstyle")
-    tasks.named("checkstyleTest") {
-        enabled = false
-    }
-
-    // ==== Code coverage ====
-    // https://docs.gradle.org/current/userguide/jacoco_plugin.html
-    apply(plugin = "jacoco")
-
-    // report is always generated after tests run
-    tasks.test {
-        finalizedBy(tasks.jacocoTestReport)
-    }
-
-    // tests are required to run before generating the report
-    tasks.jacocoTestReport {
-        dependsOn(tasks.test)
-        reports {
-            xml.required.set(false)
-            csv.required.set(false)
-            html.outputLocation.set(file("$buildDir/reports/jacoco"))
-        }
-    }
-
-    // ==== Spotbugs ====
-    // https://plugins.gradle.org/plugin/com.github.spotbugs
-    apply(plugin = "com.github.spotbugs")
-    // We don't need to lint tests.
-    tasks.named("spotbugsTest") {
-        enabled = false
-    }
-    // Configure the bug filter for spotbugs.
-    spotbugs {
-        effort.set(Effort.MAX)
-        excludeFilter.set(file("${project.rootDir}/config/spotbugs/filter.xml"))
-    }
-}
-
-// The root project doesn't produce a JAR.
-tasks.named("jar") {
-    enabled = false
-}
-
-// ==== Javadoc ====
+// Consolidated Javadoc creation
 afterEvaluate {
-    tasks.javadoc {
-        title = "Smithy API ${version}"
-        setDestinationDir(file("${project.buildDir}/docs/javadoc/latest"))
-        // Build a consolidated javadoc of all subprojects.
-        source(subprojects.map { project(it.path).sourceSets.main.get().allJava })
-        classpath = files(subprojects.map { project(it.path).sourceSets.main.get().compileClasspath })
-    }
-}
-
-// Disable HTML doclint to work around heading tag sequence validation
-// inconsistencies between JDK15 and earlier Java versions.
-allprojects {
-    tasks.withType<Javadoc> {
-        (options as StandardJavadocDocletOptions).apply {
-            addStringOption("Xdoclint:-html", "-quiet")
-            // Fixed in JDK 12: https://bugs.openjdk.java.net/browse/JDK-8215291
-            // --no-module-directories does not exist in JDK 8 and is removed in 13
-            if (JavaVersion.current().run { isJava9 || isJava10 || isJava11 }) {
-                addBooleanOption("-no-module-directories", true)
+    tasks {
+        javadoc {
+            title = "Smithy API ${version}"
+            setDestinationDir(layout.buildDirectory.dir("docs/javadoc/latest").get().asFile)
+            source(subprojects.map { project(it.path).sourceSets.main.get().allJava })
+            classpath = files(subprojects.map { project(it.path).sourceSets.main.get().compileClasspath })
+            (options as StandardJavadocDocletOptions).apply {
+                addStringOption("Xdoclint:-html", "-quiet")
+                // Fixed in JDK 12: https://bugs.openjdk.java.net/browse/JDK-8215291
+                // --no-module-directories does not exist in JDK 8 and is removed in 13
+                if (JavaVersion.current().run { isJava9 || isJava10 || isJava11 }) {
+                    addBooleanOption("-no-module-directories", true)
+                }
             }
         }
     }
@@ -280,29 +55,32 @@ allprojects {
 if (project.hasProperty("release.main")) {
     apply(plugin = "org.jreleaser")
 
-    extensions.configure<org.jreleaser.gradle.plugin.JReleaserExtension> {
-        dryrun.set(false)
+    // Workaround for https://github.com/jreleaser/jreleaser/issues/1492
+    tasks.register("clean")
+
+    configure<JReleaserExtension> {
+        dryrun = false
 
         // Used for creating and pushing the version tag, but this configuration ensures that
         // an actual GitHub release isn't created (since the CLI release does that)
         release {
             github {
-                skipRelease.set(true)
-                tagName.set("{{projectVersion}}")
+                skipRelease = true
+                tagName = "{{projectVersion}}"
             }
         }
 
         // Used to announce a release to configured announcers.
         // https://jreleaser.org/guide/latest/reference/announce/index.html
         announce {
-            active.set(Active.NEVER)
+            active = Active.NEVER
         }
 
         // Signing configuration.
         // https://jreleaser.org/guide/latest/reference/signing.html
         signing {
-            active.set(Active.ALWAYS)
-            armored.set(true)
+            active = Active.ALWAYS
+            armored = true
         }
 
         // Configuration for deploying to Maven Central.
@@ -311,15 +89,15 @@ if (project.hasProperty("release.main")) {
             maven {
                 nexus2 {
                     create("maven-central") {
-                        active.set(Active.ALWAYS)
-                        url.set("https://aws.oss.sonatype.org/service/local")
-                        snapshotUrl.set("https://aws.oss.sonatype.org/content/repositories/snapshots")
-                        closeRepository.set(true)
-                        releaseRepository.set(true)
-                        stagingRepository(stagingDirectory.get().toString())
+                        active = Active.ALWAYS
+                        url = "https://aws.oss.sonatype.org/service/local"
+                        snapshotUrl = "https://aws.oss.sonatype.org/content/repositories/snapshots"
+                        closeRepository = true
+                        releaseRepository = true
+                        stagingRepository(stagingDir().get().asFile.path)
                     }
                 }
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
new file mode 100644
index 000000000..eef7222a6
--- /dev/null
+++ b/buildSrc/build.gradle.kts
@@ -0,0 +1,18 @@
+plugins {
+    `kotlin-dsl`
+}
+
+repositories {
+    mavenCentral()
+    gradlePluginPortal()
+}
+
+dependencies {
+    // Plugins used by buildSrc scripts
+    implementation(libs.spotbugs)
+    implementation(libs.spotless)
+    implementation(libs.jmh)
+
+    // https://github.com/gradle/gradle/issues/15383
+    implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
+}
diff --git a/buildSrc/gradle.properties b/buildSrc/gradle.properties
new file mode 100644
index 000000000..d2cd2b1f5
--- /dev/null
+++ b/buildSrc/gradle.properties
@@ -0,0 +1,2 @@
+org.gradle.parallel=true
+org.gradle.jvmargs='-Dfile.encoding=UTF-8'
diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle
new file mode 100644
index 000000000..6a22136dd
--- /dev/null
+++ b/buildSrc/settings.gradle
@@ -0,0 +1,9 @@
+
+// Ensure version library is available to buildSrc plugins
+dependencyResolutionManagement {
+    versionCatalogs {
+        create("libs") {
+            from(files("../gradle/libs.versions.toml"))
+        }
+    }
+}
diff --git a/buildSrc/src/main/kotlin/Extensions.kt b/buildSrc/src/main/kotlin/Extensions.kt
new file mode 100644
index 000000000..d0f92ef86
--- /dev/null
+++ b/buildSrc/src/main/kotlin/Extensions.kt
@@ -0,0 +1,10 @@
+import org.gradle.api.Project
+import org.gradle.api.file.Directory
+import org.gradle.api.provider.Provider
+
+// JReleaser publishes artifacts from a local staging repository, rather than maven local.
+// https://jreleaser.org/guide/latest/examples/maven/staging-artifacts.html#_gradle
+fun Project.stagingDir(): Provider<Directory> {
+   // We should use the root build directory
+   return rootProject.layout.buildDirectory.dir("staging")
+}
diff --git a/buildSrc/src/main/kotlin/smithy.formatting-conventions.gradle.kts b/buildSrc/src/main/kotlin/smithy.formatting-conventions.gradle.kts
new file mode 100644
index 000000000..9302301da
--- /dev/null
+++ b/buildSrc/src/main/kotlin/smithy.formatting-conventions.gradle.kts
@@ -0,0 +1,24 @@
+plugins {
+    checkstyle
+    id("com.diffplug.spotless")
+}
+
+// TODO: Add spotless java configuration
+/*
+ * Formatting
+ * ==================
+ * see: https://github.com/diffplug/spotless/blob/main/plugin-gradle/README.md
+ */
+spotless {
+    // Formatting for build.gradle.kts files
+    kotlinGradle {
+        ktlint()
+        indentWithSpaces()
+        trimTrailingWhitespace()
+        endWithNewline()
+        licenseHeaderFile(
+            "${project.rootDir}/config/spotless/license-header.txt",
+            "import|tasks|apply|plugins|rootProject"
+        )
+    }
+}
diff --git a/buildSrc/src/main/kotlin/smithy.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/smithy.java-conventions.gradle.kts
new file mode 100644
index 000000000..5b822b737
--- /dev/null
+++ b/buildSrc/src/main/kotlin/smithy.java-conventions.gradle.kts
@@ -0,0 +1,146 @@
+import org.gradle.api.tasks.testing.logging.TestExceptionFormat
+import com.github.spotbugs.snom.Effort
+
+plugins {
+    `java-library`
+    jacoco
+    id("com.github.spotbugs")
+    id("smithy.formatting-conventions")
+}
+
+// Workaround per: https://github.com/gradle/gradle/issues/15383
+val Project.libs get() = the<org.gradle.accessors.dm.LibrariesForLibs>()
+
+repositories {
+    mavenLocal()
+    mavenCentral()
+}
+
+/*
+ * Base Java Jar Tasks
+ * ===================
+ */
+// Reusable license copySpec for building JARs
+val licenseSpec = copySpec {
+    from("${project.rootDir}/LICENSE")
+    from("${project.rootDir}/NOTICE")
+}
+
+java {
+    toolchain {
+        setSourceCompatibility(8)
+        setTargetCompatibility(8)
+    }
+}
+
+tasks {
+    // Set up tasks that build source and javadoc jars.
+    val sourcesJar by registering(Jar::class) {
+        metaInf.with(licenseSpec)
+        from(sourceSets.main.map { it.allSource })
+        archiveClassifier.set("sources")
+    }
+
+    javadoc {
+        // Disable HTML doclint to work around heading tag sequence validation
+        // inconsistencies between JDK15 and earlier Java versions.
+        (options as StandardJavadocDocletOptions).apply {
+            addStringOption("Xdoclint:-html", "-quiet")
+            // Fixed in JDK 12: https://bugs.openjdk.java.net/browse/JDK-8215291
+            // --no-module-directories does not exist in JDK 8 and is removed in 13
+            if (JavaVersion.current().run { isJava9 || isJava10 || isJava11 }) {
+                addBooleanOption("-no-module-directories", true)
+            }
+        }
+    }
+
+    // Always run javadoc after build.
+    build {
+        dependsOn(javadoc)
+    }
+
+    // Build a javadoc JAR too.
+    val javadocJar by registering(Jar::class) {
+        metaInf.with(licenseSpec)
+        from(javadoc)
+        archiveClassifier.set("javadoc")
+    }
+
+    // Include an Automatic-Module-Name in all JARs.
+    afterEvaluate {
+        jar {
+            metaInf.with(licenseSpec)
+            inputs.property("moduleName", project.extra.get("moduleName"))
+            manifest {
+                attributes("Automatic-Module-Name" to project.extra.get("moduleName"))
+            }
+        }
+    }
+}
+
+/*
+ * Common test configuration
+ * =========================
+ */
+dependencies {
+    testImplementation(libs.junit.jupiter.api)
+    testRuntimeOnly(libs.junit.jupiter.engine)
+    testImplementation(libs.junit.jupiter.params)
+    testImplementation(libs.hamcrest)
+    testCompileOnly(libs.apiguardian.api)
+}
+
+// https://docs.gradle.org/current/samples/sample_java_multi_project_with_junit5_tests.html
+tasks.withType<Test> {
+    useJUnitPlatform()
+}
+
+// Log on passed, skipped, and failed test events if the `-Plog-tests` property is set.
+// https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/testing/logging/TestLoggingContainer.html
+if (project.hasProperty("log-tests")) {
+    tasks.test {
+        testLogging {
+            events("passed", "skipped", "failed")
+            exceptionFormat = TestExceptionFormat.FULL
+        }
+    }
+}
+
+// ==== CheckStyle ====
+// https://docs.gradle.org/current/userguide/checkstyle_plugin.html
+tasks.named("checkstyleTest") {
+    enabled = false
+}
+
+// ==== Code coverage ====
+// https://docs.gradle.org/current/userguide/jacoco_plugin.html
+
+// report is always generated after tests run
+tasks.test {
+    finalizedBy(tasks.jacocoTestReport)
+}
+
+// tests are required to run before generating the report
+tasks.jacocoTestReport {
+    dependsOn(tasks.test)
+    reports {
+        xml.required.set(false)
+        csv.required.set(false)
+        html.outputLocation.set(layout.buildDirectory.dir("reports/jacoco"))
+    }
+}
+
+// ==== Spotbugs ====
+// https://plugins.gradle.org/plugin/com.github.spotbugs
+
+// We don't need to lint tests.
+tasks.named("spotbugsTest") {
+    enabled = false
+}
+
+// Configure the bug filter for spotbugs.
+spotbugs {
+    effort.set(Effort.MAX)
+    excludeFilter.set(file("${project.rootDir}/config/spotbugs/filter.xml"))
+}
+
diff --git a/buildSrc/src/main/kotlin/smithy.module-conventions.gradle.kts b/buildSrc/src/main/kotlin/smithy.module-conventions.gradle.kts
new file mode 100644
index 000000000..03154d2ab
--- /dev/null
+++ b/buildSrc/src/main/kotlin/smithy.module-conventions.gradle.kts
@@ -0,0 +1,4 @@
+plugins {
+    id("smithy.java-conventions")
+    id("smithy.publishing-conventions")
+}
diff --git a/buildSrc/src/main/kotlin/smithy.profiling-conventions.gradle.kts b/buildSrc/src/main/kotlin/smithy.profiling-conventions.gradle.kts
new file mode 100644
index 000000000..ef9d3d22b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/smithy.profiling-conventions.gradle.kts
@@ -0,0 +1,13 @@
+plugins {
+    id("me.champeau.jmh")
+}
+
+jmh {
+    timeUnit = "us"
+}
+
+tasks {
+    processJmhResources {
+        duplicatesStrategy = DuplicatesStrategy.WARN
+    }
+}
diff --git a/buildSrc/src/main/kotlin/smithy.publishing-conventions.gradle.kts b/buildSrc/src/main/kotlin/smithy.publishing-conventions.gradle.kts
new file mode 100644
index 000000000..7e96d4f69
--- /dev/null
+++ b/buildSrc/src/main/kotlin/smithy.publishing-conventions.gradle.kts
@@ -0,0 +1,84 @@
+
+plugins {
+    `maven-publish`
+    signing
+}
+
+publishing {
+    repositories {
+        // JReleaser's `publish` task publishes to all repositories, so only configure one.
+        maven {
+            name = "localStaging"
+            url = uri(stagingDir())
+        }
+    }
+
+    publications {
+        create<MavenPublication>("mavenJava") {
+            if (components.names.contains("shadow")) {
+                // Use the shadow component if its exists
+                from(components["shadow"])
+            } else {
+                // Otherwise, use the standard java component
+                from(components["java"])
+            }
+
+            // Ship the source and javadoc jars.
+            artifact(tasks["sourcesJar"])
+            artifact(tasks["javadocJar"])
+
+            // Include extra information in the POMs.
+            afterEvaluate {
+                pom {
+                    name.set(project.ext["displayName"].toString())
+                    description.set(project.description)
+                    url.set("https://github.com/smithy-lang/smithy")
+                    licenses {
+                        license {
+                            name.set("Apache License 2.0")
+                            url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
+                            distribution.set("repo")
+                        }
+                    }
+                    developers {
+                        developer {
+                            id.set("smithy")
+                            name.set("Smithy")
+                            organization.set("Amazon Web Services")
+                            organizationUrl.set("https://aws.amazon.com")
+                            roles.add("developer")
+                        }
+                    }
+                    scm {
+                        url.set("https://github.com/smithy-lang/smithy.git")
+                    }
+                }
+            }
+        }
+    }
+
+    // Don't sign the artifacts if we didn't get a key and password to use.
+    if (project.hasProperty("signingKey") && project.hasProperty("signingPassword")) {
+        signing {
+            useInMemoryPgpKeys(
+                project.property("signingKey").toString(),
+                project.property("signingPassword").toString()
+            )
+            sign(publishing.publications["mavenJava"])
+        }
+    }
+}
+
+tasks {
+    val copyMavenMetadataForDevelopment by registering(Copy::class) {
+        from(layout.buildDirectory.dir("tmp/publishMavenJavaPublicationToMavenLocal")) {
+            rename("module-maven-metadata.xml", "maven-metadata.xml")
+        }
+        val wdir = "${System.getProperty("user.home")}/.m2/repository/software/amazon/smithy/${project.name}"
+        into(wdir)
+    }
+
+    publishToMavenLocal {
+        finalizedBy(copyMavenMetadataForDevelopment)
+    }
+}
diff --git a/config/spotless/license-header.txt b/config/spotless/license-header.txt
new file mode 100644
index 000000000..35d0f3613
--- /dev/null
+++ b/config/spotless/license-header.txt
@@ -0,0 +1,4 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
diff --git a/gradle.properties b/gradle.properties
index 1ac05ea25..af1b8184d 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,3 +1,3 @@
-org.gradle.parallel=true
 org.gradle.jvmargs='-Dfile.encoding=UTF-8'
+org.gradle.parallel=true
 smithyGradleVersion=1.1.0
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 000000000..bf4a0bf88
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,57 @@
+[versions]
+junit5 = "5.11.3"
+hamcrest = "3.0"
+jmh = "0.6.6"
+spotbugs = "6.0.22"
+spotless = "6.5.0"
+smithy-gradle = "1.1.0"
+assertj = "3.26.3"
+prettier4j = "0.1.1"
+maven-resolver-provider = "3.8.6"
+maven-resolver = "1.9.2"
+slf4j = "1.7.36"
+mockserver = "3.10.8"
+compile-testing = "0.21.0"
+json-schema = "1.12.1"
+shadow = "8.3.5"
+api-guardian = "1.1.2"
+jreleaser = "1.15.0"
+runtime = "1.13.1"
+
+[libraries]
+prettier4j = { module = "com.opencastsoftware:prettier4j", version.ref = "prettier4j" }
+
+### These maven resolver dependencies are shaded into the smithy-cli JAR.
+maven-resolver-provider = { module = "org.apache.maven:maven-resolver-provider", version.ref = "maven-resolver-provider" }
+maven-resolver-api = { module = "org.apache.maven.resolver:maven-resolver-api", version.ref = "maven-resolver" }
+maven-resolver-spi = { module = "org.apache.maven.resolver:maven-resolver-spi", version.ref =  "maven-resolver" }
+maven-resolver-util = { module = "org.apache.maven.resolver:maven-resolver-util", version.ref =  "maven-resolver" }
+maven-resolver-impl = { module = "org.apache.maven.resolver:maven-resolver-impl", version.ref =  "maven-resolver" }
+maven-resolver-connector-basic = { module = "org.apache.maven.resolver:maven-resolver-connector-basic", version.ref =  "maven-resolver" }
+maven-resolver-transport-file = { module = "org.apache.maven.resolver:maven-resolver-transport-file", version.ref =  "maven-resolver" }
+maven-resolver-transport-http = { module = "org.apache.maven.resolver:maven-resolver-transport-http", version.ref =  "maven-resolver" }
+
+slf4j-jul = { module = "org.slf4j:slf4j-jdk14", version.ref = "slf4j" }
+
+# Test dependencies
+junit-bom = { module = "org.junit:junit-bom", version.ref = "junit5" }
+junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit5" }
+junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit5" }
+junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit5" }
+hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" }
+assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" }
+mockserver = { module = "org.mock-server:mockserver-netty", version.ref = "mockserver" }
+compile-testing = { module = "com.google.testing.compile:compile-testing", version.ref = "compile-testing" }
+json-schema-validator = { module = "com.github.everit-org.json-schema:org.everit.json.schema", version.ref = "json-schema" }
+apiguardian-api = { module = "org.apiguardian:apiguardian-api", version.ref = "api-guardian" }
+
+# plugin artifacts for buildsrc plugins
+spotbugs = { module = "com.github.spotbugs.snom:spotbugs-gradle-plugin", version.ref = "spotbugs" }
+spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" }
+jmh = { module = "me.champeau.jmh:jmh-gradle-plugin", version.ref = "jmh" }
+
+[plugins]
+smithy-jar = { id = "software.amazon.smithy.gradle.smithy-jar", version.ref = "smithy-gradle" }
+shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }
+jreleaser = { id = "org.jreleaser", version.ref = "jreleaser" }
+runtime = { id = "org.beryx.runtime", version.ref = "runtime"}
diff --git a/smithy-aws-apigateway-openapi/build.gradle.kts b/smithy-aws-apigateway-openapi/build.gradle.kts
index a69559c85..6c91437ce 100644
--- a/smithy-aws-apigateway-openapi/build.gradle.kts
+++ b/smithy-aws-apigateway-openapi/build.gradle.kts
@@ -1,20 +1,13 @@
 /*
- * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "This module provides support for converting the Amazon API Gateway " +
-        "Smithy traits when converting a Smithy model to OpenAPI3."
+    "Smithy traits when converting a Smithy model to OpenAPI3."
 
 extra["displayName"] = "Smithy :: Amazon API Gateway OpenAPI Support"
 extra["moduleName"] = "software.amazon.smithy.aws.apigateway.openapi"
diff --git a/smithy-aws-apigateway-traits/build.gradle.kts b/smithy-aws-apigateway-traits/build.gradle.kts
index 8813ee0b8..5188b0e01 100644
--- a/smithy-aws-apigateway-traits/build.gradle.kts
+++ b/smithy-aws-apigateway-traits/build.gradle.kts
@@ -1,17 +1,10 @@
 /*
- * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "This module provides Smithy traits and validators for API Gateway."
 
diff --git a/smithy-aws-cloudformation-traits/build.gradle.kts b/smithy-aws-cloudformation-traits/build.gradle.kts
index cc713c618..1f7b86dbc 100644
--- a/smithy-aws-cloudformation-traits/build.gradle.kts
+++ b/smithy-aws-cloudformation-traits/build.gradle.kts
@@ -1,17 +1,10 @@
 /*
- * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "This module provides Smithy traits and validators for CloudFormation."
 
diff --git a/smithy-aws-cloudformation/build.gradle.kts b/smithy-aws-cloudformation/build.gradle.kts
index a9ff1b095..269c20c49 100644
--- a/smithy-aws-cloudformation/build.gradle.kts
+++ b/smithy-aws-cloudformation/build.gradle.kts
@@ -1,17 +1,10 @@
 /*
- * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "This module contains support for converting Smithy resources to CloudFormation Resource Schemas."
 
@@ -32,5 +25,5 @@ dependencies {
 
     // For use in validating schemas used in tests against the supplied
     // CloudFormation definition schema.
-    testImplementation("com.github.everit-org.json-schema:org.everit.json.schema:1.12.1")
+    testImplementation(libs.json.schema.validator)
 }
diff --git a/smithy-aws-endpoints/build.gradle.kts b/smithy-aws-endpoints/build.gradle.kts
index ade669e8b..c142cd4ee 100644
--- a/smithy-aws-endpoints/build.gradle.kts
+++ b/smithy-aws-endpoints/build.gradle.kts
@@ -2,6 +2,9 @@
  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
  * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "AWS specific components for managing endpoints in Smithy"
 
diff --git a/smithy-aws-iam-traits/build.gradle.kts b/smithy-aws-iam-traits/build.gradle.kts
index b923ed37e..99c9c5e6f 100644
--- a/smithy-aws-iam-traits/build.gradle.kts
+++ b/smithy-aws-iam-traits/build.gradle.kts
@@ -1,17 +1,10 @@
 /*
- * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "This module provides Smithy traits and validators for IAM."
 
diff --git a/smithy-aws-traits/build.gradle.kts b/smithy-aws-traits/build.gradle.kts
index 4ff2ca248..a4ae351aa 100644
--- a/smithy-aws-traits/build.gradle.kts
+++ b/smithy-aws-traits/build.gradle.kts
@@ -1,17 +1,10 @@
 /*
- * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "This module provides Smithy traits and validators that are used by most AWS services."
 
diff --git a/smithy-build/build.gradle.kts b/smithy-build/build.gradle.kts
index 69e087c93..a44ae728d 100644
--- a/smithy-build/build.gradle.kts
+++ b/smithy-build/build.gradle.kts
@@ -1,20 +1,13 @@
 /*
- * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "This module is a library used to validate Smithy models, create filtered " +
-        "projections of a model, and generate build artifacts."
+    "projections of a model, and generate build artifacts."
 
 extra["displayName"] = "Smithy :: Build"
 extra["moduleName"] = "software.amazon.smithy.build"
@@ -24,5 +17,5 @@ dependencies {
     api(project(":smithy-model"))
 
     // Allows testing of annotation processor
-    testImplementation("com.google.testing.compile:compile-testing:0.21.0")
+    testImplementation(libs.compile.testing)
 }
diff --git a/smithy-cli/build.gradle b/smithy-cli/build.gradle
deleted file mode 100644
index e3635efb5..000000000
--- a/smithy-cli/build.gradle
+++ /dev/null
@@ -1,359 +0,0 @@
-/*
- * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
- */
-
-import org.apache.tools.ant.taskdefs.condition.Os
-
-import java.nio.file.Paths
-
-plugins {
-    id "application"
-    id "org.beryx.runtime" version "1.12.7"
-    id "org.jreleaser" version "1.12.0" apply false
-}
-
-description = "This module implements the Smithy command line interface."
-
-ext {
-    displayName = "Smithy :: CLI"
-    moduleName = "software.amazon.smithy.cli"
-    imageJreVersion = "17"
-    correttoRoot = "https://corretto.aws/downloads/latest/amazon-corretto-${imageJreVersion}"
-    generatedResourcesDir = file("$buildDir/generated-resources")
-}
-
-dependencies {
-    // Keeps these as exported transitive dependencies.
-    implementation project(":smithy-model")
-    implementation project(":smithy-build")
-    implementation project(":smithy-diff")
-    implementation project(path: ':smithy-syntax', configuration: 'shadow')
-
-    // This is needed to ensure the above dependencies are added to the runtime image.
-    shadow project(":smithy-model")
-    shadow project(":smithy-build")
-    shadow project(":smithy-diff")
-    shadow project(":smithy-syntax")
-
-    // These maven resolver dependencies are shaded into the smithy-cli JAR.
-    implementation "org.apache.maven:maven-resolver-provider:3.8.6"
-    implementation "org.apache.maven.resolver:maven-resolver-api:1.9.2"
-    implementation "org.apache.maven.resolver:maven-resolver-spi:1.9.2"
-    implementation "org.apache.maven.resolver:maven-resolver-util:1.9.2"
-    implementation "org.apache.maven.resolver:maven-resolver-impl:1.9.2"
-    implementation "org.apache.maven.resolver:maven-resolver-connector-basic:1.9.2"
-    implementation "org.apache.maven.resolver:maven-resolver-transport-file:1.9.2"
-    implementation "org.apache.maven.resolver:maven-resolver-transport-http:1.9.2"
-    implementation "org.slf4j:slf4j-jdk14:1.7.36" // Route slf4j used by Maven through JUL like the rest of Smithy.
-
-    testImplementation "org.mock-server:mockserver-netty:3.10.8"
-
-}
-
-// ------ Shade Maven dependency resolvers into the JAR. -------
-
-shadowJar {
-    // Replace the normal JAR with the shaded JAR.
-    archiveClassifier = ''
-
-    mergeServiceFiles()
-
-    // Shade dependencies to prevent conflicts with other dependencies.
-    relocate('org.slf4j', 'software.amazon.smithy.cli.shaded.slf4j')
-    relocate('org.eclipse', 'software.amazon.smithy.cli.shaded.eclipse')
-    relocate('org.apache', 'software.amazon.smithy.cli.shaded.apache')
-    relocate('org.sonatype', 'software.amazon.smithy.cli.shaded.sonatype')
-    relocate('org.codehaus', 'software.amazon.smithy.cli.shaded.codehaus')
-
-    // If other javax packages are ever pulled in, we'll need to update this list. This is more deliberate about
-    // what's shaded to ensure that things like javax.net.ssl.SSLSocketFactory are not inadvertently shaded.
-    relocate('javax.annotation', 'software.amazon.smithy.cli.shaded.javax.annotation')
-    relocate('javax.inject', 'software.amazon.smithy.cli.shaded.javax.inject')
-
-    // Don't shade Smithy dependencies into the CLI. These are normal dependencies that we want our consumers
-    // to resolve.
-    dependencies {
-        exclude(project(':smithy-utils'))
-        exclude(project(':smithy-model'))
-        exclude(project(':smithy-build'))
-        exclude(project(':smithy-diff'))
-        exclude(project(':smithy-syntax'))
-    }
-}
-
-tasks['jar'].finalizedBy(tasks['shadowJar'])
-
-// ------ Generate a file that contains the Smithy CLI version number. -------
-
-task generateVersionFile {
-    ext.versionFile = file("$generatedResourcesDir/software/amazon/smithy/cli/cli-version")
-    outputs.file(versionFile)
-    doLast {
-        versionFile.text = "${project.version}"
-    }
-}
-
-sourceSets.main.output.dir generatedResourcesDir, builtBy: generateVersionFile
-
-// ------ Setup CLI binary -------
-
-// This setting is needed by the Shadow plugin for some reason to define a main application class.
-mainClassName = "software.amazon.smithy.cli.SmithyCli"
-
-application {
-    mainClass = "${mainClassName}"
-    applicationName = "smithy"
-}
-
-// Detect which OS and arch is running to create an application class data sharing
-// archive for the current platform. This is not how we'll ultimately build and release images.
-if (Os.isFamily(Os.FAMILY_WINDOWS)) {
-    ext.set("imageOs", "windows-x64")
-} else if (Os.isFamily(Os.FAMILY_MAC)) {
-    if (Os.isArch("aarch64")) {
-        ext.set("imageOs", "darwin-aarch64")
-    } else if (Os.isArch("x86_64") || Os.isArch("amd64")) {
-        ext.set("imageOs", "darwin-x86_64")
-    } else {
-        println("No JDK for ${System.getProperty("os.arch")}")
-        ext.set("imageOs", "")
-    }
-} else if (Os.isFamily(Os.FAMILY_UNIX)) {
-    if (Os.isArch("aarch")) {
-        ext.set("imageOs", "linux-aarch64")
-    } else if (Os.isArch("x86_64") || Os.isArch("amd64")) {
-        ext.set("imageOs", "linux-x86_64")
-    } else {
-        println("No JDK for ${System.getProperty("os.arch")}")
-        ext.set("imageOs", "")
-    }
-} else {
-    println("Unknown OS and arch: ${System.getProperty("os.name")}")
-    ext.set("imageOs", "")
-}
-
-// This is needed in order for integration tests to find the build jlink CLI.
-if (Os.isFamily(Os.FAMILY_WINDOWS)) {
-    ext.set("smithyBinary", Paths.get(
-            "${project.buildDir}", "image", "smithy-cli-${imageOs}", "bin", "smithy.bat").toString())
-} else {
-    ext.set("smithyBinary", Paths.get(
-            "${project.buildDir}", "image", "smithy-cli-${imageOs}", "bin", "smithy").toString())
-}
-System.setProperty("SMITHY_BINARY", "${smithyBinary}")
-
-runtime {
-    addOptions("--compress", "2", "--strip-debug", "--no-header-files", "--no-man-pages")
-    addModules("java.logging", "java.xml", "java.naming", "jdk.crypto.ec")
-
-    launcher {
-        // This script is a combination of the default startup script used by the badass runtime
-        // plugin, and the upstream source it's based on:
-        // https://raw.githubusercontent.com/gradle/gradle/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
-        // Using the Gradle wrapper script as-is results in a huge startup penalty, so I instead updated parts of the
-        // script that didn't affect performance, and kept others that did. Namely, the set and eval code of the Gradle
-        // startup script is significantly slower than what was used by the plugin.
-        unixScriptTemplate = file('configuration/unixStartScript.txt')
-        jvmArgs = [
-            // Disable this when attempting to profile the CLI. In 99% of use cases this isn't not necessary.
-            '-XX:-UsePerfData',
-            '-Xshare:auto',
-            '-XX:SharedArchiveFile={{BIN_DIR}}/../lib/smithy.jsa'
-        ]
-    }
-
-    targetPlatform("linux-x86_64") {
-        jdkHome = jdkDownload("${correttoRoot}-x64-linux-jdk.tar.gz")
-    }
-
-    targetPlatform("linux-aarch64") {
-        jdkHome = jdkDownload("${correttoRoot}-aarch64-linux-jdk.tar.gz")
-    }
-
-    targetPlatform("darwin-x86_64") {
-        jdkHome = jdkDownload("${correttoRoot}-x64-macos-jdk.tar.gz")
-    }
-
-    targetPlatform("darwin-aarch64") {
-        jdkHome = jdkDownload("${correttoRoot}-aarch64-macos-jdk.tar.gz")
-    }
-
-    targetPlatform("windows-x64") {
-        jdkHome = jdkDownload("${correttoRoot}-x64-windows-jdk.zip")
-    }
-
-    // Because we're using target-platforms, it will use this property as a prefix for each target zip
-    imageZip = file("$buildDir/image/smithy-cli.zip")
-}
-
-// Add finishing touches to the distributables, such as an install script, before it gets zipped
-tasks.runtime.doLast {
-    targetPlatforms.each { targetPlatform ->
-        copy {
-            from "configuration"
-            include targetPlatform.value.name.contains("windows") ? "install.bat" : "install"
-            into Paths.get(
-                    "${project.buildDir}", "image", "smithy-cli-${targetPlatform.value.name}").toString()
-        }
-    }
-}
-
-tasks.register("optimize", Exec) {
-    commandLine("$smithyBinary", "warmup")
-}
-tasks["optimize"].dependsOn("runtime")
-
-// Always shadow the JAR and replace the JAR by the shadowed JAR.
-tasks['jar'].finalizedBy("shadowJar")
-
-// Prevent https://docs.gradle.org/7.3.3/userguide/validation_problems.html#implicit_dependency issues between
-// the runtime image and shadowJar tasks.
-tasks['distZip'].dependsOn("shadowJar")
-tasks['distTar'].dependsOn("shadowJar")
-tasks['startScripts'].dependsOn("shadowJar")
-tasks["runtime"].dependsOn("shadowJar")
-
-// ------ Setup integration testing -------
-
-sourceSets {
-    create("it") {
-        compileClasspath += sourceSets["main"].output + configurations["testRuntimeClasspath"] + configurations["testCompileClasspath"]
-        runtimeClasspath += output + compileClasspath + sourceSets["test"].runtimeClasspath
-    }
-}
-
-task integ(type: Test) {
-    useJUnitPlatform()
-    systemProperty "SMITHY_BINARY", "${smithyBinary}"
-    testClassesDirs = sourceSets["it"].output.classesDirs
-    classpath = sourceSets["it"].runtimeClasspath
-
-    // Configuration parameters to execute top-level classes in parallel but methods in same thread
-    systemProperties["junit.jupiter.execution.parallel.enabled"] = "true"
-    systemProperties["junit.jupiter.execution.parallel.mode.default"] = "same_thread"
-    systemProperties["junit.jupiter.execution.parallel.mode.classes.default"] = "concurrent"
-
-    testLogging {
-        events = ["passed", "skipped", "failed"]
-        exceptionFormat = "full"
-    }
-}
-
-// Runtime images need to be created before integration tests can run.
-tasks["integ"].dependsOn("runtime")
-
-// ------ Setup Jreleaser -------
-if (project.hasProperty("release.cli")) {
-    apply plugin: 'org.jreleaser'
-    tasks["assembleDist"].doFirst {
-        // This is a workaround for a weird behavior.
-        // https://github.com/jreleaser/jreleaser/issues/1292
-        mkdir "$buildDir/jreleaser"
-    }
-    tasks["assembleDist"].dependsOn("runtimeZip")
-
-    jreleaser {
-        gitRootSearch = true
-        dryrun = false
-
-        project {
-            website = 'https://smithy.io'
-            authors = ['Smithy']
-            vendor = "Smithy"
-            license = 'Apache-2.0'
-            description = "Smithy CLI - A CLI for building, validating, querying, and iterating on Smithy models"
-            copyright = "2019"
-        }
-
-        checksum {
-            individual = true
-            files = false
-        }
-
-        release {
-            github {
-                overwrite = true
-                tagName = '{{projectVersion}}'
-                skipTag = true
-                releaseName = 'Smithy CLI v{{{projectVersion}}}'
-                changelog {
-                    // For now, we won't have a changelog added to the release. In the future, we could create a changelog-snippet
-                    // from the real changelog as part of a command hook prior to the release step
-                    enabled = false
-                }
-                commitAuthor {
-                    name = "smithy-automation"
-                    email = "github-smithy-automation@amazon.com"
-                }
-            }
-        }
-
-        files {
-            active = "ALWAYS"
-            artifact {
-                // We'll include the VERSION file in the release artifacts so that the version can be easily
-                // retrieving by hitting the GitHub `releases/latest` url
-                path = "../VERSION"
-                extraProperties.put('skipSigning', true)
-            }
-        }
-
-        platform {
-            // These replacements are for the names of files that are released, *not* for names within this build config
-            replacements = [
-                    'osx': 'darwin',
-                    'aarch_64': 'aarch64',
-                    'windows_x86_64': 'windows_x64'
-            ]
-        }
-
-        distributions {
-            smithy {
-                distributionType = 'JLINK'
-                stereotype = 'CLI'
-
-                artifact {
-                    path = "build/image/smithy-cli-linux-x86_64.zip"
-                    platform = "linux-x86_64"
-                }
-
-                artifact {
-                    path = "build/image/smithy-cli-linux-aarch64.zip"
-                    platform = "linux-aarch_64"
-                }
-
-                artifact {
-                    path = "build/image/smithy-cli-darwin-x86_64.zip"
-                    platform = "osx-x86_64"
-                }
-
-                artifact {
-                    path = "build/image/smithy-cli-darwin-aarch64.zip"
-                    platform = "osx-aarch_64"
-                }
-
-                artifact {
-                    path = "build/image/smithy-cli-windows-x64.zip"
-                    platform = "windows-x86_64"
-                }
-            }
-        }
-
-        signing {
-            active = "RELEASE"
-            armored = true
-            verify = true
-        }
-    }
-}
diff --git a/smithy-cli/build.gradle.kts b/smithy-cli/build.gradle.kts
new file mode 100644
index 000000000..263c86398
--- /dev/null
+++ b/smithy-cli/build.gradle.kts
@@ -0,0 +1,368 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import org.apache.tools.ant.taskdefs.condition.Os
+import org.beryx.runtime.RuntimeTask
+import org.gradle.api.tasks.testing.logging.TestExceptionFormat
+import org.gradle.api.tasks.testing.logging.TestLogEvent
+import org.jreleaser.gradle.plugin.JReleaserExtension
+import org.jreleaser.model.Active
+import org.jreleaser.model.Distribution
+import org.jreleaser.model.Stereotype
+
+plugins {
+    application
+    alias(libs.plugins.jreleaser) apply false
+    alias(libs.plugins.runtime)
+    alias(libs.plugins.shadow)
+    id("smithy.module-conventions")
+    id("smithy.integ-test-conventions")
+}
+
+description = "This module implements the Smithy command line interface."
+
+extra["displayName"] = "Smithy :: CLI"
+extra["moduleName"] = "software.amazon.smithy.cli"
+
+val imageJreVersion = "17"
+val correttoRoot = "https://corretto.aws/downloads/latest/amazon-corretto-$imageJreVersion"
+
+dependencies {
+    // Keeps these as exported transitive dependencies.
+    implementation(project(":smithy-model"))
+    implementation(project(":smithy-build"))
+    implementation(project(":smithy-diff"))
+    implementation(project(":smithy-syntax", "shadow"))
+
+    // This is needed to ensure the above dependencies are added to the runtime image.
+    shadow(project(":smithy-model"))
+    shadow(project(":smithy-build"))
+    shadow(project(":smithy-diff"))
+    shadow(project(":smithy-syntax"))
+
+    // These maven resolver dependencies are shaded into the smithy-cli JAR.
+    implementation(libs.maven.resolver.provider)
+    implementation(libs.maven.resolver.api)
+    implementation(libs.maven.resolver.spi)
+    implementation(libs.maven.resolver.util)
+    implementation(libs.maven.resolver.impl)
+    implementation(libs.maven.resolver.connector.basic)
+    implementation(libs.maven.resolver.transport.file)
+    implementation(libs.maven.resolver.transport.http)
+    implementation(libs.slf4j.jul) // Route slf4j used by Maven through JUL like the rest of Smithy.
+
+    testImplementation(libs.mockserver)
+}
+
+// ------ Setup CLI binary -------
+// This setting is needed by the Shadow plugin for some reason to define a main application class.
+val mainClassName = "software.amazon.smithy.cli.SmithyCli"
+application {
+    mainClass = mainClassName
+    applicationName = "smithy"
+}
+
+// Detect which OS and arch is running to create an application class data sharing
+// archive for the current platform. This is not how we'll ultimately build and release images.
+var imageOs = ""
+if (Os.isFamily(Os.FAMILY_WINDOWS)) {
+    imageOs = "windows-x64"
+} else if (Os.isFamily(Os.FAMILY_MAC)) {
+    if (Os.isArch("aarch64")) {
+        imageOs = "darwin-aarch64"
+    } else if (Os.isArch("x86_64") || Os.isArch("amd64")) {
+        imageOs = "darwin-x86_64"
+    } else {
+        println("No JDK for ${System.getProperty("os.arch")}")
+    }
+} else if (Os.isFamily(Os.FAMILY_UNIX)) {
+    if (Os.isArch("aarch")) {
+        imageOs = "linux-aarch64"
+    } else if (Os.isArch("x86_64") || Os.isArch("amd64")) {
+        imageOs = "linux-x86_64"
+    } else {
+        println("No JDK for ${System.getProperty("os.arch")}")
+    }
+} else {
+    println("Unknown OS and arch: ${System.getProperty("os.name")}")
+}
+
+// This is needed in order for integration tests to find the CLI runtime image
+val smithyBinary = if (Os.isFamily(Os.FAMILY_WINDOWS)) {
+    layout.buildDirectory.file("image/smithy-cli-$imageOs/bin/smithy.bat").get().asFile.path
+} else {
+    layout.buildDirectory.file("image/smithy-cli-$imageOs/bin/smithy").get().asFile.path
+}
+System.setProperty("SMITHY_BINARY", smithyBinary)
+
+runtime {
+    addOptions("--compress", "2", "--strip-debug", "--no-header-files", "--no-man-pages")
+    addModules("java.logging", "java.xml", "java.naming", "jdk.crypto.ec")
+
+    launcher {
+        // This script is a combination of the default startup script used by the badass runtime
+        // plugin, and the upstream source it's based on:
+        // https://raw.githubusercontent.com/gradle/gradle/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+        // Using the Gradle wrapper script as-is results in a huge startup penalty, so I instead updated parts of the
+        // script that didn't affect performance, and kept others that did. Namely, the set and eval code of the Gradle
+        // startup script is significantly slower than what was used by the plugin.
+        unixScriptTemplate = file("configuration/unixStartScript.txt")
+        jvmArgs =
+            listOf(
+                // Disable this when attempting to profile the CLI. In 99% of use cases this isn't not necessary.
+                "-XX:-UsePerfData",
+                "-Xshare:auto",
+                "-XX:SharedArchiveFile={{BIN_DIR}}/../lib/smithy.jsa",
+            )
+    }
+
+    targetPlatform("linux-x86_64") {
+        setJdkHome(jdkDownload("$correttoRoot-x64-linux-jdk.tar.gz"))
+    }
+
+    targetPlatform("linux-aarch64") {
+        setJdkHome(jdkDownload("$correttoRoot-aarch64-linux-jdk.tar.gz"))
+    }
+
+    targetPlatform("darwin-x86_64") {
+        setJdkHome(jdkDownload("$correttoRoot-x64-macos-jdk.tar.gz"))
+    }
+
+    targetPlatform("darwin-aarch64") {
+        setJdkHome(jdkDownload("$correttoRoot-aarch64-macos-jdk.tar.gz"))
+    }
+
+    targetPlatform("windows-x64") {
+        setJdkHome(jdkDownload("$correttoRoot-x64-windows-jdk.zip"))
+    }
+
+    // Because we're using target-platforms, it will use this property as a prefix for each target zip
+    imageZip = layout.buildDirectory.file("image/smithy-cli.zip")
+
+    // This is needed to ensure that jlink is available (jlink is Java 9+), we should use the JDK that
+    // is configured for the runtime
+    // NOTE: For the runtime task, you *must* have the right JDK set up in your environment (17 at the time of writing)
+    javaHome = javaToolchains.launcherFor {
+        languageVersion.set(JavaLanguageVersion.of(imageJreVersion))
+    }.map { it.metadata.installationPath.asFile.path }
+}
+
+tasks {
+
+    val cliVersion by registering(Copy::class) {
+        from(configurations.runtimeClasspath).include("*-all.jar")
+        into("src/main/resources")
+    }
+
+    register("generateVersionFile") {
+        val versionFile = sourceSets.main.map { sourceSet ->
+            sourceSet.output.resourcesDir?.resolve("software/amazon/smithy/cli/cli-version")
+                ?: throw GradleException("Resources directory not found for main sourceSet")
+        }
+
+        outputs.file(versionFile)
+
+        doLast {
+            versionFile.get().writeText(project.version.toString())
+        }
+    }
+
+    processResources {
+        dependsOn("generateVersionFile")
+    }
+
+    shadowJar {
+        // Replace the normal JAR with the shaded JAR. We don't want to publish a JAR that isn't shaded.
+        archiveClassifier.set("")
+
+        mergeServiceFiles()
+
+        // Shade dependencies to prevent conflicts with other dependencies.
+        relocate("org.slf4j", "software.amazon.smithy.cli.shaded.slf4j")
+        relocate("org.eclipse", "software.amazon.smithy.cli.shaded.eclipse")
+        relocate("org.apache", "software.amazon.smithy.cli.shaded.apache")
+        relocate("org.sonatype", "software.amazon.smithy.cli.shaded.sonatype")
+        relocate("org.codehaus", "software.amazon.smithy.cli.shaded.codehaus")
+
+        // If other javax packages are ever pulled in, we'll need to update this list. This is more deliberate about
+        // what's shaded to ensure that things like javax.net.ssl.SSLSocketFactory are not inadvertently shaded.
+        relocate("javax.annotation", "software.amazon.smithy.cli.shaded.javax.annotation")
+        relocate("javax.inject", "software.amazon.smithy.cli.shaded.javax.inject")
+
+        // Don't shade Smithy dependencies into the CLI. These are normal dependencies that we want our consumers
+        // to resolve.
+        dependencies {
+            exclude(project(":smithy-utils"))
+            exclude(project(":smithy-model"))
+            exclude(project(":smithy-build"))
+            exclude(project(":smithy-diff"))
+            exclude(project(":smithy-syntax"))
+        }
+    }
+
+    val runtime by getting(RuntimeTask::class) {
+        dependsOn(shadowJar)
+
+        // Add finishing touches to the distributables, such as an install script, before it gets zipped
+        doLast {
+            targetPlatforms.get().forEach { targetPlatform ->
+                copy {
+                    from("configuration")
+                    include(if (targetPlatform.value.name.contains("windows")) "install.bat" else "install")
+                    into(layout.buildDirectory.dir("image/smithy-cli-${targetPlatform.value.name}"))
+                }
+            }
+        }
+    }
+
+    // Run the warmup command after the image is generated
+    val optimize by registering(Exec::class) {
+        commandLine(smithyBinary, "warmup")
+        dependsOn(runtime)
+    }
+
+    // Always shadow the JAR and replace the JAR by the shadowed JAR.
+    jar {
+        finalizedBy(shadowJar)
+    }
+
+    // Prevent https://docs.gradle.org/7.3.3/userguide/validation_problems.html#implicit_dependency issues between
+    // the runtime image and shadowJar tasks.
+    distZip {
+        dependsOn(shadowJar)
+    }
+
+    distTar {
+        dependsOn(shadowJar)
+    }
+
+    startScripts {
+        dependsOn(shadowJar)
+    }
+
+    // ------ Setup integration testing -------
+    integ {
+        systemProperty("SMITHY_BINARY", smithyBinary)
+        // Configuration parameters to execute top-level classes in parallel but methods in same thread
+        systemProperties["junit.jupiter.execution.parallel.enabled"] = "true"
+        systemProperties["junit.jupiter.execution.parallel.mode.default"] = "same_thread"
+        systemProperties["junit.jupiter.execution.parallel.mode.classes.default"] = "concurrent"
+
+        testLogging {
+            events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
+            exceptionFormat = TestExceptionFormat.FULL
+        }
+
+        // Runtime images need to be created before integration tests can run.
+        dependsOn(runtime)
+    }
+}
+
+// ------ Setup Jreleaser -------
+if (project.hasProperty("release.cli")) {
+    apply(plugin = "org.jreleaser")
+
+    tasks.named("assembleDist") {
+        doFirst {
+            // This is a workaround for a weird behavior.
+            // https://github.com/jreleaser/jreleaser/issues/1292
+            mkdir("$buildDir/jreleaser")
+        }
+        dependsOn("runtimeZip")
+    }
+
+    configure<JReleaserExtension> {
+        gitRootSearch = true
+        dryrun = false
+
+        project {
+            website = "https://smithy.io"
+            authors = listOf("Smithy")
+            vendor = "Smithy"
+            license = "Apache-2.0"
+            description = "Smithy CLI - A CLI for building, validating, querying, and iterating on Smithy models"
+            copyright = "2019"
+        }
+
+        checksum {
+            individual = true
+            files = false
+        }
+
+        release {
+            github {
+                overwrite = true
+                tagName = "{{projectVersion}}"
+                skipTag = true
+                releaseName = "Smithy CLI v{{{projectVersion}}}"
+                changelog {
+                    // For now, we won't have a changelog added to the release. In the future, we could create a changelog-snippet
+                    // from the real changelog as part of a command hook prior to the release step
+                    enabled = false
+                }
+                commitAuthor {
+                    name = "smithy-automation"
+                    email = "github-smithy-automation@amazon.com"
+                }
+            }
+        }
+
+        files {
+            active = Active.ALWAYS
+            artifact {
+                // We'll include the VERSION file in the release artifacts so that the version can be easily
+                // retrieving by hitting the GitHub `releases/latest` url
+                setPath("../VERSION")
+                extraProperties.put("skipSigning", true)
+            }
+        }
+
+        platform {
+            // These replacements are for the names of files that are released, *not* for names within this build config
+            replacements = mapOf(
+                "osx" to "darwin",
+                "aarch_64" to "aarch64",
+                "windows_x86_64" to "windows_x64"
+            )
+        }
+
+        distributions {
+            create("smithy") {
+                distributionType = Distribution.DistributionType.JLINK
+                stereotype = Stereotype.CLI
+
+                artifact {
+                    path = layout.buildDirectory.file("image/smithy-cli-linux-x86_64.zip")
+                    platform = ("linux-x86_64")
+                }
+
+                artifact {
+                    path = layout.buildDirectory.file("image/smithy-cli-linux-aarch64.zip")
+                    platform = ("linux-aarch_64")
+                }
+
+                artifact {
+                    path = layout.buildDirectory.file("image/smithy-cli-darwin-x86_64.zip")
+                    platform = ("osx-x86_64")
+                }
+
+                artifact {
+                    path = layout.buildDirectory.file("image/smithy-cli-darwin-aarch64.zip")
+                    platform = ("osx-aarch_64")
+                }
+
+                artifact {
+                    path = layout.buildDirectory.file("image/smithy-cli-windows-x64.zip")
+                    platform = ("windows-x86_64")
+                }
+            }
+        }
+
+        signing {
+            active = Active.RELEASE
+            armored = true
+            verify = true
+        }
+    }
+}
diff --git a/smithy-codegen-core/build.gradle.kts b/smithy-codegen-core/build.gradle.kts
index be76cb102..a8bd35c07 100644
--- a/smithy-codegen-core/build.gradle.kts
+++ b/smithy-codegen-core/build.gradle.kts
@@ -1,20 +1,13 @@
 /*
- * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "This module provides a code generation framework for generating clients, " +
-        "servers, documentation, and other artifacts for various languages from Smithy models."
+    "servers, documentation, and other artifacts for various languages from Smithy models."
 
 extra["displayName"] = "Smithy :: Code Generation Framework"
 extra["moduleName"] = "software.amazon.smithy.codegen.core"
diff --git a/smithy-diff/build.gradle.kts b/smithy-diff/build.gradle.kts
index 2aa8d3a00..7953a7361 100644
--- a/smithy-diff/build.gradle.kts
+++ b/smithy-diff/build.gradle.kts
@@ -1,20 +1,13 @@
 /*
- * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "This module detects differences between two Smithy models, identifying " +
-        "changes that are safe and changes that are backward incompatible."
+    "changes that are safe and changes that are backward incompatible."
 
 extra["displayName"] = "Smithy :: Diff"
 extra["moduleName"] = "software.amazon.smithy.diff"
diff --git a/smithy-jmespath/build.gradle.kts b/smithy-jmespath/build.gradle.kts
index 58c218a15..9c810909e 100644
--- a/smithy-jmespath/build.gradle.kts
+++ b/smithy-jmespath/build.gradle.kts
@@ -1,17 +1,10 @@
 /*
- * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "A standalone JMESPath parser"
 
diff --git a/smithy-jsonschema/build.gradle.kts b/smithy-jsonschema/build.gradle.kts
index 9f53f128a..b46cd80e3 100644
--- a/smithy-jsonschema/build.gradle.kts
+++ b/smithy-jsonschema/build.gradle.kts
@@ -1,17 +1,10 @@
 /*
- * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "This module contains support for converting a Smithy model to JSON Schema."
 
diff --git a/smithy-linters/build.gradle.kts b/smithy-linters/build.gradle.kts
index 767e5e5b1..95a9acc73 100644
--- a/smithy-linters/build.gradle.kts
+++ b/smithy-linters/build.gradle.kts
@@ -1,20 +1,13 @@
 /*
- * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "This module provides support for customizable linters declared in the " +
-        "metadata section of a Smithy model."
+    "metadata section of a Smithy model."
 
 extra["displayName"] = "Smithy :: Linters"
 extra["moduleName"] = "software.amazon.smithy.linters"
diff --git a/smithy-linters/src/main/java/software/amazon/smithy/linters/ShouldHaveUsedTimestampValidator.java b/smithy-linters/src/main/java/software/amazon/smithy/linters/ShouldHaveUsedTimestampValidator.java
index c8062aad0..13d23c2f8 100644
--- a/smithy-linters/src/main/java/software/amazon/smithy/linters/ShouldHaveUsedTimestampValidator.java
+++ b/smithy-linters/src/main/java/software/amazon/smithy/linters/ShouldHaveUsedTimestampValidator.java
@@ -24,6 +24,7 @@ import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import software.amazon.smithy.model.Model;
 import software.amazon.smithy.model.node.NodeMapper;
+import software.amazon.smithy.model.shapes.EnumShape;
 import software.amazon.smithy.model.shapes.MemberShape;
 import software.amazon.smithy.model.shapes.NumberShape;
 import software.amazon.smithy.model.shapes.Shape;
@@ -112,7 +113,7 @@ public final class ShouldHaveUsedTimestampValidator extends AbstractValidator {
             @Override
             protected List<ValidationEvent> getDefault(Shape shape) {
                 if (shape.isStringShape() || shape instanceof NumberShape) {
-                    return validateSimpleShape(shape, patterns);
+                    return validateSimpleShape(shape);
                 } else {
                     return Collections.emptyList();
                 }
@@ -120,12 +121,17 @@ public final class ShouldHaveUsedTimestampValidator extends AbstractValidator {
 
             @Override
             public List<ValidationEvent> structureShape(StructureShape shape) {
-                return validateStructure(shape, model, patterns);
+                return validateStructure(shape, model);
             }
 
             @Override
             public List<ValidationEvent> unionShape(UnionShape shape) {
-                return validateUnion(shape, model, patterns);
+                return validateUnion(shape, model);
+            }
+
+            @Override
+            public List<ValidationEvent> enumShape(EnumShape shape) {
+                return Collections.emptyList();
             }
         };
 
@@ -134,57 +140,63 @@ public final class ShouldHaveUsedTimestampValidator extends AbstractValidator {
 
     private List<ValidationEvent> validateStructure(
             StructureShape structure,
-            Model model,
-            List<Pattern> patterns
+            Model model
     ) {
         return structure
                 .getAllMembers()
                 .entrySet()
                 .stream()
-                .flatMap(entry -> validateTargetShape(entry.getKey(), entry.getValue(), model, patterns))
+                .flatMap(entry -> validateTargetShape(entry.getKey(), entry.getValue(), model))
                 .collect(Collectors.toList());
     }
 
     private List<ValidationEvent> validateUnion(
             UnionShape union,
-            Model model,
-            List<Pattern> patterns
+            Model model
     ) {
         return union
                 .getAllMembers()
                 .entrySet()
                 .stream()
-                .flatMap(entry -> validateTargetShape(entry.getKey(), entry.getValue(), model, patterns))
+                .flatMap(entry -> validateTargetShape(entry.getKey(), entry.getValue(), model))
                 .collect(Collectors.toList());
     }
 
     private Stream<ValidationEvent> validateTargetShape(
             String name,
-            MemberShape target,
-            Model model,
-            List<Pattern> patterns
+            MemberShape memberShape,
+            Model model
     ) {
-        return OptionalUtils.stream(model.getShape(target.getTarget())
-                .flatMap(shape -> validateName(name, shape.getType(), target, patterns)));
+        return OptionalUtils.stream(model.getShape(memberShape.getTarget())
+                .flatMap(targetShape -> validateName(name, targetShape, memberShape, patterns, model)));
     }
 
     private List<ValidationEvent> validateSimpleShape(
-            Shape shape,
-            List<Pattern> patterns
+            Shape shape
     ) {
-        return validateName(shape.getId().getName(), shape.getType(), shape, patterns)
-                .map(ListUtils::of)
-                .orElse(ListUtils.of());
+        String name = shape.getId().getName();
+
+        return patterns
+                .stream()
+                .filter(pattern -> pattern.matcher(name).matches())
+                .map(matcher -> buildEvent(shape, name, shape.getType()))
+                .collect(Collectors.toList());
     }
 
     private Optional<ValidationEvent> validateName(
             String name,
-            ShapeType type,
+            Shape targetShape,
             Shape context,
-            List<Pattern> patterns
+            List<Pattern> patterns,
+            Model model
     ) {
-        if (type == ShapeType.TIMESTAMP) {
+        ShapeType type = targetShape.getType();
+        if (type == ShapeType.TIMESTAMP || type == ShapeType.ENUM) {
             return Optional.empty();
+        } else if (type == ShapeType.STRUCTURE || type == ShapeType.LIST) {
+            if (this.onlyContainsTimestamps(targetShape, model)) {
+                return Optional.empty();
+            }
         }
         return patterns
                 .stream()
@@ -193,6 +205,16 @@ public final class ShouldHaveUsedTimestampValidator extends AbstractValidator {
                 .findAny();
     }
 
+    private boolean onlyContainsTimestamps(Shape shape, Model model) {
+        return shape.getAllMembers()
+                .values()
+                .stream()
+                .map(memberShape -> model.getShape(memberShape.getTarget()))
+                .filter(Optional::isPresent)
+                .map(Optional::get)
+                .allMatch(Shape::isTimestampShape);
+    }
+
     private ValidationEvent buildEvent(Shape context, String name, ShapeType type) {
         return danger(context, context.isMemberShape()
                 ? String.format("Member `%s` is named like a timestamp but references a `%s` shape", name, type)
diff --git a/smithy-model/build.gradle.kts b/smithy-model/build.gradle.kts
index 36950e4f9..a4196f76e 100644
--- a/smithy-model/build.gradle.kts
+++ b/smithy-model/build.gradle.kts
@@ -1,39 +1,19 @@
 /*
- * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+    id("smithy.profiling-conventions")
+}
 
 description = "This module provides the core implementation of loading, validating, " +
-        "traversing, mutating, and serializing a Smithy model."
+    "traversing, mutating, and serializing a Smithy model."
 
 extra["displayName"] = "Smithy :: Model"
 extra["moduleName"] = "software.amazon.smithy.model"
 
-plugins {
-    id("me.champeau.jmh")
-}
-
 dependencies {
     api(project(":smithy-utils"))
     jmh(project(":smithy-utils"))
 }
-
-jmh {
-    timeUnit = "us"
-}
-
-tasks {
-    processJmhResources {
-        duplicatesStrategy = DuplicatesStrategy.WARN
-    }
-}
\ No newline at end of file
diff --git a/smithy-mqtt-traits/build.gradle.kts b/smithy-mqtt-traits/build.gradle.kts
index 8ffe3cede..75164aff9 100644
--- a/smithy-mqtt-traits/build.gradle.kts
+++ b/smithy-mqtt-traits/build.gradle.kts
@@ -1,17 +1,10 @@
 /*
- * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "This module provides the implementation of MQTT binding traits for Smithy."
 
diff --git a/smithy-openapi-traits/build.gradle.kts b/smithy-openapi-traits/build.gradle.kts
index 2f480a508..29b082423 100644
--- a/smithy-openapi-traits/build.gradle.kts
+++ b/smithy-openapi-traits/build.gradle.kts
@@ -1,7 +1,10 @@
 /*
- * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
  * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "This module provides Smithy traits that are used in converting a Smithy model to OpenAPI."
 
diff --git a/smithy-openapi/build.gradle.kts b/smithy-openapi/build.gradle.kts
index 8bee24615..0b7817059 100644
--- a/smithy-openapi/build.gradle.kts
+++ b/smithy-openapi/build.gradle.kts
@@ -1,17 +1,10 @@
 /*
- * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "This module contains support for converting a Smithy model to OpenAPI."
 
diff --git a/smithy-protocol-traits/build.gradle.kts b/smithy-protocol-traits/build.gradle.kts
index 6010827ff..dc6d2e3bf 100644
--- a/smithy-protocol-traits/build.gradle.kts
+++ b/smithy-protocol-traits/build.gradle.kts
@@ -2,6 +2,9 @@
  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
  * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "This module provides the implementation of protocol traits for Smithy."
 
diff --git a/smithy-rules-engine/build.gradle.kts b/smithy-rules-engine/build.gradle.kts
index 509386798..8bf1bdd24 100644
--- a/smithy-rules-engine/build.gradle.kts
+++ b/smithy-rules-engine/build.gradle.kts
@@ -2,6 +2,9 @@
  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
  * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "Smithy rules engine Language and traits"
 
diff --git a/smithy-syntax/build.gradle.kts b/smithy-syntax/build.gradle.kts
index e68282759..3cea4bd23 100644
--- a/smithy-syntax/build.gradle.kts
+++ b/smithy-syntax/build.gradle.kts
@@ -1,17 +1,11 @@
 /*
  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    alias(libs.plugins.shadow)
+    id("smithy.module-conventions")
+}
 
 description = "Provides a parse tree and formatter for Smithy models."
 
@@ -21,7 +15,7 @@ extra["moduleName"] = "software.amazon.smithy.syntax"
 dependencies {
     api(project(":smithy-utils"))
     api(project(":smithy-model"))
-    implementation("com.opencastsoftware:prettier4j:0.1.1")
+    implementation(libs.prettier4j)
 
     // This is needed to export these as dependencies since we aren't shading them.
     shadow(project(":smithy-model"))
@@ -49,4 +43,4 @@ tasks {
     jar {
         finalizedBy(shadowJar)
     }
-}
\ No newline at end of file
+}
diff --git a/smithy-trait-codegen/build.gradle.kts b/smithy-trait-codegen/build.gradle.kts
index 07719f8f6..21ab5b9be 100644
--- a/smithy-trait-codegen/build.gradle.kts
+++ b/smithy-trait-codegen/build.gradle.kts
@@ -2,6 +2,10 @@
  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
  * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+    id("smithy.integ-test-conventions")
+}
 
 description = "Plugin for Generating Trait Code from Smithy Models"
 
@@ -14,25 +18,15 @@ dependencies {
 
 // Set up Integration testing source sets
 sourceSets {
-    create("it") {
-        val main by getting
-        val test by getting
-
-        compileClasspath += main.output +
-                configurations["testRuntimeClasspath"] +
-                configurations["testCompileClasspath"]
-
-        runtimeClasspath += output +
-                compileClasspath +
-                test.runtimeClasspath +
-                test.output
-
+    named("it") {
+        // Set up the generated integ source
         java {
-            srcDir("${layout.buildDirectory.get()}/integ/")
+            srcDir(layout.buildDirectory.dir("integ"))
         }
 
+        // Set up the generated integ resources
         resources {
-            srcDirs(layout.buildDirectory.dir("generated-resources").get())
+            srcDir(layout.buildDirectory.dir("generated-resources"))
         }
     }
 }
@@ -47,21 +41,12 @@ tasks.register<JavaExec>("generateTraits") {
 
 // Copy generated META-INF files to a new generated-resources directory to
 // make it easy to include as resource srcDir
-val generatedMetaInf = File("$buildDir/integ/META-INF")
-val destResourceDir = File("$buildDir/generated-resources/META-INF")
 tasks.register<Copy>("copyGeneratedSrcs") {
-    from(generatedMetaInf)
-    into(destResourceDir)
+    from(layout.buildDirectory.dir("integ/META-INF"))
+    into(layout.buildDirectory.dir("generated-resources/META-INF"))
     dependsOn("generateTraits")
 }
 
-// Add the integ test task
-tasks.register<Test>("integ") {
-    useJUnitPlatform()
-    testClassesDirs = sourceSets["it"].output.classesDirs
-    classpath = sourceSets["it"].runtimeClasspath
-}
-
 tasks {
     named("checkstyleIt") {
         enabled = false
@@ -86,4 +71,4 @@ tasks {
     named("spotbugsIt") {
         enabled = false
     }
-}
\ No newline at end of file
+}
diff --git a/smithy-utils/build.gradle.kts b/smithy-utils/build.gradle.kts
index 7699b76ca..cab2beb98 100644
--- a/smithy-utils/build.gradle.kts
+++ b/smithy-utils/build.gradle.kts
@@ -1,20 +1,12 @@
 /*
- * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "This module contains utility classes and interfaces for Smithy."
 
 extra["displayName"] = "Smithy :: Utilities"
 extra["moduleName"] = "software.amazon.smithy.utils"
-
diff --git a/smithy-validation-model/build.gradle.kts b/smithy-validation-model/build.gradle.kts
index d2ac2b936..eb28d1e97 100644
--- a/smithy-validation-model/build.gradle.kts
+++ b/smithy-validation-model/build.gradle.kts
@@ -1,20 +1,10 @@
 /*
- * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
-
 plugins {
-    id("software.amazon.smithy.gradle.smithy-jar")
+    id("smithy.module-conventions")
+    alias(libs.plugins.smithy.jar)
 }
 
 description = "This module provides support for validation in Smithy server SDKs"
@@ -34,4 +24,4 @@ tasks {
 
 smithy {
     smithyBuildConfigs.set(project.files())
-}
\ No newline at end of file
+}
diff --git a/smithy-waiters/build.gradle.kts b/smithy-waiters/build.gradle.kts
index fed5565e2..4d5700c45 100644
--- a/smithy-waiters/build.gradle.kts
+++ b/smithy-waiters/build.gradle.kts
@@ -1,17 +1,10 @@
 /*
- * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "Defines Smithy waiters."
 
@@ -21,4 +14,4 @@ extra["moduleName"] = "software.amazon.smithy.waiters"
 dependencies {
     api(project(":smithy-model"))
     api(project(":smithy-jmespath"))
-}
\ No newline at end of file
+}


Test patch:
diff --git a/buildSrc/src/main/kotlin/smithy.integ-test-conventions.gradle.kts b/buildSrc/src/main/kotlin/smithy.integ-test-conventions.gradle.kts
new file mode 100644
index 000000000..13cd02762
--- /dev/null
+++ b/buildSrc/src/main/kotlin/smithy.integ-test-conventions.gradle.kts
@@ -0,0 +1,27 @@
+import org.gradle.api.tasks.testing.logging.TestExceptionFormat
+
+configure<SourceSetContainer> {
+    val main by getting
+    val test by getting
+    create("it") {
+        compileClasspath += main.output + configurations["testRuntimeClasspath"] + configurations["testCompileClasspath"]
+        runtimeClasspath += output + compileClasspath + test.runtimeClasspath + test.output
+    }
+}
+
+// Add the integ test task
+tasks {
+    register<Test>("integ") {
+        useJUnitPlatform()
+        testClassesDirs = project.the<SourceSetContainer>()["it"].output.classesDirs
+        classpath = project.the<SourceSetContainer>()["it"].runtimeClasspath
+
+        // only log verbosely if the property is set like we do for unit-tests
+        if (project.hasProperty("log-tests")) {
+            testLogging {
+                events("passed", "skipped", "failed")
+                exceptionFormat = TestExceptionFormat.FULL
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/smithy-aws-protocol-tests/build.gradle.kts b/smithy-aws-protocol-tests/build.gradle.kts
index a03b7aa9b..67df3e93f 100644
--- a/smithy-aws-protocol-tests/build.gradle.kts
+++ b/smithy-aws-protocol-tests/build.gradle.kts
@@ -1,20 +1,10 @@
 /*
- * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
-
 plugins {
-    id("software.amazon.smithy.gradle.smithy-jar")
+    alias(libs.plugins.smithy.jar)
+    id("smithy.module-conventions")
 }
 
 description = "Defines protocol tests for AWS HTTP protocols."
diff --git a/smithy-aws-smoke-test-model/build.gradle.kts b/smithy-aws-smoke-test-model/build.gradle.kts
index 4d59232d7..998299445 100644
--- a/smithy-aws-smoke-test-model/build.gradle.kts
+++ b/smithy-aws-smoke-test-model/build.gradle.kts
@@ -2,6 +2,9 @@
  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
  * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "Defines shapes used by AWS for modeling smoke tests"
 
diff --git a/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/should-have-used-timestamp-ignore.json b/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/should-have-used-timestamp-ignore.json
index 962f9ea81..32605bd94 100644
--- a/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/should-have-used-timestamp-ignore.json
+++ b/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/should-have-used-timestamp-ignore.json
@@ -24,6 +24,48 @@
         },
         "example.namespace#CreatedOn": {
             "type": "timestamp"
+        },
+        "example.namespace#DateEnum": {
+            "type": "enum",
+            "members": {
+                "ANOMALOUS": {
+                    "target": "smithy.api#Unit",
+                    "traits": {
+                        "smithy.api#enumValue": "anomalous"
+                    }
+                },
+                "NORMAL": {
+                    "target": "smithy.api#Unit",
+                    "traits": {
+                        "smithy.api#enumValue": "normal"
+                    }
+                }
+            }
+        },
+        "example.namespace#DateStructureOuter": {
+            "type": "structure",
+            "members": {
+                "TimestampStructure": {
+                    "target": "example.namespace#DateStructure"
+                },
+                "ListOfTimestamp": {
+                    "target": "example.namespace#DateList"
+                }
+            }
+        },
+        "example.namespace#DateStructure": {
+            "type": "structure",
+            "members": {
+                "TimeOn": {
+                    "target": "example.namespace#CreatedOn"
+                }
+            }
+        },
+        "example.namespace#DateList": {
+            "type": "list",
+            "member": {
+                "target": "example.namespace#CreatedOn"
+            }
         }
     },
     "metadata": {
diff --git a/smithy-protocol-test-traits/build.gradle.kts b/smithy-protocol-test-traits/build.gradle.kts
index 1e1c1f4a5..c8a0e4e15 100644
--- a/smithy-protocol-test-traits/build.gradle.kts
+++ b/smithy-protocol-test-traits/build.gradle.kts
@@ -1,17 +1,10 @@
 /*
- * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "Defines protocol test traits."
 
diff --git a/smithy-protocol-tests/build.gradle.kts b/smithy-protocol-tests/build.gradle.kts
index 5912f06aa..744bde00d 100644
--- a/smithy-protocol-tests/build.gradle.kts
+++ b/smithy-protocol-tests/build.gradle.kts
@@ -2,9 +2,9 @@
  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
  * SPDX-License-Identifier: Apache-2.0
  */
-
 plugins {
-    id("software.amazon.smithy.gradle.smithy-jar")
+    id("smithy.module-conventions")
+    alias(libs.plugins.smithy.jar)
 }
 
 description = "Defines protocol tests for Smithy HTTP protocols."
@@ -13,13 +13,13 @@ extra["displayName"] = "Smithy :: Protocol Tests"
 extra["moduleName"] = "software.amazon.smithy.protocoltests"
 
 dependencies {
-    implementation(project(path= ":smithy-cli", configuration= "shadow"))
+    implementation(project(path = ":smithy-cli", configuration = "shadow"))
     implementation(project(":smithy-protocol-test-traits"))
     implementation(project(":smithy-protocol-traits"))
     api(project(":smithy-validation-model"))
 }
 
-tasks{
+tasks {
     sourcesJar {
         dependsOn("smithyJarStaging")
     }
diff --git a/smithy-smoke-test-traits/build.gradle.kts b/smithy-smoke-test-traits/build.gradle.kts
index 7254c3353..71051cfb5 100644
--- a/smithy-smoke-test-traits/build.gradle.kts
+++ b/smithy-smoke-test-traits/build.gradle.kts
@@ -1,17 +1,10 @@
 /*
  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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.
+ * SPDX-License-Identifier: Apache-2.0
  */
+plugins {
+    id("smithy.module-conventions")
+}
 
 description = "Defines smoke test traits."