# 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"