From b6ea0947e9e60a94702889db77e008b8eacb8b3a Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Wed, 12 Aug 2026 14:19:07 -0400 Subject: [PATCH] fix: give a clear message and exit 78 when LINEAR_API_KEY is missing A missing LINEAR_API_KEY previously surfaced as a raw %Ash.Error.Unknown{} dump reaching the generic catch-all handler ("What the heck is this?" + a full Ash stacktrace) - the Ash manual actions wrapping LinearCli.Api.call/2's {:error, :missing_api_key} produced a shape that didn't match either existing specific handle_error/3 clause. Adds a clause matching that exact wrapped shape (Ash stringifies the original reason into UnknownError's :error field rather than preserving the atom - verified directly, not guessed) and prints a clear, actionable message instead, with sysexits.h's EX_CONFIG (78) since this is a configuration problem, not a crash. Plain pattern match, no guard, so it degrades gracefully to the existing catch-all if Ash's wrapping format ever changes rather than introducing a new failure mode. Closes #74. --- app/lib/linear_cli/cli.ex | 26 +++++++++++++++++++ .../linear_cli/cli/missing_api_key_test.exs | 23 ++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 app/test/linear_cli/cli/missing_api_key_test.exs diff --git a/app/lib/linear_cli/cli.ex b/app/lib/linear_cli/cli.ex index dca9238..0e9699f 100644 --- a/app/lib/linear_cli/cli.ex +++ b/app/lib/linear_cli/cli.ex @@ -282,6 +282,32 @@ defmodule LinearCli.CLI do halt.(66) end + # LinearCli.Api.call/2's {:error, :missing_api_key} (LINEAR_API_KEY not + # set), reached through any of the Ash manual actions that wrap it - + # Ash's own action pipeline stringifies the original reason into + # Ash.Error.Unknown.UnknownError's :error field ("unknown error: + # :missing_api_key", verified directly) rather than preserving the atom, + # hence the exact-string match below instead of `error: :missing_api_key`. + # A plain pattern match (no guard - `=~` isn't guard-safe) degrades + # gracefully to the generic catch-all below if Ash's wrapping format ever + # changes, rather than raising a fresh error of its own. A missing API key + # is a configuration problem, not a surprising crash - give it a clear + # message and sysexits.h's EX_CONFIG (78) instead of the catch-all's raw + # error dump. See #74. + defp handle_error( + %Ash.Error.Unknown{ + errors: [%Ash.Error.Unknown.UnknownError{error: "unknown error: :missing_api_key"} | _] + }, + debug, + halt + ) do + IO.puts(:stderr, "LINEAR_API_KEY is not set.") + IO.puts(:stderr, "Set it to your Linear API key - see https://linear.app/settings/api") + IO.puts(:stderr, "** Missing configuration, cannot continue **") + maybe_print_backtrace(debug) + halt.(78) + end + # Ported from CLI::Caller#call's `rescue SmellsBad` clause. See # `LinearCli.CLI.IssueHelpers`'s moduledoc for where this tagged tuple # comes from. diff --git a/app/test/linear_cli/cli/missing_api_key_test.exs b/app/test/linear_cli/cli/missing_api_key_test.exs new file mode 100644 index 0000000..4526c1f --- /dev/null +++ b/app/test/linear_cli/cli/missing_api_key_test.exs @@ -0,0 +1,23 @@ +defmodule LinearCli.CLI.MissingApiKeyTest do + # async: false - unsets the real (VM-global, not per-process) + # LINEAR_API_KEY env var, same reason/pattern as LinearCli.ApiTest. + use ExUnit.Case, async: false + import ExUnit.CaptureIO + + test "a missing LINEAR_API_KEY gives a clear message and exits 78, not a raw Ash dump" do + previous = System.get_env("LINEAR_API_KEY") + System.delete_env("LINEAR_API_KEY") + on_exit(fn -> previous && System.put_env("LINEAR_API_KEY", previous) end) + + test_pid = self() + halt = fn code -> send(test_pid, {:halted, code}) end + + output = capture_io(:stderr, fn -> LinearCli.CLI.main(["whoami"], halt) end) + + assert_received {:halted, 78} + assert output =~ "LINEAR_API_KEY is not set." + assert output =~ "https://linear.app/settings/api" + refute output =~ "What the heck is this?" + refute output =~ "Ash.Error" + end +end