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
# 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
+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
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).
- **Repo (`JellyfinSyncPlus`) itself is public** -- briefly tried a
separate private-source/public-distribution repo split (Gitea has no
way to make individual release assets public on a private repo; repo
visibility is all-or-nothing there), but decided to just make this repo
public instead rather than maintain two repos. Manifest + releases live
directly on this repo.
- `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.
computes its MD5 checksum, updates `manifest.json` (schema-verified
against a real published Jellyfin plugin repo's actual file). Live-
tested locally.
- `dev/update_manifest.py` -- the manifest merge logic package-release.sh
calls; reads plugin metadata/changelog from `build.yaml`.
- `.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.
(including pulling the two private SDK assemblies straight from the
pinned `linuxserver/jellyfin:10.11.6` image via `docker cp`, mirroring
`dev/extract-private-refs.sh` for CI), packages, creates a Gitea
release with the zip attached, commits the updated `manifest.json`
back to `master`.
- `.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.
- **Live-verified 2026-07-09**: tagged and pushed `v0.2.0.0` -- confirmed
the workflow ran, created a real Gitea release with the zip attached,
and committed a real `manifest.json` back to `master`. Manifest URL:
`https://gitea.mrcynic.site/seer/JellyfinSyncPlus/raw/branch/master/manifest.json`.
- Once installed, 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)
+14 -19
View File
@@ -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)
# 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
-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"
}
]
}
]