feat(workflows): improve linter
This commit is contained in:
parent
1d9f078b70
commit
316097a07e
@ -301,6 +301,14 @@ executables:
|
|||||||
when:
|
when:
|
||||||
- condition: flag(library-only)
|
- condition: flag(library-only)
|
||||||
buildable: false
|
buildable: false
|
||||||
|
wflint:
|
||||||
|
main: WFLint.hs
|
||||||
|
ghc-options:
|
||||||
|
- -main-is WFLint
|
||||||
|
source-dirs: [wflint, src]
|
||||||
|
when:
|
||||||
|
- condition: flag(library-only)
|
||||||
|
buildable: false
|
||||||
|
|
||||||
# Test suite
|
# Test suite
|
||||||
tests:
|
tests:
|
||||||
|
|||||||
14
src/Data/MultiSet/Instances.hs
Normal file
14
src/Data/MultiSet/Instances.hs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
|
module Data.MultiSet.Instances
|
||||||
|
(
|
||||||
|
) where
|
||||||
|
|
||||||
|
import ClassyPrelude
|
||||||
|
import Data.MultiSet
|
||||||
|
|
||||||
|
|
||||||
|
type instance Element (MultiSet a) = a
|
||||||
|
|
||||||
|
instance MonoFoldable (MultiSet a)
|
||||||
|
instance GrowingAppend (MultiSet a)
|
||||||
@ -1,6 +1,6 @@
|
|||||||
module Handler.Utils.Workflow.Form
|
module Handler.Utils.Workflow.Form
|
||||||
( FileIdent
|
( FileIdent
|
||||||
, WorkflowGraphForm(..)
|
, WorkflowGraphForm(..), FormWorkflowGraph
|
||||||
, workflowGraphForm
|
, workflowGraphForm
|
||||||
, toWorkflowGraphForm, fromWorkflowGraphForm
|
, toWorkflowGraphForm, fromWorkflowGraphForm
|
||||||
, WorkflowDescriptionsFormScope(..)
|
, WorkflowDescriptionsFormScope(..)
|
||||||
|
|||||||
@ -186,6 +186,7 @@ import Control.Monad.Catch.Instances as Import ()
|
|||||||
import Text.Shakespeare.Text.Instances as Import ()
|
import Text.Shakespeare.Text.Instances as Import ()
|
||||||
import Ldap.Client.Instances as Import ()
|
import Ldap.Client.Instances as Import ()
|
||||||
import Network.URI.Instances as Import ()
|
import Network.URI.Instances as Import ()
|
||||||
|
import Data.MultiSet.Instances as Import ()
|
||||||
|
|
||||||
import Crypto.Hash as Import (Digest, SHA3_256, SHA3_512)
|
import Crypto.Hash as Import (Digest, SHA3_256, SHA3_512)
|
||||||
import Crypto.Random as Import (ChaChaDRG, Seed)
|
import Crypto.Random as Import (ChaChaDRG, Seed)
|
||||||
|
|||||||
@ -75,10 +75,10 @@ setSerializable' policy act = do
|
|||||||
transactionSaveWithIsolation ReadCommitted
|
transactionSaveWithIsolation ReadCommitted
|
||||||
return res
|
return res
|
||||||
|
|
||||||
catchSql :: forall m a. (MonadCatch m, MonadIO m) => SqlPersistT m a -> (SqlError -> SqlPersistT m a) -> SqlPersistT m a
|
catchSql :: forall e m a. (MonadCatch m, MonadIO m, Exception e) => SqlPersistT m a -> (e -> SqlPersistT m a) -> SqlPersistT m a
|
||||||
catchSql = flip handleSql
|
catchSql = flip handleSql
|
||||||
|
|
||||||
handleSql :: forall m a. (MonadCatch m, MonadIO m) => (SqlError -> SqlPersistT m a) -> SqlPersistT m a -> SqlPersistT m a
|
handleSql :: forall e m a. (MonadCatch m, MonadIO m, Exception e) => (e -> SqlPersistT m a) -> SqlPersistT m a -> SqlPersistT m a
|
||||||
handleSql recover act = do
|
handleSql recover act = do
|
||||||
savepointName <- liftIO $ UUID.toString <$> getRandom
|
savepointName <- liftIO $ UUID.toString <$> getRandom
|
||||||
|
|
||||||
@ -94,10 +94,10 @@ handleSql recover act = do
|
|||||||
rawExecute [st|RELEASE SAVEPOINT "#{savepointName}"|] []
|
rawExecute [st|RELEASE SAVEPOINT "#{savepointName}"|] []
|
||||||
return res
|
return res
|
||||||
|
|
||||||
catchIfSql :: forall m a. (MonadCatch m, MonadIO m) => (SqlError -> Bool) -> SqlPersistT m a -> (SqlError -> SqlPersistT m a) -> SqlPersistT m a
|
catchIfSql :: forall e m a. (MonadCatch m, MonadIO m, Exception e) => (e -> Bool) -> SqlPersistT m a -> (e -> SqlPersistT m a) -> SqlPersistT m a
|
||||||
catchIfSql p = flip $ handleIfSql p
|
catchIfSql p = flip $ handleIfSql p
|
||||||
|
|
||||||
handleIfSql :: forall m a. (MonadCatch m, MonadIO m) => (SqlError -> Bool) -> (SqlError -> SqlPersistT m a) -> SqlPersistT m a -> SqlPersistT m a
|
handleIfSql :: forall e m a. (MonadCatch m, MonadIO m, Exception e) => (e -> Bool) -> (e -> SqlPersistT m a) -> SqlPersistT m a -> SqlPersistT m a
|
||||||
handleIfSql p recover = handleSql (\err -> bool throwM recover (p err) err)
|
handleIfSql p recover = handleSql (\err -> bool throwM recover (p err) err)
|
||||||
|
|
||||||
isUniqueConstraintViolation :: SqlError -> Bool
|
isUniqueConstraintViolation :: SqlError -> Bool
|
||||||
|
|||||||
@ -6,7 +6,10 @@ module Utils.Workflow.Lint
|
|||||||
import Import.NoFoundation
|
import Import.NoFoundation
|
||||||
|
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
|
import Data.MultiSet (MultiSet)
|
||||||
|
import qualified Data.MultiSet as MultiSet
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
|
import qualified Data.Sequence as Seq
|
||||||
|
|
||||||
|
|
||||||
{-# ANN module ("HLint: ignore Use newtype instead of data"::String) #-}
|
{-# ANN module ("HLint: ignore Use newtype instead of data"::String) #-}
|
||||||
@ -14,26 +17,46 @@ import qualified Data.Map as Map
|
|||||||
|
|
||||||
data WorkflowGraphLinterIssue
|
data WorkflowGraphLinterIssue
|
||||||
= WGLUnknownGraphNodeLabel WorkflowGraphNodeLabel
|
= WGLUnknownGraphNodeLabel WorkflowGraphNodeLabel
|
||||||
|
| WGLPayloadInvisibleInTargetNode (WorkflowGraphNodeLabel, WorkflowGraphEdgeLabel) WorkflowPayloadLabel
|
||||||
|
| WGLFinalNodeHasOutgoingEdges WorkflowGraphNodeLabel | WGLNonFinalNodeHasNoOutgoingEdges WorkflowGraphNodeLabel
|
||||||
|
| WGLUndefinedFieldOrder (WorkflowGraphNodeLabel, WorkflowGraphEdgeLabel) (NonNull (MultiSet WorkflowPayloadLabel))
|
||||||
|
| WGLNodeUnreachable WorkflowGraphNodeLabel
|
||||||
|
| WGLNodeUnfinalizable WorkflowGraphNodeLabel
|
||||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
instance Exception WorkflowGraphLinterIssue where
|
instance Exception WorkflowGraphLinterIssue where
|
||||||
displayException = \case
|
displayException = \case
|
||||||
WGLUnknownGraphNodeLabel nodeLbl -> unpack [st|Unknown GraphNodeLabel: “#{toPathPiece nodeLbl}”|]
|
WGLUnknownGraphNodeLabel nodeLbl
|
||||||
|
-> unpack [st|Unknown GraphNodeLabel: #{tshow (toPathPiece nodeLbl)}|]
|
||||||
|
WGLPayloadInvisibleInTargetNode (nodeLbl, edgeLbl) payloadLbl
|
||||||
|
-> unpack [st|Payload #{tshow (toPathPiece payloadLbl)} has form on edge #{tshow (toPathPiece edgeLbl)} to target node #{tshow (toPathPiece nodeLbl)} but no viewers on target node|]
|
||||||
|
WGLFinalNodeHasOutgoingEdges nodeLbl
|
||||||
|
-> unpack [st|Node #{tshow (toPathPiece nodeLbl)} is marked “final” but has outgoing edges|]
|
||||||
|
WGLNonFinalNodeHasNoOutgoingEdges nodeLbl
|
||||||
|
-> unpack [st|Node #{tshow (toPathPiece nodeLbl)} isn't marked “final” but has no outgoing edges|]
|
||||||
|
WGLUndefinedFieldOrder (nodeLbl, edgeLbl) payloads
|
||||||
|
-> unpack [st|Form for edge #{tshow (toPathPiece edgeLbl)} to target node #{tshow (toPathPiece nodeLbl)} has ill defined field order for payload(s): “#{intercalate ", " (map (tshow . toPathPiece) (MultiSet.elems (toNullable payloads)))}”|]
|
||||||
|
WGLNodeUnreachable nodeLbl
|
||||||
|
-> unpack [st|Node #{tshow (toPathPiece nodeLbl)} is unreachable from all initial edges|]
|
||||||
|
WGLNodeUnfinalizable nodeLbl
|
||||||
|
-> unpack [st|Node #{tshow (toPathPiece nodeLbl)} has no path to a final node|]
|
||||||
|
|
||||||
lintWorkflowGraph :: WorkflowGraph fileid userid -> Maybe (NonNull (Set WorkflowGraphLinterIssue))
|
lintWorkflowGraph :: forall fileid userid. WorkflowGraph fileid userid -> Maybe (NonNull (Set WorkflowGraphLinterIssue))
|
||||||
lintWorkflowGraph graph = fromNullable . Set.fromList $ concatMap ($ graph)
|
lintWorkflowGraph graph = fromNullable . Set.fromList $ concatMap ($ graph)
|
||||||
[ checkEdgesForUnknownGraphNodeLabel
|
[ checkEdgesForUnknownGraphNodeLabel
|
||||||
|
, checkFormPayloadVisibleInTargetNode
|
||||||
|
, finalMatchesOutgoingEdges
|
||||||
|
, checkUndefinedFieldOrder
|
||||||
|
, checkNodeUnreachable
|
||||||
|
, checkNodeUnfinalizable
|
||||||
-- Future ideas:
|
-- Future ideas:
|
||||||
-- - node with no outgoing edges that isn't final
|
|
||||||
-- - final node with outgoing edges
|
|
||||||
-- - WorkflowRolePayloadReference for unknown payload
|
-- - WorkflowRolePayloadReference for unknown payload
|
||||||
-- - wgePayloadRestriction for unknown payload
|
-- - wgePayloadRestriction for unknown payload
|
||||||
-- - Undefined field order
|
|
||||||
-- - FieldReference for payload not defined in same form
|
-- - FieldReference for payload not defined in same form
|
||||||
-- - WorkflowRolePayloadReference to payload without user fields
|
-- - WorkflowRolePayloadReference to payload without user fields
|
||||||
-- - unreachable nodes
|
|
||||||
-- - all initial edges have only payload-reference
|
-- - all initial edges have only payload-reference
|
||||||
-- - cycles of automatic edges (also consider payload restrictions; computationally equivalent to SAT)
|
-- - cycles of automatic edges (also consider payload restrictions; computationally equivalent to SAT)
|
||||||
|
-- - unsatisfiable restrictions
|
||||||
]
|
]
|
||||||
where
|
where
|
||||||
checkEdgesForUnknownGraphNodeLabel WorkflowGraph{wgNodes} = foldMap (pure . WGLUnknownGraphNodeLabel) $ Set.fromList edgeNodeLabels `Set.difference` Map.keysSet wgNodes
|
checkEdgesForUnknownGraphNodeLabel WorkflowGraph{wgNodes} = foldMap (pure . WGLUnknownGraphNodeLabel) $ Set.fromList edgeNodeLabels `Set.difference` Map.keysSet wgNodes
|
||||||
@ -43,3 +66,78 @@ lintWorkflowGraph graph = fromNullable . Set.fromList $ concatMap ($ graph)
|
|||||||
WorkflowGraphEdgeManual{wgeSource} -> pure wgeSource
|
WorkflowGraphEdgeManual{wgeSource} -> pure wgeSource
|
||||||
WorkflowGraphEdgeAutomatic{wgeSource} -> pure wgeSource
|
WorkflowGraphEdgeAutomatic{wgeSource} -> pure wgeSource
|
||||||
WorkflowGraphEdgeInitial{} -> []
|
WorkflowGraphEdgeInitial{} -> []
|
||||||
|
checkFormPayloadVisibleInTargetNode WorkflowGraph{wgNodes} = ifoldMap (\nodeLbl node -> map (\(edgeLbl, payloadLbl) -> WGLPayloadInvisibleInTargetNode (nodeLbl, edgeLbl) payloadLbl) . Set.toList $ doCheck node) wgNodes
|
||||||
|
where
|
||||||
|
doCheck :: WorkflowGraphNode fileid userid -> Set (WorkflowGraphEdgeLabel, WorkflowPayloadLabel)
|
||||||
|
doCheck WGN{wgnEdges, wgnPayloadView} = ifoldMap (\edgeLbl -> Set.map (edgeLbl, ) . doCheck') wgnEdges
|
||||||
|
where
|
||||||
|
doCheck' :: WorkflowGraphEdge fileid userid -> Set WorkflowPayloadLabel
|
||||||
|
doCheck' wge = fromMaybe Set.empty $ do
|
||||||
|
WorkflowGraphEdgeForm{wgefFields} <- wge ^? _wgeForm
|
||||||
|
return $ Map.keysSet wgefFields `Set.difference` Map.keysSet wgnPayloadView
|
||||||
|
finalMatchesOutgoingEdges WorkflowGraph{wgNodes} = foldMap (\nodeLbl -> pure $ bool WGLFinalNodeHasOutgoingEdges WGLNonFinalNodeHasNoOutgoingEdges (nodeLbl `Set.notMember` markedFinalNodes) nodeLbl) $ markedFinalNodes `setSymmDiff` edgeFinalNodes
|
||||||
|
where
|
||||||
|
markedFinalNodes = Set.fromList $ do
|
||||||
|
(nodeLbl, WGN{wgnFinal}) <- Map.toList wgNodes
|
||||||
|
guard $ is _Just wgnFinal
|
||||||
|
return nodeLbl
|
||||||
|
edgeFinalNodes = Set.fromList $ do
|
||||||
|
nodeLbl <- Map.keys wgNodes
|
||||||
|
guard $ noneOf (folded . _wgnEdges . folded . _wgeSource) (== nodeLbl) wgNodes
|
||||||
|
return nodeLbl
|
||||||
|
checkUndefinedFieldOrder WorkflowGraph{wgNodes} = ifoldMap (\nodeLbl node -> map (\(edgeLbl, payloadLbls) -> WGLUndefinedFieldOrder (nodeLbl, edgeLbl) payloadLbls) . Set.toList $ doCheck node) wgNodes
|
||||||
|
where
|
||||||
|
doCheck :: WorkflowGraphNode fileid userid -> Set (WorkflowGraphEdgeLabel, NonNull (MultiSet (WorkflowPayloadLabel)))
|
||||||
|
doCheck WGN{wgnEdges} = ifoldMap (\edgeLbl -> foldMap (Set.singleton . (edgeLbl, )) . doCheck') wgnEdges
|
||||||
|
where
|
||||||
|
doCheck' :: WorkflowGraphEdge fileid userid -> [NonNull (MultiSet WorkflowPayloadLabel)]
|
||||||
|
doCheck' wge = do
|
||||||
|
WorkflowGraphEdgeForm{wgefFields} <- hoistMaybe $ wge ^? _wgeForm
|
||||||
|
let MergeMap orderMap = ifoldMap go wgefFields
|
||||||
|
where
|
||||||
|
go :: WorkflowPayloadLabel
|
||||||
|
-> NonNull (Set (NonNull (Map WorkflowGraphEdgeFormOrder (WorkflowPayloadSpec fileid userid))))
|
||||||
|
-> MergeMap WorkflowGraphEdgeFormOrder (NonNull (MultiSet WorkflowPayloadLabel))
|
||||||
|
go payloadLbl = foldMap go' . map (Map.keysSet . toNullable) . Set.toList . toNullable
|
||||||
|
where
|
||||||
|
go' :: Set WorkflowGraphEdgeFormOrder
|
||||||
|
-> MergeMap WorkflowGraphEdgeFormOrder (NonNull (MultiSet WorkflowPayloadLabel))
|
||||||
|
go' = foldMap $ \formOrder -> MergeMap . Map.singleton formOrder . impureNonNull $ MultiSet.singleton payloadLbl
|
||||||
|
filter ((> 1) . MultiSet.size . toNullable) $ Map.elems orderMap
|
||||||
|
checkNodeUnreachable WorkflowGraph{wgNodes} = foldMap (pure . WGLNodeUnreachable) $ Map.keysSet wgNodes `Set.difference` reachableNodes
|
||||||
|
where
|
||||||
|
initialNodes = Map.keysSet $ Map.filter isInitial wgNodes
|
||||||
|
where isInitial WGN{wgnEdges} = any (is _WorkflowGraphEdgeInitial) wgnEdges
|
||||||
|
reachableNodes = extendAfter graph initialNodes
|
||||||
|
checkNodeUnfinalizable WorkflowGraph{wgNodes} = foldMap (pure . WGLNodeUnfinalizable) $ Map.keysSet wgNodes `Set.difference` finalizableNodes
|
||||||
|
where
|
||||||
|
finalNodes = Map.keysSet $ Map.filter (has $ _wgnFinal . _Just) wgNodes
|
||||||
|
finalizableNodes = extendBefore graph finalNodes
|
||||||
|
|
||||||
|
extendAfter, extendBefore :: forall fileid userid. WorkflowGraph fileid userid -> Set WorkflowGraphNodeLabel -> Set WorkflowGraphNodeLabel
|
||||||
|
extendAfter WorkflowGraph{wgNodes} = go Set.empty . Seq.fromList . Set.toList
|
||||||
|
where
|
||||||
|
go :: Set WorkflowGraphNodeLabel -- ^ Already known reachable
|
||||||
|
-> Seq WorkflowGraphNodeLabel -- ^ Queue to check
|
||||||
|
-> Set WorkflowGraphNodeLabel
|
||||||
|
go known Seq.Empty = known
|
||||||
|
go known (n Seq.:<| ns)
|
||||||
|
| n `Set.member` known = go known ns
|
||||||
|
| otherwise = go (Set.insert n known) $ ns `searchStrategy` nextNodes
|
||||||
|
where nextNodes = Map.keysSet $ Map.filter hasSource wgNodes
|
||||||
|
hasSource WGN{wgnEdges} = anyOf (folded . _wgeSource) (== n) wgnEdges
|
||||||
|
extendBefore WorkflowGraph{wgNodes} = go Set.empty . Seq.fromList . Set.toList
|
||||||
|
where
|
||||||
|
go :: Set WorkflowGraphNodeLabel
|
||||||
|
-> Seq WorkflowGraphNodeLabel
|
||||||
|
-> Set WorkflowGraphNodeLabel
|
||||||
|
go known Seq.Empty = known
|
||||||
|
go known (n Seq.:<| ns)
|
||||||
|
| n `Set.member` known = go known ns
|
||||||
|
| otherwise = go (Set.insert n known) $ ns `searchStrategy` prevNodes
|
||||||
|
where
|
||||||
|
prevNodes = flip foldMap (wgNodes Map.!? n) $ \WGN{wgnEdges} -> setOf (folded . _wgeSource) wgnEdges
|
||||||
|
|
||||||
|
searchStrategy :: Seq WorkflowGraphNodeLabel -> Set WorkflowGraphNodeLabel -> Seq WorkflowGraphNodeLabel
|
||||||
|
-- ^ BFS
|
||||||
|
searchStrategy queue next = queue <> Seq.fromList (Set.toList next)
|
||||||
|
|||||||
@ -39,6 +39,8 @@ import Utils.Workflow.Lint
|
|||||||
import System.Directory (getModificationTime)
|
import System.Directory (getModificationTime)
|
||||||
import System.FilePath.Glob (glob)
|
import System.FilePath.Glob (glob)
|
||||||
|
|
||||||
|
import System.IO (hPutStrLn)
|
||||||
|
|
||||||
|
|
||||||
testdataDir :: FilePath
|
testdataDir :: FilePath
|
||||||
testdataDir = "testdata"
|
testdataDir = "testdata"
|
||||||
@ -1327,7 +1329,10 @@ fillDb = do
|
|||||||
|
|
||||||
liftIO . LBS.writeFile (testdataDir </> "bigAlloc_ordinal.csv") $ Csv.encode ordinalPriorities
|
liftIO . LBS.writeFile (testdataDir </> "bigAlloc_ordinal.csv") $ Csv.encode ordinalPriorities
|
||||||
|
|
||||||
do
|
let displayLinterIssue :: MonadIO m => WorkflowGraphLinterIssue -> m ()
|
||||||
|
displayLinterIssue = liftIO . hPutStrLn stderr . displayException
|
||||||
|
|
||||||
|
handleSql displayLinterIssue $ do
|
||||||
workflowDefinitionGraph <- Yaml.decodeFileThrow $ testdataDir </> "theses.yaml"
|
workflowDefinitionGraph <- Yaml.decodeFileThrow $ testdataDir </> "theses.yaml"
|
||||||
for_ (lintWorkflowGraph workflowDefinitionGraph) $ mapM_ throwM
|
for_ (lintWorkflowGraph workflowDefinitionGraph) $ mapM_ throwM
|
||||||
let
|
let
|
||||||
@ -1363,7 +1368,7 @@ fillDb = do
|
|||||||
, workflowInstanceDescriptionDescription = Just "Hier können Sie Abschlussarbeiten bei der Prüfungsverwaltung angemeldet werden, der relevante Student die Arbeit digital abgeben und im Anschluss auch die Benotung an die Prüfungsverwaltung übermittelt werden."
|
, workflowInstanceDescriptionDescription = Just "Hier können Sie Abschlussarbeiten bei der Prüfungsverwaltung angemeldet werden, der relevante Student die Arbeit digital abgeben und im Anschluss auch die Benotung an die Prüfungsverwaltung übermittelt werden."
|
||||||
}
|
}
|
||||||
|
|
||||||
do
|
handleSql displayLinterIssue $ do
|
||||||
workflowDefinitionGraph <- Yaml.decodeFileThrow $ testdataDir </> "recognitions-ifi.yaml"
|
workflowDefinitionGraph <- Yaml.decodeFileThrow $ testdataDir </> "recognitions-ifi.yaml"
|
||||||
for_ (lintWorkflowGraph workflowDefinitionGraph) $ mapM_ throwM
|
for_ (lintWorkflowGraph workflowDefinitionGraph) $ mapM_ throwM
|
||||||
let
|
let
|
||||||
|
|||||||
29
wflint/WFLint.hs
Normal file
29
wflint/WFLint.hs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
module WFLint
|
||||||
|
( main
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import Utils.Workflow.Lint
|
||||||
|
import Handler.Utils.Workflow.Form (FormWorkflowGraph)
|
||||||
|
|
||||||
|
import qualified Data.ByteString as ByteString
|
||||||
|
import qualified Data.Yaml as Yaml
|
||||||
|
|
||||||
|
import System.IO (hPutStrLn)
|
||||||
|
import System.Exit
|
||||||
|
|
||||||
|
|
||||||
|
exitParseError, exitLintIssues :: Int
|
||||||
|
exitParseError = 2
|
||||||
|
exitLintIssues = 3
|
||||||
|
|
||||||
|
die' :: (MonadIO m, Exception (Element mono), MonoFoldable mono) => Handle -> Int -> mono -> m a
|
||||||
|
die' h err excs = liftIO $ do
|
||||||
|
forM_ excs $ hPutStrLn h . displayException
|
||||||
|
exitWith $ ExitFailure err
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = do
|
||||||
|
mwf <- Yaml.decodeEither' <$> ByteString.getContents
|
||||||
|
(wf :: FormWorkflowGraph) <- either (die' stderr exitParseError . Identity) return mwf
|
||||||
|
for_ (lintWorkflowGraph wf) $ die' stdout exitLintIssues
|
||||||
Loading…
Reference in New Issue
Block a user