fix: make the release workflow build and package a real Jellyfin plugin
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>
This commit is contained in:
42
.gitea/workflows/build.yml
Normal file
42
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
name: Build Plugin
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- '**/*.md'
|
||||||
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- '**/*.md'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '9.0.x'
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
|
||||||
|
- name: Install packaging dependencies
|
||||||
|
run: python -m pip install --quiet --upgrade pyyaml
|
||||||
|
|
||||||
|
- name: Build and package plugin
|
||||||
|
id: package
|
||||||
|
run: python scripts/build_plugin.py --output "$RUNNER_TEMP/dist"
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ steps.package.outputs.zip_name }}
|
||||||
|
path: ${{ runner.temp }}/dist/${{ steps.package.outputs.zip_name }}
|
||||||
@@ -6,10 +6,10 @@ on:
|
|||||||
- 'v*'
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout tag
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -17,44 +17,99 @@ jobs:
|
|||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.0.x'
|
dotnet-version: '9.0.x'
|
||||||
|
|
||||||
- name: Extract version from tag
|
- name: Setup Python
|
||||||
id: version
|
uses: actions/setup-python@v5
|
||||||
run: |
|
|
||||||
VERSION="${GITHUB_REF_NAME#v}"
|
|
||||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Publish
|
|
||||||
run: dotnet publish JellyfinSso.csproj --configuration Release --output publish
|
|
||||||
|
|
||||||
- name: Zip build output
|
|
||||||
run: |
|
|
||||||
cd publish
|
|
||||||
zip -r "../jellyfinsso-${{ steps.version.outputs.version }}.zip" .
|
|
||||||
|
|
||||||
- name: Compute checksum
|
|
||||||
id: checksum
|
|
||||||
run: |
|
|
||||||
CHECKSUM=$(md5sum "jellyfinsso-${{ steps.version.outputs.version }}.zip" | cut -d' ' -f1)
|
|
||||||
echo "md5=$CHECKSUM" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Upload zip to Gitea Release
|
|
||||||
uses: akkuman/gitea-release-action@v1
|
|
||||||
with:
|
with:
|
||||||
files: "jellyfinsso-${{ steps.version.outputs.version }}.zip"
|
python-version: '3.x'
|
||||||
|
|
||||||
- name: Update manifest.json
|
- name: Install packaging dependencies
|
||||||
run: |
|
run: python -m pip install --quiet --upgrade pyyaml
|
||||||
python3 update_manifest.py \
|
|
||||||
"${{ steps.version.outputs.version }}" \
|
|
||||||
"${{ steps.checksum.outputs.md5 }}" \
|
|
||||||
"https://git.linxweiler.xyz/pascallinxweiler/jellyfinsso/releases/download/${GITHUB_REF_NAME}/jellyfinsso-${{ steps.version.outputs.version }}.zip"
|
|
||||||
|
|
||||||
- name: Commit manifest.json
|
- name: Read version and changelog from tag
|
||||||
|
id: meta
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Annotated tag message becomes the changelog; build.yaml is the fallback.
|
||||||
|
{
|
||||||
|
echo "changelog<<CHANGELOG_EOF"
|
||||||
|
git tag -l --format='%(contents)' "$GITHUB_REF_NAME"
|
||||||
|
echo "CHANGELOG_EOF"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Build and package plugin
|
||||||
|
id: package
|
||||||
|
env:
|
||||||
|
VERSION: ${{ steps.meta.outputs.version }}
|
||||||
|
CHANGELOG: ${{ steps.meta.outputs.changelog }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
python scripts/build_plugin.py \
|
||||||
|
--version "$VERSION" \
|
||||||
|
--changelog "$CHANGELOG" \
|
||||||
|
--output "$RUNNER_TEMP/dist"
|
||||||
|
|
||||||
|
- name: Switch to main
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git checkout -- .
|
||||||
|
git fetch origin main
|
||||||
|
git checkout -B main origin/main
|
||||||
|
|
||||||
|
- name: Update dist, build.yaml and manifest.json
|
||||||
|
env:
|
||||||
|
VERSION: ${{ steps.package.outputs.version }}
|
||||||
|
ZIP_NAME: ${{ steps.package.outputs.zip_name }}
|
||||||
|
CHECKSUM: ${{ steps.package.outputs.checksum }}
|
||||||
|
CHANGELOG: ${{ steps.meta.outputs.changelog }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p dist
|
||||||
|
cp "$RUNNER_TEMP/dist/$ZIP_NAME" "dist/$ZIP_NAME"
|
||||||
|
|
||||||
|
python scripts/build_plugin.py --version "$VERSION" --sync-only
|
||||||
|
|
||||||
|
python scripts/update_manifest.py \
|
||||||
|
--version "$VERSION" \
|
||||||
|
--checksum "$CHECKSUM" \
|
||||||
|
--changelog "$CHANGELOG" \
|
||||||
|
--source-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/raw/branch/main/dist/${ZIP_NAME}"
|
||||||
|
|
||||||
|
- name: Commit and push to main
|
||||||
|
env:
|
||||||
|
# Gitea injects GITHUB_TOKEN; set RELEASE_TOKEN if that token does not
|
||||||
|
# have write access to the repository.
|
||||||
|
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
DEFAULT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
TOKEN="${RELEASE_TOKEN:-$DEFAULT_TOKEN}"
|
||||||
|
if [ -z "$TOKEN" ]; then
|
||||||
|
echo "no token available to push manifest.json" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
git config user.name "gitea-actions"
|
git config user.name "gitea-actions"
|
||||||
git config user.email "actions@git.linxweiler.xyz"
|
git config user.email "actions@git.linxweiler.xyz"
|
||||||
git add manifest.json
|
git add dist manifest.json build.yaml Jellyfin.Plugin.OidcAuth/Jellyfin.Plugin.OidcAuth.csproj
|
||||||
git commit -m "Update manifest.json for ${GITHUB_REF_NAME}" || echo "No changes"
|
|
||||||
git push origin HEAD:main
|
if git diff --cached --quiet; then
|
||||||
|
echo "nothing to commit"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git commit -m "release: ${GITHUB_REF_NAME}"
|
||||||
|
|
||||||
|
git remote set-url origin \
|
||||||
|
"https://x-access-token:${TOKEN}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git"
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
- name: Attach zip to Gitea release
|
||||||
|
uses: akkuman/gitea-release-action@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
files: dist/${{ steps.package.outputs.zip_name }}
|
||||||
|
body: ${{ steps.meta.outputs.changelog }}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<RootNamespace>Jellyfin.Plugin.OidcAuth</RootNamespace>
|
<RootNamespace>Jellyfin.Plugin.OidcAuth</RootNamespace>
|
||||||
|
<!-- Kept in sync with build.yaml by scripts/build_plugin.py; the release
|
||||||
|
workflow overrides these with the version from the git tag. -->
|
||||||
|
<Version>1.0.3.0</Version>
|
||||||
<AssemblyVersion>1.0.3.0</AssemblyVersion>
|
<AssemblyVersion>1.0.3.0</AssemblyVersion>
|
||||||
<FileVersion>1.0.3.0</FileVersion>
|
<FileVersion>1.0.3.0</FileVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
|||||||
33
README.md
33
README.md
@@ -11,17 +11,41 @@ Log in to Jellyfin with any OpenID Connect provider (Authelia, Authentik, Keyclo
|
|||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
|
Requires the .NET 9 SDK and `pyyaml`. The script compiles the plugin and packages
|
||||||
|
it the way Jellyfin expects — the DLLs listed in `build.yaml` plus a generated
|
||||||
|
`meta.json` — into `dist/oidc-auth_<version>.zip`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet publish Jellyfin.Plugin.OidcAuth --configuration Release --output artifacts
|
pip install pyyaml
|
||||||
|
python scripts/build_plugin.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Pass `--version 1.0.4.0` to override the version in `build.yaml`.
|
||||||
|
|
||||||
|
## Release
|
||||||
|
|
||||||
|
Releases are cut by pushing an annotated tag; the tag message becomes the changelog:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag -a v1.0.4.0 -m "What changed in this release."
|
||||||
|
git push origin v1.0.4.0
|
||||||
|
```
|
||||||
|
|
||||||
|
`.gitea/workflows/release.yml` then builds the plugin at that tag, commits the zip
|
||||||
|
to `dist/` on `main` together with an updated `manifest.json`, and attaches the zip
|
||||||
|
to the Gitea release. Jellyfin picks the new version up from the repository URL below.
|
||||||
|
|
||||||
|
Pushing needs a token with write access. Gitea's built-in `GITHUB_TOKEN` is used by
|
||||||
|
default; if the repository does not grant it write access, add a PAT as the
|
||||||
|
`RELEASE_TOKEN` secret.
|
||||||
|
|
||||||
## Install via plugin repository (recommended)
|
## Install via plugin repository (recommended)
|
||||||
|
|
||||||
1. In Jellyfin: Dashboard → Plugins → Repositories → **+**
|
1. In Jellyfin: Dashboard → Plugins → Repositories → **+**
|
||||||
2. Repository URL:
|
2. Repository URL:
|
||||||
|
|
||||||
```
|
```
|
||||||
https://git.linxweiler.xyz/pascallinxweiler/jellyfinplugin/raw/branch/main/manifest.json
|
https://git.linxweiler.xyz/pascallinxweiler/jellyfinsso/raw/branch/main/manifest.json
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Catalog → Authentication → **OIDC Auth** → Install → restart Jellyfin.
|
3. Catalog → Authentication → **OIDC Auth** → Install → restart Jellyfin.
|
||||||
@@ -30,12 +54,13 @@ Updates show up in the catalog automatically when a new version is added to `man
|
|||||||
|
|
||||||
## Install manually
|
## Install manually
|
||||||
|
|
||||||
Copy these files from `artifacts/` into a new folder in Jellyfin's plugin directory
|
Extract `dist/oidc-auth_<version>.zip` into a new folder in Jellyfin's plugin
|
||||||
(e.g. `config/plugins/OidcAuth/`), then restart Jellyfin:
|
directory (e.g. `config/plugins/OidcAuth/`), then restart Jellyfin. The zip holds:
|
||||||
|
|
||||||
- `Jellyfin.Plugin.OidcAuth.dll`
|
- `Jellyfin.Plugin.OidcAuth.dll`
|
||||||
- `IdentityModel.OidcClient.dll`
|
- `IdentityModel.OidcClient.dll`
|
||||||
- `IdentityModel.dll`
|
- `IdentityModel.dll`
|
||||||
|
- `meta.json`
|
||||||
|
|
||||||
Requires Jellyfin 10.11.x.
|
Requires Jellyfin 10.11.x.
|
||||||
|
|
||||||
|
|||||||
@@ -15,5 +15,7 @@ artifacts:
|
|||||||
- "Jellyfin.Plugin.OidcAuth.dll"
|
- "Jellyfin.Plugin.OidcAuth.dll"
|
||||||
- "IdentityModel.OidcClient.dll"
|
- "IdentityModel.OidcClient.dll"
|
||||||
- "IdentityModel.dll"
|
- "IdentityModel.dll"
|
||||||
|
# Fallback changelog. The release workflow prefers the annotated tag's message.
|
||||||
changelog: |
|
changelog: |
|
||||||
Initial release.
|
Admin role mapping now only grants admin by default. New opt-in setting
|
||||||
|
"Also revoke admin when the admin role is missing" restores the old behavior.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"version": "1.0.3.0",
|
"version": "1.0.3.0",
|
||||||
"changelog": "Admin role mapping now only grants admin by default. New opt-in setting 'Also revoke admin when the admin role is missing' restores the old revoking behavior.",
|
"changelog": "Admin role mapping now only grants admin by default. New opt-in setting 'Also revoke admin when the admin role is missing' restores the old revoking behavior.",
|
||||||
"targetAbi": "10.11.0.0",
|
"targetAbi": "10.11.0.0",
|
||||||
"sourceUrl": "https://git.linxweiler.xyz/pascallinxweiler/jellyfinplugin/raw/branch/main/dist/oidc-auth_1.0.3.0.zip",
|
"sourceUrl": "https://git.linxweiler.xyz/pascallinxweiler/jellyfinsso/raw/branch/main/dist/oidc-auth_1.0.3.0.zip",
|
||||||
"checksum": "8e1435d2d4a1587797f9c0af53f6fca2",
|
"checksum": "8e1435d2d4a1587797f9c0af53f6fca2",
|
||||||
"timestamp": "2026-07-13T13:00:00Z"
|
"timestamp": "2026-07-13T13:00:00Z"
|
||||||
},
|
},
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"version": "1.0.2.0",
|
"version": "1.0.2.0",
|
||||||
"changelog": "Log OIDC claims and admin mapping decisions; role comparison is now case-insensitive.",
|
"changelog": "Log OIDC claims and admin mapping decisions; role comparison is now case-insensitive.",
|
||||||
"targetAbi": "10.11.0.0",
|
"targetAbi": "10.11.0.0",
|
||||||
"sourceUrl": "https://git.linxweiler.xyz/pascallinxweiler/jellyfinplugin/raw/branch/main/dist/oidc-auth_1.0.2.0.zip",
|
"sourceUrl": "https://git.linxweiler.xyz/pascallinxweiler/jellyfinsso/raw/branch/main/dist/oidc-auth_1.0.2.0.zip",
|
||||||
"checksum": "343bcda662a3cdd1e36391222391d2d5",
|
"checksum": "343bcda662a3cdd1e36391222391d2d5",
|
||||||
"timestamp": "2026-07-13T12:30:00Z"
|
"timestamp": "2026-07-13T12:30:00Z"
|
||||||
},
|
},
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"version": "1.0.1.0",
|
"version": "1.0.1.0",
|
||||||
"changelog": "Rebuilt for Jellyfin 10.11 (net9.0).",
|
"changelog": "Rebuilt for Jellyfin 10.11 (net9.0).",
|
||||||
"targetAbi": "10.11.0.0",
|
"targetAbi": "10.11.0.0",
|
||||||
"sourceUrl": "https://git.linxweiler.xyz/pascallinxweiler/jellyfinplugin/raw/branch/main/dist/oidc-auth_1.0.1.0.zip",
|
"sourceUrl": "https://git.linxweiler.xyz/pascallinxweiler/jellyfinsso/raw/branch/main/dist/oidc-auth_1.0.1.0.zip",
|
||||||
"checksum": "8095740b87cf862ad41bc50ccbdb7443",
|
"checksum": "8095740b87cf862ad41bc50ccbdb7443",
|
||||||
"timestamp": "2026-07-13T12:00:00Z"
|
"timestamp": "2026-07-13T12:00:00Z"
|
||||||
}
|
}
|
||||||
|
|||||||
192
scripts/build_plugin.py
Executable file
192
scripts/build_plugin.py
Executable file
@@ -0,0 +1,192 @@
|
|||||||
|
#!/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())
|
||||||
113
scripts/update_manifest.py
Executable file
113
scripts/update_manifest.py
Executable file
@@ -0,0 +1,113 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Add a version entry to the Jellyfin plugin repository manifest.
|
||||||
|
|
||||||
|
manifest.json is what Jellyfin fetches when the repository URL is added under
|
||||||
|
Dashboard -> Plugins -> Repositories. Plugin metadata comes from build.yaml so
|
||||||
|
there is a single source of truth.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
scripts/update_manifest.py --version 1.0.4.0 --checksum <md5> --source-url <url>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
BUILD_YAML = REPO_ROOT / "build.yaml"
|
||||||
|
MANIFEST = REPO_ROOT / "manifest.json"
|
||||||
|
|
||||||
|
|
||||||
|
def version_key(version):
|
||||||
|
return tuple(int(part) for part in re.findall(r"\d+", version))
|
||||||
|
|
||||||
|
|
||||||
|
def load_manifest():
|
||||||
|
if not MANIFEST.is_file():
|
||||||
|
return []
|
||||||
|
with MANIFEST.open(encoding="utf-8") as handle:
|
||||||
|
return json.load(handle)
|
||||||
|
|
||||||
|
|
||||||
|
def find_plugin(manifest, guid):
|
||||||
|
for plugin in manifest:
|
||||||
|
if plugin.get("guid", "").lower() == guid.lower():
|
||||||
|
return plugin
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--version", required=True)
|
||||||
|
parser.add_argument("--checksum", required=True, help="md5 of the zip")
|
||||||
|
parser.add_argument("--source-url", required=True, help="download URL for the zip")
|
||||||
|
parser.add_argument("--changelog", default="")
|
||||||
|
parser.add_argument("--timestamp", default="", help="ISO 8601 UTC; defaults to now")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
with BUILD_YAML.open(encoding="utf-8") as handle:
|
||||||
|
cfg = yaml.safe_load(handle)
|
||||||
|
|
||||||
|
timestamp = args.timestamp or (
|
||||||
|
datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
.replace(tzinfo=None)
|
||||||
|
.isoformat(timespec="seconds")
|
||||||
|
+ "Z"
|
||||||
|
)
|
||||||
|
|
||||||
|
manifest = load_manifest()
|
||||||
|
plugin = find_plugin(manifest, cfg["guid"])
|
||||||
|
if plugin is None:
|
||||||
|
plugin = {"guid": cfg["guid"], "versions": []}
|
||||||
|
manifest.append(plugin)
|
||||||
|
|
||||||
|
# Refresh the descriptive fields so build.yaml stays authoritative.
|
||||||
|
plugin["name"] = cfg["name"]
|
||||||
|
plugin["description"] = cfg["description"].strip()
|
||||||
|
plugin["overview"] = cfg["overview"].strip()
|
||||||
|
plugin["owner"] = cfg["owner"]
|
||||||
|
plugin["category"] = cfg["category"]
|
||||||
|
|
||||||
|
entry = {
|
||||||
|
"version": args.version,
|
||||||
|
"changelog": (args.changelog or cfg.get("changelog", "")).strip(),
|
||||||
|
"targetAbi": cfg["targetAbi"],
|
||||||
|
"sourceUrl": args.source_url,
|
||||||
|
"checksum": args.checksum,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
versions = [v for v in plugin.get("versions", []) if v.get("version") != args.version]
|
||||||
|
versions.append(entry)
|
||||||
|
versions.sort(key=lambda v: version_key(v["version"]), reverse=True)
|
||||||
|
plugin["versions"] = versions
|
||||||
|
|
||||||
|
# Key order matters only for readability; Jellyfin reads by name.
|
||||||
|
ordered = [
|
||||||
|
{
|
||||||
|
key: plugin[key]
|
||||||
|
for key in (
|
||||||
|
"guid",
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"overview",
|
||||||
|
"owner",
|
||||||
|
"category",
|
||||||
|
"versions",
|
||||||
|
)
|
||||||
|
if key in plugin
|
||||||
|
}
|
||||||
|
for plugin in manifest
|
||||||
|
]
|
||||||
|
|
||||||
|
MANIFEST.write_text(json.dumps(ordered, indent=4) + "\n", encoding="utf-8")
|
||||||
|
print(f"manifest.json now lists {args.version} at {args.source_url}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user