The release workflow was copied from another project and could not succeed on any tag: it installed the .NET 8 SDK for a net9.0 project, published a JellyfinSso.csproj that does not exist, called a missing update_manifest.py, and zipped the entire publish tree instead of the three DLLs declared in build.yaml. Replace it with a tag-driven release: - Build with the .NET 9 SDK, injecting the tag version via -p:Version, -p:AssemblyVersion and -p:FileVersion so the assembly no longer carries a hardcoded 1.0.3.0. - Package via scripts/build_plugin.py, which emits the JPRM layout Jellyfin expects: the build.yaml artifacts plus a generated meta.json. The previous zips shipped without meta.json, which only worked for repository installs because Jellyfin synthesises one from the folder name. - Update manifest.json via scripts/update_manifest.py, keeping build.yaml as the single source of truth for plugin metadata. - Commit the zip and manifest to main and attach the zip to the Gitea release. Pushing uses the injected token, falling back to a RELEASE_TOKEN secret when the built-in one lacks write access. Also fix the published download URLs. manifest.json and the README pointed at pascallinxweiler/jellyfinplugin, which 404s; the repository is jellyfinsso, so the plugin repository could not be added to Jellyfin and no version could be downloaded. The workflow now derives the URL from GITHUB_REPOSITORY. Add a build workflow so main and pull requests are compiled and packaged. Co-Authored-By: Claude <noreply@anthropic.com>
193 lines
5.9 KiB
Python
Executable File
193 lines
5.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Build and package the Jellyfin plugin described by build.yaml.
|
|
|
|
Produces the same layout as JPRM (jellyfin-plugin-repository-manager): a zip
|
|
containing the artifacts listed in build.yaml plus a meta.json, named
|
|
``<slug>_<version>.zip``.
|
|
|
|
Usage:
|
|
scripts/build_plugin.py --version 1.0.4.0 --changelog "..." --output dist
|
|
"""
|
|
|
|
import argparse
|
|
import datetime
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
BUILD_YAML = REPO_ROOT / "build.yaml"
|
|
PROJECT = REPO_ROOT / "Jellyfin.Plugin.OidcAuth" / "Jellyfin.Plugin.OidcAuth.csproj"
|
|
|
|
|
|
def normalize_version(version):
|
|
"""Pad a version to Jellyfin's four-part form (1.0.4 -> 1.0.4.0)."""
|
|
parts = version.strip().lstrip("v").split(".")
|
|
if not 1 <= len(parts) <= 4:
|
|
raise ValueError(f"'{version}' is not a 1- to 4-part version")
|
|
for part in parts:
|
|
if not part.isdigit():
|
|
raise ValueError(f"'{version}' has a non-numeric component '{part}'")
|
|
parts += ["0"] * (4 - len(parts))
|
|
return ".".join(parts)
|
|
|
|
|
|
def slugify(name):
|
|
"""'OIDC Auth' -> 'oidc-auth'."""
|
|
return re.sub(r"-+", "-", re.sub(r"[^a-z0-9]+", "-", name.lower())).strip("-")
|
|
|
|
|
|
def load_build_config():
|
|
with BUILD_YAML.open(encoding="utf-8") as handle:
|
|
return yaml.safe_load(handle)
|
|
|
|
|
|
def sync_version(version):
|
|
"""Write the release version back into build.yaml and the csproj."""
|
|
build_yaml = BUILD_YAML.read_text(encoding="utf-8")
|
|
build_yaml = re.sub(
|
|
r'^version:\s*.*$', f'version: "{version}"', build_yaml, count=1, flags=re.MULTILINE
|
|
)
|
|
BUILD_YAML.write_text(build_yaml, encoding="utf-8")
|
|
|
|
csproj = PROJECT.read_text(encoding="utf-8")
|
|
for tag in ("Version", "AssemblyVersion", "FileVersion"):
|
|
csproj = re.sub(
|
|
rf"<{tag}>[^<]*</{tag}>", f"<{tag}>{version}</{tag}>", csproj, count=1
|
|
)
|
|
PROJECT.write_text(csproj, encoding="utf-8")
|
|
|
|
|
|
def publish(cfg, version, configuration, publish_dir):
|
|
if publish_dir.exists():
|
|
shutil.rmtree(publish_dir)
|
|
|
|
command = [
|
|
"dotnet",
|
|
"publish",
|
|
str(PROJECT),
|
|
"--configuration",
|
|
configuration,
|
|
"--framework",
|
|
cfg["framework"],
|
|
"--output",
|
|
str(publish_dir),
|
|
f"-p:Version={version}",
|
|
f"-p:AssemblyVersion={version}",
|
|
f"-p:FileVersion={version}",
|
|
# Plugin DLLs ship loose; a nuspec-style layout would confuse Jellyfin.
|
|
"-p:DebugType=none",
|
|
"-p:GenerateDocumentationFile=false",
|
|
]
|
|
print("+ " + " ".join(command), flush=True)
|
|
subprocess.run(command, check=True, cwd=REPO_ROOT)
|
|
|
|
|
|
def build_metadata(cfg, version, changelog):
|
|
"""The meta.json Jellyfin reads out of the plugin folder."""
|
|
return {
|
|
"guid": cfg["guid"],
|
|
"name": cfg["name"],
|
|
"description": cfg["description"].strip(),
|
|
"overview": cfg["overview"].strip(),
|
|
"owner": cfg["owner"],
|
|
"category": cfg["category"],
|
|
"version": version,
|
|
"changelog": changelog.strip(),
|
|
"targetAbi": cfg["targetAbi"],
|
|
"timestamp": datetime.datetime.now(datetime.timezone.utc)
|
|
.replace(tzinfo=None)
|
|
.isoformat(timespec="seconds")
|
|
+ "Z",
|
|
}
|
|
|
|
|
|
def package(cfg, version, changelog, publish_dir, output_dir):
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
zip_path = output_dir / f"{slugify(cfg['name'])}_{version}.zip"
|
|
|
|
missing = [a for a in cfg["artifacts"] if not (publish_dir / a).is_file()]
|
|
if missing:
|
|
raise SystemExit(
|
|
f"error: build.yaml lists artifacts missing from {publish_dir}: {', '.join(missing)}"
|
|
)
|
|
|
|
metadata = build_metadata(cfg, version, changelog)
|
|
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as archive:
|
|
for artifact in cfg["artifacts"]:
|
|
archive.write(publish_dir / artifact, artifact)
|
|
archive.writestr("meta.json", json.dumps(metadata, indent=4) + "\n")
|
|
|
|
return zip_path
|
|
|
|
|
|
def md5sum(path):
|
|
digest = hashlib.md5()
|
|
with path.open("rb") as handle:
|
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def emit_outputs(**values):
|
|
"""Expose results to the workflow via $GITHUB_OUTPUT."""
|
|
target = os.environ.get("GITHUB_OUTPUT")
|
|
if not target:
|
|
return
|
|
with open(target, "a", encoding="utf-8") as handle:
|
|
for key, value in values.items():
|
|
handle.write(f"{key}={value}\n")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--version", help="release version; defaults to build.yaml")
|
|
parser.add_argument("--changelog", default="", help="changelog for this version")
|
|
parser.add_argument("--configuration", default="Release")
|
|
parser.add_argument("--output", default="dist", help="directory for the zip")
|
|
parser.add_argument(
|
|
"--sync-only",
|
|
action="store_true",
|
|
help="only write --version into build.yaml and the csproj, then exit",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
cfg = load_build_config()
|
|
version = normalize_version(args.version or cfg["version"])
|
|
changelog = args.changelog or cfg.get("changelog", "")
|
|
|
|
if args.sync_only:
|
|
sync_version(version)
|
|
print(f"build.yaml and csproj set to {version}")
|
|
emit_outputs(version=version)
|
|
return
|
|
|
|
publish_dir = REPO_ROOT / "artifacts" / "publish"
|
|
publish(cfg, version, args.configuration, publish_dir)
|
|
|
|
zip_path = package(cfg, version, changelog, publish_dir, Path(args.output).resolve())
|
|
checksum = md5sum(zip_path)
|
|
|
|
print(f"\npackaged {zip_path}")
|
|
print(f"version {version}")
|
|
print(f"md5 {checksum}")
|
|
|
|
emit_outputs(
|
|
version=version,
|
|
checksum=checksum,
|
|
zip_path=zip_path,
|
|
zip_name=zip_path.name,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|