|
| 1 | +name: Validate & Build Dev Version |
| 2 | + |
| 3 | +on: |
| 4 | + push: |
| 5 | + workflow_dispatch: |
| 6 | + inputs: |
| 7 | + tag: |
| 8 | + description: 'Version tag to build (if omitted, latest --describe tag from git is used)' |
| 9 | + required: false |
| 10 | + mc_upper: |
| 11 | + description: 'Upper bound of Minecraft version range (exact version), e.g. "1.21.5"' |
| 12 | + required: false |
| 13 | + mc_lower: |
| 14 | + description: 'Lower bound of Minecraft version range (exact version), e.g. "1.17"' |
| 15 | + required: false |
| 16 | + skip_check: |
| 17 | + description: 'Set to true to skip command validation' |
| 18 | + required: false |
| 19 | + type: boolean |
| 20 | + default: false |
| 21 | + |
| 22 | +env: |
| 23 | + # Fallback Lower: when Spyglass API is unreachable |
| 24 | + FALLBACK_MC_LOWER: "1.17" |
| 25 | + # Fallback Upper: when Spyglass API and Mojang API are unreachable |
| 26 | + FALLBACK_MC_UPPER: "1.21.5" |
| 27 | + |
| 28 | +jobs: |
| 29 | + deploy: |
| 30 | + runs-on: ubuntu-latest |
| 31 | + name: Validate and build development version |
| 32 | + steps: |
| 33 | + - name: Checkout code |
| 34 | + uses: actions/checkout@v4 |
| 35 | + with: |
| 36 | + show-progress: false |
| 37 | + fetch-depth: 25 # Fetch 25 commits to ensure we have enough history for git describe |
| 38 | + fetch-tags: true |
| 39 | + |
| 40 | + # Determine tag version to use |
| 41 | + - name: Determine tag version |
| 42 | + id: get_tag |
| 43 | + shell: bash |
| 44 | + run: | |
| 45 | + if [ -n "${{ github.event.inputs.tag }}" ]; then |
| 46 | + TAG="${{ github.event.inputs.tag }}" |
| 47 | + else |
| 48 | + TAG_RAW=$(git describe --tags --always) |
| 49 | + TAG="${TAG_RAW#v}" |
| 50 | + fi |
| 51 | + echo "Using tag: $TAG" |
| 52 | + echo "TAG=$TAG" >> $GITHUB_ENV |
| 53 | +
|
| 54 | + # Determine Minecraft versions from pack.mcmeta using Spyglass API |
| 55 | + - name: Determine Minecraft version bounds from pack.mcmeta using Spyglass API |
| 56 | + id: determine_versions |
| 57 | + shell: bash |
| 58 | + run: | |
| 59 | + set -euo pipefail |
| 60 | +
|
| 61 | + if [ -n "${{ github.event.inputs.mc_upper }}" ]; then |
| 62 | + MC_UPPER="${{ github.event.inputs.mc_upper }}" |
| 63 | + MC_LOWER="${{ github.event.inputs.mc_lower }}" |
| 64 | + if [ -z "$MC_LOWER" ]; then |
| 65 | + MC_LOWER="$FALLBACK_MC_LOWER" |
| 66 | + fi |
| 67 | + echo "Using provided Minecraft version bounds: MC_LOWER=$MC_LOWER, MC_UPPER=$MC_UPPER" |
| 68 | + # Build ranges |
| 69 | + MACHINE_RANGE_FABRIC=">=${MC_LOWER} <=${MC_UPPER}" |
| 70 | + MACHINE_RANGE_FORGE="[${MC_LOWER},${MC_UPPER}]" |
| 71 | + MACHINE_RANGE_NEOFORGE="[1.20.5,${MC_UPPER}]" |
| 72 | + CMD_VALIDATOR_MC=$(echo "$MC_UPPER" | awk -F. '{print $1"."$2}') |
| 73 | + MC_HUMAN_VERSION_RANGE="${MC_LOWER}-${MC_UPPER}" |
| 74 | + else |
| 75 | +
|
| 76 | + echo "::group::Determining Minecraft version bounds..." |
| 77 | + echo "Reading supported_formats from pack.mcmeta..." |
| 78 | + if [ -f pack.mcmeta ]; then |
| 79 | + LOWER_DP=$(jq -r '.pack.supported_formats[0]' pack.mcmeta) |
| 80 | + UPPER_DP=$(jq -r '.pack.supported_formats[1]' pack.mcmeta) |
| 81 | + else |
| 82 | + echo "::error:: pack.mcmeta file not found!" |
| 83 | + exit 1 |
| 84 | + fi |
| 85 | + echo "Lower data pack format: $LOWER_DP, Upper: $UPPER_DP" |
| 86 | +
|
| 87 | + echo "Fetching versions from Spyglass API..." |
| 88 | + API_JSON=$(curl -s https://api.spyglassmc.com/mcje/versions || true) |
| 89 | + if [ -z "$API_JSON" ] || ! echo "$API_JSON" | jq empty > /dev/null 2>&1; then |
| 90 | + echo "::warning:: Spyglass API unreachable, using fallback values. The annotated Minecraft versions may not be accurate." |
| 91 | + echo " Response: $API_JSON" |
| 92 | + ENDPOINT_DOWN=true |
| 93 | + else |
| 94 | + ENDPOINT_DOWN=false |
| 95 | + fi |
| 96 | +
|
| 97 | + if [ "$ENDPOINT_DOWN" = true ]; then |
| 98 | + MC_LOWER=${FALLBACK_MC_LOWER} |
| 99 | + # Use latest version from Mojang API as fallback upper |
| 100 | + MC_UPPER=$(curl -s https://piston-meta.mojang.com/mc/game/version_manifest_v2.json || true | jq -r '.latest.release' || echo "${FALLBACK_MC_UPPER}") |
| 101 | + # Build ranges |
| 102 | + MACHINE_RANGE_FABRIC=">=${MC_LOWER}" |
| 103 | + MACHINE_RANGE_FORGE="[${MC_LOWER},)" |
| 104 | + MACHINE_RANGE_NEOFORGE="[1.20.5,)" |
| 105 | + MC_HUMAN_VERSION_RANGE="${MC_LOWER}-${MC_UPPER}" |
| 106 | + # Trim patch version for mecha validation |
| 107 | + CMD_VALIDATOR_MC=$(echo "$MC_UPPER" | awk -F. '{print $1"."$2}') |
| 108 | +
|
| 109 | + echo "Used fallback values: MC_LOWER=$MC_LOWER, MC_UPPER=$MC_UPPER" |
| 110 | + else |
| 111 | + # Determine MC_LOWER: oldest version matching LOWER_DP |
| 112 | + # LIMITATION: Mod loaders don't accept snapshot versions, so we assume lower pack format is a release (and not a snapshot) |
| 113 | + MC_LOWER=$(echo "$API_JSON" | jq -r --argjson lower_dp "$LOWER_DP" '[.[] | select(.data_pack_version == $lower_dp and .type == "release")] | sort_by(.release_time | sub("\\+00:00$"; "Z") | fromdateiso8601) | .[0].id') |
| 114 | + if [ -z "$MC_LOWER" ] || [ "$MC_LOWER" = "null" ]; then |
| 115 | + echo "::error:: Expected a release version for pack format ($LOWER_DP), but none was found!" |
| 116 | + exit 1 |
| 117 | + fi |
| 118 | +
|
| 119 | + # Determine MC_UPPER: newest version matching UPPER_DP |
| 120 | + CHOSEN_UPPER_OBJ=$(echo "$API_JSON" | jq -c --argjson upper_dp "$UPPER_DP" '[.[] | select(.data_pack_version == $upper_dp)] | sort_by(.release_time | sub("\\+00:00$"; "Z") | fromdateiso8601) | reverse | .[0]') |
| 121 | + if [ -z "$CHOSEN_UPPER_OBJ" ] || [ "$CHOSEN_UPPER_OBJ" = "null" ]; then |
| 122 | + # If no version is found, use the latest version from the API |
| 123 | + CHOSEN_UPPER_OBJ=$(echo "$API_JSON" | jq -c 'sort_by(.release_time | sub("\\+00:00$"; "Z") | fromdateiso8601) | reverse | .[0]') |
| 124 | + fi |
| 125 | + MC_UPPER=$(echo "$CHOSEN_UPPER_OBJ" | jq -r '.id') |
| 126 | +
|
| 127 | + echo "Found MC_LOWER=$MC_LOWER, MC_UPPER=$MC_UPPER" |
| 128 | +
|
| 129 | + # Check if upper version is a release |
| 130 | + CHOSEN_UPPER_TYPE=$(echo "$CHOSEN_UPPER_OBJ" | jq -r '.type') |
| 131 | + if [ "$CHOSEN_UPPER_TYPE" != "release" ]; then |
| 132 | + echo "Exact upper version is not a release. Attempting to find the next release version for mod version ranges..." |
| 133 | +
|
| 134 | + # Exact upper is not a release, try to find the next higher release (i.e. one with a release_time greater than the chosen upper one) |
| 135 | + CHOSEN_TIME_NUM=$(echo "$CHOSEN_UPPER_OBJ" | jq -r '.release_time | sub("\\+00:00$"; "Z") | fromdateiso8601') |
| 136 | + NEXT_RELEASE_OBJ=$(echo "$API_JSON" | jq -c --argjson chosen_time_num "$CHOSEN_TIME_NUM" 'map(select(.type == "release" and (.release_time | sub("\\+00:00$"; "Z") | fromdateiso8601 > $chosen_time_num))) | sort_by(.release_time | sub("\\+00:00$"; "Z") | fromdateiso8601) | .[0]') |
| 137 | + NEXT_RELEASE=$(echo "$NEXT_RELEASE_OBJ" | jq -r '.id') |
| 138 | +
|
| 139 | + if [ -n "$NEXT_RELEASE" ] && [ "$NEXT_RELEASE" != "null" ]; then |
| 140 | + # Next release version is available, use it as the upper bound |
| 141 | + MACHINE_RANGE_FABRIC=">=${MC_LOWER} <${NEXT_RELEASE}" |
| 142 | + MACHINE_RANGE_FORGE="[${MC_LOWER},${NEXT_RELEASE})" |
| 143 | + MACHINE_RANGE_NEOFORGE="[1.20.5,${NEXT_RELEASE})" |
| 144 | + CMD_VALIDATOR_MC=$(echo "$NEXT_RELEASE" | awk -F. '{print $1"."$2}') |
| 145 | +
|
| 146 | + echo "Next release version found: $NEXT_RELEASE" |
| 147 | + else |
| 148 | + # Next release version is not available, omit the upper bound for Fabric and (Neo)Forge |
| 149 | + MACHINE_RANGE_FABRIC=">=${MC_LOWER}" |
| 150 | + MACHINE_RANGE_FORGE="[${MC_LOWER},)" |
| 151 | + MACHINE_RANGE_NEOFORGE="[1.20.5,)" |
| 152 | +
|
| 153 | + # Pick the most recent release that is less than or equal to the chosen upper's release_time for mecha validation |
| 154 | + CMD_VALIDATOR_OBJ=$(echo "$API_JSON" | jq -c --argjson chosen_time_num "$CHOSEN_TIME_NUM" 'map(select(.type == "release" and (.release_time | sub("\\+00:00$"; "Z") | fromdateiso8601 <= $chosen_time_num))) | sort_by(.release_time | sub("\\+00:00$"; "Z") | fromdateiso8601) | reverse | .[0]') |
| 155 | + CMD_VALIDATOR_MC=$(echo "$CMD_VALIDATOR_OBJ" | jq -r '.id' | awk -F. '{print $1"."$2}') |
| 156 | + # Use fallback upper if no release version is found |
| 157 | + if [ "$CMD_VALIDATOR_MC" = "null" ]; then |
| 158 | + CMD_VALIDATOR_MC=$(echo "${FALLBACK_MC_UPPER}" | awk -F. '{print $1"."$2}') |
| 159 | + fi |
| 160 | +
|
| 161 | + echo "No next release version found, using lower bound $MC_LOWER for mod version ranges and $CMD_VALIDATOR_MC for command validation" |
| 162 | + fi |
| 163 | +
|
| 164 | + else |
| 165 | + # Exact upper is a release, use it as the upper bound |
| 166 | + MACHINE_RANGE_FABRIC=">=${MC_LOWER} <=${MC_UPPER}" |
| 167 | + MACHINE_RANGE_FORGE="[${MC_LOWER},${MC_UPPER}]" |
| 168 | + MACHINE_RANGE_NEOFORGE="[1.20.5,${MC_UPPER}]" |
| 169 | + CMD_VALIDATOR_MC=$(echo "$MC_UPPER" | awk -F. '{print $1"."$2}') |
| 170 | + fi |
| 171 | +
|
| 172 | + # Use exact values for human-readable version range |
| 173 | + MC_HUMAN_VERSION_RANGE="${MC_LOWER}-${MC_UPPER}" |
| 174 | + fi |
| 175 | + echo "::endgroup::" |
| 176 | + fi |
| 177 | +
|
| 178 | + echo "MC_LOWER=$MC_LOWER" >> $GITHUB_ENV |
| 179 | + echo "MC_UPPER=$MC_UPPER" >> $GITHUB_ENV |
| 180 | + echo "MC_HUMAN_VERSION_RANGE=$MC_HUMAN_VERSION_RANGE" >> $GITHUB_ENV |
| 181 | + echo "MACHINE_RANGE_FABRIC=$MACHINE_RANGE_FABRIC" >> $GITHUB_ENV |
| 182 | + echo "MACHINE_RANGE_FORGE=$MACHINE_RANGE_FORGE" >> $GITHUB_ENV |
| 183 | + echo "MACHINE_RANGE_NEOFORGE=$MACHINE_RANGE_NEOFORGE" >> $GITHUB_ENV |
| 184 | + echo "CMD_VALIDATOR_MC=$CMD_VALIDATOR_MC" >> $GITHUB_ENV |
| 185 | + echo "Determined versions:" |
| 186 | + echo " Human range: $MC_HUMAN_VERSION_RANGE" |
| 187 | + echo " Fabric range: $MACHINE_RANGE_FABRIC" |
| 188 | + echo " Forge range: $MACHINE_RANGE_FORGE" |
| 189 | + echo " NeoForge range: $MACHINE_RANGE_NEOFORGE" |
| 190 | + echo " Validator version: $CMD_VALIDATOR_MC" |
| 191 | +
|
| 192 | + # Optionally validate commands |
| 193 | + - name: Validate commands |
| 194 | + if: ${{ github.event.inputs.skip_check != 'true' }} |
| 195 | + uses: mcbeet/check-commands@v1 |
| 196 | + with: |
| 197 | + source: . |
| 198 | + stats: true |
| 199 | + minecraft: ${{ env.CMD_VALIDATOR_MC }} |
| 200 | + |
| 201 | + # Update version info in files via find/replace actions |
| 202 | + - name: Replace uninstall file name |
| 203 | + uses: jacobtomlinson/gha-find-replace@v3 |
| 204 | + with: |
| 205 | + find: "${file_name}" |
| 206 | + replace: "${{ github.repository }}-v${{ env.TAG }}-mc${{ env.MC_HUMAN_VERSION_RANGE }}-datapack.zip" |
| 207 | + regex: false |
| 208 | + include: "**uninstall.mcfunction" |
| 209 | + - name: Set data pack version |
| 210 | + uses: jacobtomlinson/gha-find-replace@v3 |
| 211 | + with: |
| 212 | + find: "${version}" |
| 213 | + replace: "${{ env.TAG }}" |
| 214 | + regex: false |
| 215 | + - name: Set supported Minecraft version range (Human-readable) |
| 216 | + uses: jacobtomlinson/gha-find-replace@v3 |
| 217 | + with: |
| 218 | + find: "${mc_human_version_range}" |
| 219 | + replace: "${{ env.MC_HUMAN_VERSION_RANGE }}" |
| 220 | + regex: false |
| 221 | + - name: Set supported Minecraft version range (Fabric) |
| 222 | + uses: jacobtomlinson/gha-find-replace@v3 |
| 223 | + with: |
| 224 | + find: "${mc_version_range_fabric}" |
| 225 | + replace: "${{ env.MACHINE_RANGE_FABRIC }}" |
| 226 | + regex: false |
| 227 | + - name: Set supported Minecraft version range (Forge) |
| 228 | + uses: jacobtomlinson/gha-find-replace@v3 |
| 229 | + with: |
| 230 | + find: "${mc_version_range_forge}" |
| 231 | + replace: "${{ env.MACHINE_RANGE_FORGE }}" |
| 232 | + regex: false |
| 233 | + - name: Set supported Minecraft version range (NeoForge) |
| 234 | + uses: jacobtomlinson/gha-find-replace@v3 |
| 235 | + with: |
| 236 | + find: "${mc_version_range_neoforge}" |
| 237 | + replace: "${{ env.MACHINE_RANGE_NEOFORGE }}" |
| 238 | + regex: false |
| 239 | + |
| 240 | + # Check for existence of directories |
| 241 | + - name: Check for data pack folder |
| 242 | + id: check_datapack |
| 243 | + uses: andstor/file-existence-action@v3 |
| 244 | + with: |
| 245 | + files: "data" |
| 246 | + - name: Check for mod folder |
| 247 | + id: check_mod |
| 248 | + uses: andstor/file-existence-action@v3 |
| 249 | + with: |
| 250 | + files: "META-INF, net, fabric.mod.json, assets" |
| 251 | + - name: Check for resource pack folder |
| 252 | + id: check_assets |
| 253 | + uses: andstor/file-existence-action@v3 |
| 254 | + with: |
| 255 | + files: "assets/minecraft" |
| 256 | + |
| 257 | + # Package files (zip/jar) conditionally if folder exists |
| 258 | + - name: Create data pack zip file |
| 259 | + if: steps.check_datapack.outputs.files_exists == 'true' |
| 260 | + shell: bash |
| 261 | + run: | |
| 262 | + zip "${{ github.event.repository.name }}-v${{ env.TAG }}-mc${{ env.MC_HUMAN_VERSION_RANGE }}-datapack.zip" -r . -x "assets/**" "net/**" "META-INF/**" fabric.mod.json "unused/**" "src/**" "wiki/**" CHANGES.md ".*" |
| 263 | + - name: Create mod jar file |
| 264 | + if: steps.check_mod.outputs.files_exists == 'true' |
| 265 | + shell: bash |
| 266 | + # Use jar -cvf "${{ github.event.repository.name }}-v${{ env.TAG }}-mc${{ env.MC_HUMAN_VERSION_RANGE }}-mod.jar" -C . META-INF net fabric.mod.json assets data? |
| 267 | + run: | |
| 268 | + zip "${{ github.event.repository.name }}-v${{ env.TAG }}-mc${{ env.MC_HUMAN_VERSION_RANGE }}-mod.jar" -r . -x "unused/**" "src/**" "wiki/**" CHANGES.md ".*" "${{ github.event.repository.name }}-v${{ env.TAG }}-mc${{ env.MC_HUMAN_VERSION_RANGE }}-datapack.zip" |
| 269 | + - name: Create resource pack zip file |
| 270 | + if: steps.check_assets.outputs.files_exists == 'true' |
| 271 | + shell: bash |
| 272 | + run: | |
| 273 | + zip "${{ github.event.repository.name }}-v${{ env.TAG }}-mc${{ env.MC_HUMAN_VERSION_RANGE }}-resourcepack.zip" -r . -x "data/**" "net/**" "META-INF/**" fabric.mod.json "unused/**" "src/**" "wiki/**" CHANGES.md ".*" "${{ github.event.repository.name }}-v${{ env.TAG }}-mc${{ env.MC_HUMAN_VERSION_RANGE }}-datapack.zip" "${{ github.event.repository.name }}-v${{ env.TAG }}-mc${{ env.MC_HUMAN_VERSION_RANGE }}-mod.jar" |
| 274 | +
|
| 275 | + # Capture and upload artifacts |
| 276 | + - name: Upload artifacts |
| 277 | + id: upload |
| 278 | + uses: actions/upload-artifact@v4 |
| 279 | + with: |
| 280 | + name: "${{ github.event.repository.name }} v${{ env.TAG }} Development Builds (Unzip Me)" |
| 281 | + path: | |
| 282 | + ./${{ github.event.repository.name }}-v${{ env.TAG }}-mc${{ env.MC_HUMAN_VERSION_RANGE }}-datapack.zip |
| 283 | + ./${{ github.event.repository.name }}-v${{ env.TAG }}-mc${{ env.MC_HUMAN_VERSION_RANGE }}-mod.jar |
| 284 | + ./${{ github.event.repository.name }}-v${{ env.TAG }}-mc${{ env.MC_HUMAN_VERSION_RANGE }}-resourcepack.zip |
| 285 | +
|
| 286 | + # Build summary |
| 287 | + - name: Set job summary |
| 288 | + if: always() |
| 289 | + shell: bash |
| 290 | + run: | |
| 291 | + echo "Building summary..." |
| 292 | + if [ "${{ job.status }}" == "success" ]; then |
| 293 | + cat << EOF >> $GITHUB_STEP_SUMMARY |
| 294 | + <picture> |
| 295 | + <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Mqxx/GitHub-Markdown/main/blockquotes/badge/light-theme/success.svg"> |
| 296 | + <img alt="✅ Success" src="https://raw.githubusercontent.com/Mqxx/GitHub-Markdown/main/blockquotes/badge/dark-theme/success.svg"> |
| 297 | + </picture><br> |
| 298 | +
|
| 299 | + **${{ github.event.repository.name }} v${{ env.TAG }}** for **Minecraft ${{ env.MC_HUMAN_VERSION_RANGE }}** built successfully! [See changes since last release](https://github.com/${{ github.repository }}/blob/${{ github.ref_name }}/CHANGES.md). |
| 300 | +
|
| 301 | + [Download via nightly.link](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}) |
| 302 | +
|
| 303 | + Make sure to always back up your world before using development builds! |
| 304 | + EOF |
| 305 | +
|
| 306 | + else |
| 307 | + cat << EOF >> $GITHUB_STEP_SUMMARY |
| 308 | + <picture> |
| 309 | + <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Mqxx/GitHub-Markdown/main/blockquotes/badge/light-theme/error.svg"> |
| 310 | + <img alt="❌ Error" src="https://raw.githubusercontent.com/Mqxx/GitHub-Markdown/main/blockquotes/badge/dark-theme/error.svg"> |
| 311 | + </picture><br> |
| 312 | +
|
| 313 | + The build v${{ env.TAG }} for mc${{ env.MC_HUMAN_VERSION_RANGE }} failed! Please check the action logs for details. |
| 314 | + EOF |
| 315 | + fi |
0 commit comments