diff --git a/scripts/devloop_test.sh b/scripts/devloop_test.sh index b6af36f..fc8b8d0 100755 --- a/scripts/devloop_test.sh +++ b/scripts/devloop_test.sh @@ -36,7 +36,7 @@ equals() { [[ "$actual" == "$expected" ]] || fail "$label expected [$expected], got [$actual]" } -bash -n "$REPO_ROOT/devloop" "$SCRIPTS_DIR/install.sh" "$SCRIPTS_DIR/uninstall.sh" "$SCRIPTS_DIR/skill_helpers.sh" "$SCRIPTS_DIR/release.sh" "$REMOTE_INSTALLER" +bash -n "$REPO_ROOT/devloop" "$SCRIPTS_DIR/install.sh" "$SCRIPTS_DIR/uninstall.sh" "$SCRIPTS_DIR/skill_helpers.sh" "$SCRIPTS_DIR/release.sh" "$REMOTE_INSTALLER" "$REPO_ROOT/site/public/install" ok "bash syntax" DEVLOOP_LIB=1 @@ -114,6 +114,77 @@ ok "skill metadata" work=$(mktemp -d "${TMPDIR:-/tmp}/devloop-test.XXXXXX") trap 'rm -rf "$work"' EXIT +equals "$(sed -n '1p' "$REPO_ROOT/site/public/VERSION")" "$version" "site VERSION matches root VERSION" +ok "site version file" + +bootstrap_bin="$work/install-bootstrap-bin" +bootstrap_log="$work/install-bootstrap.log" +mkdir -p "$bootstrap_bin" +cat > "$bootstrap_bin/curl" <<'CURL' +#!/usr/bin/env bash +set -euo pipefail +if [ "${1:-}" != "-fsSL" ]; then + printf 'unexpected curl args: %s\n' "$*" >&2 + exit 1 +fi +url="${2:-}" +printf '%s\n' "$url" >> "$DEVLOOP_BOOTSTRAP_LOG" +case "$url" in + https://version.example/devloop) + printf '%s\n' '9.8.7' + ;; + https://raw.example/devloop/v*/scripts/install.remote.sh) + cat <<'SCRIPT' +#!/usr/bin/env bash +printf 'installer args:' +for arg in "$@"; do printf ' <%s>' "$arg"; done +printf '\n' +SCRIPT + ;; + *) + printf 'unexpected url: %s\n' "$url" >&2 + exit 1 + ;; +esac +CURL +chmod +x "$bootstrap_bin/curl" +bootstrap_output="$( + DEVLOOP_BOOTSTRAP_LOG="$bootstrap_log" \ + DEVLOOP_VERSION_URL="https://version.example/devloop" \ + DEVLOOP_RAW_BASE_URL="https://raw.example/devloop" \ + PATH="$bootstrap_bin:/usr/bin:/bin:/usr/sbin:/sbin" \ + bash "$REPO_ROOT/site/public/install" --dry-run +)" +contains "$bootstrap_output" "installer args: <--version> <9.8.7> <--dry-run>" "site install bootstrap" +contains "$(cat "$bootstrap_log")" "https://version.example/devloop" "site install bootstrap version" +contains "$(cat "$bootstrap_log")" "https://raw.example/devloop/v9.8.7/scripts/install.remote.sh" "site install bootstrap installer" +: > "$bootstrap_log" +bootstrap_pinned_output="$( + DEVLOOP_BOOTSTRAP_LOG="$bootstrap_log" \ + DEVLOOP_VERSION_URL="https://version.example/devloop" \ + DEVLOOP_RAW_BASE_URL="https://raw.example/devloop" \ + PATH="$bootstrap_bin:/usr/bin:/bin:/usr/sbin:/sbin" \ + bash "$REPO_ROOT/site/public/install" --version=1.2.3 --dry-run +)" +contains "$bootstrap_pinned_output" "installer args: <--version> <1.2.3> <--dry-run>" "site install bootstrap pinned" +not_contains "$(cat "$bootstrap_log")" "https://version.example/devloop" "site install bootstrap pinned" +contains "$(cat "$bootstrap_log")" "https://raw.example/devloop/v1.2.3/scripts/install.remote.sh" "site install bootstrap pinned" +: > "$bootstrap_log" +if bootstrap_bad_version_output="$( + DEVLOOP_BOOTSTRAP_LOG="$bootstrap_log" \ + DEVLOOP_VERSION_URL="https://version.example/devloop" \ + DEVLOOP_RAW_BASE_URL="https://raw.example/devloop" \ + PATH="$bootstrap_bin:/usr/bin:/bin:/usr/sbin:/sbin" \ + bash "$REPO_ROOT/site/public/install" --version 2>&1 +)"; then + printf '%s\n' "$bootstrap_bad_version_output" >&2 + fail "site install bootstrap accepted bare --version" +fi +contains "$bootstrap_bad_version_output" "error: --version requires a value" "site install bootstrap bare version" +not_contains "$(cat "$bootstrap_log")" "https://version.example/devloop" "site install bootstrap bare version" +not_contains "$(cat "$bootstrap_log")" "scripts/install.remote.sh" "site install bootstrap bare version" +ok "site install bootstrap" + make_remote_release() { local version="$1" local releases="$2" @@ -545,9 +616,13 @@ ok "pure helpers" return 0 } ROOT="$work/release-root" - mkdir -p "$ROOT" + mkdir -p "$ROOT/site/public" git init -q "$ROOT" + release_write_version_files "9.9.8" + equals "$(sed -n '1p' "$ROOT/VERSION")" "9.9.8" "release writes root version" + equals "$(sed -n '1p' "$ROOT/site/public/VERSION")" "9.9.8" "release writes site version" printf '%s\n' "9.9.9" > "$ROOT/VERSION" + printf '%s\n' "9.9.9" > "$ROOT/site/public/VERSION" dry_run_output="$(release_main "patch" --dry-run)" || fail "release dry-run required git-cliff" contains "$dry_run_output" "next: 9.9.10 (v9.9.10)" "release dry-run" contains "$dry_run_output" "would tag: v9.9.10" "release dry-run" @@ -557,7 +632,7 @@ ok "pure helpers" contains "$publish_dry_run_output" "would create GitHub release: gh release create v9.9.10 --verify-tag --generate-notes" "release publish dry-run" git -C "$ROOT" config user.email devloop-test@example.com git -C "$ROOT" config user.name "devloop test" - git -C "$ROOT" add VERSION + git -C "$ROOT" add VERSION site/public/VERSION git -C "$ROOT" commit -q -m init release_assert_clean_tree || fail "release clean tree rejected" printf '%s\n' "dirty" > "$ROOT/dirty" diff --git a/scripts/release.sh b/scripts/release.sh index 7e9c1ab..5c3c069 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -43,6 +43,15 @@ release_current_version() { sed -n '1p' "$ROOT/VERSION" 2>/dev/null || true } +release_write_version_files() { + local version="$1" + local site_version="$ROOT/site/public/VERSION" + printf '%s\n' "$version" > "$ROOT/VERSION" + if [ -d "$(dirname "$site_version")" ]; then + printf '%s\n' "$version" > "$site_version" + fi +} + release_next_version() { local bump="$1" local current="$2" @@ -245,8 +254,9 @@ release_main() { if [ "$publish" = true ] || [ "$push" = true ]; then release_assert_push_branch; fi bash "$ROOT/scripts/devloop_test.sh" - printf '%s\n' "$version" > "$ROOT/VERSION" + release_write_version_files "$version" git -C "$ROOT" add VERSION + if [ -f "$ROOT/site/public/VERSION" ]; then git -C "$ROOT" add site/public/VERSION; fi git -C "$ROOT" commit -m "chore: release $version" git -C "$ROOT" tag -a "$tag" -m "devloop $version" # Regenerate the changelog after the tag exists and render from the tagged diff --git a/site/README.md b/site/README.md index 0d6ce36..690411c 100644 --- a/site/README.md +++ b/site/README.md @@ -56,11 +56,14 @@ so deployment is only complete once two things are true: 1. `devloop.sh` points at this Pages project (Pages project > Custom domains > add `devloop.sh`; Cloudflare provisions the TLS cert). -2. `https://devloop.sh/install` serves the install script as `text/plain`. Until - the real installer exists, either drop an `install` file into the deployed - output or add a redirect to the raw `install.sh` in the CLI repo. With Pages, - a `dist/_redirects` line like `/install https://raw.githubusercontent.com/satyaborg/devloop/main/install.sh 200` - proxies it. +2. `https://devloop.sh/install` serves `public/install`, a small bootstrap that + reads `public/VERSION` from `https://devloop.sh/VERSION` and executes the + installer from the matching Git tag. + +For releases, push the Git tag and confirm Pages has deployed the release +commit. A stale Pages deploy keeps new installs on the previous version, while a +`VERSION` file that points at an unpushed tag makes `/install` fail when it +fetches the tagged installer. ## Other static hosts @@ -75,6 +78,9 @@ site/ index.html source page (vite entry) src/input.css tailwind entry + theme tokens + @font-face vite.config.js vite + @tailwindcss/vite plugin + public/_headers Cloudflare Pages headers for extensionless files + public/install VERSION-based bootstrap served at /install + public/VERSION release version served at /VERSION public/fonts/ JetBrains Mono woff2 (400, 700), served at /fonts/ dist/ build output (gitignored) ``` diff --git a/site/public/VERSION b/site/public/VERSION new file mode 100644 index 0000000..267577d --- /dev/null +++ b/site/public/VERSION @@ -0,0 +1 @@ +0.4.1 diff --git a/site/public/_headers b/site/public/_headers new file mode 100644 index 0000000..9eb125f --- /dev/null +++ b/site/public/_headers @@ -0,0 +1,2 @@ +/install + Content-Type: text/plain; charset=utf-8 diff --git a/site/public/_redirects b/site/public/_redirects deleted file mode 100644 index eb2ad6f..0000000 --- a/site/public/_redirects +++ /dev/null @@ -1 +0,0 @@ -/install https://raw.githubusercontent.com/satyaborg/devloop/main/scripts/install.remote.sh 302 diff --git a/site/public/install b/site/public/install new file mode 100644 index 0000000..1cfcc0a --- /dev/null +++ b/site/public/install @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +set -euo pipefail + +GITHUB_REPO="${DEVLOOP_GITHUB_REPO:-satyaborg/devloop}" +VERSION_URL="${DEVLOOP_VERSION_URL:-https://devloop.sh/VERSION}" +RAW_BASE_URL="${DEVLOOP_RAW_BASE_URL:-https://raw.githubusercontent.com/$GITHUB_REPO}" + +fail() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +normalize_version() { + local version="$1" + version="${version#v}" + version="$(printf '%s\n' "$version" | sed -n '1p' | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')" + if ! printf '%s\n' "$version" | grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$'; then + fail "invalid version: $1" + fi + printf '%s\n' "$version" +} + +require_curl() { + if ! command -v curl >/dev/null 2>&1; then + fail "missing curl; install curl or download the release installer manually" + fi +} + +requested_version() { + while [ "$#" -gt 0 ]; do + case "$1" in + --version) + [ "$#" -ge 2 ] || fail "--version requires a value" + printf '%s\n' "$2" + return 0 + ;; + --version=*) + printf '%s\n' "${1#--version=}" + return 0 + ;; + esac + shift + done + return 1 +} + +validate_requested_version_args() { + while [ "$#" -gt 0 ]; do + case "$1" in + --version) + [ "$#" -ge 2 ] || fail "--version requires a value" + shift + ;; + esac + shift + done +} + +site_version() { + local version + version="$(curl -fsSL "$VERSION_URL")" || fail "failed to resolve Devloop version" + normalize_version "$version" +} + +main() { + local version installer_url + local args=() + require_curl + validate_requested_version_args "$@" + if version="$(requested_version "$@")"; then + version="$(normalize_version "$version")" + else + version="$(site_version)" + fi + + args=(--version "$version") + while [ "$#" -gt 0 ]; do + case "$1" in + --version) + shift + if [ "$#" -gt 0 ]; then shift; fi + ;; + --version=*) shift ;; + *) + args+=("$1") + shift + ;; + esac + done + + installer_url="$RAW_BASE_URL/v$version/scripts/install.remote.sh" + curl -fsSL "$installer_url" | bash -s -- "${args[@]}" +} + +main "$@"