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