105 lines
4.5 KiB
Bash
Executable File
105 lines
4.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
die() { printf 'deploy: %s\n' "$*" >&2; exit 1; }
|
|
|
|
for name in DEPLOY_HOST DEPLOY_USER DEPLOY_ROOT DEPLOY_DOMAIN; do
|
|
[[ -n "${!name:-}" ]] || die "set $name before running"
|
|
done
|
|
|
|
command -v git >/dev/null || die 'git is required locally'
|
|
command -v ssh >/dev/null || die 'ssh is required locally'
|
|
git rev-parse --show-toplevel >/dev/null 2>&1 || die 'run from inside the Git repository'
|
|
|
|
repo_root="$(git rev-parse --show-toplevel)"
|
|
cd "$repo_root"
|
|
ref="${DEPLOY_REF:-$(git branch --show-current)}"
|
|
[[ -n "$ref" ]] || die 'detached HEAD: set DEPLOY_REF to a branch name'
|
|
git check-ref-format --branch "$ref" >/dev/null 2>&1 || die 'DEPLOY_REF is not a valid branch name'
|
|
|
|
if [[ -n "$(git status --porcelain --untracked-files=normal)" ]]; then
|
|
die 'working tree has changes; commit and push the intended release first'
|
|
fi
|
|
|
|
remote_url="$(git remote get-url origin 2>/dev/null)" || die 'Git remote origin is not configured'
|
|
remote_sha="$(git ls-remote --heads "$remote_url" "refs/heads/$ref" | awk 'NR == 1 {print $1}')"
|
|
[[ "$remote_sha" =~ ^[0-9a-f]{40,64}$ ]] || die "branch '$ref' was not found on origin"
|
|
local_sha="$(git rev-parse HEAD)"
|
|
[[ "$local_sha" == "$remote_sha" ]] || die 'HEAD must exactly match the commit currently pushed to origin'
|
|
|
|
port="${DEPLOY_PORT:-2101}"
|
|
[[ "$port" =~ ^[0-9]{1,5}$ ]] && (( port > 0 && port < 65536 )) || die 'DEPLOY_PORT must be between 1 and 65535'
|
|
[[ "$DEPLOY_HOST" != *$'\n'* && "$DEPLOY_USER" != *$'\n'* && "$DEPLOY_DOMAIN" != *$'\n'* ]] || die 'deployment values cannot contain newlines'
|
|
[[ "$DEPLOY_HOST" =~ ^[a-zA-Z0-9._:-]+$ ]] || die 'DEPLOY_HOST must be a hostname or IP address'
|
|
[[ "$DEPLOY_USER" =~ ^[a-zA-Z0-9._-]+$ ]] || die 'DEPLOY_USER contains unsupported characters'
|
|
[[ "$DEPLOY_DOMAIN" =~ ^[a-zA-Z0-9.-]+$ ]] || die 'DEPLOY_DOMAIN must be a plain domain name'
|
|
[[ "$DEPLOY_ROOT" == /home/*/*/* ]] || die 'DEPLOY_ROOT must be a site directory below /home, e.g. /home/siteuser/htdocs/example.com'
|
|
[[ "$DEPLOY_ROOT" != *'..'* && "$DEPLOY_ROOT" != *$'\n'* ]] || die 'DEPLOY_ROOT contains an unsafe path component'
|
|
|
|
printf 'Target: %s@%s:%s domain=%s ref=%s commit=%s\n' \
|
|
"$DEPLOY_USER" "$DEPLOY_HOST" "$port" "$DEPLOY_DOMAIN" "$ref" "$remote_sha"
|
|
|
|
if [[ "${DEPLOY_DRY_RUN:-0}" == 1 ]]; then
|
|
printf 'Dry run: local Git and target settings are valid; no SSH connection was made.\n'
|
|
exit 0
|
|
fi
|
|
|
|
ssh -p "$port" \
|
|
-o BatchMode=yes \
|
|
-o ConnectTimeout=10 \
|
|
-o StrictHostKeyChecking=yes \
|
|
"$DEPLOY_USER@$DEPLOY_HOST" \
|
|
bash -s -- "$remote_url" "$ref" "$remote_sha" "$DEPLOY_ROOT" "$DEPLOY_DOMAIN" <<'REMOTE_SCRIPT'
|
|
set -euo pipefail
|
|
die() { printf 'remote deploy: %s\n' "$*" >&2; exit 1; }
|
|
|
|
repo_url="$1"
|
|
ref="$2"
|
|
expected_sha="$3"
|
|
root="$4"
|
|
domain="$5"
|
|
|
|
[[ "$root" == /home/*/*/* && "$root" != *'..'* ]] || die 'unsafe deployment root'
|
|
[[ "$expected_sha" =~ ^[0-9a-f]{40,64}$ ]] || die 'invalid commit hash'
|
|
[[ "$domain" =~ ^[a-zA-Z0-9.-]+$ ]] || die 'invalid domain'
|
|
|
|
deploy_dir="$root/.deploy"
|
|
repo_dir="$deploy_dir/repository.git"
|
|
releases_dir="$root/releases"
|
|
shared_dir="$root/shared"
|
|
release_dir="$releases_dir/$expected_sha"
|
|
|
|
mkdir -p "$deploy_dir" "$releases_dir" "$shared_dir"
|
|
chmod 700 "$deploy_dir" "$shared_dir"
|
|
[[ -f "$shared_dir/.env" ]] || die "missing $shared_dir/.env; create it privately before the first deploy"
|
|
chmod 600 "$shared_dir/.env"
|
|
|
|
if [[ ! -d "$repo_dir" ]]; then
|
|
git init --bare --quiet "$repo_dir"
|
|
git --git-dir="$repo_dir" remote add origin "$repo_url"
|
|
fi
|
|
|
|
git --git-dir="$repo_dir" fetch --quiet --no-tags origin \
|
|
"+refs/heads/$ref:refs/remotes/origin/$ref"
|
|
actual_sha="$(git --git-dir="$repo_dir" rev-parse "refs/remotes/origin/$ref")"
|
|
[[ "$actual_sha" == "$expected_sha" ]] || die 'origin moved during deploy; rerun using the new pushed commit'
|
|
[[ ! -e "$release_dir" ]] || die "release already exists: $release_dir"
|
|
|
|
mkdir "$release_dir"
|
|
git --git-dir="$repo_dir" archive "$actual_sha" | tar -x -C "$release_dir"
|
|
[[ -d "$release_dir/public" ]] || die 'release has no public/ web root; configure the app layout before deployment'
|
|
|
|
if command -v php >/dev/null 2>&1 && [[ -d "$release_dir/backend" ]]; then
|
|
while IFS= read -r -d '' php_file; do
|
|
php -l "$php_file" >/dev/null || die "PHP syntax check failed: $php_file"
|
|
done < <(find "$release_dir/backend" -type f -name '*.php' -print0)
|
|
fi
|
|
|
|
printf '%s\n' "$domain" > "$shared_dir/.site-domain"
|
|
ln -s "$release_dir" "$root/current.next"
|
|
mv -Tf "$root/current.next" "$root/current"
|
|
|
|
printf 'Published %s to %s\n' "$actual_sha" "$root/current"
|
|
printf 'CloudPanel document root must point to: %s/current/public\n' "$root"
|
|
REMOTE_SCRIPT
|