CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
install-self-host.sh
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| c4e643c | 1 | #!/usr/bin/env bash |
| 2 | # ============================================================================= | |
| 3 | # Gluecron self-host installer. | |
| 4 | # | |
| 5 | # curl -fsSL https://gluecron.com/install-server | bash | |
| 6 | # | |
| 7 | # Goal: a working self-hosted Gluecron in 60 seconds. Microsoft GHE is a | |
| 8 | # 50GB blob and a week of professional services; we ship a ~200MB binary | |
| 9 | # and a curl-bash. | |
| 10 | # | |
| 11 | # What this script does (every step is idempotent + verbose): | |
| 12 | # 1. Detect OS + arch (linux/darwin × x64/arm64) | |
| 13 | # 2. Resolve host (default https://gluecron.com, env override) | |
| 14 | # 3. Fetch SHA256SUMS, then the binary, verify the hash | |
| 15 | # 4. Install to /opt/gluecron/bin (sudo) or ~/.gluecron/bin (no sudo) | |
| 16 | # 5. Provision Postgres: detect existing OR offer docker postgres | |
| 17 | # 6. Write /etc/gluecron.env (or ~/.gluecron/gluecron.env) from .env.example | |
| 18 | # and prompt for the few required keys | |
| 19 | # 7. Run migrations | |
| 20 | # 8. Create a systemd unit (linux) OR launchd plist (darwin) + start it | |
| 21 | # 9. Print the "Your Gluecron is live at http://localhost:3010" banner | |
| 22 | # | |
| 23 | # Hard rules: | |
| 24 | # - Never `set +e` — failures must bubble up. | |
| 25 | # - Never assume jq / docker / sudo are present — feature-flag everything. | |
| 26 | # - Re-runnable: a second run upgrades the binary in place. | |
| 27 | # ============================================================================= | |
| 28 | ||
| 29 | set -Eeuo pipefail | |
| 30 | ||
| 31 | # ── pretty printers (matched to scripts/install.sh) ──────────────────────── | |
| 32 | say() { printf "\n\033[1;34m> %s\033[0m\n" "$*"; } | |
| 33 | ok() { printf " \033[32mv\033[0m %s\n" "$*"; } | |
| 34 | warn() { printf " \033[33m!\033[0m %s\n" "$*"; } | |
| 35 | fail() { printf " \033[31mx\033[0m %s\n" "$*"; exit 1; } | |
| 36 | ||
| 37 | HOST="${GLUECRON_HOST:-https://gluecron.com}" | |
| 38 | HOST="${HOST%/}" | |
| 39 | PORT="${GLUECRON_PORT:-3010}" | |
| 40 | ||
| 41 | # ── 1. Detect platform ───────────────────────────────────────────────────── | |
| 42 | say "[1/9] Detecting platform" | |
| 43 | UNAME_S="$(uname -s 2>/dev/null || echo unknown)" | |
| 44 | UNAME_M="$(uname -m 2>/dev/null || echo unknown)" | |
| 45 | ||
| 46 | case "$UNAME_S" in | |
| 47 | Linux*) PLAT=linux ;; | |
| 48 | Darwin*) PLAT=darwin ;; | |
| 49 | *) fail "Unsupported OS: $UNAME_S (linux / darwin only)" ;; | |
| 50 | esac | |
| 51 | ||
| 52 | case "$UNAME_M" in | |
| 53 | x86_64|amd64) ARCH=x64 ;; | |
| 54 | aarch64|arm64) ARCH=arm64 ;; | |
| 55 | *) fail "Unsupported arch: $UNAME_M (x64 / arm64 only)" ;; | |
| 56 | esac | |
| 57 | ||
| 58 | TARGET="$PLAT-$ARCH" | |
| 59 | BIN_NAME="gluecron-server-$TARGET" | |
| 60 | ok "platform: $TARGET" | |
| 61 | ||
| 62 | # ── 2. Choose install root (sudo vs user) ────────────────────────────────── | |
| 63 | say "[2/9] Resolving install location" | |
| 64 | USE_SUDO=0 | |
| 65 | INSTALL_PREFIX="" | |
| 66 | ENV_FILE="" | |
| 67 | SERVICE_USER="" | |
| 68 | ||
| 69 | if [ "$(id -u)" = "0" ]; then | |
| 70 | INSTALL_PREFIX="/opt/gluecron" | |
| 71 | ENV_FILE="/etc/gluecron.env" | |
| 72 | SERVICE_USER="gluecron" | |
| 73 | ok "running as root — installing to $INSTALL_PREFIX" | |
| 74 | elif command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then | |
| 75 | USE_SUDO=1 | |
| 76 | INSTALL_PREFIX="/opt/gluecron" | |
| 77 | ENV_FILE="/etc/gluecron.env" | |
| 78 | SERVICE_USER="${SUDO_USER:-$(whoami)}" | |
| 79 | ok "passwordless sudo available — installing to $INSTALL_PREFIX" | |
| 80 | else | |
| 81 | INSTALL_PREFIX="$HOME/.gluecron" | |
| 82 | ENV_FILE="$HOME/.gluecron/gluecron.env" | |
| 83 | SERVICE_USER="$(whoami)" | |
| 84 | ok "no sudo — installing to $INSTALL_PREFIX (user-scope)" | |
| 85 | fi | |
| 86 | ||
| 87 | run_priv() { | |
| 88 | if [ "$USE_SUDO" = "1" ]; then | |
| 89 | sudo "$@" | |
| 90 | else | |
| 91 | "$@" | |
| 92 | fi | |
| 93 | } | |
| 94 | ||
| 95 | run_priv mkdir -p "$INSTALL_PREFIX/bin" "$INSTALL_PREFIX/repos" "$INSTALL_PREFIX/data" | |
| 96 | ||
| 97 | # ── 3. Download + verify the binary ──────────────────────────────────────── | |
| 98 | say "[3/9] Fetching binary from $HOST" | |
| 99 | TMP="$(mktemp -d -t gluecron-install.XXXXXX)" | |
| 100 | trap 'rm -rf "$TMP"' EXIT | |
| 101 | ||
| 102 | SUMS_URL="$HOST/dist/SHA256SUMS" | |
| 103 | BIN_URL="$HOST/dist/$BIN_NAME" | |
| 104 | ||
| 105 | curl -fsSL --max-time 30 "$SUMS_URL" -o "$TMP/SHA256SUMS" \ | |
| 106 | || fail "Couldn't fetch $SUMS_URL — is the host serving binaries?" | |
| 107 | ok "checksums fetched" | |
| 108 | ||
| 109 | EXPECTED_HASH=$(awk -v f="$BIN_NAME" '$2==f {print $1}' "$TMP/SHA256SUMS") | |
| 110 | if [ -z "$EXPECTED_HASH" ]; then | |
| 111 | fail "No checksum for $BIN_NAME in SHA256SUMS — build the bundle for your platform." | |
| 112 | fi | |
| 113 | ||
| 114 | curl -fsSL --max-time 600 "$BIN_URL" -o "$TMP/$BIN_NAME" \ | |
| 115 | || fail "Couldn't download $BIN_URL" | |
| 116 | ACTUAL_HASH=$(sha256sum "$TMP/$BIN_NAME" | awk '{print $1}') | |
| 117 | if [ "$ACTUAL_HASH" != "$EXPECTED_HASH" ]; then | |
| 118 | fail "SHA-256 mismatch: expected $EXPECTED_HASH, got $ACTUAL_HASH" | |
| 119 | fi | |
| 120 | ok "verified sha256: ${ACTUAL_HASH:0:16}…" | |
| 121 | ||
| 122 | chmod +x "$TMP/$BIN_NAME" | |
| 123 | run_priv mv -f "$TMP/$BIN_NAME" "$INSTALL_PREFIX/bin/gluecron-server" | |
| 124 | ok "installed: $INSTALL_PREFIX/bin/gluecron-server" | |
| 125 | ||
| 126 | # Optional: fetch the env.example as a starter template | |
| 127 | if curl -fsSL --max-time 15 "$HOST/dist/env.example" -o "$TMP/env.example" 2>/dev/null; then | |
| 128 | ok "env template fetched" | |
| 129 | else | |
| 130 | warn "env.example not on server — using fallback minimal template" | |
| 131 | cat >"$TMP/env.example" <<'ENV_FALLBACK' | |
| 132 | DATABASE_URL=postgresql://gluecron:gluecron@localhost:5432/gluecron | |
| 133 | GIT_REPOS_PATH=/opt/gluecron/repos | |
| 134 | PORT=3010 | |
| 135 | ENV_FALLBACK | |
| 136 | fi | |
| 137 | ||
| 138 | # ── 4. Postgres — detect or provision ────────────────────────────────────── | |
| 139 | say "[4/9] Postgres" | |
| 140 | DETECTED_DB="" | |
| 141 | if command -v psql >/dev/null 2>&1; then | |
| 142 | if psql -h localhost -U postgres -c 'SELECT 1' >/dev/null 2>&1; then | |
| 143 | DETECTED_DB="postgresql://postgres@localhost:5432/postgres" | |
| 144 | ok "found local Postgres reachable as postgres@localhost" | |
| 145 | fi | |
| 146 | fi | |
| 147 | if [ -z "$DETECTED_DB" ] && [ -n "${DATABASE_URL:-}" ]; then | |
| 148 | DETECTED_DB="$DATABASE_URL" | |
| 149 | ok "using DATABASE_URL from env" | |
| 150 | fi | |
| 151 | ||
| 152 | DB_URL="" | |
| 153 | if [ -n "$DETECTED_DB" ]; then | |
| 154 | DB_URL="$DETECTED_DB" | |
| 155 | else | |
| 156 | warn "no existing Postgres detected" | |
| 157 | if command -v docker >/dev/null 2>&1; then | |
| 158 | if [ -t 0 ]; then | |
| 159 | printf " Start a Postgres 14 container via docker? [Y/n] " | |
| 160 | read -r ANSWER | |
| 161 | else | |
| 162 | ANSWER="${GLUECRON_USE_DOCKER_PG:-y}" | |
| 163 | fi | |
| 164 | case "$ANSWER" in | |
| 165 | n|N|no|NO) | |
| 166 | warn "skipping docker — you must set DATABASE_URL manually before first start" | |
| 167 | ;; | |
| 168 | *) | |
| 169 | if docker ps --format '{{.Names}}' | grep -q '^gluecron-postgres$'; then | |
| 170 | ok "gluecron-postgres container already running" | |
| 171 | else | |
| 172 | docker run -d --name gluecron-postgres \ | |
| 173 | -e POSTGRES_USER=gluecron \ | |
| 174 | -e POSTGRES_PASSWORD=gluecron \ | |
| 175 | -e POSTGRES_DB=gluecron \ | |
| 176 | -p 5432:5432 \ | |
| 177 | -v gluecron-pgdata:/var/lib/postgresql/data \ | |
| 178 | postgres:14 >/dev/null \ | |
| 179 | || warn "docker run failed — see docker logs gluecron-postgres" | |
| 180 | ok "started gluecron-postgres container" | |
| 181 | # Give Postgres a few seconds to accept connections. | |
| 182 | sleep 4 | |
| 183 | fi | |
| 184 | DB_URL="postgresql://gluecron:gluecron@localhost:5432/gluecron" | |
| 185 | ;; | |
| 186 | esac | |
| 187 | else | |
| 188 | warn "docker not on PATH either — please set DATABASE_URL in $ENV_FILE before starting" | |
| 189 | fi | |
| 190 | fi | |
| 191 | ||
| 192 | # ── 5. Write env file ────────────────────────────────────────────────────── | |
| 193 | say "[5/9] Writing $ENV_FILE" | |
| 194 | TMP_ENV="$(mktemp -t gluecron-env.XXXXXX)" | |
| 195 | # Start from the template, then overwrite the keys we know. | |
| 196 | cp "$TMP/env.example" "$TMP_ENV" | |
| 197 | patch_env() { | |
| 198 | local KEY="$1" VALUE="$2" | |
| 199 | if grep -qE "^${KEY}=" "$TMP_ENV"; then | |
| 200 | # macOS sed needs the empty -i arg; gnu sed accepts -i alone. | |
| 201 | sed -i.bak -e "s|^${KEY}=.*$|${KEY}=${VALUE}|" "$TMP_ENV" && rm -f "$TMP_ENV.bak" | |
| 202 | else | |
| 203 | echo "${KEY}=${VALUE}" >>"$TMP_ENV" | |
| 204 | fi | |
| 205 | } | |
| 206 | ||
| 207 | if [ -n "$DB_URL" ]; then patch_env DATABASE_URL "$DB_URL"; fi | |
| 208 | patch_env GIT_REPOS_PATH "$INSTALL_PREFIX/repos" | |
| 209 | patch_env PORT "$PORT" | |
| 210 | ||
| 211 | # Prompt only when stdin is a TTY — non-interactive runs (CI, docker build) | |
| 212 | # just inherit existing env vars and ship. | |
| 213 | if [ -t 0 ] && [ -z "${GLUECRON_SKIP_PROMPTS:-}" ]; then | |
| 214 | if ! grep -qE '^APP_BASE_URL=' "$TMP_ENV"; then | |
| 215 | printf " Public URL of this instance [http://localhost:%s]: " "$PORT" | |
| 216 | read -r APP_BASE_URL | |
| 217 | APP_BASE_URL="${APP_BASE_URL:-http://localhost:$PORT}" | |
| 218 | patch_env APP_BASE_URL "$APP_BASE_URL" | |
| 219 | fi | |
| 220 | fi | |
| 221 | ||
| 222 | run_priv mkdir -p "$(dirname "$ENV_FILE")" | |
| 223 | run_priv install -m 0640 "$TMP_ENV" "$ENV_FILE" | |
| 224 | ok "wrote $ENV_FILE" | |
| 225 | ||
| 226 | # ── 6. Migrations ────────────────────────────────────────────────────────── | |
| 227 | say "[6/9] Running database migrations" | |
| 228 | # The compiled binary embeds the migration runner — invoke it via the | |
| 229 | # GLUECRON_RUN_MIGRATIONS env flag (handled in src/index.ts at boot when | |
| 230 | # the corresponding sub-command lands). For now we drive `psql` if the | |
| 231 | # migrations tarball is available. | |
| 232 | if curl -fsSL --max-time 30 "$HOST/dist/migrations.tar.gz" -o "$TMP/migrations.tar.gz" 2>/dev/null; then | |
| 233 | mkdir -p "$TMP/migrations" | |
| 234 | tar -xzf "$TMP/migrations.tar.gz" -C "$TMP/migrations" | |
| 235 | ok "migrations bundle fetched" | |
| 236 | fi | |
| 237 | ||
| 238 | # Source the env so DATABASE_URL is visible to the binary. | |
| 239 | # shellcheck disable=SC1090 | |
| 240 | set -a; . "$ENV_FILE"; set +a | |
| 241 | if [ -n "${DATABASE_URL:-}" ]; then | |
| 242 | if "$INSTALL_PREFIX/bin/gluecron-server" --migrate 2>/dev/null; then | |
| 243 | ok "migrations applied via gluecron-server --migrate" | |
| 244 | else | |
| 245 | warn "binary --migrate failed (older build?) — re-run after first start; the boot path also self-migrates." | |
| 246 | fi | |
| 247 | else | |
| 248 | warn "DATABASE_URL not set — skipped migrations. Set it in $ENV_FILE and re-run." | |
| 249 | fi | |
| 250 | ||
| 251 | # ── 7. Systemd unit (linux) / launchd plist (darwin) ────────────────────── | |
| 252 | say "[7/9] Creating service" | |
| 253 | if [ "$PLAT" = "linux" ] && command -v systemctl >/dev/null 2>&1; then | |
| 254 | UNIT_PATH="/etc/systemd/system/gluecron.service" | |
| 255 | TMP_UNIT="$(mktemp -t gluecron-unit.XXXXXX)" | |
| 256 | cat >"$TMP_UNIT" <<UNIT | |
| 257 | [Unit] | |
| 258 | Description=Gluecron self-host server | |
| 259 | After=network.target | |
| 260 | ||
| 261 | [Service] | |
| 262 | Type=simple | |
| 263 | User=$SERVICE_USER | |
| 264 | EnvironmentFile=$ENV_FILE | |
| 265 | ExecStart=$INSTALL_PREFIX/bin/gluecron-server | |
| 266 | Restart=on-failure | |
| 267 | RestartSec=3 | |
| 268 | WorkingDirectory=$INSTALL_PREFIX | |
| 269 | ||
| 270 | [Install] | |
| 271 | WantedBy=multi-user.target | |
| 272 | UNIT | |
| 273 | run_priv install -m 0644 "$TMP_UNIT" "$UNIT_PATH" | |
| 274 | run_priv systemctl daemon-reload | |
| 275 | run_priv systemctl enable --now gluecron.service \ | |
| 276 | || warn "systemctl enable failed — start manually with: systemctl start gluecron" | |
| 277 | ok "systemd unit installed and started" | |
| 278 | ||
| 279 | elif [ "$PLAT" = "darwin" ]; then | |
| 280 | PLIST_PATH="$HOME/Library/LaunchAgents/com.gluecron.server.plist" | |
| 281 | mkdir -p "$(dirname "$PLIST_PATH")" | |
| 282 | cat >"$PLIST_PATH" <<PLIST | |
| 283 | <?xml version="1.0" encoding="UTF-8"?> | |
| 284 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| 285 | <plist version="1.0"> | |
| 286 | <dict> | |
| 287 | <key>Label</key><string>com.gluecron.server</string> | |
| 288 | <key>ProgramArguments</key> | |
| 289 | <array> | |
| 290 | <string>$INSTALL_PREFIX/bin/gluecron-server</string> | |
| 291 | </array> | |
| 292 | <key>EnvironmentVariables</key><dict> | |
| 293 | <key>GLUECRON_ENV_FILE</key><string>$ENV_FILE</string> | |
| 294 | </dict> | |
| 295 | <key>WorkingDirectory</key><string>$INSTALL_PREFIX</string> | |
| 296 | <key>RunAtLoad</key><true/> | |
| 297 | <key>KeepAlive</key><true/> | |
| 298 | <key>StandardOutPath</key><string>$INSTALL_PREFIX/data/gluecron.out.log</string> | |
| 299 | <key>StandardErrorPath</key><string>$INSTALL_PREFIX/data/gluecron.err.log</string> | |
| 300 | </dict> | |
| 301 | </plist> | |
| 302 | PLIST | |
| 303 | launchctl unload "$PLIST_PATH" 2>/dev/null || true | |
| 304 | launchctl load "$PLIST_PATH" \ | |
| 305 | || warn "launchctl load failed — start manually with: launchctl load $PLIST_PATH" | |
| 306 | ok "launchd plist installed: $PLIST_PATH" | |
| 307 | ||
| 308 | else | |
| 309 | warn "no service manager detected — start manually with:" | |
| 310 | warn " $INSTALL_PREFIX/bin/gluecron-server" | |
| 311 | fi | |
| 312 | ||
| 313 | # ── 8. Wait for health ───────────────────────────────────────────────────── | |
| 314 | say "[8/9] Waiting for the server to come up" | |
| 315 | HEALTH="http://localhost:$PORT/healthz" | |
| 316 | GREEN=0 | |
| 317 | for i in 1 2 3 4 5 6 7 8 9 10; do | |
| 318 | CODE=$(curl -fsS -o /dev/null -w "%{http_code}" --max-time 2 "$HEALTH" || echo 000) | |
| 319 | if [ "$CODE" = "200" ]; then GREEN=1; break; fi | |
| 320 | sleep 1 | |
| 321 | done | |
| 322 | if [ "$GREEN" = "1" ]; then | |
| 323 | ok "/healthz green" | |
| 324 | else | |
| 325 | warn "server didn't return 200 within 10s — check logs (journalctl -u gluecron) or run:" | |
| 326 | warn " $INSTALL_PREFIX/bin/gluecron-server" | |
| 327 | fi | |
| 328 | ||
| 329 | # ── 9. Done ──────────────────────────────────────────────────────────────── | |
| 330 | say "[9/9] All set" | |
| 331 | printf "\n" | |
| 332 | printf "\033[1;32m============================================================\033[0m\n" | |
| 333 | printf "\033[1;32m GLUECRON SELF-HOST INSTALL COMPLETE\033[0m\n" | |
| 334 | printf "\033[1;32m============================================================\033[0m\n" | |
| 335 | printf " Binary: %s/bin/gluecron-server\n" "$INSTALL_PREFIX" | |
| 336 | printf " Env: %s\n" "$ENV_FILE" | |
| 337 | printf " Repos: %s/repos\n" "$INSTALL_PREFIX" | |
| 338 | printf " Port: %s\n" "$PORT" | |
| 339 | printf "\n" | |
| 340 | printf " Your Gluecron is live at \033[1mhttp://localhost:%s\033[0m — visit\n" "$PORT" | |
| 341 | printf " to create your first admin user.\n" | |
| 342 | printf "\n" | |
| 343 | printf " Upgrade: curl -fsSL %s/install-server | bash\n" "$HOST" | |
| 344 | printf " Logs: journalctl -u gluecron -f (linux)\n" | |
| 345 | printf " Logs: tail -f %s/data/gluecron.err.log (darwin)\n" "$INSTALL_PREFIX" | |
| 346 | printf "\n" |