diff --git a/cabal-install/src/Distribution/Client/BuildReports/Upload.hs b/cabal-install/src/Distribution/Client/BuildReports/Upload.hs index d7ce5d0e7bc..253daf6bc0c 100644 --- a/cabal-install/src/Distribution/Client/BuildReports/Upload.hs +++ b/cabal-install/src/Distribution/Client/BuildReports/Upload.hs @@ -28,6 +28,7 @@ import Distribution.Client.HttpUtils import Distribution.Client.Setup ( RepoContext (..) ) +import Distribution.Client.Types.Credentials (Auth) import Distribution.Simple.Utils (die') import System.FilePath.Posix ( () @@ -36,7 +37,7 @@ import System.FilePath.Posix type BuildReportId = URI type BuildLog = String -uploadReports :: Verbosity -> RepoContext -> (String, String) -> URI -> [(BuildReport, Maybe BuildLog)] -> IO () +uploadReports :: Verbosity -> RepoContext -> Auth -> URI -> [(BuildReport, Maybe BuildLog)] -> IO () uploadReports verbosity repoCtxt auth uri reports = do for_ reports $ \(report, mbBuildLog) -> do buildId <- postBuildReport verbosity repoCtxt auth uri report @@ -44,7 +45,7 @@ uploadReports verbosity repoCtxt auth uri reports = do Just buildLog -> putBuildLog verbosity repoCtxt auth buildId buildLog Nothing -> return () -postBuildReport :: Verbosity -> RepoContext -> (String, String) -> URI -> BuildReport -> IO BuildReportId +postBuildReport :: Verbosity -> RepoContext -> Auth -> URI -> BuildReport -> IO BuildReportId postBuildReport verbosity repoCtxt auth uri buildReport = do let fullURI = uri{uriPath = "/package" prettyShow (BuildReport.package buildReport) "reports"} transport <- repoContextGetTransport repoCtxt @@ -87,7 +88,7 @@ postBuildReport verbosity repoCtxt auth uri buildReport = do putBuildLog :: Verbosity -> RepoContext - -> (String, String) + -> Auth -> BuildReportId -> BuildLog -> IO () diff --git a/cabal-install/src/Distribution/Client/Config.hs b/cabal-install/src/Distribution/Client/Config.hs index 13f94146c08..a590c339cd8 100644 --- a/cabal-install/src/Distribution/Client/Config.hs +++ b/cabal-install/src/Distribution/Client/Config.hs @@ -95,7 +95,11 @@ import Distribution.Client.Types , isRelaxDeps , unRepoName ) -import Distribution.Client.Types.Credentials (Password (..), Username (..)) +import Distribution.Client.Types.Credentials + ( Password (..) + , Token (..) + , Username (..) + ) import Distribution.Utils.NubList ( NubList , fromNubList @@ -569,6 +573,7 @@ instance Semigroup SavedConfig where UploadFlags { uploadCandidate = combine uploadCandidate , uploadDoc = combine uploadDoc + , uploadToken = combine uploadToken , uploadUsername = combine uploadUsername , uploadPassword = combine uploadPassword , uploadPasswordCmd = combine uploadPasswordCmd @@ -579,7 +584,8 @@ instance Semigroup SavedConfig where combinedSavedReportFlags = ReportFlags - { reportUsername = combine reportUsername + { reportToken = combine reportToken + , reportUsername = combine reportUsername , reportPassword = combine reportPassword , reportVerbosity = combine reportVerbosity } @@ -1275,7 +1281,7 @@ configFieldDescriptions src = ++ toSavedConfig liftReportFlag (commandOptions reportCommand ParseArgs) - ["verbose", "username", "password"] + ["verbose", "token", "username", "password"] [] -- FIXME: this is a hack, hiding the user name and password. -- But otherwise it masks the upload ones. Either need to @@ -1340,6 +1346,13 @@ deprecatedFieldDescriptions = (optionalFlag parsecFilePath) globalCacheDir (\d cfg -> cfg{globalCacheDir = d}) + , liftUploadFlag $ + simpleFieldParsec + "hackage-token" + (Disp.text . fromFlagOrDefault "" . fmap unToken) + (optionalFlag (fmap Token parsecToken)) + uploadToken + (\d cfg -> cfg{uploadToken = d}) , liftUploadFlag $ simpleFieldParsec "hackage-username" diff --git a/cabal-install/src/Distribution/Client/HttpUtils.hs b/cabal-install/src/Distribution/Client/HttpUtils.hs index 8cf9bce7203..5b470a8f80f 100644 --- a/cabal-install/src/Distribution/Client/HttpUtils.hs +++ b/cabal-install/src/Distribution/Client/HttpUtils.hs @@ -27,6 +27,7 @@ import Distribution.Client.Types ( RemoteRepo (..) , unRepoName ) +import Distribution.Client.Types.Credentials (Auth) import Distribution.Client.Utils ( withTempFileName ) @@ -353,7 +354,7 @@ data HttpTransport = HttpTransport -> String -> Maybe Auth -> IO (HttpCode, String) - -- ^ POST a resource to a URI, with optional auth (username, password) + -- ^ POST a resource to a URI, with optional 'Auth' -- and return the HTTP status code and any redirect URL. , postHttpFile :: Verbosity @@ -362,7 +363,7 @@ data HttpTransport = HttpTransport -> Maybe Auth -> IO (HttpCode, String) -- ^ POST a file resource to a URI using multipart\/form-data encoding, - -- with optional auth (username, password) and return the HTTP status + -- with optional 'Auth' and return the HTTP status -- code and any error string. , putHttpFile :: Verbosity @@ -371,8 +372,8 @@ data HttpTransport = HttpTransport -> Maybe Auth -> [Header] -> IO (HttpCode, String) - -- ^ PUT a file resource to a URI, with optional auth - -- (username, password), extra headers and return the HTTP status code + -- ^ PUT a file resource to a URI, with optional 'Auth', + -- extra headers and return the HTTP status code -- and any error string. , transportSupportsHttps :: Bool -- ^ Whether this transport supports https or just http. @@ -387,13 +388,12 @@ data HttpTransport = HttpTransport type HttpCode = Int type ETag = String -type Auth = (String, String) noPostYet :: Verbosity -> URI -> String - -> Maybe (String, String) + -> Maybe Auth -> IO (Int, String) noPostYet verbosity _ _ _ = die' verbosity "Posting (for report upload) is not implemented yet" @@ -536,12 +536,13 @@ curlTransport prog = (Just (URIAuth u _ _)) | not (null u) -> Just $ filter (/= '@') u _ -> Nothing -- prefer passed in auth to auth derived from uri. If neither exist, then no auth - let mbAuthString = case (explicitAuth, uriDerivedAuth) of - (Just (uname, passwd), _) -> Just (uname ++ ":" ++ passwd) - (Nothing, Just a) -> Just a + let mbAuthStringToken = case (explicitAuth, uriDerivedAuth) of + (Just (Right token), _) -> Just $ Right token + (Just (Left (uname, passwd)), _) -> Just $ Left (uname ++ ":" ++ passwd) + (Nothing, Just a) -> Just $ Left a (Nothing, Nothing) -> Nothing - case mbAuthString of - Just up -> + case mbAuthStringToken of + Just (Left up) -> progInvocation { progInvokeInput = Just . IODataText . unlines $ @@ -550,6 +551,12 @@ curlTransport prog = ] , progInvokeArgs = ["--config", "-"] ++ progInvokeArgs progInvocation } + Just (Right token) -> + progInvocation + { progInvokeArgs = + ["--header", "Authorization: X-ApiKey " ++ token] + ++ progInvokeArgs progInvocation + } Nothing -> progInvocation posthttpfile verbosity uri path auth = do @@ -702,6 +709,7 @@ wgetTransport prog = ++ "boundary=" ++ boundary ] + ++ maybeToList (authTokenHeader auth) out <- runWGet verbosity (addUriAuth auth uri) args (code, _etag) <- parseOutput verbosity uri out withFile responseFile ReadMode $ \hnd -> do @@ -723,6 +731,7 @@ wgetTransport prog = ++ [ "--header=" ++ show name ++ ": " ++ value | Header name value <- headers ] + ++ maybeToList (authTokenHeader auth) out <- runWGet verbosity (addUriAuth auth uri) args (code, _etag) <- parseOutput verbosity uri out @@ -730,13 +739,16 @@ wgetTransport prog = resp <- hGetContents hnd evaluate $ force (code, resp) - addUriAuth Nothing uri = uri - addUriAuth (Just (user, pass)) uri = + authTokenHeader (Just (Right token)) = Just $ "--header=Authorization: X-ApiKey " ++ token + authTokenHeader _ = Nothing + + addUriAuth (Just (Left (user, pass))) uri = uri { uriAuthority = Just a{uriUserInfo = user ++ ":" ++ pass ++ "@"} } where a = fromMaybe (URIAuth "" "" "") (uriAuthority uri) + addUriAuth _ uri = uri runWGet verbosity uri args = do -- We pass the URI via STDIN because it contains the users' credentials @@ -918,14 +930,16 @@ powershellTransport prog = in "AddRange(\"bytes\", " ++ escape start ++ ", " ++ escape end ++ ");" name -> "Headers.Add(" ++ escape (show name) ++ "," ++ escape value ++ ");" - setupAuth auth = + setupAuth (Just (Left (uname, passwd))) = [ "$request.Credentials = new-object System.Net.NetworkCredential(" - ++ escape uname - ++ "," - ++ escape passwd - ++ ",\"\");" - | (uname, passwd) <- maybeToList auth + ++ escape uname + ++ "," + ++ escape passwd + ++ ",\"\");" ] + setupAuth (Just (Right token)) = + ["$request.Headers[\"Authorization\"] = " ++ escape ("X-ApiKey " ++ token)] + setupAuth Nothing = [] uploadFileAction method _uri fullPath = [ "$request.Method = " ++ show method @@ -1027,6 +1041,7 @@ plainHttpTransport = , Header HdrContentLength (show (LBS8.length body)) , Header HdrAccept ("text/plain") ] + ++ maybeToList (authTokenHeader auth) req = Request { rqURI = uri @@ -1046,7 +1061,8 @@ plainHttpTransport = , rqHeaders = Header HdrContentLength (show (LBS8.length body)) : Header HdrAccept "text/plain" - : headers + : maybeToList (authTokenHeader auth) + ++ headers , rqBody = body } (_, resp) <- cabalBrowse verbosity auth (request req) @@ -1076,9 +1092,14 @@ plainHttpTransport = setOutHandler (debug verbosity) setUserAgent userAgent setAllowBasicAuth False - setAuthorityGen (\_ _ -> return auth) + case auth of + Just (Left x) -> setAuthorityGen (\_ _ -> return $ Just x) + _ -> setAuthorityGen (\_ _ -> return Nothing) act + authTokenHeader (Just (Right token)) = Just $ Header HdrAuthorization ("X-ApiKey " ++ token) + authTokenHeader _ = Nothing + fixupEmptyProxy (Proxy uri _) | null uri = NoProxy fixupEmptyProxy p = p diff --git a/cabal-install/src/Distribution/Client/Main.hs b/cabal-install/src/Distribution/Client/Main.hs index 1a3cc94d49f..efe6bdf1f73 100644 --- a/cabal-install/src/Distribution/Client/Main.hs +++ b/cabal-install/src/Distribution/Client/Main.hs @@ -1118,6 +1118,7 @@ uploadAction uploadFlags extraArgs globalFlags = do Upload.uploadDoc verbosity repoContext + (flagToMaybe $ uploadToken uploadFlags') (flagToMaybe $ uploadUsername uploadFlags') maybe_password (fromFlag (uploadCandidate uploadFlags')) @@ -1126,6 +1127,7 @@ uploadAction uploadFlags extraArgs globalFlags = do Upload.upload verbosity repoContext + (flagToMaybe $ uploadToken uploadFlags') (flagToMaybe $ uploadUsername uploadFlags') maybe_password (fromFlag (uploadCandidate uploadFlags')) @@ -1199,6 +1201,7 @@ reportAction reportFlags extraArgs globalFlags = do Upload.report verbosity repoContext + (flagToMaybe $ reportToken reportFlags') (flagToMaybe $ reportUsername reportFlags') (flagToMaybe $ reportPassword reportFlags') diff --git a/cabal-install/src/Distribution/Client/Setup.hs b/cabal-install/src/Distribution/Client/Setup.hs index 44224d9559b..6d04d401a8a 100644 --- a/cabal-install/src/Distribution/Client/Setup.hs +++ b/cabal-install/src/Distribution/Client/Setup.hs @@ -93,7 +93,7 @@ import Distribution.Client.Compat.Prelude hiding (get) import Prelude () import Distribution.Client.Types.AllowNewer (AllowNewer (..), AllowOlder (..), RelaxDeps (..)) -import Distribution.Client.Types.Credentials (Password (..), Username (..)) +import Distribution.Client.Types.Credentials (Password (..), Token (..), Username (..)) import Distribution.Client.Types.Repo (LocalRepo (..), RemoteRepo (..)) import Distribution.Client.Types.WriteGhcEnvironmentFilesPolicy @@ -1648,7 +1648,8 @@ runCommand = -- ------------------------------------------------------------ data ReportFlags = ReportFlags - { reportUsername :: Flag Username + { reportToken :: Flag Token + , reportUsername :: Flag Username , reportPassword :: Flag Password , reportVerbosity :: Flag Verbosity } @@ -1657,7 +1658,8 @@ data ReportFlags = ReportFlags defaultReportFlags :: ReportFlags defaultReportFlags = ReportFlags - { reportUsername = mempty + { reportToken = mempty + , reportUsername = mempty , reportPassword = mempty , reportVerbosity = toFlag normal } @@ -1675,6 +1677,17 @@ reportCommand = , commandDefaultFlags = defaultReportFlags , commandOptions = \_ -> [ optionVerbosity reportVerbosity (\v flags -> flags{reportVerbosity = v}) + , option + ['t'] + ["token"] + "Hackage authentication Token." + reportToken + (\v flags -> flags{reportToken = v}) + ( reqArg' + "TOKEN" + (toFlag . Token) + (flagToList . fmap unToken) + ) , option ['u'] ["username"] @@ -2665,6 +2678,7 @@ data IsCandidate = IsCandidate | IsPublished data UploadFlags = UploadFlags { uploadCandidate :: Flag IsCandidate , uploadDoc :: Flag Bool + , uploadToken :: Flag Token , uploadUsername :: Flag Username , uploadPassword :: Flag Password , uploadPasswordCmd :: Flag [String] @@ -2677,6 +2691,7 @@ defaultUploadFlags = UploadFlags { uploadCandidate = toFlag IsCandidate , uploadDoc = toFlag False + , uploadToken = mempty , uploadUsername = mempty , uploadPassword = mempty , uploadPasswordCmd = mempty @@ -2692,7 +2707,7 @@ uploadCommand = , commandNotes = Just $ \_ -> "You can store your Hackage login in the ~/.config/cabal/config file\n" ++ "(the %APPDATA%\\cabal\\config file on Windows)\n" - ++ relevantConfigValuesText ["username", "password", "password-command"] + ++ relevantConfigValuesText ["token", "username", "password", "password-command"] , commandUsage = \pname -> "Usage: " ++ pname ++ " upload [FLAGS] TARFILES\n" , commandDefaultFlags = defaultUploadFlags @@ -2718,6 +2733,17 @@ uploadCommand = uploadDoc (\v flags -> flags{uploadDoc = v}) trueArg + , option + ['t'] + ["token"] + "Hackage authentication token." + uploadToken + (\v flags -> flags{uploadToken = v}) + ( reqArg' + "TOKEN" + (toFlag . Token) + (flagToList . fmap unToken) + ) , option ['u'] ["username"] diff --git a/cabal-install/src/Distribution/Client/Types/Credentials.hs b/cabal-install/src/Distribution/Client/Types/Credentials.hs index da208111c1f..5de185f178f 100644 --- a/cabal-install/src/Distribution/Client/Types/Credentials.hs +++ b/cabal-install/src/Distribution/Client/Types/Credentials.hs @@ -1,9 +1,15 @@ module Distribution.Client.Types.Credentials - ( Username (..) + ( Auth + , Token (..) + , Username (..) , Password (..) ) where -import Prelude (String) +import Prelude (Either, String) +-- | Either (username, password) or authentacation token +type Auth = Either (String, String) String + +newtype Token = Token {unToken :: String} newtype Username = Username {unUsername :: String} newtype Password = Password {unPassword :: String} diff --git a/cabal-install/src/Distribution/Client/Upload.hs b/cabal-install/src/Distribution/Client/Upload.hs index 7f78ee8e2e3..8b028a573cc 100644 --- a/cabal-install/src/Distribution/Client/Upload.hs +++ b/cabal-install/src/Distribution/Client/Upload.hs @@ -11,7 +11,12 @@ import Distribution.Client.Setup ( IsCandidate (..) , RepoContext (..) ) -import Distribution.Client.Types.Credentials (Password (..), Username (..)) +import Distribution.Client.Types.Credentials + ( Auth + , Password (..) + , Token (..) + , Username (..) + ) import Distribution.Client.Types.Repo (RemoteRepo (..), Repo, maybeRepoRemote) import Distribution.Client.Types.RepoName (unRepoName) @@ -32,8 +37,6 @@ import qualified System.FilePath.Posix as FilePath.Posix (()) import System.IO (hFlush, stdout) import System.IO.Echo (withoutInputEcho) -type Auth = Maybe (String, String) - -- > stripExtensions ["tar", "gz"] "foo.tar.gz" -- Just "foo" -- > stripExtensions ["tar", "gz"] "foo.gz.tar" @@ -48,12 +51,13 @@ stripExtensions exts path = foldM f path (reverse exts) upload :: Verbosity -> RepoContext + -> Maybe Token -> Maybe Username -> Maybe Password -> IsCandidate -> [FilePath] -> IO () -upload verbosity repoCtxt mUsername mPassword isCandidate paths = do +upload verbosity repoCtxt mToken mUsername mPassword isCandidate paths = do let repos :: [Repo] repos = repoContextRepos repoCtxt transport <- repoContextGetTransport repoCtxt @@ -87,9 +91,7 @@ upload verbosity repoCtxt mUsername mPassword isCandidate paths = do IsPublished -> "" ] } - Username username <- maybe (promptUsername domain) return mUsername - Password password <- maybe (promptPassword domain) return mPassword - let auth = Just (username, password) + auth <- Just <$> createAuth domain mToken mUsername mPassword for_ paths $ \path -> do notice verbosity $ "Uploading " ++ path ++ "... " case fmap takeFileName (stripExtensions ["tar", "gz"] path) of @@ -109,12 +111,13 @@ upload verbosity repoCtxt mUsername mPassword isCandidate paths = do uploadDoc :: Verbosity -> RepoContext + -> Maybe Token -> Maybe Username -> Maybe Password -> IsCandidate -> FilePath -> IO () -uploadDoc verbosity repoCtxt mUsername mPassword isCandidate path = do +uploadDoc verbosity repoCtxt mToken mUsername mPassword isCandidate path = do let repos = repoContextRepos repoCtxt transport <- repoContextGetTransport repoCtxt targetRepo <- @@ -160,11 +163,10 @@ uploadDoc verbosity repoCtxt mUsername mPassword isCandidate path = do || Unsafe.head reversePkgid /= '-' ) $ die' verbosity "Expected a file name matching the pattern -docs.tar.gz" - Username username <- maybe (promptUsername domain) return mUsername - Password password <- maybe (promptPassword domain) return mPassword - let auth = Just (username, password) - headers = + auth <- Just <$> createAuth domain mToken mUsername mPassword + + let headers = [ Header HdrContentType "application/x-tar" , Header HdrContentEncoding "gzip" ] @@ -212,18 +214,15 @@ promptPassword domain = do putStrLn "" return passwd -report :: Verbosity -> RepoContext -> Maybe Username -> Maybe Password -> IO () -report verbosity repoCtxt mUsername mPassword = do +report :: Verbosity -> RepoContext -> Maybe Token -> Maybe Username -> Maybe Password -> IO () +report verbosity repoCtxt mToken mUsername mPassword = do let repos :: [Repo] repos = repoContextRepos repoCtxt remoteRepos :: [RemoteRepo] remoteRepos = mapMaybe maybeRepoRemote repos for_ remoteRepos $ \remoteRepo -> do let domain = maybe "Hackage" uriRegName $ uriAuthority (remoteRepoURI remoteRepo) - Username username <- maybe (promptUsername domain) return mUsername - Password password <- maybe (promptPassword domain) return mPassword - let auth :: (String, String) - auth = (username, password) + auth <- createAuth domain mToken mUsername mPassword reportsDir <- defaultReportsDir let srcDir :: FilePath @@ -257,7 +256,7 @@ handlePackage -> Verbosity -> URI -> URI - -> Auth + -> Maybe Auth -> IsCandidate -> FilePath -> IO () @@ -294,3 +293,17 @@ handlePackage transport verbosity uri packageUri auth isCandidate path = formatWarnings :: String -> String formatWarnings x = "Warnings:\n" ++ (unlines . map ("- " ++) . lines) x + +createAuth + :: String + -> Maybe Token + -> Maybe Username + -> Maybe Password + -> IO Auth +createAuth domain mToken mUsername mPassword = case mToken of + Just token -> return $ Right $ unToken token + -- Use username and password if no token is provided + Nothing -> do + Username username <- maybe (promptUsername domain) return mUsername + Password password <- maybe (promptPassword domain) return mPassword + return $ Left (username, password) diff --git a/cabal-install/tests/IntegrationTests2.hs b/cabal-install/tests/IntegrationTests2.hs index fd9ed4ca19d..bf6e25c5b87 100644 --- a/cabal-install/tests/IntegrationTests2.hs +++ b/cabal-install/tests/IntegrationTests2.hs @@ -2075,6 +2075,7 @@ testConfigOptionComments = do "-- overwrite-policy" @=? findLineWith True "overwrite-policy" defaultConfigFile "-- install-method" @=? findLineWith True "install-method" defaultConfigFile "installdir" @=? findLineWith False "installdir" defaultConfigFile + "-- token" @=? findLineWith True "token" defaultConfigFile "-- username" @=? findLineWith True "username" defaultConfigFile "-- password" @=? findLineWith True "password" defaultConfigFile "-- password-command" @=? findLineWith True "password-command" defaultConfigFile diff --git a/cabal-install/tests/IntegrationTests2/config/default-config b/cabal-install/tests/IntegrationTests2/config/default-config index fab39496295..7ea0ac65d56 100644 --- a/cabal-install/tests/IntegrationTests2/config/default-config +++ b/cabal-install/tests/IntegrationTests2/config/default-config @@ -118,6 +118,7 @@ jobs: $ncpus -- overwrite-policy: -- install-method: installdir: /home/colton/.cabal/bin +-- token: -- username: -- password: -- password-command: diff --git a/cabal-install/tests/IntegrationTests2/nix-config/nix-false b/cabal-install/tests/IntegrationTests2/nix-config/nix-false index da13ba3874f..a85555fa5cd 100644 --- a/cabal-install/tests/IntegrationTests2/nix-config/nix-false +++ b/cabal-install/tests/IntegrationTests2/nix-config/nix-false @@ -111,6 +111,7 @@ jobs: $ncpus -- package-env: -- overwrite-policy: -- install-method: +-- token: -- username: -- password: -- password-command: diff --git a/cabal-install/tests/IntegrationTests2/nix-config/nix-true b/cabal-install/tests/IntegrationTests2/nix-config/nix-true index d45c34a5fdf..a7134c5c7fa 100644 --- a/cabal-install/tests/IntegrationTests2/nix-config/nix-true +++ b/cabal-install/tests/IntegrationTests2/nix-config/nix-true @@ -111,6 +111,7 @@ jobs: $ncpus -- package-env: -- overwrite-policy: -- install-method: +-- token: -- username: -- password: -- password-command: diff --git a/changelog.d/issue-6738 b/changelog.d/issue-6738 new file mode 100644 index 00000000000..d2bf4053756 --- /dev/null +++ b/changelog.d/issue-6738 @@ -0,0 +1,12 @@ +synopsis: Add support for authentication tokens for uploading to Hackage +packages: cabal-install +prs: #9058 +issues: #6738 + +description: { + +A new flag `--token` (`-t`) has been created. Token authentication takes +precedence over username and password meaning that, if a token is set, +the username and password flags are ignored. + +} \ No newline at end of file diff --git a/doc/cabal-commands.rst b/doc/cabal-commands.rst index 1c0776602ed..64077c97bb7 100644 --- a/doc/cabal-commands.rst +++ b/doc/cabal-commands.rst @@ -1109,6 +1109,12 @@ to Hackage. documentation for a published package (and not a candidate), add ``--publish``. +.. option:: -t TOKEN or -tTOKEN, --token=TOKEN + + Your Hackage authentication token. You can create and delete + authentication tokens on Hackage's `account management page + `__. + .. option:: -u USERNAME or -uUSERNAME, --username=USERNAME Your Hackage username. @@ -1138,6 +1144,12 @@ cabal report ``cabal report [FLAGS]`` uploads build reports to Hackage. +.. option:: -t TOKEN or -tTOKEN, --token=TOKEN + + Your Hackage authentication token. You can create and delete + authentication tokens on Hackage's `account management page + `__. + .. option:: -u USERNAME or -uUSERNAME, --username=USERNAME Your Hackage username.