Keeps Radarr/Sonarr quality upgrades out of "Recently Added" by keeping a ledger of the first date added per logical medium (movie or episode, identified by provider ids and the series) and restoring it on upgrades. - core logic without Jellyfin types (Reconciler, MediaKeys, GuardProcessor) - thread-safe ledger with atomic, debounced persistence - event processing via channel and background worker, seed on first start - scheduled task "Ledger synchronisieren", status API, configuration page - xUnit tests and a Jellyfin 12.2 integration test - Gitea workflows: build/test, release with jprm and manifest branch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
196 lines
8.4 KiB
YAML
196 lines
8.4 KiB
YAML
# Release on tag vA.B.C.D (Jellyfin plugins use four-part versions):
|
|
# 1. test, 2. build the plugin ZIP with jprm (DLL + meta.json only), 3. publish a Gitea release
|
|
# with the ZIP, 4. add the version to manifest.json on the `manifest` branch (the URL Jellyfin reads),
|
|
# 5. check that manifest and ZIP are reachable anonymously and the MD5 matches.
|
|
# The automatic GITEA_TOKEN does all of this; Gitea never starts workflows for pushes made with it,
|
|
# so the manifest commit triggers no further run.
|
|
name: release
|
|
|
|
on:
|
|
push:
|
|
tags: ["v*"]
|
|
|
|
concurrency:
|
|
group: recently-added-guard-release
|
|
cancel-in-progress: false
|
|
|
|
# contents: write = Code (manifest branch) + Releases
|
|
permissions:
|
|
contents: write
|
|
|
|
env:
|
|
MANIFEST_BRANCH: manifest
|
|
JPRM_VERSION: "1.1.0"
|
|
PLUGIN_GUID: 6fdfedef-adde-4ac3-a873-62a057c95574
|
|
|
|
jobs:
|
|
release:
|
|
runs-on: ubuntu-latest
|
|
container:
|
|
image: mcr.microsoft.com/dotnet/sdk:10.0
|
|
timeout-minutes: 30
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
env:
|
|
DOTNET_CLI_TELEMETRY_OPTOUT: "1"
|
|
DOTNET_NOLOGO: "1"
|
|
DEBIAN_FRONTEND: noninteractive
|
|
PYTHONUTF8: "1"
|
|
TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
steps:
|
|
# The SDK image has no Node.js, so actions/checkout cannot run; plain git instead.
|
|
- name: Checkout tag
|
|
run: |
|
|
git config --global --add safe.directory '*'
|
|
git init -q .
|
|
git remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git"
|
|
git config http.extraHeader "Authorization: basic $(printf 'x-access-token:%s' "$TOKEN" | base64 -w0)"
|
|
git fetch -q --tags origin "+refs/heads/*:refs/remotes/origin/*"
|
|
git checkout -q "$GITHUB_SHA"
|
|
git log -1 --oneline
|
|
|
|
- name: Version and changelog
|
|
id: meta
|
|
run: |
|
|
tag="$GITHUB_REF_NAME"
|
|
version="${tag#v}"
|
|
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
echo "Tag $tag hat nicht das Format vA.B.C.D" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Changelog: message of an annotated tag, otherwise the commit subjects since the previous tag.
|
|
: > changelog.md
|
|
if [ "$(git cat-file -t "refs/tags/$tag")" = tag ]; then
|
|
git tag -l --format='%(contents)' "$tag" | sed '/-----BEGIN PGP SIGNATURE-----/,$d' > changelog.md
|
|
fi
|
|
if ! grep -q '[^[:space:]]' changelog.md; then
|
|
previous=$(git describe --tags --abbrev=0 --match 'v*' "$tag^" 2>/dev/null || true)
|
|
git log --no-merges --pretty='- %s' "${previous:+$previous..}$tag" > changelog.md
|
|
fi
|
|
cat changelog.md
|
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Test
|
|
run: dotnet test -c Release
|
|
|
|
- name: Install jprm
|
|
run: |
|
|
apt-get update -qq
|
|
apt-get install -y -qq --no-install-recommends python3-venv > /dev/null
|
|
python3 -m venv /opt/jprm
|
|
/opt/jprm/bin/pip install -q "jprm==$JPRM_VERSION"
|
|
|
|
- name: Build plugin ZIP
|
|
id: build
|
|
env:
|
|
VERSION: ${{ steps.meta.outputs.version }}
|
|
run: |
|
|
# Version and changelog go through build.yaml into meta.json (only this CI checkout changes).
|
|
/opt/jprm/bin/python - "$VERSION" <<'EOF'
|
|
import sys
|
|
import yaml
|
|
|
|
with open("build.yaml", encoding="utf-8") as fh:
|
|
config = yaml.safe_load(fh)
|
|
with open("changelog.md", encoding="utf-8") as fh:
|
|
config["changelog"] = fh.read().strip() or "Version " + sys.argv[1]
|
|
config["version"] = sys.argv[1]
|
|
with open("build.yaml", "w", encoding="utf-8") as fh:
|
|
yaml.safe_dump(config, fh, allow_unicode=True, sort_keys=False)
|
|
EOF
|
|
|
|
mkdir -p artifacts
|
|
zip=$(/opt/jprm/bin/jprm plugin build . --output artifacts --version "$VERSION")
|
|
|
|
contents=$(/opt/jprm/bin/python -c 'import sys, zipfile; print(" ".join(sorted(zipfile.ZipFile(sys.argv[1]).namelist())))' "$zip")
|
|
if [ "$contents" != "Jellyfin.Plugin.RecentlyAddedGuard.dll meta.json" ]; then
|
|
echo "Unerwarteter ZIP-Inhalt: $contents" >&2
|
|
exit 1
|
|
fi
|
|
md5sum "$zip"
|
|
echo "zip=$(realpath "$zip")" >> "$GITHUB_OUTPUT"
|
|
echo "name=$(basename "$zip")" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Publish Gitea release
|
|
id: release
|
|
env:
|
|
VERSION: ${{ steps.meta.outputs.version }}
|
|
ZIP: ${{ steps.build.outputs.zip }}
|
|
ASSET: ${{ steps.build.outputs.name }}
|
|
run: |
|
|
api="$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY"
|
|
auth=(-H "Authorization: token $TOKEN")
|
|
json() { /opt/jprm/bin/python -c "import json, sys; data = json.load(sys.stdin); $1" "${@:2}"; }
|
|
|
|
# A re-run reuses the release of the tag and replaces the asset.
|
|
if ! release=$(curl -fsS "${auth[@]}" "$api/releases/tags/$GITHUB_REF_NAME"); then
|
|
body=$(/opt/jprm/bin/python -c 'import json, sys; print(json.dumps({"tag_name": sys.argv[1], "name": "Recently Added Guard " + sys.argv[2], "body": open("changelog.md", encoding="utf-8").read()}))' "$GITHUB_REF_NAME" "$VERSION")
|
|
release=$(curl -fsS -X POST "${auth[@]}" -H "Content-Type: application/json" -d "$body" "$api/releases")
|
|
fi
|
|
release_id=$(printf '%s' "$release" | json 'print(data["id"])')
|
|
for asset_id in $(printf '%s' "$release" | json '[print(a["id"]) for a in data["assets"] if a["name"] == sys.argv[1]]' "$ASSET"); do
|
|
curl -fsS -X DELETE "${auth[@]}" "$api/releases/$release_id/assets/$asset_id"
|
|
done
|
|
|
|
asset=$(curl -fsS -X POST "${auth[@]}" -F "attachment=@$ZIP" "$api/releases/$release_id/assets?name=$ASSET")
|
|
url=$(printf '%s' "$asset" | json 'print(data["browser_download_url"])')
|
|
echo "Release-Asset: $url"
|
|
echo "url=$url" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Update manifest.json
|
|
env:
|
|
VERSION: ${{ steps.meta.outputs.version }}
|
|
ZIP: ${{ steps.build.outputs.zip }}
|
|
URL: ${{ steps.release.outputs.url }}
|
|
run: |
|
|
workdir=$(mktemp -d)
|
|
if git show-ref --verify --quiet "refs/remotes/origin/$MANIFEST_BRANCH"; then
|
|
git worktree add -q --detach "$workdir" "origin/$MANIFEST_BRANCH"
|
|
else
|
|
git worktree add -q --orphan -b "$MANIFEST_BRANCH" "$workdir"
|
|
fi
|
|
|
|
cd "$workdir"
|
|
[ -f manifest.json ] || echo '[]' > manifest.json
|
|
# Adds the version in front (sorted descending) and keeps older versions; MD5 of the ZIP as checksum.
|
|
/opt/jprm/bin/jprm repo add manifest.json "$ZIP" --plugin-url "$URL"
|
|
git add manifest.json
|
|
if git diff --cached --quiet; then
|
|
echo "manifest.json unverändert"
|
|
exit 0
|
|
fi
|
|
git -c user.name="Gitea Actions" -c user.email="actions@noreply.git.menzel.center" \
|
|
commit -q -m "manifest: Recently Added Guard $VERSION"
|
|
git push -q origin "HEAD:refs/heads/$MANIFEST_BRANCH"
|
|
|
|
- name: Verify anonymous download and checksum
|
|
env:
|
|
VERSION: ${{ steps.meta.outputs.version }}
|
|
URL: ${{ steps.release.outputs.url }}
|
|
run: |
|
|
manifest_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/raw/branch/$MANIFEST_BRANCH/manifest.json"
|
|
# No credentials: Jellyfin fetches both anonymously.
|
|
curl -fsSL "$manifest_url" -o /tmp/manifest.json
|
|
curl -fsSL "$URL" -o /tmp/plugin.zip
|
|
/opt/jprm/bin/python - "$VERSION" "$URL" <<'EOF'
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
version, url = sys.argv[1], sys.argv[2]
|
|
with open("/tmp/manifest.json", encoding="utf-8") as fh:
|
|
manifest = json.load(fh)
|
|
plugin = next(p for p in manifest if p["guid"] == os.environ["PLUGIN_GUID"])
|
|
entry = next(v for v in plugin["versions"] if v["version"] == version)
|
|
with open("/tmp/plugin.zip", "rb") as fh:
|
|
md5 = hashlib.md5(fh.read()).hexdigest()
|
|
assert plugin["versions"][0]["version"] == version, "neue Version steht nicht vorne"
|
|
assert entry["sourceUrl"] == url, (entry["sourceUrl"], url)
|
|
assert entry["checksum"] == md5, (entry["checksum"], md5)
|
|
print(f"OK: {version} -> {url}, MD5 {md5}, targetAbi {entry['targetAbi']}")
|
|
EOF
|
|
echo "Repository-URL für Jellyfin: $manifest_url"
|