From 59f07d579bcdc6683f4dac973b9bdd143d6efa4c Mon Sep 17 00:00:00 2001 From: sloane <git@sloanelybutsurely.com> Date: Sun, 13 Apr 2025 07:52:36 -0400 Subject: [PATCH] setup kamal to deploy --- .dockerignore | 45 ++++++++ .kamal/hooks/docker-setup.sample | 3 + .kamal/hooks/post-app-boot.sample | 3 + .kamal/hooks/post-deploy.sample | 14 +++ .kamal/hooks/post-proxy-reboot.sample | 3 + .kamal/hooks/pre-app-boot.sample | 3 + .kamal/hooks/pre-build.sample | 51 ++++++++ .kamal/hooks/pre-connect.sample | 47 ++++++++ .kamal/hooks/pre-deploy.sample | 109 ++++++++++++++++++ .kamal/hooks/pre-proxy-reboot.sample | 3 + .kamal/secrets | 11 ++ .mise.toml | 1 + Dockerfile | 97 ++++++++++++++++ Gemfile | 3 + Gemfile.lock | 72 ++++++++++++ config/deploy.yml | 49 ++++++++ lib/core.ex | 2 +- lib/core/release.ex | 30 +++++ lib/sloanely_but_surely/application.ex | 2 + lib/sloanely_but_surely/mix.ex | 4 - lib/web/endpoint.ex | 2 + lib/web/health_check.ex | 14 +++ mix.exs | 6 +- ...vicon-91f37b602a111216f1eef3aa337ad763.ico | Bin 0 -> 152 bytes .../logo-06a11be1f2cdde2c851763d00bdd2e80.svg | 6 + ...go-06a11be1f2cdde2c851763d00bdd2e80.svg.gz | Bin 0 -> 1613 bytes priv/static/images/logo.svg.gz | Bin 0 -> 1613 bytes ...obots-9e2c81b0855bbff2baa8371bc4a78186.txt | 5 + ...ts-9e2c81b0855bbff2baa8371bc4a78186.txt.gz | Bin 0 -> 164 bytes priv/static/robots.txt.gz | Bin 0 -> 164 bytes rel/overlays/bin/migrate | 5 + rel/overlays/bin/migrate.bat | 1 + rel/overlays/bin/server | 5 + rel/overlays/bin/server.bat | 2 + 34 files changed, 590 insertions(+), 8 deletions(-) create mode 100644 .dockerignore create mode 100755 .kamal/hooks/docker-setup.sample create mode 100755 .kamal/hooks/post-app-boot.sample create mode 100755 .kamal/hooks/post-deploy.sample create mode 100755 .kamal/hooks/post-proxy-reboot.sample create mode 100755 .kamal/hooks/pre-app-boot.sample create mode 100755 .kamal/hooks/pre-build.sample create mode 100755 .kamal/hooks/pre-connect.sample create mode 100755 .kamal/hooks/pre-deploy.sample create mode 100755 .kamal/hooks/pre-proxy-reboot.sample create mode 100644 .kamal/secrets create mode 100644 Dockerfile create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 config/deploy.yml create mode 100644 lib/core/release.ex delete mode 100644 lib/sloanely_but_surely/mix.ex create mode 100644 lib/web/health_check.ex create mode 100644 priv/static/favicon-91f37b602a111216f1eef3aa337ad763.ico create mode 100644 priv/static/images/logo-06a11be1f2cdde2c851763d00bdd2e80.svg create mode 100644 priv/static/images/logo-06a11be1f2cdde2c851763d00bdd2e80.svg.gz create mode 100644 priv/static/images/logo.svg.gz create mode 100644 priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt create mode 100644 priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt.gz create mode 100644 priv/static/robots.txt.gz create mode 100755 rel/overlays/bin/migrate create mode 100755 rel/overlays/bin/migrate.bat create mode 100755 rel/overlays/bin/server create mode 100755 rel/overlays/bin/server.bat diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..61a7393 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,45 @@ +# This file excludes paths from the Docker build context. +# +# By default, Docker's build context includes all files (and folders) in the +# current directory. Even if a file isn't copied into the container it is still sent to +# the Docker daemon. +# +# There are multiple reasons to exclude files from the build context: +# +# 1. Prevent nested folders from being copied into the container (ex: exclude +# /assets/node_modules when copying /assets) +# 2. Reduce the size of the build context and improve build time (ex. /build, /deps, /doc) +# 3. Avoid sending files containing sensitive information +# +# More information on using .dockerignore is available here: +# https://docs.docker.com/engine/reference/builder/#dockerignore-file + +.dockerignore + +# Ignore git, but keep git HEAD and refs to access current commit hash if needed: +# +# $ cat .git/HEAD | awk '{print ".git/"$2}' | xargs cat +# d0b8727759e1e0e7aa3d41707d12376e373d5ecc +.git +!.git/HEAD +!.git/refs + +# Common development/test artifacts +/cover/ +/doc/ +/test/ +/tmp/ +.elixir_ls + +# Mix artifacts +/_build/ +/deps/ +*.ez + +# Generated on crash by the VM +erl_crash.dump + +# Static artifacts - These should be fetched and built inside the Docker image +/assets/node_modules/ +/priv/static/assets/ +/priv/static/cache_manifest.json diff --git a/.kamal/hooks/docker-setup.sample b/.kamal/hooks/docker-setup.sample new file mode 100755 index 0000000..2fb07d7 --- /dev/null +++ b/.kamal/hooks/docker-setup.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Docker set up on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-app-boot.sample b/.kamal/hooks/post-app-boot.sample new file mode 100755 index 0000000..70f9c4b --- /dev/null +++ b/.kamal/hooks/post-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-deploy.sample b/.kamal/hooks/post-deploy.sample new file mode 100755 index 0000000..75efafc --- /dev/null +++ b/.kamal/hooks/post-deploy.sample @@ -0,0 +1,14 @@ +#!/bin/sh + +# A sample post-deploy hook +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" diff --git a/.kamal/hooks/post-proxy-reboot.sample b/.kamal/hooks/post-proxy-reboot.sample new file mode 100755 index 0000000..1435a67 --- /dev/null +++ b/.kamal/hooks/post-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooted kamal-proxy on $KAMAL_HOSTS" diff --git a/.kamal/hooks/pre-app-boot.sample b/.kamal/hooks/pre-app-boot.sample new file mode 100755 index 0000000..45f7355 --- /dev/null +++ b/.kamal/hooks/pre-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/pre-build.sample b/.kamal/hooks/pre-build.sample new file mode 100755 index 0000000..f87d811 --- /dev/null +++ b/.kamal/hooks/pre-build.sample @@ -0,0 +1,51 @@ +#!/bin/sh + +# A sample pre-build hook +# +# Checks: +# 1. We have a clean checkout +# 2. A remote is configured +# 3. The branch has been pushed to the remote +# 4. The version we are deploying matches the remote +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) + +if [ -n "$(git status --porcelain)" ]; then + echo "Git checkout is not clean, aborting..." >&2 + git status --porcelain >&2 + exit 1 +fi + +first_remote=$(git remote) + +if [ -z "$first_remote" ]; then + echo "No git remote set, aborting..." >&2 + exit 1 +fi + +current_branch=$(git branch --show-current) + +if [ -z "$current_branch" ]; then + echo "Not on a git branch, aborting..." >&2 + exit 1 +fi + +remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) + +if [ -z "$remote_head" ]; then + echo "Branch not pushed to remote, aborting..." >&2 + exit 1 +fi + +if [ "$KAMAL_VERSION" != "$remote_head" ]; then + echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 + exit 1 +fi + +exit 0 diff --git a/.kamal/hooks/pre-connect.sample b/.kamal/hooks/pre-connect.sample new file mode 100755 index 0000000..18e61d7 --- /dev/null +++ b/.kamal/hooks/pre-connect.sample @@ -0,0 +1,47 @@ +#!/usr/bin/env ruby + +# A sample pre-connect check +# +# Warms DNS before connecting to hosts in parallel +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +hosts = ENV["KAMAL_HOSTS"].split(",") +results = nil +max = 3 + +elapsed = Benchmark.realtime do + results = hosts.map do |host| + Thread.new do + tries = 1 + + begin + Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) + rescue SocketError + if tries < max + puts "Retrying DNS warmup: #{host}" + tries += 1 + sleep rand + retry + else + puts "DNS warmup failed: #{host}" + host + end + end + + tries + end + end.map(&:value) +end + +retries = results.sum - hosts.size +nopes = results.count { |r| r == max } + +puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] diff --git a/.kamal/hooks/pre-deploy.sample b/.kamal/hooks/pre-deploy.sample new file mode 100755 index 0000000..1b280c7 --- /dev/null +++ b/.kamal/hooks/pre-deploy.sample @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby + +# A sample pre-deploy hook +# +# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. +# +# Fails unless the combined status is "success" +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_COMMAND +# KAMAL_SUBCOMMAND +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) + +# Only check the build status for production deployments +if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" + exit 0 +end + +require "bundler/inline" + +# true = install gems so this is fast on repeat invocations +gemfile(true, quiet: true) do + source "https://rubygems.org" + + gem "octokit" + gem "faraday-retry" +end + +MAX_ATTEMPTS = 72 +ATTEMPTS_GAP = 10 + +def exit_with_error(message) + $stderr.puts message + exit 1 +end + +class GithubStatusChecks + attr_reader :remote_url, :git_sha, :github_client, :combined_status + + def initialize + @remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/") + @git_sha = `git rev-parse HEAD`.strip + @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) + refresh! + end + + def refresh! + @combined_status = github_client.combined_status(remote_url, git_sha) + end + + def state + combined_status[:state] + end + + def first_status_url + first_status = combined_status[:statuses].find { |status| status[:state] == state } + first_status && first_status[:target_url] + end + + def complete_count + combined_status[:statuses].count { |status| status[:state] != "pending"} + end + + def total_count + combined_status[:statuses].count + end + + def current_status + if total_count > 0 + "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." + else + "Build not started..." + end + end +end + + +$stdout.sync = true + +puts "Checking build status..." +attempts = 0 +checks = GithubStatusChecks.new + +begin + loop do + case checks.state + when "success" + puts "Checks passed, see #{checks.first_status_url}" + exit 0 + when "failure" + exit_with_error "Checks failed, see #{checks.first_status_url}" + when "pending" + attempts += 1 + end + + exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS + + puts checks.current_status + sleep(ATTEMPTS_GAP) + checks.refresh! + end +rescue Octokit::NotFound + exit_with_error "Build status could not be found" +end diff --git a/.kamal/hooks/pre-proxy-reboot.sample b/.kamal/hooks/pre-proxy-reboot.sample new file mode 100755 index 0000000..061f805 --- /dev/null +++ b/.kamal/hooks/pre-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." diff --git a/.kamal/secrets b/.kamal/secrets new file mode 100644 index 0000000..5663118 --- /dev/null +++ b/.kamal/secrets @@ -0,0 +1,11 @@ +# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, +# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either +# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. + +SECRETS=$(kamal secrets fetch --adapter 1password --account Perrault --from Private/sloanelybutsurely.com KAMAL_REGISTRY_PASSWORD POSTGRES_PASSWORD SECRET_KEY_BASE) + +KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS) +POSTGRES_PASSWORD=$(kamal secrets extract POSTGRES_PASSWORD $SECRETS) +SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE $SECRETS) + +DATABASE_URL="postgresql://sloanely_but_surely_prod:$POSTGRES_PASSWORD@sloanelybutsurely-db:5432/sloanely_but_surely_prod" diff --git a/.mise.toml b/.mise.toml index 18d0485..08324db 100644 --- a/.mise.toml +++ b/.mise.toml @@ -2,3 +2,4 @@ elixir = "1.18.2-otp-27" erlang = "27.2.3" node = "22.14.0" +ruby = "3.4.2" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..10ffdac --- /dev/null +++ b/Dockerfile @@ -0,0 +1,97 @@ +# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian +# instead of Alpine to avoid DNS resolution issues in production. +# +# https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=ubuntu +# https://hub.docker.com/_/ubuntu?tab=tags +# +# This file is based on these images: +# +# - https://hub.docker.com/r/hexpm/elixir/tags - for the build image +# - https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye-20250203-slim - for the release image +# - https://pkgs.org/ - resource for finding needed packages +# - Ex: hexpm/elixir:1.18.2-erlang-27.2.3-debian-bullseye-20250203-slim +# +ARG ELIXIR_VERSION=1.18.2 +ARG OTP_VERSION=27.2.3 +ARG DEBIAN_VERSION=bullseye-20250203-slim + +ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" +ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}" + +FROM ${BUILDER_IMAGE} as builder + +# install build dependencies +RUN apt-get update -y && apt-get install -y build-essential git \ + && apt-get clean && rm -f /var/lib/apt/lists/*_* + +# prepare build dir +WORKDIR /app + +# install hex + rebar +RUN mix local.hex --force && \ + mix local.rebar --force + +# set build ENV +ENV MIX_ENV="prod" + +# install mix dependencies +COPY mix.exs mix.lock ./ +RUN mix deps.get --only $MIX_ENV +RUN mkdir config + +# copy compile-time config files before we compile dependencies +# to ensure any relevant config change will trigger the dependencies +# to be re-compiled. +COPY config/config.exs config/${MIX_ENV}.exs config/ +RUN mix deps.compile + +COPY priv priv + +COPY lib lib + +COPY assets assets + +# compile assets +RUN mix assets.deploy + +# Compile the release +RUN mix compile + +# Changes to config/runtime.exs don't require recompiling the code +COPY config/runtime.exs config/ + +COPY rel rel +RUN mix release + +# start a new build stage so that the final image will only contain +# the compiled release and other runtime necessities +FROM ${RUNNER_IMAGE} + +RUN apt-get update -y && \ + apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates \ + && apt-get clean && rm -f /var/lib/apt/lists/*_* + +# Set the locale +RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen + +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en +ENV LC_ALL en_US.UTF-8 + +WORKDIR "/app" +RUN chown nobody /app + +# set runner ENV +ENV MIX_ENV="prod" + +# Only copy the final release from the build stage +COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/sloanely_but_surely ./ + +USER nobody + +# If using an environment that doesn't automatically reap zombie processes, it is +# advised to add an init process such as tini via `apt-get install` +# above and adding an entrypoint. See https://github.com/krallin/tini for details +# ENTRYPOINT ["/tini", "--"] + +CMD ["/app/bin/server"] diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..2fbb4f5 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gem 'kamal' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..eda9e1b --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,72 @@ +GEM + remote: https://rubygems.org/ + specs: + activesupport (8.0.2) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + base64 (0.2.0) + bcrypt_pbkdf (1.1.1) + bcrypt_pbkdf (1.1.1-arm64-darwin) + bcrypt_pbkdf (1.1.1-x86_64-darwin) + benchmark (0.4.0) + bigdecimal (3.1.9) + concurrent-ruby (1.3.5) + connection_pool (2.5.0) + dotenv (3.1.8) + drb (2.2.1) + ed25519 (1.3.0) + i18n (1.14.7) + concurrent-ruby (~> 1.0) + kamal (2.5.3) + activesupport (>= 7.0) + base64 (~> 0.2) + bcrypt_pbkdf (~> 1.0) + concurrent-ruby (~> 1.2) + dotenv (~> 3.1) + ed25519 (~> 1.2) + net-ssh (~> 7.3) + sshkit (>= 1.23.0, < 2.0) + thor (~> 1.3) + zeitwerk (>= 2.6.18, < 3.0) + logger (1.7.0) + minitest (5.25.5) + net-scp (4.1.0) + net-ssh (>= 2.6.5, < 8.0.0) + net-sftp (4.0.0) + net-ssh (>= 5.0.0, < 8.0.0) + net-ssh (7.3.0) + ostruct (0.6.1) + securerandom (0.4.1) + sshkit (1.24.0) + base64 + logger + net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) + net-ssh (>= 2.8.0) + ostruct + thor (1.3.2) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uri (1.0.3) + zeitwerk (2.7.2) + +PLATFORMS + arm64-darwin + ruby + x86_64-darwin + +DEPENDENCIES + kamal + +BUNDLED WITH + 2.6.5 diff --git a/config/deploy.yml b/config/deploy.yml new file mode 100644 index 0000000..ffbb3a3 --- /dev/null +++ b/config/deploy.yml @@ -0,0 +1,49 @@ +service: sloanelybutsurely +image: sloanelybutsurely/sloanelybutsurely.com + +servers: + web: + - sloanelybutsurely + +env: + clear: + PHX_HOST: sloanelybutsurely.com + PORT: 4000 + secret: + - DATABASE_URL + - SECRET_KEY_BASE + +# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. +# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer. +# +# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. +proxy: + ssl: false + hosts: + - sloanelybutsurely.com + - sloanelybutsurely + app_port: 4000 + +registry: + username: sloanelybutsurely + password: + - KAMAL_REGISTRY_PASSWORD + +builder: + arch: amd64 + remote: ssh://root@sloanelybutsurely + local: false + +accessories: + db: + image: postgres:17-alpine + host: sloanelybutsurely + port: 5432 + env: + clear: + POSTGRES_USER: sloanely_but_surely_prod + POSTGRES_DB: sloanely_but_surely_prod + secret: + - POSTGRES_PASSWORD + volumes: + - postgres_data:/var/lib/postgresql/data diff --git a/lib/core.ex b/lib/core.ex index 3ce5c5e..73576ec 100644 --- a/lib/core.ex +++ b/lib/core.ex @@ -1,4 +1,4 @@ defmodule Core do @moduledoc false - use Boundary, deps: [Schema], exports: [Accounts, Posts, Author, DateTime] + use Boundary, deps: [Schema], exports: [Accounts, Posts, Author, DateTime, Release] end diff --git a/lib/core/release.ex b/lib/core/release.ex new file mode 100644 index 0000000..d91f9a0 --- /dev/null +++ b/lib/core/release.ex @@ -0,0 +1,30 @@ +defmodule Core.Release do + @moduledoc """ + Used for executing DB release tasks when run in production without Mix + installed. + """ + @app :sloanely_but_surely + + def migrate do + load_app() + + for repo <- repos() do + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) + end + + :ok + end + + def rollback(repo, version) do + load_app() + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) + end + + defp repos do + Application.fetch_env!(@app, :ecto_repos) + end + + defp load_app do + Application.load(@app) + end +end diff --git a/lib/sloanely_but_surely/application.ex b/lib/sloanely_but_surely/application.ex index c21a035..58d2b83 100644 --- a/lib/sloanely_but_surely/application.ex +++ b/lib/sloanely_but_surely/application.ex @@ -4,6 +4,8 @@ defmodule SloanelyButSurely.Application do @impl Application def start(_type, _args) do + :ok = Core.Release.migrate() + children = [ Core.Repo, {Phoenix.PubSub, name: Core.PubSub}, diff --git a/lib/sloanely_but_surely/mix.ex b/lib/sloanely_but_surely/mix.ex deleted file mode 100644 index f34549e..0000000 --- a/lib/sloanely_but_surely/mix.ex +++ /dev/null @@ -1,4 +0,0 @@ -defmodule SloanelyButSurely.Mix do - @moduledoc false - use Boundary, deps: [], exports: [] -end diff --git a/lib/web/endpoint.ex b/lib/web/endpoint.ex index 80608e7..875a36c 100644 --- a/lib/web/endpoint.ex +++ b/lib/web/endpoint.ex @@ -1,6 +1,8 @@ defmodule Web.Endpoint do use Phoenix.Endpoint, otp_app: :sloanely_but_surely + plug Web.HealthCheck + # The session will be stored in the cookie and signed, # this means its contents can be read but not tampered with. # Set :encryption_salt if you would also like to encrypt it. diff --git a/lib/web/health_check.ex b/lib/web/health_check.ex new file mode 100644 index 0000000..3de1b3d --- /dev/null +++ b/lib/web/health_check.ex @@ -0,0 +1,14 @@ +defmodule Web.HealthCheck do + @moduledoc "Silent HTTP health check plug" + import Plug.Conn + + def init(opts), do: opts + + def call(%Plug.Conn{request_path: "/up"} = conn, _opts) do + conn + |> send_resp(200, "") + |> halt() + end + + def call(conn, _opts), do: conn +end diff --git a/mix.exs b/mix.exs index 18337c0..fa6bf4f 100644 --- a/mix.exs +++ b/mix.exs @@ -84,10 +84,10 @@ defmodule SlaonelyButSurely.MixProject do "ecto.reset": ["ecto.drop", "ecto.setup"], test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"], "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"], - "assets.build": ["tailwind cms", "esbuild cms"], + "assets.build": ["tailwind sloanely_but_surely", "esbuild sloanely_but_surely"], "assets.deploy": [ - "tailwind cms --minify", - "esbuild cms --minify", + "tailwind sloanely_but_surely --minify", + "esbuild sloanely_but_surely --minify", "phx.digest" ] ] diff --git a/priv/static/favicon-91f37b602a111216f1eef3aa337ad763.ico b/priv/static/favicon-91f37b602a111216f1eef3aa337ad763.ico new file mode 100644 index 0000000000000000000000000000000000000000..7f372bfc21cdd8cb47585339d5fa4d9dd424402f GIT binary patch literal 152 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=@t!V@Ar*{oFEH`~d50E!_s``s q?{G*w(7?#d#v@^nKnY_HKaYb01EZMZjMqTJ89ZJ6T-G@yGywoKK_h|y literal 0 HcmV?d00001 diff --git a/priv/static/images/logo-06a11be1f2cdde2c851763d00bdd2e80.svg b/priv/static/images/logo-06a11be1f2cdde2c851763d00bdd2e80.svg new file mode 100644 index 0000000..9f26bab --- /dev/null +++ b/priv/static/images/logo-06a11be1f2cdde2c851763d00bdd2e80.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 71 48" fill="currentColor" aria-hidden="true"> + <path + d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.077.057c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728a13 13 0 0 0 1.182 1.106c1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zl-.006.006-.036-.004.021.018.012.053Za.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zl-.008.01.005.026.024.014Z" + fill="#FD4F00" + /> +</svg> diff --git a/priv/static/images/logo-06a11be1f2cdde2c851763d00bdd2e80.svg.gz b/priv/static/images/logo-06a11be1f2cdde2c851763d00bdd2e80.svg.gz new file mode 100644 index 0000000000000000000000000000000000000000..d8d1f38b39fd9260b40ceceae1fd11e34f4a9518 GIT binary patch literal 1613 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 Lx&T1t01N;CjKeUb literal 0 HcmV?d00001 diff --git a/priv/static/images/logo.svg.gz b/priv/static/images/logo.svg.gz new file mode 100644 index 0000000000000000000000000000000000000000..d8d1f38b39fd9260b40ceceae1fd11e34f4a9518 GIT binary patch literal 1613 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 Lx&T1t01N;CjKeUb literal 0 HcmV?d00001 diff --git a/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt b/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt new file mode 100644 index 0000000..26e06b5 --- /dev/null +++ b/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt @@ -0,0 +1,5 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-agent: * +# Disallow: / diff --git a/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt.gz b/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt.gz new file mode 100644 index 0000000000000000000000000000000000000000..a1d6ca87acf965690c360a6c298762df132975b8 GIT binary patch literal 164 zcmV;V09*ebiwFP!000006GhB14#F@D1<<{x_)<3{nmsc&01l8+w~3U*RqQGpA5#V- zFJJ!ujkpsbs_x>Q>%C8nXI9a-PTV&4Pf<(8$_)#@jzU#~Ca$oH+@Xv^2pS2$$z&U> zDbp|xBOZ)7RD_%%ds?Uo*2d-R8<iSCk`j*k;_}7MPbD+7GjggV-khgUyN1mQ9v92E SBxZ8=aKi`kRd5K)0001LQAT(G literal 0 HcmV?d00001 diff --git a/priv/static/robots.txt.gz b/priv/static/robots.txt.gz new file mode 100644 index 0000000000000000000000000000000000000000..a1d6ca87acf965690c360a6c298762df132975b8 GIT binary patch literal 164 zcmV;V09*ebiwFP!000006GhB14#F@D1<<{x_)<3{nmsc&01l8+w~3U*RqQGpA5#V- zFJJ!ujkpsbs_x>Q>%C8nXI9a-PTV&4Pf<(8$_)#@jzU#~Ca$oH+@Xv^2pS2$$z&U> zDbp|xBOZ)7RD_%%ds?Uo*2d-R8<iSCk`j*k;_}7MPbD+7GjggV-khgUyN1mQ9v92E SBxZ8=aKi`kRd5K)0001LQAT(G literal 0 HcmV?d00001 diff --git a/rel/overlays/bin/migrate b/rel/overlays/bin/migrate new file mode 100755 index 0000000..dc5607c --- /dev/null +++ b/rel/overlays/bin/migrate @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +cd -P -- "$(dirname -- "$0")" +exec ./sloanely_but_surely eval Core.Release.migrate diff --git a/rel/overlays/bin/migrate.bat b/rel/overlays/bin/migrate.bat new file mode 100755 index 0000000..2f36a80 --- /dev/null +++ b/rel/overlays/bin/migrate.bat @@ -0,0 +1 @@ +call "%~dp0\sloanely_but_surely" eval Core.Release.migrate diff --git a/rel/overlays/bin/server b/rel/overlays/bin/server new file mode 100755 index 0000000..6efe4df --- /dev/null +++ b/rel/overlays/bin/server @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +cd -P -- "$(dirname -- "$0")" +PHX_SERVER=true exec ./sloanely_but_surely start diff --git a/rel/overlays/bin/server.bat b/rel/overlays/bin/server.bat new file mode 100755 index 0000000..1b63628 --- /dev/null +++ b/rel/overlays/bin/server.bat @@ -0,0 +1,2 @@ +set PHX_SERVER=true +call "%~dp0\sloanely_but_surely" start