diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml index 167ec8ef..d6207139 100644 --- a/.forgejo/workflows/build.yaml +++ b/.forgejo/workflows/build.yaml @@ -41,6 +41,9 @@ jobs: run: | IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" TAG="${{ steps.tag.outputs.tag }}" - docker build -t "${IMAGE}:${TAG}" -t "${IMAGE}:latest" . + BUILD_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + docker build \ + --build-arg BUILD_TIMESTAMP="${BUILD_TIMESTAMP}" \ + -t "${IMAGE}:${TAG}" -t "${IMAGE}:latest" . docker push "${IMAGE}:${TAG}" docker push "${IMAGE}:latest" diff --git a/Dockerfile b/Dockerfile index e64d8e63..a7000f5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -137,6 +137,11 @@ RUN mix release # ---- Final runtime stage ---- FROM ${RUNNER_IMAGE} AS final +# Build timestamp baked in by CI (--build-arg BUILD_TIMESTAMP=...). +# Only consumed in this final stage, so it never busts the earlier +# compile cache. Read at runtime by Microwaveprop.Application.build_timestamp/0. +ARG BUILD_TIMESTAMP=unknown + RUN apt-get update \ && apt-get install -y --no-install-recommends \ libstdc++6 openssl libncurses6 locales ca-certificates snmp \ @@ -162,6 +167,9 @@ RUN ldconfig COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/microwaveprop ./ +RUN echo "${BUILD_TIMESTAMP}" > /app/BUILD_TIMESTAMP \ + && chown nobody /app/BUILD_TIMESTAMP + USER nobody CMD ["/app/bin/server"] diff --git a/lib/microwaveprop/application.ex b/lib/microwaveprop/application.ex index 1f5beee5..0f386597 100644 --- a/lib/microwaveprop/application.ex +++ b/lib/microwaveprop/application.ex @@ -5,6 +5,8 @@ defmodule Microwaveprop.Application do use Application + @build_timestamp DateTime.utc_now() + @impl true def start(_type, _args) do topologies = Application.get_env(:libcluster, :topologies, []) @@ -53,4 +55,51 @@ defmodule Microwaveprop.Application do MicrowavepropWeb.Endpoint.config_change(changed, removed) :ok end + + @doc """ + Returns the deployment (image build) timestamp. + + Sources, in priority order: + + 1. The `/app/BUILD_TIMESTAMP` file baked into the Docker image at build + time (the CI pipeline passes `--build-arg BUILD_TIMESTAMP=...` in the + final stage, so this value is fresh on every image and is not affected + by earlier-stage layer caching). The path is overridable via + `config :microwaveprop, :build_timestamp_file` for tests. + 2. The `DEPLOY_TIMESTAMP` environment variable, for environments that set + it imperatively (e.g. `kubectl set env`). + 3. The compile-time fallback captured when this module was compiled. + + All accepted values are ISO 8601, e.g. `2026-04-17T18:30:00Z`. + """ + @spec build_timestamp() :: DateTime.t() + def build_timestamp do + with :error <- from_file(), + :error <- from_env() do + @build_timestamp + end + end + + defp from_file do + path = Application.get_env(:microwaveprop, :build_timestamp_file, "/app/BUILD_TIMESTAMP") + + case File.read(path) do + {:ok, contents} -> parse_iso8601(contents) + {:error, _} -> :error + end + end + + defp from_env do + case System.get_env("DEPLOY_TIMESTAMP") do + nil -> :error + string -> parse_iso8601(string) + end + end + + defp parse_iso8601(string) do + case string |> String.trim() |> DateTime.from_iso8601() do + {:ok, dt, _offset} -> dt + {:error, _} -> :error + end + end end diff --git a/lib/microwaveprop_web/components/layouts.ex b/lib/microwaveprop_web/components/layouts.ex index 6f239aac..324aa820 100644 --- a/lib/microwaveprop_web/components/layouts.ex +++ b/lib/microwaveprop_web/components/layouts.ex @@ -80,7 +80,8 @@ defmodule MicrowavepropWeb.Layouts do <% end %> -