From ba776fe088b2d98d114a7c1cd119a443d9add6d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20P=2E=20Ren=C3=A9=20de=20Cotret?= Date: Mon, 20 Jul 2026 16:32:18 -0400 Subject: [PATCH 1/2] New token-command option for cabal upload --- cabal-install/src/Distribution/Client/Config.hs | 1 + cabal-install/src/Distribution/Client/Main.hs | 14 +++++++++++--- cabal-install/src/Distribution/Client/Setup.hs | 16 ++++++++++++++++ cabal-install/tests/IntegrationTests2.hs | 1 + .../PackageTests/UserConfig/cabal.out | 3 +++ .../PackageTests/UserConfig/cabal.test.hs | 4 ++++ changelog.d/token-command.md | 14 ++++++++++++++ doc/cabal-commands.rst | 16 ++++++++++++++++ 8 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 changelog.d/token-command.md diff --git a/cabal-install/src/Distribution/Client/Config.hs b/cabal-install/src/Distribution/Client/Config.hs index c4c5a5dfba5..4ead5f68952 100644 --- a/cabal-install/src/Distribution/Client/Config.hs +++ b/cabal-install/src/Distribution/Client/Config.hs @@ -590,6 +590,7 @@ instance Semigroup SavedConfig where { uploadCandidate = combine uploadCandidate , uploadDoc = combine uploadDoc , uploadToken = combine uploadToken + , uploadTokenCmd = combine uploadTokenCmd , uploadUsername = combine uploadUsername , uploadPassword = combine uploadPassword , uploadPasswordCmd = combine uploadPasswordCmd diff --git a/cabal-install/src/Distribution/Client/Main.hs b/cabal-install/src/Distribution/Client/Main.hs index dc41e483a5e..dc055506f22 100644 --- a/cabal-install/src/Distribution/Client/Main.hs +++ b/cabal-install/src/Distribution/Client/Main.hs @@ -164,7 +164,7 @@ import Distribution.Client.Signal ( installTerminationHandler ) import Distribution.Client.Tar (createTarGzFile) -import Distribution.Client.Types.Credentials (Password (..)) +import Distribution.Client.Types.Credentials (Password (..), Token (..)) import qualified Distribution.Client.Upload as Upload import Distribution.Client.Utils ( determineNumJobs @@ -1349,6 +1349,14 @@ uploadAction uploadFlags extraArgs globalFlags = do when (null tarfiles && not (fromFlag (uploadDoc uploadFlags'))) $ dieWithException verbosity UploadAction checkTarFiles extraArgs + maybe_token <- + case uploadTokenCmd uploadFlags' of + Flag (xs : xss) -> + Just . Token + <$> getProgramInvocationOutput + verbosity + (simpleProgramInvocation xs xss) + _ -> pure $ flagToMaybe $ uploadToken uploadFlags' maybe_password <- case uploadPasswordCmd uploadFlags' of Flag (xs : xss) -> @@ -1367,7 +1375,7 @@ uploadAction uploadFlags extraArgs globalFlags = do Upload.uploadDoc verbosity filteredRepoContext - (flagToMaybe $ uploadToken uploadFlags') + maybe_token (flagToMaybe $ uploadUsername uploadFlags') maybe_password (fromFlag (uploadCandidate uploadFlags')) @@ -1376,7 +1384,7 @@ uploadAction uploadFlags extraArgs globalFlags = do Upload.upload verbosity filteredRepoContext - (flagToMaybe $ uploadToken uploadFlags') + maybe_token (flagToMaybe $ uploadUsername uploadFlags') maybe_password (fromFlag (uploadCandidate uploadFlags')) diff --git a/cabal-install/src/Distribution/Client/Setup.hs b/cabal-install/src/Distribution/Client/Setup.hs index 22ea26850d1..96ba0c46c6f 100644 --- a/cabal-install/src/Distribution/Client/Setup.hs +++ b/cabal-install/src/Distribution/Client/Setup.hs @@ -2852,6 +2852,7 @@ data UploadFlags = UploadFlags { uploadCandidate :: Flag IsCandidate , uploadDoc :: Flag Bool , uploadToken :: Flag Token + , uploadTokenCmd :: Flag [String] , uploadUsername :: Flag Username , uploadPassword :: Flag Password , uploadPasswordCmd :: Flag [String] @@ -2867,6 +2868,7 @@ defaultUploadFlags = { uploadCandidate = toFlag IsCandidate , uploadDoc = toFlag False , uploadToken = mempty + , uploadTokenCmd = mempty , uploadUsername = mempty , uploadPassword = mempty , uploadPasswordCmd = mempty @@ -2920,6 +2922,20 @@ uploadCommand = (toFlag . Token) (flagToList . fmap unToken) ) + , option + ['T'] + ["token-command"] + "Command to get Hackage authentication token." + uploadTokenCmd + (\v flags -> flags{uploadTokenCmd = v}) + ( reqArg + "COMMAND" + ( readP_to_E + ("Cannot parse command: " ++) + (Flag <$> parseSpaceList parseTokenQ) + ) + (flagElim [] (pure . unwords . fmap show)) + ) , option ['u'] ["username"] diff --git a/cabal-install/tests/IntegrationTests2.hs b/cabal-install/tests/IntegrationTests2.hs index 16bda21ca6e..5d693d605b2 100644 --- a/cabal-install/tests/IntegrationTests2.hs +++ b/cabal-install/tests/IntegrationTests2.hs @@ -2646,6 +2646,7 @@ testConfigOptionComments = do "-- install-method" `assertHasCommentLine` "install-method" "installdir" `assertHasLine` "installdir" "-- token" `assertHasCommentLine` "token" + "-- token-command" `assertHasCommentLine` "token-command" "-- username" `assertHasCommentLine` "username" "-- password" `assertHasCommentLine` "password" "-- password-command" `assertHasCommentLine` "password-command" diff --git a/cabal-testsuite/PackageTests/UserConfig/cabal.out b/cabal-testsuite/PackageTests/UserConfig/cabal.out index d9f6042e219..1a4ef0938d8 100644 --- a/cabal-testsuite/PackageTests/UserConfig/cabal.out +++ b/cabal-testsuite/PackageTests/UserConfig/cabal.out @@ -16,3 +16,6 @@ Writing merged config to /cabal.dist/cabal-config. # cabal user-config Renaming /cabal.dist/cabal-config to /cabal.dist/cabal-config.backup. Writing merged config to /cabal.dist/cabal-config. +# cabal user-config +Renaming /cabal.dist/cabal-config to /cabal.dist/cabal-config.backup. +Writing merged config to /cabal.dist/cabal-config. diff --git a/cabal-testsuite/PackageTests/UserConfig/cabal.test.hs b/cabal-testsuite/PackageTests/UserConfig/cabal.test.hs index 300bcc59ea5..6dbbd712d09 100644 --- a/cabal-testsuite/PackageTests/UserConfig/cabal.test.hs +++ b/cabal-testsuite/PackageTests/UserConfig/cabal.test.hs @@ -21,3 +21,7 @@ main = cabalTest $ do "user-config" ["update", "-f", "-a", "password-command: sh -c \"echo secret\""] -- non-quoted tokens do get quoted when writing, but this is expected assertFileDoesContain conf "password-command: \"sh\" \"-c\" \"echo secret\"" + + cabalG ["--config-file", conf] + "user-config" ["update", "-f", "-a", "token-command: sh -c \"echo secret token\""] + assertFileDoesContain conf "token-command: \"sh\" \"-c\" \"echo secret token\"" diff --git a/changelog.d/token-command.md b/changelog.d/token-command.md new file mode 100644 index 00000000000..dbb40f859a7 --- /dev/null +++ b/changelog.d/token-command.md @@ -0,0 +1,14 @@ +synopsis: Add option to cabal upload to run a command to get Hackage token +packages: cabal-install +prs: #12132 +description:{ +Introduce `--token-command` option to `cabal upload`, to run a command to get a Hackage authentication token rather than providing it in plaintext. + +This allows the use of a secrets manager to store Hackage tokens, like `--password-command`. + +The recommended way is to add the following to your cabal config file: + +``` +token-command: sh -c "..." +``` +} diff --git a/doc/cabal-commands.rst b/doc/cabal-commands.rst index eb239a9285a..a8431132148 100644 --- a/doc/cabal-commands.rst +++ b/doc/cabal-commands.rst @@ -1639,6 +1639,22 @@ to Hackage. authentication tokens on Hackage's `account management page `__. +.. option:: -T COMMAND or -TCOMMAND, --token-command=COMMAND + + Command to get your Hackage authentication token. This is useful if your + token is stored in a secrets manager, for example. Arguments with whitespace + must be quoted (double-quotes only). For example: + + :: + + --token-command='sh -c "op read op://hackage/upload-token"' + + Or in the config file: + + :: + + token-command: sh -c "op read op://hackage/upload-token" + .. option:: -u USERNAME or -uUSERNAME, --username=USERNAME Your Hackage username. From a15005fd822d41dcbdf6013715f934931390cbbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20P=2E=20Ren=C3=A9=20de=20Cotret?= Date: Mon, 20 Jul 2026 16:32:18 -0400 Subject: [PATCH 2/2] New token-command option for cabal upload --- changelog.d/token-command.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.d/token-command.md b/changelog.d/token-command.md index dbb40f859a7..23dd6f4e6a7 100644 --- a/changelog.d/token-command.md +++ b/changelog.d/token-command.md @@ -1,5 +1,6 @@ synopsis: Add option to cabal upload to run a command to get Hackage token packages: cabal-install +significance: significant prs: #12132 description:{ Introduce `--token-command` option to `cabal upload`, to run a command to get a Hackage authentication token rather than providing it in plaintext.