From 7245bcf1efd9d5c2c5d4ff11e31e0aa666d2dcd9 Mon Sep 17 00:00:00 2001 From: seer Date: Thu, 9 Jul 2026 17:23:11 +0200 Subject: [PATCH] Revert "Split release publishing into a separate public distribution repo" This reverts commit 1d2246c5e227fbc8f9ed242c3664b1781dacedb5. --- .github/workflows/release.yml | 53 +++++++++++++++------------- PLAN.md | 56 ++++-------------------------- dev/package-release.sh | 37 +++++++++----------- dev/publish-manifest.sh | 65 ----------------------------------- manifest.json | 20 +++++++++++ 5 files changed, 72 insertions(+), 159 deletions(-) delete mode 100755 dev/publish-manifest.sh create mode 100644 manifest.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 67ba3dd..241c67b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,14 +1,5 @@ name: Release Plugin -# Releases are published to a *separate public* distribution repo, not this one. -# Gitea has no way to make individual release assets public while the source repo -# stays private -- repo visibility is all-or-nothing, confirmed against Gitea's own -# docs/issue tracker -- so the actual plugin zip + manifest.json live in -# PUBLIC_REPO_NAME below, kept in sync by this workflow. This (private source) repo -# never needs to be public for Jellyfin installs to work. -env: - PUBLIC_REPO_NAME: JellyfinSyncPlus-repo - on: push: tags: @@ -32,6 +23,7 @@ jobs: id: meta run: | echo REPO_OWNER=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $1}') >> $GITHUB_OUTPUT + echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT echo VERSION=$(echo ${GITHUB_REF_NAME} | sed 's/^v//') >> $GITHUB_OUTPUT cat $GITHUB_OUTPUT @@ -46,40 +38,53 @@ jobs: docker cp jf-extract:/usr/lib/jellyfin/bin/Emby.Server.Implementations.dll lib/jellyfin-10.11.6/ docker rm jf-extract - - name: Build and package - id: package + - name: Build, package, update manifest env: VERSION: ${{ steps.meta.outputs.VERSION }} SERVER_URL: ${{ github.server_url }} - PUBLIC_REPO_OWNER: ${{ steps.meta.outputs.REPO_OWNER }} - PUBLIC_REPO_NAME: ${{ env.PUBLIC_REPO_NAME }} + REPO_OWNER: ${{ steps.meta.outputs.REPO_OWNER }} + REPO_NAME: ${{ steps.meta.outputs.REPO_NAME }} run: dev/package-release.sh - - name: Create release and upload zip on the public repo + - name: Create Gitea release and upload zip env: GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }} SERVER_URL: ${{ github.server_url }} - PUBLIC_REPO_OWNER: ${{ steps.meta.outputs.REPO_OWNER }} + REPO_OWNER: ${{ steps.meta.outputs.REPO_OWNER }} + REPO_NAME: ${{ steps.meta.outputs.REPO_NAME }} VERSION: ${{ steps.meta.outputs.VERSION }} TAG: ${{ github.ref_name }} - ZIP_PATH: ${{ steps.package.outputs.ZIP_PATH }} run: | RELEASE_ID=$(curl -sf -X POST \ -H "Authorization: token ${GITEA_TOKEN}" \ -H "Content-Type: application/json" \ -d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\",\"body\":\"See build.yaml changelog / manifest.json for this version's notes.\"}" \ - "${SERVER_URL}/api/v1/repos/${PUBLIC_REPO_OWNER}/${PUBLIC_REPO_NAME}/releases" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])') + "${SERVER_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])') curl -sf -X POST \ -H "Authorization: token ${GITEA_TOKEN}" \ - -F "attachment=@${ZIP_PATH}" \ - "${SERVER_URL}/api/v1/repos/${PUBLIC_REPO_OWNER}/${PUBLIC_REPO_NAME}/releases/${RELEASE_ID}/assets?name=JellyfinSyncPlus_${VERSION}.zip" + -F "attachment=@release/JellyfinSyncPlus_${VERSION}.zip" \ + "${SERVER_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}/assets?name=JellyfinSyncPlus_${VERSION}.zip" - - name: Publish manifest.json to the public repo + # manifest.json has to live at a stable URL on a normal branch (master) -- + # that's the one URL admins add to Jellyfin once, and every future release + # just appends to it. The tag checkout above is detached HEAD, so switch to + # master to commit, carrying the just-updated manifest.json across. + - name: Commit updated manifest to master env: GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }} SERVER_URL: ${{ github.server_url }} - PUBLIC_REPO_OWNER: ${{ steps.meta.outputs.REPO_OWNER }} + REPO_OWNER: ${{ steps.meta.outputs.REPO_OWNER }} + REPO_NAME: ${{ steps.meta.outputs.REPO_NAME }} VERSION: ${{ steps.meta.outputs.VERSION }} - CHECKSUM: ${{ steps.package.outputs.CHECKSUM }} - SOURCE_URL: ${{ steps.package.outputs.SOURCE_URL }} - run: dev/publish-manifest.sh + run: | + cp manifest.json /tmp/manifest.json + git fetch origin master + git checkout master + cp /tmp/manifest.json manifest.json + git config user.name "gitea-actions" + git config user.email "actions@gitea.local" + git add manifest.json + git diff --cached --quiet && echo "No manifest changes to commit" && exit 0 + git commit -m "Release ${VERSION}" + HOST=$(echo "${SERVER_URL}" | sed 's#https\?://##') + git push "https://gitea-actions:${GITEA_TOKEN}@${HOST}/${REPO_OWNER}/${REPO_NAME}.git" HEAD:master diff --git a/PLAN.md b/PLAN.md index 7283cf1..b70c606 100644 --- a/PLAN.md +++ b/PLAN.md @@ -149,55 +149,13 @@ Two different loops -- do not conflate them: briefly interrupts the live, shared Jellyfin instance -- **do not do this routinely; only for actual multi-friend sync test sessions, and confirm with the user first.** -3. **Release distribution -- DONE (Phase 5, built 2026-07-09).** Package as a - versioned zip + `manifest.json`, host via Gitea, add the repo URL in - Jellyfin's dashboard. Mirrors MR-Discord's tag -> Gitea Actions -> build - pipeline, just producing a plugin zip instead of a Docker image. - - **Source repo (`JellyfinSyncPlus`) stays private; a separate public repo - (`JellyfinSyncPlus-repo`) holds only `manifest.json` and release zips.** - Confirmed via Gitea's own docs/issue tracker: there's no way to make - individual release assets public while the repo itself stays private -- - repo visibility is all-or-nothing, covering raw files, releases, and API - access alike. So the two are fully decoupled: CI builds from the private - source repo (has the checkout + build context) but publishes the - release and updates the manifest on the separate public repo, using the - same `RELEASE_TOKEN` (Gitea personal access tokens are user-scoped, not - repo-scoped, so one token works against both repos as long as the - owning account has write access to each). - - `dev/package-release.sh` -- builds Release config, zips the DLL, - computes its MD5 checksum, derives the release download URL (pointing - at the *public* repo). Live-tested locally twice (once before, once - after the public/private split) -- produces a real, valid zip + a - `manifest.json` schema-verified against a real published Jellyfin - plugin repo's actual file, not just assumed from memory. - - `dev/publish-manifest.sh` -- fetches the public repo's current - `manifest.json` via Gitea's Contents API (or starts fresh on the very - first release), merges in this version via `dev/update_manifest.py`, - and pushes it back via the same API. No git clone of the public repo - needed. - - `.github/workflows/release.yml` -- on a `v*` tag push: builds - (including `dev/extract-private-refs.sh`'s CI equivalent, pulling the - two private SDK assemblies straight from the pinned - `linuxserver/jellyfin:10.11.6` image via `docker cp`), packages, - creates a Gitea release + uploads the zip on the *public* repo, then - publishes the manifest there too. - - `.github/workflows/build.yml` -- plain build-check on every push/PR to - `master`, mirroring MR-Discord's `build.yml` pattern. - - **Live-verified 2026-07-09, partially**: tagged and pushed `v0.2.0.0` - against the *first* version of this pipeline (before the public/private - split existed) -- confirmed the workflow actually ran, created a real - Gitea release with the zip attached, and committed a real - `manifest.json`. That run published to the private source repo itself, - which is exactly the mistake the public/private split above exists to - fix; the updated, split version hasn't had a live tag-triggered CI run - yet as of this note -- the local packaging half is tested, the - `publish-manifest.sh` Contents API calls are syntax/logic-checked but - not yet exercised against a real Gitea instance. - - Once a plugin is actually installed from this repo, Jellyfin has no - hot-reload -- same as local dev, a server restart is required. On the - real k8s deployment that's the "confirm with the user first" milestone - action from the "Dev / test / iterate workflow" section above, not a - routine step. +3. **Release distribution (later, once stable):** package as a versioned zip + + `meta.json`, host a plugin-repository manifest via Gitea (raw file URLs + or release attachments both work, Jellyfin just needs HTTP(S) access to + the manifest + zip), add the repo URL in Jellyfin's dashboard. Mirrors + MR-Discord's tag -> Gitea Actions -> build pipeline, just producing a + plugin zip instead of a Docker image. Slow iteration loop -- not for + development, only for shipping tested versions. ## Known friction / risks (flagged going in, not discovered later) diff --git a/dev/package-release.sh b/dev/package-release.sh index 56662cc..b75eca5 100755 --- a/dev/package-release.sh +++ b/dev/package-release.sh @@ -1,21 +1,18 @@ #!/usr/bin/env bash -# Builds and packages a tagged release. Used by .github/workflows/release.yml; safe to -# run locally too for a dry run. Does NOT touch manifest.json -- that's published -# separately to the public distribution repo by dev/publish-manifest.sh, since the zip -# this produces gets uploaded there too (release assets on a private repo can't be made -# public on their own in Gitea; see PLAN.md "Release distribution"). +# Builds, packages, and updates manifest.json for a tagged release. Used by +# .github/workflows/release.yml; safe to run locally too for a dry run. # # Required env vars: -# VERSION -- e.g. 0.2.0.0 (must match build.yaml's version) -# SERVER_URL -- e.g. https://gitea.mrcynic.site -# PUBLIC_REPO_OWNER -- Gitea org/user the *public* distribution repo lives under -# PUBLIC_REPO_NAME -- the public distribution repo's name (holds releases + manifest.json) +# VERSION -- e.g. 0.2.0.0 (must match build.yaml's version) +# SERVER_URL -- e.g. https://gitea.mrcynic.site +# REPO_OWNER -- Gitea org/user the repo lives under +# REPO_NAME -- Gitea repo name set -euo pipefail : "${VERSION:?VERSION env var required, e.g. 0.2.0.0}" : "${SERVER_URL:?SERVER_URL env var required, e.g. https://gitea.mrcynic.site}" -: "${PUBLIC_REPO_OWNER:?PUBLIC_REPO_OWNER env var required}" -: "${PUBLIC_REPO_NAME:?PUBLIC_REPO_NAME env var required}" +: "${REPO_OWNER:?REPO_OWNER env var required}" +: "${REPO_NAME:?REPO_NAME env var required}" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" @@ -37,17 +34,15 @@ rm -f "$ZIP_PATH" (cd src/JellyfinSyncPlus/bin/Release/net9.0 && zip -j "$ROOT/$ZIP_PATH" JellyfinSyncPlus.dll) CHECKSUM=$(md5sum "$ZIP_PATH" | awk '{print $1}') -SOURCE_URL="${SERVER_URL}/${PUBLIC_REPO_OWNER}/${PUBLIC_REPO_NAME}/releases/download/v${VERSION}/${ZIP_NAME}" +SOURCE_URL="${SERVER_URL}/${REPO_OWNER}/${REPO_NAME}/releases/download/v${VERSION}/${ZIP_NAME}" + +python3 "$ROOT/dev/update_manifest.py" \ + --build-yaml "$ROOT/build.yaml" \ + --manifest "$ROOT/manifest.json" \ + --version "$VERSION" \ + --checksum "$CHECKSUM" \ + --source-url "$SOURCE_URL" echo "Packaged $ZIP_PATH" echo "Checksum: $CHECKSUM" echo "Source URL: $SOURCE_URL" - -# For the next workflow step to pick up without re-deriving them (no-op outside CI). -if [ -n "${GITHUB_OUTPUT:-}" ]; then - { - echo "CHECKSUM=${CHECKSUM}" - echo "SOURCE_URL=${SOURCE_URL}" - echo "ZIP_PATH=${ZIP_PATH}" - } >> "$GITHUB_OUTPUT" -fi diff --git a/dev/publish-manifest.sh b/dev/publish-manifest.sh deleted file mode 100755 index c590e57..0000000 --- a/dev/publish-manifest.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env bash -# Fetches the current manifest.json from the public distribution repo (or starts fresh -# if this is the first release), merges in this build's version entry, and pushes it -# back -- all via Gitea's Contents API, so this never needs a git clone of the public -# repo. Keeps the private source repo and the public distribution repo fully decoupled. -# -# Required env vars: -# GITEA_TOKEN, SERVER_URL, PUBLIC_REPO_OWNER, PUBLIC_REPO_NAME, VERSION, CHECKSUM, SOURCE_URL -set -euo pipefail - -: "${GITEA_TOKEN:?}" -: "${SERVER_URL:?}" -: "${PUBLIC_REPO_OWNER:?}" -: "${PUBLIC_REPO_NAME:?}" -: "${VERSION:?}" -: "${CHECKSUM:?}" -: "${SOURCE_URL:?}" - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -API="${SERVER_URL}/api/v1/repos/${PUBLIC_REPO_OWNER}/${PUBLIC_REPO_NAME}/contents/manifest.json" -WORKDIR=$(mktemp -d) -trap 'rm -rf "$WORKDIR"' EXIT - -HTTP_CODE=$(curl -s -o "$WORKDIR/existing.json" -w "%{http_code}" \ - -H "Authorization: token ${GITEA_TOKEN}" "${API}?ref=master") - -if [ "$HTTP_CODE" = "200" ]; then - SHA=$(python3 -c 'import json; print(json.load(open("'"$WORKDIR"'/existing.json"))["sha"])') - python3 -c 'import json,base64 -d = json.load(open("'"$WORKDIR"'/existing.json")) -open("'"$WORKDIR"'/manifest.json", "wb").write(base64.b64decode(d["content"]))' - echo "Fetched existing manifest.json (sha ${SHA})" -else - SHA="" - echo "[]" > "$WORKDIR/manifest.json" - echo "No existing manifest.json in ${PUBLIC_REPO_OWNER}/${PUBLIC_REPO_NAME} -- starting fresh (HTTP $HTTP_CODE)" -fi - -python3 "$ROOT/dev/update_manifest.py" \ - --build-yaml "$ROOT/build.yaml" \ - --manifest "$WORKDIR/manifest.json" \ - --version "$VERSION" \ - --checksum "$CHECKSUM" \ - --source-url "$SOURCE_URL" - -CONTENT_B64=$(base64 -w0 "$WORKDIR/manifest.json") - -python3 -c 'import json,sys -body = {"content": sys.argv[1], "message": "Release " + sys.argv[2], "branch": "master"} -if sys.argv[3]: - body["sha"] = sys.argv[3] -json.dump(body, open(sys.argv[4], "w"))' "$CONTENT_B64" "$VERSION" "$SHA" "$WORKDIR/body.json" - -METHOD="POST" -if [ -n "$SHA" ]; then - METHOD="PUT" -fi - -curl -sf -X "$METHOD" \ - -H "Authorization: token ${GITEA_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "@$WORKDIR/body.json" \ - "$API" > /dev/null - -echo "Published manifest.json version ${VERSION} to ${PUBLIC_REPO_OWNER}/${PUBLIC_REPO_NAME}" diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..89f7c06 --- /dev/null +++ b/manifest.json @@ -0,0 +1,20 @@ +[ + { + "guid": "267dbfe9-bb9c-4eeb-97aa-f0449283cfe6", + "name": "JellyfinSyncPlus", + "description": "Read-only live view of every active SyncPlay group's members -- drift, position, play method, bitrate, and codec info -- plus a manual \"sync me to group\" button for one-shot self-correction. Does not touch playback automatically: earlier attempts at automatic quality-forcing and automatic drift correction both caused real problems in live use and were reverted (see PLAN.md). Reachable via Dashboard -> Plugins -> JellyfinSyncPlus.", + "overview": "SyncPlay stats for nerds: live drift/playback view plus a manual one-shot sync button.", + "owner": "cynic", + "category": "General", + "versions": [ + { + "version": "0.2.0.0", + "changelog": "0.2.0.0: SyncPlay stats-for-nerds page and manual one-shot \"sync me to group\" button. No automatic playback intervention. 0.1.0.0: Phase 0 scaffold. Trivial plugin, no functionality yet.", + "targetAbi": "10.11.6.0", + "sourceUrl": "https://gitea.mrcynic.site/seer/JellyfinSyncPlus/releases/download/v0.2.0.0/JellyfinSyncPlus_0.2.0.0.zip", + "checksum": "34e384fc5914e2c459f52a9a7bfd91a5", + "timestamp": "2026-07-09T15:11:18Z" + } + ] + } +]