From a710085a0c6ca903973f1ab05476245304bef769 Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Tue, 22 Sep 2026 17:02:09 -0400 Subject: [PATCH 1/2] chore(build)!: align toolchains and harden dependency updates --- .github/workflows/ci.yaml | 8 +-- .github/workflows/main.yaml | 8 +-- .github/workflows/usage-rules-sync.yaml | 4 +- lib/mix/tasks/appdeps.update.ex | 5 +- lib/mix/tasks/ci.ex | 1 + lib/mix/tasks/toolchain.check.ex | 70 +++++++++++++++++++++++++ mise.toml | 12 ++++- test/mix/tasks/appdeps.update_test.exs | 3 +- test/mix/tasks/toolchain.check_test.exs | 45 ++++++++++++++++ vendor/stokowski | 2 +- workflow.opus.yaml | 1 + 11 files changed, 144 insertions(+), 15 deletions(-) create mode 100644 lib/mix/tasks/toolchain.check.ex create mode 100644 test/mix/tasks/toolchain.check_test.exs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5ce0de8..9ecd405 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -63,8 +63,8 @@ jobs: - uses: erlef/setup-beam@v1 with: - otp-version: "29.0.3" - elixir-version: "1.20.3" + otp-version: "29.1" + elixir-version: "1.20.4" # Without this, setup-beam's problem matchers promote every # compiler warning from deps (e.g. postgrex/rewrite's deprecated # `xref: [exclude: ...]`, yamerl's deprecated `catch ...` syntax - @@ -168,8 +168,8 @@ jobs: - uses: erlef/setup-beam@v1 with: - otp-version: "29.0.3" - elixir-version: "1.20.3" + otp-version: "29.1" + elixir-version: "1.20.4" disable_problem_matchers: true - uses: mlugg/setup-zig@v2.2.1 diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 34dfbff..96c8f12 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -106,8 +106,8 @@ jobs: - uses: erlef/setup-beam@v1 with: - otp-version: "29.0.3" - elixir-version: "1.20.3" + otp-version: "29.1" + elixir-version: "1.20.4" # Without this, setup-beam's problem matchers promote every # compiler warning from deps (e.g. postgrex/rewrite's deprecated # `xref: [exclude: ...]`, yamerl's deprecated `catch ...` syntax - @@ -230,8 +230,8 @@ jobs: # setup-zig/p7zip here. uses: erlef/setup-beam@v1 with: - otp-version: "29.0.3" - elixir-version: "1.20.3" + otp-version: "29.1" + elixir-version: "1.20.4" disable_problem_matchers: true - run: mix deps.get diff --git a/.github/workflows/usage-rules-sync.yaml b/.github/workflows/usage-rules-sync.yaml index 695aefb..0fa3e31 100644 --- a/.github/workflows/usage-rules-sync.yaml +++ b/.github/workflows/usage-rules-sync.yaml @@ -23,8 +23,8 @@ jobs: - uses: erlef/setup-beam@v1 with: - otp-version: "29.0.3" - elixir-version: "1.20.3" + otp-version: "29.1" + elixir-version: "1.20.4" disable_problem_matchers: true - run: mix deps.get diff --git a/lib/mix/tasks/appdeps.update.ex b/lib/mix/tasks/appdeps.update.ex index c3d920f..cf68f43 100644 --- a/lib/mix/tasks/appdeps.update.ex +++ b/lib/mix/tasks/appdeps.update.ex @@ -6,7 +6,8 @@ defmodule Mix.Tasks.Appdeps.Update do mix appdeps.update DEP [DEP ...] - Runs `mix deps.update` for the named dependencies from the `app/` project. + Checks that CI uses mise's pinned toolchain, runs `mix deps.update` for the + named dependencies from the `app/` project, then runs the full `mix ci` gate. """ use Mix.Task @@ -24,7 +25,9 @@ defmodule Mix.Tasks.Appdeps.Update do end def run(dependencies, shell) do + Mix.Tasks.Toolchain.Check.run([]) shell.("mix", ["deps.update" | dependencies], cd: "app") + Mix.Tasks.Ci.run([], shell) :ok end end diff --git a/lib/mix/tasks/ci.ex b/lib/mix/tasks/ci.ex index 75f0cf8..4129d7e 100644 --- a/lib/mix/tasks/ci.ex +++ b/lib/mix/tasks/ci.ex @@ -54,6 +54,7 @@ defmodule Mix.Tasks.Ci do @doc false def run([], shell) do + Mix.Tasks.Toolchain.Check.run([]) shell.("mix", ["deps.get"], cd: "app") shell.("./ci/validate_pull_request_title.sh", [], []) shell.("./ci/validate_commit_range.sh", [], []) diff --git a/lib/mix/tasks/toolchain.check.ex b/lib/mix/tasks/toolchain.check.ex new file mode 100644 index 0000000..f5b6fee --- /dev/null +++ b/lib/mix/tasks/toolchain.check.ex @@ -0,0 +1,70 @@ +defmodule Mix.Tasks.Toolchain.Check do + @shortdoc "Checks that CI and mise use the same OTP and Elixir versions" + + @moduledoc """ + #{@shortdoc}. + + mix toolchain.check + + `mise.toml` is the local toolchain source of truth. This task rejects a + workflow that configures erlef/setup-beam with different OTP or Elixir pins. + """ + + use Mix.Task + + @impl Mix.Task + def run([]) do + workflows = + ".github/workflows/*.yaml" + |> Path.wildcard() + |> Enum.map(&{&1, File.read!(&1)}) + + case validate(File.read!("mise.toml"), workflows) do + :ok -> :ok + {:error, message} -> Mix.raise(message) + end + end + + def run(_argv), do: Mix.raise("Usage: mix toolchain.check") + + @doc false + def validate(mise_toml, workflows) do + expected = %{otp: pin!(mise_toml, "erlang"), elixir: pin!(mise_toml, "elixir")} + + mismatches = + Enum.flat_map(workflows, fn {path, workflow} -> + Enum.flat_map(setup_beam_steps(workflow), fn step -> + Enum.flat_map(expected, fn {name, expected_pin} -> + case Map.get(workflow_pins(step), name) do + ^expected_pin -> [] + nil -> ["#{path}: #{name} missing (expected #{expected_pin})"] + pin -> ["#{path}: #{name} #{pin} (expected #{expected_pin})"] + end + end) + end) + end) + + case mismatches do + [] -> :ok + _ -> {:error, "Toolchain pins differ from mise.toml:\n #{Enum.join(mismatches, "\n ")}"} + end + end + + defp pin!(contents, name) do + case Regex.run(~r/^#{name}\s*=\s*"([^"]+)"$/m, contents) do + [_, pin] -> pin + nil -> Mix.raise("mise.toml does not pin #{name}") + end + end + + defp workflow_pins(workflow) do + Regex.scan(~r/^\s*(otp|elixir)-version:\s*"([^"]+)"$/m, workflow) + |> Map.new(fn [_, name, pin] -> {if(name == "otp", do: :otp, else: :elixir), pin} end) + end + + defp setup_beam_steps(workflow) do + workflow + |> String.split(~r/\n(?=\s*-\s*$)/m) + |> Enum.filter(&String.contains?(&1, "uses: erlef/setup-beam@v1")) + end +end diff --git a/mise.toml b/mise.toml index b29c54e..3741248 100644 --- a/mise.toml +++ b/mise.toml @@ -4,8 +4,8 @@ # floating on "latest" (as this used to) drifts local builds away from what # Burrito can actually fetch, breaking `mix release` locally while CI still # works. See documents/phase-8-plan.adoc. -erlang = "29.0.3" -elixir = "1.20.3" +erlang = "29.1" +elixir = "1.20.4" python = "3.14.1" [env] #ANTHROPIC_BASE_URL = "http://127.0.0.1:13305" @@ -18,6 +18,14 @@ python = "3.14.1" #CLAUDE_CODE_ATTRIBUTION_HEADER = 0 #CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = 1 +[tasks.setup] +description = "Set up the repository" +run = "mix setup" + +[tasks.deps] +description = "Fetch CLI dependencies" +run = "cd app && mix deps.get" + [hooks] # Keep a freshly cloned checkout protected without rerunning setup after it is installed. enter = ''' diff --git a/test/mix/tasks/appdeps.update_test.exs b/test/mix/tasks/appdeps.update_test.exs index 217f29b..bccf008 100644 --- a/test/mix/tasks/appdeps.update_test.exs +++ b/test/mix/tasks/appdeps.update_test.exs @@ -3,7 +3,7 @@ defmodule Mix.Tasks.Appdeps.UpdateTest do alias Mix.Tasks.Appdeps.Update - test "updates the named dependencies from app" do + test "updates the named dependencies from app, then runs the CI gate" do caller = self() shell = fn cmd, args, opts -> @@ -13,6 +13,7 @@ defmodule Mix.Tasks.Appdeps.UpdateTest do assert :ok = Update.run(["ash", "oban"], shell) assert_received {:run, "mix", ["deps.update", "ash", "oban"], [cd: "app"]} + assert_received {:run, "mix", ["deps.audit"], [cd: "app"]} end test "requires at least one dependency" do diff --git a/test/mix/tasks/toolchain.check_test.exs b/test/mix/tasks/toolchain.check_test.exs new file mode 100644 index 0000000..02e7c94 --- /dev/null +++ b/test/mix/tasks/toolchain.check_test.exs @@ -0,0 +1,45 @@ +defmodule Mix.Tasks.Toolchain.CheckTest do + use ExUnit.Case, async: true + + alias Mix.Tasks.Toolchain.Check + + @mise """ + [tools] + erlang = "29.1" + elixir = "1.20.4" + """ + + test "accepts matching workflow pins" do + assert :ok = + Check.validate(@mise, [ + {".github/workflows/ci.yaml", workflow("29.1", "1.20.4")} + ]) + end + + test "reports every workflow pin that differs from mise" do + assert {:error, message} = + Check.validate(@mise, [ + {".github/workflows/ci.yaml", workflow("29.0.3", "1.20.3")} + ]) + + assert message =~ "ci.yaml: otp 29.0.3 (expected 29.1)" + assert message =~ "ci.yaml: elixir 1.20.3 (expected 1.20.4)" + end + + test "rejects a setup-beam step without both pins" do + assert {:error, message} = + Check.validate(@mise, [ + {".github/workflows/ci.yaml", "uses: erlef/setup-beam@v1\notp-version: \"29.1\""} + ]) + + assert message =~ "ci.yaml: elixir missing (expected 1.20.4)" + end + + defp workflow(otp, elixir) do + """ + uses: erlef/setup-beam@v1 + otp-version: "#{otp}" + elixir-version: "#{elixir}" + """ + end +end diff --git a/vendor/stokowski b/vendor/stokowski index 2a43887..79fa1bb 160000 --- a/vendor/stokowski +++ b/vendor/stokowski @@ -1 +1 @@ -Subproject commit 2a43887511e8b3d16559e84c6e8626cef43646de +Subproject commit 79fa1bb032fdb4517a28a2f6dcf074ff4571829e diff --git a/workflow.opus.yaml b/workflow.opus.yaml index dcefed7..1139ddf 100644 --- a/workflow.opus.yaml +++ b/workflow.opus.yaml @@ -116,6 +116,7 @@ hooks: after_create: | git clone --depth 1 --recursive git@github.com:rubyists/linear-cli . mix setup + cd app && mix deps.get before_run: | set -eu git fetch origin main From b81a8a4e2460195c51aa187fc4069fa6a802e3b2 Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Tue, 22 Sep 2026 17:28:54 -0400 Subject: [PATCH 2/2] fix(build): pin Burrito-compatible OTP toolchain --- .github/workflows/ci.yaml | 8 ++++---- .github/workflows/main.yaml | 8 ++++---- .github/workflows/usage-rules-sync.yaml | 4 ++-- mise.toml | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9ecd405..5ce0de8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -63,8 +63,8 @@ jobs: - uses: erlef/setup-beam@v1 with: - otp-version: "29.1" - elixir-version: "1.20.4" + otp-version: "29.0.3" + elixir-version: "1.20.3" # Without this, setup-beam's problem matchers promote every # compiler warning from deps (e.g. postgrex/rewrite's deprecated # `xref: [exclude: ...]`, yamerl's deprecated `catch ...` syntax - @@ -168,8 +168,8 @@ jobs: - uses: erlef/setup-beam@v1 with: - otp-version: "29.1" - elixir-version: "1.20.4" + otp-version: "29.0.3" + elixir-version: "1.20.3" disable_problem_matchers: true - uses: mlugg/setup-zig@v2.2.1 diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 96c8f12..34dfbff 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -106,8 +106,8 @@ jobs: - uses: erlef/setup-beam@v1 with: - otp-version: "29.1" - elixir-version: "1.20.4" + otp-version: "29.0.3" + elixir-version: "1.20.3" # Without this, setup-beam's problem matchers promote every # compiler warning from deps (e.g. postgrex/rewrite's deprecated # `xref: [exclude: ...]`, yamerl's deprecated `catch ...` syntax - @@ -230,8 +230,8 @@ jobs: # setup-zig/p7zip here. uses: erlef/setup-beam@v1 with: - otp-version: "29.1" - elixir-version: "1.20.4" + otp-version: "29.0.3" + elixir-version: "1.20.3" disable_problem_matchers: true - run: mix deps.get diff --git a/.github/workflows/usage-rules-sync.yaml b/.github/workflows/usage-rules-sync.yaml index 0fa3e31..695aefb 100644 --- a/.github/workflows/usage-rules-sync.yaml +++ b/.github/workflows/usage-rules-sync.yaml @@ -23,8 +23,8 @@ jobs: - uses: erlef/setup-beam@v1 with: - otp-version: "29.1" - elixir-version: "1.20.4" + otp-version: "29.0.3" + elixir-version: "1.20.3" disable_problem_matchers: true - run: mix deps.get diff --git a/mise.toml b/mise.toml index 3741248..5dedb63 100644 --- a/mise.toml +++ b/mise.toml @@ -4,8 +4,8 @@ # floating on "latest" (as this used to) drifts local builds away from what # Burrito can actually fetch, breaking `mix release` locally while CI still # works. See documents/phase-8-plan.adoc. -erlang = "29.1" -elixir = "1.20.4" +erlang = "29.0.3" +elixir = "1.20.3" python = "3.14.1" [env] #ANTHROPIC_BASE_URL = "http://127.0.0.1:13305"