@@ -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 (catch, throwIO, try)
54+import UnliftIO.Exception (catch, displayException, handleAny, throwIO, try)
50 "github-webhook"
57 "github-webhook"
@@ -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,
81− (StaticPQ m, WithLog env Message m, MonadUnliftIO m, HasTasks m, MonadError ServerError m) =>
87+ MonadError ServerError m,
88+ WithLog env Message m
82 RepoWebhookEvent ->
90 RepoWebhookEvent ->
83 ((), PushEvent) ->
91 ((), PushEvent) ->
@@ -96,22 +104,32 @@ webhookPushEvent _ ((), ev) = do
96 M.takeWhileAntitone (repoName >) tasks
104 M.takeWhileAntitone (repoName >) tasks
97 case task of
105 case task of
98 Just (name, TaskConfig {..}) | T.take (T.length name) repoName == name -> do
106 Just (name, TaskConfig {..}) | T.take (T.length name) repoName == name -> do
99− 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)
100− createSubmission $
120+ createSubmission $
101− Submission
121+ Submission
103− (T.unpack fullRepoName)
123+ (T.unpack fullRepoName)
104− (T.unpack name)
124+ (T.unpack name)
105− (T.unpack sha)
125+ (T.unpack sha)
106− (Jsonb BuildScheduled)
126+ (Jsonb BuildScheduled)
107− scheduleTask $ Build prebuild dockerfile (N owner) (N repoName) (N sha) timeoutMinutes
127+ scheduleTask $ Build prebuild dockerfile (N owner) (N repoName) (N sha) timeoutMinutes
108− liftIO $ do
128+ logInfo . T.pack . show $ ev
111 _ -> return ()
129 _ -> return ()
113retestSubmission ::
131retestSubmission ::
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) =>
115 String ->
133 String ->
116 String ->
134 String ->
117 String ->
135 String ->
@@ -130,7 +148,7 @@ retestSubmission owner repoName sha = do
130 M.takeWhileAntitone (repoName' >) tasks
148 M.takeWhileAntitone (repoName' >) tasks
131 case task of
149 case task of
132 Just (name, TaskConfig {..}) | T.take (T.length name) repoName' == name -> do
150 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')
134 updateSubmissionStatus fullRepoName sha BuildScheduled
152 updateSubmissionStatus fullRepoName sha BuildScheduled
135 scheduleTask $ Build prebuild dockerfile (N owner') (N repoName') (N sha') timeoutMinutes
153 scheduleTask $ Build prebuild dockerfile (N owner') (N repoName') (N sha') timeoutMinutes
136 _ -> return ()
154 _ -> return ()
@@ -157,10 +175,14 @@ instance MonadError ServerError (ServerM schema) where
157server :: ServerT API (ServerM Schema)
175server :: ServerT API (ServerM Schema)
158server = (webhookInstallation :<|> webhookPushEvent) :<|> getSubmissionR :<|> retestSubmission :<|> getResults
176server = (webhookInstallation :<|> webhookPushEvent) :<|> getSubmissionR :<|> retestSubmission :<|> getResults
160−runServer :: IO ()
178+runServer :: FilePath -> IO ()
161−runServer = do
179+runServer config = do
162− 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)
164 auth <- mkInstallationAuth (AppAuth appId appPkPem) installationId
186 auth <- mkInstallationAuth (AppAuth appId appPkPem) installationId
165 ts <- newIORef mempty
187 ts <- newIORef mempty
166 pool <- createConnectionPool databseUrl 1 0.5 10
188 pool <- createConnectionPool databseUrl 1 0.5 10
@@ -173,27 +195,35 @@ runServer = do
173 serverData =
195 serverData =
174 ServerData
196 ServerData
175 { githubAppAuth = auth,
197 { githubAppAuth = auth,
176− githubUserName = GithubUserName githubUsername,
177− githubAccessToken = GithubAccessToken personalAccessToken,
178 tasks = ts,
198 tasks = ts,
179 logger = cfilter ((logSeverity <=) . msgSeverity) richMessageAction,
199 logger = cfilter ((logSeverity <=) . msgSeverity) richMessageAction,
180 baseUrl = baseSiteUrl,
200 baseUrl = baseSiteUrl,
181− dockerUrl = T.pack cfgDockerUrl
201+ dockerUrl = T.pack cfgDockerUrl,
202+ context = githubContext
183 repeatIfNotEmpty n f = f >>= \m -> do
204 repeatIfNotEmpty n f = f >>= \m -> do
184 when (m == 0) $ liftIO $ threadDelay n
205 when (m == 0) $ liftIO $ threadDelay n
185 repeatIfNotEmpty n f
206 repeatIfNotEmpty n f
186− _ <-
207+ printingErrors =
208+ handleAny (logError . T.pack . displayException >=> const (return 0))
209+ usingConnectionPool pool
210+ . (`runReaderT` serverData)
212+ . handleAny (error . displayException)
214+ replicateM_ builderCount
187− forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
215+ $ forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
188 . usingConnectionPool pool
216 . usingConnectionPool pool
189 . (`runReaderT` serverData)
217 . (`runReaderT` serverData)
191− $ runTasks @'[Build] 1
220+ $ runTasks @'[Build] 1
193 forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
222 forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
194 . usingConnectionPool pool
223 . usingConnectionPool pool
195 . (`runReaderT` serverData)
224 . (`runReaderT` serverData)
197 $ runTasks @'[StatusUpdate] 5
227 $ runTasks @'[StatusUpdate] 5
198 run port $ serveWithContext (Proxy @API) context $
228 run port $ serveWithContext (Proxy @API) context $
199 hoistServerWithContext
229 hoistServerWithContext
@@ -211,12 +241,11 @@ updateTasks ref path =
211data ServerData m
241data ServerData m
212 = ServerData
242 = ServerData
213 { githubAppAuth :: !InstallationAuth,
243 { githubAppAuth :: !InstallationAuth,
214− githubUserName :: !GithubUserName,
215− githubAccessToken :: !GithubAccessToken,
216 tasks :: IORef TasksConfig,
244 tasks :: IORef TasksConfig,
217 logger :: LogAction m Message,
245 logger :: LogAction m Message,
218− baseUrl :: String,
246+ baseUrl :: T.Text,
219− dockerUrl :: T.Text
247+ dockerUrl :: T.Text,
222newtype PolyGitHubKey = PolyGitHubKey (forall result. GitHubKey result)
251newtype PolyGitHubKey = PolyGitHubKey (forall result. GitHubKey result)
@@ -227,10 +256,18 @@ gitPolyHubKey k = PolyGitHubKey (Servant.GitHub.Webhook.gitHubKey k)
227instance HasContextEntry '[PolyGitHubKey] (GitHubKey result) where
256instance HasContextEntry '[PolyGitHubKey] (GitHubKey result) where
228 getContextEntry (PolyGitHubKey x :. _) = x
257 getContextEntry (PolyGitHubKey x :. _) = x
230−instance MonadReader (ServerData n) m => GithubCloner m where
259+instance (MonadReader (ServerData n) m, MonadIO m, MonadError ServerError m) => GithubCloner m where
231− getGithubAccessToken = asks githubAccessToken
260+ getGithubInstallationToken =
233− getGithubUserName = asks githubUserName
262+ (\e -> throwError err500 {errBody = BSCL.pack . displayException $ e})
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."
235instance (MonadIO m, MonadReader (ServerData n) m) => AppRequestable m where
272instance (MonadIO m, MonadReader (ServerData n) m) => AppRequestable m where
236 appRequest req = do
273 appRequest req = do
@@ -243,6 +280,71 @@ instance MonadReader (ServerData n) m => MonadHasBaseUrl m where
243instance (MonadIO m, MonadReader (ServerData n) m) => HasTasks m where
280instance (MonadIO m, MonadReader (ServerData n) m) => HasTasks m where
244 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
246instance (MonadReader (ServerData n) m) => HasDockerInfo m where
348instance (MonadReader (ServerData n) m) => HasDockerInfo m where
247 getDockerUrl = asks dockerUrl
349 getDockerUrl = asks dockerUrl
@@ -252,7 +354,8 @@ data TaskConfig
252 = TaskConfig
354 = TaskConfig
253 { prebuild :: String,
355 { prebuild :: String,
254 dockerfile :: String,
356 dockerfile :: String,
255− timeoutMinutes :: Int
357+ timeoutMinutes :: Int,
257 deriving stock (Show, Eq, Generic)
360 deriving stock (Show, Eq, Generic)
258 deriving anyclass (FromJSON, ToJSON)
361 deriving anyclass (FromJSON, ToJSON)