Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
*.swp
*.swo
*~

# Root-level repo-management Mix project (mix.exs/lib/ at repo root -
# see RepoTasks.MixProject) - app/'s own equivalents are in app/.gitignore.
/_build/
/deps/
57 changes: 57 additions & 0 deletions lib/mix/tasks/container.build.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
defmodule Mix.Tasks.Container.Build do
@shortdoc "Builds the linux_x86_64 Burrito binary, then the container image"

@moduledoc """
#{@shortdoc}.

mix container.build TAG [--push]

Builds `app/`'s `linux_x86_64` Burrito binary (the container's payload -
`MIX_ENV=prod BURRITO_TARGET=linux_x86_64 mix release lc --overwrite` -
`--overwrite` so re-running this locally for a version already built
doesn't block on `mix release`'s own interactive prompt), then the
container image itself via `ci/build_image.sh`. With `--push` (`-p`),
chains straight into `mix container.publish TAG` afterward.

This is the exact flow `.github/workflows/main.yaml`'s `container` job
runs - CI and a local `mix container.build v1.2.3 --push` do the
identical thing, so there's one place to fix if either ever breaks.
Comment on lines +16 to +18
"""

use Mix.Task

alias RepoTasks.Shell

@impl Mix.Task
def run(argv) do
{opts, args} =
OptionParser.parse!(argv, strict: [push: :boolean], aliases: [p: :push])

tag =
case args do
[tag] -> tag
_ -> Mix.raise("Usage: mix container.build TAG [--push]")
end

Mix.shell().info("==> Fetching app/ deps")
Shell.run!("mix", ["deps.get"], cd: "app")

Mix.shell().info("==> Building the linux_x86_64 Burrito binary")

Shell.run!(
"mix",
["release", "lc", "--overwrite"],
cd: "app",
env: [{"MIX_ENV", "prod"}, {"BURRITO_TARGET", "linux_x86_64"}]
)

Mix.shell().info("==> Building the container image (#{tag})")
Shell.run!("./ci/build_image.sh", [tag], env: [{"APP_VERSION", tag}])

if opts[:push] do
Mix.Task.run("container.publish", [tag])
end

:ok
end
end
32 changes: 32 additions & 0 deletions lib/mix/tasks/container.publish.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule Mix.Tasks.Container.Publish do
@shortdoc "Publishes an already-built local container image to the registry"

@moduledoc """
#{@shortdoc}.

mix container.publish TAG

Thin wrapper around `ci/publish.sh` - the image must already be built
locally under the same TAG (`mix container.build TAG`, or
`ci/build_image.sh` directly). Needs `REGISTRY_TOKEN` or `GITHUB_TOKEN`
set to log in to the registry - `ci/publish.sh`'s own requirement,
unchanged here.
"""

use Mix.Task

alias RepoTasks.Shell

@impl Mix.Task
def run(argv) do
case argv do
[tag] ->
Mix.shell().info("==> Publishing #{tag}")
Shell.run!("./ci/publish.sh", [tag])
:ok

_ ->
Mix.raise("Usage: mix container.publish TAG")
end
end
end
46 changes: 46 additions & 0 deletions lib/repo_tasks/shell.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
defmodule RepoTasks.Shell do
@moduledoc """
Runs an external command (a `mix release` inside `app/`, or one of the
`ci/*.sh` scripts) with its output streamed live to stdout as it's
produced, raising via `Mix.raise/1` on a non-zero exit - a clean error
message and exit code, not a stacktrace, matching how every other Mix
task failure in this codebase behaves.

Every repo-management task built on top of this shells out rather than
running anything in-process - see `RepoTasks.MixProject`'s own
moduledoc for why.
Comment on lines +9 to +11
"""

@doc """
Runs `cmd` with `args`, streaming combined stdout/stderr live.

`opts` forwards to `System.cmd/3` (e.g. `cd:`, `env:`) - `:into` and
`:stderr_to_stdout` are already set here and can't be overridden, since
live streaming is this function's whole point.
"""
@spec run!(String.t(), [String.t()], keyword()) :: :ok
def run!(cmd, args, opts \\ []) do
# System.cmd/3 only resolves a bare name (e.g. "mix") via PATH lookup -
# a relative script path like "./ci/build_image.sh" isn't in PATH nor
# absolute, so it raises :enoent (verified directly - this isn't a
# shell, "./" isn't special to it the way it is to bash). Expand any
# path-shaped cmd (contains "/") to absolute, relative to this
# process's cwd; bare command names are left alone for PATH lookup.
cmd = if String.contains?(cmd, "/"), do: Path.expand(cmd), else: cmd

Mix.shell().info("+ #{cmd} #{Enum.join(args, " ")}")

cmd_opts =
opts
|> Keyword.put(:into, IO.stream(:stdio, :line))
|> Keyword.put(:stderr_to_stdout, true)

{_io, status} = System.cmd(cmd, args, cmd_opts)

if status != 0 do
Mix.raise("#{cmd} #{Enum.join(args, " ")} exited with status #{status}")
end

:ok
end
end
31 changes: 31 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
defmodule RepoTasks.MixProject do
use Mix.Project

# Repo-management tasks (mix container.build/publish, and more to come) -
# deliberately its own Mix project, sibling to app/, not nested inside it.
# app/ is the CLI itself; this is tooling that operates ON the repo as a
# whole (this file's own directory, plus ci/, oci/, .github/workflows/ -
# things app/'s own mix.exs has no business knowing about). Kept
# dependency-free on purpose: every task here just orchestrates other
# already-existing tools (mix release inside app/, the ci/*.sh scripts)
# via System.cmd/3, never runs anything in-process.
def project do
[
app: :repo_tasks,
version: "0.1.0",
elixir: "~> 1.20",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end

def application do
[
extra_applications: [:logger]
]
end

defp deps do
[]
end
end
15 changes: 15 additions & 0 deletions test/mix/tasks/container.build_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule Mix.Tasks.Container.BuildTest do
use ExUnit.Case, async: true

test "requires a TAG argument" do
assert_raise Mix.Error, "Usage: mix container.build TAG [--push]", fn ->
Mix.Tasks.Container.Build.run([])
end
end

test "rejects more than one positional argument" do
assert_raise Mix.Error, "Usage: mix container.build TAG [--push]", fn ->
Mix.Tasks.Container.Build.run(["v1.0.0", "extra"])
end
end
end
15 changes: 15 additions & 0 deletions test/mix/tasks/container.publish_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule Mix.Tasks.Container.PublishTest do
use ExUnit.Case, async: true

test "requires a TAG argument" do
assert_raise Mix.Error, "Usage: mix container.publish TAG", fn ->
Mix.Tasks.Container.Publish.run([])
end
end

test "rejects more than one positional argument" do
assert_raise Mix.Error, "Usage: mix container.publish TAG", fn ->
Mix.Tasks.Container.Publish.run(["v1.0.0", "extra"])
end
end
end
1 change: 1 addition & 0 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ExUnit.start()