@@ -0,0 +1,256 @@
1+{-# LANGUAGE TypeApplications #-}
2+{-# OPTIONS_GHC -Wno-orphans #-}
10+import Control.AppRequestable
11+import Control.Concurrent
12+import Control.GithubCloner
13+import Control.Monad.Except
14+import Control.Monad.IO.Unlift
15+import Control.Monad.Reader
16+import Control.Task.Scheduler
18+import Data.ByteString
21+import qualified Data.Map as M
22+import Data.Submission
23+import Data.Submission.Query
24+import Data.Tasks.Build
25+import Data.Tasks.StatusUpdate
26+import qualified Data.Text as T
30+import GitHub.App.Auth
31+import GitHub.App.Request
32+import GitHub.Data.Name
33+import GitHub.Data.Webhooks.Events
34+import GitHub.Data.Webhooks.Payload
35+import Network.Wai.Handler.Warp
37+import Servant.GitHub.Webhook
38+import Servant.HTML.Blaze
42+import Squeal.PostgreSQL
44+import Text.Blaze.Html
45+import UnliftIO.Exception (try)
49+ :> ( GitHubEvent '[ 'WebhookInstallationEvent]
50+ :> GitHubSignedReqBody '[JSON] InstallationEvent
52+ :<|> GitHubEvent '[ 'WebhookPushEvent]
53+ :> GitHubSignedReqBody '[JSON] PushEvent
57+ :> Capture "user name" String
58+ :> Capture "repo name" String
59+ :> Capture "sha" String
60+ :> Get '[HTML] Markup
62+ :> Capture "user name" String
63+ :> Capture "repo name" String
64+ :> Capture "sha" String
66+ :> Post '[HTML] Markup
68+webhookInstallation :: MonadIO m => RepoWebhookEvent -> ((), InstallationEvent) -> m ()
69+webhookInstallation _ ((), ev) =
74+pendingStatus :: NewStatus
75+pendingStatus = NewStatus StatusPending Nothing Nothing Nothing
78+ (StaticPQ m, WithLog env Message m, MonadUnliftIO m, HasTasks m) =>
82+webhookPushEvent _ ((), ev) = do
83+ let repo = evPushRepository ev
84+ user = T.unpack . whUserLogin . evPushSender $ ev
85+ fullRepoName = whRepoFullName repo
86+ [owner, repoName] = T.splitOn "/" fullRepoName
87+ Just sha = evPushHeadSha ev
91+ M.takeWhileAntitone (repoName >) tasks
93+ Just (name, TaskConfig {..}) | T.take (T.length name) repoName == name -> do
94+ scheduleTask $ StatusUpdate (N owner) (N repoName) (N sha) pendingStatus
98+ (T.unpack fullRepoName)
101+ (Jsonb BuildScheduled)
102+ scheduleTask $ Build prebuild build (N owner) (N repoName) (N sha)
109+ (StaticPQ m, WithLog env Message m, MonadUnliftIO m, HasTasks m, MonadHasBaseUrl m) =>
114+retestSubmission owner repoName sha = do
115+ let fullRepoName = owner <> "/" <> repoName
116+ repoName' = T.pack repoName
118+ owner' = T.pack owner
119+ getSubmission fullRepoName sha >>= \case
120+ Nothing -> return ()
125+ M.takeWhileAntitone (repoName' >) tasks
127+ Just (name, TaskConfig {..}) | T.take (T.length name) repoName' == name -> do
128+ scheduleTask $ StatusUpdate (N owner') (N repoName') (N sha') pendingStatus
129+ updateSubmissionStatus fullRepoName sha BuildScheduled
130+ scheduleTask $ Build prebuild build (N owner') (N repoName') (N sha')
132+ redirectToSubmission owner repoName sha
134+newtype ServerM (schema :: SchemasType) a
136+ { unApp :: ReaderT (ServerData (ServerM schema)) (PQ schema schema IO) a
143+ MonadReader (ServerData (ServerM schema)),
148+server :: ServerT API (ServerM Schema)
149+server = (webhookInstallation :<|> webhookPushEvent) :<|> getSubmissionR :<|> retestSubmission
153+ c@Config {..} <- getConfig
155+ auth <- mkInstallationAuth (AppAuth appId appPkPem) installationId
156+ ts <- newIORef mempty
157+ pool <- createConnectionPool databseUrl 1 0.5 10
159+ updateTasks ts tasksPath
160+ threadDelay $ 10 * 60 * (10 ^ (6 :: Int))
162+ gitPolyHubKey (return webhookSecret)
164+ serverData = ServerData
165+ { githubAppAuth = auth,
166+ githubUserName = GithubUserName githubUsername,
167+ githubAccessToken = GithubAccessToken personalAccessToken,
169+ logger = simpleMessageAction,
170+ baseUrl = baseSiteUrl
172+ repeatIfNotEmpty n f = f >>= \m -> do
173+ when (m == 0) $ liftIO $ threadDelay n
174+ repeatIfNotEmpty n f
176+ forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
177+ . usingConnectionPool pool
178+ . (`runReaderT` serverData)
180+ $ runTasks @'[Build] 1
182+ forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
183+ . usingConnectionPool pool
184+ . (`runReaderT` serverData)
186+ $ runTasks @'[StatusUpdate] 5
187+ run port $ serveWithContext (Proxy @API) context $
188+ hoistServerWithContext
190+ (Proxy @'[PolyGitHubKey])
191+ (Handler . ExceptT . try . usingConnectionPool pool . (`runReaderT` serverData) . unApp)
194+updateTasks :: IORef TasksConfig -> FilePath -> IO ()
195+updateTasks ref path =
196+ decodeFileEither path >>= \case
197+ Right a -> writeIORef ref a
202+ { githubAppAuth :: !InstallationAuth,
203+ githubUserName :: !GithubUserName,
204+ githubAccessToken :: !GithubAccessToken,
205+ tasks :: IORef TasksConfig,
206+ logger :: LogAction m Message,
210+newtype PolyGitHubKey = PolyGitHubKey (forall result. GitHubKey result)
212+gitPolyHubKey :: IO ByteString -> PolyGitHubKey
213+gitPolyHubKey k = PolyGitHubKey (Servant.GitHub.Webhook.gitHubKey k)
215+instance HasContextEntry '[PolyGitHubKey] (GitHubKey result) where
216+ getContextEntry (PolyGitHubKey x :. _) = x
218+instance MonadReader (ServerData n) m => GithubCloner m where
220+ getGithubAccessToken = asks githubAccessToken
222+ getGithubUserName = asks githubUserName
224+instance (MonadIO m, MonadReader (ServerData n) m) => AppRequestable m where
226+ auth <- asks githubAppAuth
227+ liftIO $ executeAppRequest auth req
229+instance MonadReader (ServerData n) m => MonadHasBaseUrl m where
230+ getBaseUrl = asks baseUrl
232+instance (MonadIO m, MonadReader (ServerData n) m) => HasTasks m where
233+ getTasks = asks tasks >>= liftIO . readIORef
235+type TasksConfig = Map T.Text TaskConfig
239+ { prebuild :: String,
242+ deriving stock (Show, Eq, Generic)
243+ deriving anyclass (FromJSON, ToJSON)
245+class HasTasks m where
246+ getTasks :: m TasksConfig
248+instance HasLog (ServerData m) Message m where
250+ getLogAction :: ServerData m -> LogAction m Message
251+ getLogAction = logger
252+ {-# INLINE getLogAction #-}
254+ setLogAction :: LogAction m Message -> ServerData m -> ServerData m
255+ setLogAction newLogAction env = env {logger = newLogAction}
256+ {-# INLINE setLogAction #-}