-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
create-release.sh -> create-release.hs
- Loading branch information
1 parent
4a834d2
commit c88d233
Showing
1 changed file
with
265 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,265 @@ | ||
#!/usr/bin/env cabal | ||
{- project: | ||
with-compiler: ghc-9.4 | ||
-} | ||
{- cabal: | ||
build-depends: | ||
base, | ||
commonmark, | ||
containers, | ||
filepath, | ||
foldl, | ||
text, | ||
turtle ^>=1.6.0, | ||
-} | ||
{-# OPTIONS_GHC -Wall -Wextra #-} | ||
{-# LANGUAGE GHC2021 #-} | ||
{-# LANGUAGE BlockArguments, LambdaCase, OverloadedStrings, ScopedTypeVariables #-} | ||
|
||
import Commonmark | ||
import Control.Monad | ||
import Data.Map (Map) | ||
import Data.Monoid (First(..)) | ||
import Data.Semigroup (Max(..)) | ||
import Data.Version | ||
import System.FilePath | ||
import Turtle hiding (fp, d, o) | ||
import qualified Data.Map as Map | ||
import qualified Data.Text as Text | ||
import qualified Data.Text.IO as Text | ||
import qualified Control.Foldl as Foldl | ||
|
||
main :: IO () | ||
main = sh do | ||
|
||
(isDryRun, skipGit) <- options "Create releases for ouroboros-consensus packages" $ | ||
(,) <$> switch "dry-run" 'd' "Make no changes" | ||
<*> switch "skip-git" 's' "Skip creating a new release branch or commits for these changes" | ||
|
||
-- Determine how breaking the sum of the changes in each package is | ||
packageChangeSeverities <- reduce collectSeverities do | ||
|
||
(packageName, dependencies) <- select packages | ||
|
||
maxMaybe <- reduce Foldl.maximum do | ||
fragment <- findChangelogFragments packageName | ||
findChangeSeverity fragment | ||
|
||
pure (packageName, maxMaybe, dependencies) | ||
|
||
packageVersions <- reduce Foldl.map do | ||
(packageName, maybeSeverity) <- select $ Map.toList packageChangeSeverities | ||
|
||
case maybeSeverity of | ||
Nothing -> do | ||
liftIO do | ||
putStrLn $ "No changes need to be made for package " <> packageName <> "!" | ||
mzero | ||
Just severity -> do | ||
currentPackageVersion <- findCurrentPackageVersion packageName | ||
let nextPackageVersion = calculateNextPackageVersion severity currentPackageVersion | ||
|
||
pure (packageName, (currentPackageVersion, nextPackageVersion)) | ||
|
||
void $ liftIO $ flip Map.traverseWithKey packageVersions $ \package (current, next) -> do | ||
putStrLn $ package <> ": " <> showVersion current <> " -> " <> showVersion next | ||
|
||
unless isDryRun do | ||
unless skipGit do | ||
createGitBranch packageVersions | ||
|
||
(packageName, (current, next)) <- select $ Map.toList packageVersions | ||
|
||
if isDryRun | ||
then | ||
liftIO do | ||
putStrLn $ "This is a dry run, so no changes will be made for " <> packageName | ||
else do | ||
updateCabalFile packageName current next packageVersions | ||
runScrivCollect packageName next | ||
unless skipGit do | ||
createGitCommit packageName next | ||
|
||
-- | Pairs of packages with a list of their dependencies. We use the | ||
-- dependencies to calculate which version changes should require a version | ||
-- bump in a package's dependencies as well. | ||
-- | ||
-- BEWARE! This list should always be ordered such that any given package's | ||
-- dependencies are located BEFORE that package in the list! | ||
packages :: [(FilePath, [FilePath])] | ||
packages = | ||
[ ("ouroboros-consensus", []) | ||
, ("ouroboros-consensus-diffusion", ["ouroboros-consensus"]) | ||
, ("ouroboros-consensus-protocol", ["ouroboros-consensus"]) | ||
, ("ouroboros-consensus-cardano", ["ouroboros-consensus", "ouroboros-consensus-protocol"]) | ||
] | ||
|
||
findChangelogFragments :: FilePath -> Shell FilePath | ||
findChangelogFragments pkg = do | ||
changeLogEntry <- ls $ pkg </> "changelog.d" | ||
guard $ takeExtension changeLogEntry == ".md" | ||
pure changeLogEntry | ||
|
||
-- | Find a package's version by parsing its cabal file (only by searching for | ||
-- the "version:" field, not by invoking the cabal library itself) | ||
findCurrentPackageVersion :: FilePath -> Shell Version | ||
findCurrentPackageVersion packageName = do | ||
maybeFirstMatch <- reduce (Foldl.foldMap (First . Just) getFirst) do | ||
line <- input $ packageName </> packageName <.> "cabal" | ||
case match versionLinePattern (lineToText line) of | ||
[] -> mzero | ||
version : _ -> pure version | ||
case maybeFirstMatch of | ||
Nothing -> do | ||
liftIO $ putStrLn $ | ||
"Couldn't parse a version number from package " <> packageName <> "!" | ||
mzero | ||
Just version -> pure version | ||
|
||
calculateNextPackageVersion :: ChangeSeverity -> Version -> Version | ||
calculateNextPackageVersion c (Version branch tags) = do | ||
let incrementIndex :: Int -> [Int] -> [Int] | ||
incrementIndex 0 (h : t) = succ h : (0 <$ t) | ||
incrementIndex n [] = 0 : incrementIndex (pred n) [] | ||
incrementIndex n (h : t) = h : incrementIndex (pred n) t | ||
ix = case c of | ||
Breaking -> 1 | ||
NonBreaking -> 2 | ||
Patch -> 3 | ||
Version (incrementIndex ix branch) tags | ||
|
||
versionLinePattern :: Pattern Version | ||
versionLinePattern = | ||
"version:" *> spaces *> (Version <$> decimal `sepBy1` "." <*> pure mempty) | ||
|
||
data ChangeSeverity | ||
= Patch | ||
| NonBreaking | ||
| Breaking | ||
deriving (Show, Eq, Ord, Bounded) | ||
|
||
findChangeSeverity :: FilePath -> Shell ChangeSeverity | ||
findChangeSeverity frag = do | ||
contents <- liftIO $ Text.readFile frag | ||
case commonmark frag contents of | ||
Left markdownError -> do | ||
liftIO $ putStrLn $ "Failed to parse markdown file " <> frag <> ":" | ||
error $ show markdownError | ||
Right (Headings Nothing) -> | ||
error $ "Couldn't find any change severity headers in " <> frag <> ", exiting!" | ||
Right (Headings (Just (Max sev))) -> pure sev | ||
|
||
collectSeverities :: Fold (FilePath, Maybe ChangeSeverity, [FilePath]) (Map FilePath (Maybe ChangeSeverity)) | ||
collectSeverities = Foldl.Fold insert mempty id | ||
where | ||
insert :: Map FilePath (Maybe ChangeSeverity) | ||
-> (FilePath, Maybe ChangeSeverity, [FilePath]) | ||
-> Map FilePath (Maybe ChangeSeverity) | ||
insert m (pkg, sev, deps) = do | ||
-- If a package is unchanged, but its dependencies have breaking changes, | ||
-- we should consider that package to also have breaking changes | ||
let dependenciesChanges = map (\dep -> join (Map.lookup dep m)) deps | ||
maxChangeSeverity = | ||
fmap getMax $ | ||
foldMap (fmap Max) (sev : dependenciesChanges) | ||
Map.insert pkg maxChangeSeverity m | ||
|
||
createGitBranch :: Map FilePath (Version, Version) -> Shell () | ||
createGitBranch versions = do | ||
let branchName = Text.intercalate "/" ("release" : Map.foldMapWithKey (\p (_, v) -> pure (packageNameWithVersion p v)) versions) | ||
|
||
procs "git" ["branch", branchName] mempty | ||
|
||
inDirectory :: MonadIO m => FilePath -> m a -> m a | ||
inDirectory targetDir act = do | ||
restoreDir <- pwd | ||
cd targetDir | ||
res <- act | ||
cd restoreDir | ||
pure res | ||
|
||
updateCabalFile :: FilePath | ||
-> Version -- ^ The current version of the package | ||
-> Version -- ^ The next version of the package | ||
-> Map FilePath (Version, Version) | ||
-> Shell () | ||
updateCabalFile package current next dependenciesVersions = do | ||
inplace (updateVersion <|> updateDependencies) (package </> package <.> "cabal") | ||
where | ||
versionText = Text.pack . showVersion | ||
updateVersion = | ||
replaceIfContains "version:" (versionText current) (versionText next) | ||
updateDependencies = do | ||
Map.foldlWithKey (\pat pkg (i, o) -> replaceIfContains (fromString pkg) (versionText i) (versionText o) <|> pat) empty dependenciesVersions | ||
|
||
replaceIfContains :: Pattern Text -> Text -> Text -> Pattern Text | ||
replaceIfContains t i o = do | ||
contains $ | ||
t <> chars <> (text i *> pure o) | ||
|
||
runScrivCollect :: MonadIO m => FilePath -> Version -> m () | ||
runScrivCollect fp v = do | ||
inDirectory fp do | ||
procs "scriv" ["collect", "--version", Text.pack (showVersion v)] mempty | ||
|
||
createGitCommit :: FilePath -> Version -> Shell () | ||
createGitCommit package next = do | ||
let commitString = | ||
"release " <> packageNameWithVersion package next | ||
liftIO $ putStrLn $ "Creating git commit: " <> show commitString | ||
procs "git" ["commit", "-am", commitString] mempty | ||
|
||
packageNameWithVersion :: FilePath -> Version -> Text | ||
packageNameWithVersion package v = Text.pack $ | ||
package <> "-" <> showVersion v | ||
|
||
-- The following newtypes and instances are only used to pick out the headings | ||
-- in the parsed Markdown files and can be safely ignored unless you care about | ||
-- the internals of `findChangeSeverity` | ||
|
||
newtype HeadingText = HeadingText Text | ||
deriving (Show, Read, Eq, Ord, Semigroup, Monoid) | ||
|
||
instance Rangeable HeadingText where | ||
ranged _ = id | ||
|
||
instance HasAttributes HeadingText where | ||
addAttributes _ = id | ||
|
||
instance IsInline HeadingText where | ||
lineBreak = HeadingText "\n" | ||
softBreak = HeadingText "\n" | ||
str = HeadingText | ||
entity = HeadingText | ||
escapedChar = mempty | ||
emph = id | ||
strong = id | ||
link _dest txt _desc = HeadingText txt | ||
image = mempty | ||
code = HeadingText | ||
rawInline = mempty | ||
|
||
newtype Headings = Headings (Maybe (Max ChangeSeverity)) | ||
deriving (Show, Eq, Ord, Semigroup, Monoid) | ||
|
||
instance Rangeable Headings where | ||
ranged _ = id | ||
|
||
instance HasAttributes Headings where | ||
addAttributes _ = id | ||
|
||
instance IsBlock HeadingText Headings where | ||
heading _lvl (HeadingText txt) = | ||
case txt of | ||
"Patch" -> Headings (Just (Max Patch)) | ||
"Non-Breaking" -> Headings (Just (Max NonBreaking)) | ||
"Breaking" -> Headings (Just (Max Breaking)) | ||
_ -> mempty | ||
paragraph = mempty | ||
plain = mempty | ||
thematicBreak = mempty | ||
blockQuote = mempty | ||
codeBlock = mempty | ||
rawBlock = mempty | ||
referenceLinkDefinition = mempty | ||
list = mempty |