@@ -10,6 +10,7 @@ import Colog
10import Control.AppRequestable
10import Control.AppRequestable
11import Control.Concurrent
11import Control.Concurrent
12import Control.GithubCloner
12import Control.GithubCloner
13+import Control.HasGithubStatus
13import Control.Monad.Except
14import Control.Monad.Except
14import Control.Monad.IO.Unlift
15import Control.Monad.IO.Unlift
15import Control.Monad.Reader
16import Control.Monad.Reader
@@ -29,6 +30,7 @@ import Data.Submission.Query
29import Data.Tasks.Build
30import Data.Tasks.Build
30import Data.Tasks.StatusUpdate
31import Data.Tasks.StatusUpdate
31import qualified Data.Text as T
32import qualified Data.Text as T
33+import Data.Time.Clock
32import Data.Yaml
34import Data.Yaml
33import GHC.Generics
35import GHC.Generics
34import GitHub hiding (Accept)
36import GitHub hiding (Accept)
@@ -37,7 +39,7 @@ import GitHub.App.Request
37import GitHub.Auth (Auth (OAuth))
39import GitHub.Auth (Auth (OAuth))
38import GitHub.Data.Name
40import GitHub.Data.Name
39import GitHub.Data.Webhooks.Events
41import GitHub.Data.Webhooks.Events
40−import GitHub.Data.Webhooks.Payload
42+import GitHub.Data.Webhooks.Payload hiding (URL (..))
41import Network.HTTP.Client.TLS (newTlsManager)
43import Network.HTTP.Client.TLS (newTlsManager)
42import Network.HTTP.Media.MediaType
44import Network.HTTP.Media.MediaType
43import Network.Wai.Handler.Warp
45import Network.Wai.Handler.Warp
@@ -48,7 +50,6 @@ import Server.Config
48import Server.Html
50import Server.Html
49import Server.Schema
51import Server.Schema
50import Squeal.PostgreSQL
52import Squeal.PostgreSQL
52import Text.Blaze.Html
53import Text.Blaze.Html
53import UnliftIO.Exception (catch, displayException, handleAny, throwIO, try)
54import UnliftIO.Exception (catch, displayException, handleAny, throwIO, try)
@@ -74,17 +75,18 @@ type API =
74 :> Post '[HTML] Markup
75 :> Post '[HTML] Markup
75 :<|> "results" :> Capture "task name" String :> Get '[CSV, HTML] [Score]
76 :<|> "results" :> Capture "task name" String :> Get '[CSV, HTML] [Score]
77−webhookInstallation :: MonadIO m => RepoWebhookEvent -> ((), InstallationEvent) -> m ()
78+webhookInstallation :: WithLog env Message m => RepoWebhookEvent -> ((), InstallationEvent) -> m ()
78webhookInstallation _ ((), ev) =
79webhookInstallation _ ((), ev) =
79− liftIO $ do
80+ logInfo . T.pack . show $ ev
83−pendingStatus :: NewStatus
84−pendingStatus = NewStatus StatusPending Nothing Nothing Nothing
86webhookPushEvent ::
82webhookPushEvent ::
84+ MonadReader (ServerData m) m,
87− (StaticPQ m, WithLog env Message m, MonadUnliftIO m, HasTasks m, MonadError ServerError m) =>
87+ MonadError ServerError m,
88+ WithLog env Message m
88 RepoWebhookEvent ->
90 RepoWebhookEvent ->
89 ((), PushEvent) ->
91 ((), PushEvent) ->
@@ -102,22 +104,32 @@ webhookPushEvent _ ((), ev) = do
102 M.takeWhileAntitone (repoName >) tasks
104 M.takeWhileAntitone (repoName >) tasks
103 case task of
105 case task of
104 Just (name, TaskConfig {..}) | T.take (T.length name) repoName == name -> do
106 Just (name, TaskConfig {..}) | T.take (T.length name) repoName == name -> do
105− scheduleTask $ StatusUpdate (N owner) (N repoName) (N sha) pendingStatus
107+ nowTime <- liftIO getCurrentTime
108+ if nowTime > deadline
110+ deadlineHasPassedStatus (N owner) (N repoName) (N sha)
114+ (T.unpack fullRepoName)
117+ (Jsonb PastDeadline)
119+ schedulePendingStatus (N owner) (N repoName) (N sha)
106− createSubmission $
120+ createSubmission $
107− Submission
121+ Submission
109− (T.unpack fullRepoName)
123+ (T.unpack fullRepoName)
110− (T.unpack name)
124+ (T.unpack name)
111− (T.unpack sha)
125+ (T.unpack sha)
112− (Jsonb BuildScheduled)
126+ (Jsonb BuildScheduled)
113− scheduleTask $ Build prebuild dockerfile (N owner) (N repoName) (N sha) timeoutMinutes
127+ scheduleTask $ Build prebuild dockerfile (N owner) (N repoName) (N sha) timeoutMinutes
114− liftIO $ do
128+ logInfo . T.pack . show $ ev
117 _ -> return ()
129 _ -> return ()
119retestSubmission ::
131retestSubmission ::
120− (StaticPQ m, WithLog env Message m, MonadUnliftIO m, HasTasks m, MonadHasBaseUrl m) =>
132+ (StaticPQ m, MonadReader (ServerData m) m, MonadUnliftIO m, HasTasks m, MonadHasBaseUrl m) =>
121 String ->
133 String ->
122 String ->
134 String ->
123 String ->
135 String ->
@@ -136,7 +148,7 @@ retestSubmission owner repoName sha = do
136 M.takeWhileAntitone (repoName' >) tasks
148 M.takeWhileAntitone (repoName' >) tasks
137 case task of
149 case task of
138 Just (name, TaskConfig {..}) | T.take (T.length name) repoName' == name -> do
150 Just (name, TaskConfig {..}) | T.take (T.length name) repoName' == name -> do
139− scheduleTask $ StatusUpdate (N owner') (N repoName') (N sha') pendingStatus
151+ schedulePendingStatus (N owner') (N repoName') (N sha')
140 updateSubmissionStatus fullRepoName sha BuildScheduled
152 updateSubmissionStatus fullRepoName sha BuildScheduled
141 scheduleTask $ Build prebuild dockerfile (N owner') (N repoName') (N sha') timeoutMinutes
153 scheduleTask $ Build prebuild dockerfile (N owner') (N repoName') (N sha') timeoutMinutes
142 _ -> return ()
154 _ -> return ()
@@ -163,9 +175,9 @@ instance MonadError ServerError (ServerM schema) where
163server :: ServerT API (ServerM Schema)
175server :: ServerT API (ServerM Schema)
164server = (webhookInstallation :<|> webhookPushEvent) :<|> getSubmissionR :<|> retestSubmission :<|> getResults
176server = (webhookInstallation :<|> webhookPushEvent) :<|> getSubmissionR :<|> retestSubmission :<|> getResults
166−runServer :: IO ()
178+runServer :: FilePath -> IO ()
167−runServer = do
179+runServer config = do
168− c@Config {..} <- getConfig
180+ c@Config {..} <- getConfig config
170 pem <- BS.readFile appPkPemPath
182 pem <- BS.readFile appPkPemPath
171 appPkPem <- case readRsaPem pem of
183 appPkPem <- case readRsaPem pem of
@@ -186,20 +198,26 @@ runServer = do
186 tasks = ts,
198 tasks = ts,
187 logger = cfilter ((logSeverity <=) . msgSeverity) richMessageAction,
199 logger = cfilter ((logSeverity <=) . msgSeverity) richMessageAction,
188 baseUrl = baseSiteUrl,
200 baseUrl = baseSiteUrl,
189− dockerUrl = T.pack cfgDockerUrl
201+ dockerUrl = T.pack cfgDockerUrl,
202+ context = githubContext
191 repeatIfNotEmpty n f = f >>= \m -> do
204 repeatIfNotEmpty n f = f >>= \m -> do
192 when (m == 0) $ liftIO $ threadDelay n
205 when (m == 0) $ liftIO $ threadDelay n
193 repeatIfNotEmpty n f
206 repeatIfNotEmpty n f
194 printingErrors =
207 printingErrors =
195 handleAny (logError . T.pack . displayException >=> const (return 0))
208 handleAny (logError . T.pack . displayException >=> const (return 0))
196− _ <-
209+ usingConnectionPool pool
210+ . (`runReaderT` serverData)
212+ . handleAny (error . displayException)
214+ replicateM_ builderCount
197− forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
215+ $ forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
198 . usingConnectionPool pool
216 . usingConnectionPool pool
199 . (`runReaderT` serverData)
217 . (`runReaderT` serverData)
201 . printingErrors
219 . printingErrors
202− $ runTasks @'[Build] 1
220+ $ runTasks @'[Build] 1
204 forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
222 forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
205 . usingConnectionPool pool
223 . usingConnectionPool pool
@@ -225,8 +243,9 @@ data ServerData m
225 { githubAppAuth :: !InstallationAuth,
243 { githubAppAuth :: !InstallationAuth,
226 tasks :: IORef TasksConfig,
244 tasks :: IORef TasksConfig,
227 logger :: LogAction m Message,
245 logger :: LogAction m Message,
228− baseUrl :: String,
246+ baseUrl :: T.Text,
229− dockerUrl :: T.Text
247+ dockerUrl :: T.Text,
232newtype PolyGitHubKey = PolyGitHubKey (forall result. GitHubKey result)
251newtype PolyGitHubKey = PolyGitHubKey (forall result. GitHubKey result)
@@ -261,6 +280,71 @@ instance MonadReader (ServerData n) m => MonadHasBaseUrl m where
261instance (MonadIO m, MonadReader (ServerData n) m) => HasTasks m where
280instance (MonadIO m, MonadReader (ServerData n) m) => HasTasks m where
262 getTasks = asks tasks >>= liftIO . readIORef
281 getTasks = asks tasks >>= liftIO . readIORef
283+instance (MonadReader (ServerData m) m, StaticPQ m, MonadUnliftIO m) => HasGithubStatus m where
284+ schedulePendingStatus nOwner@(N owner) nRepo@(N repo) nCommit@(N sha) = do
294+ (Just . URL $ h <> "/submission/" <> owner <> "/" <> repo <> "/" <> sha)
298+ scheduleTestedStatus TestResult {..} nOwner@(N owner) nRepo@(N repo) nCommit@(N sha) = do
301+ let total = M.size tests
302+ passed = M.size . M.filter id $ tests
303+ description = T.pack $ show passed <> "/" <> show total <> if passed == total then " ✅" else " ❌"
311+ if total == passed then StatusSuccess else StatusFailure,
312+ newStatusTargetUrl = Just . URL $ h <> "/submission/" <> owner <> "/" <> repo <> "/" <> sha,
313+ newStatusDescription = Just description,
314+ newStatusContext = Just c
317+ scheduleFailedStatus _ nOwner@(N owner) nRepo@(N repo) nCommit@(N sha) = do
326+ { newStatusState = StatusError,
327+ newStatusTargetUrl = Just . URL $ h <> "/submission/" <> owner <> "/" <> repo <> "/" <> sha,
328+ newStatusDescription = Just "Build failed ☠️",
329+ newStatusContext = Just c
332+ deadlineHasPassedStatus nOwner@(N owner) nRepo@(N repo) nCommit@(N sha) = do
341+ { newStatusState = StatusError,
342+ newStatusTargetUrl = Just . URL $ h <> "/submission/" <> owner <> "/" <> repo <> "/" <> sha,
343+ newStatusDescription = Just "The deadline has passed ⏰",
344+ newStatusContext = Just c
264instance (MonadReader (ServerData n) m) => HasDockerInfo m where
348instance (MonadReader (ServerData n) m) => HasDockerInfo m where
265 getDockerUrl = asks dockerUrl
349 getDockerUrl = asks dockerUrl
@@ -270,7 +354,8 @@ data TaskConfig
270 = TaskConfig
354 = TaskConfig
271 { prebuild :: String,
355 { prebuild :: String,
272 dockerfile :: String,
356 dockerfile :: String,
273− timeoutMinutes :: Int
357+ timeoutMinutes :: Int,
275 deriving stock (Show, Eq, Generic)
360 deriving stock (Show, Eq, Generic)
276 deriving anyclass (FromJSON, ToJSON)
361 deriving anyclass (FromJSON, ToJSON)