From bf91fbb4930b6f2d4d0817322b24e0177c18dae5 Mon Sep 17 00:00:00 2001 From: Naohiro CHIKAMATSU Date: Fri, 17 Apr 2026 23:28:31 +0900 Subject: [PATCH 1/2] feat: add one-line installer and v0.1.0 release prep Lower the friction of the public install story (Issue #136) and clear the way for a v0.1.0 Hex publish. - scripts/install.sh: new POSIX installer that resolves the latest release tag via the GitHub API, downloads the released escript into `$HOME/.local/bin/sqlode`, warns about missing Erlang/OTP with per-distro install hints, and notes when the install directory is not on PATH. Respects `SQLODE_VERSION` and `SQLODE_INSTALL_DIR`. - README.md: promote the one-line `curl | sh` install as the primary path, reorganise the manual escript and `gleam run` options as fallbacks, and add Hex version, Hex downloads, and CI badges in the style already used by nao1215/oaspec. - .github/workflows/release.yml: pipe `I am not using semantic versioning` into `gleam publish -y`, matching oaspec's workaround for the pre-1.0 semver confirmation prompt so the v0.1.0 publish step does not hang. Closes #136 --- .github/workflows/release.yml | 2 +- README.md | 31 +++++++-- scripts/install.sh | 127 ++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 8 deletions(-) create mode 100755 scripts/install.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 15d830c..2cc3d60 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -87,7 +87,7 @@ jobs: run: gleam build - name: Publish to Hex - run: gleam publish -y + run: echo 'I am not using semantic versioning' | gleam publish -y env: HEXPM_API_KEY: ${{ secrets.HEXPM_API_KEY }} diff --git a/README.md b/README.md index 1ef0981..73bc3f5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # sqlode +[![Hex](https://img.shields.io/hexpm/v/sqlode)](https://hex.pm/packages/sqlode) +[![Hex Downloads](https://img.shields.io/hexpm/dt/sqlode)](https://hex.pm/packages/sqlode) +[![CI](https://github.com/nao1215/sqlode/actions/workflows/ci.yml/badge.svg)](https://github.com/nao1215/sqlode/actions/workflows/ci.yml) [![license](https://img.shields.io/github/license/nao1215/sqlode)](./LICENSE) sqlode reads SQL schema and query files, then generates typed Gleam code. The workflow follows [sqlc](https://sqlc.dev/) conventions: write SQL, run the generator, get type-safe functions. @@ -10,15 +13,20 @@ Supported engines: PostgreSQL, MySQL (parsing only), SQLite. ### Install -There are two ways to install sqlode: +sqlode ships as an Erlang escript. Every install path therefore needs an Erlang/OTP runtime on the host (`escript` on PATH). The easiest way to cover both downloading the escript and detecting a missing runtime is the one-line installer: -#### Option A: Standalone CLI (escript) +#### Option A: One-line install (recommended) -Download the pre-built escript from [GitHub Releases](https://github.com/nao1215/sqlode/releases) and place it on your PATH. Requires Erlang/OTP runtime on the host machine. +```console +curl -fsSL https://raw.githubusercontent.com/nao1215/sqlode/main/scripts/install.sh | sh +``` + +The script downloads the latest release's escript into `$HOME/.local/bin/sqlode`, makes it executable, and warns if Erlang/OTP is missing (with a per-distro install hint). Override the target with `SQLODE_INSTALL_DIR=/usr/local/bin` or pin a version with `SQLODE_VERSION=v0.1.0`. + +If `$HOME/.local/bin` is not on your `PATH`, add it to your shell config: ```console -chmod +x sqlode -./sqlode generate --config=sqlode.yaml +export PATH="$HOME/.local/bin:$PATH" ``` You still need sqlode as a project dependency because generated code imports `sqlode/runtime`: @@ -27,9 +35,18 @@ You still need sqlode as a project dependency because generated code imports `sq gleam add sqlode ``` -#### Option B: Run via Gleam +#### Option B: Manual escript download + +Download the pre-built escript from [GitHub Releases](https://github.com/nao1215/sqlode/releases) and place it on your `PATH`: + +```console +chmod +x sqlode +./sqlode generate --config=sqlode.yaml +``` + +#### Option C: Run via Gleam -Add sqlode as a dependency and invoke the CLI through `gleam run`: +If you already have a Gleam project, you can invoke the CLI through `gleam run` without downloading a separate binary: ```console gleam add sqlode diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..26eb81f --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,127 @@ +#!/bin/sh +# One-line installer for sqlode. Downloads the released escript into a +# user-writable directory and verifies it runs. +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/nao1215/sqlode/main/scripts/install.sh | sh +# +# Environment overrides: +# SQLODE_VERSION - tag to install (default: latest). Example: v0.1.0 +# SQLODE_INSTALL_DIR - directory to install into (default: $HOME/.local/bin) + +set -eu + +REPO="nao1215/sqlode" +BIN_NAME="sqlode" +VERSION="${SQLODE_VERSION:-latest}" +INSTALL_DIR="${SQLODE_INSTALL_DIR:-$HOME/.local/bin}" + +info() { printf '==> %s\n' "$*"; } +warn() { printf 'warning: %s\n' "$*" >&2; } +die() { printf 'error: %s\n' "$*" >&2; exit 1; } + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || die "missing required command: $1" +} + +need_cmd curl +need_cmd uname + +# sqlode ships as an escript, which is a portable Erlang archive. It needs +# `escript` (part of the Erlang/OTP runtime) on the PATH at run time. +detect_distro_id() { + if [ -r /etc/os-release ]; then + # shellcheck disable=SC1091 + (. /etc/os-release; printf '%s' "${ID:-}") + fi +} + +check_erlang() { + if command -v escript >/dev/null 2>&1; then + return 0 + fi + + warn "Erlang/OTP runtime not found on PATH." + os="$(uname -s)" + case "$os" in + Linux) + case "$(detect_distro_id)" in + ubuntu|debian) warn "install it with: sudo apt-get install -y erlang" ;; + fedora|rhel|centos) warn "install it with: sudo dnf install -y erlang" ;; + arch|manjaro) warn "install it with: sudo pacman -S --noconfirm erlang" ;; + alpine) warn "install it with: sudo apk add erlang" ;; + *) warn "install Erlang/OTP via your distribution's package manager" ;; + esac + ;; + Darwin) + warn "install it with: brew install erlang" + ;; + *) + warn "see https://www.erlang.org/downloads for install options" + ;; + esac + warn "sqlode will still be downloaded, but you must install Erlang/OTP before running it." +} + +resolve_version() { + if [ "$VERSION" = "latest" ]; then + url="https://api.github.com/repos/${REPO}/releases/latest" + tag="$(curl -fsSL "$url" | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n1)" + if [ -z "$tag" ]; then + die "could not determine latest release tag. Set SQLODE_VERSION=vX.Y.Z to pin a version." + fi + VERSION="$tag" + fi +} + +download() { + url="https://github.com/${REPO}/releases/download/${VERSION}/${BIN_NAME}" + info "downloading ${BIN_NAME} ${VERSION} from ${url}" + tmp="$(mktemp -t sqlode.XXXXXX)" + if ! curl -fsSL -o "$tmp" "$url"; then + rm -f "$tmp" + die "failed to download ${url}. Check SQLODE_VERSION or visit https://github.com/${REPO}/releases." + fi + DOWNLOAD="$tmp" +} + +install_bin() { + mkdir -p "$INSTALL_DIR" + dest="$INSTALL_DIR/$BIN_NAME" + mv "$DOWNLOAD" "$dest" + chmod +x "$dest" + INSTALLED="$dest" +} + +verify() { + if ! command -v escript >/dev/null 2>&1; then + info "skipping run check (Erlang/OTP not on PATH yet)" + return 0 + fi + if ! "$INSTALLED" --help >/dev/null 2>&1; then + warn "${INSTALLED} was installed but --help failed. Inspect manually." + fi +} + +path_hint() { + case ":$PATH:" in + *":$INSTALL_DIR:"*) ;; + *) + warn "$INSTALL_DIR is not on your PATH." + warn "add it to your shell config, e.g. for bash/zsh:" + warn " export PATH=\"$INSTALL_DIR:\$PATH\"" + ;; + esac +} + +main() { + check_erlang + resolve_version + download + install_bin + verify + path_hint + info "installed ${BIN_NAME} ${VERSION} at ${INSTALLED}" +} + +main "$@" From 1c451295714b027670b69dd12808145298e519de Mon Sep 17 00:00:00 2001 From: Naohiro CHIKAMATSU Date: Fri, 17 Apr 2026 23:38:27 +0900 Subject: [PATCH 2/2] fix(install.sh): address CodeRabbit review feedback - Add EXIT/INT/HUP/TERM trap so the mktemp'd download is always removed if the script dies between download() and install_bin() (Ctrl-C, failing mkdir, etc). - Fall back to parsing the Location header from /releases/latest when the JSON API call returns empty. The API is rate limited to 60 req/h for unauthenticated clients; shared networks and CI runners hit that limit. The HEAD-redirect path is not subject to the same limit, and the die message now mentions both the rate-limit possibility and pinning via SQLODE_VERSION. - Detect non-writable INSTALL_DIR up front in install_bin() and emit a sudo example instead of letting `mv` fail with a raw permission error. Clear DOWNLOAD after a successful move so the cleanup trap does not try to rm the now-installed binary. README changes: - Add a two-step "inspect then run" install snippet alongside the curl | sh one-liner for security-conscious users. - Replace the bare `SQLODE_INSTALL_DIR=/usr/local/bin` example with a `sudo SQLODE_INSTALL_DIR=/usr/local/bin sh` form, and call out that system paths need elevated privileges. - Move environment variable docs into a bullet list so the sudo note is visible alongside the path example. Deferred to follow-up: - Checksum/signature verification needs release.yml to also publish sqlode.sha256 alongside the escript; too large for this PR. - sed `tag_name` extraction is fragile if GitHub minifies the API response, but grep -P is not POSIX and the current API still pretty-prints, so the regex stays. --- README.md | 14 ++++++++++++- scripts/install.sh | 50 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 73bc3f5..a44dbba 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,19 @@ sqlode ships as an Erlang escript. Every install path therefore needs an Erlang/ curl -fsSL https://raw.githubusercontent.com/nao1215/sqlode/main/scripts/install.sh | sh ``` -The script downloads the latest release's escript into `$HOME/.local/bin/sqlode`, makes it executable, and warns if Erlang/OTP is missing (with a per-distro install hint). Override the target with `SQLODE_INSTALL_DIR=/usr/local/bin` or pin a version with `SQLODE_VERSION=v0.1.0`. +Prefer to inspect the script before executing it? Download it first, read it, then run it: + +```console +curl -fsSL -o install.sh https://raw.githubusercontent.com/nao1215/sqlode/main/scripts/install.sh +sh install.sh +``` + +The installer writes the latest release's escript to `$HOME/.local/bin/sqlode`, makes it executable, and warns if Erlang/OTP is missing (with a per-distro install hint). + +Environment variables: + +- `SQLODE_VERSION=v0.1.0` — pin a specific release tag instead of `latest`. +- `SQLODE_INSTALL_DIR=/path/to/bin` — install into a different directory. System paths such as `/usr/local/bin` require elevated privileges, e.g. `curl -fsSL ... | sudo SQLODE_INSTALL_DIR=/usr/local/bin sh`. If `$HOME/.local/bin` is not on your `PATH`, add it to your shell config: diff --git a/scripts/install.sh b/scripts/install.sh index 26eb81f..ff7a5fe 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -16,10 +16,20 @@ BIN_NAME="sqlode" VERSION="${SQLODE_VERSION:-latest}" INSTALL_DIR="${SQLODE_INSTALL_DIR:-$HOME/.local/bin}" +DOWNLOAD="" + info() { printf '==> %s\n' "$*"; } warn() { printf 'warning: %s\n' "$*" >&2; } die() { printf 'error: %s\n' "$*" >&2; exit 1; } +# Clean up any half-downloaded temp file on early exit (ctrl-C, mkdir/mv +# failure, etc). install_bin clears DOWNLOAD after a successful move so the +# trap becomes a no-op. +cleanup() { + [ -n "$DOWNLOAD" ] && [ -e "$DOWNLOAD" ] && rm -f "$DOWNLOAD" +} +trap cleanup EXIT INT HUP TERM + need_cmd() { command -v "$1" >/dev/null 2>&1 || die "missing required command: $1" } @@ -64,14 +74,27 @@ check_erlang() { } resolve_version() { - if [ "$VERSION" = "latest" ]; then - url="https://api.github.com/repos/${REPO}/releases/latest" - tag="$(curl -fsSL "$url" | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n1)" - if [ -z "$tag" ]; then - die "could not determine latest release tag. Set SQLODE_VERSION=vX.Y.Z to pin a version." - fi - VERSION="$tag" + [ "$VERSION" = "latest" ] || return 0 + + # Prefer the JSON API because it gives a clean tag name, but it is rate + # limited to 60 req/h for unauthenticated clients. Fall back to parsing + # the Location header from /releases/latest, which is not subject to the + # same limit. + api_url="https://api.github.com/repos/${REPO}/releases/latest" + tag="$(curl -fsSL "$api_url" | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n1)" + + if [ -z "$tag" ]; then + redirect_url="https://github.com/${REPO}/releases/latest" + tag="$(curl -fsSI "$redirect_url" \ + | sed -n 's/^[Ll]ocation: .*\/releases\/tag\/\([^[:space:]]*\).*/\1/p' \ + | tr -d '\r' \ + | tail -n1)" fi + + if [ -z "$tag" ]; then + die "could not determine latest release tag (GitHub API rate limit or no releases yet). Set SQLODE_VERSION=vX.Y.Z to pin a version." + fi + VERSION="$tag" } download() { @@ -86,11 +109,22 @@ download() { } install_bin() { - mkdir -p "$INSTALL_DIR" + # Detect non-writable targets up front so the user sees an actionable + # hint instead of a raw `mv: Permission denied` from `set -e`. + if ! mkdir -p "$INSTALL_DIR" 2>/dev/null; then + die "cannot create $INSTALL_DIR. Re-run with sudo (e.g. 'sudo SQLODE_INSTALL_DIR=$INSTALL_DIR sh') or pick a user-writable path like \$HOME/.local/bin." + fi + if [ ! -w "$INSTALL_DIR" ]; then + die "$INSTALL_DIR is not writable. Re-run with sudo (e.g. 'sudo SQLODE_INSTALL_DIR=$INSTALL_DIR sh') or pick a user-writable path like \$HOME/.local/bin." + fi + dest="$INSTALL_DIR/$BIN_NAME" mv "$DOWNLOAD" "$dest" chmod +x "$dest" INSTALLED="$dest" + # Successful move: the temp file no longer exists, so clear the trap + # state to avoid a stray `rm -f` on EXIT. + DOWNLOAD="" } verify() {