diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bd79398..041bd752 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,297 +1,104 @@ -import groovy.json.JsonSlurper -import groovy.json.JsonOutput - -// ===================================================================== -// dataLib — Datapack build system (pure Gradle, no plugins required) -// Replaces: build.yml, dp-lint.yml, lint-and-build.yml -// generate-build-py.yml logic is kept in .github/workflows/ci.yml -// because it commits+pushes — not a build responsibility. -// ===================================================================== - -def REPO_SLUG = "runtoolkit/dataLib" - -// label -> pack_format. Single target now that overlays are gone. -def TARGETS = [ - "full" : 101 -] - -def buildRoot = layout.buildDirectory.dir("datapack").get().asFile -def distRoot = layout.buildDirectory.dir("dist").get().asFile - -// --------------------------------------------------------------------- -// Repo lock — refuse to run outside runtoolkit/dataLib -// --------------------------------------------------------------------- -tasks.register("repoLock") { - group = "verification" - description = "Fails the build if GITHUB_REPOSITORY != ${REPO_SLUG}" - doLast { - def repo = System.getenv("GITHUB_REPOSITORY") - if (repo != null && repo != REPO_SLUG) { - throw new GradleException("This build may only run in ${REPO_SLUG} (got: ${repo})") - } - } -} - -// --------------------------------------------------------------------- -// Lint: JSON validity -// --------------------------------------------------------------------- -tasks.register("lintJson") { - group = "verification" - description = "Validates every *.json file parses as JSON" - doLast { - def slurper = new JsonSlurper() - def failures = [] - fileTree(projectDir) { - include "**/*.json" - exclude ".git/**" - }.each { f -> - try { - slurper.parse(f) - } catch (Exception e) { - failures << "${f}: ${e.message}" - } - } - if (failures) { - failures.each { logger.error("JSON error: ${it}") } - throw new GradleException("${failures.size()} invalid JSON file(s) found.") - } - logger.lifecycle("All JSON files valid.") - } -} - -// --------------------------------------------------------------------- -// Lint: mcfunction pre-check (BOM / null byte / CRLF) -// --------------------------------------------------------------------- -tasks.register("lintMcfunctionPrecheck") { - group = "verification" - description = "Scans *.mcfunction files for BOM, null bytes, CRLF" - doLast { - def errors = 0 - fileTree(projectDir) { - include "**/*.mcfunction" - exclude ".git/**" - }.each { f -> - byte[] bytes = f.bytes - if (bytes.length >= 3 && (bytes[0] & 0xFF) == 0xEF && (bytes[1] & 0xFF) == 0xBB && (bytes[2] & 0xFF) == 0xBF) { - logger.error("BOM detected: ${f}") - errors++ - } - if (bytes.any { it == 0 }) { - logger.error("Null byte detected: ${f}") - errors++ - } - if (bytes.any { it == (byte) 0x0D }) { - logger.error("CRLF line endings: ${f}") - errors++ - } - } - if (errors > 0) { - throw new GradleException("${errors} issue(s) found in .mcfunction files.") - } - logger.lifecycle("mcfunction pre-check passed.") - } -} - -// --------------------------------------------------------------------- -// Lint: mecha syntax validation -// Mirrors dp-lint.yml's compat patching — applied to a scratch copy only, -// source files are never touched. -// --------------------------------------------------------------------- -tasks.register("lintMecha", Exec) { - group = "verification" - description = "Runs mecha against a patched scratch copy of the datapack" - dependsOn "lintMcfunctionPrecheck" - - def scratch = layout.buildDirectory.dir("mecha-scratch").get().asFile - - doFirst { - delete(scratch) - mkdir(scratch) - copy { - from(projectDir) { - exclude "build/**", ".git/**", ".github/**" - } - into(scratch) - } - - fileTree(scratch) { include "**/*.mcfunction" }.each { f -> - def text = f.getText("UTF-8") - def patched = text - .replaceAll(/([a-z0-9._-]+): _/, '$1:_') - .replaceAll(/time of [a-z0-9_:.-]+ /, 'time ') - .replace('time query day repetition', 'time query daytime') - if (patched != text) { - f.setText(patched, "UTF-8") - } - } - - exec { - commandLine "pip", "install", "--break-system-packages", "-q", - "git+https://github.com/mcbeet/mecha.git" - } - } - - workingDir = scratch - commandLine "mecha", "." -} - -// --------------------------------------------------------------------- -// Lint: pack.mcmeta presence + validity -// --------------------------------------------------------------------- -tasks.register("lintPackMcmeta") { - group = "verification" - description = "Checks pack.mcmeta exists and is valid JSON" - doLast { - def meta = file("pack.mcmeta") - if (!meta.exists()) { - throw new GradleException("pack.mcmeta is missing!") - } - new JsonSlurper().parse(meta) - logger.lifecycle("pack.mcmeta is valid JSON.") - } -} - -tasks.register("lint") { - group = "verification" - description = "Runs all datapack lint checks" - dependsOn "lintJson", "lintMcfunctionPrecheck", "lintMecha", "lintPackMcmeta" -} - -// --------------------------------------------------------------------- -// Build: one task per matrix target (1_20_3, 1_20_5, pre_1_21_4, ...) -// --------------------------------------------------------------------- -def buildTasks = [] - -TARGETS.each { label, packFormat -> - def stageDir = new File(buildRoot, label) - - def prepareTask = tasks.register("prepare${label.capitalize()}") { - group = "build" - description = "Assembles the ${label} datapack variant into build/datapack/${label}" - dependsOn "repoLock" - - doLast { - delete(stageDir) - mkdir(stageDir) - - copy { - from(projectDir) { - exclude "build/**", ".git/**", ".github/**", ".gitattributes", ".gitignore", - "README.md", "version.json", "dependencies.json" - } - into(stageDir) - } - - // Patch pack.mcmeta: set pack_format only. No overlay entries to filter anymore. - def metaFile = new File(stageDir, "pack.mcmeta") - def meta = new JsonSlurper().parse(metaFile) - meta.pack.pack_format = packFormat - metaFile.text = JsonOutput.prettyPrint(JsonOutput.toJson(meta)) - } - } - - def zipTask = tasks.register("zip${label.capitalize()}", Zip) { - group = "build" - description = "Packages the ${label} datapack variant as a zip artifact" - dependsOn prepareTask - from(stageDir) - destinationDirectory.set(distRoot) - archiveFileName.set("dataLib-${label}.zip") - } - - buildTasks << zipTask -} - -tasks.register("buildAll") { - group = "build" - description = "Builds every datapack variant (equivalent to the old build.yml matrix)" - dependsOn buildTasks -} - -// --------------------------------------------------------------------- -// Repack: strips every dist zip down to pack.mcmeta + data/ only. -// Deletes the original zip and writes the trimmed one in its place. -// --------------------------------------------------------------------- -tasks.register("repack") { - group = "build" - description = "Trims each dist zip to pack.mcmeta + data/ only, replacing the original" - dependsOn buildTasks - - doLast { - def scratch = layout.buildDirectory.dir("repack-scratch").get().asFile - - distRoot.listFiles({ f -> f.name.endsWith(".zip") } as FileFilter)?.each { zipFile -> - def name = zipFile.name - delete(scratch) - mkdir(scratch) - - copy { - from(zipTree(zipFile)) { - include "pack.mcmeta" - include "data/**" - } - into(scratch) - } - - if (!new File(scratch, "pack.mcmeta").exists()) { - throw new GradleException("${name}: pack.mcmeta missing after filtering — refusing to repack.") - } - - delete(zipFile) - - ant.zip(destfile: zipFile, basedir: scratch) - - logger.lifecycle("Repacked ${name} -> pack.mcmeta + data/ only") - } - } -} - -// --------------------------------------------------------------------- -// Release: reads version from the full build's pack.mcmeta, publishes -// via gh CLI. Only meaningful on push to main — that branch/event gate -// stays in ci.yml, not here, since it's CI trigger logic, not a build step. -// --------------------------------------------------------------------- -tasks.register("release") { - group = "publish" - description = "Creates/updates the GitHub release for the full build" - dependsOn "repoLock", "zipFull" - - doLast { - def metaFile = new File(buildRoot, "full/pack.mcmeta") - def meta = new JsonSlurper().parse(metaFile) - def desc = meta.pack.description - - String version = null - if (desc instanceof List) { - desc.each { entry -> - if (entry instanceof Map && entry.text) { - def m = (entry.text =~ /^\s*\|\s*(v[0-9][^\s]*)/) - if (m.find()) version = m.group(1) - } - } - } else if (desc instanceof String) { - def m = (desc =~ /v[\d.]+/) - if (m.find()) version = m.group(0) - } - version = version ?: "latest" - - def zip = new File(distRoot, "dataLib-full.zip") - - exec { commandLine "bash", "-c", "gh release delete '${version}' --yes 2>/dev/null || true" } - exec { commandLine "bash", "-c", "git push origin ':refs/tags/${version}' 2>/dev/null || true" } - exec { - commandLine "gh", "release", "create", version, - "--title", "dataLib ${version}", - "--notes", "**Datapack build** (pack_format 101).", - "--latest", - "${zip}#dataLib-full.zip" - } - logger.lifecycle("Released ${version}") - } -} - -tasks.register("clean", Delete) { - group = "build" - description = "Deletes the build/ directory" - delete(layout.buildDirectory) -} +name: CI + +on: + push: + branches: [main, workflows] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: write + +jobs: + # ===================================================================== + # lint — mecha, JSON, mcfunction pre-check, pack.mcmeta + # ===================================================================== + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "21" + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - name: Gradle lint + run: chmod +x gradlew && ./gradlew lint --no-daemon + + # ===================================================================== + # build — matrix zips (1_20_3, 1_20_5, pre_1_21_4, 1_21_5, 1_21_6, full) + # ===================================================================== + build: + needs: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "21" + - name: Gradle buildAll + run: chmod +x gradlew && ./gradlew buildAll --no-daemon + - uses: actions/upload-artifact@v7 + with: + name: dataLib-datapacks + path: build/dist/*.zip + retention-days: 14 + if-no-files-found: error + + # ===================================================================== + # release — only on push to main + # ===================================================================== + release: + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "21" + - name: Gradle release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + chmod +x gradlew + ./gradlew zipFull --no-daemon + ./gradlew release --no-daemon + + # ===================================================================== + # generate-build-py — only on push to 'workflows' branch + # Kept out of Gradle: this job commits + pushes to the repo, which is + # a CI/VCS action, not a build responsibility. + # ===================================================================== + generate-build-py: + if: github.event_name == 'push' && github.ref == 'refs/heads/workflows' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.ref }} + fetch-depth: 0 + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - name: Run generate build script + run: python3 .github/scripts/generate_buid-py.py + - uses: actions/upload-artifact@v7 + with: + name: build-py + path: build.py + - name: Commit and push build.py + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add build.py + git diff --cached --quiet && exit 0 + git commit -m "Auto generate build.py" + git push origin HEAD:${GITHUB_REF#refs/heads/} diff --git a/build.gradle b/build.gradle index 993f40b9..2502edbf 100644 --- a/build.gradle +++ b/build.gradle @@ -109,7 +109,7 @@ tasks.register("lintMecha", Exec) { mkdir(scratch) copy { from(projectDir) { - exclude ".git/**", ".github/**" + exclude "build/**", ".git/**", ".github/**" } into(scratch) } @@ -176,7 +176,7 @@ TARGETS.each { label, packFormat -> copy { from(projectDir) { - exclude ".git/**", ".github/**", ".gitattributes", ".gitignore", + exclude "build/**", ".git/**", ".github/**", ".gitattributes", ".gitignore", "README.md", "version.json", "dependencies.json" } into(stageDir) @@ -204,8 +204,9 @@ TARGETS.each { label, packFormat -> tasks.register("buildAll") { group = "build" - description = "Builds every datapack variant (equivalent to the old build.yml matrix)" + description = "Builds every datapack variant and repacks each zip to pack.mcmeta + pack.png + data/ only" dependsOn buildTasks + finalizedBy "repack" } // --------------------------------------------------------------------- @@ -228,6 +229,7 @@ tasks.register("repack") { copy { from(zipTree(zipFile)) { include "pack.mcmeta" + include "pack.png" include "data/**" } into(scratch)