From 3414a69bc8e589a0fbfc52f932f6b7df8d05f365 Mon Sep 17 00:00:00 2001 From: Eugen Wissner Date: Tue, 7 Nov 2023 19:36:40 +0100 Subject: Support GHC 9.4 --- app/Main.hs | 421 ---------------------------------------- app/SlackBuilder/CommandLine.hs | 58 ------ app/SlackBuilder/Updater.hs | 158 --------------- lib/SlackBuilder/Download.hs | 2 +- slackbuilder.cabal | 4 +- src/Main.hs | 421 ++++++++++++++++++++++++++++++++++++++++ src/SlackBuilder/CommandLine.hs | 58 ++++++ src/SlackBuilder/Updater.hs | 158 +++++++++++++++ 8 files changed, 640 insertions(+), 640 deletions(-) delete mode 100644 app/Main.hs delete mode 100644 app/SlackBuilder/CommandLine.hs delete mode 100644 app/SlackBuilder/Updater.hs create mode 100644 src/Main.hs create mode 100644 src/SlackBuilder/CommandLine.hs create mode 100644 src/SlackBuilder/Updater.hs diff --git a/app/Main.hs b/app/Main.hs deleted file mode 100644 index 535b655..0000000 --- a/app/Main.hs +++ /dev/null @@ -1,421 +0,0 @@ -module Main - ( main - ) where - -import Data.Char (isNumber) -import Control.Applicative (Applicative(liftA2)) -import Data.List.NonEmpty (NonEmpty(..)) -import qualified Data.List.NonEmpty as NonEmpty -import Control.Monad.IO.Class (MonadIO(..)) -import Data.Maybe (fromJust) -import qualified Data.Map as Map -import Options.Applicative (execParser) -import SlackBuilder.CommandLine -import SlackBuilder.Config -import SlackBuilder.Trans -import SlackBuilder.Updater -import qualified Toml -import qualified Data.ByteString as ByteString -import Data.Text (Text) -import qualified Data.Text as Text -import qualified Data.Text.IO as Text.IO -import Control.Monad.Trans.Reader (ReaderT(..), asks) -import SlackBuilder.Download -import SlackBuilder.Package (Package(..)) -import qualified SlackBuilder.Package as Package -import Text.URI (URI(..), mkURI) -import Text.URI.QQ (uri) -import Data.Foldable (Foldable(..), for_, find) -import qualified Text.URI as URI -import System.FilePath ((), (<.>), dropExtension, takeBaseName, makeRelative, splitFileName) -import SlackBuilder.Info -import Text.Megaparsec (parse, errorBundlePretty) -import GHC.Records (HasField(..)) -import System.Process - ( CmdSpec(..) - , CreateProcess(..) - , StdStream(..) - , callProcess - , withCreateProcess - , waitForProcess - ) -import System.Console.ANSI - ( setSGR - , SGR(..) - , ColorIntensity(..) - , Color(..) - , ConsoleLayer(..) - ) -import System.Directory (listDirectory, doesDirectoryExist) -import Control.Monad (filterM) -import Data.List (isPrefixOf, isSuffixOf) - -autoUpdatable :: [Package] -autoUpdatable = - [ Package - { latest = - let ghArguments = GhArguments{ owner = "universal-ctags", name = "ctags", transform = Nothing} - latest' = latestGitHub ghArguments pure - templateTail = - [ Package.VersionPlaceholder - , Package.StaticPlaceholder "/ctags-" - , Package.VersionPlaceholder - , Package.StaticPlaceholder ".tar.gz" - ] - template = Package.DownloadTemplate - $ Package.StaticPlaceholder "https://github.com/universal-ctags/ctags/archive/" - :| templateTail - in Package.Updater latest' $ reuploadWithTemplate template [] - , category = "development" - , name = "universal-ctags" - , downloaders = mempty - } - , Package - { latest = - let packagistArguments = PackagistArguments{ vendor = "composer", name = "composer" } - latest' = latestPackagist packagistArguments - template = Package.DownloadTemplate - $ Package.StaticPlaceholder "https://getcomposer.org/download/" - :| [Package.VersionPlaceholder, Package.StaticPlaceholder "/composer.phar"] - in Package.Updater latest' $ downloadWithTemplate template - , category = "development" - , name = "composer" - , downloaders = mempty - } - , Package - { latest = - let ghArguments = GhArguments - { owner = "jitsi" - , name = "jitsi-meet-electron" - , transform = Nothing - } - latest' = latestGitHub ghArguments $ Text.stripPrefix "v" - template = Package.DownloadTemplate - $ Package.StaticPlaceholder "https://github.com/jitsi/jitsi-meet-electron/releases/download/v" - :| Package.VersionPlaceholder - : [Package.StaticPlaceholder "/jitsi-meet-x86_64.AppImage"] - in Package.Updater latest' $ downloadWithTemplate template - , category = "network" - , name = "jitsi-meet-desktop" - , downloaders = mempty - } - , Package - { latest = - let ghArguments = GhArguments - { owner = "php" - , name = "php-src" - , transform = Nothing - } - checkVersion x - | not $ Text.isInfixOf "RC" x - , Text.isPrefixOf "php-8.2." x = Text.stripPrefix "php-" x - | otherwise = Nothing - latest' = latestGitHub ghArguments checkVersion - template = Package.DownloadTemplate - $ Package.StaticPlaceholder "https://www.php.net/distributions/php-" - :| Package.VersionPlaceholder - : [Package.StaticPlaceholder ".tar.xz"] - in Package.Updater latest' $ downloadWithTemplate template - , category = "development" - , name = "php82" - , downloaders = mempty - } - , Package - { latest = - let ghArguments = GhArguments - { owner = "kovidgoyal" - , name = "kitty" - , transform = Nothing - } - latest' = latestGitHub ghArguments $ Text.stripPrefix "v" - templateTail = - [ Package.StaticPlaceholder "/kitty-" - , Package.VersionPlaceholder - , Package.StaticPlaceholder ".tar.xz" - ] - template = Package.DownloadTemplate - $ Package.StaticPlaceholder "https://github.com/kovidgoyal/kitty/releases/download/v" - :| Package.VersionPlaceholder - : templateTail - in Package.Updater latest' $ reuploadWithTemplate template [RawCommand "go" ["mod", "vendor"]] - , category = "system" - , name = "kitty" - , downloaders = mempty - } - , Package - { latest = - let ghArguments = GhArguments - { owner = "rdiff-backup" - , name = "rdiff-backup" - , transform = Nothing - } - latest' = latestGitHub ghArguments $ Text.stripPrefix "v" - template = Package.DownloadTemplate - $ Package.StaticPlaceholder "https://github.com/rdiff-backup/rdiff-backup/releases/download/v" - :| Package.VersionPlaceholder - : Package.StaticPlaceholder "/rdiff-backup-" - : Package.VersionPlaceholder - : [Package.StaticPlaceholder ".tar.gz"] - in Package.Updater latest' $ reuploadWithTemplate template [] - , category = "system" - , name = "rdiff-backup" - , downloaders = mempty - } - , Package - { latest = - let needle = "Linux—" - textArguments = TextArguments - { textURL = "https://help.webex.com/en-us/article/mqkve8/Webex-App-%7C-Release-notes" - , versionPicker = Text.takeWhile (liftA2 (||) (== '.') isNumber) - . Text.drop (Text.length needle) - . snd - . Text.breakOn needle - } - latest' = latestText textArguments - template = Package.DownloadTemplate $ pure - $ Package.StaticPlaceholder - "https://binaries.webex.com/WebexDesktop-Ubuntu-Official-Package/Webex.deb" - in Package.Updater latest' $ downloadWithTemplate template - , category = "network" - , name = "webex" - , downloaders = mempty - } - , Package - { latest = - let ghArguments = GhArguments - { owner = "librsync" - , name = "librsync" - , transform = Nothing - } - latest' = latestGitHub ghArguments $ Text.stripPrefix "v" - template = Package.DownloadTemplate - $ Package.StaticPlaceholder "https://github.com/librsync/librsync/archive/v" - :| Package.VersionPlaceholder - : Package.StaticPlaceholder "/librsync-" - : Package.VersionPlaceholder - : [Package.StaticPlaceholder ".tar.gz"] - in Package.Updater latest' $ reuploadWithTemplate template [] - , category = "libraries" - , name = "librsync" - , downloaders = mempty - } - , Package - { latest = - let textArguments = TextArguments - { textURL = "https://downloads.dlang.org/releases/LATEST" - , versionPicker = Text.strip - } - latest' = latestText textArguments - template = Package.DownloadTemplate - $ Package.StaticPlaceholder "https://downloads.dlang.org/releases/2.x/" - :| Package.VersionPlaceholder - : Package.StaticPlaceholder "/dmd." - : Package.VersionPlaceholder - : [Package.StaticPlaceholder ".linux.tar.xz"] - in Package.Updater latest' $ downloadWithTemplate template - , category = "development" - , name = "dmd" - , downloaders = mempty - } - , Package - { latest = - let textArguments = TextArguments - { textURL = "https://downloads.dlang.org/releases/LATEST" - , versionPicker = Text.strip - } - latest' = latestText textArguments - template = Package.DownloadTemplate - $ Package.StaticPlaceholder "https://codeload.github.com/dlang/tools/tar.gz/v" - :| [Package.VersionPlaceholder] - in Package.Updater latest' $ reuploadWithTemplate template [] - , category = "development" - , name = "d-tools" - , downloaders = - let dubArguments = GhArguments{ owner = "dlang", name = "dub", transform = Nothing} - dscannerArguments = GhArguments{ owner = "dlang-community", name = "D-Scanner", transform = Nothing } - dcdArguments = GhArguments{ owner = "dlang-community", name = "DCD", transform = Nothing } - latestDub = latestGitHub dubArguments pure - latestDscanner = latestGitHub dscannerArguments pure - latestDcd = latestGitHub dcdArguments pure - dubTemplate = Package.DownloadTemplate - $ Package.StaticPlaceholder "https://codeload.github.com/dlang/dub/tar.gz/v" - :| [Package.VersionPlaceholder] - dscannerURI = [uri|https://github.com/dlang-community/D-Scanner.git|] - dcdURI = [uri|https://github.com/dlang-community/DCD.git|] - in Map.fromList - [ ("DUB", Package.Updater latestDub $ downloadWithTemplate dubTemplate) - , ("DSCANNER", Package.Updater latestDscanner $ cloneFromGit dscannerURI "v") - , ("DCD", Package.Updater latestDcd $ cloneFromGit dcdURI "v") - ] - } - ] - -up2Date :: SlackBuilderT () -up2Date = for_ autoUpdatable go - where - go package = getAndLogLatest package - >>= mapM_ (updatePackageIfRequired package) - >> liftIO (putStrLn "") - getAndLogLatest Package{ latest = Package.Updater{ detectLatest }, name } - = liftIO (putStrLn $ Text.unpack name <> ": Retreiving the latest version.") - >> detectLatest - -updatePackageIfRequired :: Package -> Text -> SlackBuilderT () -updatePackageIfRequired package@Package{..} version = do - let packagePath = Text.unpack category Text.unpack name (Text.unpack name <.> "info") - repository' <- SlackBuilderT $ asks repository - infoContents <- liftIO $ ByteString.readFile $ repository' packagePath - - case parse parseInfoFile packagePath infoContents of - Right parsedInfoFile - | version == getField @"version" parsedInfoFile -> - liftIO $ do - setSGR [SetColor Foreground Dull Green] - Text.IO.putStrLn - $ name <> " is up to date (Version " <> version <> ")." - setSGR [Reset] - | otherwise -> do - liftIO $ do - setSGR [SetColor Foreground Dull Yellow] - Text.IO.putStrLn - $ "A new version of " - <> name <> " " <> getField @"version" parsedInfoFile - <> " is available (" <> version <> ")." - setSGR [Reset] - updatePackage package parsedInfoFile version - Left errorBundle -> liftIO $ putStr $ errorBundlePretty errorBundle - -updateDownload :: Package -> Package.Updater -> SlackBuilderT (Package.Download, Text) -updateDownload Package{..} Package.Updater{..} = do - latestDownloadVersion <- fromJust <$> detectLatest - result <- getVersion (Text.pack $ Text.unpack category Text.unpack name) latestDownloadVersion - pure (result, latestDownloadVersion) - -cloneFromGit :: URI -> Text -> Text -> Text -> SlackBuilderT Package.Download -cloneFromGit repo tagPrefix packagePath version = do - repository' <- SlackBuilderT $ asks repository - let downloadFileName = URI.unRText - $ NonEmpty.last $ snd $ fromJust $ URI.uriPath repo - relativeTarball = Text.unpack packagePath - (dropExtension (Text.unpack downloadFileName) <> "-" <> Text.unpack version) - tarball = repository' relativeTarball - name' = Text.pack (takeBaseName $ Text.unpack packagePath) - checksum <- clone (URI.render repo) (Text.pack tarball) tagPrefix - uploadCommand (Text.pack relativeTarball) ("/" <> name') - (flip . flip Package.Download) (fromJust checksum) False - <$> liftIO (mkURI $ "https://download.dlackware.com/hosted-sources/" <> name' <> "/" <> downloadFileName) - -downloadWithTemplate :: Package.DownloadTemplate -> Text -> Text -> SlackBuilderT Package.Download -downloadWithTemplate downloadTemplate packagePath version = do - repository' <- SlackBuilderT $ asks repository - uri' <- liftIO $ Package.renderDownloadWithVersion downloadTemplate version - let downloadFileName = URI.unRText - $ NonEmpty.last $ snd $ fromJust $ URI.uriPath uri' - relativeTarball = packagePath <> "/" <> downloadFileName - tarball = repository' Text.unpack relativeTarball - checksum <- fromJust <$> download uri' tarball - pure $ Package.Download uri' checksum False - -reuploadWithTemplate :: Package.DownloadTemplate -> [CmdSpec] -> Text -> Text -> SlackBuilderT Package.Download -reuploadWithTemplate downloadTemplate commands packagePath version = do - Package.Download{ download = uri', md5sum = checksum } <- downloadWithTemplate downloadTemplate packagePath version - let downloadFileName = URI.unRText - $ NonEmpty.last $ snd $ fromJust $ URI.uriPath uri' - relativeTarball = packagePath <> "/" <> downloadFileName - download' <- handleReupload relativeTarball downloadFileName - - pure $ Package.Download download' checksum False - where - name' = Text.pack $ takeBaseName $ Text.unpack packagePath - handleReupload relativeTarball downloadFileName = do - repository' <- SlackBuilderT $ asks repository - case commands of - [] -> uploadTarball relativeTarball downloadFileName - _ -> - let tarballPath = repository' Text.unpack relativeTarball - packedDirectory = takeBaseName $ dropExtension tarballPath - in liftIO (callProcess "tar" ["xvf", tarballPath]) - >> liftIO (traverse (defaultCreateProcess packedDirectory) commands) - >> liftIO (callProcess "tar" ["Jcvf", tarballPath, packedDirectory]) - >> uploadTarball relativeTarball downloadFileName - uploadTarball relativeTarball downloadFileName - = liftIO (putStrLn $ "Upload the source tarball " <> Text.unpack relativeTarball) - >> uploadCommand relativeTarball ("/" <> name') - >> liftIO (mkURI $ "https://download.dlackware.com/hosted-sources/" <> name' <> "/" <> downloadFileName) - defaultCreateProcess cwd' cmdSpec - = flip withCreateProcess (const . const . const waitForProcess) - $ CreateProcess - { use_process_jobs = False - , std_out = Inherit - , std_in = NoStream - , std_err = Inherit - , new_session = False - , env = Nothing - , detach_console = False - , delegate_ctlc = False - , cwd = Just cwd' - , create_new_console = False - , create_group = False - , cmdspec = cmdSpec - , close_fds = True - , child_user = Nothing - , child_group = Nothing - } - -renderAndDownload :: Package -> Text -> SlackBuilderT Package.Download -renderAndDownload Package{..} version = do - let packagePath = category <> "/" <> name - Package.Updater _ getVersion = latest - - getVersion packagePath version - -updatePackage :: Package -> PackageInfo -> Text -> SlackBuilderT () -updatePackage package@Package{..} info version = do - let packagePath = category <> "/" <> name - - repository' <- SlackBuilderT $ asks repository - mainDownload <- renderAndDownload package version - moreDownloads <- traverse (updateDownload package) downloaders - let allDownloads = mainDownload : toList (fst <$> moreDownloads) - let infoFilePath = repository' Text.unpack packagePath - (Text.unpack name <.> "info") - package' = info - { version = version - , downloads = getField @"download" <$> allDownloads - , checksums = getField @"md5sum" <$> allDownloads - } - liftIO $ Text.IO.writeFile infoFilePath $ generate package' - updateSlackBuildVersion packagePath version $ snd <$> moreDownloads - - commit packagePath version - -findCategory :: FilePath -> IO [FilePath] -findCategory currentDirectory = do - contents <- liftIO $ listDirectory currentDirectory - case find (isSuffixOf ".info") contents of - Just _ -> pure [currentDirectory] - Nothing -> do - let contents' = (currentDirectory ) <$> filter (not . isPrefixOf ".") contents - directories <- filterM doesDirectoryExist contents' - subCategories <- traverse findCategory directories - pure $ concat subCategories - -main :: IO () -main = do - programCommand <- execParser slackBuilderParser - settings <- Toml.decodeFile settingsCodec "config/config.toml" - latestVersion <- flip runReaderT settings - $ runSlackBuilderT - $ executeCommand programCommand - - maybe (pure ()) Text.IO.putStrLn latestVersion - where - executeCommand = \case - CategoryCommand _packageName -> do - repository' <- SlackBuilderT $ asks repository - categories <- liftIO $ findCategory repository' - liftIO $ print $ splitFileName . makeRelative repository' <$> categories - pure Nothing - CloneCommand repo tarball tagPrefix -> fmap (Text.pack . show) - <$> clone repo tarball tagPrefix - Up2DateCommand -> up2Date >> pure Nothing diff --git a/app/SlackBuilder/CommandLine.hs b/app/SlackBuilder/CommandLine.hs deleted file mode 100644 index 7cfe747..0000000 --- a/app/SlackBuilder/CommandLine.hs +++ /dev/null @@ -1,58 +0,0 @@ -module SlackBuilder.CommandLine - ( GhArguments(..) - , SlackBuilderCommand(..) - , PackagistArguments(..) - , TextArguments(..) - , slackBuilderParser - ) where - -import Data.Text (Text) -import Options.Applicative - ( Parser - , ParserInfo(..) - , metavar - , argument - , str - , info - , fullDesc - , subparser - , command, - ) - -data SlackBuilderCommand - = CategoryCommand Text - | CloneCommand Text Text Text - | Up2DateCommand - -data PackagistArguments = PackagistArguments - { vendor :: Text - , name :: Text - } deriving (Eq, Show) - -data GhArguments = GhArguments - { owner :: Text - , name :: Text - , transform :: Maybe Text - } deriving (Eq, Show) - -data TextArguments = TextArguments - { versionPicker :: Text -> Text - , textURL :: Text - } - -slackBuilderParser :: ParserInfo SlackBuilderCommand -slackBuilderParser = info slackBuilderCommand fullDesc - -slackBuilderCommand :: Parser SlackBuilderCommand -slackBuilderCommand = subparser - $ command "category" (info categoryCommand mempty) - <> command "clone" (info cloneCommand mempty) - <> command "up2date" (info up2DateCommand mempty) - where - categoryCommand = CategoryCommand - <$> argument str (metavar "PKGNAM") - cloneCommand = CloneCommand - <$> argument str (metavar "REPO") - <*> argument str (metavar "TARBALL") - <*> argument str (metavar "TAG_PREFIX") - up2DateCommand = pure Up2DateCommand diff --git a/app/SlackBuilder/Updater.hs b/app/SlackBuilder/Updater.hs deleted file mode 100644 index 1ebf7fe..0000000 --- a/app/SlackBuilder/Updater.hs +++ /dev/null @@ -1,158 +0,0 @@ -module SlackBuilder.Updater - ( latestGitHub - , latestPackagist - , latestText - ) where - -import SlackBuilder.Config -import qualified Data.Aeson as Aeson -import Data.Aeson ((.:)) -import Data.Aeson.TH (defaultOptions, deriveJSON) -import Data.HashMap.Strict (HashMap) -import qualified Data.HashMap.Strict as HashMap -import Data.Text (Text) -import qualified Data.Text as Text -import qualified Data.Text.Encoding as Text.Encoding -import Data.Vector (Vector, (!?)) -import qualified Data.Vector as Vector -import Network.HTTP.Req - ( header - , runReq - , defaultHttpConfig - , req - , GET(..) - , https - , jsonResponse - , NoReqBody(..) - , (/:) - , responseBody - , useHttpsURI - , bsResponse - , POST(..) - , ReqBodyJson(..) - ) -import Text.URI (mkURI) -import SlackBuilder.CommandLine -import SlackBuilder.Trans -import qualified Data.Aeson.KeyMap as KeyMap -import GHC.Records (HasField(..)) -import Control.Monad.Trans.Reader (asks) -import Control.Monad.IO.Class (MonadIO(..)) - -newtype PackagistPackage = PackagistPackage - { version :: Text - } deriving (Eq, Show) - -$(deriveJSON defaultOptions ''PackagistPackage) - -newtype PackagistResponse = PackagistResponse - { packages :: HashMap Text (Vector PackagistPackage) - } deriving (Eq, Show) - -$(deriveJSON defaultOptions ''PackagistResponse) - -newtype GhRefNode = GhRefNode - { name :: Text - } deriving (Eq, Show) - -$(deriveJSON defaultOptions ''GhRefNode) - -newtype GhRef = GhRef - { nodes :: Vector GhRefNode - } deriving (Eq, Show) - -$(deriveJSON defaultOptions ''GhRef) - -newtype GhRepository = GhRepository - { refs :: GhRef - } deriving (Eq, Show) - -$(deriveJSON defaultOptions ''GhRepository) - -newtype GhData = GhData - { repository :: GhRepository - } deriving (Eq, Show) - -instance Aeson.FromJSON GhData where - parseJSON (Aeson.Object keyMap) - | Just data' <- KeyMap.lookup "data" keyMap = - GhData <$> Aeson.withObject "GhData" (.: "repository") data' - parseJSON _ = fail "data key not found in the response" - -data GhVariables = GhVariables - { name :: Text - , owner :: Text - } deriving (Eq, Show) - -$(deriveJSON defaultOptions ''GhVariables) - -data GhQuery = GhQuery - { query :: Text - , variables :: GhVariables - } deriving (Eq, Show) - -$(deriveJSON defaultOptions ''GhQuery) - -latestPackagist :: PackagistArguments -> SlackBuilderT (Maybe Text) -latestPackagist PackagistArguments{..} = do - packagistResponse <- runReq defaultHttpConfig $ - let uri = https "repo.packagist.org" /: "p2" - /: vendor - /: name <> ".json" - in req GET uri NoReqBody jsonResponse mempty - let packagistPackages = packages $ responseBody packagistResponse - fullName = Text.intercalate "/" [vendor, name] - - pure $ HashMap.lookup fullName packagistPackages - >>= fmap (version . fst) . Vector.uncons - -latestText :: TextArguments -> SlackBuilderT (Maybe Text) -latestText TextArguments{..} = do - uri <- liftIO $ useHttpsURI <$> mkURI textURL - packagistResponse <- traverse (runReq defaultHttpConfig . go . fst) uri - - pure $ versionPicker . Text.Encoding.decodeUtf8 . responseBody - <$> packagistResponse - where - go uri = req GET uri NoReqBody bsResponse mempty - -latestGitHub - :: GhArguments - -> (Text -> Maybe Text) - -> SlackBuilderT (Maybe Text) -latestGitHub GhArguments{..} versionTransform = do - ghToken' <- SlackBuilderT $ asks ghToken - ghResponse <- runReq defaultHttpConfig $ - let uri = https "api.github.com" /: "graphql" - query = GhQuery - { query = githubQuery - , variables = GhVariables - { owner = owner - , name = name - } - } - authorizationHeader = header "authorization" - $ Text.Encoding.encodeUtf8 - $ "Bearer " <> ghToken' - in req POST uri (ReqBodyJson query) jsonResponse - $ authorizationHeader <> header "User-Agent" "SlackBuilder" - let ghNodes = nodes - $ refs - $ (getField @"repository" :: GhData -> GhRepository) - $ responseBody ghResponse - refs' = Vector.reverse - $ Vector.catMaybes - $ versionTransform . getField @"name" <$> ghNodes - pure $ refs' !? 0 - where - githubQuery = - "query ($name: String!, $owner: String!) {\n\ - \ repository(name: $name, owner: $owner) {\n\ - \ refs(last: 10, refPrefix: \"refs/tags/\", orderBy: { field: TAG_COMMIT_DATE, direction: ASC }) {\n\ - \ nodes {\n\ - \ id,\n\ - \ name\n\ - \ }\n\ - \ }\n\ - \ }\n\ - \}" diff --git a/lib/SlackBuilder/Download.hs b/lib/SlackBuilder/Download.hs index 7729b0b..2dc503d 100644 --- a/lib/SlackBuilder/Download.hs +++ b/lib/SlackBuilder/Download.hs @@ -77,7 +77,7 @@ updateSlackBuildVersion packagePath version additionalDownloads = do updateLineDependencyVersion dependencyName = updateLineVariable $ dependencyName <> "_VERSION" updateLineVariable variableName variableValue line - | Text.isSuffixOf (variableName <> "=") line = + | Text.isPrefixOf (variableName <> "=") line = variableName <> "=${" <> variableName <> ":-" <> variableValue <> "}" | otherwise = line diff --git a/slackbuilder.cabal b/slackbuilder.cabal index a6748d4..3334aa0 100644 --- a/slackbuilder.cabal +++ b/slackbuilder.cabal @@ -17,7 +17,7 @@ extra-source-files: CHANGELOG.md common dependencies build-depends: - base ^>= 4.16.4.0, + base >= 4.16 && < 5, bytestring ^>= 0.11.0, containers ^>= 0.6, cryptonite >= 0.30, @@ -77,7 +77,7 @@ executable slackbuilder slackbuilder, unordered-containers ^>= 0.2.19, vector ^>= 0.13.0 - hs-source-dirs: app + hs-source-dirs: src ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall diff --git a/src/Main.hs b/src/Main.hs new file mode 100644 index 0000000..535b655 --- /dev/null +++ b/src/Main.hs @@ -0,0 +1,421 @@ +module Main + ( main + ) where + +import Data.Char (isNumber) +import Control.Applicative (Applicative(liftA2)) +import Data.List.NonEmpty (NonEmpty(..)) +import qualified Data.List.NonEmpty as NonEmpty +import Control.Monad.IO.Class (MonadIO(..)) +import Data.Maybe (fromJust) +import qualified Data.Map as Map +import Options.Applicative (execParser) +import SlackBuilder.CommandLine +import SlackBuilder.Config +import SlackBuilder.Trans +import SlackBuilder.Updater +import qualified Toml +import qualified Data.ByteString as ByteString +import Data.Text (Text) +import qualified Data.Text as Text +import qualified Data.Text.IO as Text.IO +import Control.Monad.Trans.Reader (ReaderT(..), asks) +import SlackBuilder.Download +import SlackBuilder.Package (Package(..)) +import qualified SlackBuilder.Package as Package +import Text.URI (URI(..), mkURI) +import Text.URI.QQ (uri) +import Data.Foldable (Foldable(..), for_, find) +import qualified Text.URI as URI +import System.FilePath ((), (<.>), dropExtension, takeBaseName, makeRelative, splitFileName) +import SlackBuilder.Info +import Text.Megaparsec (parse, errorBundlePretty) +import GHC.Records (HasField(..)) +import System.Process + ( CmdSpec(..) + , CreateProcess(..) + , StdStream(..) + , callProcess + , withCreateProcess + , waitForProcess + ) +import System.Console.ANSI + ( setSGR + , SGR(..) + , ColorIntensity(..) + , Color(..) + , ConsoleLayer(..) + ) +import System.Directory (listDirectory, doesDirectoryExist) +import Control.Monad (filterM) +import Data.List (isPrefixOf, isSuffixOf) + +autoUpdatable :: [Package] +autoUpdatable = + [ Package + { latest = + let ghArguments = GhArguments{ owner = "universal-ctags", name = "ctags", transform = Nothing} + latest' = latestGitHub ghArguments pure + templateTail = + [ Package.VersionPlaceholder + , Package.StaticPlaceholder "/ctags-" + , Package.VersionPlaceholder + , Package.StaticPlaceholder ".tar.gz" + ] + template = Package.DownloadTemplate + $ Package.StaticPlaceholder "https://github.com/universal-ctags/ctags/archive/" + :| templateTail + in Package.Updater latest' $ reuploadWithTemplate template [] + , category = "development" + , name = "universal-ctags" + , downloaders = mempty + } + , Package + { latest = + let packagistArguments = PackagistArguments{ vendor = "composer", name = "composer" } + latest' = latestPackagist packagistArguments + template = Package.DownloadTemplate + $ Package.StaticPlaceholder "https://getcomposer.org/download/" + :| [Package.VersionPlaceholder, Package.StaticPlaceholder "/composer.phar"] + in Package.Updater latest' $ downloadWithTemplate template + , category = "development" + , name = "composer" + , downloaders = mempty + } + , Package + { latest = + let ghArguments = GhArguments + { owner = "jitsi" + , name = "jitsi-meet-electron" + , transform = Nothing + } + latest' = latestGitHub ghArguments $ Text.stripPrefix "v" + template = Package.DownloadTemplate + $ Package.StaticPlaceholder "https://github.com/jitsi/jitsi-meet-electron/releases/download/v" + :| Package.VersionPlaceholder + : [Package.StaticPlaceholder "/jitsi-meet-x86_64.AppImage"] + in Package.Updater latest' $ downloadWithTemplate template + , category = "network" + , name = "jitsi-meet-desktop" + , downloaders = mempty + } + , Package + { latest = + let ghArguments = GhArguments + { owner = "php" + , name = "php-src" + , transform = Nothing + } + checkVersion x + | not $ Text.isInfixOf "RC" x + , Text.isPrefixOf "php-8.2." x = Text.stripPrefix "php-" x + | otherwise = Nothing + latest' = latestGitHub ghArguments checkVersion + template = Package.DownloadTemplate + $ Package.StaticPlaceholder "https://www.php.net/distributions/php-" + :| Package.VersionPlaceholder + : [Package.StaticPlaceholder ".tar.xz"] + in Package.Updater latest' $ downloadWithTemplate template + , category = "development" + , name = "php82" + , downloaders = mempty + } + , Package + { latest = + let ghArguments = GhArguments + { owner = "kovidgoyal" + , name = "kitty" + , transform = Nothing + } + latest' = latestGitHub ghArguments $ Text.stripPrefix "v" + templateTail = + [ Package.StaticPlaceholder "/kitty-" + , Package.VersionPlaceholder + , Package.StaticPlaceholder ".tar.xz" + ] + template = Package.DownloadTemplate + $ Package.StaticPlaceholder "https://github.com/kovidgoyal/kitty/releases/download/v" + :| Package.VersionPlaceholder + : templateTail + in Package.Updater latest' $ reuploadWithTemplate template [RawCommand "go" ["mod", "vendor"]] + , category = "system" + , name = "kitty" + , downloaders = mempty + } + , Package + { latest = + let ghArguments = GhArguments + { owner = "rdiff-backup" + , name = "rdiff-backup" + , transform = Nothing + } + latest' = latestGitHub ghArguments $ Text.stripPrefix "v" + template = Package.DownloadTemplate + $ Package.StaticPlaceholder "https://github.com/rdiff-backup/rdiff-backup/releases/download/v" + :| Package.VersionPlaceholder + : Package.StaticPlaceholder "/rdiff-backup-" + : Package.VersionPlaceholder + : [Package.StaticPlaceholder ".tar.gz"] + in Package.Updater latest' $ reuploadWithTemplate template [] + , category = "system" + , name = "rdiff-backup" + , downloaders = mempty + } + , Package + { latest = + let needle = "Linux—" + textArguments = TextArguments + { textURL = "https://help.webex.com/en-us/article/mqkve8/Webex-App-%7C-Release-notes" + , versionPicker = Text.takeWhile (liftA2 (||) (== '.') isNumber) + . Text.drop (Text.length needle) + . snd + . Text.breakOn needle + } + latest' = latestText textArguments + template = Package.DownloadTemplate $ pure + $ Package.StaticPlaceholder + "https://binaries.webex.com/WebexDesktop-Ubuntu-Official-Package/Webex.deb" + in Package.Updater latest' $ downloadWithTemplate template + , category = "network" + , name = "webex" + , downloaders = mempty + } + , Package + { latest = + let ghArguments = GhArguments + { owner = "librsync" + , name = "librsync" + , transform = Nothing + } + latest' = latestGitHub ghArguments $ Text.stripPrefix "v" + template = Package.DownloadTemplate + $ Package.StaticPlaceholder "https://github.com/librsync/librsync/archive/v" + :| Package.VersionPlaceholder + : Package.StaticPlaceholder "/librsync-" + : Package.VersionPlaceholder + : [Package.StaticPlaceholder ".tar.gz"] + in Package.Updater latest' $ reuploadWithTemplate template [] + , category = "libraries" + , name = "librsync" + , downloaders = mempty + } + , Package + { latest = + let textArguments = TextArguments + { textURL = "https://downloads.dlang.org/releases/LATEST" + , versionPicker = Text.strip + } + latest' = latestText textArguments + template = Package.DownloadTemplate + $ Package.StaticPlaceholder "https://downloads.dlang.org/releases/2.x/" + :| Package.VersionPlaceholder + : Package.StaticPlaceholder "/dmd." + : Package.VersionPlaceholder + : [Package.StaticPlaceholder ".linux.tar.xz"] + in Package.Updater latest' $ downloadWithTemplate template + , category = "development" + , name = "dmd" + , downloaders = mempty + } + , Package + { latest = + let textArguments = TextArguments + { textURL = "https://downloads.dlang.org/releases/LATEST" + , versionPicker = Text.strip + } + latest' = latestText textArguments + template = Package.DownloadTemplate + $ Package.StaticPlaceholder "https://codeload.github.com/dlang/tools/tar.gz/v" + :| [Package.VersionPlaceholder] + in Package.Updater latest' $ reuploadWithTemplate template [] + , category = "development" + , name = "d-tools" + , downloaders = + let dubArguments = GhArguments{ owner = "dlang", name = "dub", transform = Nothing} + dscannerArguments = GhArguments{ owner = "dlang-community", name = "D-Scanner", transform = Nothing } + dcdArguments = GhArguments{ owner = "dlang-community", name = "DCD", transform = Nothing } + latestDub = latestGitHub dubArguments pure + latestDscanner = latestGitHub dscannerArguments pure + latestDcd = latestGitHub dcdArguments pure + dubTemplate = Package.DownloadTemplate + $ Package.StaticPlaceholder "https://codeload.github.com/dlang/dub/tar.gz/v" + :| [Package.VersionPlaceholder] + dscannerURI = [uri|https://github.com/dlang-community/D-Scanner.git|] + dcdURI = [uri|https://github.com/dlang-community/DCD.git|] + in Map.fromList + [ ("DUB", Package.Updater latestDub $ downloadWithTemplate dubTemplate) + , ("DSCANNER", Package.Updater latestDscanner $ cloneFromGit dscannerURI "v") + , ("DCD", Package.Updater latestDcd $ cloneFromGit dcdURI "v") + ] + } + ] + +up2Date :: SlackBuilderT () +up2Date = for_ autoUpdatable go + where + go package = getAndLogLatest package + >>= mapM_ (updatePackageIfRequired package) + >> liftIO (putStrLn "") + getAndLogLatest Package{ latest = Package.Updater{ detectLatest }, name } + = liftIO (putStrLn $ Text.unpack name <> ": Retreiving the latest version.") + >> detectLatest + +updatePackageIfRequired :: Package -> Text -> SlackBuilderT () +updatePackageIfRequired package@Package{..} version = do + let packagePath = Text.unpack category Text.unpack name (Text.unpack name <.> "info") + repository' <- SlackBuilderT $ asks repository + infoContents <- liftIO $ ByteString.readFile $ repository' packagePath + + case parse parseInfoFile packagePath infoContents of + Right parsedInfoFile + | version == getField @"version" parsedInfoFile -> + liftIO $ do + setSGR [SetColor Foreground Dull Green] + Text.IO.putStrLn + $ name <> " is up to date (Version " <> version <> ")." + setSGR [Reset] + | otherwise -> do + liftIO $ do + setSGR [SetColor Foreground Dull Yellow] + Text.IO.putStrLn + $ "A new version of " + <> name <> " " <> getField @"version" parsedInfoFile + <> " is available (" <> version <> ")." + setSGR [Reset] + updatePackage package parsedInfoFile version + Left errorBundle -> liftIO $ putStr $ errorBundlePretty errorBundle + +updateDownload :: Package -> Package.Updater -> SlackBuilderT (Package.Download, Text) +updateDownload Package{..} Package.Updater{..} = do + latestDownloadVersion <- fromJust <$> detectLatest + result <- getVersion (Text.pack $ Text.unpack category Text.unpack name) latestDownloadVersion + pure (result, latestDownloadVersion) + +cloneFromGit :: URI -> Text -> Text -> Text -> SlackBuilderT Package.Download +cloneFromGit repo tagPrefix packagePath version = do + repository' <- SlackBuilderT $ asks repository + let downloadFileName = URI.unRText + $ NonEmpty.last $ snd $ fromJust $ URI.uriPath repo + relativeTarball = Text.unpack packagePath + (dropExtension (Text.unpack downloadFileName) <> "-" <> Text.unpack version) + tarball = repository' relativeTarball + name' = Text.pack (takeBaseName $ Text.unpack packagePath) + checksum <- clone (URI.render repo) (Text.pack tarball) tagPrefix + uploadCommand (Text.pack relativeTarball) ("/" <> name') + (flip . flip Package.Download) (fromJust checksum) False + <$> liftIO (mkURI $ "https://download.dlackware.com/hosted-sources/" <> name' <> "/" <> downloadFileName) + +downloadWithTemplate :: Package.DownloadTemplate -> Text -> Text -> SlackBuilderT Package.Download +downloadWithTemplate downloadTemplate packagePath version = do + repository' <- SlackBuilderT $ asks repository + uri' <- liftIO $ Package.renderDownloadWithVersion downloadTemplate version + let downloadFileName = URI.unRText + $ NonEmpty.last $ snd $ fromJust $ URI.uriPath uri' + relativeTarball = packagePath <> "/" <> downloadFileName + tarball = repository' Text.unpack relativeTarball + checksum <- fromJust <$> download uri' tarball + pure $ Package.Download uri' checksum False + +reuploadWithTemplate :: Package.DownloadTemplate -> [CmdSpec] -> Text -> Text -> SlackBuilderT Package.Download +reuploadWithTemplate downloadTemplate commands packagePath version = do + Package.Download{ download = uri', md5sum = checksum } <- downloadWithTemplate downloadTemplate packagePath version + let downloadFileName = URI.unRText + $ NonEmpty.last $ snd $ fromJust $ URI.uriPath uri' + relativeTarball = packagePath <> "/" <> downloadFileName + download' <- handleReupload relativeTarball downloadFileName + + pure $ Package.Download download' checksum False + where + name' = Text.pack $ takeBaseName $ Text.unpack packagePath + handleReupload relativeTarball downloadFileName = do + repository' <- SlackBuilderT $ asks repository + case commands of + [] -> uploadTarball relativeTarball downloadFileName + _ -> + let tarballPath = repository' Text.unpack relativeTarball + packedDirectory = takeBaseName $ dropExtension tarballPath + in liftIO (callProcess "tar" ["xvf", tarballPath]) + >> liftIO (traverse (defaultCreateProcess packedDirectory) commands) + >> liftIO (callProcess "tar" ["Jcvf", tarballPath, packedDirectory]) + >> uploadTarball relativeTarball downloadFileName + uploadTarball relativeTarball downloadFileName + = liftIO (putStrLn $ "Upload the source tarball " <> Text.unpack relativeTarball) + >> uploadCommand relativeTarball ("/" <> name') + >> liftIO (mkURI $ "https://download.dlackware.com/hosted-sources/" <> name' <> "/" <> downloadFileName) + defaultCreateProcess cwd' cmdSpec + = flip withCreateProcess (const . const . const waitForProcess) + $ CreateProcess + { use_process_jobs = False + , std_out = Inherit + , std_in = NoStream + , std_err = Inherit + , new_session = False + , env = Nothing + , detach_console = False + , delegate_ctlc = False + , cwd = Just cwd' + , create_new_console = False + , create_group = False + , cmdspec = cmdSpec + , close_fds = True + , child_user = Nothing + , child_group = Nothing + } + +renderAndDownload :: Package -> Text -> SlackBuilderT Package.Download +renderAndDownload Package{..} version = do + let packagePath = category <> "/" <> name + Package.Updater _ getVersion = latest + + getVersion packagePath version + +updatePackage :: Package -> PackageInfo -> Text -> SlackBuilderT () +updatePackage package@Package{..} info version = do + let packagePath = category <> "/" <> name + + repository' <- SlackBuilderT $ asks repository + mainDownload <- renderAndDownload package version + moreDownloads <- traverse (updateDownload package) downloaders + let allDownloads = mainDownload : toList (fst <$> moreDownloads) + let infoFilePath = repository' Text.unpack packagePath + (Text.unpack name <.> "info") + package' = info + { version = version + , downloads = getField @"download" <$> allDownloads + , checksums = getField @"md5sum" <$> allDownloads + } + liftIO $ Text.IO.writeFile infoFilePath $ generate package' + updateSlackBuildVersion packagePath version $ snd <$> moreDownloads + + commit packagePath version + +findCategory :: FilePath -> IO [FilePath] +findCategory currentDirectory = do + contents <- liftIO $ listDirectory currentDirectory + case find (isSuffixOf ".info") contents of + Just _ -> pure [currentDirectory] + Nothing -> do + let contents' = (currentDirectory ) <$> filter (not . isPrefixOf ".") contents + directories <- filterM doesDirectoryExist contents' + subCategories <- traverse findCategory directories + pure $ concat subCategories + +main :: IO () +main = do + programCommand <- execParser slackBuilderParser + settings <- Toml.decodeFile settingsCodec "config/config.toml" + latestVersion <- flip runReaderT settings + $ runSlackBuilderT + $ executeCommand programCommand + + maybe (pure ()) Text.IO.putStrLn latestVersion + where + executeCommand = \case + CategoryCommand _packageName -> do + repository' <- SlackBuilderT $ asks repository + categories <- liftIO $ findCategory repository' + liftIO $ print $ splitFileName . makeRelative repository' <$> categories + pure Nothing + CloneCommand repo tarball tagPrefix -> fmap (Text.pack . show) + <$> clone repo tarball tagPrefix + Up2DateCommand -> up2Date >> pure Nothing diff --git a/src/SlackBuilder/CommandLine.hs b/src/SlackBuilder/CommandLine.hs new file mode 100644 index 0000000..7cfe747 --- /dev/null +++ b/src/SlackBuilder/CommandLine.hs @@ -0,0 +1,58 @@ +module SlackBuilder.CommandLine + ( GhArguments(..) + , SlackBuilderCommand(..) + , PackagistArguments(..) + , TextArguments(..) + , slackBuilderParser + ) where + +import Data.Text (Text) +import Options.Applicative + ( Parser + , ParserInfo(..) + , metavar + , argument + , str + , info + , fullDesc + , subparser + , command, + ) + +data SlackBuilderCommand + = CategoryCommand Text + | CloneCommand Text Text Text + | Up2DateCommand + +data PackagistArguments = PackagistArguments + { vendor :: Text + , name :: Text + } deriving (Eq, Show) + +data GhArguments = GhArguments + { owner :: Text + , name :: Text + , transform :: Maybe Text + } deriving (Eq, Show) + +data TextArguments = TextArguments + { versionPicker :: Text -> Text + , textURL :: Text + } + +slackBuilderParser :: ParserInfo SlackBuilderCommand +slackBuilderParser = info slackBuilderCommand fullDesc + +slackBuilderCommand :: Parser SlackBuilderCommand +slackBuilderCommand = subparser + $ command "category" (info categoryCommand mempty) + <> command "clone" (info cloneCommand mempty) + <> command "up2date" (info up2DateCommand mempty) + where + categoryCommand = CategoryCommand + <$> argument str (metavar "PKGNAM") + cloneCommand = CloneCommand + <$> argument str (metavar "REPO") + <*> argument str (metavar "TARBALL") + <*> argument str (metavar "TAG_PREFIX") + up2DateCommand = pure Up2DateCommand diff --git a/src/SlackBuilder/Updater.hs b/src/SlackBuilder/Updater.hs new file mode 100644 index 0000000..1ebf7fe --- /dev/null +++ b/src/SlackBuilder/Updater.hs @@ -0,0 +1,158 @@ +module SlackBuilder.Updater + ( latestGitHub + , latestPackagist + , latestText + ) where + +import SlackBuilder.Config +import qualified Data.Aeson as Aeson +import Data.Aeson ((.:)) +import Data.Aeson.TH (defaultOptions, deriveJSON) +import Data.HashMap.Strict (HashMap) +import qualified Data.HashMap.Strict as HashMap +import Data.Text (Text) +import qualified Data.Text as Text +import qualified Data.Text.Encoding as Text.Encoding +import Data.Vector (Vector, (!?)) +import qualified Data.Vector as Vector +import Network.HTTP.Req + ( header + , runReq + , defaultHttpConfig + , req + , GET(..) + , https + , jsonResponse + , NoReqBody(..) + , (/:) + , responseBody + , useHttpsURI + , bsResponse + , POST(..) + , ReqBodyJson(..) + ) +import Text.URI (mkURI) +import SlackBuilder.CommandLine +import SlackBuilder.Trans +import qualified Data.Aeson.KeyMap as KeyMap +import GHC.Records (HasField(..)) +import Control.Monad.Trans.Reader (asks) +import Control.Monad.IO.Class (MonadIO(..)) + +newtype PackagistPackage = PackagistPackage + { version :: Text + } deriving (Eq, Show) + +$(deriveJSON defaultOptions ''PackagistPackage) + +newtype PackagistResponse = PackagistResponse + { packages :: HashMap Text (Vector PackagistPackage) + } deriving (Eq, Show) + +$(deriveJSON defaultOptions ''PackagistResponse) + +newtype GhRefNode = GhRefNode + { name :: Text + } deriving (Eq, Show) + +$(deriveJSON defaultOptions ''GhRefNode) + +newtype GhRef = GhRef + { nodes :: Vector GhRefNode + } deriving (Eq, Show) + +$(deriveJSON defaultOptions ''GhRef) + +newtype GhRepository = GhRepository + { refs :: GhRef + } deriving (Eq, Show) + +$(deriveJSON defaultOptions ''GhRepository) + +newtype GhData = GhData + { repository :: GhRepository + } deriving (Eq, Show) + +instance Aeson.FromJSON GhData where + parseJSON (Aeson.Object keyMap) + | Just data' <- KeyMap.lookup "data" keyMap = + GhData <$> Aeson.withObject "GhData" (.: "repository") data' + parseJSON _ = fail "data key not found in the response" + +data GhVariables = GhVariables + { name :: Text + , owner :: Text + } deriving (Eq, Show) + +$(deriveJSON defaultOptions ''GhVariables) + +data GhQuery = GhQuery + { query :: Text + , variables :: GhVariables + } deriving (Eq, Show) + +$(deriveJSON defaultOptions ''GhQuery) + +latestPackagist :: PackagistArguments -> SlackBuilderT (Maybe Text) +latestPackagist PackagistArguments{..} = do + packagistResponse <- runReq defaultHttpConfig $ + let uri = https "repo.packagist.org" /: "p2" + /: vendor + /: name <> ".json" + in req GET uri NoReqBody jsonResponse mempty + let packagistPackages = packages $ responseBody packagistResponse + fullName = Text.intercalate "/" [vendor, name] + + pure $ HashMap.lookup fullName packagistPackages + >>= fmap (version . fst) . Vector.uncons + +latestText :: TextArguments -> SlackBuilderT (Maybe Text) +latestText TextArguments{..} = do + uri <- liftIO $ useHttpsURI <$> mkURI textURL + packagistResponse <- traverse (runReq defaultHttpConfig . go . fst) uri + + pure $ versionPicker . Text.Encoding.decodeUtf8 . responseBody + <$> packagistResponse + where + go uri = req GET uri NoReqBody bsResponse mempty + +latestGitHub + :: GhArguments + -> (Text -> Maybe Text) + -> SlackBuilderT (Maybe Text) +latestGitHub GhArguments{..} versionTransform = do + ghToken' <- SlackBuilderT $ asks ghToken + ghResponse <- runReq defaultHttpConfig $ + let uri = https "api.github.com" /: "graphql" + query = GhQuery + { query = githubQuery + , variables = GhVariables + { owner = owner + , name = name + } + } + authorizationHeader = header "authorization" + $ Text.Encoding.encodeUtf8 + $ "Bearer " <> ghToken' + in req POST uri (ReqBodyJson query) jsonResponse + $ authorizationHeader <> header "User-Agent" "SlackBuilder" + let ghNodes = nodes + $ refs + $ (getField @"repository" :: GhData -> GhRepository) + $ responseBody ghResponse + refs' = Vector.reverse + $ Vector.catMaybes + $ versionTransform . getField @"name" <$> ghNodes + pure $ refs' !? 0 + where + githubQuery = + "query ($name: String!, $owner: String!) {\n\ + \ repository(name: $name, owner: $owner) {\n\ + \ refs(last: 10, refPrefix: \"refs/tags/\", orderBy: { field: TAG_COMMIT_DATE, direction: ASC }) {\n\ + \ nodes {\n\ + \ id,\n\ + \ name\n\ + \ }\n\ + \ }\n\ + \ }\n\ + \}" -- cgit v1.2.3