Hostityourself/scripts/git-shell
Claude cb0795617f
feat: git push deploy (roadmap step 2)
Full self-contained git push flow — no GitHub required:

  git remote add hiy ssh://hiy@myserver/myapp
  git push hiy main

What was added:

- Bare git repo per app (HIY_DATA_DIR/repos/<app-id>.git)
  Initialised automatically on app create; removed on app delete.
  post-receive hook is written into each repo and calls the internal
  API to queue a build using the same pipeline as webhook deploys.

- SSH key management
  New ssh_keys DB table. Admin UI (/admin/users) now shows SSH keys
  per user with add/remove. New API routes:
    GET/POST /api/users/:id/ssh-keys
    DELETE   /api/ssh-keys/:key_id
  On every change, HIY rewrites HIY_SSH_AUTHORIZED_KEYS with
  command= restricted entries pointing at hiy-git-shell.

- scripts/git-shell
  SSH command= override installed at HIY_GIT_SHELL (default
  /usr/local/bin/hiy-git-shell). Validates the push via
  GET /internal/git/auth, then exec's git-receive-pack on the
  correct bare repo.

- Internal API routes (authenticated by shared internal_token)
    GET  /internal/git/auth          -- git-shell permission check
    POST /internal/git/:app_id/push  -- post-receive build trigger

- Builder: git-push deploys use file:// path to the local bare repo
  instead of the app's remote repo_url.

- internal_token persists across restarts in HIY_DATA_DIR/internal-token.

New env vars:
  HIY_SSH_AUTHORIZED_KEYS  path to the authorized_keys file to manage
  HIY_GIT_SHELL            path to the git-shell script on the host

Both webhook and git-push deploys feed the same build queue.

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-23 08:54:55 +00:00

67 lines
2.1 KiB
Bash
Executable file

#!/usr/bin/env bash
# HIY git-shell — SSH authorized_keys command= override
#
# Install at: /usr/local/bin/hiy-git-shell (or set HIY_GIT_SHELL in .env)
# Each authorized_keys entry is written by HIY in this form:
#
# command="/usr/local/bin/hiy-git-shell <user-id> <api-url> <token> <repos-dir>",
# no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty <public-key>
#
# OpenSSH sets SSH_ORIGINAL_COMMAND to what the developer actually ran, e.g.:
# git-receive-pack '/myapp'
# This script validates the push and exec's git-receive-pack on the bare repo.
set -euo pipefail
USER_ID="${1:-}"
API_URL="${2:-http://localhost:3000}"
TOKEN="${3:-}"
REPOS_DIR="${4:-/data/repos}"
if [ -z "$USER_ID" ] || [ -z "$TOKEN" ]; then
echo "hiy: internal configuration error — contact your administrator." >&2
exit 1
fi
ORIG="${SSH_ORIGINAL_COMMAND:-}"
# Only git-receive-pack (push) is supported; reject everything else.
if [[ "$ORIG" != git-receive-pack* ]]; then
echo "hiy: only 'git push' is supported on this host." >&2
exit 1
fi
# Parse the app name from: git-receive-pack '/myapp' or git-receive-pack 'myapp'
APP_NAME=$(echo "$ORIG" | sed "s/git-receive-pack '\\///;s/git-receive-pack '//;s/'.*//")
APP_NAME="${APP_NAME##/}" # strip leading slash if still present
APP_NAME="${APP_NAME%/}" # strip trailing slash
if [ -z "$APP_NAME" ]; then
echo "hiy: could not parse app name from: $ORIG" >&2
exit 1
fi
# Ask HIY whether this user may push to this app.
RESPONSE=$(curl -sf \
-H "X-Hiy-Token: $TOKEN" \
"${API_URL}/internal/git/auth?user_id=${USER_ID}&app=${APP_NAME}" \
2>/dev/null) || {
echo "hiy: push denied (cannot reach server or access denied for '${APP_NAME}')." >&2
exit 1
}
APP_ID=$(echo "$RESPONSE" | python3 -c \
"import sys, json; print(json.load(sys.stdin)['app_id'])" 2>/dev/null)
if [ -z "$APP_ID" ]; then
echo "hiy: push denied for '${APP_NAME}'." >&2
exit 1
fi
REPO_PATH="${REPOS_DIR}/${APP_ID}.git"
if [ ! -d "$REPO_PATH" ]; then
echo "hiy: repository not found for app '${APP_NAME}' (expected ${REPO_PATH})." >&2
exit 1
fi
exec git-receive-pack "$REPO_PATH"