├── .formatter.exs ├── .gitignore ├── .shipit.exs ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── lib └── mix │ └── tasks │ └── shipit.ex ├── mix.exs └── mix.lock /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | -------------------------------------------------------------------------------- /.shipit.exs: -------------------------------------------------------------------------------- 1 | # check archive url 2 | "v" <> version = version 3 | unless File.read!("README.md") |> String.contains?("shipit-#{version}.ez") do 4 | Mix.raise "archive url is missing version #{version}" 5 | end 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.2.3 (2017-10-13) 4 | 5 | * Add support for custom `.shipit.exs` 6 | 7 | ## v0.2.2 (2017-07-07) 8 | 9 | * Allow LICENSE.md and LICENSE 10 | 11 | ## v0.2.1 (2017-06-26) 12 | 13 | * Perform remote branch check later as it's slow 14 | * Check that local branch tracks remote branch 15 | 16 | ## v0.2.0 (2017-06-26) 17 | 18 | * Rename to ShipIt 19 | * Ensure there are no uncommited changes in the working tree 20 | * Ensure current branch matches the given branch 21 | * Ensure local branch is in sync with remote branch 22 | 23 | ## v0.1.0 (2017-04-12) 24 | 25 | * Initial release 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2017 Wojciech Mach 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ShipIt 2 | 3 | ShipIt automates Hex package publishing to avoid common mistakes. 4 | 5 | It automates these steps: 6 | 7 | * ensure there are no uncommited changes in the working tree 8 | * ensure current branch matches the given branch 9 | * ensure local branch is in sync with remote branch 10 | * ensure project version in mix.exs matches the given version 11 | * ensure CHANGELOG.md contains an entry for the version 12 | * ensure LICENSE.md file is present 13 | * create a git tag and push it 14 | * publish to Hex.pm and HexDocs.pm 15 | 16 | ## Usage 17 | 18 | $ mix shipit master 1.0.0 19 | 20 | For more information, see: 21 | 22 | $ mix help shipit 23 | 24 | ## Installation 25 | 26 | On Elixir v1.4+: 27 | 28 | $ mix archive.install hex shipit 29 | 30 | On Elixir v1.3: 31 | 32 | $ curl -L -O https://github.com/wojtekmach/shipit/releases/download/v0.2.3/shipit-0.2.3.ez 33 | $ mix archive.install shipit-0.2.3.ez 34 | 35 | ## License 36 | 37 | ShipIt is released under the MIT license, see [LICENSE.md](LICENSE.md). 38 | -------------------------------------------------------------------------------- /lib/mix/tasks/shipit.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Shipit do 2 | use Mix.Task 3 | 4 | @shortdoc "Publishes new Hex package version" 5 | 6 | @moduledoc """ 7 | ShipIt automates Hex package publishing to avoid common mistakes. 8 | 9 | mix shipit BRANCH VERSION 10 | 11 | It automates these steps: 12 | 13 | * ensure there are no uncommited changes in the working tree 14 | * ensure current branch matches the given branch 15 | * ensure local branch is in sync with remote branch 16 | * ensure project version in mix.exs matches the given version 17 | * ensure CHANGELOG.md contains an entry for the version 18 | * ensure LICENSE.md file is present 19 | * create a git tag and push it 20 | * publish to Hex.pm and HexDocs.pm 21 | 22 | A `--dry-run` option might be given to only perform local checks. 23 | """ 24 | 25 | @changelog "CHANGELOG.md" 26 | @licenses ["LICENSE.md", "LICENSE"] 27 | 28 | @switches [dry_run: :boolean] 29 | 30 | def run(args) do 31 | case OptionParser.parse(args, strict: @switches) do 32 | {opts, [branch, version], []} -> 33 | project = Mix.Project.config() 34 | version = normalize_version(project, version) 35 | 36 | check_working_tree() 37 | check_branch(branch) 38 | check_changelog(version) 39 | check_license() 40 | check_dot_shipit(branch, version) 41 | check_remote_branch(branch) 42 | 43 | unless opts[:dry_run] do 44 | publish() 45 | create_and_push_tag(version) 46 | end 47 | 48 | _ -> 49 | Mix.raise("Usage: mix shipit BRANCH VERSION [--dry-run]") 50 | end 51 | end 52 | 53 | defp check_working_tree() do 54 | {out, 0} = System.cmd("git", ["status", "--porcelain"]) 55 | 56 | if out != "" do 57 | Mix.raise("Found uncommitted changes in the working tree") 58 | end 59 | end 60 | 61 | defp check_branch(expected) do 62 | current = current_branch() 63 | 64 | if expected != current do 65 | Mix.raise("Expected branch #{inspect(expected)} does not match current #{inspect(current)}") 66 | end 67 | end 68 | 69 | defp check_remote_branch(local_branch) do 70 | {_, 0} = System.cmd("git", ["fetch"]) 71 | 72 | case System.cmd("git", [ 73 | "rev-parse", 74 | "--symbolic-full-name", 75 | "--abbrev-ref", 76 | "#{local_branch}@{upstream}" 77 | ]) do 78 | {_out, 0} -> 79 | true 80 | 81 | {_, _} -> 82 | Mix.raise("Aborting due to git error") 83 | end 84 | 85 | {out, 0} = System.cmd("git", ["status", "--branch", local_branch, "--porcelain"]) 86 | 87 | if String.contains?(out, "ahead") do 88 | Mix.raise("Local branch is ahead of the remote branch, aborting") 89 | end 90 | 91 | if String.contains?(out, "behind") do 92 | Mix.raise("Local branch is behind the remote branch, aborting") 93 | end 94 | end 95 | 96 | defp current_branch() do 97 | {branch, 0} = System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]) 98 | String.trim(branch) 99 | end 100 | 101 | defp normalize_version(project, "v" <> rest), do: normalize_version(project, rest) 102 | 103 | defp normalize_version(project, version) do 104 | check_version(version, project[:version]) 105 | "v#{version}" 106 | end 107 | 108 | defp check_version(version, mix_version) do 109 | if version != mix_version do 110 | Mix.raise("Expected #{inspect(version)} to match mix.exs version #{inspect(mix_version)}") 111 | end 112 | end 113 | 114 | defp check_changelog(version) do 115 | unless File.exists?(@changelog) do 116 | Mix.raise("#{@changelog} is missing") 117 | end 118 | 119 | unless File.read!(@changelog) |> String.contains?(version) do 120 | Mix.raise("#{@changelog} does not include an entry for #{version}") 121 | end 122 | end 123 | 124 | defp check_license do 125 | unless Enum.any?(@licenses, &File.exists?(&1)) do 126 | Mix.raise("LICENSE file is missing, add LICENSE.md or LICENSE") 127 | end 128 | end 129 | 130 | defp check_dot_shipit(branch, version) do 131 | dot_shipit = ".shipit.exs" 132 | 133 | if File.exists?(dot_shipit) do 134 | binding = [branch: branch, version: version] 135 | File.read!(dot_shipit) |> Code.eval_string(binding, file: dot_shipit) 136 | end 137 | end 138 | 139 | defp create_and_push_tag(version) do 140 | Mix.shell().info("Creating tag #{version}...") 141 | {_, 0} = System.cmd("git", ["tag", version, "-a", "-m", version]) 142 | Mix.shell().info("done\n") 143 | 144 | Mix.shell().info("Pushing tag #{version}...") 145 | {_, 0} = System.cmd("git", ["push", "origin", version]) 146 | Mix.shell().info("done\n") 147 | end 148 | 149 | defp publish do 150 | Mix.Tasks.Hex.Publish.run([]) 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ShipIt.Mixfile do 2 | use Mix.Project 3 | 4 | @version "0.2.3" 5 | 6 | def project do 7 | [ 8 | app: :shipit, 9 | version: @version, 10 | elixir: "~> 1.3", 11 | build_embedded: Mix.env() == :prod, 12 | start_permanent: Mix.env() == :prod, 13 | deps: deps(), 14 | package: package(), 15 | 16 | # Docs 17 | name: "ShipIt", 18 | docs: [ 19 | source_ref: "v#{@version}", 20 | source_url: "https://github.com/wojtekmach/shipit", 21 | main: "readme", 22 | extras: ["README.md"] 23 | ] 24 | ] 25 | end 26 | 27 | def application do 28 | [ 29 | applications: [:logger] 30 | ] 31 | end 32 | 33 | defp deps do 34 | [ 35 | {:ex_doc, ">= 0.0.0", only: :dev} 36 | ] 37 | end 38 | 39 | def package do 40 | [ 41 | description: "ShipIt automates Hex package publishing to avoid common mistakes", 42 | licenses: ["MIT"], 43 | links: %{"GitHub" => "https://github.com/wojtekmach/shipit"}, 44 | maintainers: ["Wojtek Mach"] 45 | ] 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"earmark": {:hex, :earmark, "1.2.0", "bf1ce17aea43ab62f6943b97bd6e3dc032ce45d4f787504e3adf738e54b42f3a", [:mix], []}, 2 | "ex_doc": {:hex, :ex_doc, "0.15.0", "e73333785eef3488cf9144a6e847d3d647e67d02bd6fdac500687854dd5c599f", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]}} 3 | --------------------------------------------------------------------------------