Fix deployment timestamp always showing 'less than a minute ago' in k8s

Previously, the deployment timestamp was set to DateTime.utc_now() when
the application started, which meant it would always show as recent in
k8s environments where pods are recreated on each deployment.

This commit:
- Updates Release.init() to check for DEPLOYED_AT environment variable
- Falls back to current time if env var is not set (backwards compatible)
- Adds proper ISO8601 timestamp parsing with error handling
- Updates GitHub Actions deploy workflow to set DEPLOYED_AT during deployment
- Creates k8s patch file for future manual deployments

The deployment timestamp will now persist across pod restarts in k8s,
showing the actual deployment time rather than pod startup time.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Graham McIntire 2025-07-28 12:06:12 -05:00
parent ece5c82e99
commit 7e4bbbaff5
No known key found for this signature in database
6 changed files with 77 additions and 23 deletions

View file

@ -74,7 +74,32 @@ jobs:
- name: Deploy to K3s
run: |
# Get current timestamp in ISO8601 format
DEPLOYED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Create a temporary patch file with the deployment timestamp
cat > /tmp/deployed-at-patch.yaml <<EOF
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: aprs
namespace: aprs
spec:
template:
spec:
containers:
- name: aprs
env:
- name: DEPLOYED_AT
value: "$DEPLOYED_AT"
EOF
# Apply the patch to set the deployment timestamp
kubectl patch statefulset aprs -n aprs --patch-file=/tmp/deployed-at-patch.yaml
# Update the image
kubectl set image statefulset/aprs aprs=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest -n aprs
# Force a rollout to ensure latest image is pulled
kubectl rollout restart statefulset/aprs -n aprs
echo "Deployment initiated - not waiting for rollout to complete"
echo "Deployment initiated with timestamp $DEPLOYED_AT - not waiting for rollout to complete"

View file

@ -57,9 +57,8 @@ ENV LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8
WORKDIR /app
# Create deployment timestamp before switching users
RUN date -u +"%Y-%m-%dT%H:%M:%SZ" > /app/deployed_at.txt && \
chown elixir:root /app/deployed_at.txt
# Ensure proper ownership
RUN chown -R elixir:root /app
COPY --from=builder --chown=elixir:root /app/_build/prod/rel/aprsme ./

View file

@ -0,0 +1,13 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: aprs
namespace: aprs
spec:
template:
spec:
containers:
- name: aprs
env:
- name: DEPLOYED_AT
value: "TIMESTAMP_PLACEHOLDER"

View file

@ -9,6 +9,9 @@ defmodule Aprsme.Application do
@impl true
def start(_type, _args) do
# Initialize deployment timestamp
Aprsme.Release.init()
# Run migrations on startup
migrate()

View file

@ -75,10 +75,29 @@ defmodule Aprsme.Release do
This is called during application startup.
"""
def init do
# Read deployment timestamp from file
deployed_at = read_deployment_timestamp()
# Check if deployment timestamp is provided via environment variable
# This allows k8s deployments to set a persistent timestamp
deployed_at =
case System.get_env("DEPLOYED_AT") do
nil ->
# Fallback to current time for local development or non-k8s deployments
DateTime.utc_now()
# Add to application config
timestamp_str ->
# Parse the ISO8601 timestamp from environment variable
case DateTime.from_iso8601(timestamp_str) do
{:ok, datetime, _offset} ->
datetime
{:error, _} ->
require Logger
Logger.warning("Invalid DEPLOYED_AT timestamp: #{timestamp_str}, using current time")
DateTime.utc_now()
end
end
# Store in application config
Application.put_env(:aprsme, :deployed_at, deployed_at)
deployed_at
@ -88,7 +107,16 @@ defmodule Aprsme.Release do
Get the deployment timestamp.
"""
def deployed_at do
Application.get_env(:aprsme, :deployed_at) || DateTime.utc_now()
case Application.get_env(:aprsme, :deployed_at) do
nil ->
# If not initialized yet, initialize now
timestamp = DateTime.utc_now()
Application.put_env(:aprsme, :deployed_at, timestamp)
timestamp
timestamp ->
timestamp
end
end
defp run_migrations_with_timeout(timeout) do
@ -113,18 +141,4 @@ defmodule Aprsme.Release do
timeout: timeout
)
end
defp read_deployment_timestamp do
case File.read("/app/deployed_at.txt") do
{:ok, timestamp} ->
case DateTime.from_iso8601(String.trim(timestamp)) do
{:ok, datetime, _} -> datetime
_ -> DateTime.utc_now()
end
_ ->
# Fallback to current time if file doesn't exist
DateTime.utc_now()
end
end
end

View file

@ -671,7 +671,7 @@ defmodule AprsmeWeb.MapLive.Index do
end)
|> Enum.filter(& &1)
socket =
socket =
if Enum.any?(station_packets) do
# Build packet data for the RF path stations
packet_data_list = DataBuilder.build_packet_data_list(station_packets)