diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d87347a..4630fac 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -44,7 +44,8 @@ jobs: app/_build key: ${{ runner.os }}-mix-${{ hashFiles('app/mix.lock') }} - - # mix ci covers deps.get, hex.audit, format --check-formatted, + # mix ci covers: deps.get, hex.audit, deps.audit (Elixir security + # advisories), format --check-formatted, credo (static analysis), # usage_rules.sync --check (catches dep-bump drift, see #79), # and mix test. Runs from the repo root; cd: app/ is handled # inside the task itself. diff --git a/app/.credo.exs b/app/.credo.exs new file mode 100644 index 0000000..4637dea --- /dev/null +++ b/app/.credo.exs @@ -0,0 +1,227 @@ +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any config using `mix credo -C `. If no config name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: [ + "lib/", + "src/", + "test/", + "web/", + "apps/*/lib/", + "apps/*/src/", + "apps/*/test/", + "apps/*/web/" + ], + excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] + }, + # + # Load and configure plugins here: + # + plugins: [], + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: false, + # + # To modify the timeout for parsing files, change this value: + # + parse_timeout: 5000, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: %{ + enabled: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + # AliasUsage intentionally disabled: the codebase consistently uses full + # module paths (e.g. LinearCli.Linear.Issue) for clarity in Ash DSL contexts. + # {Credo.Check.Design.AliasUsage, []}, + {Credo.Check.Design.TagFIXME, []}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 2]}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + {Credo.Check.Readability.WithSingleClause, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.Apply, []}, + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.FilterCount, []}, + {Credo.Check.Refactor.FilterFilter, []}, + {Credo.Check.Refactor.FunctionArity, []}, + # max_nesting: 3 - if/case/case (depth 3) is common in Elixir and clear; + # the default of 2 fires on legitimate patterns throughout this codebase. + {Credo.Check.Refactor.Nesting, max_nesting: 3}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + {Credo.Check.Refactor.MapJoin, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.RedundantWithClauseResult, []}, + {Credo.Check.Refactor.RejectReject, []}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.WithClauses, []}, + + # + ## Warnings + # + {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.Dbg, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.StructFieldAmount, []}, + {Credo.Check.Warning.UnsafeExec, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedMapOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + {Credo.Check.Warning.WrongTestFilename, []} + ], + disabled: [ + # @spec annotations use struct literals (%Issue{} etc.) throughout; + # migrating to t() types across all @specs is tracked as a follow-up. + {Credo.Check.Warning.SpecWithStruct, []}, + + # + # Checks scheduled for next check update (opt-in for now) + {Credo.Check.Refactor.UtcNowTruncate, []}, + + # + # Controversial and experimental checks (opt-in, just move the check to `:enabled` + # and be sure to use `mix credo --strict` to see low priority checks) + # + {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, + {Credo.Check.Consistency.UnusedVariableNames, []}, + {Credo.Check.Design.DuplicatedCode, []}, + {Credo.Check.Design.SkipTestWithoutComment, []}, + {Credo.Check.Readability.AliasAs, []}, + {Credo.Check.Readability.BlockPipe, []}, + {Credo.Check.Readability.ImplTrue, []}, + {Credo.Check.Readability.MultiAlias, []}, + {Credo.Check.Readability.NestedFunctionCalls, []}, + {Credo.Check.Readability.OneArityFunctionInPipe, []}, + {Credo.Check.Readability.OnePipePerLine, []}, + {Credo.Check.Readability.SeparateAliasRequire, []}, + {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, + {Credo.Check.Readability.SinglePipe, []}, + {Credo.Check.Readability.Specs, []}, + {Credo.Check.Readability.StrictModuleLayout, []}, + {Credo.Check.Readability.WithCustomTaggedTuple, []}, + {Credo.Check.Refactor.ABCSize, []}, + {Credo.Check.Refactor.AppendSingleItem, []}, + {Credo.Check.Refactor.CondInsteadOfIfElse, []}, + {Credo.Check.Refactor.DoubleBooleanNegation, []}, + {Credo.Check.Refactor.FilterReject, []}, + {Credo.Check.Refactor.IoPuts, []}, + {Credo.Check.Refactor.MapMap, []}, + {Credo.Check.Refactor.ModuleDependencies, []}, + {Credo.Check.Refactor.NegatedIsNil, []}, + {Credo.Check.Refactor.PassAsyncInTestCases, []}, + {Credo.Check.Refactor.PipeChainStart, []}, + {Credo.Check.Refactor.RejectFilter, []}, + {Credo.Check.Refactor.VariableRebinding, []}, + {Credo.Check.Warning.LazyLogging, []}, + {Credo.Check.Warning.LeakyEnvironment, []}, + {Credo.Check.Warning.MapGetUnsafePass, []}, + {Credo.Check.Warning.MixEnv, []}, + {Credo.Check.Warning.UnsafeToAtom, []} + # {Credo.Check.Warning.UnusedOperation, [{MyMagicModule, [:fun1, :fun2]}]} + + # {Credo.Check.Refactor.MapInto, []}, + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + } + ] +} diff --git a/app/lib/linear_cli/favorites.ex b/app/lib/linear_cli/favorites.ex index 201cfc7..762b1c6 100644 --- a/app/lib/linear_cli/favorites.ex +++ b/app/lib/linear_cli/favorites.ex @@ -18,7 +18,7 @@ defmodule LinearCli.Favorites do alias Exqlite.Sqlite3 - @doc "Favorites `value` under `kind` (`\"team\"` or `\"project\"`). A no-op if already favorited." + @doc ~s[Favorites `value` under `kind` (`"team"` or `"project"`). A no-op if already favorited.] @spec add(String.t(), String.t()) :: :ok def add(kind, value) do with_db(fn conn -> diff --git a/app/lib/linear_cli/linear/user.ex b/app/lib/linear_cli/linear/user.ex index 60ad370..6a6dc74 100644 --- a/app/lib/linear_cli/linear/user.ex +++ b/app/lib/linear_cli/linear/user.ex @@ -74,12 +74,15 @@ defmodule LinearCli.Linear.User.Read.ByTeam do document = "query($id: String!) { team(id: $id) { members(first: 50) { nodes { #{User.base_fields()} } } } }" - with {:ok, %{"team" => %{"members" => %{"nodes" => nodes}}}} <- - Api.call(document, %{"id" => team_id}) do - {:ok, Enum.map(nodes, &User.from_map/1)} - else - {:ok, _} -> {:ok, []} - error -> error + case Api.call(document, %{"id" => team_id}) do + {:ok, %{"team" => %{"members" => %{"nodes" => nodes}}}} -> + {:ok, Enum.map(nodes, &User.from_map/1)} + + {:ok, _} -> + {:ok, []} + + error -> + error end end end diff --git a/app/lib/linear_cli/oban_repo.ex b/app/lib/linear_cli/oban_repo.ex index b88439c..532e88e 100644 --- a/app/lib/linear_cli/oban_repo.ex +++ b/app/lib/linear_cli/oban_repo.ex @@ -34,7 +34,7 @@ defmodule LinearCli.ObanRepo do valid other -> - raise "LINEAR_CLI_DB_ADAPTER must be \"sqlite\" or \"postgres\", got: #{inspect(other)}" + raise ~s[LINEAR_CLI_DB_ADAPTER must be "sqlite" or "postgres", got: #{inspect(other)}] end end diff --git a/app/lib/linear_cli/rollover.ex b/app/lib/linear_cli/rollover.ex index 05354c3..73cefca 100644 --- a/app/lib/linear_cli/rollover.ex +++ b/app/lib/linear_cli/rollover.ex @@ -30,7 +30,7 @@ defmodule LinearCli.Rollover do @doc "e.g. month_name(8) == \"August\"" def month_name(month) when month in 1..12, do: Enum.at(@month_names, month - 1) - @doc "e.g. project_name(\"PAYMENTS SWAT\", {2026, 8}) == \"PAYMENTS SWAT August 2026\"" + @doc ~s[e.g. project_name("PAYMENTS SWAT", {2026, 8}) == "PAYMENTS SWAT August 2026"] def project_name(prefix, {year, month}), do: "#{prefix} #{month_name(month)} #{year}" @doc """ diff --git a/app/mix.exs b/app/mix.exs index 8650bd4..4ee1f28 100644 --- a/app/mix.exs +++ b/app/mix.exs @@ -9,6 +9,7 @@ defmodule LinearCli.MixProject do # x-release-please-end elixir: "~> 1.20", start_permanent: Mix.env() == :prod, + elixirc_options: [warnings_as_errors: Mix.env() == :test], deps: deps(), consolidate_protocols: Mix.env() != :dev, usage_rules: usage_rules(), @@ -71,7 +72,9 @@ defmodule LinearCli.MixProject do {:sourceror, "~> 1.8", only: [:dev, :test]}, {:ash, "~> 3.0"}, {:igniter, "~> 0.6", only: [:dev, :test]}, - {:sbom, "~> 0.10", only: :dev, runtime: false} + {:sbom, "~> 0.10", only: :dev, runtime: false}, + {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, + {:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false} # {:dep_from_hexpm, "~> 0.3.0"}, # {:dep_from_git, git: "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/elixir-lang/my_dep.git", tag: "0.1.0"} ] diff --git a/app/mix.lock b/app/mix.lock index 8e5446b..f7c99e8 100644 --- a/app/mix.lock +++ b/app/mix.lock @@ -1,7 +1,9 @@ %{ "ash": {:hex, :ash, "3.31.3", "b36672bff745eadd52265d8b0a303f98ba26a31173f4116ebd2df7be0cbafeff", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c105da2080475d114908c32e70970e69c857a9068a577a4e99dbf2801baa11e"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "burrito": {:hex, :burrito, "1.6.0", "7af0a75f11680e8a6e9c01370c9af51cb9d0e15b3226eddf4f438dbc68570520", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:req, ">= 0.5.0", [hex: :req, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.2.0 or ~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "e636a00b032c45a69ff755d9fc53fa5fdc9e1d21bdbd229075fe4a15b05355fe"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, + "credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"}, "crux": {:hex, :crux, "0.1.4", "1fa21f5ca886d3498f83871a3ef19379b80abf8cfcf2ed32007e80279e714f1e", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "ff7d880cf732d82360aa81e439b8d4831cd30768061c9233f90c97e10162b9c0"}, "db_connection": {:hex, :db_connection, "2.10.2", "ae391e803a5adff104da913c2fc1c0c14a37f8b10001dcef568796e1fb7bf95c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "510b14482330f1af6490a2fa0efd8d4f1435d1529b165647df22ac0f2df0fa93"}, "decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"}, @@ -12,6 +14,7 @@ "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, "ex_ast": {:hex, :ex_ast, "0.13.1", "b3d80ec163733176f63662ac44d2511445c224f6b5e4e3ce01f5eff83c4a5993", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.7", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "bd15f68cde5ec945b859bd67416f26cf5499f1aef9b067eb2163ed244c7e703a"}, "exqlite": {:hex, :exqlite, "0.39.0", "08f8ff646e14d14bab289f4cfc61b95ffc5fe7c82d420049a480eee69ca8b292", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "603de0f7637adc88275fa12ccbd58954ff6000f75386e876565b49032d9aede9"}, + "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "finch": {:hex, :finch, "0.23.0", "e3f9287ac25a8832f848b144c2b57346aac65b205e2e0629a52adfe6507fd837", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "80e58d3f936f57e3fdf404f83a3642897ae6d9fb642934e46da4d8fe761b99d5"}, "glob_ex": {:hex, :glob_ex, "0.1.12", "7b2d9369c20e2697efcfd185d13d6e84c94cd3bfd2730fbde613141c2e015c00", [:mix], [], "hexpm", "2e2fac83f113514434c7eaf267b4c38af2f91766f1cab2c5db7053b7fc1ee0bb"}, "hex_core": {:hex, :hex_core, "0.15.0", "8eadc0ccb08e3742f2313073d04f39eaa7904617329039e9d3c402f5dd227673", [:rebar3], [], "hexpm", "c2093764c7af8ef0818c104fa141eba431e7be93f8374638c45c7037b26a52f8"}, @@ -24,6 +27,7 @@ "mdex_native": {:hex, :mdex_native, "0.2.7", "46203ee9c4fe2c94feef26e4d976235ad022b4770d3d307fa6b445d293b2571f", [:mix], [{:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "05efa68775644ef6be6109f76c1f5f8778ff7e7ed9fe4a03541758158172bc77"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mint": {:hex, :mint, "1.9.3", "3337184d69179695c7a9f1714d92c11e629d36c8c037a21cf490131d3d150554", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5f7c9342480c069dbbc4eeac3490303c9e01870ff01a7f1d29b6107054fc1e74"}, + "mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"}, "multigraph": {:hex, :multigraph, "0.16.1-mg.4", "2bbe149f5411b0e3bf0624c7bf2e3da2738efeac2f9a67bbbcb807ab171f0a76", [:mix], [], "hexpm", "b9f3e2577cef4658eeedf97c76d22a86d33a7aab702a93c1da9c122e849e9037"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, diff --git a/app/test/linear_cli/cli/profile_defaults_test.exs b/app/test/linear_cli/cli/profile_defaults_test.exs index 09f0a55..8dfc1a1 100644 --- a/app/test/linear_cli/cli/profile_defaults_test.exs +++ b/app/test/linear_cli/cli/profile_defaults_test.exs @@ -264,13 +264,11 @@ defmodule LinearCli.CLI.ProfileDefaultsTest do decoded = Jason.decode!(body) query = decoded["query"] - cond do - String.contains?(query, "issues(filter") -> - send(test_pid, {:filter, decoded["variables"]["filter"]}) - Req.Test.json(conn, issues_response([issue_map()])) - - true -> - raise "no stub matched query: #{query}" + if String.contains?(query, "issues(filter") do + send(test_pid, {:filter, decoded["variables"]["filter"]}) + Req.Test.json(conn, issues_response([issue_map()])) + else + raise "no stub matched query: #{query}" end end) @@ -335,13 +333,11 @@ defmodule LinearCli.CLI.ProfileDefaultsTest do decoded = Jason.decode!(body) query = decoded["query"] - cond do - String.contains?(query, "issue(id: $id)") -> - send(test_pid, {:id, decoded["variables"]["id"]}) - Req.Test.json(conn, %{"data" => %{"issue" => issue_map()}}) - - true -> - raise "no stub matched query: #{query}" + if String.contains?(query, "issue(id: $id)") do + send(test_pid, {:id, decoded["variables"]["id"]}) + Req.Test.json(conn, %{"data" => %{"issue" => issue_map()}}) + else + raise "no stub matched query: #{query}" end end) @@ -410,21 +406,19 @@ defmodule LinearCli.CLI.ProfileDefaultsTest do decoded = Jason.decode!(body) query = decoded["query"] - cond do - String.contains?(query, "issue(id: $id)") -> - send(test_pid, {:id, decoded["variables"]["id"]}) + if String.contains?(query, "issue(id: $id)") do + send(test_pid, {:id, decoded["variables"]["id"]}) - Req.Test.json( - conn, - %{ - "data" => %{ - "issue" => issue_map(%{"branchName" => "main", "assignee" => me_map()}) - } + Req.Test.json( + conn, + %{ + "data" => %{ + "issue" => issue_map(%{"branchName" => "main", "assignee" => me_map()}) } - ) - - true -> - raise "no stub matched query: #{query}" + } + ) + else + raise "no stub matched query: #{query}" end end) @@ -452,21 +446,19 @@ defmodule LinearCli.CLI.ProfileDefaultsTest do decoded = Jason.decode!(body) query = decoded["query"] - cond do - String.contains?(query, "issue(id: $id)") -> - send(test_pid, {:id, decoded["variables"]["id"]}) + if String.contains?(query, "issue(id: $id)") do + send(test_pid, {:id, decoded["variables"]["id"]}) - Req.Test.json( - conn, - %{ - "data" => %{ - "issue" => issue_map(%{"branchName" => "main", "assignee" => me_map()}) - } + Req.Test.json( + conn, + %{ + "data" => %{ + "issue" => issue_map(%{"branchName" => "main", "assignee" => me_map()}) } - ) - - true -> - raise "no stub matched query: #{query}" + } + ) + else + raise "no stub matched query: #{query}" end end) diff --git a/lib/mix/tasks/ci.ex b/lib/mix/tasks/ci.ex index 68e8211..3b6dccc 100644 --- a/lib/mix/tasks/ci.ex +++ b/lib/mix/tasks/ci.ex @@ -12,12 +12,14 @@ defmodule Mix.Tasks.Ci do either ever breaks: 1. `mix deps.get` — ensure deps are present - 2. `mix hex.audit` — reject retired or vulnerable dependencies - 3. `mix format --check-formatted` — code is formatted - 4. `mix usage_rules.sync --check` — usage rules are in sync with deps + 2. `mix hex.audit` — reject retired or vulnerable Hex packages + 3. `mix deps.audit` — scan dependencies for known security advisories + 4. `mix format --check-formatted` — code is formatted + 5. `mix credo --strict` — static analysis (style, complexity, common bugs) + 6. `mix usage_rules.sync --check` — usage rules are in sync with deps (catches drift introduced by a dep bump without re-running the sync; see #79) - 5. `mix test` — all tests pass + 7. `mix test` — all tests pass All steps run inside `app/`. """ @@ -35,7 +37,9 @@ defmodule Mix.Tasks.Ci do def run(_argv, shell) do shell.("mix", ["deps.get"], cd: "app") shell.("mix", ["hex.audit"], cd: "app") + shell.("mix", ["deps.audit"], cd: "app") shell.("mix", ["format", "--check-formatted"], cd: "app") + shell.("mix", ["credo", "--strict"], cd: "app") shell.("mix", ["usage_rules.sync", "--check"], cd: "app") shell.("mix", ["test"], cd: "app") :ok