From 81fda874e9c325ecc5e2a49637cf9492e0350d23 Mon Sep 17 00:00:00 2001 From: enyineer Date: Sun, 8 Mar 2026 09:25:07 +0100 Subject: [PATCH 1/8] ci: Add GitHub Actions workflow to automatically update manifest.json on release. Signed-off-by: enyineer --- .github/workflows/update-manifest.yml | 107 ++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 .github/workflows/update-manifest.yml diff --git a/.github/workflows/update-manifest.yml b/.github/workflows/update-manifest.yml new file mode 100644 index 0000000..924b969 --- /dev/null +++ b/.github/workflows/update-manifest.yml @@ -0,0 +1,107 @@ +name: Update manifest.json on release + +on: + release: + types: [published] + +permissions: + contents: write + +jobs: + update-manifest: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Extract version from release tag + id: version + run: | + TAG="${{ github.event.release.tag_name }}" + # Strip leading 'v' if present + VERSION="${TAG#v}" + # Ensure four-part version (append .0 if needed) + DOTS=$(echo "$VERSION" | tr -cd '.' | wc -c | tr -d ' ') + if [ "$DOTS" -eq 2 ]; then + VERSION="${VERSION}.0" + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION" + + - name: Extract targetAbi from .csproj + id: abi + run: | + # Pull the Jellyfin.Controller PackageReference version from the .csproj + ABI=$(grep -oP 'Jellyfin\.Controller"[^>]*Version="\K[^"]+' backend/Moonfin.Server.csproj) + echo "targetAbi=${ABI}.0" >> "$GITHUB_OUTPUT" + echo "Target ABI: ${ABI}.0" + + - name: Download release zip asset + id: asset + env: + GH_TOKEN: ${{ github.token }} + RELEASE_ASSETS: ${{ toJSON(github.event.release.assets) }} + run: | + # Find the first .zip asset attached to the release + ASSET_URL=$(echo "$RELEASE_ASSETS" | jq -r '.[] | select(.name | endswith(".zip")) | .browser_download_url' | head -1) + + if [ -z "$ASSET_URL" ]; then + echo "::error::No .zip asset found on this release" + exit 1 + fi + + ASSET_NAME=$(basename "$ASSET_URL") + echo "Downloading $ASSET_NAME from $ASSET_URL" + curl -fSL -o "$ASSET_NAME" "$ASSET_URL" + + echo "asset_name=$ASSET_NAME" >> "$GITHUB_OUTPUT" + echo "source_url=$ASSET_URL" >> "$GITHUB_OUTPUT" + + - name: Compute MD5 checksum + id: checksum + run: | + CHECKSUM=$(md5sum "${{ steps.asset.outputs.asset_name }}" | awk '{print toupper($1)}') + echo "checksum=$CHECKSUM" >> "$GITHUB_OUTPUT" + echo "MD5: $CHECKSUM" + + - name: Format timestamp + id: timestamp + env: + RELEASE_CREATED_AT: ${{ github.event.release.created_at }} + run: | + # Convert release created_at to YYYY-MM-DDTHH:MM:SS (no timezone suffix) + TIMESTAMP=$(date -u -d "$RELEASE_CREATED_AT" +"%Y-%m-%dT%H:%M:%S") + echo "timestamp=$TIMESTAMP" >> "$GITHUB_OUTPUT" + echo "Timestamp: $TIMESTAMP" + + - name: Update manifest.json + env: + RELEASE_BODY: ${{ github.event.release.body }} + run: | + jq --arg ver "${{ steps.version.outputs.version }}" \ + --arg abi "${{ steps.abi.outputs.targetAbi }}" \ + --arg url "${{ steps.asset.outputs.source_url }}" \ + --arg sum "${{ steps.checksum.outputs.checksum }}" \ + --arg time "${{ steps.timestamp.outputs.timestamp }}" \ + --arg log "$RELEASE_BODY" \ + '.[0].versions = [{ + version: $ver, + changelog: $log, + targetAbi: $abi, + sourceUrl: $url, + checksum: $sum, + timestamp: $time, + dependencies: [] + }] + .[0].versions' \ + manifest.json > manifest.tmp && mv manifest.tmp manifest.json + + echo "Updated manifest.json — new version ${{ steps.version.outputs.version }} prepended" + + - name: Commit and push + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add manifest.json + git diff --cached --quiet && echo "No changes to commit" && exit 0 + git commit -m "chore: update manifest.json for v${{ steps.version.outputs.version }}" + git push From 7a33d8b00c3845240c3b3387cdc3d54f88ba91f1 Mon Sep 17 00:00:00 2001 From: enyineer Date: Sun, 8 Mar 2026 09:29:04 +0100 Subject: [PATCH 2/8] fix: checkout default branch to avoid detached HEAD on release event --- .github/workflows/update-manifest.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/update-manifest.yml b/.github/workflows/update-manifest.yml index 924b969..bdc8d99 100644 --- a/.github/workflows/update-manifest.yml +++ b/.github/workflows/update-manifest.yml @@ -13,6 +13,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + ref: ${{ github.event.repository.default_branch }} - name: Extract version from release tag id: version From f602e86e9379f710a8aca4177f74d4f1594acc57 Mon Sep 17 00:00:00 2001 From: enyineer Date: Sun, 8 Mar 2026 09:35:21 +0100 Subject: [PATCH 3/8] fix: refine release asset selection in workflow to specifically target "Moonfin.Server-" zip files Signed-off-by: enyineer --- .github/workflows/update-manifest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-manifest.yml b/.github/workflows/update-manifest.yml index bdc8d99..78d6cf4 100644 --- a/.github/workflows/update-manifest.yml +++ b/.github/workflows/update-manifest.yml @@ -45,7 +45,7 @@ jobs: RELEASE_ASSETS: ${{ toJSON(github.event.release.assets) }} run: | # Find the first .zip asset attached to the release - ASSET_URL=$(echo "$RELEASE_ASSETS" | jq -r '.[] | select(.name | endswith(".zip")) | .browser_download_url' | head -1) + ASSET_URL=$(echo "$RELEASE_ASSETS" | jq -r '.[] | select(.name | startswith("Moonfin.Server-") and endswith(".zip")) | .browser_download_url' | head -1) if [ -z "$ASSET_URL" ]; then echo "::error::No .zip asset found on this release" From 9b1bf2d8ab4b37ea5c7a09ca386b5c80427b9256 Mon Sep 17 00:00:00 2001 From: enyineer Date: Sun, 8 Mar 2026 09:42:11 +0100 Subject: [PATCH 4/8] feat: Extend manifest update workflow to handle release deletion and upsert version entries for published or edited releases. Signed-off-by: enyineer --- .github/workflows/update-manifest.yml | 64 ++++++++++++++++++++------- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/.github/workflows/update-manifest.yml b/.github/workflows/update-manifest.yml index 78d6cf4..1e7bba9 100644 --- a/.github/workflows/update-manifest.yml +++ b/.github/workflows/update-manifest.yml @@ -2,7 +2,7 @@ name: Update manifest.json on release on: release: - types: [published] + types: [published, edited, deleted] permissions: contents: write @@ -30,25 +30,38 @@ jobs: echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "Version: $VERSION" + # ── Delete: remove the version entry ────────────────────────────── + + - name: Remove version from manifest.json + if: github.event.action == 'deleted' + run: | + jq --arg ver "${{ steps.version.outputs.version }}" \ + '.[0].versions |= map(select(.version != $ver))' \ + manifest.json > manifest.tmp && mv manifest.tmp manifest.json + + echo "Removed version ${{ steps.version.outputs.version }} from manifest.json" + + # ── Published / Edited: upsert the version entry ────────────────── + - name: Extract targetAbi from .csproj + if: github.event.action != 'deleted' id: abi run: | - # Pull the Jellyfin.Controller PackageReference version from the .csproj ABI=$(grep -oP 'Jellyfin\.Controller"[^>]*Version="\K[^"]+' backend/Moonfin.Server.csproj) echo "targetAbi=${ABI}.0" >> "$GITHUB_OUTPUT" echo "Target ABI: ${ABI}.0" - name: Download release zip asset + if: github.event.action != 'deleted' id: asset env: GH_TOKEN: ${{ github.token }} RELEASE_ASSETS: ${{ toJSON(github.event.release.assets) }} run: | - # Find the first .zip asset attached to the release ASSET_URL=$(echo "$RELEASE_ASSETS" | jq -r '.[] | select(.name | startswith("Moonfin.Server-") and endswith(".zip")) | .browser_download_url' | head -1) if [ -z "$ASSET_URL" ]; then - echo "::error::No .zip asset found on this release" + echo "::error::No Moonfin.Server-*.zip asset found on this release" exit 1 fi @@ -60,6 +73,7 @@ jobs: echo "source_url=$ASSET_URL" >> "$GITHUB_OUTPUT" - name: Compute MD5 checksum + if: github.event.action != 'deleted' id: checksum run: | CHECKSUM=$(md5sum "${{ steps.asset.outputs.asset_name }}" | awk '{print toupper($1)}') @@ -67,16 +81,17 @@ jobs: echo "MD5: $CHECKSUM" - name: Format timestamp + if: github.event.action != 'deleted' id: timestamp env: RELEASE_CREATED_AT: ${{ github.event.release.created_at }} run: | - # Convert release created_at to YYYY-MM-DDTHH:MM:SS (no timezone suffix) TIMESTAMP=$(date -u -d "$RELEASE_CREATED_AT" +"%Y-%m-%dT%H:%M:%S") echo "timestamp=$TIMESTAMP" >> "$GITHUB_OUTPUT" echo "Timestamp: $TIMESTAMP" - - name: Update manifest.json + - name: Upsert version in manifest.json + if: github.event.action != 'deleted' env: RELEASE_BODY: ${{ github.event.release.body }} run: | @@ -86,24 +101,39 @@ jobs: --arg sum "${{ steps.checksum.outputs.checksum }}" \ --arg time "${{ steps.timestamp.outputs.timestamp }}" \ --arg log "$RELEASE_BODY" \ - '.[0].versions = [{ - version: $ver, - changelog: $log, - targetAbi: $abi, - sourceUrl: $url, - checksum: $sum, - timestamp: $time, - dependencies: [] - }] + .[0].versions' \ + '.[0].versions |= ( + map(select(.version != $ver)) | + [{ + version: $ver, + changelog: $log, + targetAbi: $abi, + sourceUrl: $url, + checksum: $sum, + timestamp: $time, + dependencies: [] + }] + . + )' \ manifest.json > manifest.tmp && mv manifest.tmp manifest.json - echo "Updated manifest.json — new version ${{ steps.version.outputs.version }} prepended" + echo "Upserted version ${{ steps.version.outputs.version }} in manifest.json" + + # ── Commit ──────────────────────────────────────────────────────── - name: Commit and push run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add manifest.json + + ACTION="${{ github.event.action }}" + VERSION="${{ steps.version.outputs.version }}" + + if [ "$ACTION" = "deleted" ]; then + MSG="chore: remove v${VERSION} from manifest.json" + else + MSG="chore: update manifest.json for v${VERSION}" + fi + git diff --cached --quiet && echo "No changes to commit" && exit 0 - git commit -m "chore: update manifest.json for v${{ steps.version.outputs.version }}" + git commit -m "$MSG" git push From 55efb4b7733414bb9982caf9466ec4623968a485 Mon Sep 17 00:00:00 2001 From: enyineer Date: Sun, 8 Mar 2026 09:56:52 +0100 Subject: [PATCH 5/8] feat: sanitize markdown from release body for plain-text changelog --- .github/workflows/update-manifest.yml | 30 +++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-manifest.yml b/.github/workflows/update-manifest.yml index 1e7bba9..c876720 100644 --- a/.github/workflows/update-manifest.yml +++ b/.github/workflows/update-manifest.yml @@ -90,17 +90,43 @@ jobs: echo "timestamp=$TIMESTAMP" >> "$GITHUB_OUTPUT" echo "Timestamp: $TIMESTAMP" - - name: Upsert version in manifest.json + - name: Sanitize changelog if: github.event.action != 'deleted' + id: changelog env: RELEASE_BODY: ${{ github.event.release.body }} run: | + # Convert markdown release body to clean plain text for Jellyfin manifest + CHANGELOG=$(echo "$RELEASE_BODY" | sed -E \ + -e 's/\[!\[[^]]*\]\([^)]*\)\]\([^)]*\)//g' \ + -e 's/!\[[^]]*\]\([^)]*\)//g' \ + -e 's/\[([^]]*)\]\([^)]*\)/\1/g' \ + -e 's/\*\*([^*]*)\*\*/\1/g' \ + -e 's/`([^`]*)`/\1/g' \ + -e 's/^#{1,6} +//' \ + -e 's/[[:space:]]+$//' \ + -e '/^---$/d' \ + -e '/^[[:space:]]*$/{ N; /^\n[[:space:]]*$/d; }' \ + ) + # Remove leading/trailing blank lines + CHANGELOG=$(echo "$CHANGELOG" | sed -e '/./,$!d' -e :a -e '/^\n*$/{$d;N;ba' -e '}') + + # Write to file to safely pass multi-line content + echo "$CHANGELOG" > /tmp/changelog.txt + echo "Sanitized changelog:" + cat /tmp/changelog.txt + + - name: Upsert version in manifest.json + if: github.event.action != 'deleted' + run: | + CHANGELOG=$(cat /tmp/changelog.txt) + jq --arg ver "${{ steps.version.outputs.version }}" \ --arg abi "${{ steps.abi.outputs.targetAbi }}" \ --arg url "${{ steps.asset.outputs.source_url }}" \ --arg sum "${{ steps.checksum.outputs.checksum }}" \ --arg time "${{ steps.timestamp.outputs.timestamp }}" \ - --arg log "$RELEASE_BODY" \ + --arg log "$CHANGELOG" \ '.[0].versions |= ( map(select(.version != $ver)) | [{ From 428b1e250e5090f52a8cbb20173be86341a1db95 Mon Sep 17 00:00:00 2001 From: enyineer Date: Sun, 8 Mar 2026 09:58:25 +0100 Subject: [PATCH 6/8] fix: add git pull --rebase before push to handle race conditions --- .github/workflows/update-manifest.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/update-manifest.yml b/.github/workflows/update-manifest.yml index c876720..bf80ed5 100644 --- a/.github/workflows/update-manifest.yml +++ b/.github/workflows/update-manifest.yml @@ -162,4 +162,5 @@ jobs: git diff --cached --quiet && echo "No changes to commit" && exit 0 git commit -m "$MSG" + git pull --rebase git push From b11e2cc81931015a37462a09db87530ef35f950c Mon Sep 17 00:00:00 2001 From: enyineer Date: Sun, 8 Mar 2026 10:00:34 +0100 Subject: [PATCH 7/8] fix: use piped sed for reliable markdown stripping on Ubuntu --- .github/workflows/update-manifest.yml | 46 +++++++++++++++++---------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/.github/workflows/update-manifest.yml b/.github/workflows/update-manifest.yml index bf80ed5..d8ba0d5 100644 --- a/.github/workflows/update-manifest.yml +++ b/.github/workflows/update-manifest.yml @@ -96,23 +96,35 @@ jobs: env: RELEASE_BODY: ${{ github.event.release.body }} run: | - # Convert markdown release body to clean plain text for Jellyfin manifest - CHANGELOG=$(echo "$RELEASE_BODY" | sed -E \ - -e 's/\[!\[[^]]*\]\([^)]*\)\]\([^)]*\)//g' \ - -e 's/!\[[^]]*\]\([^)]*\)//g' \ - -e 's/\[([^]]*)\]\([^)]*\)/\1/g' \ - -e 's/\*\*([^*]*)\*\*/\1/g' \ - -e 's/`([^`]*)`/\1/g' \ - -e 's/^#{1,6} +//' \ - -e 's/[[:space:]]+$//' \ - -e '/^---$/d' \ - -e '/^[[:space:]]*$/{ N; /^\n[[:space:]]*$/d; }' \ - ) - # Remove leading/trailing blank lines - CHANGELOG=$(echo "$CHANGELOG" | sed -e '/./,$!d' -e :a -e '/^\n*$/{$d;N;ba' -e '}') - - # Write to file to safely pass multi-line content - echo "$CHANGELOG" > /tmp/changelog.txt + # Write release body to file to preserve multi-line content + printf '%s' "$RELEASE_BODY" > /tmp/raw_changelog.txt + + # Convert markdown release body to clean plain text for Jellyfin manifest: + # 1. Strip carriage returns + # 2. Remove badge images: [![alt](img)](url) + # 3. Remove inline images: ![alt](url) + # 4. Convert links to text: [text](url) → text + # 5. Strip bold markers: **text** → text + # 6. Strip inline code: `text` → text + # 7. Strip header prefixes: ## Title → Title + # 8. Remove horizontal rules: --- + # 9. Trim trailing whitespace + # 10. Remove leading/trailing blank lines + # 11. Collapse consecutive blank lines + sed -i 's/\r$//' /tmp/raw_changelog.txt + cat /tmp/raw_changelog.txt \ + | sed -E 's/\[!\[[^]]*\]\([^)]*\)\]\([^)]*\)//g' \ + | sed -E 's/!\[[^]]*\]\([^)]*\)//g' \ + | sed -E 's/\[([^]]*)\]\([^)]*\)/\1/g' \ + | sed -E 's/\*\*([^*]*)\*\*/\1/g' \ + | sed -E 's/`([^`]*)`/\1/g' \ + | sed -E 's/^#{1,6} +//' \ + | sed -E 's/[[:space:]]+$//' \ + | sed -E '/^---$/d' \ + | sed -E '/^$/N;/^\n$/d' \ + | sed '/./,$!d' \ + > /tmp/changelog.txt + echo "Sanitized changelog:" cat /tmp/changelog.txt From f7b4689f633971616426d421ab02c60a854c6315 Mon Sep 17 00:00:00 2001 From: enyineer Date: Sun, 8 Mar 2026 10:03:57 +0100 Subject: [PATCH 8/8] feat: add concurrency configuration to the update-manifest workflow. Signed-off-by: enyineer --- .github/workflows/update-manifest.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/update-manifest.yml b/.github/workflows/update-manifest.yml index d8ba0d5..7a6292b 100644 --- a/.github/workflows/update-manifest.yml +++ b/.github/workflows/update-manifest.yml @@ -7,6 +7,10 @@ on: permissions: contents: write +concurrency: + group: update-manifest + cancel-in-progress: false + jobs: update-manifest: runs-on: ubuntu-latest