Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,5 +132,5 @@ jobs:
deps
- if: steps.cache.outputs.cache-hit != 'true'
run: mix deps.get
- run: mix compile --warnings-as-errors
- run: mix compile
- run: mix test
15 changes: 5 additions & 10 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,7 @@ jobs:
images: |
ghcr.io/${{ github.repository }}
tags: |
type=sha
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=ref,event=branch
type=ref,event=pr
type=raw,value={{date 'YYYYMMDD-HHmmss'}}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
Expand Down Expand Up @@ -70,12 +65,12 @@ jobs:
with:
script: |
const metadata = JSON.parse(process.env.BUILD_OUTPUT)
const shaUrl = metadata.tags.find((t) => t.includes(':sha-'))
const timestampUrl = metadata.tags.find((t) => t.match(/:\d{8}-\d{6}$/))

if (shaUrl == null) {
core.error('Unable to find sha tag of image')
if (timestampUrl == null) {
core.error('Unable to find timestamp tag of image')
} else {
const [image, tag] = shaUrl.split(':')
const [image, tag] = timestampUrl.split(':')
core.setOutput('image', image)
core.setOutput('tag', tag)
}
Expand Down
4 changes: 2 additions & 2 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
elixir 1.15
erlang 25.0
erlang 27.3.4.1
elixir 1.18.4-otp-27
41 changes: 29 additions & 12 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,26 @@
# - https://pkgs.org/ - resource for finding needed packages
# - Ex: hexpm/elixir:1.13.4-erlang-25.0.4-debian-bullseye-20220801-slim
#
ARG ELIXIR_VERSION=1.15.8
ARG OTP_VERSION=25.0.4
ARG DEBIAN_VERSION=bullseye-20240722-slim
ARG ELIXIR_VERSION=1.18.4
ARG OTP_VERSION=27.3.4.1
ARG DEBIAN_VERSION=bookworm-20250630-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
FROM ${BUILDER_IMAGE} AS builder

ARG DEPLOY_DOMAIN="https://elixirschool.com"

# install build dependencies
RUN apt-get update -y && apt-get install -y build-essential git npm --fix-missing --no-install-recommends \
&& apt-get clean && rm -f /var/lib/apt/lists/*_*
RUN rm -f /etc/apt/apt.conf.d/docker-clean

RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update -y && \
apt-get install -y build-essential git npm --fix-missing --no-install-recommends && \
apt-get clean && \
rm -f /var/lib/apt/lists/*_*

# prepare build dir
WORKDIR /app
Expand Down Expand Up @@ -73,7 +79,7 @@ RUN mix assets.deploy
RUN mix compile

# Changes to config/runtime.exs don't require recompiling the code
COPY config/releases.exs config/
COPY config/runtime.exs config/

COPY rel rel
RUN mix release
Expand All @@ -82,15 +88,21 @@ RUN mix release
# the compiled release and other runtime necessities
FROM ${RUNNER_IMAGE}

RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales \
&& apt-get clean && rm -f /var/lib/apt/lists/*_*
RUN rm -f /etc/apt/apt.conf.d/docker-clean

RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update -y && \
apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates tini && \
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
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US:en
ENV LC_ALL=en_US.UTF-8

WORKDIR "/app"
RUN chown nobody /app
Expand All @@ -102,10 +114,15 @@ ENV MIX_ENV="prod"
ENV ECTO_IPV6=true
ENV ERL_AFLAGS="-proto_dist inet6_tcp"

# Allow any release script to be ran without prefix
ENV PATH="$PATH:/app/bin"

# Only copy the final release from the build stage
COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/school_house ./
COPY --from=builder --chown=nobody:root /app/priv/static ./static

USER nobody

ENTRYPOINT ["tini", "--"]

CMD ["/app/bin/server"]
5 changes: 1 addition & 4 deletions config/prod.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ import Config
# manifest is generated by the `mix phx.digest` task,
# which you should run after static files are built and
# before starting your production server.
config :school_house, SchoolHouseWeb.Endpoint,
http: [port: {:system, "PORT"}],
url: [scheme: "https", host: "elixirschool.com", port: 443],
cache_static_manifest: "priv/static/cache_manifest.json"
config :school_house, SchoolHouseWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"

# Do not print debug messages in production
config :logger, level: :info
Expand Down
45 changes: 0 additions & 45 deletions config/releases.exs

This file was deleted.

47 changes: 47 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Config

if config_env() == :prod do
secret_key_base =
System.get_env("SECRET_KEY_BASE") ||
raise """
environment variable SECRET_KEY_BASE is missing.
You can generate one by calling: mix phx.gen.secret
"""

live_view_salt =
System.get_env("LIVE_VIEW_SALT") ||
raise """
environment variable LIVE_VIEW_SALT is missing.
You can generate one by calling: mix phx.gen.secret
"""

app_name = System.get_env("FLY_APP_NAME", "elixirschool")

host =
case app_name do
"elixirschool" -> "elixirschool.com"
app_name -> "#{app_name}.fly.dev"
end

config :school_house, SchoolHouseWeb.Endpoint,
http: [
ip: {0, 0, 0, 0},
port: String.to_integer(System.get_env("PORT", "4000"))
],
secret_key_base: secret_key_base,
url: [scheme: "https", host: host, port: 443],
live_view: [signing_salt: live_view_salt],
server: true

config :libcluster,
topologies: [
fly6pn: [
strategy: Cluster.Strategy.DNSPoll,
config: [
polling_interval: 5_000,
query: "#{app_name}.internal",
node_basename: app_name
]
]
]
end
4 changes: 1 addition & 3 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import Config

# We don't run a server during test. If one is required,
# you can enable the server option below.
config :school_house, SchoolHouseWeb.Endpoint,
http: [port: 4002],
server: false
config :school_house, SchoolHouseWeb.Endpoint, server: false

# Print only warnings and errors during test
config :logger, level: :warn
Expand Down
64 changes: 26 additions & 38 deletions fly.toml
Original file line number Diff line number Diff line change
@@ -1,43 +1,31 @@
app = "elixirschool"
# fly.toml app configuration file generated for elixirschool on 2025-08-02T09:40:13-04:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#

kill_signal = "SIGINT"
kill_timeout = 5
processes = []
app = 'elixirschool'
primary_region = 'atl'
kill_signal = 'SIGTERM'

[env]
PORT = 4000
[build]

[experimental]
allowed_public_ports = []
auto_rollback = true
[env]
PORT = '4000'

[[services]]
http_checks = []
[http_service]
internal_port = 4000
processes = ["app"]
protocol = "tcp"
script_checks = []

[services.concurrency]
hard_limit = 1_000
soft_limit = 750
type = "connections"

[[services.ports]]
force_https = true
handlers = ["http"]
port = 80

[[services.ports]]
handlers = ["tls", "http"]
port = 443

[[services.tcp_checks]]
grace_period = "5s"
interval = "15s"
restart_limit = 0
timeout = "2s"

[[statics]]
guest_path = "/app/static"
url_prefix = "/"
force_https = true
auto_stop_machines = 'stop'
auto_start_machines = true
min_machines_running = 0
processes = ['app']

[http_service.concurrency]
type = 'connections'
hard_limit = 1000
soft_limit = 1000

[[vm]]
memory = '1gb'
cpu_kind = 'shared'
cpus = 1
16 changes: 7 additions & 9 deletions lib/school_house/conferences.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,19 @@ defmodule SchoolHouse.Conferences do
from: Application.compile_env!(:school_house, :conference_dir),
as: :conferences

@ordered_conferences Enum.sort(@conferences, &(Date.compare(&1.date, &2.date) == :gt))
@online Enum.filter(@ordered_conferences, &is_nil(&1.location))
@by_countries Enum.group_by(@ordered_conferences, & &1.country)

def list, do: @ordered_conferences
def list, do: ordered_conferences()

def countries do
list()
|> Enum.map(& &1.country)
|> Enum.reject(&is_nil/1)
end

def online, do: @online
def online, do: Enum.filter(ordered_conferences(), &is_nil(&1.location))

def by_country(country) do
Map.get(@by_countries, country, [])
end
def by_country(country), do: Map.get(by_countries(), country, [])

defp ordered_conferences, do: Enum.sort(@conferences, &(Date.compare(&1.date, &2.date) == :gt))

defp by_countries, do: Enum.group_by(ordered_conferences(), & &1.country)
end
2 changes: 1 addition & 1 deletion lib/school_house/content/post.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ defmodule SchoolHouse.Content.Post do
slug =
filename
|> Path.basename(".md")
|> String.slice(date_prefix_length..-1)
|> String.slice(date_prefix_length..-1//1)

{title_text, attrs} = Map.pop!(attrs, :title)
{excerpt, attrs} = Map.pop!(attrs, :excerpt)
Expand Down
23 changes: 15 additions & 8 deletions lib/school_house/posts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,8 @@ defmodule SchoolHouse.Posts do
as: :posts,
highlighters: [:makeup_elixir, :makeup_erlang]

@paged_posts @posts |> Enum.sort(&(Date.compare(&1.date, &2.date) == :gt)) |> Enum.chunk_every(20)
@posts_by_slug Enum.into(@posts, %{}, fn %{slug: slug} = post ->
{slug, post}
end)

def get(slug) do
case Map.get(@posts_by_slug, slug) do
case Map.get(posts_by_slug(), slug) do
nil -> {:error, :not_found}
%Post{} = post -> {:ok, post}
end
Expand All @@ -37,9 +32,9 @@ defmodule SchoolHouse.Posts do
|> Enum.member?(tag)
end

def page(n), do: Enum.at(@paged_posts, n)
def page(n), do: Enum.at(paged_posts(), n)

def pages, do: Enum.count(@paged_posts)
def pages, do: Enum.count(paged_posts())

def tag_cloud do
Enum.reduce(@posts, %{}, fn %{tags: tags}, acc ->
Expand All @@ -49,4 +44,16 @@ defmodule SchoolHouse.Posts do
end)
end)
end

defp paged_posts do
@posts
|> Enum.sort(&(Date.compare(&1.date, &2.date) == :gt))
|> Enum.chunk_every(20)
end

defp posts_by_slug do
Enum.into(@posts, %{}, fn %{slug: slug} = post ->
{slug, post}
end)
end
end
Loading
Loading