diff --git a/.github/workflows/publish-tdlm.yml b/.github/workflows/publish-tdlm.yml new file mode 100644 index 0000000000..d186be33bc --- /dev/null +++ b/.github/workflows/publish-tdlm.yml @@ -0,0 +1,249 @@ +name: Publish TDLM launcher + +# Builds HMCL.exe / HMCL.jar and publishes them for TDLMPack / players: +# 1. Gitea Release assets (versioned download page) +# 2. Gitea generic package tdlm-hmcl/latest (stable curl URLs) +# +# Mirrors TDLM/TDLMPack's pack.yml publish style (nest API host; Cloudflare +# in front of git.potatolab.uk blocks Python urllib for large uploads). +# +# Auth (same name as TDLMPack pack.yml): +# secrets.TDLM_PACK_TOKEN — Gitea API token with package + release write. +# Prefer an *org* secret (like MAVEN_USER/MAVEN_PASSWORD) so HMCL inherits it; +# today it exists only as a TDLMPack *repo* secret unless promoted. +# Falls back to github.token when TDLM_PACK_TOKEN is unset (same-repo release; +# org generic packages may still need the pack token). + +on: + workflow_dispatch: + inputs: + release_tag: + description: "Release tag (also used as the release title). Example: 2026.11.08 or v3.dev-abc1234" + required: true + type: string + # Gitea records a push run whenever this file changes; without a push job the run + # fails with 0 jobs. Keep a no-op so the check is green; real publish stays dispatch-only. + push: + paths: + - '.github/workflows/publish-tdlm.yml' + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + noop: + if: ${{ github.event_name == 'push' }} + runs-on: ubuntu-latest + steps: + - run: echo "publish-tdlm is workflow_dispatch-only; push no-op ok" + + publish: + if: ${{ github.event_name == 'workflow_dispatch' }} + runs-on: ubuntu-latest + timeout-minutes: 90 + steps: + - uses: actions/checkout@v6 + + # Same caching pattern as gradle.yml / TDLM ForestryMC+IC2 setup-build. + - name: Set up JDK 17 + JavaFX + uses: actions/setup-java@v6 + with: + distribution: 'liberica' + java-version: '17' + java-package: 'jdk+fx' + cache-jdk: true + + - name: Setup Gradle + uses: actions/gradle-actions/setup-gradle@v6 + with: + cache-cleanup: never + cache-provider: ${{ github.server_url == 'https://github.com' && 'basic' || 'external' }} + + # Wrapper only — do not cache ~/.gradle/caches (org siblings don't; ~4GB killed Docker). + - name: Cache Gradle wrapper + uses: actions/cache@v4 + with: + path: | + ~/.gradle/wrapper + .gradle/wrapper-neo + key: gradle-wrapper-${{ runner.os }}-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/wrapper/GradleWrapperNeo.java') }} + + - name: Cache apt packages + uses: actions/cache@v4 + with: + path: ~/apt-cache + key: apt-javafx-xvfb-${{ runner.os }}-${{ runner.arch }}-1 + + - name: Install Xvfb and JavaFX GTK deps + run: | + pkgs='xvfb libgtk-3-0 libxtst6 libxxf86vm1 libgl1 libxi6 libx11-6 libxext6 libxrender1' + sudo mkdir -p /var/cache/apt/archives/partial + if [ -d "${HOME}/apt-cache" ] && [ -n "$(ls -A "${HOME}/apt-cache" 2>/dev/null)" ]; then + sudo cp -a "${HOME}/apt-cache/." /var/cache/apt/archives/ + fi + sudo apt-get install -y --no-install-recommends $pkgs \ + || { sudo apt-get update && sudo apt-get install -y --no-install-recommends $pkgs; } + mkdir -p "${HOME}/apt-cache" + cp -a /var/cache/apt/archives/*.deb "${HOME}/apt-cache/" 2>/dev/null || true + + + - name: Build executables + run: xvfb-run --auto-servernum ./gradlew :HMCL:makeExecutables --no-daemon --parallel + env: + JAVA_TOOL_OPTIONS: -Djava.awt.headless=true -Dprism.order=sw + VERSION_TYPE: unofficial + MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }} + CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }} + + - name: Stage stable launcher names + run: | + set -euo pipefail + mkdir -p build/tdlm-hmcl + jar="$(ls -1 HMCL/build/libs/HMCL-*.jar | grep -v '\.sha' | head -n1)" + exe="$(ls -1 HMCL/build/libs/HMCL-*.exe | grep -v '\.sha' | head -n1)" + test -n "$jar" && test -n "$exe" + cp -f "$jar" build/tdlm-hmcl/HMCL.jar + cp -f "$exe" build/tdlm-hmcl/HMCL.exe + # Prefer Gradle-produced checksums when present; otherwise compute. + if [ -f "${jar}.sha256" ]; then cp -f "${jar}.sha256" build/tdlm-hmcl/HMCL.jar.sha256 + else sha256sum build/tdlm-hmcl/HMCL.jar | awk '{print $1}' > build/tdlm-hmcl/HMCL.jar.sha256 + fi + if [ -f "${exe}.sha256" ]; then cp -f "${exe}.sha256" build/tdlm-hmcl/HMCL.exe.sha256 + else sha256sum build/tdlm-hmcl/HMCL.exe | awk '{print $1}' > build/tdlm-hmcl/HMCL.exe.sha256 + fi + echo "STAGED_JAR=$jar" >> "$GITHUB_ENV" + echo "STAGED_EXE=$exe" >> "$GITHUB_ENV" + ls -la build/tdlm-hmcl + + # Stable player/CI channel: overwritten in place under tdlm-hmcl/latest. + - name: Publish generic package tdlm-hmcl/latest + env: + # Same secret name as TDLM/TDLMPack pack.yml (org or repo). Not MAVEN_*. + TDLM_PACK_TOKEN: ${{ secrets.TDLM_PACK_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} + GITEA_PACKAGE_ROOT: https://git.nest.potatolab.uk:8443/api/packages/TDLM/generic/tdlm-hmcl/latest + run: | + python3 - <<'PY' + import os, pathlib, urllib.error, urllib.parse, urllib.request + token = (os.environ.get("TDLM_PACK_TOKEN") or os.environ.get("GITHUB_TOKEN") or "").strip() + token_src = "TDLM_PACK_TOKEN" if os.environ.get("TDLM_PACK_TOKEN", "").strip() else "github.token" + if not token: + raise SystemExit( + "Need secrets.TDLM_PACK_TOKEN (promote TDLMPack's repo secret to org, like MAVEN_*) " + "or a usable github.token to publish tdlm-hmcl") + print("auth=" + token_src) + upload_root = os.environ["GITEA_PACKAGE_ROOT"].rstrip("/") + root = pathlib.Path("build/tdlm-hmcl") + + def put(name: str, data: bytes): + url = upload_root + "/" + urllib.parse.quote(name, safe="") + request = urllib.request.Request(url, data=data, method="PUT") + request.add_header("Authorization", "token " + token) + request.add_header("Content-Type", "application/octet-stream") + request.add_header("User-Agent", "tdlm-hmcl-publish") + try: + with urllib.request.urlopen(request, timeout=1800) as response: + response.read() + except urllib.error.HTTPError as exc: + raise SystemExit("HTTP %s uploading %s: %s" % (exc.code, name, exc.read()[:500])) + + for path in sorted(root.iterdir()): + if path.is_file(): + put(path.name, path.read_bytes()) + print("PACKAGE " + path.name) + print("generic=" + upload_root) + PY + + # Gitea release (versioned). Nest API avoids Cloudflare blocking large urllib uploads. + - name: Publish Gitea release + env: + RELEASE_TAG: ${{ inputs.release_tag }} + TDLM_PACK_TOKEN: ${{ secrets.TDLM_PACK_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} + GITEA_API_URL: https://git.nest.potatolab.uk:8443/api/v1 + GITEA_PACKAGE_ROOT: https://git.nest.potatolab.uk:8443/api/packages/TDLM/generic/tdlm-hmcl/latest + TARGET_COMMIT: ${{ github.sha }} + run: | + python3 - <<'PY' + import json, os, urllib.error, urllib.parse, urllib.request + tag = os.environ["RELEASE_TAG"].strip() + token = (os.environ.get("TDLM_PACK_TOKEN") or os.environ.get("GITHUB_TOKEN") or "").strip() + token_src = "TDLM_PACK_TOKEN" if os.environ.get("TDLM_PACK_TOKEN", "").strip() else "github.token" + api = os.environ["GITEA_API_URL"].rstrip("/") + "/repos/TDLM/HMCL" + package_root = os.environ["GITEA_PACKAGE_ROOT"].rstrip("/") + target = os.environ.get("TARGET_COMMIT", "main").strip() or "main" + if not token: + raise SystemExit( + "Need secrets.TDLM_PACK_TOKEN (org-preferred, same as TDLMPack) or github.token") + print("auth=" + token_src) + if not tag: + raise SystemExit("release_tag is empty") + + def call(method, path, data=None, content_type="application/json", timeout=90): + request = urllib.request.Request(api + path, data=data, method=method) + request.add_header("Authorization", "token " + token) + request.add_header("Accept", "application/json") + request.add_header("User-Agent", "tdlm-hmcl-release") + if data is not None and content_type: + request.add_header("Content-Type", content_type) + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + body = response.read() + return json.loads(body) if body else {} + except urllib.error.HTTPError as exc: + if exc.code == 404: + return None + detail = exc.read()[:500] + raise SystemExit("HTTP %s for %s: %s" % (exc.code, path, detail)) + + body = ( + "TDLM/HMCL launcher binaries for players and TDLMPack.\n\n" + "Stable generic (overwrite in place):\n" + "- %s/HMCL.exe\n" + "- %s/HMCL.jar\n\n" + "TDLMPack does not Maven-resolve this launcher; point the pack README / install docs at these URLs " + "instead of hand-copying exe/jar.\n" + "Client pack fileApi default: https://packages.dandanton.com/tdlm-client/latest\n" + "(Pack must publish/mirror the same layout; see docs/TDLM_CONSUME.md).\n" + ) % (package_root, package_root) + + quoted = urllib.parse.quote(tag, safe="") + release = call("GET", "/releases/tags/" + quoted) + if release and not release.get("draft"): + raise SystemExit("Release tag already exists: " + tag) + if not release: + release = call("POST", "/releases", json.dumps({ + "tag_name": tag, + "name": tag, + "target_commitish": target, + "draft": True, + "prerelease": False, + "body": body, + }).encode("utf-8")) + release_id = release["id"] + + staged = [ + ("HMCL.exe", "build/tdlm-hmcl/HMCL.exe"), + ("HMCL.jar", "build/tdlm-hmcl/HMCL.jar"), + ("HMCL.exe.sha256", "build/tdlm-hmcl/HMCL.exe.sha256"), + ("HMCL.jar.sha256", "build/tdlm-hmcl/HMCL.jar.sha256"), + ] + for name, path in staged: + if not os.path.exists(path): + raise SystemExit("missing " + path) + for old in call("GET", "/releases/%s/assets" % release_id) or []: + if old.get("name") == name: + call("DELETE", "/releases/%s/assets/%s" % (release_id, old["id"])) + with open(path, "rb") as asset: + call( + "POST", + "/releases/%s/assets?name=%s" % (release_id, urllib.parse.quote(name)), + asset.read(), + "application/octet-stream", + 1800, + ) + print("RELEASE asset " + name) + call("PATCH", "/releases/%s" % release_id, json.dumps({"draft": False}).encode("utf-8")) + print(release.get("html_url", "Release created")) + PY diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java index a1cb3d4438..5032e7a82a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java @@ -141,9 +141,9 @@ public final class Metadata { return null; } - /// Default client Server Modpack fileApi published by TDLMPack (Gitea generic package `latest`). + /// Default client Server Modpack fileApi (CDN mirror of Pack's `tdlm-client/latest` layout). public static final String DEFAULT_CLIENT_FILE_API = - "https://git.nest.potatolab.uk:8443/api/packages/TDLM/generic/tdlm-client/latest"; + "https://packages.dandanton.com/tdlm-client/latest"; /// Stable client update root (`server-manifest.json` and `overrides/` live under this URL). /// diff --git a/docs/TDLM_CONSUME.md b/docs/TDLM_CONSUME.md new file mode 100644 index 0000000000..d3ec69220e --- /dev/null +++ b/docs/TDLM_CONSUME.md @@ -0,0 +1,71 @@ +# TDLMPack consume path for TDLM/HMCL + +TDLMPack does **not** pull this launcher via Maven, a version pin, or a pack-build download step. +The pack build publishes the HMCL Server Modpack client (`server-manifest.json` + `overrides/`) +under the `tdlm-client/latest` layout. Players install the launcher separately. + +## After this repo’s publish workflow + +Run **Actions → Publish TDLM launcher → Run workflow** with a new `release_tag`. + +### Secrets (sibling pattern) + +| Name | Where it lives today | Used by | +|---|---|---| +| `MAVEN_USER` / `MAVEN_PASSWORD` | **Org** secrets | Stratum / mod Maven publish — **not** used for launcher binaries | +| `TDLM_PACK_TOKEN` | **TDLMPack repo** secret only (not org) | TDLMPack `pack.yml` Gitea package + release uploads | +| `github.token` | Actions runtime | HMCL publish fallback when `TDLM_PACK_TOKEN` is unset | + +HMCL `publish-tdlm.yml` uses the **same name** as TDLMPack: `secrets.TDLM_PACK_TOKEN`, then falls back to `github.token`. +Org Maven secrets cannot publish Gitea generic packages / releases. + +**Recommended:** promote `TDLM_PACK_TOKEN` to an **org** secret (like `MAVEN_*`) so HMCL inherits it with no new repo secret. Until then, either copy it onto `TDLM/HMCL` or rely on `github.token` (same-repo release usually works; org `tdlm-hmcl` package write may still need the pack token). + +### 1. Stable generic URLs (preferred for README / curl / CI) + +Overwrite-in-place, same host style as the client fileApi: + +```text +https://git.nest.potatolab.uk:8443/api/packages/TDLM/generic/tdlm-hmcl/latest/HMCL.exe +https://git.nest.potatolab.uk:8443/api/packages/TDLM/generic/tdlm-hmcl/latest/HMCL.jar +``` + +Checksums (when published): `HMCL.exe.sha256`, `HMCL.jar.sha256` next to those names. + +### 2. Gitea Release (versioned page) + +```text +https://git.potatolab.uk/TDLM/HMCL/releases/latest +``` + +Assets on each tag: `HMCL.exe`, `HMCL.jar`, plus `.sha256` files. +(Use the nest API host for scripted uploads; public download links on `git.potatolab.uk` are fine for browsers.) + +### 3. What TDLMPack should change + +In `TDLM/TDLMPack` README §启动, replace the repo-only “安装 TDLM/HMCL” step with one of the URLs above. +**Do not** add HMCL as a Gradle/Maven dependency of the pack — pack CI stays pack-only. + +### 4. Client pack fileApi (CDN) + +HMCL default (`Metadata.DEFAULT_CLIENT_FILE_API`): + +```text +https://packages.dandanton.com/tdlm-client/latest +``` + +First install / updates fetch `{fileApi}/server-manifest.json` (and `overrides/**` under that root). +Override at runtime: `-Dhmcl.client.fileApi=…` or jar attribute `hmcl.client.fileApi`. + +**TDLMPack must use the same URL** so `server-manifest.json` embeds it: + +```text +TDLM_CLIENT_FILE_API=https://packages.dandanton.com/tdlm-client/latest +``` + +Set that as the Gitea Actions **variable** on `TDLM/TDLMPack` (pack.yml already passes `vars.TDLM_CLIENT_FILE_API`). + +**Publish caveat:** Pack’s current workflow PUTs the client tree to whatever `TDLM_CLIENT_FILE_API` is (upload root = fileApi). Today that defaults to Gitea generic +`https://git.nest.potatolab.uk:8443/api/packages/TDLM/generic/tdlm-client/latest`. +If you only change the HMCL default to the CDN and leave Pack publishing to Gitea, players will 404 unless you **mirror/sync** Gitea → `packages.dandanton.com/tdlm-client/latest`. +If you set `TDLM_CLIENT_FILE_API` to the CDN URL, Pack CI must be able to PUT to that CDN (or you must split “upload origin” from “public fileApi” in Pack — not implemented today). Do not point HMCL at the CDN without one of those two ops paths.