From 41151d47f026719a776c9ff524a0d73711fc0bc1 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sat, 1 Nov 2025 16:10:41 +0000 Subject: [PATCH] Add publish workflow (#798) * Change build-mod to belong to packages * Add publish script and workflow * Fix typo in variable name --- .github/workflows/publish.yml | 48 ++++++++++++ clean.js | 6 +- package.json | 2 +- packages/host/package.json | 1 + packages/lib/build_mod.js | 16 +++- publish.js | 134 ++++++++++++++++++++++++++++++++++ 6 files changed, 201 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/publish.yml create mode 100644 publish.js diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..c72afbb1 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,48 @@ + +name: Publish + +on: + release: + types: + - released + +jobs: + npm: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 8 + - uses: actions/setup-node@v4 + with: + node-version: 22.x + registry-url: https://registry.npmjs.org + - run: pnpm install --no-frozen-lockfile + - run: pnpm -r --filter='./{plugins,packages}/*' publish + env: + NODE_AUTH_TOKEN: $${{ secrets.NPM_TOKEN }} + - uses: softprops/action-gh-release@v2 + with: + files: pnpm-lock.yaml + + factorio: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 8 + - uses: actions/setup-node@v4 + with: + node-version: 22.x + - run: pnpm install --no-frozen-lockfile + - run: pnpm -r --filter='./{plugins,packages}/*' run build-mod --output-dir ../../dist + - run: node ./publish.js factorio ./dist + env: + FACTORIO_TOKEN: ${{ secrets.FACTORIO_TOKEN }} + - uses: softprops/action-gh-release@v2 + with: + files: ./dist/* diff --git a/clean.js b/clean.js index 60d4aa04..8e15dcba 100644 --- a/clean.js +++ b/clean.js @@ -86,8 +86,8 @@ async function removeExecutionArtifacts() { async function main() { await yargs(process.argv.slice(2)) .scriptName("clean") - .usage("$0 [options]") - .option("dry", { + .option("dry-run", { + alias: "dry", nargs: 0, describe: "Will not delete any files", default: false, @@ -108,7 +108,7 @@ async function main() { DRY = argv.dry; removeBuildArtifacts(); }) - .demandCommand(1, "You need to specify a command to run") + .demandCommand(1, "A command must be specified") .strict() .parse(); } diff --git a/package.json b/package.json index 068ef25e..11d5cdbb 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "tsc --build", "watch": "tsc --build --watch", - "build-mod": "node ./packages/lib/build_mod.js --source-dir ./packages/host/lua/clusterio_lib", + "build-mod": "pnpm run -r build-mod --output-dir ../../dist", "test": "pnpm run build && mocha --enable-source-maps --check-leaks -R spec --recursive test \"plugins/*/test/**\" --exclude \"test/manual/**\"", "silent-test": "SILENT_TEST=y pnpm test", "fast-test": "FAST_TEST=y pnpm test", diff --git a/packages/host/package.json b/packages/host/package.json index 2808b282..15ee76f9 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -7,6 +7,7 @@ "main": "dist/node/index.js", "scripts": { "prepare": "tsc --build", + "build-mod": "node ../lib/build_mod.js --source-dir ./lua/clusterio_lib", "test": "echo \"Error: no test specified\" && exit 1" }, "bin": { diff --git a/packages/lib/build_mod.js b/packages/lib/build_mod.js index eee00d8d..30cc4326 100644 --- a/packages/lib/build_mod.js +++ b/packages/lib/build_mod.js @@ -115,9 +115,21 @@ async function main() { "clean": { describe: "Remove previous builds", type: "boolean", default: false }, "build": { describe: "Build mod(s)", type: "boolean", default: true }, "pack": { describe: "Pack into zip file", type: "boolean", default: true }, - "source-dir": { describe: "Path to mod source files", nargs: 1, type: "string", demandOption: true }, - "output-dir": { describe: "Path to output built mod(s)", nargs: 1, type: "string", default: "dist" }, "factorio-version": { describe: "Override factorio_version", type: "string" }, + "source-dir": { + describe: "Path to mod source files", + type: "string", + nargs: 1, + normalize: true, + demandOption: true, + }, + "output-dir": { + describe: "Path to output built mod(s)", + type: "string", + nargs: 1, + normalize: true, + default: "dist/factorio", + }, }) .strict() .argv diff --git a/publish.js b/publish.js new file mode 100644 index 00000000..447a1610 --- /dev/null +++ b/publish.js @@ -0,0 +1,134 @@ +/** + * Publish any mods that need to be published + */ + +"use strict"; +const FormData = require("form-data"); +const fs = require("fs-extra"); +const yargs = require("yargs"); + +let DRY = false; + +const modReleaseCache = new Map(); +/** + * Gets a list of all releases for a mod + */ +async function requestModReleases(mod) { + const cache = modReleaseCache.get(mod); + if (cache) { + return cache; + } + + // eslint-disable-next-line no-console + console.log(`Requesting current releases of ${mod}`); + + const response = await fetch(`https://mods.factorio.com/api/mods/${mod}`); + if (!response.ok) { + throw new Error(`Failed request to mod portal api: ${response.statusText}\n${await response.text()}`); + } + + const json = await response.json(); + modReleaseCache.set(mod, json.releases); + return json.releases; +} + +/** + * Upload a mod to the mod portal + */ +async function uploadModRelease(file, name) { + // eslint-disable-next-line no-console + console.log(`Uploading new release ${file}`); + if (DRY) { + return; + } + + const headers = { + Authorization: `Bearer ${process.env.FACTORIO_TOKEN}`, + }; + + // Start the upload process by requesting an upload url + const initFormData = new FormData(); + initFormData.append("mod", name); + + const initResponse = await fetch("https://mods.factorio.com/api/v2/mods/releases/init_upload", { + method: "POST", + headers: headers, + body: initFormData, + }); + if (!initResponse.ok) { + throw new Error(`Failed init mod upload: ${initResponse.statusText}\n${await initResponse.text()}`); + } + + // Upload the new version of the mod + const uploadUrl = (await initResponse.json()).upload_url; + const uploadFormData = new FormData(); + uploadFormData.append("file", fs.createReadStream(file)); + + const uploadResponse = await fetch(uploadUrl, { + method: "POST", + headers: headers, + body: uploadFormData, + }); + if (!uploadResponse.ok) { + throw new Error(`Failed mod upload: ${uploadResponse.statusText}\n${await uploadResponse.text()}`); + } +} + +const regexModZip = /(\w+)_\d+.\d+.\d+\.zip/; +/** + * Publishes all mods that require publishing + */ +async function publishMods(dir) { + const files = await fs.readdir(dir); + const mods = files + .map(file => regexModZip.exec(file)) + .filter(file => Boolean(file)); + + const uploading = []; + for (const mod of mods) { + const [file, name] = mod; + const releases = await requestModReleases(name); + if (!releases.some(release => release.file_name === file)) { + uploading.push(uploadModRelease(file, name)); + } else { + // eslint-disable-next-line no-console + console.log(`Skipped ${file}`); + } + } + + await Promise.all(uploading); +} + +/** + * Main function for this script + */ +async function main() { + await yargs(process.argv.slice(2)) + .scriptName("publish") + .option("dry-run", { + alias: "dry", + nargs: 0, + describe: "Will not publish anything", + default: false, + type: "boolean", + }) + .command(["factorio ", "$0 "], "Publish factorio mods", yargs => { + yargs + .positional("mods-dir", { + describe: "Path to packed mods to publish", + normalize: true, + type: "string", + }) + .demandOption("mods-dir", "A path to the mods dir must be specified"); + }, async argv => { + DRY = argv.dry; + publishMods(argv.modsDir); + }) + .strict() + .parse(); +} + +// Run main if started from command line +if (module === require.main) { + main(); +}