135 lines
4.7 KiB
Bash
135 lines
4.7 KiB
Bash
#!/usr/bin/env bash
|
|
# Package a plugin directory and publish it to the sync server.
|
|
#
|
|
# bash tools/publish-plugin.sh team-defaults
|
|
# bash tools/publish-plugin.sh team-defaults --url http://host:8020 --token <admin>
|
|
# bash tools/publish-plugin.sh team-defaults --dry-run
|
|
#
|
|
# Reads the id and version out of the plugin's manifest.json, builds a .tar.gz
|
|
# whose members sit at the archive root (manifest.json + files/...), verifies
|
|
# the result the same way the server will, then POSTs it.
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
PLUGIN_ID="${1:-}"
|
|
shift || true
|
|
|
|
URL="${DSH_URL:-http://192.168.5.2:8020}"
|
|
TOKEN="${DSH_ADMIN_TOKEN:-74020d5458c05a43e6abca0ddea020b5}"
|
|
DRY_RUN=0
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--url) URL="$2"; shift 2 ;;
|
|
--token) TOKEN="$2"; shift 2 ;;
|
|
--dry-run) DRY_RUN=1; shift ;;
|
|
*) echo "unknown option: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
if [ -z "$PLUGIN_ID" ]; then
|
|
echo "usage: $0 <plugin-id> [--url U] [--token T] [--dry-run]" >&2
|
|
echo "available:" >&2
|
|
ls -1 "$ROOT/plugins" 2>/dev/null | sed 's/^/ /' >&2
|
|
exit 2
|
|
fi
|
|
|
|
SRC="$ROOT/plugins/$PLUGIN_ID"
|
|
[ -d "$SRC" ] || { echo "no such plugin dir: $SRC" >&2; exit 1; }
|
|
[ -f "$SRC/manifest.json" ] || { echo "missing $SRC/manifest.json" >&2; exit 1; }
|
|
|
|
VERSION="$(python -c "import json,sys; print(json.load(open(sys.argv[1]))['version'])" \
|
|
"$SRC/manifest.json")"
|
|
MANIFEST_ID="$(python -c "import json,sys; print(json.load(open(sys.argv[1]))['id'])" \
|
|
"$SRC/manifest.json")"
|
|
|
|
if [ "$MANIFEST_ID" != "$PLUGIN_ID" ]; then
|
|
echo "manifest.id ($MANIFEST_ID) != directory name ($PLUGIN_ID)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# The archive must contain manifest.json at its root: the server looks for that
|
|
# exact path and rejects anything it cannot find. Packing the directory itself
|
|
# would put everything under <id>/ and fail validation.
|
|
#
|
|
# Staged inside build/ rather than the system temp dir: curl on Windows is a
|
|
# native binary and cannot open an MSYS-style /tmp/... path for -F @file.
|
|
STAGE="$ROOT/build/publish-$PLUGIN_ID"
|
|
rm -rf "$STAGE"
|
|
mkdir -p "$ROOT/build" "$STAGE/pkg"
|
|
trap 'rm -rf "$STAGE"' EXIT
|
|
cp -r "$SRC/." "$STAGE/pkg/"
|
|
rm -rf "$STAGE/pkg/.git" "$STAGE/pkg/__pycache__" 2>/dev/null || true
|
|
find "$STAGE/pkg" -name '*.pyc' -delete 2>/dev/null || true
|
|
|
|
ARCHIVE="$STAGE/$PLUGIN_ID-$VERSION.tar.gz"
|
|
# Build the tar with Python: GNU tar's `-C dir .` emits a bare `./` member and
|
|
# listing entries explicitly duplicates whatever find returns for directories.
|
|
# Python's TarFile.add writes exactly the relative paths we choose.
|
|
python - "$STAGE/pkg" "$ARCHIVE" <<'PY'
|
|
import os, sys, tarfile
|
|
from pathlib import Path
|
|
|
|
src, dst = Path(sys.argv[1]), Path(sys.argv[2])
|
|
SKIP_DIRS = {".git", "__pycache__", ".pytest_cache"}
|
|
with tarfile.open(dst, "w:gz") as tf:
|
|
for path in sorted(src.rglob("*")):
|
|
rel = path.relative_to(src)
|
|
if any(part in SKIP_DIRS for part in rel.parts):
|
|
continue
|
|
if path.suffix == ".pyc":
|
|
continue
|
|
# recursive=False: each path is added once, by its own rel name.
|
|
tf.add(path, arcname=rel.as_posix(), recursive=False)
|
|
PY
|
|
|
|
echo "plugin : $PLUGIN_ID"
|
|
echo "version : $VERSION"
|
|
echo "archive : $(stat -c %s "$ARCHIVE") bytes"
|
|
echo "sha256 : $(sha256sum "$ARCHIVE" | cut -d' ' -f1)"
|
|
echo "members :"
|
|
tar -tzf "$ARCHIVE" | sed 's/^/ /'
|
|
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
echo
|
|
echo "dry run — not uploading. archive kept at $ARCHIVE"
|
|
trap - EXIT
|
|
exit 0
|
|
fi
|
|
|
|
echo
|
|
echo ">> uploading to $URL"
|
|
# Python rather than curl -F: the multipart body has to be built by hand to
|
|
# avoid path-format disagreements between MSYS paths and the native curl binary.
|
|
python - "$URL" "$TOKEN" "$PLUGIN_ID" "$ARCHIVE" <<'PY'
|
|
import json, sys, urllib.error, urllib.request, uuid
|
|
from pathlib import Path
|
|
|
|
url, token, plugin_id, archive = sys.argv[1:5]
|
|
blob = Path(archive).read_bytes()
|
|
boundary = uuid.uuid4().hex
|
|
body = b"".join([
|
|
f"--{boundary}\r\n".encode(),
|
|
f'Content-Disposition: form-data; name="file"; filename="{Path(archive).name}"\r\n'.encode(),
|
|
b"Content-Type: application/gzip\r\n\r\n",
|
|
blob,
|
|
f"\r\n--{boundary}--\r\n".encode(),
|
|
])
|
|
req = urllib.request.Request(
|
|
f"{url}/v1/plugins/{plugin_id}", data=body, method="POST",
|
|
headers={"Authorization": f"Bearer {token}",
|
|
"Content-Type": f"multipart/form-data; boundary={boundary}"})
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=60) as r:
|
|
print(json.dumps(json.loads(r.read()), indent=2, ensure_ascii=False))
|
|
except urllib.error.HTTPError as e:
|
|
detail = e.read().decode("utf-8", "replace")
|
|
print(f"publish failed (HTTP {e.code}): {detail}", file=sys.stderr)
|
|
sys.exit(1)
|
|
PY
|
|
STATUS=$?
|
|
[ "$STATUS" = "0" ] || exit "$STATUS"
|
|
echo
|
|
echo "published. install with:"
|
|
echo " python client/dsh_sync_client.py install $PLUGIN_ID"
|