From fe5716720cec627079df5e3a0ad8d2307d455852 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 31 Jul 2026 11:32:32 +0000 Subject: [PATCH 1/4] Migrate from Leiningen to deps.edn and tools.build. Replace project.clj with Clojure CLI aliases for run, migrate, test, CSS, and uberjar builds, and update CI, FreeBSD scripts, and docs to match. Co-authored-by: Chad Stovern --- .env.example | 3 ++ .github/workflows/ci.yml | 26 ++++++----- .gitignore | 2 - DEPLOYMENT_FREEBSD.md | 15 +++--- README.org | 52 ++++++++++++--------- build.clj | 38 +++++++++++++++ deps.edn | 78 +++++++++++++++++++++++++++++++ dev.env.example | 13 ++++++ dev/css.clj | 44 ++++++++++++++++++ dev/dev.clj | 15 ++++++ profiles_example.clj | 20 -------- project.clj | 95 -------------------------------------- run-tests-local.sh | 18 ++++++-- scripts/build-prod.sh | 15 +++--- scripts/create-env.sh | 6 +-- scripts/with-env.sh | 34 ++++++++++++++ src/self_destruct/core.clj | 2 +- 17 files changed, 303 insertions(+), 173 deletions(-) create mode 100644 build.clj create mode 100644 deps.edn create mode 100644 dev.env.example create mode 100644 dev/css.clj create mode 100644 dev/dev.clj delete mode 100644 profiles_example.clj delete mode 100644 project.clj create mode 100755 scripts/with-env.sh diff --git a/.env.example b/.env.example index c4c6a99..b729271 100644 --- a/.env.example +++ b/.env.example @@ -25,3 +25,6 @@ WORKER_DELAY_SECONDS=3600 REPORTED_LOG_LEVEL=warn LOG_APPENDER=println # SENTRY_DSN= only needed when LOG_APPENDER=sentry + +# Ring secure-site-defaults (recommended behind HTTPS/nginx) +SECURE_DEFAULTS=true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db5abbd..db49b9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,6 @@ jobs: --health-retries 5 env: - LEIN_ROOT: "true" JVM_OPTS: -Xmx2g DATABASE_URL: jdbc:postgresql://localhost:5432/self-destruct-test?user=selfdestruct&password=selfdestruct DATABASE_ENCRYPTION_KEY: changemedbkey2 @@ -47,25 +46,30 @@ jobs: distribution: temurin java-version: "21" - - name: Set up Clojure / Leiningen + - name: Set up Clojure CLI uses: DeLaGuardo/setup-clojure@13.6.1 with: - # 2.13.0 currently fails to install via setup-clojure; stay on 2.12.x - lein: 2.12.0 + cli: 1.12.5.1664 - - name: Cache Maven dependencies + - name: Cache Maven / Clojure deps uses: actions/cache@v4 with: - path: ~/.m2/repository - key: ${{ runner.os }}-m2-${{ hashFiles('project.clj') }} + path: | + ~/.m2/repository + ~/.gitlibs + .cpcache + key: ${{ runner.os }}-clj-${{ hashFiles('deps.edn') }} restore-keys: | - ${{ runner.os }}-m2- + ${{ runner.os }}-clj- - name: Install dependencies - run: lein deps + run: clojure -P -M:test:dev:build - name: Run database migrations - run: lein migrate + run: clojure -M:migrate - name: Run tests - run: lein test + run: clojure -M:test + + - name: Build uberjar + run: clojure -T:build uber diff --git a/.gitignore b/.gitignore index 7e99d42..d2b8683 100644 --- a/.gitignore +++ b/.gitignore @@ -4,11 +4,9 @@ /checkouts/ /resources/public/css/ .cpcache/ -.lein-* .nrepl-port *.jar *.class pom.xml pom.xml.asc -profiles.clj .env diff --git a/DEPLOYMENT_FREEBSD.md b/DEPLOYMENT_FREEBSD.md index 234dc00..6a84e7c 100644 --- a/DEPLOYMENT_FREEBSD.md +++ b/DEPLOYMENT_FREEBSD.md @@ -1,7 +1,7 @@ # self-destruct FreeBSD Deployment Deploy self-destruct on a FreeBSD home server using the same pattern as -DoThisWeek: dedicated app user, PostgreSQL, `.env`, Leiningen uberjar, `rc.d` +DoThisWeek: dedicated app user, PostgreSQL, `.env`, Clojure CLI uberjar, `rc.d` service, nginx reverse proxy, certbot, and daily DB backups. ## Assumptions @@ -18,17 +18,14 @@ service, nginx reverse proxy, certbot, and daily DB backups. ## 1. Install Java Runtime and Build Tools ```sh -sudo pkg install openjdk21 git +sudo pkg install openjdk21 git clojure ``` -Install Leiningen if it is not already on the system (e.g. `sudo pkg install -leiningen`, or place a `lein` script on `PATH`). - Verify: ```sh /usr/local/openjdk21/bin/java -version -lein version +clj -Sdescribe ``` The deployment scripts force `JAVA_HOME=/usr/local/openjdk21` and put @@ -109,8 +106,8 @@ sudo -u selfdestruct sh -c 'cd /home/selfdestruct/self-destruct && ./scripts/bui This: -- Fetches dependencies with `lein deps` -- Builds `target/self-destruct.jar` with `lein uberjar` +- Fetches dependencies with `clojure -P` +- Builds `target/self-destruct.jar` with `clojure -T:build uber` - Runs migrations with `java -jar target/self-destruct.jar --migrate` Optional local server test from the FreeBSD host: @@ -296,5 +293,5 @@ curl -s https://your.domain.example/health | Backup log | /var/log/selfdestruct-backup.log | | Backup time | 3:45 AM daily | | Backup retention | 30 days | -| Build | `lein uberjar` | +| Build | `clojure -T:build uber` | | Migrate | `java -jar target/self-destruct.jar --migrate` | diff --git a/README.org b/README.org index 44adaea..a8a21e7 100644 --- a/README.org +++ b/README.org @@ -5,7 +5,7 @@ ** Prerequisites - [[https://adoptium.net/][Java]] 17 or newer (21 recommended; required by Ring / Jetty 12) - - [[https://github.com/technomancy/leiningen][Leiningen]] 2.11.0 or newer (2.12.x recommended) + - [[https://clojure.org/guides/install_clojure][Clojure CLI]] (~clj~ / ~clojure~) - PostgreSQL available over TCP with a role/password your JDBC URL can use @@ -13,13 +13,14 @@ *** Set Environment Variables & Secrets - Copy the example file, then edit values to match your machine: + Copy the local example, then edit values to match your machine: #+BEGIN_SRC sh - cp profiles_example.clj profiles.clj + cp dev.env.example .env #+END_SRC - ~profiles.clj~ is gitignored. The example includes local defaults for: + ~.env~ is gitignored. Use ~./scripts/with-env.sh ...~ to load it before + ~clj~ commands, or export the variables in your shell. The example includes: - ~DATABASE_URL~ — JDBC URL with user/password (not Unix peer auth) - ~DATABASE_ENCRYPTION_KEY~ — key used to encrypt messages at rest @@ -28,26 +29,25 @@ *** Setup Local Development Database - Create a Postgres role and database that match ~profiles.clj~ (example uses - user/password ~selfdestruct~ / ~selfdestruct~): + Create a Postgres role and database that match ~dev.env.example~ (user/password + ~selfdestruct~ / ~selfdestruct~): #+BEGIN_SRC sh createuser selfdestruct psql -c "ALTER USER selfdestruct WITH PASSWORD 'selfdestruct';" createdb -O selfdestruct self-destruct-dev - lein migrate + ./scripts/with-env.sh clj -M:migrate #+END_SRC - If you use a different role, update ~:database-url~ in ~profiles.clj~ (and - export ~DATABASE_URL~ if you override it in the shell). JDBC connections use - TCP auth; peer auth via Unix sockets is not used. + If you use a different role, update ~DATABASE_URL~ in ~.env~. JDBC connections + use TCP auth; peer auth via Unix sockets is not used. *** Run the app After migrations have been applied: #+BEGIN_SRC sh - lein dev + ./scripts/with-env.sh clj -M:dev-main #+END_SRC That starts Garden CSS auto-compile and the Ring server (default port 8000). @@ -55,38 +55,48 @@ Equivalent pieces if you prefer to run them separately: #+BEGIN_SRC sh - lein garden auto - lein ring server + ./scripts/with-env.sh clj -M:css watch + ./scripts/with-env.sh clj -M:run -- --port 8000 #+END_SRC + ~:run~ uses the production entrypoint (~-main~). ~:dev-main~ uses + ~-dev-main~ with Ring reload. + *** Running Tests #+BEGIN_SRC sh ./run-tests-local.sh #+END_SRC - This recreates ~self-destruct-test~, runs migrations, then ~lein test~. - Ensure the JDBC role in ~profiles.clj~ / ~DATABASE_URL~ can create that database - (or create it yourself and point ~DATABASE_URL~ at it). + This recreates ~self-destruct-test~, runs migrations, then ~clj -M:test~. + Ensure your Postgres role can create that database (or create it yourself and + point ~DATABASE_URL~ at it). ** Continuous Integration Pull requests and pushes to ~master~ run on GitHub Actions (~.github/workflows/ci.yml~): - Postgres service, ~lein migrate~, then ~lein test~. + Postgres service, ~clj -M:migrate~, ~clj -M:test~, and an uberjar build. ** Database Migrations - Migratus handles schema changes; files live in ~resources/migrations~. - - Create a migration with ~lein migratus create ~, then edit the ~up~ / ~down~ SQL. - - Apply migrations with ~lein migrate~ (runs the app ~--migrate~ CLI) or + - Create a new pair of ~*.up.sql~ / ~*.down.sql~ files under ~resources/migrations~ + (timestamp prefix, e.g. ~20260731120000-add-thing.up.sql~). + - Apply migrations with ~./scripts/with-env.sh clj -M:migrate~ or ~java -jar target/self-destruct.jar --migrate~ in production. -** Production Setup +** Production Build + + #+BEGIN_SRC sh + clj -T:build uber + #+END_SRC + + Writes ~target/self-destruct.jar~ (compiles Garden CSS first, then AOT + uberjar). - Primary target is a FreeBSD home server. See [[file:DEPLOYMENT_FREEBSD.md][DEPLOYMENT_FREEBSD.md]] + Primary deploy target is a FreeBSD home server. See [[file:DEPLOYMENT_FREEBSD.md][DEPLOYMENT_FREEBSD.md]] for install, ~rc.d~, nginx, certbot, and backup scripts under ~scripts/~. Required environment variables (also listed in [[file:.env.example][.env.example]]): diff --git a/build.clj b/build.clj new file mode 100644 index 0000000..a82757d --- /dev/null +++ b/build.clj @@ -0,0 +1,38 @@ +(ns build + (:require [clojure.java.io :as io] + [clojure.tools.build.api :as b] + [garden.core :as garden])) + + +(def class-dir "target/classes") +(def basis (delay (b/create-basis {:project "deps.edn"}))) +(def uber-file "target/self-destruct.jar") +(def css-output "resources/public/css/style.css") + + +(defn clean [_] + (b/delete {:path "target"}) + (b/delete {:path "resources/public/css"})) + + +(defn compile-css [_] + (require 'self-destruct.css) + (let [style @(resolve 'self-destruct.css/style)] + (io/make-parents css-output) + (spit css-output (apply garden/css {:pretty-print? true} style)) + (println "Wrote" css-output))) + + +(defn uber [_] + (clean nil) + (compile-css nil) + (b/copy-dir {:src-dirs ["src" "resources"] + :target-dir class-dir}) + (b/compile-clj {:basis @basis + :ns-compile '[self-destruct.core] + :class-dir class-dir}) + (b/uber {:class-dir class-dir + :uber-file uber-file + :basis @basis + :main 'self-destruct.core}) + (println "Uberjar written to" uber-file)) diff --git a/deps.edn b/deps.edn new file mode 100644 index 0000000..23948d6 --- /dev/null +++ b/deps.edn @@ -0,0 +1,78 @@ +{:paths ["src" "resources"] + + :deps {org.clojure/clojure {:mvn/version "1.12.5"} + + ;; core + ring/ring {:mvn/version "1.15.5"} + compojure/compojure {:mvn/version "1.7.2"} + + ;; environment + environ/environ {:mvn/version "1.2.0"} + org.clojure/tools.cli {:mvn/version "1.1.230"} + + ;; database + com.layerware/hugsql {:mvn/version "0.5.3"} + org.clojure/java.jdbc {:mvn/version "0.7.12"} + org.postgresql/postgresql {:mvn/version "42.7.7"} + migratus/migratus {:mvn/version "1.6.7"} + com.github.seancorfield/next.jdbc {:mvn/version "1.3.1048"} + + ;; logging + com.taoensso/timbre {:mvn/version "6.8.0"} + raven-clj/raven-clj {:mvn/version "1.7.0"} + + ;; security + buddy/buddy-core {:mvn/version "1.12.0-430"} + + ;; ui + hiccup/hiccup {:mvn/version "2.0.0"} + garden/garden {:mvn/version "1.3.10"} + + ;; middleware + ring/ring-defaults {:mvn/version "0.7.1"} + + ;; data + cheshire/cheshire {:mvn/version "5.13.0"} + + ;; scheduling + tea-time/tea-time {:mvn/version "1.0.1"} + + ;; hosted assets + ring-webjars/ring-webjars {:mvn/version "0.3.1"} + org.webjars/font-awesome {:mvn/version "6.7.2"}} + + :aliases + { :dev + {:extra-paths ["dev"] + :extra-deps {ring/ring-mock {:mvn/version "0.6.2"} + org.clojure/tools.namespace {:mvn/version "1.5.0"}}} + + :run + {:main-opts ["-m" "self-destruct.core"]} + + :dev-main + {:extra-paths ["dev"] + :extra-deps {ring/ring-mock {:mvn/version "0.6.2"}} + :main-opts ["-m" "dev"]} + + :migrate + {:main-opts ["-m" "self-destruct.core" "--migrate"]} + + :css + {:extra-paths ["dev"] + :main-opts ["-m" "css"]} + + :test + {:extra-paths ["test"] + :extra-deps {ring/ring-mock {:mvn/version "0.6.2"} + io.github.cognitect-labs/test-runner + {:git/tag "v0.5.1" :git/sha "dfb30dd"}} + :main-opts ["-m" "cognitect.test-runner"] + :exec-fn cognitect.test-runner.api/test} + + :build + {:deps {io.github.clojure/tools.build {:mvn/version "0.10.10"} + garden/garden {:mvn/version "1.3.10"}} + ;; "." hosts build.clj; "src" is needed so compile-css can require Garden styles + :paths ["." "src"] + :ns-default build}}} diff --git a/dev.env.example b/dev.env.example new file mode 100644 index 0000000..5d56494 --- /dev/null +++ b/dev.env.example @@ -0,0 +1,13 @@ +# Local development defaults (copy to .env) +# cp dev.env.example .env + +DATABASE_URL=jdbc:postgresql://localhost:5432/self-destruct-dev?user=selfdestruct&password=selfdestruct +DATABASE_ENCRYPTION_KEY=changemedbkey1 +SESSION_COOKIE_KEY=changecookiekey1 + +PORT=8000 +ENABLE_WORKERS=false +MESSAGE_EXPIRE_MINUTES=1440 +WORKER_DELAY_SECONDS=3600 +REPORTED_LOG_LEVEL=debug +LOG_APPENDER=println diff --git a/dev/css.clj b/dev/css.clj new file mode 100644 index 0000000..780fadd --- /dev/null +++ b/dev/css.clj @@ -0,0 +1,44 @@ +(ns css + "Compile Garden stylesheets to resources/public/css/style.css." + (:require [clojure.java.io :as io] + [garden.core :as garden])) + + +(def output-path "resources/public/css/style.css") + + +(defn compile-once! + [] + (require 'self-destruct.css) + (let [style @(resolve 'self-destruct.css/style)] + (io/make-parents output-path) + (spit output-path (apply garden/css {:pretty-print? true} style)) + (println "Wrote" output-path))) + + +(defn watch! + "Recompile CSS when the stylesheet source changes." + [] + (println "Watching Garden CSS (Ctrl-C to stop)...") + (loop [last-mtime 0] + (let [source (io/file "src/self_destruct/css.clj") + mtime (.lastModified source)] + (when (> mtime last-mtime) + (try + (require 'self-destruct.css :reload) + (compile-once!) + (catch Exception e + (println "CSS compile failed:" (.getMessage e))))) + (Thread/sleep 1000) + (recur mtime)))) + + +(defn -main + [& args] + (case (first args) + "once" (compile-once!) + "watch" (watch!) + nil (compile-once!) + (do + (println "Usage: clj -M:css [once|watch]") + (System/exit 1)))) diff --git a/dev/dev.clj b/dev/dev.clj new file mode 100644 index 0000000..9ddf13f --- /dev/null +++ b/dev/dev.clj @@ -0,0 +1,15 @@ +(ns dev + "Development entrypoint: optional CSS watch + reloadable Ring server." + (:require [css] + [self-destruct.core :as core])) + + +(defn -main + [& args] + (future + (try + (css/watch!) + (catch Exception e + (binding [*out* *err*] + (println "CSS watch stopped:" (.getMessage e)))))) + (apply core/-dev-main args)) diff --git a/profiles_example.clj b/profiles_example.clj deleted file mode 100644 index 138845f..0000000 --- a/profiles_example.clj +++ /dev/null @@ -1,20 +0,0 @@ -{:profiles/dev - {:env - {:database-url "jdbc:postgresql://localhost:5432/self-destruct-dev?user=selfdestruct&password=selfdestruct" - :database-encryption-key "changemedbkey1" - :reported-log-level "debug" - :session-cookie-key "changecookiekey1" - :enable-workers "false" - :message-expire-minutes "1440" - :worker-delay-seconds "3600"}} - :profiles/test - {:env - {:database-url "jdbc:postgresql://localhost:5432/self-destruct-test?user=selfdestruct&password=selfdestruct" - :database-encryption-key "changemedbkey2" - :reported-log-level "debug" - :session-cookie-key "changecookiekey2" - :disable-anti-forgery "true" - :enable-workers "false"}} - :profiles/prod - ;; intentionally empty to ensure builds do not rely on run time values - {:env {}}} diff --git a/project.clj b/project.clj deleted file mode 100644 index 0ec4ab6..0000000 --- a/project.clj +++ /dev/null @@ -1,95 +0,0 @@ -(defproject self-destruct "0.1.0-SNAPSHOT" - :description "run your own self destucting note service" - :url "http://github.com/chadhs/self-destruct" - :min-lein-version "2.11.0" - :dependencies [[org.clojure/clojure "1.12.5"] - ;;; core - [ring/ring "1.15.5"] - [compojure "1.7.2"] - ;;; environment - [environ "1.2.0"] - [org.clojure/tools.cli "1.1.230"] - ;;; database - [com.layerware/hugsql "0.5.3"] - [org.clojure/java.jdbc "0.7.12"] - [org.postgresql/postgresql "42.7.7"] - [migratus "1.6.7"] - ;;; logging - [com.taoensso/timbre "6.8.0"] - [raven-clj "1.7.0"] ; timbre sentry support - ;;; security - [buddy/buddy-core "1.12.0-430"] - ;;; ui - [hiccup/hiccup "2.0.0"] - [garden "1.3.10"] - ;;; middleware - [ring/ring-defaults "0.7.1"] - ;;; data - [cheshire "5.13.0"] - ;;; scheduling - [tea-time "1.0.1"] - ;;; hosted assests - [ring-webjars "0.3.1"] - [org.webjars/font-awesome "6.7.2"]] - - - :plugins [[lein-environ "1.2.0"] - [lein-ring "0.12.6"] - [migratus-lein "0.7.3"] - [lein-garden "0.3.0"] - [lein-pdo "0.1.1"]] - - - :ring {:init self-destruct.core/init - :handler self-destruct.core/app - :port 8000 - :auto-refresh? true} - - - :garden {:builds [{:source-paths ["src"] - :id "style" - :stylesheet self-destruct.css/style - :compiler {:output-to "resources/public/css/style.css" - :pretty-print? true}}]} - - - :clean-targets ^{:protect false} ["resources/public/css"] - - - :migratus {:store :database - :migration-dir "migrations" - :db {:jdbcUrl ~(or (System/getenv "DATABASE_URL") - "jdbc:postgresql://localhost:5432/self-destruct-dev?user=selfdestruct&password=selfdestruct")}} - - - :profiles {:uberjar {:aot :all - :env {:secure-defaults "true"}} - :dev [:project/dev :profiles/dev] - :test [:project/test :profiles/test] - :prod [:project/prod :profiles/prod ] - ;; only edit :profiles/* in profiles.clj - :profiles/dev {} - :profiles/test {} - :profiles/prod {} - :project/dev {:main self-destruct.core/-dev-main - :dependencies [[ring/ring-mock "0.6.2"]]} - :project/test {:dependencies [[ring/ring-mock "0.6.2"]]} - :project/prod {}} - - - :main self-destruct.core - - - :uberjar-name "self-destruct.jar" - - - :prep-tasks ["clean" ["garden" "once"] "compile"] - - - ;; Use the app CLI for migrations so we always run migratus 1.6+ / next.jdbc - ;; from project deps (migratus-lein still pins an older migratus). - :aliases {"migrate" ["run" "--" "--migrate"] - "dev" ["pdo" ["garden" "auto"] ["ring" "server-headless"]]} - - - ) diff --git a/run-tests-local.sh b/run-tests-local.sh index ea01191..781e606 100755 --- a/run-tests-local.sh +++ b/run-tests-local.sh @@ -1,12 +1,20 @@ #!/bin/sh set -e +ROOT="$(CDPATH= cd -- "$(dirname "$0")" && pwd)" +cd "$ROOT" + export DATABASE_URL="${DATABASE_URL:-jdbc:postgresql://localhost:5432/self-destruct-test?user=selfdestruct&password=selfdestruct}" +export DATABASE_ENCRYPTION_KEY="${DATABASE_ENCRYPTION_KEY:-changemedbkey2}" +export SESSION_COOKIE_KEY="${SESSION_COOKIE_KEY:-changecookiekey2}" +export DISABLE_ANTI_FORGERY="${DISABLE_ANTI_FORGERY:-true}" +export ENABLE_WORKERS="${ENABLE_WORKERS:-false}" +export PGPASSWORD="${PGPASSWORD:-selfdestruct}" -if psql -lqt | cut -d \| -f 1 | grep -qw self-destruct-test; then - dropdb self-destruct-test +if psql -h localhost -U selfdestruct -lqt | cut -d \| -f 1 | grep -qw self-destruct-test; then + dropdb -h localhost -U selfdestruct self-destruct-test fi -createdb self-destruct-test -lein migrate -lein with-profile test test +createdb -h localhost -U selfdestruct self-destruct-test +clj -M:migrate +clj -M:test diff --git a/scripts/build-prod.sh b/scripts/build-prod.sh index ab488fd..e1c9479 100755 --- a/scripts/build-prod.sh +++ b/scripts/build-prod.sh @@ -19,7 +19,7 @@ if ! "${JAVA_HOME}/bin/java" -version 2>&1 | grep -q 'version "21'; then exit 1 fi -if [ ! -f "project.clj" ]; then +if [ ! -f "deps.edn" ]; then echo "Error: Please run this script from the self-destruct project root directory" exit 1 fi @@ -29,12 +29,15 @@ if [ ! -f ".env" ]; then exit 1 fi -if ! command -v lein >/dev/null 2>&1; then - echo "Error: lein not found on PATH" - echo "Install Leiningen (e.g. sudo pkg install leiningen) or place lein in PATH" +if ! command -v clojure >/dev/null 2>&1 && ! command -v clj >/dev/null 2>&1; then + echo "Error: clojure/clj not found on PATH" + echo "Install the Clojure CLI (e.g. sudo pkg install clojure)" exit 1 fi +CLJ_CMD="clojure" +command -v clojure >/dev/null 2>&1 || CLJ_CMD="clj" + echo "Loading environment variables..." while IFS= read -r line || [ -n "$line" ]; do case "$line" in @@ -56,14 +59,14 @@ while IFS= read -r line || [ -n "$line" ]; do done < .env echo "Fetching Clojure dependencies..." -lein deps +"$CLJ_CMD" -P -M:build if [ $? -ne 0 ]; then echo "Error: Failed to fetch dependencies" exit 1 fi echo "Building uberjar..." -lein uberjar +"$CLJ_CMD" -T:build uber if [ $? -ne 0 ]; then echo "Error: Failed to build uberjar" exit 1 diff --git a/scripts/create-env.sh b/scripts/create-env.sh index dcfd38d..dc49386 100755 --- a/scripts/create-env.sh +++ b/scripts/create-env.sh @@ -3,7 +3,7 @@ echo "Creating production environment file..." # Check if we're in the right directory -if [ ! -f "project.clj" ]; then +if [ ! -f "deps.edn" ]; then echo "Error: Please run this script from the self-destruct project root directory" exit 1 fi @@ -60,8 +60,8 @@ WORKER_DELAY_SECONDS=3600 REPORTED_LOG_LEVEL=warn LOG_APPENDER=println -# Optional: Ring secure-site-defaults when terminating TLS at nginx -# SECURE_DEFAULTS=true +# Ring secure-site-defaults (TLS terminated at nginx) +SECURE_DEFAULTS=true # Optional reference (not read by the app) APP_BASE_URL=https://${DOMAIN} diff --git a/scripts/with-env.sh b/scripts/with-env.sh new file mode 100755 index 0000000..5e08ce9 --- /dev/null +++ b/scripts/with-env.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# Load .env from the project root, then exec the remaining command. +# Usage: ./scripts/with-env.sh clj -M:migrate + +set -e + +ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +cd "$ROOT" + +if [ ! -f .env ]; then + echo "Error: .env not found in $ROOT" + echo "Copy dev.env.example or .env.example to .env and edit values." + exit 1 +fi + +while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + ''|'#'*) continue ;; + *'='*) + key="${line%%=*}" + value="${line#*=}" + if echo "$key" | grep -q '^[A-Za-z_][A-Za-z0-9_]*$'; then + export "$key=$value" + fi + ;; + esac +done < .env + +if [ "$#" -eq 0 ]; then + echo "Usage: $0 [args...]" + exit 1 +fi + +exec "$@" diff --git a/src/self_destruct/core.clj b/src/self_destruct/core.clj index d523542..814bc1c 100644 --- a/src/self_destruct/core.clj +++ b/src/self_destruct/core.clj @@ -116,7 +116,7 @@ (let [parsed (parse-opts args cli-options) port (get-in parsed [:options :port])] (config/configure-logging) - ;; honor --help/--migrate in dev as well (lein run defaults to -dev-main) + ;; honor --help/--migrate in dev as well (clj -M:dev-main) (if-let [exit-code (handle-cli-options parsed)] (System/exit exit-code) (do From 66e10102602be0893aebafab48b6fedf60a9e8e3 Mon Sep 17 00:00:00 2001 From: Chad Stovern Date: Fri, 31 Jul 2026 19:54:02 -0400 Subject: [PATCH 2/4] Load .env in-process so local clj commands need no shell wrapper. Prefer existing process environment variables so FreeBSD/CI exports still win, and drop scripts/with-env.sh. Co-authored-by: Cursor --- README.org | 15 ++++---- dev.env.example | 4 ++- scripts/with-env.sh | 34 ------------------ src/self_destruct/config.clj | 4 ++- src/self_destruct/env.clj | 70 ++++++++++++++++++++++++++++++++++++ 5 files changed, 84 insertions(+), 43 deletions(-) delete mode 100755 scripts/with-env.sh create mode 100644 src/self_destruct/env.clj diff --git a/README.org b/README.org index a8a21e7..e8d5971 100644 --- a/README.org +++ b/README.org @@ -19,8 +19,9 @@ cp dev.env.example .env #+END_SRC - ~.env~ is gitignored. Use ~./scripts/with-env.sh ...~ to load it before - ~clj~ commands, or export the variables in your shell. The example includes: + ~.env~ is gitignored. On startup the app loads it automatically for any + variables not already set in the process environment (so exported env vars + and CI/production still win). The example includes: - ~DATABASE_URL~ — JDBC URL with user/password (not Unix peer auth) - ~DATABASE_ENCRYPTION_KEY~ — key used to encrypt messages at rest @@ -36,7 +37,7 @@ createuser selfdestruct psql -c "ALTER USER selfdestruct WITH PASSWORD 'selfdestruct';" createdb -O selfdestruct self-destruct-dev - ./scripts/with-env.sh clj -M:migrate + clj -M:migrate #+END_SRC If you use a different role, update ~DATABASE_URL~ in ~.env~. JDBC connections @@ -47,7 +48,7 @@ After migrations have been applied: #+BEGIN_SRC sh - ./scripts/with-env.sh clj -M:dev-main + clj -M:dev-main #+END_SRC That starts Garden CSS auto-compile and the Ring server (default port 8000). @@ -55,8 +56,8 @@ Equivalent pieces if you prefer to run them separately: #+BEGIN_SRC sh - ./scripts/with-env.sh clj -M:css watch - ./scripts/with-env.sh clj -M:run -- --port 8000 + clj -M:css watch + clj -M:run -- --port 8000 #+END_SRC ~:run~ uses the production entrypoint (~-main~). ~:dev-main~ uses @@ -84,7 +85,7 @@ - Migratus handles schema changes; files live in ~resources/migrations~. - Create a new pair of ~*.up.sql~ / ~*.down.sql~ files under ~resources/migrations~ (timestamp prefix, e.g. ~20260731120000-add-thing.up.sql~). - - Apply migrations with ~./scripts/with-env.sh clj -M:migrate~ or + - Apply migrations with ~clj -M:migrate~ or ~java -jar target/self-destruct.jar --migrate~ in production. diff --git a/dev.env.example b/dev.env.example index 5d56494..394cb3b 100644 --- a/dev.env.example +++ b/dev.env.example @@ -1,6 +1,8 @@ # Local development defaults (copy to .env) # cp dev.env.example .env - +# +# The app loads .env automatically for unset variables. Process env / CI / +# production exports always take precedence. DATABASE_URL=jdbc:postgresql://localhost:5432/self-destruct-dev?user=selfdestruct&password=selfdestruct DATABASE_ENCRYPTION_KEY=changemedbkey1 SESSION_COOKIE_KEY=changecookiekey1 diff --git a/scripts/with-env.sh b/scripts/with-env.sh deleted file mode 100755 index 5e08ce9..0000000 --- a/scripts/with-env.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/sh -# Load .env from the project root, then exec the remaining command. -# Usage: ./scripts/with-env.sh clj -M:migrate - -set -e - -ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" -cd "$ROOT" - -if [ ! -f .env ]; then - echo "Error: .env not found in $ROOT" - echo "Copy dev.env.example or .env.example to .env and edit values." - exit 1 -fi - -while IFS= read -r line || [ -n "$line" ]; do - case "$line" in - ''|'#'*) continue ;; - *'='*) - key="${line%%=*}" - value="${line#*=}" - if echo "$key" | grep -q '^[A-Za-z_][A-Za-z0-9_]*$'; then - export "$key=$value" - fi - ;; - esac -done < .env - -if [ "$#" -eq 0 ]; then - echo "Usage: $0 [args...]" - exit 1 -fi - -exec "$@" diff --git a/src/self_destruct/config.clj b/src/self_destruct/config.clj index b71892c..df8fed1 100644 --- a/src/self_destruct/config.clj +++ b/src/self_destruct/config.clj @@ -1,5 +1,7 @@ (ns self-destruct.config - (:require [environ.core :as environ] + ;; self-destruct.env must load first so .env is available before environ init + (:require [self-destruct.env] + [environ.core :as environ] [migratus.core :as migratus] [taoensso.timbre :as timbre]) (:import [java.nio.charset StandardCharsets])) diff --git a/src/self_destruct/env.clj b/src/self_destruct/env.clj new file mode 100644 index 0000000..627d0c0 --- /dev/null +++ b/src/self_destruct/env.clj @@ -0,0 +1,70 @@ +(ns self-destruct.env + "Load a local .env file into JVM system properties for environ. + + Existing process environment variables always win. Values from .env are + applied only when the corresponding env var is unset/blank — so production + and CI (which export vars before starting Java) are unchanged, while local + `clj` commands pick up `.env` automatically." + (:require [clojure.java.io :as io] + [clojure.string :as str])) + + +(defn- keywordize + [k] + (-> (name k) + str/lower-case + (str/replace #"[_\\.]" "-") + keyword)) + + +(defn- parse-dotenv + "Parse KEY=VAL lines. Ignores blanks and # comments. Splits on the first =." + [contents] + (into [] + (keep (fn [line] + (let [line (str/trim line)] + (when (and (not (str/blank? line)) + (not (str/starts-with? line "#")) + (str/includes? line "=")) + (let [idx (str/index-of line "=") + k (str/trim (subs line 0 idx)) + v (str/trim (subs line (inc idx)))] + (when (re-matches #"[A-Za-z_][A-Za-z0-9_]*" k) + [k v]))))) + (str/split-lines contents)))) + + +(defn load-dotenv! + "Load `.env` from the process working directory (or `file` if given). + + - Skips keys already present in the process environment + - Sets JVM system properties so environ can see them + - If environ is already initialized, merges missing keys into `environ.core/env` + + Returns the parsed pairs (possibly empty), or nil when no file exists." + ([] + (load-dotenv! (io/file ".env"))) + ([file] + (let [f (io/file file)] + (when (.isFile f) + (let [pairs (parse-dotenv (slurp f))] + (doseq [[k v] pairs] + (when (str/blank? (System/getenv k)) + (System/setProperty k v))) + (when-let [environ-ns (find-ns 'environ.core)] + (when-let [env-var (ns-resolve environ-ns 'env)] + (alter-var-root + env-var + (fn [current] + (reduce (fn [m [k v]] + (let [kw (keywordize k)] + (if (str/blank? (str (get m kw))) + (assoc m kw v) + m))) + current + pairs))))) + pairs))))) + + +;; Load before other app namespaces require environ (see self-destruct.config). +(load-dotenv!) From 57b8bab70239ef3fb2789aee161d40aed99d367f Mon Sep 17 00:00:00 2001 From: Chad Stovern Date: Sat, 1 Aug 2026 16:25:10 -0400 Subject: [PATCH 3/4] Sanitize FreeBSD deploy docs for public consumption. Remove private multi-app / DDNS specifics so the guide stays generic for open-source readers. Co-authored-by: Cursor --- DEPLOYMENT_FREEBSD.md | 43 ++++++++++++++----------------------------- README.org | 4 ++-- 2 files changed, 16 insertions(+), 31 deletions(-) diff --git a/DEPLOYMENT_FREEBSD.md b/DEPLOYMENT_FREEBSD.md index 6a84e7c..f39a209 100644 --- a/DEPLOYMENT_FREEBSD.md +++ b/DEPLOYMENT_FREEBSD.md @@ -1,19 +1,18 @@ # self-destruct FreeBSD Deployment -Deploy self-destruct on a FreeBSD home server using the same pattern as -DoThisWeek: dedicated app user, PostgreSQL, `.env`, Clojure CLI uberjar, `rc.d` -service, nginx reverse proxy, certbot, and daily DB backups. +Deploy self-destruct on FreeBSD with a dedicated app user, PostgreSQL, `.env`, +Clojure CLI uberjar, `rc.d` service, nginx reverse proxy, certbot, and daily DB +backups. ## Assumptions -- PostgreSQL, nginx, certbot, firewall rules, and (optionally) Cloudflare DDNS - are already available on the server. +- PostgreSQL, nginx, certbot, and basic firewall rules are available (or you will + install them as you go). - `sudo` is available. -- The server has outbound HTTPS for GitHub, Maven/Clojars, Cloudflare, and - Let's Encrypt. +- The server has outbound HTTPS for GitHub, Maven/Clojars, and Let's Encrypt. - App path is `/home/selfdestruct/self-destruct`. - App user is `selfdestruct`. -- App port is `4003` (DoThisWeek uses `4002`). +- App port is `4003`. ## 1. Install Java Runtime and Build Tools @@ -75,7 +74,7 @@ psql -U selfdestruct -h localhost selfdestruct_prod ``` If needed, update `pg_hba.conf` to allow `md5` or `scram-sha-256` -authentication for localhost connections, matching the other deployed apps. +authentication for localhost TCP connections. ## 4. Create Production Environment @@ -168,29 +167,15 @@ sudo sysrc nginx_enable="YES" sudo service nginx start ``` -## 8. Configure DNS and Cloudflare DDNS +## 8. Configure DNS -In Cloudflare (or your DNS provider): +At your DNS provider: -- Create an A record for `@` pointing to the server public IP +- Create an A (or AAAA) record for the apex hostname pointing at the server - Prefer a CNAME for `www` pointing at the apex hostname -- Keep records DNS-only unless you intentionally want a CDN proxy -If you already run a shared Cloudflare DDNS script on this host, add another -zone entry for this domain (same pattern as DoThisWeek / other apps). Example: - -```sh -sudo sysrc cloudflare_ddns_zoneN_id="YOUR_ZONE_ID" -sudo sysrc cloudflare_ddns_zoneN_record_id="YOUR_RECORD_ID" -sudo sysrc cloudflare_ddns_zoneN_name="your.domain.example" -``` - -Then update the DDNS script to call `process_zone` for the new zone and verify: - -```sh -sudo /usr/local/bin/cloudflare-ddns.sh -tail /var/log/cloudflare-ddns.log -``` +If the server has a dynamic public IP, configure whatever DDNS mechanism you +already use so the apex record stays current before requesting certificates. ## 9. Configure HTTPS @@ -208,7 +193,7 @@ Verify HTTPS: curl -I https://your.domain.example ``` -Certbot renewal should already be configured if other apps use it. Verify: +Confirm certbot renewal is scheduled (or set it up), then dry-run: ```sh sudo certbot renew --dry-run diff --git a/README.org b/README.org index e8d5971..0b384f5 100644 --- a/README.org +++ b/README.org @@ -97,8 +97,8 @@ Writes ~target/self-destruct.jar~ (compiles Garden CSS first, then AOT + uberjar). - Primary deploy target is a FreeBSD home server. See [[file:DEPLOYMENT_FREEBSD.md][DEPLOYMENT_FREEBSD.md]] - for install, ~rc.d~, nginx, certbot, and backup scripts under ~scripts/~. + For FreeBSD production deploy (app user, ~rc.d~, nginx, certbot, backups), see + [[file:DEPLOYMENT_FREEBSD.md][DEPLOYMENT_FREEBSD.md]] and the scripts under ~scripts/~. Required environment variables (also listed in [[file:.env.example][.env.example]]): From 688a837c88d0a4e3e2ad25a4dcc2d855a671f4d1 Mon Sep 17 00:00:00 2001 From: Chad Stovern Date: Sat, 1 Aug 2026 16:34:34 -0400 Subject: [PATCH 4/4] Restore generic DNS/HTTPS operational tips from the FreeBSD deploy guide. Keep CDN-off-until-certs, www-on-dynamic-IP, and Secure-defaults guidance without private host-specific DDNS details. Co-authored-by: Cursor --- DEPLOYMENT_FREEBSD.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/DEPLOYMENT_FREEBSD.md b/DEPLOYMENT_FREEBSD.md index f39a209..9c3b2b8 100644 --- a/DEPLOYMENT_FREEBSD.md +++ b/DEPLOYMENT_FREEBSD.md @@ -173,9 +173,14 @@ At your DNS provider: - Create an A (or AAAA) record for the apex hostname pointing at the server - Prefer a CNAME for `www` pointing at the apex hostname +- If your DNS provider offers a CDN/proxy toggle (e.g. Cloudflare orange-cloud), + keep records DNS-only until certificates are issued, unless you intentionally + want that proxy in front of nginx -If the server has a dynamic public IP, configure whatever DDNS mechanism you -already use so the apex record stays current before requesting certificates. +If the server has a dynamic public IP, configure DDNS so the apex record stays +current before requesting certificates. If `www` is an A/AAAA record instead of +a CNAME, manage that record with DDNS too — otherwise it can go stale when the +IP changes. ## 9. Configure HTTPS @@ -199,6 +204,9 @@ Confirm certbot renewal is scheduled (or set it up), then dry-run: sudo certbot renew --dry-run ``` +Production `.env` sets `SECURE_DEFAULTS=true`, so prefer finishing HTTPS before +relying on browser sessions over plain HTTP. + ## 10. Configure Database Backups Run as root: