diff --git a/.github/workflows/dependency_graph.yml b/.github/workflows/dependency_graph.yml new file mode 100644 index 0000000..4d2765b --- /dev/null +++ b/.github/workflows/dependency_graph.yml @@ -0,0 +1,40 @@ +name: Update Gradle Dependency Graph + +on: + push: + branches: + # Submitting dependency graph reports on non-default branches does nothing + - "master" + tags-ignore: + - "**" + paths: + - "gradle**" + - "*.gradle" + workflow_dispatch: + +permissions: + contents: write + +jobs: + dependency-submission: + runs-on: ubuntu-latest + steps: + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Java 21 + uses: actions/setup-java@v4 + with: + java-version: "21" + distribution: "microsoft" + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Generate and submit dependency graph + uses: gradle/actions/dependency-submission@v4 + with: + build-scan-publish: true + build-scan-terms-of-use-url: "https://gradle.com/help/legal-terms-of-use" + build-scan-terms-of-use-agree: "yes" diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index ce698ca..c01d84e 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -2,36 +2,41 @@ name: Java CI with Gradle on: push: + branches: + - "**" + tags-ignore: + - "**" paths: - - '**.java' - - '**.json' - - 'gradle**' - - 'build.gradle' + - "**.java" + - "**.json" + - "**.yml" + - "gradle**" + - "*.gradle" pull_request: paths: - - '**.java' - - '**.json' - - 'gradle**' - - 'build.gradle' - # Makes it possible to run this workflow manually from the Actions tab + - "**.java" + - "**.json" + - "**.yml" + - "gradle**" + - "*.gradle" workflow_dispatch: -permissions: - contents: write - jobs: build: runs-on: ubuntu-latest - + env: + VIRUSTOTAL_API_KEY: ${{ secrets.VIRUSTOTAL_API_KEY }} + IMGUR_CLIENT_ID: ${{ secrets.IMGUR_CLIENT_ID }} steps: + - name: Checkout repository uses: actions/checkout@v4 - name: Set up Java 21 uses: actions/setup-java@v4 with: - java-version: '21' - distribution: 'microsoft' + java-version: "21" + distribution: "microsoft" - name: Grant execute permission for gradlew run: chmod +x gradlew @@ -45,18 +50,48 @@ jobs: # Enable cache writing for NeoForge branches, since they don't benefit from the Fabric cache on master cache-read-only: ${{ github.ref != 'refs/heads/master' && !contains(github.ref, 'neoforge') }} - - name: Generate and submit dependency graph - if: ${{ github.event_name == 'push' }} - uses: gradle/actions/dependency-submission@v4 + - name: Compile Java code + run: ./gradlew remapJar --stacktrace --warning-mode=fail + + - name: Validate JSON files + run: ./gradlew spotlessJsonCheck || (echo "::error::JSON validation failed! Run './gradlew spotlessApply' to fix style issues, or check the full error message for syntax errors." && exit 1) - - name: Run tests and build + - name: Validate Java code style + run: ./gradlew spotlessCheck || (echo "::error::Java code style validation failed! To fix, run 'Clean Up' and then 'Format' in Eclipse, or './gradlew spotlessApply' in the terminal." && exit 1) + + - name: Run unit tests + run: ./gradlew test --stacktrace --warning-mode=fail + + - name: Validate access widener + run: ./gradlew validateAccessWidener --stacktrace --warning-mode=fail + + - name: Build run: ./gradlew build --stacktrace --warning-mode=fail - - name: VirusTotal scan - if: ${{ github.event_name == 'push' }} + - name: Upload to VirusTotal for analysis + id: virustotal + if: ${{ env.VIRUSTOTAL_API_KEY }} uses: crazy-max/ghaction-virustotal@v4 with: - vt_api_key: ${{ secrets.VIRUSTOTAL_API_KEY }} + vt_api_key: ${{ env.VIRUSTOTAL_API_KEY }} files: | ./build/libs/*.jar + # An error in this step means that the upload failed, not that a false + # positive was detected. continue-on-error: true + + - name: Add VirusTotal links to build summary + if: ${{ env.VIRUSTOTAL_API_KEY && steps.virustotal.outputs.analysis }} + shell: bash + run: | + echo "
" >> $GITHUB_STEP_SUMMARY + echo "🛡️ VirusTotal Scans" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + IFS=',' read -ra ANALYSIS <<< "${{ steps.virustotal.outputs.analysis }}" + for i in "${ANALYSIS[@]}"; do + filepath=${i%%=*} + url=${i#*=} + filename=$(basename "$filepath") + echo "- [$filename]($url)" >> $GITHUB_STEP_SUMMARY + done + echo "
" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/jsonsyntax.yml b/.github/workflows/jsonsyntax.yml deleted file mode 100644 index e6f9a4d..0000000 --- a/.github/workflows/jsonsyntax.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: JSON syntax - -on: - push: - paths: - - '**.json' - pull_request: - paths: - - '**.json' - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Check JSON syntax - uses: limitusus/json-syntax-check@v2 - with: - pattern: "\\.json$" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..8c38cf5 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,87 @@ +name: Publish Release + +on: + workflow_dispatch: + inputs: + close_milestone: + description: "Close milestone" + required: true + type: boolean + default: true + upload_backups: + description: "Upload to backups server" + required: true + type: boolean + default: true + publish_github: + description: "Publish to GitHub" + required: true + type: boolean + default: true + publish_curseforge: + description: "Publish to CurseForge" + required: true + type: boolean + default: true + publish_modrinth: + description: "Publish to Modrinth" + required: true + type: boolean + default: true + +jobs: + publish: + runs-on: ubuntu-latest + env: + WI_BACKUPS_API_KEY: ${{ secrets.WI_BACKUPS_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }} + MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Java 21 + uses: actions/setup-java@v4 + with: + java-version: "21" + distribution: "microsoft" + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build + run: ./gradlew build --stacktrace --warning-mode=fail + + - name: Create and push tag + run: | + MOD_VERSION=$(grep "mod_version" gradle.properties | cut -d'=' -f2 | tr -d ' ') + git config --global user.name "Wurst-Bot" + git config --global user.email "contact.wurstimperium@gmail.com" + git tag $MOD_VERSION + git push origin $MOD_VERSION + + - name: Close milestone + if: ${{ inputs.close_milestone }} + run: ./gradlew closeMilestone --stacktrace + + - name: Upload backups + if: ${{ inputs.upload_backups }} + run: ./gradlew uploadBackups --stacktrace + + - name: Publish to GitHub + if: ${{ inputs.publish_github }} + env: + GITHUB_TOKEN: ${{ secrets.MCX_PUBLISH_TOKEN }} + run: ./gradlew github --stacktrace + + - name: Publish to CurseForge + if: ${{ inputs.publish_curseforge }} + run: ./gradlew publishCurseforge --stacktrace + + - name: Publish to Modrinth + if: ${{ inputs.publish_modrinth }} + run: ./gradlew publishModrinth --stacktrace diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 3218d53..e0a1612 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,4 +1,5 @@ -name: "Close stale issues and pull requests" +name: Close stale issues and pull requests + on: schedule: - cron: "30 1 * * 1-5" @@ -11,6 +12,7 @@ jobs: stale: runs-on: ubuntu-latest steps: + - uses: actions/stale@v9 with: stale-issue-message: | diff --git a/build.gradle b/build.gradle index 755455d..4d86f2a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { dependencies { - classpath 'org.kohsuke:github-api:1.326' + classpath "org.kohsuke:github-api:1.326" } } @@ -52,36 +52,36 @@ runs { // "REGISTRIES": For firing of registry events. // "REGISTRYDUMP": For getting the contents of all registries. systemProperty 'forge.logging.markers', 'REGISTRIES' - + // Recommended logging level for the console // You can set various levels here. // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels systemProperty 'forge.logging.console.level', 'debug' - + modSource project.sourceSets.main } - + client { // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. systemProperty 'forge.enabledGameTestNamespaces', project.mod_id } - + server { systemProperty 'forge.enabledGameTestNamespaces', project.mod_id programArgument '--nogui' } - + // This run config launches GameTestServer and runs all registered gametests, then exits. // By default, the server will crash when no gametests are provided. // The gametest system is also enabled by default for other run configs under the /test command. gameTestServer { systemProperty 'forge.enabledGameTestNamespaces', project.mod_id } - + data { // example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it // workingDirectory project.file('run-data') - + // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() } @@ -115,25 +115,25 @@ dependencies { // And its provides the option to then use net.minecraft as the group, and one of; client, server or joined as the module name, plus the game version as version. // For all intends and purposes: You can treat this dependency as if it is a normal library you would use. implementation "net.neoforged:neoforge:${neo_version}" - + // Example optional mod dependency with JEI // The JEI API is declared for compile time use, while the full JEI artifact is used at runtime // compileOnly "mezz.jei:jei-${mc_version}-common-api:${jei_version}" // compileOnly "mezz.jei:jei-${mc_version}-neoforge-api:${jei_version}" // We add the full version to localRuntime, not runtimeOnly, so that we do not publish a dependency on it // localRuntime "mezz.jei:jei-${mc_version}-neoforge:${jei_version}" - + // Example mod dependency using a mod jar from ./libs with a flat dir repository // This maps to ./libs/coolmod-${mc_version}-${coolmod_version}.jar // The group id is ignored when searching -- in this case, it is "blank" // implementation "blank:coolmod-${mc_version}:${coolmod_version}" - + // Example mod dependency using a file as dependency // implementation files("libs/coolmod-${mc_version}-${coolmod_version}.jar") - + // Example project dependency using a sister or child project: // implementation project(":myproject") - + // For more info: // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html // http://www.gradle.org/docs/current/userguide/dependency_management.html @@ -158,7 +158,7 @@ tasks.withType(ProcessResources).configureEach { mod_description : mod_description ] inputs.properties replaceProperties - + filesMatching(['META-INF/neoforge.mods.toml']) { expand replaceProperties } @@ -194,14 +194,26 @@ spotless { } } +def getGhVersion() { + return "v" + version.substring(0, version.indexOf("-MC")) +} + +def getChangelogUrl() { + def modSlug = mod_name.toLowerCase().replace(" ", "-") + def versionSlug = version.substring(0, version.indexOf("-MC")).replace(".", "-") + return "https://www.wimods.net/${modSlug}/${modSlug}-${versionSlug}/" +} + publishMods { - file = jar.archiveFile - def versionString = project.version as String - def ghVersion = "v" + versionString.substring(0, versionString.indexOf("-")) - def changelogUrl = "https://www.wimods.net/mo-glass/mo-glass-1-10/" - type = ghVersion.contains("pre") ? BETA : STABLE + file = tasks.named("jar").flatMap { task -> + def libsDir = layout.buildDirectory.dir("libs").get().asFile + def jarFiles = libsDir.listFiles().findAll { it.name.endsWith(".jar") } + layout.file(providers.provider { jarFiles[0] }) + } + type = getGhVersion().contains("pre") ? BETA : STABLE modLoaders.add("neoforge") - + def changelogUrl = getChangelogUrl() + curseforge { projectId = "353426" accessToken = providers.environmentVariable("CURSEFORGE_API_KEY") @@ -216,16 +228,24 @@ publishMods { } } -afterEvaluate { - tasks.publishMods.dependsOn build - tasks.publishCurseforge.dependsOn build - tasks.publishModrinth.dependsOn build +tasks.named("publishMods").configure { + dependsOn(tasks.named("build")) +} + +tasks.named("publishCurseforge").configure { + dependsOn(tasks.named("build")) +} + +tasks.named("publishModrinth").configure { + dependsOn(tasks.named("build")) } import org.kohsuke.github.GHReleaseBuilder import org.kohsuke.github.GitHub -task github(dependsOn: build) { +task github { + dependsOn tasks.named("build") + onlyIf { ENV.GITHUB_TOKEN } @@ -233,7 +253,7 @@ task github(dependsOn: build) { doLast { def github = GitHub.connectUsingOAuth(ENV.GITHUB_TOKEN as String) def repository = github.getRepository("Wurst-Imperium-MCX/Mo-Glass") - def ghVersion = "v" + version.substring(0, version.indexOf("-")) + def ghVersion = getGhVersion() def ghRelease = repository.getReleaseByTagName(ghVersion as String) if(ghRelease == null) { @@ -242,6 +262,79 @@ task github(dependsOn: build) { ghRelease = releaseBuilder.create() } - ghRelease.uploadAsset(jar.archiveFile.get().getAsFile(), "application/java-archive") + def libsDir = layout.buildDirectory.dir("libs").get().asFile + def jarFiles = libsDir.listFiles().findAll { it.name.endsWith(".jar") } + File publishJar = jarFiles[0] + + ghRelease.uploadAsset(publishJar, "application/java-archive") + } +} + +import java.time.LocalDate +import org.kohsuke.github.GHIssueState +import org.kohsuke.github.GHMilestoneState +import java.time.ZoneId + +task closeMilestone { + onlyIf { + ENV.GITHUB_TOKEN + } + + doLast { + def github = GitHub.connectUsingOAuth(ENV.GITHUB_TOKEN as String) + def repository = github.getRepository("Wurst-Imperium/Mo-Glass") + def ghVersion = getGhVersion() + + // Weird API design: listMilestones() requires GHIssueState while everything else uses GHMilestoneState. + def milestone = repository.listMilestones(GHIssueState.ALL).find { it.title == ghVersion } + if (milestone == null) { + milestone = repository.createMilestone(ghVersion, "") + } + + if (milestone.getState() != GHMilestoneState.CLOSED) { + milestone.setDueOn(Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant())) + milestone.setDescription(getChangelogUrl()) + milestone.close() + } + } +} + +task uploadBackups { + dependsOn tasks.named("build") + + onlyIf { + ENV.WI_BACKUPS_API_KEY + } + + doLast { + def shortVersion = getGhVersion().substring(1) + def backupUrl = "https://api.wurstclient.net/artifact-backups/Mo-Glass/${shortVersion}" + + def connection = new URL(backupUrl).openConnection() as HttpURLConnection + def boundary = UUID.randomUUID().toString() + connection.setRequestMethod("POST") + connection.setRequestProperty("X-API-Key", ENV.WI_BACKUPS_API_KEY) + connection.setRequestProperty("Accept", "application/json") + connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=$boundary") + connection.doOutput = true + + def output = connection.outputStream + def libsDir = layout.buildDirectory.dir("libs").get().asFile + def jarFiles = libsDir.listFiles().findAll { it.name.endsWith(".jar") } + + jarFiles.each { file -> + output << "--${boundary}\r\n" + output << "Content-Disposition: form-data; name=\"files\"; filename=\"${file.name}\"\r\n" + output << "Content-Type: application/java-archive\r\n\r\n" + file.withInputStream { input -> + output << input + } + output << "\r\n" + } + output << "--${boundary}--\r\n" + output.flush() + + if(connection.responseCode != 200) + throw new GradleException("Failed to upload backups: ${connection.responseCode} ${connection.responseMessage}") } }