diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 8dc98243..234bf631 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,4 +1,5 @@ name: Pull Request Check + on: pull_request: branches: @@ -16,6 +17,12 @@ on: - gradle.properties - settings.gradle +permissions: + contents: read + actions: read + packages: read + pull-requests: write + jobs: codec: uses: ./.github/workflows/build_and_test.yml @@ -184,3 +191,227 @@ jobs: ci_build: true pr_build: true gametest: true + + # ── Roseau API checks ────────────────────────────────────────── + + codec-roseau: + needs: codec + uses: ./.github/workflows/roseau_check.yml + with: + module: codec + module_id: anvillib-codec + + collision-roseau: + needs: collision + uses: ./.github/workflows/roseau_check.yml + with: + module: collision + module_id: anvillib-collision + + config-roseau: + needs: config + uses: ./.github/workflows/roseau_check.yml + with: + module: config + module_id: anvillib-config + + font-roseau: + needs: font + uses: ./.github/workflows/roseau_check.yml + with: + module: font + module_id: anvillib-font + + integration-roseau: + needs: integration + uses: ./.github/workflows/roseau_check.yml + with: + module: integration + module_id: anvillib-integration + + moveable-entity-block-roseau: + needs: moveable-entity-block + uses: ./.github/workflows/roseau_check.yml + with: + module: moveable-entity-block + module_id: anvillib-moveable-entity-block + + multiblock-roseau: + needs: multiblock + uses: ./.github/workflows/roseau_check.yml + with: + module: multiblock + module_id: anvillib-multiblock + + network-roseau: + needs: network + uses: ./.github/workflows/roseau_check.yml + with: + module: network + module_id: anvillib-network + + recipe-roseau: + needs: recipe + uses: ./.github/workflows/roseau_check.yml + with: + module: recipe + module_id: anvillib-recipe + + registrum-roseau: + needs: registrum + uses: ./.github/workflows/roseau_check.yml + with: + module: registrum + module_id: anvillib-registrum + + rendering-roseau: + needs: rendering + uses: ./.github/workflows/roseau_check.yml + with: + module: rendering + module_id: anvillib-rendering + + space-select-roseau: + needs: space-select + uses: ./.github/workflows/roseau_check.yml + with: + module: space-select + module_id: anvillib-space-select + + sync-roseau: + needs: sync + uses: ./.github/workflows/roseau_check.yml + with: + module: sync + module_id: anvillib-sync + + util-roseau: + needs: util + uses: ./.github/workflows/roseau_check.yml + with: + module: util + module_id: anvillib-util + + wheel-roseau: + needs: wheel + uses: ./.github/workflows/roseau_check.yml + with: + module: wheel + module_id: anvillib-wheel + + main-roseau: + needs: main + uses: ./.github/workflows/roseau_check.yml + with: + module: main + module_id: anvillib + + # ── Roseau summary comment ───────────────────────────────────── + + roseau-summary: + needs: + - codec-roseau + - collision-roseau + - config-roseau + - font-roseau + - integration-roseau + - moveable-entity-block-roseau + - multiblock-roseau + - network-roseau + - recipe-roseau + - registrum-roseau + - rendering-roseau + - space-select-roseau + - sync-roseau + - util-roseau + - wheel-roseau + - main-roseau + if: always() + runs-on: ubuntu-latest + env: + CODEC_BC: ${{ needs.codec-roseau.outputs.has_bc }} + CODEC_N: ${{ needs.codec-roseau.outputs.bc_count }} + COLLISION_BC: ${{ needs.collision-roseau.outputs.has_bc }} + COLLISION_N: ${{ needs.collision-roseau.outputs.bc_count }} + CONFIG_BC: ${{ needs.config-roseau.outputs.has_bc }} + CONFIG_N: ${{ needs.config-roseau.outputs.bc_count }} + FONT_BC: ${{ needs.font-roseau.outputs.has_bc }} + FONT_N: ${{ needs.font-roseau.outputs.bc_count }} + INTEGRATION_BC: ${{ needs.integration-roseau.outputs.has_bc }} + INTEGRATION_N: ${{ needs.integration-roseau.outputs.bc_count }} + MEB_BC: ${{ needs.moveable-entity-block-roseau.outputs.has_bc }} + MEB_N: ${{ needs.moveable-entity-block-roseau.outputs.bc_count }} + MULTIBLOCK_BC: ${{ needs.multiblock-roseau.outputs.has_bc }} + MULTIBLOCK_N: ${{ needs.multiblock-roseau.outputs.bc_count }} + NETWORK_BC: ${{ needs.network-roseau.outputs.has_bc }} + NETWORK_N: ${{ needs.network-roseau.outputs.bc_count }} + RECIPE_BC: ${{ needs.recipe-roseau.outputs.has_bc }} + RECIPE_N: ${{ needs.recipe-roseau.outputs.bc_count }} + REGISTRUM_BC: ${{ needs.registrum-roseau.outputs.has_bc }} + REGISTRUM_N: ${{ needs.registrum-roseau.outputs.bc_count }} + RENDERING_BC: ${{ needs.rendering-roseau.outputs.has_bc }} + RENDERING_N: ${{ needs.rendering-roseau.outputs.bc_count }} + SPACESELECT_BC: ${{ needs.space-select-roseau.outputs.has_bc }} + SPACESELECT_N: ${{ needs.space-select-roseau.outputs.bc_count }} + SYNC_BC: ${{ needs.sync-roseau.outputs.has_bc }} + SYNC_N: ${{ needs.sync-roseau.outputs.bc_count }} + UTIL_BC: ${{ needs.util-roseau.outputs.has_bc }} + UTIL_N: ${{ needs.util-roseau.outputs.bc_count }} + WHEEL_BC: ${{ needs.wheel-roseau.outputs.has_bc }} + WHEEL_N: ${{ needs.wheel-roseau.outputs.bc_count }} + MAIN_BC: ${{ needs.main-roseau.outputs.has_bc }} + MAIN_N: ${{ needs.main-roseau.outputs.bc_count }} + steps: + - name: Generate and post PR comment + run: | + row() { + local name=$1 bc=$2 count=$3 + if [ -z "$bc" ]; then + echo "| ${name} | ⚪ Skipped | — |" + elif [ "$bc" = "true" ]; then + echo "| ${name} | 🔴 BC detected | ${count} |" + else + echo "| ${name} | ✅ Compatible | 0 |" + fi + } + + { + echo "## 🌿 Roseau API Breaking Change Report" + echo "" + echo "| Module | Status | Breaking Changes |" + echo "|--------|--------|-----------------|" + row "codec" "$CODEC_BC" "$CODEC_N" + row "collision" "$COLLISION_BC" "$COLLISION_N" + row "config" "$CONFIG_BC" "$CONFIG_N" + row "font" "$FONT_BC" "$FONT_N" + row "integration" "$INTEGRATION_BC" "$INTEGRATION_N" + row "moveable-entity-block" "$MEB_BC" "$MEB_N" + row "multiblock" "$MULTIBLOCK_BC" "$MULTIBLOCK_N" + row "network" "$NETWORK_BC" "$NETWORK_N" + row "recipe" "$RECIPE_BC" "$RECIPE_N" + row "registrum" "$REGISTRUM_BC" "$REGISTRUM_N" + row "rendering" "$RENDERING_BC" "$RENDERING_N" + row "space-select" "$SPACESELECT_BC" "$SPACESELECT_N" + row "sync" "$SYNC_BC" "$SYNC_N" + row "util" "$UTIL_BC" "$UTIL_N" + row "wheel" "$WHEEL_BC" "$WHEEL_N" + row "main" "$MAIN_BC" "$MAIN_N" + echo "" + echo "> Detailed reports: see the *Artifacts* section of [this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." + } > /tmp/roseau-body.md + + - name: Find existing Roseau comment + id: find-comment + uses: peter-evans/find-comment@v3 + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: github-actions[bot] + body-includes: Roseau API Breaking Change Report + + - name: Create or update PR comment + uses: peter-evans/create-or-update-comment@v4 + with: + comment-id: ${{ steps.find-comment.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body-path: /tmp/roseau-body.md + edit-mode: replace diff --git a/.github/workflows/roseau_check.yml b/.github/workflows/roseau_check.yml new file mode 100644 index 00000000..a3a3d052 --- /dev/null +++ b/.github/workflows/roseau_check.yml @@ -0,0 +1,75 @@ +name: Roseau Check + +on: + workflow_call: + inputs: + module: + description: 'Module directory name (e.g. codec, config)' + required: true + type: string + module_id: + description: 'Module short ID (e.g. anvillib-codec)' + required: true + type: string + outputs: + has_bc: + description: 'Whether breaking changes were detected' + value: ${{ jobs.check.outputs.has_bc }} + bc_count: + description: 'Number of breaking changes' + value: ${{ jobs.check.outputs.bc_count }} + +jobs: + check: + runs-on: ubuntu-latest + outputs: + has_bc: ${{ steps.result.outputs.has_bc }} + bc_count: ${{ steps.result.outputs.bc_count }} + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Read Java version + id: properties + uses: christian-draeger/read-properties@1.1.1 + with: + path: gradle.properties + properties: java_version + + - name: Setup Java ${{ steps.properties.outputs.java_version }} + uses: actions/setup-java@v5.2.0 + with: + distribution: zulu + java-version: ${{ steps.properties.outputs.java_version }} + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Run roseauCheck + id: roseau + continue-on-error: true + run: ./gradlew :${{ inputs.module_id }}-neoforge-26.1:roseauCheck + + - name: Collect result + id: result + if: always() + run: | + CSV="module.${{ inputs.module }}/build/reports/roseau/report.csv" + if [ -f "$CSV" ] && [ "$(wc -l < "$CSV")" -gt 1 ]; then + COUNT=$(($(wc -l < "$CSV") - 1)) + echo "has_bc=true" >> $GITHUB_OUTPUT + echo "bc_count=${COUNT}" >> $GITHUB_OUTPUT + echo "Roseau: ${COUNT} breaking change(s) in ${{ inputs.module }}" + else + echo "has_bc=false" >> $GITHUB_OUTPUT + echo "bc_count=0" >> $GITHUB_OUTPUT + echo "Roseau: no breaking changes in ${{ inputs.module }}" + fi + + - name: Upload report + if: always() + uses: actions/upload-artifact@v7 + with: + name: roseau-${{ inputs.module }} + path: module.${{ inputs.module }}/build/reports/roseau/ + retention-days: 7 diff --git a/build.gradle b/build.gradle index b56cc632..17481b2e 100644 --- a/build.gradle +++ b/build.gradle @@ -124,4 +124,7 @@ subprojects { apply plugin: "me.modmuss50.mod-publish-plugin" apply from: rootProject.file("module.gradle") + if (it.name != "anvillib-test-neoforge-26.1") { + apply from: rootProject.file("gradle/scripts/roseau.gradle") + } } diff --git a/gradle/scripts/roseau.gradle b/gradle/scripts/roseau.gradle new file mode 100644 index 00000000..ec0ca4e6 --- /dev/null +++ b/gradle/scripts/roseau.gradle @@ -0,0 +1,76 @@ +configurations { + roseau +} + +dependencies { + roseau 'io.github.alien-tools:roseau-cli:0.6.0' +} + +def jarFileProvider = tasks.named('jar').flatMap { it.archiveFile } +def reportsDirProvider = layout.buildDirectory.dir('reports/roseau') +def compileClasspathProvider = provider { sourceSets.main.compileClasspath.asPath } +def roseauGroup = project.group.toString() +def roseauArtifact = project.name +def roseauBaselineVersion = project.findProperty('roseauBaselineVersion') ?: { + def groupPath = project.group.toString().replace('.', '/') + def artifact = project.name + def repos = [ + 'https://repo1.maven.org/maven2', + 'https://server.cjsah.net:1002/maven', + ] + for (repoUrl in repos) { + try { + def metadataUrl = new URL("${repoUrl}/${groupPath}/${artifact}/maven-metadata.xml") + def conn = metadataUrl.openConnection() + conn.connectTimeout = 5000 + conn.readTimeout = 5000 + def xmlText = conn.inputStream.getText('UTF-8') + def matcher = xmlText =~ /([^<]+)<\/latest>/ + if (matcher.find()) { + def latest = matcher.group(1) + logger.lifecycle("Roseau: resolved latest version ${latest} from ${repoUrl}") + return latest + } + } catch (Exception ignored) { + // try next repo + } + } + logger.warn("Roseau: could not resolve latest version from Maven, falling back to ${version}") + return version.toString() +}() +def roseauMvnCoord = "${roseauGroup}:${roseauArtifact}:${roseauBaselineVersion}" +def roseauBaselineDep = dependencies.create(group: roseauGroup, name: roseauArtifact, version: roseauBaselineVersion) +def roseauBaselineConfig = configurations.detachedConfiguration(roseauBaselineDep) +def roseauBaselineJarProvider = provider { roseauBaselineConfig.singleFile } + +tasks.register('roseauCheck', JavaExec) { + group = 'verification' + description = 'Detects API breaking changes against a baseline version using Roseau' + + dependsOn tasks.named('jar') + + classpath = configurations.roseau + mainClass = 'io.github.alien.roseau.cli.RoseauCLI' + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(Integer.parseInt(project.java_version)) + } + + doFirst { + def currentJar = jarFileProvider.get().asFile + def baselineJar = roseauBaselineJarProvider.get() + def reportsDir = reportsDirProvider.get().asFile + reportsDir.mkdirs() + + logger.lifecycle("Roseau: comparing baseline ${roseauMvnCoord} against ${currentJar.name}") + + args( + '--diff', + '--v1', baselineJar.absolutePath, + '--v2', currentJar.absolutePath, + '--classpath', compileClasspathProvider.get(), + '--fail-on-bc', + '--report', "HTML=${new File(reportsDir, 'report.html').absolutePath}", + '--report', "CSV=${new File(reportsDir, 'report.csv').absolutePath}", + ) + } +}