diff --git a/docs/superpowers/evidence/syncyomi-local-fork-build-2026-08-28.json b/docs/superpowers/evidence/syncyomi-local-fork-build-2026-08-28.json new file mode 100644 index 00000000..595a7820 --- /dev/null +++ b/docs/superpowers/evidence/syncyomi-local-fork-build-2026-08-28.json @@ -0,0 +1,47 @@ +{ + "intent": "Run the SyncYomi server from kuhy's own fork instead of the published image, so server-side changes reach the live service, and stop the setup script from breaking the shared Caddy edge on every re-run.", + "scope": [ + "linux_configuration/features/setup_syncyomi.sh", + "linux_configuration/features/lib/syncyomi_stack.sh", + "linux_configuration/features/lib/syncyomi_expose.sh", + "Non-goal: changing SyncYomi's schema, credentials or the phone's sync settings" + ], + "changes": [ + "SYNCYOMI_IMAGE defaults to the local build syncyomi-kuhy:local, overridable back to a registry tag; adds SYNCYOMI_SRC and the fork URL.", + "ensure_local_image() builds that image from ~/syncyomi-src (cloning the fork if absent) before the container starts, under DOCKER_BUILDKIT=1 because the Dockerfile reads $BUILDPLATFORM; SYNCYOMI_REBUILD=1 forces a rebuild.", + "configure_caddy() now owns gitea/sites/.caddy instead of appending to the shared Caddyfile, deletes a legacy inline block, and validates the config before reloading.", + "Preflight installs git and docker-buildx alongside the existing packages." + ], + "verification": [ + { + "command": "./linux_configuration/features/setup_syncyomi.sh setup", + "result": "pass", + "evidence": "Second (idempotent) run: 'Local image syncyomi-kuhy:local already built', 'Caddy snippet already current', caddy validate+reload OK, report printed. The first run caught the bug this fixes: 'ambiguous site definition: kuhy-sync.duckdns.org'." + }, + { + "command": "docker ps --filter name=^syncyomi$ + docker logs syncyomi", + "result": "pass", + "evidence": "Image syncyomi-kuhy:local, 'Version: v1.1.14-24-g21c6acd-kuhy', commit 21c6acd." + }, + { + "command": "systemctl --user start syncyomi-guard.service", + "result": "pass", + "evidence": "ok: 2163 manga, 69776 chapters, 1 categories, 23 sources (13346096 bytes) - unchanged across the image swap." + }, + { + "command": "TachiyomiSY 'Sync library' on the phone, then read docker logs + sync_data row", + "result": "pass", + "evidence": "'Sync data replaced' at 14:42:34; payload length 13346096 before and after; https://kuhy-sync.duckdns.org/api/auth/onboard returns 403 (user exists)." + } + ], + "risks": [ + "The deployed build is develop (v1.1.14 + dependency bumps), not the v1.1.11 image it replaced; schema stays at version 8 and internal/database is unchanged between those points, so no migration runs.", + "A machine without ~/syncyomi-src now clones the fork on first setup, which needs network and a Docker build (~4 min) instead of an image pull.", + "The Caddy snippet is only rewritten when its content differs, so hand edits to that file are overwritten by design." + ], + "rollback": [ + "SYNCYOMI_IMAGE=ghcr.io/syncyomi/syncyomi:v1.1.11 ./linux_configuration/features/setup_syncyomi.sh setup", + "Cold database copy: ~/syncyomi_safe_2026-08-28/pre-local-build/cold/syncyomi.db (schema version 8).", + "After rollback, re-run the guard and confirm 2163 manga, then a phone sync round-trip." + ] +} diff --git a/linux_configuration/features/lib/syncyomi_expose.sh b/linux_configuration/features/lib/syncyomi_expose.sh index 0aa606f4..da39b741 100755 --- a/linux_configuration/features/lib/syncyomi_expose.sh +++ b/linux_configuration/features/lib/syncyomi_expose.sh @@ -7,16 +7,34 @@ # --- Step 6: Caddy reverse-proxy block (append + reload) --------------------- configure_caddy() { - if grep -qE "^${SYNCYOMI_SUBDOMAIN} \{" "${CADDYFILE}"; then - log_ok "Caddyfile already has a block for ${SYNCYOMI_SUBDOMAIN}" + local snippet="${CADDY_SITES_DIR}/${SYNCYOMI_SUBDOMAIN}.caddy" + local desired + desired="$(printf '# Managed by setup_syncyomi.sh.\n%s {\n\treverse_proxy 127.0.0.1:%s\n}' \ + "${SYNCYOMI_SUBDOMAIN}" "${SYNCYOMI_PORT}")" + + grep -qE '^[[:space:]]*import[[:space:]]+/etc/caddy/sites' "${CADDYFILE}" || + die "Root Caddyfile does not import sites/ — run setup_personal_website.sh first." + + # Every service on this edge owns one snippet under sites/. Writing into the + # shared Caddyfile instead is what produced "ambiguous site definition" on + # 2026-08-28: the site was already defined in its snippet, the block was + # appended a second time, and the reload failed while the edge kept serving + # the last config it had accepted — so the damage only showed up later. + ensure_dir "${CADDY_SITES_DIR}" + if [[ -f ${snippet} && "$(cat "${snippet}")" == "${desired}" ]]; then + log_ok "Caddy snippet already current: ${snippet}" else - log_info "Adding Caddy block for ${SYNCYOMI_SUBDOMAIN}" - # NOTE: appended to the shared Gitea Caddyfile. Re-running setup_gitea.sh - # regenerates that file and drops this block — re-run setup_syncyomi.sh - # to restore it. Caddyfile uses tab indentation. - printf '\n%s {\n\treverse_proxy 127.0.0.1:%s\n}\n' "${SYNCYOMI_SUBDOMAIN}" "${SYNCYOMI_PORT}" \ - >>"${CADDYFILE}" + log_info "Writing Caddy snippet ${snippet}" + printf '%s\n' "${desired}" >"${snippet}" fi + + if grep -qE "^${SYNCYOMI_SUBDOMAIN} \{" "${CADDYFILE}"; then + log_info "Removing the legacy inline ${SYNCYOMI_SUBDOMAIN} block from ${CADDYFILE}" + sed -i -E "/^${SYNCYOMI_SUBDOMAIN} \{/,/^\}/d" "${CADDYFILE}" + fi + + docker exec "${CADDY_CONTAINER}" caddy validate --config "${CADDYFILE_IN_CONTAINER}" >/dev/null 2>&1 || + die "Caddy config invalid — not reloading. Check ${CADDY_SITES_DIR}/*.caddy." docker exec "${CADDY_CONTAINER}" caddy reload --config "${CADDYFILE_IN_CONTAINER}" || die "caddy reload failed — check 'docker logs ${CADDY_CONTAINER}'." log_ok "Caddy reloaded (auto-HTTPS will be issued once the subdomain resolves)" diff --git a/linux_configuration/features/lib/syncyomi_stack.sh b/linux_configuration/features/lib/syncyomi_stack.sh index b22f0c7a..b957bf93 100755 --- a/linux_configuration/features/lib/syncyomi_stack.sh +++ b/linux_configuration/features/lib/syncyomi_stack.sh @@ -28,7 +28,41 @@ EOF log_ok "Wrote ${COMPOSE_FILE}" } +# Build the local image the compose file asks for. A local tag exists in no +# registry, so `docker compose up` would fail with a pull error rather than an +# explanation -- and a fork whose changes are never rebuilt is worse than no +# fork at all. Builds whatever is checked out in ${SYNCYOMI_SRC}; never moves +# that working tree. Set SYNCYOMI_REBUILD=1 to rebuild after editing it. +ensure_local_image() { + if [[ ${SYNCYOMI_IMAGE} == */* ]]; then + log_info "Using registry image ${SYNCYOMI_IMAGE}" + return 0 + fi + if [[ ${SYNCYOMI_REBUILD:-0} != "1" ]] && docker image inspect "${SYNCYOMI_IMAGE}" >/dev/null 2>&1; then + log_ok "Local image ${SYNCYOMI_IMAGE} already built" + return 0 + fi + if [[ ! -d ${SYNCYOMI_SRC}/.git ]]; then + log_info "Cloning ${SYNCYOMI_FORK_URL} into ${SYNCYOMI_SRC}" + git clone "${SYNCYOMI_FORK_URL}" "${SYNCYOMI_SRC}" + fi + local version revision + version="$(git -C "${SYNCYOMI_SRC}" describe --tags --always)" + revision="$(git -C "${SYNCYOMI_SRC}" rev-parse HEAD)" + log_info "Building ${SYNCYOMI_IMAGE} from ${SYNCYOMI_SRC} (${version})" + # BuildKit is required: the Dockerfile reads $BUILDPLATFORM, which the + # legacy builder leaves empty and then rejects as an invalid platform. + DOCKER_BUILDKIT=1 docker build \ + -t "${SYNCYOMI_IMAGE}" \ + --build-arg VERSION="${version}-kuhy" \ + --build-arg REVISION="${revision}" \ + --build-arg BUILDTIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + "${SYNCYOMI_SRC}" + log_ok "Built ${SYNCYOMI_IMAGE} from ${version}" +} + start_containers() { + ensure_local_image log_info "Starting SyncYomi container" docker compose -f "${COMPOSE_FILE}" up -d } diff --git a/linux_configuration/features/setup_syncyomi.sh b/linux_configuration/features/setup_syncyomi.sh index 4b9a921a..c91368a4 100755 --- a/linux_configuration/features/setup_syncyomi.sh +++ b/linux_configuration/features/setup_syncyomi.sh @@ -22,8 +22,15 @@ set -euo pipefail -# --- Pinned SyncYomi image --------------------------------------------------- -readonly SYNCYOMI_IMAGE="ghcr.io/syncyomi/syncyomi:v1.1.11" +# --- SyncYomi image ---------------------------------------------------------- +# Runs a local build of the personal fork by default, so server-side changes +# actually reach the service instead of sitting unbuilt in a clone. The +# published image stays one variable away: +# SYNCYOMI_IMAGE=ghcr.io/syncyomi/syncyomi:v1.1.11 ./setup_syncyomi.sh +# Any value containing a "/" is treated as a registry image and pulled as-is. +readonly SYNCYOMI_IMAGE="${SYNCYOMI_IMAGE:-syncyomi-kuhy:local}" +readonly SYNCYOMI_SRC="${SYNCYOMI_SRC:-${HOME}/syncyomi-src}" +readonly SYNCYOMI_FORK_URL="https://github.com/kuhyx/syncyomi.git" readonly SYNCYOMI_PORT=8282 readonly SYNCYOMI_LOCAL="http://127.0.0.1:${SYNCYOMI_PORT}" @@ -37,6 +44,7 @@ readonly TOKEN_FILE="${DATA_DIR}/.api_token" # --- Existing host infrastructure (from setup_gitea.sh / install_joplin.sh) -- readonly CADDY_CONTAINER="gitea-caddy" readonly CADDYFILE="${HOME}/gitea/Caddyfile" +readonly CADDY_SITES_DIR="${HOME}/gitea/sites" readonly CADDYFILE_IN_CONTAINER="/etc/caddy/Caddyfile" readonly DUCKDNS_UPDATER="${HOME}/.joplin-server/duckdns-update.sh" @@ -73,7 +81,9 @@ preflight() { [[ ${EUID} -ne 0 ]] || die "Run as your normal user, not root (it will sudo where needed)." # Runtime deps; install any that are missing from the official repos. - install_missing_pacman_packages docker docker-compose jq curl openssl + # docker-buildx: the fork's Dockerfile uses $BUILDPLATFORM, which only the + # BuildKit builder defines; the legacy builder rejects the empty value. + install_missing_pacman_packages docker docker-compose jq curl openssl git docker-buildx # Ensure the docker daemon is enabled+running (auto-start after reboot). Only # touch systemd via sudo when it is not already active, to stay a no-op on the