mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-09 05:24:49 +09:00
backend/app/data/registry_index.json is a mirror of serverkit-extensions' index.json, and nothing automated the mirror -- so every registry publish left the panel's offline fallback stale until CI complained. That is how `serverkit-localkit` sat missing from it: an offline panel offered a different extension set than an online one. Same shape as scripts/sync-agent-installers.sh: prefer a sibling checkout so it works offline and before the registry PR is merged, else the published branch, resolving the default branch from the API rather than hardcoding a name. Two things it is deliberately careful about: - It vendors the RAW index, never https://serverkit.ai/ext/index.json. The panel fetches through that proxy at runtime and the proxy rewrites every relative `logo` path to an absolute serverkit.ai URL, while the bundled loader resolves relative paths itself. Vendoring the proxied copy would bake in the wrong paths, and the tuple the tests compare (version/source/sha256) is identical either way -- nothing would have caught it. - It keeps the local `note` (which does not exist upstream) and writes with indent=2/ensure_ascii=True/trailing newline, which round-trips the existing file byte-for-byte. An in-sync run is a true no-op, not a reformat. It refuses to write an index with no entries, so a broken or truncated canonical source cannot blank the Marketplace fallback. Verified: no-op when in sync; reproducing the real bug (dropping the localkit entry) and re-running restores the file byte-identical to HEAD; repins are reported; the empty-canonical guard exits non-zero leaving the file untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
174 lines
6.5 KiB
Bash
174 lines
6.5 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# Re-vendor the bundled extension registry index from its canonical home.
|
|
#
|
|
# backend/app/data/registry_index.json is the Marketplace's offline fallback and
|
|
# what the test suite reads. The canonical copy lives in the serverkit-extensions
|
|
# repo. That is two copies of one document, which is the shape that rots: the
|
|
# registry gained `serverkit-localkit` and the bundled mirror was never updated,
|
|
# so an offline panel would have offered a different extension set than an online
|
|
# one -- and it stayed that way until CI complained.
|
|
#
|
|
# So: merge the registry PR, run this, commit the result. Never hand-edit the
|
|
# entries in the bundled file. tests/test_registry_bundled_index.py fails if the
|
|
# two disagree on what any entry pins.
|
|
#
|
|
# Usage:
|
|
# bash scripts/sync-registry-index.sh # sibling checkout, else GitHub
|
|
# bash scripts/sync-registry-index.sh <path|url> # explicit source
|
|
#
|
|
# REGISTRY_REF=v3 bash scripts/sync-registry-index.sh # pin a ref when fetching
|
|
|
|
set -uo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
REGISTRY_REPO="${SERVERKIT_REGISTRY_REPO:-jhd3197/serverkit-extensions}"
|
|
REGISTRY_REF="${REGISTRY_REF:-}"
|
|
SOURCE="${1:-}"
|
|
|
|
DEST="${REPO_ROOT}/backend/app/data/registry_index.json"
|
|
|
|
if ! command -v python3 >/dev/null 2>&1; then
|
|
printf '\033[31m✘\033[0m python3 is required (this is a maintainer tool, not an installer path)\n' >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Ask the repo which branch is its default rather than hardcoding one -- the
|
|
# agent repo was renamed master -> main and every hardcoded name became a 404
|
|
# that read as "cannot reach the canonical copy". Same helper as
|
|
# scripts/sync-agent-installers.sh.
|
|
resolve_registry_ref() {
|
|
if [ -n "$REGISTRY_REF" ]; then printf '%s' "$REGISTRY_REF"; return 0; fi
|
|
|
|
local declared ref
|
|
declared="$(curl -fsSL "https://api.github.com/repos/${REGISTRY_REPO}" 2>/dev/null \
|
|
| grep -o '"default_branch": *"[^"]*"' | head -1 | sed -e 's/.*: *"//' -e 's/"$//')"
|
|
if [ -n "$declared" ]; then printf '%s' "$declared"; return 0; fi
|
|
|
|
for ref in main master; do
|
|
if curl -fsSL -o /dev/null \
|
|
"https://raw.githubusercontent.com/${REGISTRY_REPO}/${ref}/index.json" 2>/dev/null; then
|
|
printf '%s' "$ref"; return 0
|
|
fi
|
|
done
|
|
printf 'main'
|
|
}
|
|
|
|
# Default source: a sibling checkout if there is one (so this works offline and
|
|
# before the registry PR is merged), otherwise the published branch.
|
|
#
|
|
# NOTE: deliberately the RAW index, never https://serverkit.ai/ext/index.json.
|
|
# The panel fetches through that proxy at runtime and the proxy rewrites every
|
|
# relative `logo` path to an absolute serverkit.ai URL. Vendoring the proxied
|
|
# copy would bake those absolute URLs into the fallback, and the bundled loader
|
|
# resolves relative paths against the registry URL itself -- so the proxied copy
|
|
# is the wrong document to mirror even though it looks equivalent. The pinned
|
|
# tuple the tests compare (version/source/sha256) is identical either way, so
|
|
# nothing would have caught it.
|
|
if [ -z "$SOURCE" ]; then
|
|
if [ -f "${REPO_ROOT}/../serverkit-extensions/index.json" ]; then
|
|
SOURCE="$(cd "${REPO_ROOT}/../serverkit-extensions" && pwd)/index.json"
|
|
else
|
|
SOURCE="https://raw.githubusercontent.com/${REGISTRY_REPO}/$(resolve_registry_ref)/index.json"
|
|
fi
|
|
fi
|
|
|
|
printf '\nvendoring registry index from: %s\n\n' "$SOURCE"
|
|
|
|
TMP="$(mktemp)"
|
|
trap 'rm -f "$TMP"' EXIT
|
|
|
|
if [ -f "$SOURCE" ]; then
|
|
cp "$SOURCE" "$TMP"
|
|
elif [ -d "$SOURCE" ]; then
|
|
if [ ! -f "${SOURCE}/index.json" ]; then
|
|
printf ' \033[31m✘\033[0m index.json not found in %s\n' "$SOURCE"
|
|
exit 1
|
|
fi
|
|
cp "${SOURCE}/index.json" "$TMP"
|
|
else
|
|
if ! curl -fsSL "$SOURCE" -o "$TMP"; then
|
|
printf ' \033[31m✘\033[0m could not fetch %s\n' "$SOURCE"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
python3 - "$TMP" "$DEST" <<'PY'
|
|
import json
|
|
import sys
|
|
|
|
canonical_path, dest_path = sys.argv[1], sys.argv[2]
|
|
|
|
try:
|
|
with open(canonical_path, encoding='utf-8') as f:
|
|
canonical = json.load(f)
|
|
except (OSError, ValueError) as exc:
|
|
print(f' \033[31m✘\033[0m canonical index is not readable JSON: {exc}')
|
|
sys.exit(1)
|
|
|
|
if not isinstance(canonical.get('extensions'), list) or not canonical['extensions']:
|
|
print(' \033[31m✘\033[0m canonical index has no extensions — refusing to blank the fallback')
|
|
sys.exit(1)
|
|
|
|
with open(dest_path, encoding='utf-8') as f:
|
|
before = f.read()
|
|
current = json.loads(before)
|
|
|
|
def pinned(index):
|
|
return {
|
|
e['slug']: (e.get('version'), e.get('source'), e.get('sha256'))
|
|
for e in index['extensions']
|
|
}
|
|
|
|
ours, theirs = pinned(current), pinned(canonical)
|
|
added = sorted(set(theirs) - set(ours))
|
|
removed = sorted(set(ours) - set(theirs))
|
|
repinned = sorted(s for s in set(ours) & set(theirs) if ours[s] != theirs[s])
|
|
|
|
# Take the canonical document wholesale, but keep the local `note`: it explains
|
|
# to the next reader what this file is and that it must not be hand-edited, and
|
|
# it does not exist upstream. Everything else -- including entry order -- comes
|
|
# from canonical, because a mirror that reorders is a mirror that diffs forever.
|
|
merged = dict(canonical)
|
|
if 'note' in current:
|
|
merged = {
|
|
'schema_version': canonical.get('schema_version', current.get('schema_version')),
|
|
'updated': canonical.get('updated', current.get('updated')),
|
|
'note': current['note'],
|
|
'extensions': canonical['extensions'],
|
|
}
|
|
|
|
# indent=2 + ensure_ascii=True + trailing newline round-trips the existing file
|
|
# byte-for-byte, so an in-sync run produces an empty diff rather than a reformat.
|
|
after = json.dumps(merged, indent=2, ensure_ascii=True) + '\n'
|
|
|
|
if after == before:
|
|
print(' \033[32m=\033[0m registry_index.json already up to date')
|
|
sys.exit(0)
|
|
|
|
with open(dest_path, 'w', encoding='utf-8', newline='') as f:
|
|
f.write(after)
|
|
|
|
print(' \033[33m↓\033[0m registry_index.json updated')
|
|
for slug in added:
|
|
print(f' + {slug} {theirs[slug][0]}')
|
|
for slug in removed:
|
|
print(f' - {slug} {ours[slug][0]}')
|
|
for slug in repinned:
|
|
print(f' ~ {slug}: {ours[slug][0]} -> {theirs[slug][0]}')
|
|
if not (added or removed or repinned):
|
|
print(' (metadata only — no entry added, removed, or repinned)')
|
|
sys.exit(10)
|
|
PY
|
|
rc=$?
|
|
|
|
printf '\n'
|
|
if [ "$rc" -eq 10 ]; then
|
|
printf 'Review and commit backend/app/data/registry_index.json.\n'
|
|
printf 'Bundled (bundled: true) entries must match builtin-extensions/<slug>/plugin.json;\n'
|
|
printf 'run: cd backend && python -m pytest tests/test_registry_bundled_index.py\n\n'
|
|
exit 0
|
|
fi
|
|
|
|
exit "$rc"
|