packages feed

stacker 0.0.0.0 → 0.0.1.0

raw patch · 20 files changed

+425/−76 lines, 20 filesdep +tasty-hunitPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: tasty-hunit

API changes (from Hackage documentation)

- Types: [actionNewSnapshot] :: Action -> Maybe Text
+ Analysis: determineNewSnapshot :: SnapshotDB -> Text -> Bump
+ Types: BumpTo :: Text -> Bump
+ Types: InfoAll :: InfoCmd
+ Types: InfoRepo :: InfoCmd
+ Types: InfoSnapshots :: InfoCmd
+ Types: Unknown :: Bump
+ Types: UpToDate :: Bump
+ Types: [actionBump] :: Action -> Bump
+ Types: data Bump
+ Types: data InfoCmd
+ Types: instance GHC.Classes.Eq Types.Bump
+ Types: instance GHC.Classes.Eq Types.InfoCmd
+ Types: instance GHC.Generics.Generic Types.Bump
+ Types: instance GHC.Show.Show Types.Bump
+ Types: instance GHC.Show.Show Types.InfoCmd
- Types: Action :: FilePath -> Text -> Maybe Text -> Bool -> (Int, Int) -> Maybe FilePath -> Action
+ Types: Action :: FilePath -> Text -> Bump -> Bool -> (Int, Int) -> Maybe FilePath -> Action
- Types: Info :: Command
+ Types: Info :: InfoCmd -> Command

Files

CHANGELOG.md view
@@ -1,3 +1,28 @@+0.0.1.0+=======++* Fix: a nightly snapshot missing from the database (e.g. a date for which+  no nightly was published) was reported as `✓ up to date`.+  Now its GHC version is inferred from the closest known nightly.++* Snapshots that cannot be analyzed are now reported as `? unknown snapshot`+  instead of `✓ up to date`.  This includes snapshots newer than the database+  (previously a downgrade could be suggested), unknown LTS major versions,+  and resolvers that are not Stackage snapshots.++* `stacker update` is much faster (seconds instead of minutes): the GHC version+  is now read from the end of each snapshot file instead of parsing the whole YAML.+  If this fails for a file, a warning is printed and full YAML parsing is used.++* New subcommands `stacker info repo` and `stacker info snapshots`+  that print just the repository path and just the snapshot list, respectively.++* Shipped snapshot database updated (latest: lts-24.63, nightly-2026-10-12).++* Tested with GHC 9.4.8 - 9.14.1.++_Andreas Abel, 2026-10-15_+ 0.0.0.0 ======= 
README.md view
@@ -61,7 +61,7 @@ | `dry-run [FILES...]` | Shows what updates would be performed by the respective `bump` (default command) | | `config`             | Configures `stacker`                                                             | | `update`             | Updates its database of Stackage snapshots                                       |-| `info`               | Prints table mapping GHC major versions to their latest snapshots                |+| `info [repo\|snapshots]` | Prints repository path and/or table mapping GHC major versions to their latest snapshots |  These can be modified with the following option(s): @@ -189,7 +189,7 @@ stack-9.10.yaml   nightly-2024-07-07   → bump to lts-24.36 stack-9.12.yaml   nightly-2025-10-03   → bump to nightly-2026-04-01 ```-When coloring is on, the `stack*.yml` column is printed in bold, `✓ up to date` in green and any bump action in bright yellow.+When coloring is on, the `stack*.yml` column is printed in bold, `✓ up to date` in green, any bump action in bright yellow, and `? unknown snapshot` in red.  Internally, `bump` and `dry-run` share the analysis phase that reads the `stack*.yaml` and creates for each an action plan, yet `dry-run` just pretty-prints this plan as detailed above whereas `bump` executes it.@@ -254,6 +254,14 @@      9.10.3: lts-24.36      9.12.4: nightly-2026-04-01    ```++These can also be printed separately:+- `stacker info repo` prints just the bare path `$REPO`.+- `stacker info snapshots` prints just the map, without indentation, e.g.+  ```+  9.8.4: lts-23.28+  9.10.3: lts-24.36+  ```  ## Testing 
data/ghc.csv view
@@ -53,7 +53,7 @@ 9.8.4,lts-23.28 9.10.1,nightly-2025-05-05 9.10.2,lts-24.11-9.10.3,lts-24.36+9.10.3,lts-24.63 9.12.2,nightly-2025-12-30 9.12.3,nightly-2026-03-28-9.12.4,nightly-2026-04-07+9.12.4,nightly-2026-10-12
data/lts.csv view
@@ -608,3 +608,30 @@ 24.34,9.10.3 24.35,9.10.3 24.36,9.10.3+24.37,9.10.3+24.38,9.10.3+24.39,9.10.3+24.40,9.10.3+24.41,9.10.3+24.42,9.10.3+24.43,9.10.3+24.44,9.10.3+24.45,9.10.3+24.46,9.10.3+24.47,9.10.3+24.48,9.10.3+24.49,9.10.3+24.50,9.10.3+24.51,9.10.3+24.52,9.10.3+24.53,9.10.3+24.54,9.10.3+24.55,9.10.3+24.56,9.10.3+24.57,9.10.3+24.58,9.10.3+24.59,9.10.3+24.60,9.10.3+24.61,9.10.3+24.62,9.10.3+24.63,9.10.3
data/nightly.csv view
@@ -3094,3 +3094,160 @@ 2026-04-05,9.12.4 2026-04-06,9.12.4 2026-04-07,9.12.4+2026-04-09,9.12.4+2026-04-10,9.12.4+2026-04-11,9.12.4+2026-04-12,9.12.4+2026-04-14,9.12.4+2026-04-15,9.12.4+2026-04-16,9.12.4+2026-04-17,9.12.4+2026-04-18,9.12.4+2026-04-19,9.12.4+2026-04-20,9.12.4+2026-04-21,9.12.4+2026-04-22,9.12.4+2026-04-23,9.12.4+2026-04-24,9.12.4+2026-04-25,9.12.4+2026-04-26,9.12.4+2026-04-28,9.12.4+2026-04-29,9.12.4+2026-04-30,9.12.4+2026-05-01,9.12.4+2026-05-02,9.12.4+2026-05-03,9.12.4+2026-05-04,9.12.4+2026-05-05,9.12.4+2026-05-06,9.12.4+2026-05-10,9.12.4+2026-05-11,9.12.4+2026-05-12,9.12.4+2026-05-14,9.12.4+2026-05-15,9.12.4+2026-05-16,9.12.4+2026-05-17,9.12.4+2026-05-18,9.12.4+2026-05-19,9.12.4+2026-05-21,9.12.4+2026-05-22,9.12.4+2026-05-23,9.12.4+2026-05-24,9.12.4+2026-05-25,9.12.4+2026-05-26,9.12.4+2026-05-27,9.12.4+2026-05-29,9.12.4+2026-06-01,9.12.4+2026-06-02,9.12.4+2026-06-03,9.12.4+2026-06-04,9.12.4+2026-06-05,9.12.4+2026-06-06,9.12.4+2026-06-07,9.12.4+2026-06-08,9.12.4+2026-06-09,9.12.4+2026-06-10,9.12.4+2026-06-11,9.12.4+2026-06-12,9.12.4+2026-06-13,9.12.4+2026-06-14,9.12.4+2026-06-15,9.12.4+2026-06-16,9.12.4+2026-06-18,9.12.4+2026-06-19,9.12.4+2026-06-20,9.12.4+2026-06-21,9.12.4+2026-06-22,9.12.4+2026-06-25,9.12.4+2026-06-26,9.12.4+2026-06-27,9.12.4+2026-06-28,9.12.4+2026-06-29,9.12.4+2026-06-30,9.12.4+2026-07-01,9.12.4+2026-07-02,9.12.4+2026-07-03,9.12.4+2026-07-06,9.12.4+2026-07-07,9.12.4+2026-07-08,9.12.4+2026-07-09,9.12.4+2026-07-10,9.12.4+2026-07-11,9.12.4+2026-07-12,9.12.4+2026-07-13,9.12.4+2026-07-15,9.12.4+2026-07-16,9.12.4+2026-07-17,9.12.4+2026-07-18,9.12.4+2026-07-19,9.12.4+2026-07-20,9.12.4+2026-07-21,9.12.4+2026-07-22,9.12.4+2026-07-23,9.12.4+2026-07-24,9.12.4+2026-07-26,9.12.4+2026-07-27,9.12.4+2026-07-28,9.12.4+2026-07-29,9.12.4+2026-07-30,9.12.4+2026-07-31,9.12.4+2026-08-01,9.12.4+2026-08-02,9.12.4+2026-08-03,9.12.4+2026-08-04,9.12.4+2026-08-05,9.12.4+2026-08-06,9.12.4+2026-08-07,9.12.4+2026-08-08,9.12.4+2026-08-09,9.12.4+2026-08-10,9.12.4+2026-08-11,9.12.4+2026-08-12,9.12.4+2026-08-13,9.12.4+2026-08-14,9.12.4+2026-08-15,9.12.4+2026-08-16,9.12.4+2026-08-19,9.12.4+2026-08-20,9.12.4+2026-08-23,9.12.4+2026-08-24,9.12.4+2026-08-25,9.12.4+2026-08-26,9.12.4+2026-08-27,9.12.4+2026-08-30,9.12.4+2026-08-31,9.12.4+2026-09-01,9.12.4+2026-09-06,9.12.4+2026-09-07,9.12.4+2026-09-08,9.12.4+2026-09-09,9.12.4+2026-09-10,9.12.4+2026-09-11,9.12.4+2026-09-12,9.12.4+2026-09-13,9.12.4+2026-09-14,9.12.4+2026-09-15,9.12.4+2026-09-16,9.12.4+2026-09-18,9.12.4+2026-09-19,9.12.4+2026-09-20,9.12.4+2026-09-21,9.12.4+2026-09-22,9.12.4+2026-09-23,9.12.4+2026-09-24,9.12.4+2026-09-25,9.12.4+2026-09-26,9.12.4+2026-09-27,9.12.4+2026-09-29,9.12.4+2026-09-30,9.12.4+2026-10-01,9.12.4+2026-10-02,9.12.4+2026-10-03,9.12.4+2026-10-04,9.12.4+2026-10-05,9.12.4+2026-10-06,9.12.4+2026-10-08,9.12.4+2026-10-09,9.12.4+2026-10-10,9.12.4+2026-10-11,9.12.4+2026-10-12,9.12.4
src/Analysis.hs view
@@ -4,22 +4,24 @@   ( analyzeStackYaml   , analyzeAllStackYamls   , analyzeStackYamls+  , determineNewSnapshot   ) where  import Prelude hiding (min, span) +import Control.Applicative ((<|>)) import Control.Monad (filterM) import Data.Functor ((<&>)) import Data.List (maximumBy) import Data.Map.Strict qualified as Map-import Data.Maybe (catMaybes)+import Data.Maybe (catMaybes, fromMaybe) import Data.Ord (comparing) import Data.Text (Text) import Data.Text qualified as T import System.Directory (doesFileExist) import Text.Printf (printf) -import Types (Action(..), SnapshotDB(..), LTSVersion(..), NightlyVersion(..), GHCVersion(..), Snapshot(..))+import Types (Action(..), Bump(..), SnapshotDB(..), LTSVersion(..), NightlyVersion(..), GHCVersion(..), Snapshot(..)) import StackYaml (parseStackYaml, findStackYamlFiles, findStackYamlFilesRecursive, getSymlinkMap)  -- | Analyze a single stack.yaml file@@ -40,8 +42,8 @@ analyzeStackYamls db files recursive = do   -- Get files to analyze: either auto-discover or use provided list   -- Non-existent files are silently filtered out-  filesToAnalyze <- -    if null files +  filesToAnalyze <-+    if null files       then if recursive            then findStackYamlFilesRecursive            else findStackYamlFiles@@ -55,14 +57,23 @@ analyzeAllStackYamls db = analyzeStackYamls db [] False  -- | Determine the new snapshot for a given old snapshot-determineNewSnapshot :: SnapshotDB -> Text -> Maybe Text+determineNewSnapshot :: SnapshotDB -> Text -> Bump determineNewSnapshot db oldSnap-  | T.isPrefixOf "lts-" oldSnap = determineLTSBump db oldSnap-  | T.isPrefixOf "nightly-" oldSnap = determineNightlyBump db oldSnap-  | otherwise = Nothing+  | T.isPrefixOf "lts-" oldSnap = fromMaybe Unknown $ determineLTSBump db oldSnap+  | T.isPrefixOf "nightly-" oldSnap = fromMaybe Unknown $ determineNightlyBump db oldSnap+  | otherwise = Unknown +-- | Compare the latest known snapshot with the old one.+-- If the old snapshot is newer than anything we know of,+-- our database is outdated and we cannot give advice.+compareWithLatest :: Ord a => a -> a -> Snapshot -> Bump+compareWithLatest old latest snap = case compare latest old of+  GT -> BumpTo (formatSnapshot snap)+  EQ -> UpToDate+  LT -> Unknown+ -- | Determine bump for LTS snapshot-determineLTSBump :: SnapshotDB -> Text -> Maybe Text+determineLTSBump :: SnapshotDB -> Text -> Maybe Bump determineLTSBump db oldSnap = do   oldVersion <- parseLTSSnapshot oldSnap   -- Find the latest LTS with the same major version@@ -71,16 +82,16 @@     then Nothing     else do       let (latestVersion, _) = maximumBy (comparing fst) sameMajor-      let newSnap = formatSnapshot (LTS latestVersion)-      if newSnap == oldSnap-        then Nothing  -- Already up to date-        else Just newSnap+      return $ compareWithLatest oldVersion latestVersion (LTS latestVersion)  -- | Determine bump for nightly snapshot-determineNightlyBump :: SnapshotDB -> Text -> Maybe Text+determineNightlyBump :: SnapshotDB -> Text -> Maybe Bump determineNightlyBump db oldSnap = do   oldVersion <- parseNightlySnapshot oldSnap-  oldGHC <- Map.lookup oldVersion (dbNightly db)+  -- The exact nightly may be missing from the database+  -- (e.g. a date for which no nightly was published);+  -- then take the GHC version of the closest known neighbour.+  (_, oldGHC) <- Map.lookupLE oldVersion (dbNightly db) <|> Map.lookupGE oldVersion (dbNightly db)   -- Get GHC major version (e.g., (9,6) from GHCVersion 9 6 1)   let ghcMajor = getGHCMajor oldGHC   -- Find if there's an LTS for this GHC major version@@ -89,7 +100,7 @@     then do       -- Bump to latest LTS for this GHC major       let (latestLTS, _) = maximumBy (comparing fst) ltsForGHC-      return $ formatSnapshot (LTS latestLTS)+      return $ BumpTo $ formatSnapshot (LTS latestLTS)     else do       -- Bump to latest nightly for this GHC major       let nightliesForGHC = filter (\(_, ghc) -> getGHCMajor ghc == ghcMajor) $ Map.toList (dbNightly db)@@ -97,10 +108,7 @@         then Nothing         else do           let (latestNightly, _) = maximumBy (comparing fst) nightliesForGHC-          let newSnap = formatSnapshot (Nightly latestNightly)-          if newSnap == oldSnap-            then Nothing-            else Just newSnap+          return $ compareWithLatest oldVersion latestNightly (Nightly latestNightly)  -- | Get GHC major version from full version (e.g., (9,6) from GHCVersion 9 6 1) getGHCMajor :: GHCVersion -> (Int, Int)
src/CSV.hs view
@@ -9,7 +9,8 @@ import Prelude hiding (lines, min)  import Control.Monad (unless)-import Data.List (sortBy, sortOn)+import Data.ByteString.Char8 qualified as BS+import Data.List (find, sortBy, sortOn) import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map import Data.Ord (comparing)@@ -25,7 +26,7 @@ import Text.Printf (printf) import Types (LTSVersion(..), NightlyVersion(..), GHCVersion(..), Snapshot(..), SnapshotDB(..)) import XDG (getStateDir)-import System.IO (stderr)+import System.IO (IOMode(ReadMode), SeekMode(AbsoluteSeek), hFileSize, hSeek, stderr, withBinaryFile) import Paths_stacker (getDataDir) import System.Directory qualified as Dir @@ -83,7 +84,7 @@   stateDir <- getStateDir   createDirectoryIfMissing True stateDir -  TIO.hPutStrLn stderr $ "Processing snapshot database.  This might take a couple of minutes..."+  TIO.hPutStrLn stderr $ "Processing snapshot database..."    -- Process LTS snapshots   TIO.hPutStrLn stderr $ "Processing LTSs..."@@ -165,9 +166,46 @@       | otherwise = error $ "Unexpected file in nightly directory: " ++ fname ++                            " (expected .yaml extension)" --- | Extract GHC version from a snapshot YAML file+-- | Extract GHC version from a snapshot YAML file.+-- Tries the fast method first, falling back to (slow) full YAML parsing with a warning. extractGHCVersion :: FilePath -> IO GHCVersion extractGHCVersion file = do+  fast <- extractGHCVersionFast file+  case fast of+    Just ghc -> return ghc+    Nothing -> do+      TIO.hPutStrLn stderr $ "Warning: could not find compiler field at the end of " <> T.pack file+        <> ", falling back to slow YAML parsing"+      extractGHCVersionYaml file++-- | Number of bytes at the end of a snapshot file to search for the compiler field+tailSize :: Integer+tailSize = 4096++-- | Extract GHC version by searching the end of a snapshot YAML file for the compiler field.+-- This avoids parsing the whole (large) file.  The compiler field is the last entry,+-- either at top level (old format) or under @resolver:@ (new format):+--+-- > compiler: ghc-7.8.3+--+-- > resolver:+-- >   compiler: ghc-9.12.4+extractGHCVersionFast :: FilePath -> IO (Maybe GHCVersion)+extractGHCVersionFast file = do+  chunk <- withBinaryFile file ReadMode $ \h -> do+    size <- hFileSize h+    hSeek h AbsoluteSeek (max 0 (size - tailSize))+    BS.hGetContents h+  return $ do+    line <- find isCompilerLine $ reverse $ BS.lines chunk+    parseGHCVersionText $ T.pack $ BS.unpack $ BS.strip $ BS.drop (BS.length prefix) $ BS.dropWhile (== ' ') line+  where+    prefix = "compiler: ghc-"+    isCompilerLine l = any (`BS.isPrefixOf` l) [prefix, "  " <> prefix]++-- | Extract GHC version from a snapshot YAML file by full YAML parsing+extractGHCVersionYaml :: FilePath -> IO GHCVersion+extractGHCVersionYaml file = do   result <- Yaml.decodeFileEither file   case result of     Left err -> error $ "Failed to parse " ++ file ++ ": " ++ show err
src/Commands.hs view
@@ -48,7 +48,7 @@       putStrLn "  dry-run [FILES...]       Show what would be updated by 'bump' (default)"       putStrLn "                           Use --recursive/-r to search subdirectories"       putStrLn "  update                   Update stackage snapshots database"-      putStrLn "  info                     Print GHC version to snapshot mapping"+      putStrLn "  info [repo|snapshots]    Print repository path and/or GHC version to snapshot mapping"       putStrLn "  config                   Configure stacker"       putStrLn ""       putStrLn "Available options:"@@ -70,6 +70,7 @@       putStrLn ""       putStrLn "For more information, see the README"     Config configCmd -> runConfig configCmd  -- Handle config first!+    Info InfoRepo -> getRepoPath >>= putStrLn  -- Does not need CSV files     cmd -> runEssentialCommand useColor cmd  -- | Run an essential command (requires CSV files)@@ -86,10 +87,7 @@       repoPath <- getRepoPath       ensureRepo repoPath       runUpdate repoPath-    Info -> do-      -- Info command shows the repo path-      repoPath <- getRepoPath-      runInfo repoPath+    Info infoCmd -> runInfo infoCmd     _ -> return ()  -- | Print version information@@ -111,13 +109,13 @@   -- Validate: recursive cannot be used with explicit files   when (recursive && not (null files)) $     die "Error: --recursive cannot be used with explicit file arguments"-  +   db <- loadSnapshotDB   actions <- analyzeStackYamls db files recursive    -- Sort actions by filename   let sortedActions = sortBy (comparing actionFile) actions-  +   -- Calculate the maximum filename length for proper alignment (add 2 for padding)   let maxFileLen = case sortedActions of         [] -> 20@@ -131,7 +129,7 @@ printActionWithWidth useColor fileWidth action = do   let file = actionFile action   let oldSnap = actionOldSnapshot action-  let newSnap = actionNewSnapshot action+  let bump = actionBump action   let symlinkTarget = actionSymlinkTarget action    -- Print with proper alignment (file padded to fileWidth chars, oldSnap to 25 chars)@@ -147,11 +145,14 @@         putStr "= symlink to "         putStr target     Nothing ->-      case newSnap of-        Nothing -> do+      case bump of+        UpToDate -> do           withColor useColor [SetColor Foreground Vivid Green] $             putStr "✓ up to date"-        Just new -> do+        Unknown -> do+          withColor useColor [SetColor Foreground Vivid Red] $+            putStr "? unknown snapshot"+        BumpTo new -> do           withColor useColor [SetColor Foreground Vivid Yellow] $ do             putStr "→ bump to "             putStr $ T.unpack new@@ -168,7 +169,7 @@   -- Validate: recursive cannot be used with explicit files   when (recursive && not (null files)) $     die "Error: --recursive cannot be used with explicit file arguments"-  +   db <- loadSnapshotDB   actions <- analyzeStackYamls db files recursive   mapM_ (applyAction True) actions@@ -181,15 +182,20 @@   generateCSVs repoPath  -- | Run info command-runInfo :: FilePath -> IO ()-runInfo repoPath = do-  putStrLn $ "repo: " ++ repoPath-  db <- loadSnapshotDB-  putStrLn "snapshots:"--  let ghcEntries = Map.toAscList (dbGHC db)-  forM_ ghcEntries $ \(ghc, snapshot) -> do-    putStrLn $ "  " ++ formatGHCVersionText ghc ++ ": " ++ T.unpack (formatSnapshotText snapshot)+runInfo :: InfoCmd -> IO ()+runInfo = \case+  InfoAll -> do+    repoPath <- getRepoPath+    putStrLn $ "repo: " ++ repoPath+    putStrLn "snapshots:"+    printSnapshots "  "+  InfoRepo -> getRepoPath >>= putStrLn+  InfoSnapshots -> printSnapshots ""+  where+    printSnapshots indent = do+      db <- loadSnapshotDB+      forM_ (Map.toAscList (dbGHC db)) $ \(ghc, snapshot) ->+        putStrLn $ indent ++ formatGHCVersionText ghc ++ ": " ++ T.unpack (formatSnapshotText snapshot)  -- | Format GHC version as text formatGHCVersionText :: GHCVersion -> String
src/Options.hs view
@@ -36,27 +36,27 @@     -- Helper to combine a command parser with the color option, creating an Options value     withColorOption :: Parser Command -> Parser Options     withColorOption cmdParser = Options <$> cmdParser <*> colorOption-    +     -- Subcommand parser that includes color options for each command     subcommandWithOptions = subparser       ( command "bump" (info (withColorOption bumpParser) (progDesc "Update stack*.yaml files (optionally specify files, or use --recursive/-r to search subdirectories)"))      <> command "dry-run" (info (withColorOption dryRunParser) (progDesc "Show what would be updated (default, optionally specify files, or use --recursive/-r to search subdirectories)"))      <> command "update" (info (withColorOption (pure Update)) (progDesc "Update stackage snapshots database"))-     <> command "info" (info (withColorOption (pure Info)) (progDesc "Print GHC version to snapshot mapping"))+     <> command "info" (info (withColorOption infoCmdParser) (progDesc "Print repository path and GHC version to snapshot mapping"))      <> command "config" (info (withColorOption configParser) (progDesc "Configure stacker"))      <> command "version" (info (withColorOption (pure Version)) (progDesc "Print version information (also: -V, --version)"))      <> command "numeric-version" (info (withColorOption (pure NumericVersion)) (progDesc "Print version number (also: --numeric-version)"))      <> command "license" (info (withColorOption (pure PrintLicense)) (progDesc "Print license text (also: --license)"))      <> command "help" (info (withColorOption (pure Help)) (progDesc "Print this help (also: -h, --help)"))       )-    +     -- Top-level parser for version/help flags and default command when no subcommand is specified     topLevelOptions = Options       <$> commandParserTopLevel       <*> colorOption-    +     -- Top-level command parser (for flags)-    commandParserTopLevel = +    commandParserTopLevel =       flag' Version (long "version" <> short 'V' <> help "Print version information")       <|> flag' NumericVersion (long "numeric-version" <> help "Print version number")       <|> flag' PrintLicense (long "license" <> help "Print license text")@@ -69,13 +69,20 @@   ( command "repo" (info repoParser (progDesc "Set repository path"))   ) +-- | Info command parser+infoCmdParser :: Parser Command+infoCmdParser = Info <$> (subparser+  ( command "repo" (info (pure InfoRepo) (progDesc "Print repository path"))+ <> command "snapshots" (info (pure InfoSnapshots) (progDesc "Print GHC version to snapshot mapping"))+  ) <|> pure InfoAll)+ -- | Repo config parser repoParser :: Parser ConfigCmd repoParser = SetRepo <$> argument str (metavar "PATH")  -- | Helper to parse file arguments with optional recursive flag filesParser :: ([FilePath] -> Bool -> Command) -> Parser Command-filesParser cmd = cmd +filesParser cmd = cmd   <$> many (argument str (metavar "FILES..." <> action "file"))   <*> recursiveOption 
src/PathUtil.hs view
@@ -1,6 +1,6 @@ -- | Path utilities for cross-platform compatibility--- --- This module provides path utilities that use forward slash (/) as the +--+-- This module provides path utilities that use forward slash (/) as the -- path separator on all platforms, including Windows. While Windows primarily -- uses backslash (\), it also accepts forward slash for relative paths, -- which is what this tool deals with.
src/StackYaml.hs view
@@ -18,7 +18,7 @@ import Data.Text.IO qualified as TIO import System.Directory (listDirectory, doesFileExist, pathIsSymbolicLink, getSymbolicLinkTarget, doesDirectoryExist) import PathUtil ((</>), normalizeFilePath, collapseDots, takeDirectory, takeFileName, toForwardSlashes)-import Types (Action(..))+import Types (Action(..), Bump(..))  -- | Check if a filename is a stack*.yaml file isStackYaml :: FilePath -> Bool@@ -43,17 +43,17 @@   entries <- listDirectory dir   -- Use custom </> to ensure consistent path output with "/" separator on all platforms   let fullPaths = map (dir </>) entries-  +   -- Process files and directories separately   files <- filterM doesFileExist fullPaths   dirs <- filterM doesDirectoryExist fullPaths-  +   -- Find stack*.yaml files in current directory, normalize to remove "./" prefix   let stackYamls = map normalizeFilePath $ filter isStackYaml files-  +   -- Recursively search subdirectories   subResults <- mapM findStackYamlFilesInDir dirs-  +   -- Combine and sort all results (all paths are already normalized)   return $ sort (stackYamls ++ concat subResults) @@ -122,9 +122,10 @@   case actionSymlinkTarget action of     Just _ -> return ()  -- Skip symlinks     Nothing ->-      case actionNewSnapshot action of-        Nothing -> return ()  -- No update needed-        Just newSnap -> do+      case actionBump action of+        UpToDate -> return ()+        Unknown -> return ()+        BumpTo newSnap -> do           when verbose $ putStrLn $ "Updating " ++ actionFile action           content <- TIO.readFile (actionFile action)           let (before, after) = splitAtSpan (actionSpan action) content
src/Types.hs view
@@ -34,11 +34,18 @@   | Nightly NightlyVersion   deriving (Eq, Ord, Show, Generic) +-- | Outcome of analyzing a snapshot+data Bump+  = UpToDate      -- ^ Already the latest snapshot for its GHC major version+  | BumpTo Text   -- ^ Should be bumped to this snapshot+  | Unknown       -- ^ Snapshot not recognized (e.g. not in the database)+  deriving (Eq, Show, Generic)+ -- | Action to take for a stack*.yaml file data Action = Action   { actionFile :: FilePath   , actionOldSnapshot :: Text-  , actionNewSnapshot :: Maybe Text+  , actionBump :: Bump   , actionIsResolver :: Bool  -- True if 'resolver' field, False if 'snapshot'   , actionSpan :: (Int, Int)  -- Character span to replace   , actionSymlinkTarget :: Maybe FilePath  -- Just target if this is a symlink to another stack*.yaml in the list@@ -62,11 +69,18 @@   | DryRun [FilePath] Bool  -- Optional list of files to check, recursive flag   | Config ConfigCmd   | Update-  | Info+  | Info InfoCmd   | Version   | NumericVersion   | PrintLicense   | Help+  deriving (Eq, Show)++-- | Info command+data InfoCmd+  = InfoAll        -- ^ Print repo path and snapshots (in YAML format)+  | InfoRepo       -- ^ Print just the repo path+  | InfoSnapshots  -- ^ Print just the GHC version to snapshot mapping   deriving (Eq, Show)  -- | Config command
stacker.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               stacker-version:            0.0.0.0+version:            0.0.1.0 synopsis:           Bump snapshots in stack.yaml files description:   A tool to bump snapshots (resolvers) in stack*.yaml files.@@ -19,6 +19,7 @@   GHC == 9.8.4   GHC == 9.10.3   GHC == 9.12.4+  GHC == 9.14.1  extra-doc-files:   CHANGELOG.md@@ -84,6 +85,7 @@     Paths_stacker   build-depends:     , base                  >= 4.17 && < 5+    , bytestring            >= 0.11     , directory             >= 1.3.7.0         -- XdgState requires directory-1.3.7.0 or higher     , filepath              >= 1.4@@ -110,6 +112,7 @@   hs-source-dirs:   test   main-is:          Main.hs   other-modules:+    TestAnalysis     TestDryRun     TestBump   build-depends:@@ -117,8 +120,10 @@     , stacker     , tasty         >= 1.4     , tasty-golden  >= 2.3+    , tasty-hunit   >= 0.10     , directory     >= 1.3.7.0     , text          >= 2.0+    , containers    >= 0.6     , bytestring    >= 0.11     , temporary     >= 1.3     , process
test/Main.hs view
@@ -1,6 +1,7 @@ module Main where  import Test.Tasty ( defaultMain, testGroup, TestTree )+import TestAnalysis ( analysisTests ) import TestDryRun ( dryRunTests ) import TestBump ( bumpTestsIO ) @@ -11,6 +12,7 @@  tests :: [TestTree] -> TestTree tests bumpTests = testGroup "stack-snapshots tests"-  [ testGroup "dry-run tests" dryRunTests+  [ testGroup "analysis tests" analysisTests+  , testGroup "dry-run tests" dryRunTests   , testGroup "bump tests" bumpTests   ]
+ test/TestAnalysis.hs view
@@ -0,0 +1,51 @@+{-# LANGUAGE OverloadedStrings #-}++module TestAnalysis (analysisTests) where++import Data.Map.Strict qualified as Map+import Test.Tasty (TestTree)+import Test.Tasty.HUnit (testCase, (@?=))++import Analysis (determineNewSnapshot)+import Types++-- | A small, fixed snapshot database, independent of the user's state directory.+testDB :: SnapshotDB+testDB = SnapshotDB+  { dbLTS = Map.fromList+      [ (LTSVersion 24 1, GHCVersion 9 10 2)+      , (LTSVersion 24 5, GHCVersion 9 10 3)+      ]+  , dbNightly = Map.fromList+      [ (NightlyVersion 2025 5 9, GHCVersion 9 10 2)+      , (NightlyVersion 2026 8 27, GHCVersion 9 12 4)+      -- No nightly for 2026-08-28 and 2026-08-29.+      , (NightlyVersion 2026 8 30, GHCVersion 9 12 4)+      , (NightlyVersion 2026 10 12, GHCVersion 9 12 4)+      ]+  , dbGHC = Map.empty+  }++analysisTests :: [TestTree]+analysisTests =+  [ testCase "nightly with LTS for its GHC bumps to LTS" $+      determineNewSnapshot testDB "nightly-2025-05-09" @?= BumpTo "lts-24.5"+  , testCase "known nightly bumps to latest nightly" $+      determineNewSnapshot testDB "nightly-2026-08-27" @?= BumpTo "nightly-2026-10-12"+  , testCase "nightly missing from database bumps to latest nightly" $+      determineNewSnapshot testDB "nightly-2026-08-28" @?= BumpTo "nightly-2026-10-12"+  , testCase "latest nightly is up to date" $+      determineNewSnapshot testDB "nightly-2026-10-12" @?= UpToDate+  , testCase "nightly newer than database is unknown" $+      determineNewSnapshot testDB "nightly-2026-10-20" @?= Unknown+  , testCase "LTS bumps to latest minor" $+      determineNewSnapshot testDB "lts-24.1" @?= BumpTo "lts-24.5"+  , testCase "latest LTS is up to date" $+      determineNewSnapshot testDB "lts-24.5" @?= UpToDate+  , testCase "LTS newer than database is unknown" $+      determineNewSnapshot testDB "lts-24.9" @?= Unknown+  , testCase "LTS with unknown major is unknown" $+      determineNewSnapshot testDB "lts-99.1" @?= Unknown+  , testCase "non-Stackage resolver is unknown" $+      determineNewSnapshot testDB "ghc-9.12.4" @?= Unknown+  ]
test/golden/dry-run-recursive.golden view
@@ -1,6 +1,6 @@-other/stack.yaml                 lts-24.20                → bump to lts-24.36+other/stack.yaml                 lts-24.20                → bump to lts-24.63 stack-9.6.yaml                   lts-22.40                → bump to lts-22.44 stack.yaml                       lts-22.40                = symlink to stack-9.6.yaml subdir1/stack-9.8.yaml           lts-23.20                → bump to lts-23.28 subdir1/stack-other.yaml         lts-24.20                = symlink to other/stack.yaml-subdir1/subdir2/stack-9.10.yaml  nightly-2025-01-01       → bump to lts-24.36+subdir1/subdir2/stack-9.10.yaml  nightly-2025-01-01       → bump to lts-24.63
test/golden/dry-run.golden view
@@ -1,6 +1,6 @@ stack-9.0.yaml      nightly-2021-12-04       → bump to lts-19.33-stack-9.10.yaml     nightly-2025-05-14       → bump to lts-24.36-stack-9.12.yaml     nightly-2025-10-05       → bump to nightly-2026-04-07+stack-9.10.yaml     nightly-2025-05-14       → bump to lts-24.63+stack-9.12.yaml     nightly-2025-10-05       → bump to nightly-2026-10-12 stack-9.2.yaml      lts-20.1                 → bump to lts-20.26 stack-9.4.yaml      lts-21.20                → bump to lts-21.25 stack-9.6.yaml      nightly-2023-08-04       → bump to lts-22.44
test/golden/tests/stack-9.10.yaml view
@@ -1,4 +1,4 @@-resolver: lts-24.36+resolver: lts-24.63  packages:   - lib/
test/golden/tests/stack-9.12.yaml view
@@ -1,1 +1,1 @@-resolver: nightly-2026-04-07+resolver: nightly-2026-10-12
test/golden/tests/stack.yaml view
@@ -1,4 +1,4 @@-resolver: lts-24.36+resolver: lts-24.63  packages:   - lib/