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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: ["main"]
|
||||||
paths:
|
paths:
|
||||||
- 'modpacks/**/manifest.json'
|
- 'modpacks/**/manifest.json'
|
||||||
- 'datapacks/**/manifest.json'
|
- 'datapacks/**/manifest.json'
|
||||||
- 'resourcepacks/**/manifest.json'
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
detect:
|
||||||
runs-on: technocality
|
runs-on: technocality
|
||||||
|
outputs:
|
||||||
|
manifests: ${{ steps.find.outputs.manifests }}
|
||||||
|
has_manifests: ${{ steps.find.outputs.has_manifests }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 2
|
||||||
filter: blob:none
|
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
|
- name: Set up Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -25,48 +67,61 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate Changelog
|
- name: Generate Changelog
|
||||||
id: changelog
|
id: changelog
|
||||||
run: |
|
run: npx tsx tools/changelog/generate-changelog.ts "${{ matrix.manifest }}"
|
||||||
# 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"
|
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Cache Publisher Binary
|
||||||
uses: actions/setup-go@v5
|
id: cache-publisher
|
||||||
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
go-version: 'stable'
|
path: ./publisher-bin
|
||||||
cache: true
|
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
|
- name: Cache Packwiz Binaries
|
||||||
id: cache-go
|
id: cache-go
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/go/bin
|
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'
|
if: steps.cache-go.outputs.cache-hit != 'true'
|
||||||
run: go install github.com/packwiz/packwiz@latest
|
run: go install github.com/packwiz/packwiz@latest
|
||||||
|
|
||||||
- name: Add Path
|
- name: Add Go bin to PATH
|
||||||
run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
run: echo "$HOME/go/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: Run Publisher
|
- name: Run Publisher
|
||||||
id: meta
|
id: meta
|
||||||
run: |
|
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
|
- 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
|
uses: https://github.com/Kir-Antipov/mc-publish@v3.3
|
||||||
with:
|
with:
|
||||||
modrinth-id: ${{ steps.meta.outputs.mr_id }}
|
modrinth-id: ${{ steps.meta.outputs.mr_id }}
|
||||||
@@ -78,5 +133,5 @@ jobs:
|
|||||||
name: "${{ steps.meta.outputs.name }}"
|
name: "${{ steps.meta.outputs.name }}"
|
||||||
version: "${{ steps.meta.outputs.ver }}"
|
version: "${{ steps.meta.outputs.ver }}"
|
||||||
changelog: "${{ steps.changelog.outputs.notes }}"
|
changelog: "${{ steps.changelog.outputs.notes }}"
|
||||||
loaders: ${{ steps.meta.outputs.type == 'modpack' && 'fabric' || 'minecraft' }}
|
loaders: ${{ steps.meta.outputs.type == 'modpack' && steps.meta.outputs.loader || 'minecraft' }}
|
||||||
game-versions: "${{ steps.meta.outputs.mc }}"
|
game-versions: "${{ steps.meta.outputs.mc }}"
|
||||||
|
|||||||
@@ -1,101 +1,215 @@
|
|||||||
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::env;
|
use std::{
|
||||||
use std::fs::{self, OpenOptions};
|
env,
|
||||||
use std::io::Write;
|
fs::{self, OpenOptions},
|
||||||
use std::path::{Path, PathBuf};
|
io::Write,
|
||||||
use std::process::Command;
|
path::Path,
|
||||||
|
process::Command,
|
||||||
|
};
|
||||||
|
|
||||||
fn main() {
|
#[derive(Clone, Copy)]
|
||||||
let args: Vec<String> = env::args().collect();
|
enum Platform {
|
||||||
let manifest_path_str = args.get(1).expect("Usage: publish <path_to_manifest.json>");
|
Modrinth,
|
||||||
|
Curseforge,
|
||||||
|
}
|
||||||
|
|
||||||
let manifest_path = Path::new(manifest_path_str);
|
impl Platform {
|
||||||
let p_dir = manifest_path.parent().expect("Could not find parent directory");
|
const ALL: [Platform; 2] = [Platform::Modrinth, Platform::Curseforge];
|
||||||
|
|
||||||
let manifest_content = fs::read_to_string(manifest_path).expect("Failed to read manifest");
|
fn short(self) -> &'static str {
|
||||||
let manifest: Value = serde_json::from_str(&manifest_content).expect("Failed to parse manifest JSON");
|
match self {
|
||||||
|
Platform::Modrinth => "mr",
|
||||||
let raw_name = manifest["name"].as_str().unwrap();
|
Platform::Curseforge => "cf",
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("::endgroup::");
|
|
||||||
|
|
||||||
if let Ok(out_path) = env::var("GITHUB_OUTPUT") {
|
fn cli(self) -> &'static str {
|
||||||
let mut out_file = OpenOptions::new()
|
match self {
|
||||||
.append(true)
|
Platform::Modrinth => "modrinth",
|
||||||
.create(true)
|
Platform::Curseforge => "curseforge",
|
||||||
.open(out_path)
|
}
|
||||||
.expect("Could not open GITHUB_OUTPUT");
|
}
|
||||||
|
|
||||||
writeln!(out_file, "mr_id={}", mr_id).unwrap();
|
fn ext(self) -> &'static str {
|
||||||
writeln!(out_file, "cf_id={}", cf_id).unwrap();
|
match self {
|
||||||
writeln!(out_file, "name={} {}", raw_name, p_ver).unwrap();
|
Platform::Modrinth => "mrpack",
|
||||||
writeln!(out_file, "ver={}", p_ver).unwrap();
|
Platform::Curseforge => "zip",
|
||||||
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 run_cmd(cmd: &str, args: &[&str], dir: &Path) {
|
fn main() -> Result<()> {
|
||||||
let status = Command::new(cmd)
|
let args: Vec<String> = env::args().collect();
|
||||||
.args(args)
|
let manifest_path_str = args
|
||||||
.current_dir(dir)
|
.get(1)
|
||||||
.status();
|
.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 {
|
let manifest_content = fs::read_to_string(manifest_path)
|
||||||
Ok(s) if s.success() => (),
|
.with_context(|| format!("failed to read {}", manifest_path.display()))?;
|
||||||
Ok(s) => {
|
let manifest: Value = serde_json::from_str(&manifest_content)
|
||||||
eprintln!("Command '{} {:?}' failed with exit code: {}", cmd, args, s);
|
.with_context(|| format!("invalid JSON in {}", manifest_path.display()))?;
|
||||||
std::process::exit(1);
|
|
||||||
}
|
let raw_name = required_str(&manifest, "name")?;
|
||||||
Err(e) => {
|
let p_name = raw_name.replace(' ', "-");
|
||||||
eprintln!("Failed to execute '{}': {}", cmd, e);
|
let p_ver = required_str(&manifest, "version")?;
|
||||||
std::process::exit(1);
|
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