@@ -10,12 +10,17 @@ 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
16import Control.Task.Scheduler
17import Control.Task.Scheduler
18+import Crypto.PubKey.RSA.Read
17import Data.Aeson
19import Data.Aeson
18import Data.ByteString
20import Data.ByteString
21+import qualified Data.ByteString as BS
22+import qualified Data.ByteString.Char8 as BSC
23+import qualified Data.ByteString.Lazy.Char8 as BSCL
19import qualified Data.Csv as Csv
24import qualified Data.Csv as Csv
20import Data.IORef
25import Data.IORef
21import Data.Map (Map)
26import Data.Map (Map)
@@ -25,14 +30,17 @@ import Data.Submission.Query
25import Data.Tasks.Build
30import Data.Tasks.Build
26import Data.Tasks.StatusUpdate
31import Data.Tasks.StatusUpdate
27import qualified Data.Text as T
32import qualified Data.Text as T
33+import Data.Time.Clock
28import Data.Yaml
34import Data.Yaml
29import GHC.Generics
35import GHC.Generics
30import GitHub hiding (Accept)
36import GitHub hiding (Accept)
31import GitHub.App.Auth
37import GitHub.App.Auth
32import GitHub.App.Request
38import GitHub.App.Request
39+import GitHub.Auth (Auth (OAuth))
33import GitHub.Data.Name
40import GitHub.Data.Name
34import GitHub.Data.Webhooks.Events
41import GitHub.Data.Webhooks.Events
35−import GitHub.Data.Webhooks.Payload
42+import GitHub.Data.Webhooks.Payload hiding (URL (..))
43+import Network.HTTP.Client.TLS (newTlsManager)
36import Network.HTTP.Media.MediaType
44import Network.HTTP.Media.MediaType
37import Network.Wai.Handler.Warp
45import Network.Wai.Handler.Warp
38import Servant
46import Servant
@@ -42,9 +50,8 @@ import Server.Config
42import Server.Html
50import Server.Html
43import Server.Schema
51import Server.Schema
44import Squeal.PostgreSQL
52import Squeal.PostgreSQL
46import Text.Blaze.Html
53import Text.Blaze.Html
47−import UnliftIO.Exception (try)
54+import UnliftIO.Exception (catch, displayException, handleAny, throwIO, try)
50 "github-webhook"
57 "github-webhook"
@@ -52,8 +59,8 @@ type API =
52 :> GitHubSignedReqBody '[JSON] InstallationEvent
59 :> GitHubSignedReqBody '[JSON] InstallationEvent
53 :> Post '[JSON] ()
60 :> Post '[JSON] ()
54 :<|> GitHubEvent '[ 'WebhookPushEvent]
61 :<|> GitHubEvent '[ 'WebhookPushEvent]
55− :> GitHubSignedReqBody '[JSON] PushEvent
62+ :> GitHubSignedReqBody '[JSON] PushEvent
56− :> Post '[JSON] ()
63+ :> Post '[JSON] ()
58 :<|> "submission"
65 :<|> "submission"
59 :> Capture "user name" String
66 :> Capture "user name" String
@@ -68,17 +75,18 @@ type API =
68 :> Post '[HTML] Markup
75 :> Post '[HTML] Markup
69 :<|> "results" :> Capture "task name" String :> Get '[CSV, HTML] [Score]
76 :<|> "results" :> Capture "task name" String :> Get '[CSV, HTML] [Score]
71−webhookInstallation :: MonadIO m => RepoWebhookEvent -> ((), InstallationEvent) -> m ()
78+webhookInstallation :: WithLog env Message m => RepoWebhookEvent -> ((), InstallationEvent) -> m ()
72webhookInstallation _ ((), ev) =
79webhookInstallation _ ((), ev) =
73− liftIO $ do
80+ logInfo . T.pack . show $ ev
77−pendingStatus :: NewStatus
78−pendingStatus = NewStatus StatusPending Nothing Nothing Nothing
80webhookPushEvent ::
82webhookPushEvent ::
84+ MonadReader (ServerData m) m,
87+ MonadError ServerError m,
81− (StaticPQ m, WithLog env Message m, MonadUnliftIO m, HasTasks m) =>
88+ WithLog env Message m
82 RepoWebhookEvent ->
90 RepoWebhookEvent ->
83 ((), PushEvent) ->
91 ((), PushEvent) ->
@@ -86,30 +94,42 @@ webhookPushEvent _ ((), ev) = do
86 let repo = evPushRepository ev
94 let repo = evPushRepository ev
87 user = T.unpack . whUserLogin . evPushSender $ ev
95 user = T.unpack . whUserLogin . evPushSender $ ev
88 fullRepoName = whRepoFullName repo
96 fullRepoName = whRepoFullName repo
89− [owner, repoName] = T.splitOn "/" fullRepoName
97+ (owner, repoName) <- case T.splitOn "/" fullRepoName of
98+ [a, b] -> return (a, b)
99+ _ -> throwError err422
90− Just sha = evPushHeadSha ev
100+ sha <- maybe (throwError err422) return $ evPushHeadSha ev
91 tasks <- getTasks
101 tasks <- getTasks
92 let task =
102 let task =
93 M.lookupMax $
103 M.lookupMax $
94 M.takeWhileAntitone (repoName >) tasks
104 M.takeWhileAntitone (repoName >) tasks
95 case task of
105 case task of
96 Just (name, TaskConfig {..}) | T.take (T.length name) repoName == name -> do
106 Just (name, TaskConfig {..}) | T.take (T.length name) repoName == name -> do
97− 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)
98− createSubmission $
120+ createSubmission $
99− Submission
121+ Submission
101− (T.unpack fullRepoName)
123+ (T.unpack fullRepoName)
102− (T.unpack name)
124+ (T.unpack name)
103− (T.unpack sha)
125+ (T.unpack sha)
104− (Jsonb BuildScheduled)
126+ (Jsonb BuildScheduled)
105− scheduleTask $ Build prebuild dockerfile (N owner) (N repoName) (N sha)
127+ scheduleTask $ Build prebuild dockerfile (N owner) (N repoName) (N sha) timeoutMinutes
106− liftIO $ do
128+ logInfo . T.pack . show $ ev
109 _ -> return ()
129 _ -> return ()
111retestSubmission ::
131retestSubmission ::
112− (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) =>
113 String ->
133 String ->
114 String ->
134 String ->
115 String ->
135 String ->
@@ -128,9 +148,9 @@ retestSubmission owner repoName sha = do
128 M.takeWhileAntitone (repoName' >) tasks
148 M.takeWhileAntitone (repoName' >) tasks
129 case task of
149 case task of
130 Just (name, TaskConfig {..}) | T.take (T.length name) repoName' == name -> do
150 Just (name, TaskConfig {..}) | T.take (T.length name) repoName' == name -> do
131− scheduleTask $ StatusUpdate (N owner') (N repoName') (N sha') pendingStatus
151+ schedulePendingStatus (N owner') (N repoName') (N sha')
132 updateSubmissionStatus fullRepoName sha BuildScheduled
152 updateSubmissionStatus fullRepoName sha BuildScheduled
133− scheduleTask $ Build prebuild dockerfile (N owner') (N repoName') (N sha')
153+ scheduleTask $ Build prebuild dockerfile (N owner') (N repoName') (N sha') timeoutMinutes
134 _ -> return ()
154 _ -> return ()
135 redirectToSubmission owner repoName sha
155 redirectToSubmission owner repoName sha
@@ -148,13 +168,21 @@ newtype ServerM (schema :: SchemasType) a
148 MonadUnliftIO
168 MonadUnliftIO
171+instance MonadError ServerError (ServerM schema) where
172+ throwError = throwIO
151server :: ServerT API (ServerM Schema)
175server :: ServerT API (ServerM Schema)
152server = (webhookInstallation :<|> webhookPushEvent) :<|> getSubmissionR :<|> retestSubmission :<|> getResults
176server = (webhookInstallation :<|> webhookPushEvent) :<|> getSubmissionR :<|> retestSubmission :<|> getResults
154−runServer :: IO ()
178+runServer :: FilePath -> IO ()
155−runServer = do
179+runServer config = do
156− c@Config {..} <- getConfig
180+ c@Config {..} <- getConfig config
182+ pem <- BS.readFile appPkPemPath
183+ appPkPem <- case readRsaPem pem of
185+ Left e -> error (show e)
158 auth <- mkInstallationAuth (AppAuth appId appPkPem) installationId
186 auth <- mkInstallationAuth (AppAuth appId appPkPem) installationId
159 ts <- newIORef mempty
187 ts <- newIORef mempty
160 pool <- createConnectionPool databseUrl 1 0.5 10
188 pool <- createConnectionPool databseUrl 1 0.5 10
@@ -164,29 +192,38 @@ runServer = do
164 let context =
192 let context =
165 gitPolyHubKey (return webhookSecret)
193 gitPolyHubKey (return webhookSecret)
166 :. EmptyContext
194 :. EmptyContext
167− serverData = ServerData
195+ serverData =
168− { githubAppAuth = auth,
197+ { githubAppAuth = auth,
169− githubUserName = GithubUserName githubUsername,
170− githubAccessToken = GithubAccessToken personalAccessToken,
171− tasks = ts,
198+ tasks = ts,
172− logger = cfilter ((logSeverity <=) . msgSeverity) richMessageAction,
199+ logger = cfilter ((logSeverity <=) . msgSeverity) richMessageAction,
173− baseUrl = baseSiteUrl,
200+ baseUrl = baseSiteUrl,
174− dockerUrl = T.pack cfgDockerUrl
201+ dockerUrl = T.pack cfgDockerUrl,
202+ context = githubContext
176 repeatIfNotEmpty n f = f >>= \m -> do
204 repeatIfNotEmpty n f = f >>= \m -> do
177 when (m == 0) $ liftIO $ threadDelay n
205 when (m == 0) $ liftIO $ threadDelay n
178 repeatIfNotEmpty n f
206 repeatIfNotEmpty n f
179− _ <-
207+ printingErrors =
208+ handleAny (logError . T.pack . displayException >=> const (return 0))
209+ usingConnectionPool pool
210+ . (`runReaderT` serverData)
212+ . handleAny (error . displayException)
214+ replicateM_ builderCount
180− forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
215+ $ forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
181 . usingConnectionPool pool
216 . usingConnectionPool pool
182 . (`runReaderT` serverData)
217 . (`runReaderT` serverData)
184− $ runTasks @'[Build] 1
220+ $ runTasks @'[Build] 1
186 forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
222 forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
187 . usingConnectionPool pool
223 . usingConnectionPool pool
188 . (`runReaderT` serverData)
224 . (`runReaderT` serverData)
190 $ runTasks @'[StatusUpdate] 5
227 $ runTasks @'[StatusUpdate] 5
191 run port $ serveWithContext (Proxy @API) context $
228 run port $ serveWithContext (Proxy @API) context $
192 hoistServerWithContext
229 hoistServerWithContext
@@ -204,12 +241,11 @@ updateTasks ref path =
204data ServerData m
241data ServerData m
205 = ServerData
242 = ServerData
206 { githubAppAuth :: !InstallationAuth,
243 { githubAppAuth :: !InstallationAuth,
207− githubUserName :: !GithubUserName,
208− githubAccessToken :: !GithubAccessToken,
209 tasks :: IORef TasksConfig,
244 tasks :: IORef TasksConfig,
210 logger :: LogAction m Message,
245 logger :: LogAction m Message,
211− baseUrl :: String,
246+ baseUrl :: T.Text,
212− dockerUrl :: T.Text
247+ dockerUrl :: T.Text,
215newtype PolyGitHubKey = PolyGitHubKey (forall result. GitHubKey result)
251newtype PolyGitHubKey = PolyGitHubKey (forall result. GitHubKey result)
@@ -220,11 +256,18 @@ gitPolyHubKey k = PolyGitHubKey (Servant.GitHub.Webhook.gitHubKey k)
220instance HasContextEntry '[PolyGitHubKey] (GitHubKey result) where
256instance HasContextEntry '[PolyGitHubKey] (GitHubKey result) where
221 getContextEntry (PolyGitHubKey x :. _) = x
257 getContextEntry (PolyGitHubKey x :. _) = x
223−instance MonadReader (ServerData n) m => GithubCloner m where
259+instance (MonadReader (ServerData n) m, MonadIO m, MonadError ServerError m) => GithubCloner m where
224−
260+ getGithubInstallationToken =
225− getGithubAccessToken = asks githubAccessToken
261+ either
226−
262+ (\e -> throwError err500 {errBody = BSCL.pack . displayException $ e})
227− getGithubUserName = asks githubUserName
263+ return
266+ iAuth <- asks githubAppAuth
267+ manager <- newTlsManager
268+ liftIO (obtainAccessToken manager iAuth) >>= either throwError return >>= \case
269+ OAuth token -> return $ InstallationToken (BSC.unpack token)
270+ _ -> throwError $ UserError "Not OAuth token. This should never happen."
229instance (MonadIO m, MonadReader (ServerData n) m) => AppRequestable m where
272instance (MonadIO m, MonadReader (ServerData n) m) => AppRequestable m where
230 appRequest req = do
273 appRequest req = do
@@ -237,6 +280,71 @@ instance MonadReader (ServerData n) m => MonadHasBaseUrl m where
237instance (MonadIO m, MonadReader (ServerData n) m) => HasTasks m where
280instance (MonadIO m, MonadReader (ServerData n) m) => HasTasks m where
238 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
240instance (MonadReader (ServerData n) m) => HasDockerInfo m where
348instance (MonadReader (ServerData n) m) => HasDockerInfo m where
241 getDockerUrl = asks dockerUrl
349 getDockerUrl = asks dockerUrl
@@ -245,8 +353,9 @@ type TasksConfig = Map T.Text TaskConfig
245data TaskConfig
353data TaskConfig
246 = TaskConfig
354 = TaskConfig
247 { prebuild :: String,
355 { prebuild :: String,
248− dockerfile :: String
356+ dockerfile :: String,
249− -- build :: T.Text
357+ timeoutMinutes :: Int,
251 deriving stock (Show, Eq, Generic)
360 deriving stock (Show, Eq, Generic)
252 deriving anyclass (FromJSON, ToJSON)
361 deriving anyclass (FromJSON, ToJSON)
@@ -255,7 +364,6 @@ class HasTasks m where
255 getTasks :: m TasksConfig
364 getTasks :: m TasksConfig
257instance HasLog (ServerData m) Message m where
366instance HasLog (ServerData m) Message m where
259 getLogAction :: ServerData m -> LogAction m Message
367 getLogAction :: ServerData m -> LogAction m Message
260 getLogAction = logger
368 getLogAction = logger
261 {-# INLINE getLogAction #-}
369 {-# INLINE getLogAction #-}