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
67 lines
2.1 KiB
Bash
Executable file
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"
|