@@ -10,12 +10,17 @@ import Colog
1010import Control.AppRequestable
1111import Control.Concurrent
1212import Control.GithubCloner
13+import Control.HasGithubStatus
1314import Control.Monad.Except
1415import Control.Monad.IO.Unlift
1516import Control.Monad.Reader
1617import Control.Task.Scheduler
18+import Crypto.PubKey.RSA.Read
1719import Data.Aeson
1820import 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
1924import qualified Data.Csv as Csv
2025import Data.IORef
2126import Data.Map (Map)
@@ -25,14 +30,17 @@ import Data.Submission.Query
2530import Data.Tasks.Build
2631import Data.Tasks.StatusUpdate
2732import qualified Data.Text as T
33+import Data.Time.Clock
2834import Data.Yaml
2935import GHC.Generics
3036import GitHub hiding (Accept)
3137import GitHub.App.Auth
3238import GitHub.App.Request
39+import GitHub.Auth (Auth (OAuth))
3340import GitHub.Data.Name
3441import 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)
3644import Network.HTTP.Media.MediaType
3745import Network.Wai.Handler.Warp
3846import Servant
@@ -42,9 +50,8 @@ import Server.Config
4250import Server.Html
4351import Server.Schema
4452import Squeal.PostgreSQL
45−import System.IO
4653import Text.Blaze.Html
47−import UnliftIO.Exception (catch, throwIO, try)
54+import UnliftIO.Exception (catch, displayException, handleAny, throwIO, try)
4855
4956type API =
5057 "github-webhook"
@@ -68,17 +75,18 @@ type API =
6875 :> Post '[HTML] Markup
6976 :<|> "results" :> Capture "task name" String :> Get '[CSV, HTML] [Score]
7077
71−webhookInstallation :: MonadIO m => RepoWebhookEvent -> ((), InstallationEvent) -> m ()
78+webhookInstallation :: WithLog env Message m => RepoWebhookEvent -> ((), InstallationEvent) -> m ()
7279webhookInstallation _ ((), ev) =
73− liftIO $ do
74− print ev
75− hFlush stdout
76−
77−pendingStatus :: NewStatus
78−pendingStatus = NewStatus StatusPending Nothing Nothing Nothing
80+ logInfo . T.pack . show $ ev
7981
8082webhookPushEvent ::
81− (StaticPQ m, WithLog env Message m, MonadUnliftIO m, HasTasks m, MonadError ServerError m) =>
83+ ( StaticPQ m,
84+ MonadReader (ServerData m) m,
85+ MonadUnliftIO m,
86+ HasTasks m,
87+ MonadError ServerError m,
88+ WithLog env Message m
89+ ) =>
8290 RepoWebhookEvent ->
8391 ((), PushEvent) ->
8492 m ()
@@ -96,22 +104,32 @@ webhookPushEvent _ ((), ev) = do
96104 M.takeWhileAntitone (repoName >) tasks
97105 case task of
98106 Just (name, TaskConfig {..}) | T.take (T.length name) repoName == name -> do
99− scheduleTask $ StatusUpdate (N owner) (N repoName) (N sha) pendingStatus
100− createSubmission $
101− Submission
102− user
103− (T.unpack fullRepoName)
104− (T.unpack name)
105− (T.unpack sha)
106− (Jsonb BuildScheduled)
107− scheduleTask $ Build prebuild dockerfile (N owner) (N repoName) (N sha) timeoutMinutes
108− liftIO $ do
109− print ev
110− hFlush stdout
107+ nowTime <- liftIO getCurrentTime
108+ if nowTime > deadline
109+ then do
110+ deadlineHasPassedStatus (N owner) (N repoName) (N sha)
111+ createSubmission $
112+ Submission
113+ user
114+ (T.unpack fullRepoName)
115+ (T.unpack name)
116+ (T.unpack sha)
117+ (Jsonb PastDeadline)
118+ else do
119+ schedulePendingStatus (N owner) (N repoName) (N sha)
120+ createSubmission $
121+ Submission
122+ user
123+ (T.unpack fullRepoName)
124+ (T.unpack name)
125+ (T.unpack sha)
126+ (Jsonb BuildScheduled)
127+ scheduleTask $ Build prebuild dockerfile (N owner) (N repoName) (N sha) timeoutMinutes
128+ logInfo . T.pack . show $ ev
111129 _ -> return ()
112130
113131retestSubmission ::
114− (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) =>
115133 String ->
116134 String ->
117135 String ->
@@ -130,7 +148,7 @@ retestSubmission owner repoName sha = do
130148 M.takeWhileAntitone (repoName' >) tasks
131149 case task of
132150 Just (name, TaskConfig {..}) | T.take (T.length name) repoName' == name -> do
133− scheduleTask $ StatusUpdate (N owner') (N repoName') (N sha') pendingStatus
151+ schedulePendingStatus (N owner') (N repoName') (N sha')
134152 updateSubmissionStatus fullRepoName sha BuildScheduled
135153 scheduleTask $ Build prebuild dockerfile (N owner') (N repoName') (N sha') timeoutMinutes
136154 _ -> return ()
@@ -157,10 +175,14 @@ instance MonadError ServerError (ServerM schema) where
157175server :: ServerT API (ServerM Schema)
158176server = (webhookInstallation :<|> webhookPushEvent) :<|> getSubmissionR :<|> retestSubmission :<|> getResults
159177
160−runServer :: IO ()
161−runServer = do
162− c@Config {..} <- getConfig
178+runServer :: FilePath -> IO ()
179+runServer config = do
180+ c@Config {..} <- getConfig config
163181 print c
182+ pem <- BS.readFile appPkPemPath
183+ appPkPem <- case readRsaPem pem of
184+ Right k -> return k
185+ Left e -> error (show e)
164186 auth <- mkInstallationAuth (AppAuth appId appPkPem) installationId
165187 ts <- newIORef mempty
166188 pool <- createConnectionPool databseUrl 1 0.5 10
@@ -173,27 +195,35 @@ runServer = do
173195 serverData =
174196 ServerData
175197 { githubAppAuth = auth,
176− githubUserName = GithubUserName githubUsername,
177− githubAccessToken = GithubAccessToken personalAccessToken,
178198 tasks = ts,
179199 logger = cfilter ((logSeverity <=) . msgSeverity) richMessageAction,
180200 baseUrl = baseSiteUrl,
181− dockerUrl = T.pack cfgDockerUrl
201+ dockerUrl = T.pack cfgDockerUrl,
202+ context = githubContext
182203 }
183204 repeatIfNotEmpty n f = f >>= \m -> do
184205 when (m == 0) $ liftIO $ threadDelay n
185206 repeatIfNotEmpty n f
186− _ <-
187− forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
207+ printingErrors =
208+ handleAny (logError . T.pack . displayException >=> const (return 0))
209+ usingConnectionPool pool
210+ . (`runReaderT` serverData)
211+ . unApp
212+ . handleAny (error . displayException)
213+ $ makeAllRunnable
214+ replicateM_ builderCount
215+ $ forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
188216 . usingConnectionPool pool
189217 . (`runReaderT` serverData)
190218 . unApp
191− $ runTasks @'[Build] 1
219+ . printingErrors
220+ $ runTasks @'[Build] 1
192221 _ <-
193222 forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
194223 . usingConnectionPool pool
195224 . (`runReaderT` serverData)
196225 . unApp
226+ . printingErrors
197227 $ runTasks @'[StatusUpdate] 5
198228 run port $ serveWithContext (Proxy @API) context $
199229 hoistServerWithContext
@@ -211,12 +241,11 @@ updateTasks ref path =
211241data ServerData m
212242 = ServerData
213243 { githubAppAuth :: !InstallationAuth,
214− githubUserName :: !GithubUserName,
215− githubAccessToken :: !GithubAccessToken,
216244 tasks :: IORef TasksConfig,
217245 logger :: LogAction m Message,
218− baseUrl :: String,
219− dockerUrl :: T.Text
246+ baseUrl :: T.Text,
247+ dockerUrl :: T.Text,
248+ context :: T.Text
220249 }
221250
222251newtype PolyGitHubKey = PolyGitHubKey (forall result. GitHubKey result)
@@ -227,10 +256,18 @@ gitPolyHubKey k = PolyGitHubKey (Servant.GitHub.Webhook.gitHubKey k)
227256instance HasContextEntry '[PolyGitHubKey] (GitHubKey result) where
228257 getContextEntry (PolyGitHubKey x :. _) = x
229258
230−instance MonadReader (ServerData n) m => GithubCloner m where
231− getGithubAccessToken = asks githubAccessToken
232−
233− getGithubUserName = asks githubUserName
259+instance (MonadReader (ServerData n) m, MonadIO m, MonadError ServerError m) => GithubCloner m where
260+ getGithubInstallationToken =
261+ either
262+ (\e -> throwError err500 {errBody = BSCL.pack . displayException $ e})
263+ return
264+ <=< runExceptT
265+ $ do
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."
234271
235272instance (MonadIO m, MonadReader (ServerData n) m) => AppRequestable m where
236273 appRequest req = do
@@ -243,6 +280,71 @@ instance MonadReader (ServerData n) m => MonadHasBaseUrl m where
243280instance (MonadIO m, MonadReader (ServerData n) m) => HasTasks m where
244281 getTasks = asks tasks >>= liftIO . readIORef
245282
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
285+ h <- asks baseUrl
286+ c <- asks context
287+ scheduleTask $
288+ StatusUpdate
289+ nOwner
290+ nRepo
291+ nCommit
292+ ( NewStatus
293+ StatusPending
294+ (Just . URL $ h <> "/submission/" <> owner <> "/" <> repo <> "/" <> sha)
295+ (Just "⌛")
296+ (Just c)
297+ )
298+ scheduleTestedStatus TestResult {..} nOwner@(N owner) nRepo@(N repo) nCommit@(N sha) = do
299+ h <- asks baseUrl
300+ c <- asks context
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 " ❌"
304+ status =
305+ StatusUpdate
306+ nOwner
307+ nRepo
308+ nCommit
309+ NewStatus
310+ { newStatusState =
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
315+ }
316+ scheduleTask status
317+ scheduleFailedStatus _ nOwner@(N owner) nRepo@(N repo) nCommit@(N sha) = do
318+ h <- asks baseUrl
319+ c <- asks context
320+ let status =
321+ StatusUpdate
322+ nOwner
323+ nRepo
324+ nCommit
325+ NewStatus
326+ { newStatusState = StatusError,
327+ newStatusTargetUrl = Just . URL $ h <> "/submission/" <> owner <> "/" <> repo <> "/" <> sha,
328+ newStatusDescription = Just "Build failed ☠️",
329+ newStatusContext = Just c
330+ }
331+ scheduleTask status
332+ deadlineHasPassedStatus nOwner@(N owner) nRepo@(N repo) nCommit@(N sha) = do
333+ h <- asks baseUrl
334+ c <- asks context
335+ let status =
336+ StatusUpdate
337+ nOwner
338+ nRepo
339+ nCommit
340+ NewStatus
341+ { newStatusState = StatusError,
342+ newStatusTargetUrl = Just . URL $ h <> "/submission/" <> owner <> "/" <> repo <> "/" <> sha,
343+ newStatusDescription = Just "The deadline has passed ⏰",
344+ newStatusContext = Just c
345+ }
346+ scheduleTask status
347+
246348instance (MonadReader (ServerData n) m) => HasDockerInfo m where
247349 getDockerUrl = asks dockerUrl
248350
@@ -252,7 +354,8 @@ data TaskConfig
252354 = TaskConfig
253355 { prebuild :: String,
254356 dockerfile :: String,
255− timeoutMinutes :: Int
357+ timeoutMinutes :: Int,
358+ deadline :: UTCTime
256359 }
257360 deriving stock (Show, Eq, Generic)
258361 deriving anyclass (FromJSON, ToJSON)