diff --git a/.gitea/workflows/release_jellyfin_plugin.yml b/.gitea/workflows/release_jellyfin_plugin.yml index 6b99918..30f2b88 100644 --- a/.gitea/workflows/release_jellyfin_plugin.yml +++ b/.gitea/workflows/release_jellyfin_plugin.yml @@ -1,174 +1,201 @@ -name: Release Jellyfin plugin +name: Build and Release Jellyfin plugins on: push: - tags: - - "jellyfin/*/v*" + paths: + - "Jellyfin/**" jobs: release: runs-on: ubuntu-latest + # Avoid rebuild loops from catalog commits + if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} + steps: - name: Checkout uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 + fetch-depth: 2 - - name: Parse tag - id: meta + - name: Detect changed plugins + id: detect run: | set -euo pipefail - TAG="${GITHUB_REF_NAME}" - # jellyfin//v1.2.3 - if [[ ! "$TAG" =~ ^jellyfin/([a-z0-9-]+)/v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then - echo "Tag must match jellyfin//vX.Y.Z (got: $TAG)" - exit 1 + + changed=$(git diff --name-only HEAD~1 HEAD -- 'Jellyfin/' || true) + if [[ -z "$changed" ]]; then + echo "No Jellyfin changes" + echo "plugins=" >> "$GITHUB_OUTPUT" + exit 0 fi - SLUG="${BASH_REMATCH[1]}" - VERSION="${BASH_REMATCH[2]}" - PLUGIN_DIR="Jellyfin/${SLUG}" - if [[ ! -f "${PLUGIN_DIR}/build.yaml" ]]; then - echo "Missing ${PLUGIN_DIR}/build.yaml" - exit 1 + + # Ignore catalog-only pushes + non_manifest=$(echo "$changed" | grep -v '^Jellyfin/manifest.json$' || true) + if [[ -z "$non_manifest" ]]; then + echo "Only Jellyfin/manifest.json changed — skipping" + echo "plugins=" >> "$GITHUB_OUTPUT" + exit 0 fi - echo "slug=${SLUG}" >> "$GITHUB_OUTPUT" - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "plugin_dir=${PLUGIN_DIR}" >> "$GITHUB_OUTPUT" - echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - echo "version4=${VERSION}.0" >> "$GITHUB_OUTPUT" + + plugins=$(echo "$non_manifest" \ + | awk -F/ '$1=="Jellyfin" && NF>=2 {print $2}' \ + | grep -v '^_template$' \ + | grep -v '^README.md$' \ + | sort -u) + + to_release="" + for slug in $plugins; do + dir="Jellyfin/${slug}" + if [[ -f "${dir}/build.yaml" && -f "${dir}/version" ]]; then + to_release="${to_release}${slug}"$'\n' + elif [[ -d "$dir" ]]; then + echo "Skipping ${slug}: need build.yaml + version file" + fi + done + + to_release=$(printf '%s' "$to_release" | sed '/^$/d') + echo "Changed plugins to release:" + echo "$to_release" + { + echo "plugins<> "$GITHUB_OUTPUT" - name: Setup .NET + if: steps.detect.outputs.plugins != '' uses: actions/setup-dotnet@v4 with: dotnet-version: "9.0.x" - name: Install jprm + if: steps.detect.outputs.plugins != '' run: | python3 -m venv /tmp/jprm-venv /tmp/jprm-venv/bin/pip install --upgrade pip jprm - - name: Build plugin package - id: build - run: | - set -euo pipefail - PLUGIN_DIR="${{ steps.meta.outputs.plugin_dir }}" - mkdir -p "${PLUGIN_DIR}/artifacts" - sed -i -E "s/^version:.*/version: \"${{ steps.meta.outputs.version4 }}\"/" "${PLUGIN_DIR}/build.yaml" - /tmp/jprm-venv/bin/jprm --verbosity=info plugin build "${PLUGIN_DIR}" --output "${PLUGIN_DIR}/artifacts" - ARTIFACT=$(find "${PLUGIN_DIR}/artifacts" -type f -name '*.zip' | head -n1) - if [[ -z "$ARTIFACT" ]]; then - echo "No zip produced by jprm" - ls -laR "${PLUGIN_DIR}/artifacts" || true - exit 1 - fi - NAME=$(basename "$ARTIFACT") - CHECKSUM=$(sha256sum "$ARTIFACT" | awk '{print $1}') - echo "artifact=${ARTIFACT}" >> "$GITHUB_OUTPUT" - echo "artifact_name=${NAME}" >> "$GITHUB_OUTPUT" - echo "checksum=${CHECKSUM}" >> "$GITHUB_OUTPUT" - echo "Built ${NAME} sha256=${CHECKSUM}" - - - name: Create Gitea release and upload asset - id: release + - name: Build and release plugins + if: steps.detect.outputs.plugins != '' env: GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PLUGINS: ${{ steps.detect.outputs.plugins }} run: | set -euo pipefail + GITEA_URL="${{ github.server_url }}" REPO_OWNER="${{ github.repository_owner }}" REPO_NAME="${{ github.event.repository.name }}" - TAG="${{ steps.meta.outputs.tag }}" - VERSION="${{ steps.meta.outputs.version }}" - SLUG="${{ steps.meta.outputs.slug }}" - ARTIFACT="${{ steps.build.outputs.artifact }}" - ARTIFACT_NAME="${{ steps.build.outputs.artifact_name }}" - - BODY=$(printf 'Jellyfin plugin **%s** %s\n\nAdd the raw Jellyfin/manifest.json URL from this repo as a plugin repository.' \ - "$SLUG" "$VERSION") - BODY_JSON=$(printf '%s' "$BODY" | jq -Rs .) - - RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST \ - -H "Authorization: token ${GITEA_TOKEN}" \ - -H "Content-Type: application/json" \ - "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases" \ - -d "{ - \"tag_name\": \"${TAG}\", - \"name\": \"${SLUG} ${VERSION}\", - \"body\": ${BODY_JSON}, - \"draft\": false, - \"prerelease\": false - }") - HTTP_CODE=$(echo "$RESPONSE" | tail -n1) - RESP_BODY=$(echo "$RESPONSE" | sed '$d') - if [[ "$HTTP_CODE" != "201" && "$HTTP_CODE" != "200" ]]; then - echo "Failed to create release: HTTP $HTTP_CODE" - echo "$RESP_BODY" - exit 1 - fi - RELEASE_ID=$(echo "$RESP_BODY" | jq -r '.id') - echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT" - - curl -sS -f -X POST \ - -H "Authorization: token ${GITEA_TOKEN}" \ - -H "Content-Type: application/zip" \ - --data-binary "@${ARTIFACT}" \ - "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}/assets?name=${ARTIFACT_NAME}" - - DOWNLOAD_URL="${GITEA_URL}/${REPO_OWNER}/${REPO_NAME}/releases/download/${TAG}/${ARTIFACT_NAME}" - echo "download_url=${DOWNLOAD_URL}" >> "$GITHUB_OUTPUT" - - - name: Update Jellyfin/manifest.json - env: - GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - SLUG="${{ steps.meta.outputs.slug }}" - VERSION="${{ steps.meta.outputs.version4 }}" - CHECKSUM="${{ steps.build.outputs.checksum }}" - DOWNLOAD_URL="${{ steps.release.outputs.download_url }}" - TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - PLUGIN_DIR="${{ steps.meta.outputs.plugin_dir }}" - GUID=$(grep -E '^guid:' "${PLUGIN_DIR}/build.yaml" | head -1 | sed -E 's/guid:[[:space:]]*"?([^"]*)"?/\1/') - TARGET_ABI=$(grep -E '^targetAbi:' "${PLUGIN_DIR}/build.yaml" | head -1 | sed -E 's/targetAbi:[[:space:]]*"?([^"]*)"?/\1/') - - NEW_VERSION=$(jq -n \ - --arg version "$VERSION" \ - --arg changelog "Release ${VERSION}" \ - --arg targetAbi "$TARGET_ABI" \ - --arg sourceUrl "$DOWNLOAD_URL" \ - --arg checksum "$CHECKSUM" \ - --arg timestamp "$TIMESTAMP" \ - '{ - version: $version, - changelog: $changelog, - targetAbi: $targetAbi, - sourceUrl: $sourceUrl, - checksum: $checksum, - timestamp: $timestamp - }') - - jq --arg guid "$GUID" --argjson newver "$NEW_VERSION" ' - map(if .guid == $guid then .versions = ([$newver] + (.versions // [])) else . end) - ' Jellyfin/manifest.json > Jellyfin/manifest.json.tmp - mv Jellyfin/manifest.json.tmp Jellyfin/manifest.json git config user.name "gitea-actions" git config user.email "actions@local" - # Detach from tag checkout: update default branch - DEFAULT_BRANCH=$(git remote show origin | sed -n '/HEAD branch/s/.*: //p') - git fetch origin "$DEFAULT_BRANCH" - git checkout -B "$DEFAULT_BRANCH" "origin/${DEFAULT_BRANCH}" - # Re-apply manifest change if checkout overwrote - jq --arg guid "$GUID" --argjson newver "$NEW_VERSION" ' - map(if .guid == $guid then .versions = ([$newver] + (.versions // [])) else . end) - ' Jellyfin/manifest.json > Jellyfin/manifest.json.tmp - mv Jellyfin/manifest.json.tmp Jellyfin/manifest.json - git add Jellyfin/manifest.json - if git diff --staged --quiet; then - echo "Manifest already up to date" - exit 0 + manifest_dirty=0 + + while IFS= read -r SLUG; do + [[ -z "$SLUG" ]] && continue + + PLUGIN_DIR="Jellyfin/${SLUG}" + VERSION=$(tr -d '[:space:]' < "${PLUGIN_DIR}/version") + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Invalid version in ${PLUGIN_DIR}/version: '${VERSION}' (expected X.Y.Z)" + exit 1 + fi + VERSION4="${VERSION}.0" + TAG="jellyfin/${SLUG}/v${VERSION}" + + echo "::group::Release ${SLUG} ${VERSION}" + + # Skip if this version was already released + HTTP=$(curl -sS -o /tmp/rel.json -w "%{http_code}" \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/tags/${TAG}") + if [[ "$HTTP" == "200" ]]; then + echo "Release ${TAG} already exists — skipping build" + echo "::endgroup::" + continue + fi + + sed -i -E "s/^version:.*/version: \"${VERSION4}\"/" "${PLUGIN_DIR}/build.yaml" + mkdir -p "${PLUGIN_DIR}/artifacts" + rm -f "${PLUGIN_DIR}/artifacts"/*.zip + + /tmp/jprm-venv/bin/jprm --verbosity=info plugin build "${PLUGIN_DIR}" --output "${PLUGIN_DIR}/artifacts" + ARTIFACT=$(find "${PLUGIN_DIR}/artifacts" -type f -name '*.zip' | head -n1) + if [[ -z "$ARTIFACT" ]]; then + echo "No zip produced for ${SLUG}" + ls -laR "${PLUGIN_DIR}/artifacts" || true + exit 1 + fi + ARTIFACT_NAME=$(basename "$ARTIFACT") + CHECKSUM=$(sha256sum "$ARTIFACT" | awk '{print $1}') + + BODY=$(printf 'Jellyfin plugin **%s** %s\n\nBuilt automatically from Jellyfin/%s changes.' "$SLUG" "$VERSION" "$SLUG") + BODY_JSON=$(printf '%s' "$BODY" | jq -Rs .) + + RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases" \ + -d "{ + \"tag_name\": \"${TAG}\", + \"name\": \"${SLUG} ${VERSION}\", + \"body\": ${BODY_JSON}, + \"draft\": false, + \"prerelease\": false + }") + HTTP_CODE=$(echo "$RESPONSE" | tail -n1) + RESP_BODY=$(echo "$RESPONSE" | sed '$d') + if [[ "$HTTP_CODE" != "201" && "$HTTP_CODE" != "200" ]]; then + echo "Failed to create release ${TAG}: HTTP $HTTP_CODE" + echo "$RESP_BODY" + exit 1 + fi + RELEASE_ID=$(echo "$RESP_BODY" | jq -r '.id') + + curl -sS -f -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/zip" \ + --data-binary "@${ARTIFACT}" \ + "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}/assets?name=${ARTIFACT_NAME}" + + DOWNLOAD_URL="${GITEA_URL}/${REPO_OWNER}/${REPO_NAME}/releases/download/${TAG}/${ARTIFACT_NAME}" + TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + GUID=$(grep -E '^guid:' "${PLUGIN_DIR}/build.yaml" | head -1 | sed -E 's/guid:[[:space:]]*"?([^"]*)"?/\1/') + TARGET_ABI=$(grep -E '^targetAbi:' "${PLUGIN_DIR}/build.yaml" | head -1 | sed -E 's/targetAbi:[[:space:]]*"?([^"]*)"?/\1/') + + NEW_VERSION=$(jq -n \ + --arg version "$VERSION4" \ + --arg changelog "Release ${VERSION}" \ + --arg targetAbi "$TARGET_ABI" \ + --arg sourceUrl "$DOWNLOAD_URL" \ + --arg checksum "$CHECKSUM" \ + --arg timestamp "$TIMESTAMP" \ + '{ + version: $version, + changelog: $changelog, + targetAbi: $targetAbi, + sourceUrl: $sourceUrl, + checksum: $checksum, + timestamp: $timestamp + }') + + jq --arg guid "$GUID" --argjson newver "$NEW_VERSION" ' + map(if .guid == $guid then .versions = ([$newver] + (.versions // [])) else . end) + ' Jellyfin/manifest.json > Jellyfin/manifest.json.tmp + mv Jellyfin/manifest.json.tmp Jellyfin/manifest.json + manifest_dirty=1 + + echo "Released ${TAG} → ${DOWNLOAD_URL}" + echo "::endgroup::" + done <<< "$PLUGINS" + + if [[ "$manifest_dirty" -eq 1 ]]; then + git add Jellyfin/manifest.json + # Also commit synced build.yaml version lines if changed + git add Jellyfin/*/build.yaml 2>/dev/null || true + if ! git diff --staged --quiet; then + git commit -m "chore(jellyfin): update plugin catalog [skip ci]" + git push + fi fi - git commit -m "chore(jellyfin): publish ${SLUG} ${VERSION} to catalog" - git push origin "$DEFAULT_BRANCH" diff --git a/.gitea/workflows/scaffold_jellyfin_plugin.yml b/.gitea/workflows/scaffold_jellyfin_plugin.yml index 49408c5..6047493 100644 --- a/.gitea/workflows/scaffold_jellyfin_plugin.yml +++ b/.gitea/workflows/scaffold_jellyfin_plugin.yml @@ -85,6 +85,7 @@ jobs: replace Jellyfin/_template/build.yaml.template "${TARGET}/build.yaml" replace Jellyfin/_template/README.md.template "${TARGET}/README.md" + echo "1.0.0" > "${TARGET}/version" replace Jellyfin/_template/Plugin/Plugin.csproj.template "${TARGET}/${ASSEMBLY}/${ASSEMBLY}.csproj" replace Jellyfin/_template/Plugin/Plugin.cs.template "${TARGET}/${ASSEMBLY}/Plugin.cs" replace Jellyfin/_template/Plugin/Configuration/PluginConfiguration.cs.template \ diff --git a/Jellyfin/README.md b/Jellyfin/README.md index 1a972b0..5787c8c 100644 --- a/Jellyfin/README.md +++ b/Jellyfin/README.md @@ -21,4 +21,6 @@ https://///raw/branch//Jellyfin/manifest.json ## Workflows - **Scaffold:** `.gitea/workflows/scaffold_jellyfin_plugin.yml` (`workflow_dispatch`) -- **Release:** `.gitea/workflows/release_jellyfin_plugin.yml` (tag `jellyfin//vX.Y.Z`) +- **Release:** `.gitea/workflows/release_jellyfin_plugin.yml` — on push to `Jellyfin/**`, builds each changed plugin that has `build.yaml` + `version` + +Bump `Jellyfin//version` before pushing when you want a new release. Re-pushing the same version skips (release already exists). \ No newline at end of file diff --git a/Jellyfin/_template/README.md.template b/Jellyfin/_template/README.md.template index a36bf03..f6e3ca3 100644 --- a/Jellyfin/_template/README.md.template +++ b/Jellyfin/_template/README.md.template @@ -10,12 +10,10 @@ dotnet build -c Release ## Release -Tag and push: +1. Bump the `version` file (e.g. `1.0.1`) +2. Push changes under `Jellyfin/{{PLUGIN_SLUG}}/` -```bash -git tag jellyfin/{{PLUGIN_SLUG}}/v1.0.0 -git push origin jellyfin/{{PLUGIN_SLUG}}/v1.0.0 -``` +CI detects changed plugins, builds with jprm, creates a Gitea release, and updates `Jellyfin/manifest.json`. Install from the aggregated catalog: diff --git a/Jellyfin/jellyfin-plugin-personal-recordings/README.md b/Jellyfin/jellyfin-plugin-personal-recordings/README.md index 274effd..fb9335b 100644 --- a/Jellyfin/jellyfin-plugin-personal-recordings/README.md +++ b/Jellyfin/jellyfin-plugin-personal-recordings/README.md @@ -45,10 +45,10 @@ Restart Jellyfin. ## Release (monorepo CI) -```bash -git tag jellyfin/jellyfin-plugin-personal-recordings/v1.0.0 -git push origin jellyfin/jellyfin-plugin-personal-recordings/v1.0.0 -``` +1. Bump [`version`](version) (SemVer `X.Y.Z`) +2. Push changes under this plugin folder + +CI builds only changed plugins, creates release `jellyfin//vX.Y.Z`, and updates the catalog. Workflow: [`.gitea/workflows/release_jellyfin_plugin.yml`](../../.gitea/workflows/release_jellyfin_plugin.yml) diff --git a/Jellyfin/jellyfin-plugin-personal-recordings/version b/Jellyfin/jellyfin-plugin-personal-recordings/version new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/Jellyfin/jellyfin-plugin-personal-recordings/version @@ -0,0 +1 @@ +1.0.0