tinfoil

Distribution automation for Burrito-based Elixir CLIs.

Be to Burrito what cargo-dist is to Cargo: a single tool that takes your mix release output to platform binaries in a GitHub Release, with Homebrew and installer support, in under 30 minutes of setup.

Status: v0.1 — generate-and-forget. mix tinfoil.init scaffolds a self-contained GitHub Actions workflow. Later versions will evolve the workflow to call mix tinfoil.* tasks directly, the way cargo-dist does.

The problem

Burrito solves binary packaging. Nobody has solved what comes after: CI matrix builds, GitHub Releases, checksums, Homebrew formulas, installer scripts. Every team shipping a Burrito-based CLI (Next LS, lazyasdf, etc.) hand-rolls the same pipeline. tinfoil is that pipeline, as a Hex package.

Installation

Add tinfoil to your dev dependencies alongside Burrito:

def deps do
  [
    {:burrito, "~> 1.0"},
    {:tinfoil, "~> 0.1", only: :dev, runtime: false}
  ]
end

Then add a :tinfoil key to project/0 in mix.exs:

def project do
  [
    app: :my_cli,
    version: "0.1.0",
    # ... standard project config ...
    tinfoil: [
      targets: [:darwin_arm64, :darwin_x86_64, :linux_x86_64, :linux_arm64],
      homebrew: [
        enabled: true,
        tap: "owner/homebrew-tap"
      ],
      installer: [
        enabled: true
      ]
    ]
  ]
end

Then run:

mix deps.get
mix tinfoil.init

That generates .github/workflows/release.yml and any enabled extras (installer, Homebrew formula template, tap update script). Commit the generated files, push a tag like v0.1.0, and watch the workflow build and publish platform binaries to a GitHub Release.

What you get

your-project/
├── .github/workflows/release.yml    ← CI pipeline (always)
├── .tinfoil/formula.rb.eex          ← if homebrew enabled
├── scripts/
│   ├── install.sh                   ← if installer enabled
│   └── update-homebrew.sh           ← if homebrew enabled
└── mix.exs

The workflow builds for every configured target in parallel, packages each binary into a .tar.gz with a SHA256 sidecar, creates a GitHub Release, and (optionally) pushes an updated Homebrew formula to your tap.

Tasks

Task Description
mix tinfoil.init Interactive scaffold — writes config guidance and generates the workflow.
mix tinfoil.generate Regenerate the workflow and scripts from the current config. Run after editing :tinfoil in mix.exs or upgrading tinfoil.
mix tinfoil.plan Show what would be built and released. Supports --format human (default), --format json, and --format matrix for GitHub Actions consumption.

v0.2+ will add mix tinfoil.build and mix tinfoil.publish, and evolve the generated workflow to call these tasks directly.

mix tinfoil.plan

Read-only preview of the release plan:

$ mix tinfoil.plan
tinfoil plan for my_cli 1.2.3

  target         runner            archive
  ─────────────  ────────────────  ──────────────────────────────────────────────
  darwin_arm64   macos-latest      my_cli-1.2.3-aarch64-apple-darwin.tar.gz
  darwin_x86_64  macos-13          my_cli-1.2.3-x86_64-apple-darwin.tar.gz
  linux_x86_64   ubuntu-latest     my_cli-1.2.3-x86_64-unknown-linux-musl.tar.gz
  linux_arm64    ubuntu-24.04-arm  my_cli-1.2.3-aarch64-unknown-linux-musl.tar.gz

  format:    tar_gz (sha256)
  github:    owner/my_cli (draft: false)
  homebrew:  disabled
  installer: disabled

For CI consumption, --format matrix emits a compact GitHub Actions matrix fragment:

- id: plan
  run: echo "matrix=$(mix tinfoil.plan --format matrix)" >> "$GITHUB_OUTPUT"

build:
  needs: plan
  strategy:
    matrix: ${{ fromJson(needs.plan.outputs.matrix) }}

Supported targets

Target Triple GitHub runner
:darwin_arm64aarch64-apple-darwinmacos-latest
:darwin_x86_64x86_64-apple-darwinmacos-13
:linux_x86_64x86_64-unknown-linux-muslubuntu-latest
:linux_arm64aarch64-unknown-linux-muslubuntu-24.04-arm

Triples follow the standard Rust-style convention since that is what users expect to see in release asset names.

Windows support is deferred until Burrito's Windows story stabilizes.

Configuration reference

tinfoil: [
  # Required. Targets to build.
  targets: [:darwin_arm64, :linux_x86_64],

  # Archive naming template. Interpolations: {app}, {version}, {target}.
  archive_name: "{app}-{version}-{target}",
  archive_format: :tar_gz,

  # GitHub Release configuration. repo is inferred from `git remote get-url
  # origin` if omitted.
  github: [
    repo: "owner/my_cli",
    draft: false
  ],

  # Homebrew formula generation. Requires a HOMEBREW_TAP_TOKEN secret with
  # repo access to the tap.
  homebrew: [
    enabled: true,
    tap: "owner/homebrew-tap",
    formula_name: "my_cli"  # defaults to the app name
  ],

  # Shell installer script.
  installer: [
    enabled: true,
    install_dir: "~/.local/bin"
  ],

  checksums: :sha256,

  ci: [
    provider: :github_actions,
    # elixir_version is auto-detected from the project's :elixir
    # requirement if not explicitly set; these are the current fallbacks.
    elixir_version: "1.19",
    otp_version: "28",
    zig_version: "0.15.2"
  ]
]

The only required key is :targets. Everything else has a sensible default.

How it compares

License

MIT.