diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.markdown
@@ -0,0 +1,4 @@
+# Change log
+
+GitHub Release follows the [Package Versioning Policy](https://pvp.haskell.org).
+You can find release notes [on GitHub](https://github.com/tfausak/github-release/releases).
diff --git a/LICENSE.markdown b/LICENSE.markdown
--- a/LICENSE.markdown
+++ b/LICENSE.markdown
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2021 Taylor Fausak
+Copyright (c) 2022 Taylor Fausak
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/README.markdown b/README.markdown
new file mode 100644
--- /dev/null
+++ b/README.markdown
@@ -0,0 +1,54 @@
+# [GitHub Release][]
+
+[![CI](https://github.com/tfausak/github-release/workflows/CI/badge.svg)](https://github.com/tfausak/github-release/actions/new)
+[![Hackage](https://img.shields.io/hackage/v/github-release)](https://hackage.haskell.org/package/github-release)
+[![Stackage](https://www.stackage.org/package/github-release/badge/nightly?label=stackage)](https://www.stackage.org/package/github-release)
+
+GitHub Release is a command-line utility for uploading files to GitHub
+releases.
+
+Once you've got it, run it like so:
+
+``` sh
+github-release upload \
+  --token '...' \
+  --owner 'someone' \
+  --repo 'something' \
+  --tag 'v1.2.3' \
+  --file 'path/to/example.tgz' \
+  --name 'example-1.2.3.tgz'
+```
+
+You can generate a token on the [personal access tokens][] page of your
+personal settings. The `file` option is the path to the local file you want to
+upload. The `name` option is what the file should be called on the GitHub
+release.
+
+GitHub Release is written in Haskell. If you want to build it yourself or use
+it in your project, you'll want to get [Stack][]. Once you've done that, you
+can install and use it from the command line.
+
+``` sh
+stack --resolver nightly install github-release
+stack exec -- github-release upload # as above ...
+```
+
+Or you can use it from Haskell.
+
+``` hs
+import qualified GitHubRelease
+GitHubRelease.upload
+    "..."                 -- token
+    "someone"             -- owner
+    "something"           -- repo
+    "1.2.3"               -- tag
+    "path/to/example.tgz" -- file
+    "example-1.2.3.tgz"   -- name
+```
+
+Inspired by <https://github.com/aktau/github-release>.
+
+[GitHub Release]: https://github.com/tfausak/github-release
+[the latest release]: https://github.com/tfausak/github-release/releases/latest
+[personal access tokens]: https://github.com/settings/tokens
+[Stack]: http://docs.haskellstack.org/en/stable/README/
diff --git a/github-release.cabal b/github-release.cabal
--- a/github-release.cabal
+++ b/github-release.cabal
@@ -1,13 +1,14 @@
-cabal-version: 2.0
+cabal-version: 2.2
 
 name: github-release
-version: 1.3.10
+version: 2.0.0.0
 
 synopsis: Upload files to GitHub releases.
 description: GitHub Release uploads files to GitHub releases.
 
 build-type: Simple
 category: Utility
+extra-source-files: CHANGELOG.markdown README.markdown
 license-file: LICENSE.markdown
 license: MIT
 maintainer: Taylor Fausak
@@ -16,12 +17,16 @@
   location: https://github.com/tfausak/github-release
   type: git
 
-library
-  autogen-modules: Paths_github_release
+flag pedantic
+  default: False
+  description: Enables @-Werror@, which turns warnings into errors.
+  manual: True
+
+common library
   build-depends:
-    base >= 4.13.0 && < 4.16
+    , base >= 4.13.0 && < 4.17
     , aeson >= 1.4.7 && < 1.6 || >= 2.0.0 && < 2.1
-    , burrito >= 1.2.0 && < 1.3
+    , burrito >= 1.2.0 && < 1.3 || >= 2.0.0 && < 2.1
     , bytestring >= 0.10.10 && < 0.12
     , http-client >= 0.6.4 && < 0.8
     , http-client-tls >= 0.3.5 && < 0.4
@@ -31,7 +36,6 @@
     , text >= 1.2.4 && < 1.3
     , unordered-containers >= 0.2.10 && < 0.3
   default-language: Haskell2010
-  exposed-modules: GitHubRelease
   ghc-options:
     -Weverything
     -Wno-implicit-prelude
@@ -39,16 +43,39 @@
     -Wno-missing-exported-signatures
     -Wno-safe
     -Wno-unsafe
-  hs-source-dirs: src/lib
-  other-modules: Paths_github_release
+    -Wno-all-missed-specialisations
 
+  if flag(pedantic)
+    ghc-options: -Werror
+
   if impl(ghc >= 8.10)
     ghc-options:
       -Wno-missing-safe-haskell-mode
       -Wno-prepositive-qualified-module
 
+  if impl(ghc >= 9.2)
+    ghc-options:
+      -Wno-missing-kind-signatures
+
+common executable
+  import: library
+
+  build-depends: github-release
+  ghc-options:
+    -rtsopts
+    -threaded
+    -Wno-unused-packages
+
+library
+  import: library
+
+  autogen-modules: Paths_github_release
+  exposed-modules: GitHubRelease
+  hs-source-dirs: source/library
+  other-modules: Paths_github_release
+
 executable github-release
-  build-depends: base, github-release
-  default-language: Haskell2010
-  hs-source-dirs: src/exe
+  import: executable
+
+  hs-source-dirs: source/executable
   main-is: Main.hs
diff --git a/source/executable/Main.hs b/source/executable/Main.hs
new file mode 100644
--- /dev/null
+++ b/source/executable/Main.hs
@@ -0,0 +1,5 @@
+module Main
+  ( module GitHubRelease
+  ) where
+
+import GitHubRelease (main)
diff --git a/source/library/GitHubRelease.hs b/source/library/GitHubRelease.hs
new file mode 100644
--- /dev/null
+++ b/source/library/GitHubRelease.hs
@@ -0,0 +1,339 @@
+{-# OPTIONS_GHC -Wno-partial-fields #-}
+
+{-# LANGUAGE DataKinds           #-}
+{-# LANGUAGE DeriveGeneric       #-}
+{-# LANGUAGE OverloadedStrings   #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeOperators       #-}
+
+module GitHubRelease
+  ( Command(..)
+  , main
+  , runCommand
+  , upload
+  , getUploadUrl
+  , getTag
+  , authorizationHeader
+  , userAgentHeader
+  , userAgent
+  , versionString
+  , uploadFile
+  , uploadBody
+  ) where
+
+import Options.Generic (type (<?>))
+
+import qualified Burrito
+import Data.Aeson ((.=), object)
+import qualified Data.Aeson as Aeson
+import qualified Data.ByteString.Char8 as BS8
+import qualified Data.ByteString.Lazy as BSL
+import qualified Data.HashMap.Strict as HashMap
+import qualified Data.Maybe as Maybe
+import qualified Data.Text as Text
+import qualified Data.Version as Version
+import qualified GHC.Generics as Generics
+import qualified Network.HTTP.Client as Client
+import qualified Network.HTTP.Client.TLS as TLS
+import qualified Network.HTTP.Types as HTTP
+import qualified Network.Mime as MIME
+import qualified Options.Generic as Options
+import qualified Paths_github_release as This
+import qualified System.Environment as Environment
+import qualified System.IO as IO
+import qualified Text.Printf as Printf
+
+data Command
+  = Upload { file :: FilePath <?> "The path to the local file to upload."
+          ,  name :: String <?> "The name to give the file on the release."
+          ,  owner :: Maybe String <?> "The GitHub owner, either a user or organization."
+          ,  repo :: String <?> "The GitHub repository name."
+          ,  tag :: String <?> "The tag name."
+          ,  token :: Maybe String <?> "Your OAuth2 token."}
+  | Release { title :: String <?> "The name of the release"
+          ,  owner :: Maybe String <?> "The GitHub owner, either a user or organization."
+          ,  repo :: String <?> "The GitHub repository name."
+          ,  tag :: String <?> "The tag name."
+          ,  description :: Maybe String <?> "Release description."
+          ,  token :: Maybe String <?> "Your OAuth2 token."
+          ,  preRelease :: Maybe Bool <?> "Indicates if this is a pre-release."
+          ,  draft :: Maybe Bool <?> "Indicates if this is a draft."
+          }
+  | Delete
+    { name :: String <?> "The name to give the file on the release."
+    , owner :: Maybe String <?> "The GitHub owner, either a user or organization."
+    , repo :: String <?> "The GitHub repository name."
+    , tag :: String <?> "The tag name."
+    , token :: Maybe String <?> "Your OAuth2 token."
+    }
+  | Version
+  deriving (Generics.Generic, Show)
+
+instance Options.ParseRecord Command
+
+main :: IO ()
+main = do
+  command <- Options.getRecord (Text.pack "Upload a file to a GitHub release.")
+  runCommand command
+
+runCommand :: Command -> IO ()
+runCommand command = case command of
+  Upload aFile aName anOwner aRepo aTag helpfulToken -> do
+    aToken <- maybe (Environment.getEnv "GITHUB_TOKEN") pure
+      $ Options.unHelpful helpfulToken
+    upload
+      aToken
+      (Options.unHelpful anOwner)
+      (Options.unHelpful aRepo)
+      (Options.unHelpful aTag)
+      (Options.unHelpful aFile)
+      (Options.unHelpful aName)
+  Release aTitle anOwner aRepo aTag aDescription helpfulToken aPreRelease aDraft
+    -> do
+      aToken <- maybe (Environment.getEnv "GITHUB_TOKEN") pure
+        $ Options.unHelpful helpfulToken
+      release
+        aToken
+        (Options.unHelpful anOwner)
+        (Options.unHelpful aRepo)
+        (Options.unHelpful aTag)
+        (Options.unHelpful aTitle)
+        (Options.unHelpful aDescription)
+        (Options.unHelpful aPreRelease)
+        (Options.unHelpful aDraft)
+  Delete aName anOwner aRepo aTag helpfulToken -> do
+    aToken <- maybe (Environment.getEnv "GITHUB_TOKEN") pure
+      $ Options.unHelpful helpfulToken
+    delete
+      (Options.unHelpful aName)
+      (Options.unHelpful anOwner)
+      (Options.unHelpful aRepo)
+      (Options.unHelpful aTag)
+      aToken
+  Version -> putStrLn versionString
+
+upload
+  :: String -> Maybe String -> String -> String -> FilePath -> String -> IO ()
+upload aToken anOwner aRepo aTag aFile aName = do
+  manager <- Client.newManager TLS.tlsManagerSettings
+  uploadUrl <- getUploadUrl manager aToken anOwner aRepo aTag
+  response <- uploadFile manager uploadUrl aToken aFile aName
+  case HTTP.statusCode (Client.responseStatus response) of
+    201 -> pure ()
+    _ -> fail "Failed to upload file to release!"
+
+release
+  :: String
+  -> Maybe String
+  -> String
+  -> String
+  -> String
+  -> Maybe String
+  -> Maybe Bool
+  -> Maybe Bool
+  -> IO ()
+release aToken anOwner aRepo aTag aTitle aDescription aPreRelease aDraft = do
+  manager <- Client.newManager TLS.tlsManagerSettings
+  (owner', repo') <- getOwnerRepo anOwner aRepo
+  let format = "https://api.github.com/repos/%s/%s/releases" :: String
+  let
+    url :: String
+    url = Printf.printf format owner' repo'
+  response <- mkRelease
+    manager
+    url
+    aToken
+    aTag
+    aTitle
+    aDescription
+    aPreRelease
+    aDraft
+  let
+    body =
+      Aeson.eitherDecode $ Client.responseBody response :: Either
+          String
+          Aeson.Object
+  case HTTP.statusCode (Client.responseStatus response) of
+    201 -> pure ()
+    422 -> IO.hPutStrLn IO.stderr "Release aready exists. Ignoring."
+    _ -> fail $ "Failed to create release! Reason: " <> show body
+
+delete :: String -> Maybe String -> String -> String -> String -> IO ()
+delete aName rawOwner rawRepo aTag aToken = do
+  manager <- Client.newManager TLS.tlsManagerSettings
+  (anOwner, aRepo) <- getOwnerRepo rawOwner rawRepo
+  ghRelease <- do
+    result <- getTag manager aToken anOwner aRepo aTag
+    case result of
+      Left problem -> fail $ "Failed to get tag JSON: " <> show problem
+      Right json -> pure json
+  case filter ((== aName) . ghAssetName) $ ghReleaseAssets ghRelease of
+    [] -> fail "Failed to find asset on release."
+    ghAsset : _ -> do
+      request <- Client.parseRequest $ ghAssetUrl ghAsset
+      response <- Client.httpLbs
+        request
+          { Client.method = HTTP.methodDelete
+          , Client.requestHeaders =
+            [authorizationHeader aToken, userAgentHeader]
+          }
+        manager
+      case HTTP.statusCode $ Client.responseStatus response of
+        204 -> pure ()
+        _ -> fail $ "Failed to delete asset from release! " <> show response
+
+newtype GHRelease = GHRelease
+  { ghReleaseAssets :: [GHAsset]
+  } deriving (Eq, Show)
+
+instance Aeson.FromJSON GHRelease where
+  parseJSON =
+    Aeson.withObject "GHRelease" $ \obj -> GHRelease <$> obj Aeson..: "assets"
+
+data GHAsset = GHAsset
+  { ghAssetName :: String
+  , ghAssetUrl :: String
+  }
+  deriving (Eq, Show)
+
+instance Aeson.FromJSON GHAsset where
+  parseJSON = Aeson.withObject "GHAsset"
+    $ \obj -> GHAsset <$> obj Aeson..: "name" <*> obj Aeson..: "url"
+
+getUploadUrl
+  :: Client.Manager
+  -> String
+  -> Maybe String
+  -> String
+  -> String
+  -> IO Burrito.Template
+getUploadUrl manager aToken rawOwner rawRepo aTag = do
+  json <- do
+    (anOwner, aRepo) <- getOwnerRepo rawOwner rawRepo
+    result <- getTag manager aToken anOwner aRepo aTag
+    case result of
+      Left problem -> fail ("Failed to get tag JSON: " <> show problem)
+      Right json -> pure json
+  text <- case HashMap.lookup (Text.pack "upload_url") json of
+    Just (Aeson.String text) -> pure text
+    _ -> fail ("Failed to get upload URL: " <> show json)
+  let uploadUrl = Text.unpack text
+  case Burrito.parse uploadUrl of
+    Nothing -> fail ("Failed to parse URL template: " <> show uploadUrl)
+    Just template -> pure template
+
+getOwnerRepo :: Maybe String -> String -> IO (String, String)
+getOwnerRepo rawOwner rawRepo = do
+  (anOwner, aRepo) <- case break (== '/') rawRepo of
+    (aRepo, "") -> case rawOwner of
+      Nothing -> fail "Missing required option --owner."
+      Just anOwner -> pure (anOwner, aRepo)
+    (anOwner, aRepo) -> do
+      case rawOwner of
+        Nothing -> pure ()
+        Just _ -> IO.hPutStrLn IO.stderr "Ignoring --owner option."
+      pure (anOwner, drop 1 aRepo)
+  return (anOwner, aRepo)
+
+getTag
+  :: Aeson.FromJSON a
+  => Client.Manager
+  -> String
+  -> String
+  -> String
+  -> String
+  -> IO (Either String a)
+getTag manager aToken anOwner aRepo aTag = do
+  let format = "https://api.github.com/repos/%s/%s/releases/tags/%s" :: String
+  let
+    url :: String
+    url = Printf.printf format anOwner aRepo aTag
+  initialRequest <- Client.parseRequest url
+  let
+    request = initialRequest
+      { Client.requestHeaders = [authorizationHeader aToken, userAgentHeader]
+      }
+  response <- Client.httpLbs request manager
+  let body = Client.responseBody response
+  return (Aeson.eitherDecode body)
+
+authorizationHeader :: String -> HTTP.Header
+authorizationHeader aToken =
+  (HTTP.hAuthorization, BS8.pack (Printf.printf "token %s" aToken))
+
+userAgentHeader :: HTTP.Header
+userAgentHeader = (HTTP.hUserAgent, BS8.pack userAgent)
+
+userAgent :: String
+userAgent = Printf.printf
+  "%s/%s-%s"
+  ("tfausak" :: String)
+  ("github-release" :: String)
+  versionString
+
+versionString :: String
+versionString = Version.showVersion This.version
+
+uploadFile
+  :: Client.Manager
+  -> Burrito.Template
+  -> String
+  -> FilePath
+  -> String
+  -> IO (Client.Response BSL.ByteString)
+uploadFile manager template aToken aFile aName = do
+  contents <- BSL.readFile aFile
+  let body = Client.RequestBodyLBS contents
+  uploadBody manager template aToken body aName
+
+uploadBody
+  :: Client.Manager
+  -> Burrito.Template
+  -> String
+  -> Client.RequestBody
+  -> String
+  -> IO (Client.Response BSL.ByteString)
+uploadBody manager template aToken body aName = do
+  let
+    url :: String
+    url = Burrito.expand [("name", Burrito.stringValue aName)] template
+  initialRequest <- Client.parseRequest url
+  let
+    request = initialRequest
+      { Client.method = BS8.pack "POST"
+      , Client.requestBody = body
+      , Client.requestHeaders =
+        [ authorizationHeader aToken
+        , (HTTP.hContentType, MIME.defaultMimeLookup (Text.pack aName))
+        , userAgentHeader
+        ]
+      }
+  Client.httpLbs request manager
+
+mkRelease
+  :: Client.Manager
+  -> String
+  -> String
+  -> String
+  -> String
+  -> Maybe String
+  -> Maybe Bool
+  -> Maybe Bool
+  -> IO (Client.Response BSL.ByteString)
+mkRelease manager url aToken aTag aTitle aDescription aPreRelease aDraft = do
+  initialRequest <- Client.parseRequest url
+  let
+    requestObject = object
+      [ "tag_name" .= aTag
+      , "name" .= aTitle
+      , "body" .= Maybe.fromMaybe "" aDescription
+      , "prerelease" .= Maybe.fromMaybe False aPreRelease
+      , "draft" .= Maybe.fromMaybe False aDraft
+      ]
+  let
+    request = initialRequest
+      { Client.method = BS8.pack "POST"
+      , Client.requestBody = Client.RequestBodyLBS $ Aeson.encode requestObject
+      , Client.requestHeaders = [authorizationHeader aToken, userAgentHeader]
+      }
+  Client.httpLbs request manager
diff --git a/src/exe/Main.hs b/src/exe/Main.hs
deleted file mode 100644
--- a/src/exe/Main.hs
+++ /dev/null
@@ -1,5 +0,0 @@
-module Main
-  ( module GitHubRelease
-  ) where
-
-import GitHubRelease (main)
diff --git a/src/lib/GitHubRelease.hs b/src/lib/GitHubRelease.hs
deleted file mode 100644
--- a/src/lib/GitHubRelease.hs
+++ /dev/null
@@ -1,314 +0,0 @@
-{-# OPTIONS_GHC -Wno-partial-fields #-}
-
-{-# LANGUAGE DataKinds           #-}
-{-# LANGUAGE DeriveGeneric       #-}
-{-# LANGUAGE ExplicitNamespaces  #-}
-{-# LANGUAGE OverloadedStrings   #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeOperators       #-}
-
-module GitHubRelease
-  ( Command(..)
-  , main
-  , runCommand
-  , upload
-  , getUploadUrl
-  , getTag
-  , authorizationHeader
-  , userAgentHeader
-  , userAgent
-  , versionString
-  , uploadFile
-  , uploadBody
-  ) where
-
-import           Options.Generic            (type (<?>))
-
-import qualified Burrito
-import           Data.Aeson                 (object, (.=))
-import qualified Data.Aeson                 as Aeson
-import qualified Data.ByteString.Char8      as BS8
-import qualified Data.ByteString.Lazy       as BSL
-import qualified Data.HashMap.Strict        as HashMap
-import qualified Data.Text                  as Text
-import qualified Data.Version               as Version
-import qualified GHC.Generics               as Generics
-import qualified Network.HTTP.Client        as Client
-import qualified Network.HTTP.Client.TLS    as TLS
-import qualified Network.HTTP.Types         as HTTP
-import qualified Network.Mime               as MIME
-import qualified Options.Generic            as Options
-import qualified Paths_github_release       as This
-import qualified System.Environment         as Environment
-import qualified System.IO                  as IO
-import qualified Text.Printf                as Printf
-
-data Command
-  = Upload { file :: FilePath <?> "The path to the local file to upload."
-          ,  name :: String <?> "The name to give the file on the release."
-          ,  owner :: Maybe String <?> "The GitHub owner, either a user or organization."
-          ,  repo :: String <?> "The GitHub repository name."
-          ,  tag :: String <?> "The tag name."
-          ,  token :: Maybe String <?> "Your OAuth2 token."}
-  | Release { title :: String <?> "The name of the release"
-          ,  owner :: Maybe String <?> "The GitHub owner, either a user or organization."
-          ,  repo :: String <?> "The GitHub repository name."
-          ,  tag :: String <?> "The tag name."
-          ,  description :: Maybe String <?> "Release description."
-          ,  token :: Maybe String <?> "Your OAuth2 token."
-          ,  preRelease :: Maybe Bool <?> "Indicates if this is a pre-release."
-          ,  draft :: Maybe Bool <?> "Indicates if this is a draft."
-          }
-  | Delete
-    { name :: String <?> "The name to give the file on the release."
-    , owner :: Maybe String <?> "The GitHub owner, either a user or organization."
-    , repo :: String <?> "The GitHub repository name."
-    , tag :: String <?> "The tag name."
-    , token :: Maybe String <?> "Your OAuth2 token."
-    }
-  | Version
-  deriving (Generics.Generic, Show)
-
-instance Options.ParseRecord Command
-
-main :: IO ()
-main = do
-  command <- Options.getRecord (Text.pack "Upload a file to a GitHub release.")
-  runCommand command
-
-runCommand :: Command -> IO ()
-runCommand command =
-  case command of
-    Upload aFile aName anOwner aRepo aTag helpfulToken -> do
-      aToken <- maybe (Environment.getEnv "GITHUB_TOKEN") pure
-        $ Options.unHelpful helpfulToken
-      upload
-        aToken
-        (Options.unHelpful anOwner)
-        (Options.unHelpful aRepo)
-        (Options.unHelpful aTag)
-        (Options.unHelpful aFile)
-        (Options.unHelpful aName)
-    Release aTitle anOwner aRepo aTag aDescription helpfulToken aPreRelease aDraft -> do
-      aToken <- maybe (Environment.getEnv "GITHUB_TOKEN") pure
-        $ Options.unHelpful helpfulToken
-      release
-        aToken
-        (Options.unHelpful anOwner)
-        (Options.unHelpful aRepo)
-        (Options.unHelpful aTag)
-        (Options.unHelpful aTitle)
-        (Options.unHelpful aDescription)
-        (Options.unHelpful aPreRelease)
-        (Options.unHelpful aDraft)
-    Delete aName anOwner aRepo aTag helpfulToken -> do
-      aToken <- maybe (Environment.getEnv "GITHUB_TOKEN") pure
-        $ Options.unHelpful helpfulToken
-      delete
-        (Options.unHelpful aName)
-        (Options.unHelpful anOwner)
-        (Options.unHelpful aRepo)
-        (Options.unHelpful aTag)
-        aToken
-    Version -> putStrLn versionString
-
-upload :: String -> Maybe String -> String -> String -> FilePath -> String -> IO ()
-upload aToken anOwner aRepo aTag aFile aName = do
-  manager <- Client.newManager TLS.tlsManagerSettings
-  uploadUrl <- getUploadUrl manager aToken anOwner aRepo aTag
-  response <- uploadFile manager uploadUrl aToken aFile aName
-  case HTTP.statusCode (Client.responseStatus response) of
-    201 -> pure ()
-    _   -> fail "Failed to upload file to release!"
-
-release :: String -> Maybe String -> String -> String -> String -> Maybe String -> Maybe Bool -> Maybe Bool -> IO ()
-release aToken anOwner aRepo aTag aTitle aDescription aPreRelease aDraft = do
-  manager <- Client.newManager TLS.tlsManagerSettings
-  (owner', repo') <- getOwnerRepo anOwner aRepo
-  let format = "https://api.github.com/repos/%s/%s/releases" :: String
-  let
-    url :: String
-    url = Printf.printf format owner' repo'
-  response <- mkRelease manager url aToken aTag aTitle aDescription aPreRelease aDraft
-  let body = Aeson.eitherDecode $ Client.responseBody response :: Either String Aeson.Object
-  case HTTP.statusCode (Client.responseStatus response) of
-    201 -> pure ()
-    422 -> IO.hPutStrLn IO.stderr "Release aready exists. Ignoring."
-    _   -> fail $ "Failed to create release! Reason: " <> (show body)
-
-delete :: String -> Maybe String -> String -> String -> String -> IO ()
-delete aName rawOwner rawRepo aTag aToken = do
-  manager <- Client.newManager TLS.tlsManagerSettings
-  (anOwner, aRepo) <- getOwnerRepo rawOwner rawRepo
-  ghRelease <- do
-    result <- getTag manager aToken anOwner aRepo aTag
-    case result of
-      Left problem -> fail $ "Failed to get tag JSON: " ++ show problem
-      Right json -> pure json
-  case filter ((== aName) . ghAssetName) $ ghReleaseAssets ghRelease of
-    [] -> fail $ "Failed to find asset on release."
-    ghAsset : _ -> do
-      request <- Client.parseRequest $ ghAssetUrl ghAsset
-      response <- Client.httpLbs request
-        { Client.method = HTTP.methodDelete
-        , Client.requestHeaders = [authorizationHeader aToken, userAgentHeader]
-        } manager
-      case HTTP.statusCode $ Client.responseStatus response of
-        204 -> pure ()
-        _ -> fail $ "Failed to delete asset from release! " <> show response
-
-newtype GHRelease = GHRelease
-  { ghReleaseAssets :: [GHAsset]
-  } deriving (Eq, Show)
-
-instance Aeson.FromJSON GHRelease where
-  parseJSON = Aeson.withObject "GHRelease" $ \ obj -> GHRelease
-    <$> obj Aeson..: "assets"
-
-data GHAsset = GHAsset
-  { ghAssetName :: String
-  , ghAssetUrl :: String
-  } deriving (Eq, Show)
-
-instance Aeson.FromJSON GHAsset where
-  parseJSON = Aeson.withObject "GHAsset" $ \ obj -> GHAsset
-    <$> obj Aeson..: "name"
-    <*> obj Aeson..: "url"
-
-getUploadUrl
-  :: Client.Manager
-  -> String
-  -> Maybe String
-  -> String
-  -> String
-  -> IO Burrito.Template
-getUploadUrl manager aToken rawOwner rawRepo aTag = do
-  json <- do
-    (anOwner, aRepo) <- getOwnerRepo rawOwner rawRepo
-    result <- getTag manager aToken anOwner aRepo aTag
-    case result of
-      Left problem -> fail ("Failed to get tag JSON: " ++ show problem)
-      Right json   -> pure json
-  text <- case HashMap.lookup (Text.pack "upload_url") json of
-    Just (Aeson.String text) -> pure text
-    _ -> fail ("Failed to get upload URL: " ++ show json)
-  let uploadUrl = Text.unpack text
-  template <- case Burrito.parse uploadUrl of
-    Nothing        -> fail ("Failed to parse URL template: " ++ show uploadUrl)
-    Just template  -> pure template
-  pure template
-
-getOwnerRepo :: Maybe String -> String -> IO ((String, String))
-getOwnerRepo rawOwner rawRepo = do
-  (anOwner, aRepo) <- case break (== '/') rawRepo of
-    (aRepo, "") -> case rawOwner of
-      Nothing      -> fail "Missing required option --owner."
-      Just anOwner -> pure (anOwner, aRepo)
-    (anOwner, aRepo) -> do
-      case rawOwner of
-        Nothing -> pure ()
-        Just _  -> IO.hPutStrLn IO.stderr "Ignoring --owner option."
-      pure (anOwner, drop 1 aRepo)
-  return (anOwner, aRepo)
-
-getTag
-  :: Aeson.FromJSON a
-  => Client.Manager
-  -> String
-  -> String
-  -> String
-  -> String
-  -> IO (Either String a)
-getTag manager aToken anOwner aRepo aTag = do
-  let format = "https://api.github.com/repos/%s/%s/releases/tags/%s" :: String
-  let
-    url :: String
-    url = Printf.printf format anOwner aRepo aTag
-  initialRequest <- Client.parseRequest url
-  let request =
-        initialRequest
-        {Client.requestHeaders = [authorizationHeader aToken, userAgentHeader]}
-  response <- Client.httpLbs request manager
-  let body = Client.responseBody response
-  return (Aeson.eitherDecode body)
-
-authorizationHeader :: String -> HTTP.Header
-authorizationHeader aToken =
-  (HTTP.hAuthorization, BS8.pack (Printf.printf "token %s" aToken))
-
-userAgentHeader :: HTTP.Header
-userAgentHeader = (HTTP.hUserAgent, BS8.pack userAgent)
-
-userAgent :: String
-userAgent = Printf.printf "%s/%s-%s" ("tfausak" :: String) ("github-release" :: String) versionString
-
-versionString :: String
-versionString = Version.showVersion This.version
-
-uploadFile
-  :: Client.Manager
-  -> Burrito.Template
-  -> String
-  -> FilePath
-  -> String
-  -> IO (Client.Response BSL.ByteString)
-uploadFile manager template aToken aFile aName = do
-  contents <- BSL.readFile aFile
-  let body = Client.RequestBodyLBS contents
-  uploadBody manager template aToken body aName
-
-uploadBody
-  :: Client.Manager
-  -> Burrito.Template
-  -> String
-  -> Client.RequestBody
-  -> String
-  -> IO (Client.Response BSL.ByteString)
-uploadBody manager template aToken body aName = do
-  let
-    url :: String
-    url = Burrito.expand
-      [("name", Burrito.stringValue aName)]
-      template
-  initialRequest <- Client.parseRequest url
-  let request =
-        initialRequest
-        { Client.method = BS8.pack "POST"
-        , Client.requestBody = body
-        , Client.requestHeaders =
-            [ authorizationHeader aToken
-            , (HTTP.hContentType, MIME.defaultMimeLookup (Text.pack aName))
-            , userAgentHeader
-            ]
-        }
-  Client.httpLbs request manager
-
-mkRelease
-  :: Client.Manager
-  -> String
-  -> String
-  -> String
-  -> String
-  -> Maybe String
-  -> Maybe Bool
-  -> Maybe Bool
-  -> IO (Client.Response BSL.ByteString)
-mkRelease manager url aToken aTag aTitle aDescription aPreRelease aDraft = do
-  initialRequest <- Client.parseRequest url
-  let requestObject = object
-            [ "tag_name" .= aTag
-            , "name"  .= aTitle
-            , "body" .= maybe "" id aDescription
-            , "prerelease" .= maybe False id aPreRelease
-            , "draft" .= maybe False id aDraft
-            ]
-  let request =
-        initialRequest
-        { Client.method = BS8.pack "POST"
-        , Client.requestBody = Client.RequestBodyLBS $ Aeson.encode requestObject
-        , Client.requestHeaders =
-            [ authorizationHeader aToken
-            , userAgentHeader
-            ]
-        }
-  Client.httpLbs request manager
