diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml index 1240f68..9d5a76c 100644 --- a/.forgejo/workflows/build.yaml +++ b/.forgejo/workflows/build.yaml @@ -42,6 +42,30 @@ jobs: path: | ./build/libs/*.jar !./build/libs/*-sources.jar + - name: Make publish CFG + run: | + echo '{ ' > publish.config.json + echo ' "version": "1.0.2", ' >> publish.config.json + echo ' "featured": false, ' >> publish.config.json + echo ' "releaseChannel": "dev", ' >> publish.config.json + echo ' "loaders": ["quilt"], ' >> publish.config.json + echo ' "gameVersions": { ' >> publish.config.json + echo ' "file": "src/main/resources/quilt.mod.json", ' >> publish.config.json + echo ' "key": "quilt_loader.depends.1.versions" ' >> publish.config.json + echo ' }, ' >> publish.config.json + echo ' "files": { ' >> publish.config.json + echo ' "primary": "build/libs/CookieServerUtils-1.0.2.jar" ' >> publish.config.json + echo ' }, ' >> publish.config.json + echo ' "repoApi": "", ' >> publish.config.json + echo ' "gitReleaseUrl": "/releases/latest", ' >> publish.config.json + echo ' "crmm": { ' >> publish.config.json + echo ' "authToken": "${{ CRMM_TOKEN_ADMKAWL }}", ' >> publish.config.json + echo ' "projectId": "cookie-server-utils" ' >> publish.config.json + echo ' } ' >> publish.config.json + echo '} ' >> publish.config.json + - name: Make publish CFG + run: | + apt-get install -y --no-install-recommends nodejs - run: echo "🍏 This job's status is ${{ job.status }}." diff --git a/publish.js b/publish.js new file mode 100644 index 0000000..a679049 --- /dev/null +++ b/publish.js @@ -0,0 +1,284 @@ +import { readFile } from "node:fs/promises"; + +const CONFIG_FILE = "publish.config.json"; +const CRMM_API_URL = "https://api.crmm.tech/api"; +const includeGameVersionTypes = [ + "release", + "beta", + "alpha", + // "pre-release", + // "snapshot" +]; + +async function main() { + const configData = await loadConfigData(); + // console.log({ ...configData, authToken: "REDACTED" }); + + console.log("Preparing form data..."); + const formData = await prepareMultipartFormData(configData); + + console.log("Uploading version..."); + const response = await fetch( + `${CRMM_API_URL}/project/${configData.projectId}/version`, + { + method: "POST", + body: formData, + headers: { + Cookie: `auth-token=${configData.authToken}`, + Origin: "https://crmm.tech", + Referer: "https://crmm.tech/", + }, + }, + ); + + const data = await response.json(); + console.log(data); +} + +await main(); + +async function prepareMultipartFormData(data) { + const formData = new FormData(); + formData.append("title", data.title); + formData.append("changelog", data.changelog || ""); + formData.append("featured", `${data.featured === true}`); + formData.append("releaseChannel", data.releaseChannel); + formData.append("versionNumber", data.version); + formData.append("loaders", JSON.stringify(data.loaders || [])); + formData.append("gameVersions", JSON.stringify(data.gameVersions)); + formData.append("dependencies", JSON.stringify(data.dependencies || [])); + formData.append("primaryFile", data.primaryFile); + for (const additionalFile of data.additionalFiles || []) { + formData.append("additionalFiles", additionalFile); + } + + return formData; +} + +async function loadConfigData() { + console.log("Loading config..."); + const config = await getConfig(); + + // Version + const version = config.version; + if (!version) { + throw new Error("Version is required"); + } + const title = config.title || version; + + const featured = config.featured === true; + const releaseChannel = config.releaseChannel || "release"; + const loaders = config.loaders || []; + const gameVersions = await getGameVersions(config.gameVersions); + if (!gameVersions) { + throw new Error("Game versions are required"); + } + + // Changelog + const repoUrl = config.repoApi; + const releasePath = config.gitReleaseUrl; + + const releaseUrl = releasePath.startsWith("http") + ? releasePath + : `${repoUrl}${releasePath}`; + const changelog = await getVersionChangelog(releaseUrl); + + // Files + const primaryFilePath = config.files?.primary; + if (!primaryFilePath) { + throw new Error("Primary file path is required"); + } + + const primaryFile = await getFile(primaryFilePath); + const additionalFiles = await getFiles(config.files?.additional || []); + + // Crmm API data + const authToken = config?.crmm?.authToken; + if (!authToken) throw new Error("Auth token is required"); + + const projectId = config?.crmm?.projectId; + if (!projectId) throw new Error("Project ID is required"); + + return { + title, + version, + featured, + releaseChannel, + loaders, + gameVersions, + changelog, + primaryFile, + additionalFiles, + authToken, + projectId, + }; +} + +async function getGameVersions(source) { + // The API returns sorted versions list in descending order + const res = await fetch(`${CRMM_API_URL}/tags/game-versions`); + const availableVersions = await res.json(); + + if (!availableVersions || !Array.isArray(availableVersions)) + throw new Error("Failed to fetch game versions"); + + const filteredAvailableVersions = availableVersions + .filter((version) => includeGameVersionTypes.includes(version.releaseType)) + .map((version) => version.value); + + let gameVersions_raw = []; + + if (Array.isArray(source)) { + gameVersions_raw = source; + } else if (typeof source === "string") { + gameVersions_raw = [source]; + } else { + const gameVersionSourceKey = source.key; + if (!gameVersionSourceKey) + throw new Error("Missing field 'key' in 'gameVersions'"); + const keys = gameVersionSourceKey.split("."); + + const gameVersionSourceFile = await getFileContents(source.file, "utf-8"); + const gameVersionSourceJson = JSON.parse(gameVersionSourceFile); + + gameVersions_raw = getObjValue(gameVersionSourceJson, keys); + if (!gameVersions_raw) + throw new Error( + `Failed to get game versions from source file using key '${gameVersionSourceKey}'`, + ); + + if (typeof gameVersions_raw === "string") gameVersions_raw = [gameVersions_raw]; + } + + const parsedGameVersions = new Set(); + for (const version of gameVersions_raw) { + const parsedList = parseVersion(version, filteredAvailableVersions); + if (!parsedList) continue; + + for (const v of parsedList) { + parsedGameVersions.add(v); + } + } + + const gameVersionsList = sortVersions( + Array.from(parsedGameVersions), + filteredAvailableVersions, + ); + if (gameVersionsList.length === 0) throw new Error("No valid game versions"); + + return gameVersionsList; +} + +async function getVersionChangelog(url) { + console.log("Fetching changelog..."); + let fetchUrl = url; + if (url.includes("/tag/")) { + fetchUrl = url.replace("/tag/", "/tags/"); + } + + const res = await fetch(fetchUrl); + const data = await res.json(); + + return data?.body || ""; +} + +async function getConfig() { + const file = await getFileContents(CONFIG_FILE, "utf-8"); + const json = JSON.parse(file); + + return json; +} + +async function getFiles(paths) { + const files = await Promise.all(paths.map((path) => getFile(path))); + return files; +} + +async function getFile(path) { + const fileContents = await getFileContents(path); + const fileName = path.split("/").pop(); + + return new File([fileContents], fileName); +} + +async function getFileContents(path, encoding) { + const fileContents = await readFile(path, { encoding: encoding }); + return fileContents; +} + +function parseVersion(version, refList) { + let versionStr = version; + if (versionStr.startsWith("v")) versionStr = versionStr.slice(1); + + // If the version string doesn't include any operators, return it as is + if ( + !versionStr.includes("=") && + !versionStr.includes("<") && + !versionStr.includes(">") + ) { + return [versionStr]; + } + + // Throw error if the version string doesn't start with a valid operator but includes one + if ( + !versionStr.startsWith("=") && + !versionStr.startsWith(">=") && + !versionStr.startsWith("<=") && + !versionStr.startsWith(">") && + !versionStr.startsWith("<") + ) { + throw new Error(`Cannot parse version string: '${versionStr}'`); + } + + const parsedVersions = []; + if (versionStr.startsWith("<")) { + versionStr = versionStr.slice(1); + if (versionStr.startsWith("=")) { + versionStr = versionStr.slice(1); + parsedVersions.push(versionStr); + } + + const refIndex = refList.indexOf(versionStr); + if (refIndex === -1) return null; + + parsedVersions.push(...refList.slice(refIndex + 1)); + } + + if (versionStr.startsWith(">")) { + versionStr = versionStr.slice(1); + if (versionStr.startsWith("=")) { + versionStr = versionStr.slice(1); + parsedVersions.push(versionStr); + } + + const refIndex = refList.indexOf(versionStr); + if (refIndex === -1) return null; + + parsedVersions.push(...refList.slice(0, refIndex)); + } + + if (versionStr.startsWith("=")) { + versionStr = versionStr.slice(1); + parsedVersions.push(versionStr); + } + + return parsedVersions; +} + +function sortVersions(list, refList) { + const sorted = []; + for (const version of refList) { + if (list.includes(version)) sorted.push(version); + } + + return sorted; +} + +function getObjValue(obj, keys) { + let curr = obj; + for (const key of keys) { + curr = curr[key]; + } + + return curr; +} \ No newline at end of file