Files
ClawSkills/git-hangman-lab/scripts/publish-package
lyn 1e7af09245 publish-package: read current link state before re-linking
Gitea's /-/link/{repo} endpoint returns HTTP 400 "invalid argument"
whenever the package already has ANY linked repo, and /-/unlink returns
the same 400 when nothing is linked. Both are non-idempotent. Fix by
GET-ing the package version first, comparing the current repository to
the target, and only unlinking/linking when the state actually needs to
change. Idempotent across repeated publishes of the same image.
2026-04-15 11:03:32 +00:00

208 lines
6.6 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
COMMAND=""
REGISTRY=""
IMAGE=""
TAG=""
PACKAGE_FILE=""
REPO=""
usage() {
echo "Usage:"
echo " publish-package docker <registry> <image> <tag> --proj <repo>"
echo " publish-package nuget <source> <package-file> --proj <repo>"
echo " publish-package pypi <package-file> --proj <repo>"
echo " publish-package npm --proj <repo>"
exit 1
}
while [[ $# -gt 0 ]]; do
case $1 in
docker|nuget|pypi|npm)
COMMAND="$1"
shift
;;
--proj)
REPO="$2"
shift 2
;;
*)
if [[ -z "$COMMAND" ]]; then
usage
fi
case $COMMAND in
docker)
[[ -z "$REGISTRY" ]] && REGISTRY="$1" && shift && continue
[[ -z "$IMAGE" ]] && IMAGE="$1" && shift && continue
[[ -z "$TAG" ]] && TAG="$1" && shift && continue
;;
nuget)
[[ -z "$SOURCE" ]] && SOURCE="$1" && shift && continue
[[ -z "$PACKAGE_FILE" ]] && PACKAGE_FILE="$1" && shift && continue
;;
pypi)
[[ -z "$PACKAGE_FILE" ]] && PACKAGE_FILE="$1" && shift && continue
;;
esac
shift
;;
esac
done
if [[ -z "$COMMAND" ]] || [[ -z "$REPO" ]]; then
usage
fi
do_docker() {
if [[ -z "$REGISTRY" ]] || [[ -z "$IMAGE" ]] || [[ -z "$TAG" ]]; then
echo "Error: docker requires <registry> <image> <tag>"
exit 1
fi
OWNER=$(ego-mgr get default-username)
if [[ -z "$OWNER" ]]; then
echo "Error: cannot get username from ego-mgr"
exit 1
fi
KEY=$(python3 -c "import uuid; print(uuid.uuid4())")
LOCKFILE="$HOME/.openclaw/.docker"
lock-mgr acquire "$LOCKFILE" "$KEY"
# Push via SSH tunnel to bypass Cloudflare's 100MB request body limit.
# Tunnel forwards 127.0.0.1:$TUNNEL_PORT on this host to Gitea's HTTP port on vps.git.
TUNNEL_HOST="root@vps.git"
TUNNEL_PORT="5000"
TUNNEL_LOCAL="127.0.0.1:${TUNNEL_PORT}"
TUNNEL_CTL="$HOME/.openclaw/.docker-tunnel.sock"
rm -f "$TUNNEL_CTL"
ssh -fN -o ExitOnForwardFailure=yes -o ControlMaster=yes -o ControlPath="$TUNNEL_CTL" \
-L "${TUNNEL_LOCAL}:127.0.0.1:3000" "$TUNNEL_HOST"
cleanup() {
ssh -O exit -o ControlPath="$TUNNEL_CTL" "$TUNNEL_HOST" 2>/dev/null || true
rm -f "$TUNNEL_CTL"
docker logout "$REGISTRY" 2>/dev/null || true
docker logout "$TUNNEL_LOCAL" 2>/dev/null || true
lock-mgr release "$LOCKFILE" "$KEY" 2>/dev/null || true
}
trap cleanup EXIT
echo "Logging in to $REGISTRY (via tunnel $TUNNEL_LOCAL)..."
docker login "$TUNNEL_LOCAL" -u "$OWNER" -p "$(secret-mgr get-secret --key git)" 2>/dev/null
FULL_IMAGE="${REGISTRY}/${OWNER}/${IMAGE}:${TAG}"
TUNNEL_IMAGE="${TUNNEL_LOCAL}/${OWNER}/${IMAGE}:${TAG}"
echo "Building: $FULL_IMAGE"
cd "$REPO"
docker build -t "$FULL_IMAGE" .
docker tag "$FULL_IMAGE" "$TUNNEL_IMAGE"
echo "Pushing via tunnel: $TUNNEL_IMAGE"
docker push "$TUNNEL_IMAGE"
docker rmi "$TUNNEL_IMAGE" 2>/dev/null || true
# Link package to repository
TOKEN=$(secret-mgr get-secret --key git-access-token)
REPO_NAME=$(basename "$REPO")
# Determine the actual owner of the git repository
REPO_OWNER="$OWNER"
if [[ -d "$REPO/.git" ]]; then
REMOTE_URL=$(git -C "$REPO" remote get-url origin 2>/dev/null || true)
if [[ "$REMOTE_URL" =~ git\.hangman-lab\.top/([^/]+)/ ]]; then
REPO_OWNER="${BASH_REMATCH[1]}"
fi
fi
# Gitea's link/unlink endpoints are not idempotent: POST /-/link/{repo}
# returns HTTP 400 "invalid argument" when the package already has ANY link
# (even to the same repo), and POST /-/unlink returns 400 when nothing is
# linked. So read the current link first and branch accordingly.
TARGET_LINK="${REPO_OWNER}/${REPO_NAME}"
STATE_RESP=$(curl -s -w "\n%{http_code}" -u "${OWNER}:${TOKEN}" \
"https://git.hangman-lab.top/api/v1/packages/${OWNER}/container/${IMAGE}/${TAG}")
STATE_STATUS=$(printf "%s" "$STATE_RESP" | tail -n1)
STATE_BODY=$(printf "%s" "$STATE_RESP" | sed '$d')
CURRENT_LINK=""
if [[ "$STATE_STATUS" == "200" ]]; then
CURRENT_LINK=$(printf "%s" "$STATE_BODY" | python3 -c \
'import sys, json; p=json.load(sys.stdin); r=p.get("repository") or {}; print(r.get("full_name") or "")' \
2>/dev/null || echo "")
fi
if [[ "$CURRENT_LINK" == "$TARGET_LINK" ]]; then
echo "Package already linked to ${TARGET_LINK}, skipping link step."
else
if [[ -n "$CURRENT_LINK" ]]; then
echo "Package currently linked to ${CURRENT_LINK}, unlinking first..."
UNLINK_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST -u "${OWNER}:${TOKEN}" \
"https://git.hangman-lab.top/api/v1/packages/${OWNER}/container/${IMAGE}/-/unlink")
if [[ "$UNLINK_STATUS" != "204" ]]; then
echo "Warning: unlink returned HTTP $UNLINK_STATUS, proceeding with link attempt anyway..."
fi
fi
LINK_RESP=$(curl -s -w "%{http_code}" -X POST \
-u "${OWNER}:${TOKEN}" \
"https://git.hangman-lab.top/api/v1/packages/${OWNER}/container/${IMAGE}/-/link/${REPO_NAME}")
LINK_STATUS="${LINK_RESP: -3}"
LINK_BODY="${LINK_RESP:0:-3}"
if [[ "$LINK_STATUS" != "200" && "$LINK_STATUS" != "201" ]]; then
if echo "$LINK_BODY" | grep -q '"message".*repository does not exist'; then
echo "Warning: repository '$REPO_NAME' is not owned by '$OWNER' — skipping link (requires site admin or matching owner)."
else
echo "Warning: package link failed (HTTP $LINK_STATUS): $LINK_BODY"
fi
else
echo "Linked package to ${TARGET_LINK}."
fi
fi
lock-mgr release "$LOCKFILE" "$KEY"
echo "Done: $FULL_IMAGE"
}
do_nuget() {
echo "publish-package nuget: not yet implemented"
exit 1
}
do_pypi() {
echo "publish-package pypi: not yet implemented"
exit 1
}
do_npm() {
echo "publish-package npm: not yet implemented"
exit 1
}
# For docker, determine the actual repo owner via search API and switch to that owner's agent-id
if [[ "$COMMAND" == "docker" ]]; then
REPO_NAME=$(basename "$REPO")
SCRIPT_DIR_CALLER=$(cd "$(dirname "$0")" && pwd)
search_result=$("$SCRIPT_DIR_CALLER/repo" search "$REPO_NAME" 2>&1) || true
if [[ -n "$search_result" ]] && echo "$search_result" | python3 -q -c "import sys,json; sys.exit(0 if json.load(sys.stdin) else 1)" 2>/dev/null; then
repo_owner=$(echo "$search_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('owner',''))" 2>/dev/null)
if [[ -n "$repo_owner" ]]; then
owner_agent_id=$(ego-mgr lookup "$repo_owner" 2>/dev/null || echo "")
if [[ -n "$owner_agent_id" ]]; then
export AGENT_ID="$owner_agent_id"
fi
fi
fi
fi
case "$COMMAND" in
docker) do_docker ;;
nuget) do_nuget ;;
pypi) do_pypi ;;
npm) do_npm ;;
esac