Packages
nerves
1.14.3
1.15.0
1.14.3
1.14.2
1.14.1
1.14.0
1.13.2
1.13.1
1.13.0
1.12.0
1.11.3
1.11.2
1.11.1
1.11.0
1.10.5
1.10.4
1.10.3
1.10.2
1.10.1
1.10.0
1.9.3
1.9.2
retired
1.9.1
1.9.0
1.8.0
1.7.17
1.7.16
1.7.15
1.7.14
retired
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
retired
1.7.1
retired
1.7.0
retired
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.2.1
1.2.0
1.1.1
1.1.0
1.0.1
1.0.0
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.11.0
0.10.1
0.10.0
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.3
0.8.2
0.8.1
0.8.0
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.4.0-rc.0
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
Craft and deploy bulletproof embedded software
Current section
Files
Jump to
Current section
Files
lib/nerves/artifact/resolvers/github_api.ex
# SPDX-FileCopyrightText: 2018 Justin Schneck
# SPDX-FileCopyrightText: 2018 Matt Ludwigs
# SPDX-FileCopyrightText: 2022 Frank Hunleth
# SPDX-FileCopyrightText: 2023 Jon Carstens
#
# SPDX-License-Identifier: Apache-2.0
#
defmodule Nerves.Artifact.Resolvers.GithubAPI do
@moduledoc false
@behaviour Nerves.Artifact.Resolver
alias Nerves.Utils.HTTPClient
alias Nerves.Utils.Shell
@github_url "https://github.com"
@github_api_url "https://api.github.com"
defstruct [
:api_url,
:web_url,
:org_repo,
:custom_auth_token,
:artifact_filename,
:tag,
:method,
:use_gh_cli?
]
@impl Nerves.Artifact.Resolver
def plan({:github_api, org_repo, opts}, version, artifact_filename) do
{api_url, web_url} =
case Keyword.get(opts, :github_url) do
nil -> {URI.parse(@github_api_url), URI.parse(@github_url)}
custom -> {URI.parse(custom), nil}
end
prepared = %__MODULE__{
api_url: api_url,
web_url: web_url,
org_repo: org_repo,
custom_auth_token: opts[:token],
artifact_filename: artifact_filename,
tag: Keyword.get(opts, :tag, "v#{version}"),
method: :github_api,
use_gh_cli?: Keyword.get(opts, :use_gh_cli?, true)
}
{__MODULE__, prepared}
end
def plan({:github_releases, org_repo}, version, artifact_filename) do
plan({:github_releases, org_repo, []}, version, artifact_filename)
end
def plan({:github_releases, org_repo, opts}, version, artifact_filename) do
{api_url, web_url} =
case Keyword.get(opts, :github_url) do
nil -> {URI.parse(@github_api_url), URI.parse(@github_url)}
custom -> {nil, URI.parse(custom)}
end
prepared = %__MODULE__{
api_url: api_url,
web_url: web_url,
org_repo: org_repo,
custom_auth_token: opts[:token],
artifact_filename: artifact_filename,
tag: Keyword.get(opts, :tag, "v#{version}"),
method: :github_release,
use_gh_cli?: Keyword.get(opts, :use_gh_cli?, true)
}
{__MODULE__, prepared}
end
def plan(_site, _version, _artifact_filename), do: nil
@impl Nerves.Artifact.Resolver
def get(%__MODULE__{} = opts, dest_path) do
info =
if System.get_env("NERVES_DEBUG") == "1",
do: "#{opts.org_repo} #{opts.tag}/#{opts.artifact_filename}",
else: opts.artifact_filename
Shell.info([" [GitHub] ", info])
auth_token = get_auth_token(opts)
auth_headers =
if auth_token,
do: [{"Authorization", "Bearer " <> auth_token}],
else: []
case download(opts.method, opts, dest_path, auth_headers) do
:ok ->
:ok
{:error, reason} ->
elided_token = if auth_token, do: String.slice(auth_token, 0, 6) <> "...", else: "unset"
check_message =
if opts.web_url,
do: """
Check the release page for available artifacts:
#{URI.to_string(opts.web_url)}/#{opts.org_repo}/releases/tag/#{opts.tag}
""",
else: ""
{:error,
"""
Download failed: #{reason}
If this is a private repository or you're getting rate limited, please check
if you have a GitHub auth token. Nerves supports the `GITHUB_TOKEN` and `GH_TOKEN`
environment variables and can call the GitHub CLI to find it. Alternatively, you
can set a default strategy in your Nerves system's `artifact_sites` specification.
For private repositories, `:github_api` is the recommended strategy.
`:github_release` may work with authentication, but release downloads can still
fail depending on GitHub's access controls and behavior.
This failure was specifically for downloading #{opts.artifact_filename}. Other
files could have been tried.
#{check_message}
Download method: #{opts.method}
Web endpoint: #{safe_uri_to_string(opts.web_url)}
API endpoint: #{safe_uri_to_string(opts.api_url)}
Repository: #{opts.org_repo}
Release tag: #{opts.tag}
GitHub auth token: #{elided_token}
"""}
end
end
defp safe_uri_to_string(nil), do: "nil"
defp safe_uri_to_string(uri), do: URI.to_string(uri)
defp download(:github_release, opts, dest_path, auth_headers) do
download_url =
URI.append_path(
opts.web_url,
"/#{opts.org_repo}/releases/download/#{opts.tag}/#{opts.artifact_filename}"
)
result = HTTPClient.download(download_url, dest_path, headers: auth_headers)
if match?({:error, _}, result) and auth_headers != [] and opts.api_url != nil do
# Fall back to API if we have an auth token and know the API URL
download(:github_api, opts, dest_path, auth_headers)
else
result
end
end
defp download(:github_api, opts, dest_path, auth_headers) do
release_url =
URI.append_path(opts.api_url, "/repos/#{opts.org_repo}/releases/tags/#{opts.tag}")
with {:ok, release} <- HTTPClient.get_json(release_url, headers: auth_headers),
{:ok, asset_api_url} <- find_asset_url(release, opts.artifact_filename) do
download_headers = [{"Accept", "application/octet-stream"} | auth_headers]
HTTPClient.download(asset_api_url, dest_path, headers: download_headers)
end
end
defp find_asset_url(%{"assets" => assets}, filename) do
case Enum.find(assets, fn a -> a["name"] == filename end) do
%{"url" => url} -> {:ok, url}
nil -> {:error, "Asset '#{filename}' not found in release"}
end
end
defp get_auth_token(opts) do
# Environment variables always override. gh is used last.
System.get_env("GITHUB_TOKEN") || System.get_env("GH_TOKEN") || opts.custom_auth_token ||
(opts.use_gh_cli? && gh_token())
end
defp gh_token() do
with gh when not is_nil(gh) <- System.find_executable("gh"),
{result, 0} <- System.cmd(gh, ["auth", "token"]) do
String.trim(result)
else
_err -> nil
end
end
end