Updates all docs, UI copy, release checker, proto go_package, container image refs, and tests to point at codeberg.org/towerops-agent/towerops-agent. Also deletes stray empty package.json/package-lock.json (Phoenix uses esbuild — no npm) and an unused test_encode_decode.exs scratch file.
153 lines
4.1 KiB
Elixir
153 lines
4.1 KiB
Elixir
defmodule Towerops.Agents.ReleaseChecker do
|
|
@moduledoc """
|
|
Fetches latest release info from the Codeberg (Forgejo) Releases API for the
|
|
towerops-agent repo.
|
|
|
|
Caches results for 5 minutes to avoid excessive API calls.
|
|
"""
|
|
|
|
require Logger
|
|
|
|
@codeberg_repo "towerops-agent/towerops-agent"
|
|
@cache_ttl_ms to_timeout(minute: 5)
|
|
|
|
@arch_map %{
|
|
"x86_64" => "amd64",
|
|
"aarch64" => "arm64"
|
|
}
|
|
|
|
@doc """
|
|
Clears the cached release info so the next call to `get_latest_release/0`
|
|
fetches fresh data from Codeberg.
|
|
"""
|
|
@spec invalidate_cache() :: :ok
|
|
def invalidate_cache do
|
|
:persistent_term.erase({__MODULE__, :release})
|
|
:ok
|
|
rescue
|
|
ArgumentError -> :ok
|
|
end
|
|
|
|
@doc """
|
|
Returns the latest release info from Codeberg.
|
|
|
|
Returns `{:ok, %{version: String.t(), assets: [asset]}}` where each asset
|
|
has `name`, `url`, and `checksum` fields.
|
|
|
|
Results are cached for 5 minutes.
|
|
"""
|
|
@spec get_latest_release() :: {:ok, map()} | {:error, term()}
|
|
def get_latest_release do
|
|
case get_cached() do
|
|
{:ok, release} ->
|
|
{:ok, release}
|
|
|
|
:miss ->
|
|
fetch_and_cache()
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Finds the asset matching the given architecture.
|
|
|
|
Maps Rust architecture strings (x86_64, aarch64) to release asset names (amd64, arm64).
|
|
"""
|
|
@spec asset_for_arch([map()], String.t()) :: {:ok, map()} | {:error, :no_matching_asset}
|
|
def asset_for_arch(assets, arch) do
|
|
target = Map.get(@arch_map, arch, arch)
|
|
|
|
case Enum.find(assets, fn asset -> String.contains?(asset.name, target) end) do
|
|
nil -> {:error, :no_matching_asset}
|
|
asset -> {:ok, asset}
|
|
end
|
|
end
|
|
|
|
defp get_cached do
|
|
case :persistent_term.get({__MODULE__, :release}, nil) do
|
|
{release, cached_at} ->
|
|
if System.monotonic_time(:millisecond) - cached_at < @cache_ttl_ms do
|
|
{:ok, release}
|
|
else
|
|
:miss
|
|
end
|
|
|
|
nil ->
|
|
:miss
|
|
end
|
|
end
|
|
|
|
defp fetch_and_cache do
|
|
url = "https://codeberg.org/api/v1/repos/#{@codeberg_repo}/releases/latest"
|
|
|
|
case req_get(url, headers: [{"accept", "application/json"}], retry: false) do
|
|
{:ok, %Req.Response{status: 200, body: body}} ->
|
|
release = parse_release(body)
|
|
:persistent_term.put({__MODULE__, :release}, {release, System.monotonic_time(:millisecond)})
|
|
{:ok, release}
|
|
|
|
{:ok, %Req.Response{status: status}} ->
|
|
Logger.warning("Codeberg API returned status #{status} for latest release")
|
|
{:error, {:codeberg_api, status}}
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("Failed to fetch latest release from Codeberg: #{inspect(reason)}")
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp parse_release(body) do
|
|
version = String.trim_leading(body["tag_name"], "v")
|
|
|
|
assets =
|
|
(body["assets"] || [])
|
|
|> Enum.filter(fn asset ->
|
|
name = asset["name"]
|
|
String.starts_with?(name, "towerops-agent-linux-") and not String.ends_with?(name, ".sha256")
|
|
end)
|
|
|> Enum.map(fn asset ->
|
|
checksum = fetch_checksum(body["assets"], asset["name"])
|
|
|
|
%{
|
|
name: asset["name"],
|
|
url: asset["browser_download_url"],
|
|
checksum: checksum
|
|
}
|
|
end)
|
|
|
|
%{version: version, assets: assets}
|
|
end
|
|
|
|
defp fetch_checksum(all_assets, binary_name) do
|
|
sha_asset = Enum.find(all_assets, fn a -> a["name"] == "#{binary_name}.sha256" end)
|
|
|
|
if sha_asset do
|
|
case req_get(sha_asset["browser_download_url"], retry: false) do
|
|
{:ok, %Req.Response{status: 200, body: body}} ->
|
|
body |> String.trim() |> String.split(" ") |> List.first()
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
end
|
|
end
|
|
|
|
defp req_get(url, opts) do
|
|
opts =
|
|
if Application.get_env(:towerops, :env) == :test do
|
|
opts
|
|
|> Keyword.put(:plug, {Req.Test, __MODULE__})
|
|
|> Keyword.put_new(:retry, false)
|
|
else
|
|
opts
|
|
end
|
|
|
|
Req.get(url, opts)
|
|
rescue
|
|
error in RuntimeError ->
|
|
if String.contains?(Exception.message(error), "cannot find mock/stub") do
|
|
{:error, :no_test_stub}
|
|
else
|
|
reraise error, __STACKTRACE__
|
|
end
|
|
end
|
|
end
|