2 Commits
Author SHA1 Message Date
seerandClaude Sonnet 5 8dadef4184 Document release distribution as single public repo, not split
Build Check / build (push) Successful in 19s
The public/private repo split got reverted (see previous commit) in
favor of just making this repo public directly. PLAN.md's Release
distribution section still described the old "later, once stable"
placeholder from before either version was built -- update it to match
what's actually running.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 17:23:52 +02:00
seer 7245bcf1ef Revert "Split release publishing into a separate public distribution repo"
This reverts commit 1d2246c5e2.
2026-07-09 17:23:11 +02:00
5 changed files with 89 additions and 151 deletions
+29 -24
View File
@@ -1,14 +1,5 @@
name: Release Plugin 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: on:
push: push:
tags: tags:
@@ -32,6 +23,7 @@ jobs:
id: meta id: meta
run: | run: |
echo REPO_OWNER=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $1}') >> $GITHUB_OUTPUT 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 echo VERSION=$(echo ${GITHUB_REF_NAME} | sed 's/^v//') >> $GITHUB_OUTPUT
cat $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 cp jf-extract:/usr/lib/jellyfin/bin/Emby.Server.Implementations.dll lib/jellyfin-10.11.6/
docker rm jf-extract docker rm jf-extract
- name: Build and package - name: Build, package, update manifest
id: package
env: env:
VERSION: ${{ steps.meta.outputs.VERSION }} VERSION: ${{ steps.meta.outputs.VERSION }}
SERVER_URL: ${{ github.server_url }} SERVER_URL: ${{ github.server_url }}
PUBLIC_REPO_OWNER: ${{ steps.meta.outputs.REPO_OWNER }} REPO_OWNER: ${{ steps.meta.outputs.REPO_OWNER }}
PUBLIC_REPO_NAME: ${{ env.PUBLIC_REPO_NAME }} REPO_NAME: ${{ steps.meta.outputs.REPO_NAME }}
run: dev/package-release.sh run: dev/package-release.sh
- name: Create release and upload zip on the public repo - name: Create Gitea release and upload zip
env: env:
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }} GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
SERVER_URL: ${{ github.server_url }} 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 }} VERSION: ${{ steps.meta.outputs.VERSION }}
TAG: ${{ github.ref_name }} TAG: ${{ github.ref_name }}
ZIP_PATH: ${{ steps.package.outputs.ZIP_PATH }}
run: | run: |
RELEASE_ID=$(curl -sf -X POST \ RELEASE_ID=$(curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \ -H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\",\"body\":\"See build.yaml changelog / manifest.json for this version's notes.\"}" \ -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 \ curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \ -H "Authorization: token ${GITEA_TOKEN}" \
-F "attachment=@${ZIP_PATH}" \ -F "attachment=@release/JellyfinSyncPlus_${VERSION}.zip" \
"${SERVER_URL}/api/v1/repos/${PUBLIC_REPO_OWNER}/${PUBLIC_REPO_NAME}/releases/${RELEASE_ID}/assets?name=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: env:
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }} GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
SERVER_URL: ${{ github.server_url }} 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 }} VERSION: ${{ steps.meta.outputs.VERSION }}
CHECKSUM: ${{ steps.package.outputs.CHECKSUM }} run: |
SOURCE_URL: ${{ steps.package.outputs.SOURCE_URL }} cp manifest.json /tmp/manifest.json
run: dev/publish-manifest.sh 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
+24 -41
View File
@@ -153,51 +153,34 @@ Two different loops -- do not conflate them:
versioned zip + `manifest.json`, host via Gitea, add the repo URL in versioned zip + `manifest.json`, host via Gitea, add the repo URL in
Jellyfin's dashboard. Mirrors MR-Discord's tag -> Gitea Actions -> build Jellyfin's dashboard. Mirrors MR-Discord's tag -> Gitea Actions -> build
pipeline, just producing a plugin zip instead of a Docker image. pipeline, just producing a plugin zip instead of a Docker image.
- **Source repo (`JellyfinSyncPlus`) stays private; a separate public repo - **Repo (`JellyfinSyncPlus`) itself is public** -- briefly tried a
(`JellyfinSyncPlus-repo`) holds only `manifest.json` and release zips.** separate private-source/public-distribution repo split (Gitea has no
Confirmed via Gitea's own docs/issue tracker: there's no way to make way to make individual release assets public on a private repo; repo
individual release assets public while the repo itself stays private -- visibility is all-or-nothing there), but decided to just make this repo
repo visibility is all-or-nothing, covering raw files, releases, and API public instead rather than maintain two repos. Manifest + releases live
access alike. So the two are fully decoupled: CI builds from the private directly on this repo.
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, - `dev/package-release.sh` -- builds Release config, zips the DLL,
computes its MD5 checksum, derives the release download URL (pointing computes its MD5 checksum, updates `manifest.json` (schema-verified
at the *public* repo). Live-tested locally twice (once before, once against a real published Jellyfin plugin repo's actual file). Live-
after the public/private split) -- produces a real, valid zip + a tested locally.
`manifest.json` schema-verified against a real published Jellyfin - `dev/update_manifest.py` -- the manifest merge logic package-release.sh
plugin repo's actual file, not just assumed from memory. calls; reads plugin metadata/changelog from `build.yaml`.
- `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 - `.github/workflows/release.yml` -- on a `v*` tag push: builds
(including `dev/extract-private-refs.sh`'s CI equivalent, pulling the (including pulling the two private SDK assemblies straight from the
two private SDK assemblies straight from the pinned pinned `linuxserver/jellyfin:10.11.6` image via `docker cp`, mirroring
`linuxserver/jellyfin:10.11.6` image via `docker cp`), packages, `dev/extract-private-refs.sh` for CI), packages, creates a Gitea
creates a Gitea release + uploads the zip on the *public* repo, then release with the zip attached, commits the updated `manifest.json`
publishes the manifest there too. back to `master`.
- `.github/workflows/build.yml` -- plain build-check on every push/PR to - `.github/workflows/build.yml` -- plain build-check on every push/PR to
`master`, mirroring MR-Discord's `build.yml` pattern. `master`, mirroring MR-Discord's `build.yml` pattern.
- **Live-verified 2026-07-09, partially**: tagged and pushed `v0.2.0.0` - **Live-verified 2026-07-09**: tagged and pushed `v0.2.0.0` -- confirmed
against the *first* version of this pipeline (before the public/private the workflow ran, created a real Gitea release with the zip attached,
split existed) -- confirmed the workflow actually ran, created a real and committed a real `manifest.json` back to `master`. Manifest URL:
Gitea release with the zip attached, and committed a real `https://gitea.mrcynic.site/seer/JellyfinSyncPlus/raw/branch/master/manifest.json`.
`manifest.json`. That run published to the private source repo itself, - Once installed, Jellyfin has no hot-reload -- same as local dev, a
which is exactly the mistake the public/private split above exists to server restart is required. On the real k8s deployment that's the
fix; the updated, split version hasn't had a live tag-triggered CI run "confirm with the user first" milestone action from the "Dev / test /
yet as of this note -- the local packaging half is tested, the iterate workflow" section above, not a routine step.
`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.
## Known friction / risks (flagged going in, not discovered later) ## Known friction / risks (flagged going in, not discovered later)
+14 -19
View File
@@ -1,21 +1,18 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Builds and packages a tagged release. Used by .github/workflows/release.yml; safe to # Builds, packages, and updates manifest.json for a tagged release. Used by
# run locally too for a dry run. Does NOT touch manifest.json -- that's published # .github/workflows/release.yml; safe to run locally too for a dry run.
# 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").
# #
# Required env vars: # Required env vars:
# VERSION -- e.g. 0.2.0.0 (must match build.yaml's version) # VERSION -- e.g. 0.2.0.0 (must match build.yaml's version)
# SERVER_URL -- e.g. https://gitea.mrcynic.site # SERVER_URL -- e.g. https://gitea.mrcynic.site
# PUBLIC_REPO_OWNER -- Gitea org/user the *public* distribution repo lives under # REPO_OWNER -- Gitea org/user the repo lives under
# PUBLIC_REPO_NAME -- the public distribution repo's name (holds releases + manifest.json) # REPO_NAME -- Gitea repo name
set -euo pipefail set -euo pipefail
: "${VERSION:?VERSION env var required, e.g. 0.2.0.0}" : "${VERSION:?VERSION env var required, e.g. 0.2.0.0}"
: "${SERVER_URL:?SERVER_URL env var required, e.g. https://gitea.mrcynic.site}" : "${SERVER_URL:?SERVER_URL env var required, e.g. https://gitea.mrcynic.site}"
: "${PUBLIC_REPO_OWNER:?PUBLIC_REPO_OWNER env var required}" : "${REPO_OWNER:?REPO_OWNER env var required}"
: "${PUBLIC_REPO_NAME:?PUBLIC_REPO_NAME env var required}" : "${REPO_NAME:?REPO_NAME env var required}"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT" cd "$ROOT"
@@ -37,17 +34,15 @@ rm -f "$ZIP_PATH"
(cd src/JellyfinSyncPlus/bin/Release/net9.0 && zip -j "$ROOT/$ZIP_PATH" JellyfinSyncPlus.dll) (cd src/JellyfinSyncPlus/bin/Release/net9.0 && zip -j "$ROOT/$ZIP_PATH" JellyfinSyncPlus.dll)
CHECKSUM=$(md5sum "$ZIP_PATH" | awk '{print $1}') 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 "Packaged $ZIP_PATH"
echo "Checksum: $CHECKSUM" echo "Checksum: $CHECKSUM"
echo "Source URL: $SOURCE_URL" 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
-65
View File
@@ -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}"
+20
View File
@@ -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"
}
]
}
]