@@ -1,381 +0,0 @@
1−{-# LANGUAGE TypeApplications #-}
2−{-# OPTIONS_GHC -Wno-orphans #-}
10−import Control.AppRequestable
11−import Control.Concurrent
12−import Control.GithubCloner
13−import Control.HasGithubStatus
14−import Control.Monad.Except
15−import Control.Monad.IO.Unlift
16−import Control.Monad.Reader
17−import Control.Task.Scheduler
18−import Crypto.PubKey.RSA.Read
20−import 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
27−import qualified Data.Map as M
28−import Data.Submission
29−import Data.Submission.Query
30−import Data.Tasks.Build
31−import Data.Tasks.StatusUpdate
32−import qualified Data.Text as T
33−import Data.Time.Clock
36−import GitHub hiding (Accept)
37−import GitHub.App.Auth
38−import GitHub.App.Request
39−import GitHub.Auth (Auth (OAuth))
40−import GitHub.Data.Name
41−import GitHub.Data.Webhooks.Events
42−import GitHub.Data.Webhooks.Payload hiding (URL (..))
43−import Network.HTTP.Client.TLS (newTlsManager)
44−import Network.HTTP.Media.MediaType
45−import Network.Wai.Handler.Warp
47−import Servant.GitHub.Webhook
48−import Servant.HTML.Blaze
52−import Squeal.PostgreSQL
53−import Text.Blaze.Html
54−import UnliftIO.Exception (catch, displayException, handleAny, throwIO, try)
58− :> ( GitHubEvent '[ 'WebhookInstallationEvent]
59− :> GitHubSignedReqBody '[JSON] InstallationEvent
61− :<|> GitHubEvent '[ 'WebhookPushEvent]
62− :> GitHubSignedReqBody '[JSON] PushEvent
66− :> Capture "user name" String
67− :> Capture "repo name" String
68− :> Capture "sha" String
69− :> Get '[HTML] Markup
71− :> Capture "user name" String
72− :> Capture "repo name" String
73− :> Capture "sha" String
75− :> Post '[HTML] Markup
76− :<|> "results" :> Capture "task name" String :> Get '[CSV, HTML] [Score]
78−webhookInstallation :: WithLog env Message m => RepoWebhookEvent -> ((), InstallationEvent) -> m ()
79−webhookInstallation _ ((), ev) =
80− logInfo . T.pack . show $ ev
84− MonadReader (ServerData m) m,
87− MonadError ServerError m,
88− WithLog env Message m
93−webhookPushEvent _ ((), ev) = do
94− let repo = evPushRepository ev
95− user = T.unpack . whUserLogin . evPushSender $ ev
96− fullRepoName = whRepoFullName repo
97− (owner, repoName) <- case T.splitOn "/" fullRepoName of
98− [a, b] -> return (a, b)
99− _ -> throwError err422
100− sha <- maybe (throwError err422) return $ evPushHeadSha ev
104− M.takeWhileAntitone (repoName >) tasks
106− Just (name, TaskConfig {..}) | T.take (T.length name) repoName == name -> do
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)
123− (T.unpack fullRepoName)
126− (Jsonb BuildScheduled)
127− scheduleTask $ Build prebuild dockerfile (N owner) (N repoName) (N sha) timeoutMinutes
128− logInfo . T.pack . show $ ev
132− (StaticPQ m, MonadReader (ServerData m) m, MonadUnliftIO m, HasTasks m, MonadHasBaseUrl m) =>
137−retestSubmission owner repoName sha = do
138− let fullRepoName = owner <> "/" <> repoName
139− repoName' = T.pack repoName
141− owner' = T.pack owner
142− getSubmission fullRepoName sha >>= \case
143− Nothing -> return ()
148− M.takeWhileAntitone (repoName' >) tasks
150− Just (name, TaskConfig {..}) | T.take (T.length name) repoName' == name -> do
151− schedulePendingStatus (N owner') (N repoName') (N sha')
152− updateSubmissionStatus fullRepoName sha BuildScheduled
153− scheduleTask $ Build prebuild dockerfile (N owner') (N repoName') (N sha') timeoutMinutes
155− redirectToSubmission owner repoName sha
157−newtype ServerM (schema :: SchemasType) a
159− { unApp :: ReaderT (ServerData (ServerM schema)) (PQ schema schema IO) a
166− MonadReader (ServerData (ServerM schema)),
171−instance MonadError ServerError (ServerM schema) where
172− throwError = throwIO
175−server :: ServerT API (ServerM Schema)
176−server = (webhookInstallation :<|> webhookPushEvent) :<|> getSubmissionR :<|> retestSubmission :<|> getResults
178−runServer :: FilePath -> IO ()
179−runServer config = do
180− c@Config {..} <- getConfig config
182− pem <- BS.readFile appPkPemPath
183− appPkPem <- case readRsaPem pem of
185− Left e -> error (show e)
186− auth <- mkInstallationAuth (AppAuth appId appPkPem) installationId
187− ts <- newIORef mempty
188− pool <- createConnectionPool databseUrl 1 0.5 10
190− updateTasks ts tasksPath
191− threadDelay $ 5 * 60 * (10 ^ (6 :: Int))
193− gitPolyHubKey (return webhookSecret)
197− { githubAppAuth = auth,
199− logger = cfilter ((logSeverity <=) . msgSeverity) richMessageAction,
200− baseUrl = baseSiteUrl,
201− dockerUrl = T.pack cfgDockerUrl,
202− context = githubContext
204− repeatIfNotEmpty n f = f >>= \m -> do
205− when (m == 0) $ liftIO $ threadDelay n
206− repeatIfNotEmpty n f
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))
216− . usingConnectionPool pool
217− . (`runReaderT` serverData)
220− $ runTasks @'[Build] 1
222− forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
223− . usingConnectionPool pool
224− . (`runReaderT` serverData)
227− $ runTasks @'[StatusUpdate] 5
228− run port $ serveWithContext (Proxy @API) context $
229− hoistServerWithContext
231− (Proxy @'[PolyGitHubKey])
232− (Handler . ExceptT . try . usingConnectionPool pool . (`runReaderT` serverData) . unApp)
235−updateTasks :: IORef TasksConfig -> FilePath -> IO ()
236−updateTasks ref path =
237− decodeFileEither path >>= \case
238− Right a -> writeIORef ref a
243− { githubAppAuth :: !InstallationAuth,
244− tasks :: IORef TasksConfig,
245− logger :: LogAction m Message,
247− dockerUrl :: T.Text,
251−newtype PolyGitHubKey = PolyGitHubKey (forall result. GitHubKey result)
253−gitPolyHubKey :: IO ByteString -> PolyGitHubKey
254−gitPolyHubKey k = PolyGitHubKey (Servant.GitHub.Webhook.gitHubKey k)
256−instance HasContextEntry '[PolyGitHubKey] (GitHubKey result) where
257− getContextEntry (PolyGitHubKey x :. _) = x
259−instance (MonadReader (ServerData n) m, MonadIO m, MonadError ServerError m) => GithubCloner m where
260− getGithubInstallationToken =
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."
272−instance (MonadIO m, MonadReader (ServerData n) m) => AppRequestable m where
274− auth <- asks githubAppAuth
275− liftIO $ executeAppRequest auth req
277−instance MonadReader (ServerData n) m => MonadHasBaseUrl m where
278− getBaseUrl = asks baseUrl
280−instance (MonadIO m, MonadReader (ServerData n) m) => HasTasks m where
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
348−instance (MonadReader (ServerData n) m) => HasDockerInfo m where
349− getDockerUrl = asks dockerUrl
351−type TasksConfig = Map T.Text TaskConfig
355− { prebuild :: String,
356− dockerfile :: String,
357− timeoutMinutes :: Int,
360− deriving stock (Show, Eq, Generic)
361− deriving anyclass (FromJSON, ToJSON)
363−class HasTasks m where
364− getTasks :: m TasksConfig
366−instance HasLog (ServerData m) Message m where
367− getLogAction :: ServerData m -> LogAction m Message
368− getLogAction = logger
369− {-# INLINE getLogAction #-}
371− setLogAction :: LogAction m Message -> ServerData m -> ServerData m
372− setLogAction newLogAction env = env {logger = newLogAction}
373− {-# 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