{"id":20332495,"url":"https://github.com/fidr/minex","last_synced_at":"2025-04-11T21:32:59.574Z","repository":{"id":57526793,"uuid":"260202906","full_name":"fidr/minex","owner":"fidr","description":"A deployment helper for Elixir","archived":false,"fork":false,"pushed_at":"2020-04-30T14:02:03.000Z","size":21,"stargazers_count":10,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-25T17:23:23.957Z","etag":null,"topics":["deployment","elixir"],"latest_commit_sha":null,"homepage":null,"language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fidr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-04-30T12:16:01.000Z","updated_at":"2024-07-21T09:43:13.000Z","dependencies_parsed_at":"2022-09-07T02:52:41.313Z","dependency_job_id":null,"html_url":"https://github.com/fidr/minex","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fidr%2Fminex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fidr%2Fminex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fidr%2Fminex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fidr%2Fminex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fidr","download_url":"https://codeload.github.com/fidr/minex/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247999866,"owners_count":21031046,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["deployment","elixir"],"created_at":"2024-11-14T20:26:58.849Z","updated_at":"2025-04-11T21:32:59.535Z","avatar_url":"https://github.com/fidr.png","language":"Elixir","readme":"# Minex\n\nA deployment helper for Elixir.\n\nMinex has no strong opinion on how you should do your deployments. It has no deployment strategies, but allows you to define them youself with a simple syntax. It does provide helpers to easily run commands. Both locally and over SSH (in a single session where possible).\n\nLocal commands are run with an interactive shell by default (using [Erlexec](https://github.com/saleyn/erlexec)). Remote commands are run as `ssh` commands.\n\n## Installation\n\nAdd to your `mix.exs` and run `mix deps.get`:\n\n```elixir\ndef deps do\n  [\n    {:minex, \"~\u003e 0.1.0\", only: :dev}\n  ]\nend\n```\n\nGet started by running:\n\n```\nmix minex.init\n```\n\nThis will create the following files:\n\n- `config/deploy.exs`\n- `config/deploy/tasks.exs`\n\n## Get Started\n\nThe initial `deploy.exs` contains a basic sample of how a deployment can work. You define tasks that can execute commands locally or on a remote server.\n\nExample deploy file:\n\n```elixir\nuse Minex\n\nset(:name, \"my_app\")\nset(:deploy_to, \"/apps/#{get(:name)}\")\nset(:host, \"deploy@1.2.3.4\")\n\nCode.require_file(\"deploy/tasks.exs\", Path.dirname(__ENV__.file))\n\n# expects a config/deploy/Dockerfile\npublic_task(:build, fn -\u003e\n  command(\"docker container rm dummy\", allow_fail: true)\n  command(\"docker build -f config/deploy/Dockerfile  -t #{get(:name)} .\")\n  command(\"docker create -ti --name dummy #{get(:name)}:latest bash\")\n  command(\"docker cp dummy:/release.tar.gz release.tar.gz\")\nend)\n\npublic_task(:deploy, fn -\u003e\n  run(:build)\n  run(:upload, [\"release.tar.gz\", \"#{get(:deploy_to)}/release.tar.gz\"])\n  run(:remote, fn -\u003e\n    command(\"cd #{get(:deploy_to)}\")\n    command(\"( [ -d \\\"bin\\\" ] \u0026\u0026 ./bin/#{get(:name)} stop || true )\")\n    command(\"tar -zxf release.tar.gz -C .\")\n    command(\"./bin/#{get(:name)} daemon\")\n  end)\nend)\n\nMinex.run(System.argv())\n```\n\nA public task can be run with `mix minex task_name`, for example:\n\n```\nmix minex deploy\n```\n\n*Tip:*  in your `mix.exs` you can add aliases for your most used commands. You can for example add `deploy: [\"minex deploy\"]` and then deploy with `mix deploy`.\n\n## Basics\n\n### Tasks and commands\n\nA normal `task` can only be run internally and not from the command line.\n\n```elixir\ntask(:my_task, fn -\u003e\n  command(\"echo a\")\n  command(\"echo b\")\n  command(\"echo c\")\nend)\n\n```\n\nBy default a `command` is run from the local shell. Only if it's included in a `:remote` block, the nested commands will be run over SSH remotely.\n\n```elixir\nrun(:remote, fn -\u003e\n  run(:my_task)\nend)\n```\n\nThe code above will result in the following command being executed:\n\n```\nssh deploy@1.2.3.4 -- $'( echo a \u0026\u0026 echo b \u0026\u0026 echo c )'\n```\n\nIf a command results in a non-zero exit code, the rest of the script is aborted (both locally and remotely).\n\nTasks can accept arguments if needed:\n\n```elixir\ntask(:my_task, fn [arg] -\u003e\n  command(\"echo #{arg}\")\nend)\n\nrun(:my_task, [\"test\"])\n```\n\n#### Minex defines some base tasks:\n\nPublic tasks:\n\n - `:help` - public task to display the help message (list available public tasks). This is als triggered if you run minex without arguments.\n - `:generate_script` - public task to generate a bash script for the tasks defined with `generate_script_task`.\n\nHelper tasks:\n\n - `:command` - this task is triggered for each `command()` you call. Can execute locally or remotely, depending on the context.\n - `:remote` - collect all nested commands and execute them on the `:host` over SSH by chaining them.\n - `:upload` - takes a `[local_path, remote_path]` as argument and uses `:scp` to upload the file to the `:host`\n - `:download` -  takes a `[remote_path, local_path]` as argument and uses `:scp` to download the file from the `:host`\n - `:scp` -  takes a `[source, dest]`, so this should include the host. Used by upload and download internally.\n\n And some internal commands:\n\n - `:remote_command` - run a single command remotely. Used internally by the `:remote` task.\n - `:local_command` - run a single command locally. Used internally if not in remote mode. This will raise on non-zero exit codes.\n - `:remote_exec` - core task to run a command over SSH. this actually triggers a local `command` that executes `ssh` from the shell.\n - `:local_exec` - core task to run a command locally (uses Erlexec)\n\n### Settings\n\nThese settings wills be used by the base tasks:\n\n - `:host` - target server for remote tasks\n - `:ssh_opts` - string with options for the `ssh` command line call\n - `:scp_opts` - string with options for the `scp` command line call\n - `:remote_command_options` - keyword list of commands options the executing the `ssh` commands locally. Default is `[]`\n - `:local_command_options` - keyword list of basic commands options for all local commands. Default is `[interact: true, echo_cmd: true]`\n - `:generate_script_template` - EEx template that renders the bash script for the `generate_script` command.\n\nOther settings can be defined at will for your own use.\n\n## Advanced usage\n\n### Multiple environments\n\nA common usecase is having multiple environments like staging and production.\n\nA way to support that is by first specifying the environment before you execute a task.\n\n```elixir\nuse Minex\n\n# ...\n\nargs =\n  case System.argv() do\n    [\"staging\" | args] -\u003e\n      set(:host, \"your_staging_host\")\n      args\n\n    [\"production\" | args] -\u003e\n      set(:host, \"your_production_host\")\n      args\n\n    [_other | _] -\u003e\n      raise \"please supply an environment as the first argument\"\n\n    [] -\u003e\n      # allow empty to display help\n      []\n  end\n\n# Run with the rest of the args\nMinex.run(args)\n```\n\n### Build on a seperate build server\n\nIf you want to run your commands on a seperate build server, you can either change your host to the build server and change it back to the target server later or you can create a specific task to temporarily change the settings:\n\n```elixir\ntask(:build_server, fn [fun] -\u003e\n  # set always returns the old settings of the keys you set\n  previous_settings =\n    set(\n      host: get(:build_server_host),\n      ssh_opts: \"-i ~/.ssh/build_key\"\n    )\n\n  run(:remote, fn -\u003e\n    fun.()\n  end)\n\n  # reset\n  set(previous_settings)\nend)\n\nrun(:build_server, fn -\u003e\n  # ...\nend)\n```\n\n### Use a single SSH connection\n\nTo re-use the same connection you can set up a control master. You can do this by specifiying it in your `~/.ssh/config` or by starting it in the task:\n\n```elixir\n# Build in settings that are used in the scp/ssh task\nset(:ssh_opts, ~s[-o ControlPath=\".ssh-control.%h\"])\nset(:scp_opts, ~s[-o ControlPath=\".ssh-control.%h\"])\n\n# Start SSH connection in master mode and detach the first time data is received\ntask(:start_ssh, fn -\u003e\n  command(\"ssh #{get(:ssh_opts)} -TM #{get(:host)}\", on_receive: fn _, _ -\u003e :detach end, interact: false)\nend)\n\ntask(:deploy, fn -\u003e\n  run(:build)\n\n  run(:start_ssh)\n\n  # ...\nend)\n```\n\n### Overriding tasks\n\nIt's possible to override the build-in (or your own) tasks:\n\n```elixir\ntask(:remote_exec, [override: true], fn [command, options] -\u003e\n  # your own implementation\nend)\n```\n\n### Generate script tasks\n\nThe `generate_script_task` creates tasks that will be exported to a bash script. This is because for some tasks (like a remote console) you need a full shell/pty and this is not easy to start from the beam.\n\n```elixir\ngenerate_script_task(:iex, fn -\u003e\n  run(:remote, fn -\u003e\n    command(\"cd #{get(:deploy_to)} \u0026\u0026 ./bin/#{get(:name)} remote\")\n  end)\nend)\n```\n\nGenerate the bash script to a file of your choosing:\n```\nmix minex generate_script my_script\n\n./my_script iex\n```\n\nIf you have multiple environments configured:\n```\nmix minex production generate_script script/production\nmix minex staging generate_script script/staging\n\n./script/production iex\n```\n\nNote: to create both public and generate_script tasks, you can define them like this:\n\n```elixir\npublic_task(:name, [generate_script: true], fn -\u003e\n  #...\nend)\n```\n\n### Use a PTY\n\nSome (interactive) commands need a PTY to be present. The enable this:\n\n```\nset(:remote_command_options, [pty: true])\nset(:ssh_opts, \"-t\")\n```\n\n### Entering passwords\n\nEntering passwords works, but they will be visible in the terminal. The easiest way around this would be `generate_script` tasks and then actually executing the script from the shell.\n\nThe other option is grabbing the password with some helper that clears the terminal when typing and then using the `on_receive` option to send the password when needed.\n\n### Semi interactive commands\n\nIt's possible to automatically respond to promps by specifying a custom handler in your command. This can be abused to automatically login for example (use at your own risk):\n\n```elixir\npublic_task(:login_and_ls, fn -\u003e\n  on_receive = fn pid, %{buffer: [last | _]} = state -\u003e\n    cond do\n      last =~ ~r/Permission denied/ -\u003e\n        raise(\"Password failed\")\n\n      last =~ ~r/password: $/ -\u003e\n        Minex.Command.send_input(pid, \"your_password\\n\")\n        {:cont, state}\n\n      true -\u003e\n        {:cont, state}\n    end\n  end\n\n  set(:host, \"user_with_password@1.2.3.4\")\n  set(:remote_command_options, [on_receive: on_receive, pty: true])\n\n  run(:remote, fn -\u003e\n    command(\"ls\")\n  end)\nend)\n```\n\n### Dry run\n\nOutput the commands that would be executed otherwise:\n\n```elixir\ntask(:dry_run, fn [fun] -\u003e\n  IO.puts \"# Dry run:\"\n  collect(fun)\n  |\u003e Enum.join(\"\\n\")\n  |\u003e IO.puts()\nend)\n\ncase System.argv() do\n  [\"dry_run\" | args] -\u003e\n    run(:dry_run, fn -\u003e\n      Minex.run(args)\n    end)\n\n  args -\u003e\n    Minex.run(args)\nend\n```\n\n### Local environment variables\n\nSet environment variables for local commands:\n\nInline:\n```elixir\ncommand(\"VAR=val; echo $VAR\")\n```\n\nPer command:\n```elixir\ncommand(\"echo $VAR\", env: %{\"VAR\" =\u003e \"val\"})\n```\n\nFor all commands:\n```elixir\nset(:local_command_options, [interact: true, echo_cmd: true, env: %{\"VAR\" =\u003e \"val\"}])\ncommand(\"echo $VAR\")\n```\n\n## Acknowledgements\n\nInspired by the Ruby gem [Mina](https://github.com/mina-deploy/mina) and by [Bootleg](https://github.com/labzero/bootleg)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffidr%2Fminex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffidr%2Fminex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffidr%2Fminex/lists"}