mirror of
https://github.com/Nostalgica-Reverie/Content-Monorepo.git
synced 2026-05-09 00:24:15 +00:00
chore(ci): improve auto publish
This commit is contained in:
@@ -2,21 +2,63 @@ name: "Publish"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- 'modpacks/**/manifest.json'
|
||||
- 'datapacks/**/manifest.json'
|
||||
- 'resourcepacks/**/manifest.json'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
detect:
|
||||
runs-on: technocality
|
||||
outputs:
|
||||
manifests: ${{ steps.find.outputs.manifests }}
|
||||
has_manifests: ${{ steps.find.outputs.has_manifests }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 2
|
||||
filter: blob:none
|
||||
sparse-checkout: |
|
||||
modpacks
|
||||
datapacks
|
||||
|
||||
- name: Find changed manifests
|
||||
id: find
|
||||
run: |
|
||||
MANIFESTS=$(git diff-tree --no-commit-id --name-only -r HEAD \
|
||||
| grep -E '^(modpacks|datapacks)/.*/manifest\.json$' || true)
|
||||
if [ -z "$MANIFESTS" ]; then
|
||||
echo "has_manifests=false" >> $GITHUB_OUTPUT
|
||||
echo "manifests=[]" >> $GITHUB_OUTPUT
|
||||
echo "no changed manifests."
|
||||
else
|
||||
JSON=$(echo "$MANIFESTS" | jq -R -s -c 'split("\n") | map(select(length > 0))')
|
||||
echo "has_manifests=true" >> $GITHUB_OUTPUT
|
||||
echo "manifests=$JSON" >> $GITHUB_OUTPUT
|
||||
echo "manifests to publish: $JSON"
|
||||
fi
|
||||
|
||||
publish:
|
||||
needs: detect
|
||||
if: needs.detect.outputs.has_manifests == 'true'
|
||||
runs-on: technocality
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
manifest: ${{ fromJson(needs.detect.outputs.manifests) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
filter: blob:none
|
||||
sparse-checkout: |
|
||||
modpacks
|
||||
datapacks
|
||||
src/actions/publish
|
||||
tools/changelog
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -25,48 +67,61 @@ jobs:
|
||||
|
||||
- name: Generate Changelog
|
||||
id: changelog
|
||||
run: |
|
||||
# The blobless fetch allows this git command to work perfectly
|
||||
MANIFEST=$(git diff-tree --no-commit-id --name-only -r HEAD | grep 'manifest.json' | head -n 1)
|
||||
echo "manifest=$MANIFEST" >> $GITHUB_OUTPUT
|
||||
|
||||
npx tsx src/scripts/changelog.ts "$MANIFEST"
|
||||
run: npx tsx tools/changelog/generate-changelog.ts "${{ matrix.manifest }}"
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
- name: Cache Publisher Binary
|
||||
id: cache-publisher
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
go-version: 'stable'
|
||||
cache: true
|
||||
path: ./publisher-bin
|
||||
key: publisher-v1-${{ runner.os }}-${{ hashFiles('src/actions/publish/**/*.rs', 'src/actions/publish/Cargo.toml', 'src/actions/publish/Cargo.lock') }}
|
||||
|
||||
- name: Install Rust
|
||||
if: steps.cache-publisher.outputs.cache-hit != 'true'
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Rust Cache
|
||||
if: steps.cache-publisher.outputs.cache-hit != 'true'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "src/actions/publish -> target"
|
||||
|
||||
- name: Build Publisher
|
||||
if: steps.cache-publisher.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cargo build --release --manifest-path src/actions/publish/Cargo.toml --bin publish
|
||||
mkdir -p ./publisher-bin
|
||||
cp src/actions/publish/target/release/publish ./publisher-bin/publish
|
||||
|
||||
- name: Cache Packwiz Binaries
|
||||
id: cache-go
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/go/bin
|
||||
key: go-bin-v1-${{ runner.os }}
|
||||
key: go-bin-packwiz-v1-${{ runner.os }}
|
||||
|
||||
- name: Install packwiz
|
||||
- name: Setup Go
|
||||
if: steps.cache-go.outputs.cache-hit != 'true'
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Install Packwiz
|
||||
if: steps.cache-go.outputs.cache-hit != 'true'
|
||||
run: go install github.com/packwiz/packwiz@latest
|
||||
|
||||
- name: Add Path
|
||||
run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "src/actions -> target"
|
||||
- name: Add Go bin to PATH
|
||||
run: echo "$HOME/go/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Run Publisher
|
||||
id: meta
|
||||
run: |
|
||||
cargo run --release --manifest-path src/actions/Cargo.toml --bin publish -- "${{ steps.changelog.outputs.manifest }}"
|
||||
chmod +x ./publisher-bin/publish
|
||||
./publisher-bin/publish "${{ matrix.manifest }}"
|
||||
|
||||
- name: Upload to Platforms
|
||||
if: "steps.meta.outputs.mr_id != ''"
|
||||
if: "steps.meta.outputs.mr_id != '' || steps.meta.outputs.cf_id != ''"
|
||||
uses: https://github.com/Kir-Antipov/mc-publish@v3.3
|
||||
with:
|
||||
modrinth-id: ${{ steps.meta.outputs.mr_id }}
|
||||
@@ -78,5 +133,5 @@ jobs:
|
||||
name: "${{ steps.meta.outputs.name }}"
|
||||
version: "${{ steps.meta.outputs.ver }}"
|
||||
changelog: "${{ steps.changelog.outputs.notes }}"
|
||||
loaders: ${{ steps.meta.outputs.type == 'modpack' && 'fabric' || 'minecraft' }}
|
||||
game-versions: "${{ steps.meta.outputs.mc }}"
|
||||
loaders: ${{ steps.meta.outputs.type == 'modpack' && steps.meta.outputs.loader || 'minecraft' }}
|
||||
game-versions: "${{ steps.meta.outputs.mc }}"
|
||||
|
||||
@@ -1,101 +1,215 @@
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use serde_json::Value;
|
||||
use std::env;
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::{
|
||||
env,
|
||||
fs::{self, OpenOptions},
|
||||
io::Write,
|
||||
path::Path,
|
||||
process::Command,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let manifest_path_str = args.get(1).expect("Usage: publish <path_to_manifest.json>");
|
||||
#[derive(Clone, Copy)]
|
||||
enum Platform {
|
||||
Modrinth,
|
||||
Curseforge,
|
||||
}
|
||||
|
||||
let manifest_path = Path::new(manifest_path_str);
|
||||
let p_dir = manifest_path.parent().expect("Could not find parent directory");
|
||||
impl Platform {
|
||||
const ALL: [Platform; 2] = [Platform::Modrinth, Platform::Curseforge];
|
||||
|
||||
let manifest_content = fs::read_to_string(manifest_path).expect("Failed to read manifest");
|
||||
let manifest: Value = serde_json::from_str(&manifest_content).expect("Failed to parse manifest JSON");
|
||||
|
||||
let raw_name = manifest["name"].as_str().unwrap();
|
||||
let p_name = raw_name.replace(" ", "-");
|
||||
let p_ver = manifest["version"].as_str().unwrap();
|
||||
let mc_ver = manifest["mc_version"].as_str().unwrap();
|
||||
let p_type = manifest["type"].as_str().unwrap();
|
||||
let mr_id = manifest["modrinth_id"].as_str().unwrap_or("");
|
||||
let cf_id = manifest["curseforge_id"].as_str().unwrap_or("");
|
||||
|
||||
let filename_base = format!("{}-{}-fabric-{}", p_name, mc_ver, p_ver);
|
||||
|
||||
let workspace = env::var("GITHUB_WORKSPACE").unwrap_or_else(|_| ".".to_string());
|
||||
let artifacts_dir = Path::new(&workspace).join(p_dir).join("artifacts");
|
||||
|
||||
if artifacts_dir.exists() {
|
||||
fs::remove_dir_all(&artifacts_dir).unwrap();
|
||||
}
|
||||
fs::create_dir_all(&artifacts_dir).unwrap();
|
||||
|
||||
println!("::group::Building Artifacts for {}", raw_name);
|
||||
|
||||
if p_type == "modpack" {
|
||||
for platform in &["mr", "cf"] {
|
||||
let target_folder = format!("{}-{}", mc_ver, platform);
|
||||
let target_path = p_dir.join(&target_folder);
|
||||
|
||||
if target_path.exists() {
|
||||
run_cmd("packwiz", &["refresh"], &target_path);
|
||||
|
||||
let (export_cmd, ext) = if *platform == "mr" { ("modrinth", "mrpack") } else { ("curseforge", "zip") };
|
||||
let out_file = artifacts_dir.join(format!("{}-{}.{}", filename_base, platform, ext));
|
||||
|
||||
run_cmd("packwiz", &[export_cmd, "export", "--output", out_file.to_str().unwrap()], &target_path);
|
||||
} else {
|
||||
println!("Skipping {}: folder {} not found", platform, target_path.display());
|
||||
}
|
||||
}
|
||||
} else if p_type == "resourcepack" || p_type == "datapack" {
|
||||
let out_file = artifacts_dir.join(format!("{}-{}.zip", manifest["id"].as_str().unwrap_or("project"), p_ver));
|
||||
let content_dir = p_dir.join("content");
|
||||
|
||||
if content_dir.exists() {
|
||||
run_cmd("zip", &["-r", out_file.to_str().unwrap(), "."], &content_dir);
|
||||
} else {
|
||||
println!("Error: content directory not found at {}", content_dir.display());
|
||||
std::process::exit(1);
|
||||
fn short(self) -> &'static str {
|
||||
match self {
|
||||
Platform::Modrinth => "mr",
|
||||
Platform::Curseforge => "cf",
|
||||
}
|
||||
}
|
||||
println!("::endgroup::");
|
||||
|
||||
if let Ok(out_path) = env::var("GITHUB_OUTPUT") {
|
||||
let mut out_file = OpenOptions::new()
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open(out_path)
|
||||
.expect("Could not open GITHUB_OUTPUT");
|
||||
fn cli(self) -> &'static str {
|
||||
match self {
|
||||
Platform::Modrinth => "modrinth",
|
||||
Platform::Curseforge => "curseforge",
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(out_file, "mr_id={}", mr_id).unwrap();
|
||||
writeln!(out_file, "cf_id={}", cf_id).unwrap();
|
||||
writeln!(out_file, "name={} {}", raw_name, p_ver).unwrap();
|
||||
writeln!(out_file, "ver={}", p_ver).unwrap();
|
||||
writeln!(out_file, "mc={}", mc_ver).unwrap();
|
||||
writeln!(out_file, "type={}", p_type).unwrap();
|
||||
writeln!(out_file, "path={}", p_dir.to_str().unwrap()).unwrap();
|
||||
fn ext(self) -> &'static str {
|
||||
match self {
|
||||
Platform::Modrinth => "mrpack",
|
||||
Platform::Curseforge => "zip",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_cmd(cmd: &str, args: &[&str], dir: &Path) {
|
||||
let status = Command::new(cmd)
|
||||
.args(args)
|
||||
.current_dir(dir)
|
||||
.status();
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let manifest_path_str = args
|
||||
.get(1)
|
||||
.ok_or_else(|| anyhow!("usage: publish <path_to_manifest.json>"))?;
|
||||
let manifest_path = Path::new(manifest_path_str);
|
||||
let p_dir = manifest_path
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("manifest has no parent directory"))?;
|
||||
|
||||
match status {
|
||||
Ok(s) if s.success() => (),
|
||||
Ok(s) => {
|
||||
eprintln!("Command '{} {:?}' failed with exit code: {}", cmd, args, s);
|
||||
std::process::exit(1);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to execute '{}': {}", cmd, e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
let manifest_content = fs::read_to_string(manifest_path)
|
||||
.with_context(|| format!("failed to read {}", manifest_path.display()))?;
|
||||
let manifest: Value = serde_json::from_str(&manifest_content)
|
||||
.with_context(|| format!("invalid JSON in {}", manifest_path.display()))?;
|
||||
|
||||
let raw_name = required_str(&manifest, "name")?;
|
||||
let p_name = raw_name.replace(' ', "-");
|
||||
let p_ver = required_str(&manifest, "version")?;
|
||||
let mc_ver = required_str(&manifest, "mc_version")?;
|
||||
let p_type = required_str(&manifest, "type")?;
|
||||
let loader = manifest["loader"].as_str().unwrap_or("fabric");
|
||||
let mr_id = manifest["modrinth_id"].as_str().unwrap_or("");
|
||||
let cf_id = manifest["curseforge_id"].as_str().unwrap_or("");
|
||||
|
||||
let workspace = env::var("GITHUB_WORKSPACE").unwrap_or_else(|_| ".".into());
|
||||
let artifacts_dir = Path::new(&workspace).join(p_dir).join("artifacts");
|
||||
if artifacts_dir.exists() {
|
||||
fs::remove_dir_all(&artifacts_dir)
|
||||
.with_context(|| format!("failed to clear {}", artifacts_dir.display()))?;
|
||||
}
|
||||
fs::create_dir_all(&artifacts_dir)
|
||||
.with_context(|| format!("failed to create {}", artifacts_dir.display()))?;
|
||||
|
||||
println!("::group::Building artifacts for {raw_name}");
|
||||
|
||||
match p_type {
|
||||
"modpack" => build_modpack(p_dir, &artifacts_dir, &p_name, mc_ver, p_ver, loader)?,
|
||||
"datapack" => build_datapack(p_dir, &artifacts_dir, &manifest, p_ver)?,
|
||||
other => bail!("unsupported pack type: {other}"),
|
||||
}
|
||||
|
||||
println!("::endgroup::");
|
||||
|
||||
write_outputs(OutputData {
|
||||
mr_id,
|
||||
cf_id,
|
||||
raw_name,
|
||||
p_ver,
|
||||
mc_ver,
|
||||
p_type,
|
||||
loader,
|
||||
p_dir,
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn required_str<'a>(value: &'a Value, key: &str) -> Result<&'a str> {
|
||||
value[key]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!("missing or non-string '{key}' in manifest"))
|
||||
}
|
||||
|
||||
fn build_modpack(
|
||||
p_dir: &Path,
|
||||
artifacts_dir: &Path,
|
||||
p_name: &str,
|
||||
mc_ver: &str,
|
||||
p_ver: &str,
|
||||
loader: &str,
|
||||
) -> Result<()> {
|
||||
let filename_base = format!("{p_name}-{mc_ver}-{loader}-{p_ver}");
|
||||
let mut built = 0;
|
||||
|
||||
for platform in Platform::ALL {
|
||||
let target_folder = format!("{mc_ver}-{}", platform.short());
|
||||
let target_path = p_dir.join(&target_folder);
|
||||
if !target_path.exists() {
|
||||
println!(
|
||||
"skipping {}: folder {} not found",
|
||||
platform.short(),
|
||||
target_path.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let refresh = Command::new("packwiz")
|
||||
.args(["refresh", "-y"])
|
||||
.current_dir(&target_path)
|
||||
.status()
|
||||
.context("failed to invoke packwiz refresh")?;
|
||||
if !refresh.success() {
|
||||
bail!("packwiz refresh failed in {}", target_path.display());
|
||||
}
|
||||
|
||||
let out_file = artifacts_dir.join(format!(
|
||||
"{filename_base}-{}.{}",
|
||||
platform.short(),
|
||||
platform.ext()
|
||||
));
|
||||
let out_str = out_file
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("non-UTF8 output path"))?;
|
||||
|
||||
let export = Command::new("packwiz")
|
||||
.args([platform.cli(), "export", "--output", out_str])
|
||||
.current_dir(&target_path)
|
||||
.status()
|
||||
.context("failed to invoke packwiz export")?;
|
||||
if !export.success() {
|
||||
bail!("packwiz export failed for {target_folder}");
|
||||
}
|
||||
built += 1;
|
||||
}
|
||||
|
||||
if built == 0 {
|
||||
bail!("no platform folders (mc_ver-mr / mc_ver-cf) found");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_datapack(p_dir: &Path, artifacts_dir: &Path, manifest: &Value, p_ver: &str) -> Result<()> {
|
||||
let id = manifest["id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!("datapack manifest missing 'id' field"))?;
|
||||
let out_file = artifacts_dir.join(format!("{id}-{p_ver}.zip"));
|
||||
let content_dir = p_dir.join("content");
|
||||
if !content_dir.exists() {
|
||||
bail!("content directory not found at {}", content_dir.display());
|
||||
}
|
||||
let out_str = out_file
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("non-UTF8 output path"))?;
|
||||
let status = Command::new("zip")
|
||||
.args(["-r", out_str, "."])
|
||||
.current_dir(&content_dir)
|
||||
.status()
|
||||
.context("failed to invoke zip")?;
|
||||
if !status.success() {
|
||||
bail!("zip failed for datapack");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct OutputData<'a> {
|
||||
mr_id: &'a str,
|
||||
cf_id: &'a str,
|
||||
raw_name: &'a str,
|
||||
p_ver: &'a str,
|
||||
mc_ver: &'a str,
|
||||
p_type: &'a str,
|
||||
loader: &'a str,
|
||||
p_dir: &'a Path,
|
||||
}
|
||||
|
||||
fn write_outputs(d: OutputData) -> Result<()> {
|
||||
let Ok(out_path) = env::var("GITHUB_OUTPUT") else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut f = OpenOptions::new()
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open(&out_path)
|
||||
.with_context(|| format!("failed to open {out_path}"))?;
|
||||
writeln!(f, "mr_id={}", d.mr_id)?;
|
||||
writeln!(f, "cf_id={}", d.cf_id)?;
|
||||
writeln!(f, "name={} {}", d.raw_name, d.p_ver)?;
|
||||
writeln!(f, "ver={}", d.p_ver)?;
|
||||
writeln!(f, "mc={}", d.mc_ver)?;
|
||||
writeln!(f, "type={}", d.p_type)?;
|
||||
writeln!(f, "loader={}", d.loader)?;
|
||||
writeln!(f, "path={}", d.p_dir.display())?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user