mirror of
https://github.com/ilyakooo0/sc-build.git
synced 2024-11-23 20:39:24 +03:00
Initial working commit
This commit is contained in:
parent
9a05dcd06e
commit
33713bd307
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.stack-work
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 iko
|
||||
Copyright (c) 2020 Ilya Kostyuchenko
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
6
app/Main.hs
Normal file
6
app/Main.hs
Normal file
@ -0,0 +1,6 @@
|
||||
module Main (main) where
|
||||
|
||||
import Server
|
||||
|
||||
main :: IO ()
|
||||
main = runServer
|
4
hie.yaml
Normal file
4
hie.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
cradle:
|
||||
stack:
|
||||
- path: "."
|
||||
component: "sc-build:lib"
|
14
migrations/Main.hs
Normal file
14
migrations/Main.hs
Normal file
@ -0,0 +1,14 @@
|
||||
module Main
|
||||
( main,
|
||||
)
|
||||
where
|
||||
|
||||
import Data.ByteString.Char8 (pack)
|
||||
import Server.Schema
|
||||
import Squeal.PostgreSQL
|
||||
import System.Environment
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
Just dbString <- fmap pack <$> lookupEnv "DATABASE_URL"
|
||||
defaultMain dbString migration
|
102
package.yaml
Normal file
102
package.yaml
Normal file
@ -0,0 +1,102 @@
|
||||
name: sc-build
|
||||
version: 0.1.0.0
|
||||
#synopsis:
|
||||
#description:
|
||||
homepage: https://github.com/ilyakooo0/sc-build#readme
|
||||
license: BSD3
|
||||
author: Ilya Kostyuchenko
|
||||
maintainer: ilyakooo0@gmail.com
|
||||
copyright: Ilya Kostyuchenko
|
||||
category: Web
|
||||
extra-source-files:
|
||||
- README.md
|
||||
|
||||
dependencies:
|
||||
- base >= 4.7 && < 5
|
||||
|
||||
library:
|
||||
source-dirs: src
|
||||
dependencies:
|
||||
- servant
|
||||
- servant-server
|
||||
- process
|
||||
- yaml
|
||||
- aeson
|
||||
- github
|
||||
- github-app
|
||||
- github-webhooks
|
||||
- warp
|
||||
- vector
|
||||
- bytestring
|
||||
- servant-github-webhook
|
||||
- cryptonite
|
||||
- mtl
|
||||
- text
|
||||
- containers
|
||||
- random
|
||||
- time
|
||||
- typed-process
|
||||
- directory
|
||||
- squeal-postgresql
|
||||
- generics-sop
|
||||
- co-log
|
||||
- unliftio-core
|
||||
- unliftio
|
||||
- servant-blaze
|
||||
- blaze-markup
|
||||
- blaze-html
|
||||
- clay
|
||||
|
||||
executables:
|
||||
sc-build-exe:
|
||||
main: Main.hs
|
||||
source-dirs: app
|
||||
ghc-options:
|
||||
- -threaded
|
||||
- -rtsopts
|
||||
- -with-rtsopts=-N
|
||||
dependencies:
|
||||
- sc-build
|
||||
sc-build-migrations:
|
||||
main: Main.hs
|
||||
source-dirs: migrations
|
||||
ghc-options:
|
||||
- -threaded
|
||||
- -rtsopts
|
||||
- -with-rtsopts=-N
|
||||
dependencies:
|
||||
- sc-build
|
||||
- bytestring
|
||||
- squeal-postgresql
|
||||
|
||||
default-extensions:
|
||||
- AllowAmbiguousTypes
|
||||
- ConstraintKinds
|
||||
- DataKinds
|
||||
- DeriveAnyClass
|
||||
- DeriveFunctor
|
||||
- DeriveGeneric
|
||||
- DerivingStrategies
|
||||
- DuplicateRecordFields
|
||||
- FlexibleContexts
|
||||
- FlexibleInstances
|
||||
- FunctionalDependencies
|
||||
- GADTs
|
||||
- GeneralizedNewtypeDeriving
|
||||
- InstanceSigs
|
||||
- LambdaCase
|
||||
- MultiParamTypeClasses
|
||||
- OverloadedLabels
|
||||
- OverloadedStrings
|
||||
- PatternSynonyms
|
||||
- PolyKinds
|
||||
- QuantifiedConstraints
|
||||
- RankNTypes
|
||||
- RecordWildCards
|
||||
- ScopedTypeVariables
|
||||
- StandaloneDeriving
|
||||
- TupleSections
|
||||
- TypeApplications
|
||||
- TypeFamilies
|
||||
- TypeOperators
|
||||
- UndecidableInstances
|
103
sc-build.cabal
Normal file
103
sc-build.cabal
Normal file
@ -0,0 +1,103 @@
|
||||
cabal-version: 1.12
|
||||
|
||||
-- This file has been generated from package.yaml by hpack version 0.31.2.
|
||||
--
|
||||
-- see: https://github.com/sol/hpack
|
||||
--
|
||||
-- hash: bb953e6231fcc3939bfe085afcd3e792cbc40462461bbe4fb82ae69f1796be4c
|
||||
|
||||
name: sc-build
|
||||
version: 0.1.0.0
|
||||
category: Web
|
||||
homepage: https://github.com/ilyakooo0/sc-build#readme
|
||||
author: Ilya Kostyuchenko
|
||||
maintainer: ilyakooo0@gmail.com
|
||||
copyright: Ilya Kostyuchenko
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
build-type: Simple
|
||||
extra-source-files:
|
||||
README.md
|
||||
|
||||
library
|
||||
exposed-modules:
|
||||
Control.AppRequestable
|
||||
Control.GithubCloner
|
||||
Control.Squeal
|
||||
Control.Task
|
||||
Control.Task.Scheduler
|
||||
Control.Task.Scheduler.Query
|
||||
Data.Submission
|
||||
Data.Submission.Query
|
||||
Data.Tasks.Build
|
||||
Data.Tasks.StatusUpdate
|
||||
Server
|
||||
Server.Config
|
||||
Server.Html
|
||||
Server.Schema
|
||||
Server.Schema.V1
|
||||
other-modules:
|
||||
Paths_sc_build
|
||||
hs-source-dirs:
|
||||
src
|
||||
default-extensions: AllowAmbiguousTypes ConstraintKinds DataKinds DeriveAnyClass DeriveFunctor DeriveGeneric DerivingStrategies DuplicateRecordFields FlexibleContexts FlexibleInstances FunctionalDependencies GADTs GeneralizedNewtypeDeriving InstanceSigs LambdaCase MultiParamTypeClasses OverloadedLabels OverloadedStrings PatternSynonyms PolyKinds QuantifiedConstraints RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TupleSections TypeApplications TypeFamilies TypeOperators UndecidableInstances
|
||||
build-depends:
|
||||
aeson
|
||||
, base >=4.7 && <5
|
||||
, blaze-html
|
||||
, blaze-markup
|
||||
, bytestring
|
||||
, clay
|
||||
, co-log
|
||||
, containers
|
||||
, cryptonite
|
||||
, directory
|
||||
, generics-sop
|
||||
, github
|
||||
, github-app
|
||||
, github-webhooks
|
||||
, mtl
|
||||
, process
|
||||
, random
|
||||
, servant
|
||||
, servant-blaze
|
||||
, servant-github-webhook
|
||||
, servant-server
|
||||
, squeal-postgresql
|
||||
, text
|
||||
, time
|
||||
, typed-process
|
||||
, unliftio
|
||||
, unliftio-core
|
||||
, vector
|
||||
, warp
|
||||
, yaml
|
||||
default-language: Haskell2010
|
||||
|
||||
executable sc-build-exe
|
||||
main-is: Main.hs
|
||||
other-modules:
|
||||
Paths_sc_build
|
||||
hs-source-dirs:
|
||||
app
|
||||
default-extensions: AllowAmbiguousTypes ConstraintKinds DataKinds DeriveAnyClass DeriveFunctor DeriveGeneric DerivingStrategies DuplicateRecordFields FlexibleContexts FlexibleInstances FunctionalDependencies GADTs GeneralizedNewtypeDeriving InstanceSigs LambdaCase MultiParamTypeClasses OverloadedLabels OverloadedStrings PatternSynonyms PolyKinds QuantifiedConstraints RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TupleSections TypeApplications TypeFamilies TypeOperators UndecidableInstances
|
||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
||||
build-depends:
|
||||
base >=4.7 && <5
|
||||
, sc-build
|
||||
default-language: Haskell2010
|
||||
|
||||
executable sc-build-migrations
|
||||
main-is: Main.hs
|
||||
other-modules:
|
||||
Paths_sc_build
|
||||
hs-source-dirs:
|
||||
migrations
|
||||
default-extensions: AllowAmbiguousTypes ConstraintKinds DataKinds DeriveAnyClass DeriveFunctor DeriveGeneric DerivingStrategies DuplicateRecordFields FlexibleContexts FlexibleInstances FunctionalDependencies GADTs GeneralizedNewtypeDeriving InstanceSigs LambdaCase MultiParamTypeClasses OverloadedLabels OverloadedStrings PatternSynonyms PolyKinds QuantifiedConstraints RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TupleSections TypeApplications TypeFamilies TypeOperators UndecidableInstances
|
||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
||||
build-depends:
|
||||
base >=4.7 && <5
|
||||
, bytestring
|
||||
, sc-build
|
||||
, squeal-postgresql
|
||||
default-language: Haskell2010
|
10
src/Control/AppRequestable.hs
Normal file
10
src/Control/AppRequestable.hs
Normal file
@ -0,0 +1,10 @@
|
||||
module Control.AppRequestable
|
||||
( AppRequestable (..),
|
||||
)
|
||||
where
|
||||
|
||||
import Data.Aeson
|
||||
import GitHub
|
||||
|
||||
class AppRequestable m where
|
||||
appRequest :: FromJSON a => GenRequest 'MtJSON rw a -> m (Either Error a)
|
16
src/Control/GithubCloner.hs
Normal file
16
src/Control/GithubCloner.hs
Normal file
@ -0,0 +1,16 @@
|
||||
module Control.GithubCloner
|
||||
( GithubAccessToken (..),
|
||||
GithubUserName (..),
|
||||
GithubCloner (..),
|
||||
)
|
||||
where
|
||||
|
||||
newtype GithubAccessToken = GithubAccessToken String
|
||||
|
||||
newtype GithubUserName = GithubUserName String
|
||||
|
||||
class GithubCloner m where
|
||||
|
||||
getGithubAccessToken :: m GithubAccessToken
|
||||
|
||||
getGithubUserName :: m GithubUserName
|
81
src/Control/Squeal.hs
Normal file
81
src/Control/Squeal.hs
Normal file
@ -0,0 +1,81 @@
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Control.Squeal
|
||||
( db,
|
||||
db',
|
||||
dbRead,
|
||||
dbRead',
|
||||
dbWrite,
|
||||
dbWrite',
|
||||
)
|
||||
where
|
||||
|
||||
import Colog
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift
|
||||
import qualified Data.Text as T
|
||||
import Squeal.PostgreSQL
|
||||
|
||||
db ::
|
||||
forall schemas m env x.
|
||||
(MonadPQ schemas m, WithLog env Message m, MonadUnliftIO m) =>
|
||||
Maybe TransactionMode ->
|
||||
(SquealException -> m x) ->
|
||||
m x ->
|
||||
m x
|
||||
db tr err pqx = (trySqueal . maybe id transactionallyRetry tr) pqx >>= \case
|
||||
Left e -> do
|
||||
logWarning . T.pack $ "SQL ERROR: " <> show e
|
||||
err e
|
||||
Right x -> return x
|
||||
|
||||
dbRead ::
|
||||
(MonadPQ schemas m, WithLog env Message m, MonadUnliftIO m) =>
|
||||
(SquealException -> m x) ->
|
||||
m x ->
|
||||
m x
|
||||
dbRead = db Nothing
|
||||
|
||||
dbRead' ::
|
||||
( MonadPQ schemas m,
|
||||
MonadError err m,
|
||||
WithLog env Message m,
|
||||
MonadUnliftIO m
|
||||
) =>
|
||||
err ->
|
||||
m x ->
|
||||
m x
|
||||
dbRead' = db' Nothing
|
||||
|
||||
dbWrite ::
|
||||
(MonadPQ schemas m, WithLog env Message m, MonadUnliftIO m) =>
|
||||
(SquealException -> m x) ->
|
||||
m x ->
|
||||
m x
|
||||
dbWrite = db $ Just defaultWriteTransactionMode
|
||||
|
||||
dbWrite' ::
|
||||
( MonadPQ schemas m,
|
||||
MonadError err m,
|
||||
WithLog env Message m,
|
||||
MonadUnliftIO m
|
||||
) =>
|
||||
err ->
|
||||
m x ->
|
||||
m x
|
||||
dbWrite' = db' $ Just defaultWriteTransactionMode
|
||||
|
||||
defaultWriteTransactionMode :: TransactionMode
|
||||
defaultWriteTransactionMode = TransactionMode RepeatableRead ReadWrite NotDeferrable
|
||||
|
||||
db' ::
|
||||
( MonadPQ schemas m,
|
||||
MonadError err m,
|
||||
WithLog env Message m,
|
||||
MonadUnliftIO m
|
||||
) =>
|
||||
Maybe TransactionMode ->
|
||||
err ->
|
||||
m x ->
|
||||
m x
|
||||
db' tr err = db tr (const $ throwError err)
|
26
src/Control/Task.hs
Normal file
26
src/Control/Task.hs
Normal file
@ -0,0 +1,26 @@
|
||||
module Control.Task
|
||||
( Task (..),
|
||||
Result (..),
|
||||
)
|
||||
where
|
||||
|
||||
import Colog
|
||||
import Data.Aeson hiding (Result)
|
||||
import Data.Kind
|
||||
import GHC.Generics
|
||||
import GHC.TypeLits
|
||||
|
||||
data Result
|
||||
= Success
|
||||
| Failure
|
||||
deriving (Eq, Ord, Show, Generic)
|
||||
|
||||
class
|
||||
(ToJSON t, FromJSON t, KnownSymbol s, Show t) =>
|
||||
Task t s
|
||||
| s -> t,
|
||||
t -> s where
|
||||
|
||||
type TaskMonad t (m :: * -> *) :: Constraint
|
||||
|
||||
performTask :: (TaskMonad t m, WithLog env Message m) => t -> m Result
|
86
src/Control/Task/Scheduler.hs
Normal file
86
src/Control/Task/Scheduler.hs
Normal file
@ -0,0 +1,86 @@
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Control.Task.Scheduler
|
||||
( scheduleTask,
|
||||
runTasks,
|
||||
)
|
||||
where
|
||||
|
||||
import Colog
|
||||
import Control.Monad
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Task
|
||||
import Control.Task.Scheduler.Query
|
||||
import Data.Aeson hiding (Result (..))
|
||||
import qualified Data.Aeson as A
|
||||
import Data.Foldable
|
||||
import Data.Proxy
|
||||
import qualified Data.Text as T
|
||||
import Data.Word
|
||||
import GHC.TypeLits
|
||||
import Server.Schema
|
||||
import Squeal.PostgreSQL
|
||||
|
||||
scheduleTask ::
|
||||
(StaticPQ m, WithLog env Message m, Task t s, MonadUnliftIO m) =>
|
||||
t ->
|
||||
m ()
|
||||
scheduleTask = scheduleTaskQuery
|
||||
|
||||
runTasks ::
|
||||
forall tt m env.
|
||||
(StaticPQ m, WithLog env Message m, RunTasks tt m, MonadUnliftIO m) =>
|
||||
Word64 ->
|
||||
m Int
|
||||
runTasks n = do
|
||||
tasks <- pickTasksQuery (taskNames @tt @m) n
|
||||
for_ tasks $ runTask @tt @m
|
||||
return $ length tasks
|
||||
|
||||
rescheduleTaskTime ::
|
||||
forall t s m env.
|
||||
(Task t s, StaticPQ m, WithLog env Message m, MonadUnliftIO m) =>
|
||||
t ->
|
||||
m ()
|
||||
rescheduleTaskTime = rescheduleTaskQuery
|
||||
|
||||
class RunTasks (ts :: [*]) m where
|
||||
|
||||
runTask :: WithLog env Message m => PickedTask -> m ()
|
||||
|
||||
taskNames :: [String]
|
||||
|
||||
instance
|
||||
forall t ts tt m.
|
||||
(Task t ts, TaskMonad t m, StaticPQ m, RunTasks tt m, MonadUnliftIO m) =>
|
||||
RunTasks (t ': tt) m
|
||||
where
|
||||
|
||||
runTask (PickedTask s (Jsonb v) initialStartTime) | symbolVal @ts Proxy == s =
|
||||
case fromJSON @t v of
|
||||
A.Error e ->
|
||||
logError . T.pack $
|
||||
"Coulndn't decode payload for task " <> s
|
||||
<> " with payload "
|
||||
<> show v
|
||||
<> " with error "
|
||||
<> e
|
||||
A.Success t -> do
|
||||
logInfo . T.pack $ "running task " <> task <> " with " <> show t <> " created at " <> show initialStartTime
|
||||
r <- performTask @t @ts @m t
|
||||
when (r == Failure) $ logWarning . T.pack $ "Task " <> task <> " failed"
|
||||
case r of
|
||||
Success -> completeTaskQuery t
|
||||
Failure -> rescheduleTaskTime t
|
||||
where
|
||||
task = symbolVal @ts Proxy
|
||||
runTask t = runTask @tt @m t
|
||||
|
||||
taskNames = symbolVal @ts Proxy : taskNames @tt @m
|
||||
|
||||
instance RunTasks '[] m where
|
||||
|
||||
runTask (PickedTask s (Jsonb v) _) =
|
||||
logError . T.pack $ "Unknown task " <> s <> " with payload " <> show v
|
||||
|
||||
taskNames = []
|
121
src/Control/Task/Scheduler/Query.hs
Normal file
121
src/Control/Task/Scheduler/Query.hs
Normal file
@ -0,0 +1,121 @@
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Control.Task.Scheduler.Query
|
||||
( scheduleTaskQuery,
|
||||
completeTaskQuery,
|
||||
rescheduleTaskQuery,
|
||||
pickTasksQuery,
|
||||
PickedTask (..),
|
||||
)
|
||||
where
|
||||
|
||||
import Colog
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Squeal
|
||||
import Control.Task
|
||||
import Data.Aeson hiding (Result (..))
|
||||
import Data.Proxy
|
||||
import qualified Data.Text as T
|
||||
import Data.Time
|
||||
import Data.Word
|
||||
import GHC.Generics (Generic)
|
||||
import GHC.TypeLits
|
||||
import qualified Generics.SOP as SOP
|
||||
import Server.Schema
|
||||
import Squeal.PostgreSQL
|
||||
|
||||
scheduleTaskQuery ::
|
||||
forall m t s env.
|
||||
(StaticPQ m, Task t s, WithLog env Message m, MonadUnliftIO m) =>
|
||||
t ->
|
||||
m ()
|
||||
scheduleTaskQuery task = do
|
||||
logInfo . T.pack $ "schdeuling task: " <> show task
|
||||
let query =
|
||||
insertInto
|
||||
#tasks
|
||||
( Values_ $
|
||||
Set (param @1) `as` #task
|
||||
:* Set (param @2) `as` #payload
|
||||
:* Default `as` #creation_time
|
||||
)
|
||||
( OnConflict
|
||||
(OnConstraint #pk_task_payload)
|
||||
(DoUpdate (Default `as` #creation_time) [])
|
||||
)
|
||||
(Returning_ Nil)
|
||||
dbWrite (const $ return ()) $
|
||||
manipulateParams_
|
||||
query
|
||||
(symbolVal @s Proxy, Jsonb $ toJSON task)
|
||||
|
||||
completeTaskQuery ::
|
||||
forall m t s env.
|
||||
(StaticPQ m, Task t s, WithLog env Message m, MonadUnliftIO m) =>
|
||||
t ->
|
||||
m ()
|
||||
completeTaskQuery payload = do
|
||||
let task = symbolVal @s Proxy
|
||||
logInfo . T.pack $ "Completing task: " <> task <> " with payload " <> show payload
|
||||
let query :: Manipulation_ Schema (String, Jsonb Value) ()
|
||||
query =
|
||||
deleteFrom_
|
||||
#tasks
|
||||
(param @1 .== #task .&& param @2 .== #payload)
|
||||
dbWrite (const $ return ()) $
|
||||
manipulateParams_
|
||||
query
|
||||
(symbolVal @s Proxy, Jsonb $ toJSON payload)
|
||||
|
||||
rescheduleTaskQuery ::
|
||||
forall s t m env.
|
||||
(StaticPQ m, Task t s, WithLog env Message m, MonadUnliftIO m) =>
|
||||
t ->
|
||||
m ()
|
||||
rescheduleTaskQuery payload = do
|
||||
let task = symbolVal @s Proxy
|
||||
logInfo . T.pack $
|
||||
"reschdeuling task: " <> task
|
||||
<> " with payload "
|
||||
<> show payload
|
||||
let query :: Manipulation_ Schema (String, Jsonb Value) ()
|
||||
query =
|
||||
update_
|
||||
#tasks
|
||||
(Default `as` #creation_time)
|
||||
(param @1 .== #task .&& param @2 .== #payload)
|
||||
dbWrite (const $ return ()) $
|
||||
manipulateParams_
|
||||
query
|
||||
(symbolVal @s Proxy, Jsonb $ toJSON payload)
|
||||
|
||||
pickTasksQuery ::
|
||||
forall m env.
|
||||
(StaticPQ m, WithLog env Message m, MonadUnliftIO m) =>
|
||||
[String] ->
|
||||
Word64 ->
|
||||
m [PickedTask]
|
||||
pickTasksQuery tasks limitCount = do
|
||||
logDebug "reading tasks"
|
||||
let query :: Query_ Schema () PickedTask
|
||||
query =
|
||||
select_
|
||||
( #task `as` #task
|
||||
:* #payload `as` #payload
|
||||
:* #creation_time `as` #creationTime
|
||||
)
|
||||
( from (table #tasks)
|
||||
& where_ (#task `in_` (literal <$> tasks))
|
||||
& orderBy [#creation_time & Asc]
|
||||
& limit limitCount
|
||||
)
|
||||
dbRead (const $ return []) $
|
||||
runQuery query >>= getRows
|
||||
|
||||
data PickedTask
|
||||
= PickedTask
|
||||
{ task :: String,
|
||||
payload :: Jsonb Value,
|
||||
creationTime :: UTCTime
|
||||
}
|
||||
deriving (Eq, Show, Generic, SOP.Generic, SOP.HasDatatypeInfo)
|
36
src/Data/Submission.hs
Normal file
36
src/Data/Submission.hs
Normal file
@ -0,0 +1,36 @@
|
||||
module Data.Submission
|
||||
( Submission (..),
|
||||
SubmissionStatus (..),
|
||||
TestResult (..),
|
||||
)
|
||||
where
|
||||
|
||||
import Data.Aeson
|
||||
import Data.Map (Map)
|
||||
import Data.Text (Text)
|
||||
import GHC.Generics (Generic)
|
||||
import qualified Generics.SOP as SOP
|
||||
import Squeal.PostgreSQL
|
||||
|
||||
data Submission
|
||||
= Submission
|
||||
{ userName :: String,
|
||||
repoFullName :: String,
|
||||
problem :: String,
|
||||
sha :: String,
|
||||
status :: Jsonb SubmissionStatus
|
||||
}
|
||||
deriving (Eq, Show, Generic, SOP.Generic, SOP.HasDatatypeInfo)
|
||||
|
||||
data SubmissionStatus
|
||||
= SubmissionRun TestResult
|
||||
| SubmissionFailed String
|
||||
| BuildScheduled
|
||||
deriving (Generic, FromJSON, ToJSON, Eq, Show)
|
||||
|
||||
newtype TestResult
|
||||
= TestResult
|
||||
{ tests :: Map Text Bool
|
||||
}
|
||||
deriving stock (Eq, Show, Generic)
|
||||
deriving anyclass (FromJSON, ToJSON)
|
85
src/Data/Submission/Query.hs
Normal file
85
src/Data/Submission/Query.hs
Normal file
@ -0,0 +1,85 @@
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Data.Submission.Query
|
||||
( createSubmission,
|
||||
updateSubmissionStatus,
|
||||
getSubmission,
|
||||
)
|
||||
where
|
||||
|
||||
import Colog
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Squeal
|
||||
import Data.Submission
|
||||
import qualified Data.Text as T
|
||||
import Server.Schema
|
||||
import Squeal.PostgreSQL
|
||||
|
||||
createSubmission ::
|
||||
(StaticPQ m, WithLog env Message m, MonadUnliftIO m) =>
|
||||
Submission ->
|
||||
m ()
|
||||
createSubmission s@Submission {..} = do
|
||||
logInfo . T.pack $ "Creating submission: " <> show s
|
||||
let query =
|
||||
insertInto
|
||||
#submissions
|
||||
( Values_ $
|
||||
Set (param @1) `as` #user_name
|
||||
:* Set (param @2) `as` #repo_full_name
|
||||
:* Set (param @3) `as` #problem
|
||||
:* Set (param @4) `as` #sha
|
||||
:* Set (param @5) `as` #status
|
||||
)
|
||||
( OnConflict
|
||||
(OnConstraint #pk_submission_repo_full_name_sha)
|
||||
DoNothing
|
||||
)
|
||||
(Returning_ Nil)
|
||||
dbWrite (const $ return ()) $
|
||||
manipulateParams_
|
||||
query
|
||||
(userName, repoFullName, problem, sha, status)
|
||||
|
||||
updateSubmissionStatus ::
|
||||
(StaticPQ m, WithLog env Message m, MonadUnliftIO m) =>
|
||||
String ->
|
||||
String ->
|
||||
SubmissionStatus ->
|
||||
m ()
|
||||
updateSubmissionStatus repoFullName sha status = do
|
||||
let query :: Manipulation_ Schema (Jsonb SubmissionStatus, String, String) ()
|
||||
query =
|
||||
update_
|
||||
#submissions
|
||||
(Set (param @1) `as` #status)
|
||||
(#repo_full_name .== param @2 .&& #sha .== param @3)
|
||||
dbWrite (const $ return ()) $
|
||||
manipulateParams_
|
||||
query
|
||||
(Jsonb status, repoFullName, sha)
|
||||
|
||||
getSubmission ::
|
||||
(StaticPQ m, WithLog env Message m, MonadUnliftIO m) =>
|
||||
String ->
|
||||
String ->
|
||||
m (Maybe Submission)
|
||||
getSubmission repoFullName sha = do
|
||||
let query :: Query_ Schema (String, String) Submission
|
||||
query =
|
||||
select_
|
||||
( #user_name `as` #userName
|
||||
:* #repo_full_name `as` #repoFullName
|
||||
:* #problem `as` #problem
|
||||
:* #sha `as` #sha
|
||||
:* #status `as` #status
|
||||
)
|
||||
( from (table #submissions)
|
||||
& where_
|
||||
(#repo_full_name .== param @1 .&& #sha .== param @2)
|
||||
)
|
||||
dbRead (const $ return Nothing) $
|
||||
runQueryParams
|
||||
query
|
||||
(repoFullName, sha)
|
||||
>>= firstRow
|
133
src/Data/Tasks/Build.hs
Normal file
133
src/Data/Tasks/Build.hs
Normal file
@ -0,0 +1,133 @@
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -Wno-orphans #-}
|
||||
|
||||
module Data.Tasks.Build
|
||||
( Build (..),
|
||||
)
|
||||
where
|
||||
|
||||
import Colog
|
||||
import Control.GithubCloner
|
||||
import Control.Monad.IO.Class
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Task
|
||||
import Control.Task.Scheduler
|
||||
import Data.Aeson hiding (Success)
|
||||
import qualified Data.ByteString.Lazy.Char8 as BS
|
||||
import qualified Data.Map as M
|
||||
import Data.String
|
||||
import Data.Submission
|
||||
import Data.Submission.Query
|
||||
import Data.Tasks.StatusUpdate
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock.POSIX
|
||||
import GHC.Generics
|
||||
import GitHub
|
||||
import Server.Schema
|
||||
import System.Directory
|
||||
import System.Exit
|
||||
import System.Process.Typed
|
||||
import System.Random
|
||||
import UnliftIO.Exception
|
||||
|
||||
data Build
|
||||
= Build
|
||||
{ preProcessShell :: String,
|
||||
buildCommand :: String,
|
||||
owner :: Name Owner,
|
||||
repoName :: Name Repo,
|
||||
sha :: Name Commit
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON, ToJSON)
|
||||
|
||||
instance Task Build "build-repo" where
|
||||
|
||||
type TaskMonad Build m = (MonadUnliftIO m, GithubCloner m, StaticPQ m)
|
||||
|
||||
performTask Build {..} =
|
||||
bracket createDir (liftIO . removePathForcibly) $ \dir -> do
|
||||
let fullRepoName = T.unpack $ untagName owner <> "/" <> untagName repoName
|
||||
sha' = T.unpack $ untagName sha
|
||||
cloneRepo fullRepoName sha' dir
|
||||
(shellCode, _, shellErr) <- readProcess . setWorkingDir dir $ shell preProcessShell
|
||||
case shellCode of
|
||||
ExitFailure n -> do
|
||||
let err = BS.unpack shellErr
|
||||
logError . T.pack $
|
||||
"preprocessing command for repo " <> show fullRepoName <> " at sha "
|
||||
<> show sha
|
||||
<> " exited with code "
|
||||
<> show n
|
||||
<> " and error "
|
||||
<> err
|
||||
scheduleTask erroredTask
|
||||
updateSubmissionStatus fullRepoName sha' (SubmissionFailed err)
|
||||
ExitSuccess -> do
|
||||
(buildCode, buildOut, buildErr) <- readProcess . setWorkingDir dir . fromString $ buildCommand
|
||||
case buildCode of
|
||||
ExitFailure n -> do
|
||||
let err = BS.unpack buildErr
|
||||
logError . T.pack $
|
||||
"test command for repo " <> show fullRepoName <> " at sha "
|
||||
<> show sha
|
||||
<> " exited with code "
|
||||
<> show n
|
||||
<> " and error "
|
||||
<> err
|
||||
scheduleTask erroredTask
|
||||
updateSubmissionStatus fullRepoName sha' (SubmissionFailed err)
|
||||
ExitSuccess ->
|
||||
case decode buildOut of
|
||||
Nothing -> do
|
||||
let err = BS.unpack buildOut
|
||||
logError . T.pack $
|
||||
"could not decode test result from repo " <> show fullRepoName
|
||||
<> " at sha "
|
||||
<> show sha
|
||||
<> ": "
|
||||
<> err
|
||||
scheduleTask erroredTask
|
||||
updateSubmissionStatus fullRepoName sha' (SubmissionFailed err)
|
||||
Just testResult@TestResult {..} -> do
|
||||
let total = M.size tests
|
||||
passed = M.size . M.filter id $ tests
|
||||
description = T.pack $ show passed <> "/" <> show total
|
||||
status = statusTask NewStatus
|
||||
{ newStatusState =
|
||||
if total == passed then StatusSuccess else StatusFailure,
|
||||
newStatusTargetUrl = Nothing,
|
||||
newStatusDescription = Just description,
|
||||
newStatusContext = Nothing
|
||||
}
|
||||
scheduleTask status
|
||||
updateSubmissionStatus fullRepoName sha' (SubmissionRun testResult)
|
||||
return Success
|
||||
where
|
||||
statusTask = StatusUpdate owner repoName sha
|
||||
erroredTask = statusTask $ NewStatus
|
||||
{ newStatusState = StatusError,
|
||||
newStatusTargetUrl = Nothing,
|
||||
newStatusDescription = Just "Build failed",
|
||||
newStatusContext = Nothing
|
||||
}
|
||||
|
||||
createDir :: MonadIO m => m FilePath
|
||||
createDir = do
|
||||
dir <- ("tmp/" <>) <$> getTmpDirName
|
||||
liftIO $ createDirectoryIfMissing True dir
|
||||
return dir
|
||||
|
||||
cloneRepo :: (MonadIO m, GithubCloner m) => String -> String -> FilePath -> m ()
|
||||
cloneRepo repoName sha path = do
|
||||
GithubAccessToken token <- getGithubAccessToken
|
||||
GithubUserName username <- getGithubUserName
|
||||
runProcess_ . setWorkingDir path . fromString $
|
||||
"git clone https://" <> username <> ":" <> token <> "@github.com/" <> repoName <> ".git ."
|
||||
runProcess_ . setWorkingDir path . fromString $ "git checkout " <> sha
|
||||
return ()
|
||||
|
||||
getTmpDirName :: MonadIO m => m FilePath
|
||||
getTmpDirName = liftIO $ do
|
||||
time :: Integer <- round <$> getPOSIXTime
|
||||
i <- randomIO @Int
|
||||
return $ show time <> "-" <> show i
|
41
src/Data/Tasks/StatusUpdate.hs
Normal file
41
src/Data/Tasks/StatusUpdate.hs
Normal file
@ -0,0 +1,41 @@
|
||||
{-# OPTIONS_GHC -Wno-orphans #-}
|
||||
|
||||
module Data.Tasks.StatusUpdate
|
||||
( StatusUpdate (..),
|
||||
)
|
||||
where
|
||||
|
||||
import Colog
|
||||
import Control.AppRequestable
|
||||
import Control.Task
|
||||
import Data.Aeson hiding (Success)
|
||||
import qualified Data.Text as T
|
||||
import GHC.Generics
|
||||
import GitHub
|
||||
|
||||
data StatusUpdate
|
||||
= StatusUpdate
|
||||
{ owner :: Name Owner,
|
||||
repoName :: Name Repo,
|
||||
sha :: Name Commit,
|
||||
status :: NewStatus
|
||||
}
|
||||
deriving (Eq, Show, Generic, ToJSON, FromJSON)
|
||||
|
||||
instance FromJSON NewStatus where
|
||||
parseJSON = withObject "NewStatus" $ \v ->
|
||||
NewStatus
|
||||
<$> v .: "state"
|
||||
<*> v .:? "target_url"
|
||||
<*> v .:? "description"
|
||||
<*> v .:? "context"
|
||||
|
||||
instance Task StatusUpdate "update-commit-ci-status" where
|
||||
|
||||
type TaskMonad StatusUpdate m = (AppRequestable m)
|
||||
|
||||
performTask StatusUpdate {..} = do
|
||||
response <- appRequest $ createStatusR owner repoName sha status
|
||||
case response of
|
||||
Left err -> logError (T.pack $ show err) >> return Failure
|
||||
Right st -> logDebug (T.pack $ show st) >> return Success
|
256
src/Server.hs
Normal file
256
src/Server.hs
Normal file
@ -0,0 +1,256 @@
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -Wno-orphans #-}
|
||||
|
||||
module Server
|
||||
( runServer,
|
||||
)
|
||||
where
|
||||
|
||||
import Colog
|
||||
import Control.AppRequestable
|
||||
import Control.Concurrent
|
||||
import Control.GithubCloner
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Monad.Reader
|
||||
import Control.Task.Scheduler
|
||||
import Data.Aeson
|
||||
import Data.ByteString
|
||||
import Data.IORef
|
||||
import Data.Map (Map)
|
||||
import qualified Data.Map as M
|
||||
import Data.Submission
|
||||
import Data.Submission.Query
|
||||
import Data.Tasks.Build
|
||||
import Data.Tasks.StatusUpdate
|
||||
import qualified Data.Text as T
|
||||
import Data.Yaml
|
||||
import GHC.Generics
|
||||
import GitHub
|
||||
import GitHub.App.Auth
|
||||
import GitHub.App.Request
|
||||
import GitHub.Data.Name
|
||||
import GitHub.Data.Webhooks.Events
|
||||
import GitHub.Data.Webhooks.Payload
|
||||
import Network.Wai.Handler.Warp
|
||||
import Servant
|
||||
import Servant.GitHub.Webhook
|
||||
import Servant.HTML.Blaze
|
||||
import Server.Config
|
||||
import Server.Html
|
||||
import Server.Schema
|
||||
import Squeal.PostgreSQL
|
||||
import System.IO
|
||||
import Text.Blaze.Html
|
||||
import UnliftIO.Exception (try)
|
||||
|
||||
type API =
|
||||
"github-webhook"
|
||||
:> ( GitHubEvent '[ 'WebhookInstallationEvent]
|
||||
:> GitHubSignedReqBody '[JSON] InstallationEvent
|
||||
:> Post '[JSON] ()
|
||||
:<|> GitHubEvent '[ 'WebhookPushEvent]
|
||||
:> GitHubSignedReqBody '[JSON] PushEvent
|
||||
:> Post '[JSON] ()
|
||||
)
|
||||
:<|> "submission"
|
||||
:> Capture "user name" String
|
||||
:> Capture "repo name" String
|
||||
:> Capture "sha" String
|
||||
:> Get '[HTML] Markup
|
||||
:<|> "submission"
|
||||
:> Capture "user name" String
|
||||
:> Capture "repo name" String
|
||||
:> Capture "sha" String
|
||||
:> "restart"
|
||||
:> Post '[HTML] Markup
|
||||
|
||||
webhookInstallation :: MonadIO m => RepoWebhookEvent -> ((), InstallationEvent) -> m ()
|
||||
webhookInstallation _ ((), ev) =
|
||||
liftIO $ do
|
||||
print ev
|
||||
hFlush stdout
|
||||
|
||||
pendingStatus :: NewStatus
|
||||
pendingStatus = NewStatus StatusPending Nothing Nothing Nothing
|
||||
|
||||
webhookPushEvent ::
|
||||
(StaticPQ m, WithLog env Message m, MonadUnliftIO m, HasTasks m) =>
|
||||
RepoWebhookEvent ->
|
||||
((), PushEvent) ->
|
||||
m ()
|
||||
webhookPushEvent _ ((), ev) = do
|
||||
let repo = evPushRepository ev
|
||||
user = T.unpack . whUserLogin . evPushSender $ ev
|
||||
fullRepoName = whRepoFullName repo
|
||||
[owner, repoName] = T.splitOn "/" fullRepoName
|
||||
Just sha = evPushHeadSha ev
|
||||
tasks <- getTasks
|
||||
let task =
|
||||
M.lookupMax $
|
||||
M.takeWhileAntitone (repoName >) tasks
|
||||
case task of
|
||||
Just (name, TaskConfig {..}) | T.take (T.length name) repoName == name -> do
|
||||
scheduleTask $ StatusUpdate (N owner) (N repoName) (N sha) pendingStatus
|
||||
createSubmission $
|
||||
Submission
|
||||
user
|
||||
(T.unpack fullRepoName)
|
||||
(T.unpack name)
|
||||
(T.unpack sha)
|
||||
(Jsonb BuildScheduled)
|
||||
scheduleTask $ Build prebuild build (N owner) (N repoName) (N sha)
|
||||
liftIO $ do
|
||||
print ev
|
||||
hFlush stdout
|
||||
_ -> return ()
|
||||
|
||||
retestSubmission ::
|
||||
(StaticPQ m, WithLog env Message m, MonadUnliftIO m, HasTasks m, MonadHasBaseUrl m) =>
|
||||
String ->
|
||||
String ->
|
||||
String ->
|
||||
m Markup
|
||||
retestSubmission owner repoName sha = do
|
||||
let fullRepoName = owner <> "/" <> repoName
|
||||
repoName' = T.pack repoName
|
||||
sha' = T.pack sha
|
||||
owner' = T.pack owner
|
||||
getSubmission fullRepoName sha >>= \case
|
||||
Nothing -> return ()
|
||||
Just _ -> do
|
||||
tasks <- getTasks
|
||||
let task =
|
||||
M.lookupMax $
|
||||
M.takeWhileAntitone (repoName' >) tasks
|
||||
case task of
|
||||
Just (name, TaskConfig {..}) | T.take (T.length name) repoName' == name -> do
|
||||
scheduleTask $ StatusUpdate (N owner') (N repoName') (N sha') pendingStatus
|
||||
updateSubmissionStatus fullRepoName sha BuildScheduled
|
||||
scheduleTask $ Build prebuild build (N owner') (N repoName') (N sha')
|
||||
_ -> return ()
|
||||
redirectToSubmission owner repoName sha
|
||||
|
||||
newtype ServerM (schema :: SchemasType) a
|
||||
= ServerM
|
||||
{ unApp :: ReaderT (ServerData (ServerM schema)) (PQ schema schema IO) a
|
||||
}
|
||||
deriving newtype
|
||||
( Functor,
|
||||
Applicative,
|
||||
Monad,
|
||||
MonadIO,
|
||||
MonadReader (ServerData (ServerM schema)),
|
||||
MonadPQ schema,
|
||||
MonadUnliftIO
|
||||
)
|
||||
|
||||
server :: ServerT API (ServerM Schema)
|
||||
server = (webhookInstallation :<|> webhookPushEvent) :<|> getSubmissionR :<|> retestSubmission
|
||||
|
||||
runServer :: IO ()
|
||||
runServer = do
|
||||
c@Config {..} <- getConfig
|
||||
print c
|
||||
auth <- mkInstallationAuth (AppAuth appId appPkPem) installationId
|
||||
ts <- newIORef mempty
|
||||
pool <- createConnectionPool databseUrl 1 0.5 10
|
||||
_ <- forkIO $ do
|
||||
updateTasks ts tasksPath
|
||||
threadDelay $ 10 * 60 * (10 ^ (6 :: Int))
|
||||
let context =
|
||||
gitPolyHubKey (return webhookSecret)
|
||||
:. EmptyContext
|
||||
serverData = ServerData
|
||||
{ githubAppAuth = auth,
|
||||
githubUserName = GithubUserName githubUsername,
|
||||
githubAccessToken = GithubAccessToken personalAccessToken,
|
||||
tasks = ts,
|
||||
logger = simpleMessageAction,
|
||||
baseUrl = baseSiteUrl
|
||||
}
|
||||
repeatIfNotEmpty n f = f >>= \m -> do
|
||||
when (m == 0) $ liftIO $ threadDelay n
|
||||
repeatIfNotEmpty n f
|
||||
_ <-
|
||||
forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
|
||||
. usingConnectionPool pool
|
||||
. (`runReaderT` serverData)
|
||||
. unApp
|
||||
$ runTasks @'[Build] 1
|
||||
_ <-
|
||||
forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
|
||||
. usingConnectionPool pool
|
||||
. (`runReaderT` serverData)
|
||||
. unApp
|
||||
$ runTasks @'[StatusUpdate] 5
|
||||
run port $ serveWithContext (Proxy @API) context $
|
||||
hoistServerWithContext
|
||||
(Proxy @API)
|
||||
(Proxy @'[PolyGitHubKey])
|
||||
(Handler . ExceptT . try . usingConnectionPool pool . (`runReaderT` serverData) . unApp)
|
||||
server
|
||||
|
||||
updateTasks :: IORef TasksConfig -> FilePath -> IO ()
|
||||
updateTasks ref path =
|
||||
decodeFileEither path >>= \case
|
||||
Right a -> writeIORef ref a
|
||||
Left e -> print e
|
||||
|
||||
data ServerData m
|
||||
= ServerData
|
||||
{ githubAppAuth :: !InstallationAuth,
|
||||
githubUserName :: !GithubUserName,
|
||||
githubAccessToken :: !GithubAccessToken,
|
||||
tasks :: IORef TasksConfig,
|
||||
logger :: LogAction m Message,
|
||||
baseUrl :: String
|
||||
}
|
||||
|
||||
newtype PolyGitHubKey = PolyGitHubKey (forall result. GitHubKey result)
|
||||
|
||||
gitPolyHubKey :: IO ByteString -> PolyGitHubKey
|
||||
gitPolyHubKey k = PolyGitHubKey (Servant.GitHub.Webhook.gitHubKey k)
|
||||
|
||||
instance HasContextEntry '[PolyGitHubKey] (GitHubKey result) where
|
||||
getContextEntry (PolyGitHubKey x :. _) = x
|
||||
|
||||
instance MonadReader (ServerData n) m => GithubCloner m where
|
||||
|
||||
getGithubAccessToken = asks githubAccessToken
|
||||
|
||||
getGithubUserName = asks githubUserName
|
||||
|
||||
instance (MonadIO m, MonadReader (ServerData n) m) => AppRequestable m where
|
||||
appRequest req = do
|
||||
auth <- asks githubAppAuth
|
||||
liftIO $ executeAppRequest auth req
|
||||
|
||||
instance MonadReader (ServerData n) m => MonadHasBaseUrl m where
|
||||
getBaseUrl = asks baseUrl
|
||||
|
||||
instance (MonadIO m, MonadReader (ServerData n) m) => HasTasks m where
|
||||
getTasks = asks tasks >>= liftIO . readIORef
|
||||
|
||||
type TasksConfig = Map T.Text TaskConfig
|
||||
|
||||
data TaskConfig
|
||||
= TaskConfig
|
||||
{ prebuild :: String,
|
||||
build :: String
|
||||
}
|
||||
deriving stock (Show, Eq, Generic)
|
||||
deriving anyclass (FromJSON, ToJSON)
|
||||
|
||||
class HasTasks m where
|
||||
getTasks :: m TasksConfig
|
||||
|
||||
instance HasLog (ServerData m) Message m where
|
||||
|
||||
getLogAction :: ServerData m -> LogAction m Message
|
||||
getLogAction = logger
|
||||
{-# INLINE getLogAction #-}
|
||||
|
||||
setLogAction :: LogAction m Message -> ServerData m -> ServerData m
|
||||
setLogAction newLogAction env = env {logger = newLogAction}
|
||||
{-# INLINE setLogAction #-}
|
56
src/Server/Config.hs
Normal file
56
src/Server/Config.hs
Normal file
@ -0,0 +1,56 @@
|
||||
{-# OPTIONS_GHC -Wno-orphans #-}
|
||||
|
||||
module Server.Config
|
||||
( Config (..),
|
||||
getConfig,
|
||||
)
|
||||
where
|
||||
|
||||
import Crypto.PubKey.RSA (PrivateKey)
|
||||
import Crypto.PubKey.RSA.Read
|
||||
import Data.ByteString as BS
|
||||
import qualified Data.ByteString.Char8 as BS8
|
||||
import Data.Maybe
|
||||
import GitHub.Data.Apps
|
||||
import GitHub.Data.Id
|
||||
import GitHub.Data.Installations
|
||||
import System.Environment
|
||||
|
||||
data Config
|
||||
= Config
|
||||
{ webhookSecret :: !ByteString,
|
||||
appId :: !(Id App),
|
||||
appPkPem :: !PrivateKey,
|
||||
port :: !Int,
|
||||
installationId :: !(Id Installation),
|
||||
personalAccessToken :: !String,
|
||||
githubUsername :: !String,
|
||||
tasksPath :: !FilePath,
|
||||
databseUrl :: !ByteString,
|
||||
baseSiteUrl :: !String
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
getConfig :: IO Config
|
||||
getConfig =
|
||||
Config
|
||||
<$> (maybe (error "no GITHUB_WEBHOOK_SECRET") BS8.pack <$> lookupEnv "GITHUB_WEBHOOK_SECRET")
|
||||
<*> (maybe (error "no GITHUB_APP_ID") (Id . read) <$> lookupEnv "GITHUB_APP_ID")
|
||||
<*> readPem
|
||||
<*> (maybe 8080 read <$> lookupEnv "PORT")
|
||||
<*> (maybe (error "no GITHUB_INSTALLATION_ID") (Id . read) <$> lookupEnv "GITHUB_INSTALLATION_ID")
|
||||
<*> (fromMaybe (error "no PERSONAL_ACCESS_TOKEN") <$> lookupEnv "PERSONAL_ACCESS_TOKEN")
|
||||
<*> (fromMaybe (error "no GITHUB_USERNAME") <$> lookupEnv "GITHUB_USERNAME")
|
||||
<*> (fromMaybe (error "no TASKS") <$> lookupEnv "TASKS")
|
||||
<*> (maybe (error "no DATABASE_URL") BS8.pack <$> lookupEnv "DATABASE_URL")
|
||||
<*> (fromMaybe "http://localhost:8080" <$> lookupEnv "PORT")
|
||||
where
|
||||
readPem :: IO PrivateKey
|
||||
readPem = do
|
||||
path <- fromMaybe (error "no app pk") <$> lookupEnv "GITHUB_APP_PK"
|
||||
pem <- BS.readFile path
|
||||
case readRsaPem pem of
|
||||
Right k -> return k
|
||||
Left e -> error (show e)
|
||||
|
||||
deriving instance Show ReadRsaKeyError
|
111
src/Server/Html.hs
Normal file
111
src/Server/Html.hs
Normal file
@ -0,0 +1,111 @@
|
||||
module Server.Html
|
||||
( getSubmissionR,
|
||||
MonadHasBaseUrl (..),
|
||||
redirectToSubmission,
|
||||
getUrl,
|
||||
)
|
||||
where
|
||||
|
||||
import Clay as C
|
||||
import Colog
|
||||
import Control.Monad.IO.Class
|
||||
import Control.Monad.IO.Unlift
|
||||
import Data.Aeson hiding (Success)
|
||||
import qualified Data.ByteString.Lazy.Char8 as BS
|
||||
import qualified Data.Map as M
|
||||
import Data.String
|
||||
import Data.Submission
|
||||
import Data.Submission.Query
|
||||
import qualified Data.Text as T
|
||||
import GHC.Generics
|
||||
import Server.Schema
|
||||
import Squeal.PostgreSQL (Jsonb (..))
|
||||
import Text.Blaze
|
||||
import Text.Blaze.Html
|
||||
import Text.Blaze.Html4.Strict.Attributes as A
|
||||
import Text.Blaze.Html5 as H
|
||||
|
||||
getSubmissionR ::
|
||||
(MonadUnliftIO m, StaticPQ m, WithLog env Message m, MonadHasBaseUrl m) =>
|
||||
String ->
|
||||
String ->
|
||||
String ->
|
||||
m Markup
|
||||
getSubmissionR user repo sha = do
|
||||
restartUrl <- (<> "/restart") <$> getUrl (user <> "/" <> repo) sha
|
||||
let restartBuild = do
|
||||
H.form H.! A.action (fromString restartUrl) H.! A.method "post" $
|
||||
H.input H.! A.type_ "submit" H.! A.value "Restart test"
|
||||
(titleText, inner) <- getSubmission (user <> "/" <> repo) sha >>= \case
|
||||
Nothing ->
|
||||
return . ("Not found",) $
|
||||
H.h1 "404. not found. go away."
|
||||
Just Submission {..} -> return . (repo,) $ do
|
||||
H.h1 $ toHtml problem
|
||||
H.h2 $ toHtml userName
|
||||
case status of
|
||||
Jsonb BuildScheduled -> do
|
||||
H.h3 "Waiting to build"
|
||||
restartBuild
|
||||
Jsonb (SubmissionFailed err) -> do
|
||||
H.h3 "Failed to build"
|
||||
restartBuild
|
||||
H.code . H.pre $ toHtml err
|
||||
Jsonb (SubmissionRun (TestResult tests)) -> do
|
||||
let total = M.size tests
|
||||
passed = M.size . M.filter Prelude.id $ tests
|
||||
H.h3 . toHtml $
|
||||
"Tests have run: " <> show passed <> "/" <> show total
|
||||
<> if passed == total then " ✅" else ""
|
||||
restartBuild
|
||||
H.table $ flip M.foldMapWithKey tests $ \testName testPassed -> H.tr $ do
|
||||
(H.td H.! A.align "right") . H.p $ toHtml testName
|
||||
H.td $ if testPassed then "✅" else "❌"
|
||||
return . docTypeHtml $ do
|
||||
H.head $ do
|
||||
H.title $ toHtml titleText
|
||||
H.style . preEscapedToHtml . render $ do
|
||||
C.body ? do
|
||||
fontFamily [] [sansSerif]
|
||||
C.maxWidth (px 800)
|
||||
C.display C.block
|
||||
C.marginLeft auto
|
||||
C.marginRight auto
|
||||
C.paddingLeft (px 16)
|
||||
C.paddingRight (px 16)
|
||||
(C.h1 <> C.h2 <> C.h3) ? do
|
||||
textAlign center
|
||||
C.pre ? do
|
||||
C.whiteSpace C.preWrap
|
||||
C.table ? do
|
||||
C.width (pct 100)
|
||||
(C.tr <> C.td) ? do
|
||||
C.width (pct 50)
|
||||
C.td ? do
|
||||
C.padding (px 4) (px 4) (px 4) (px 4)
|
||||
C.form ? do
|
||||
C.display C.block
|
||||
C.marginLeft auto
|
||||
C.marginRight auto
|
||||
H.body inner
|
||||
|
||||
class MonadHasBaseUrl m where
|
||||
getBaseUrl :: m String
|
||||
|
||||
getUrl :: (MonadHasBaseUrl m, Monad m) => String -> String -> m String
|
||||
getUrl fullName sha = do
|
||||
siteBase <- getBaseUrl
|
||||
return $ siteBase <> "/submission/" <> fullName <> "/" <> sha
|
||||
|
||||
redirectToSubmission ::
|
||||
(MonadHasBaseUrl m, Monad m) =>
|
||||
String ->
|
||||
String ->
|
||||
String ->
|
||||
m Markup
|
||||
redirectToSubmission user repo sha = do
|
||||
rUrl <- getUrl (user <> "/" <> repo) sha
|
||||
return $ docTypeHtml $ H.head $
|
||||
H.meta
|
||||
H.! A.httpEquiv "refresh"
|
||||
H.! A.content (fromString $ "0; URL=" <> rUrl)
|
18
src/Server/Schema.hs
Normal file
18
src/Server/Schema.hs
Normal file
@ -0,0 +1,18 @@
|
||||
module Server.Schema
|
||||
( StaticPQ,
|
||||
Schema,
|
||||
migration,
|
||||
)
|
||||
where
|
||||
|
||||
import Server.Schema.V1
|
||||
import Squeal.PostgreSQL
|
||||
|
||||
type StaticPQ m = MonadPQ Schema m
|
||||
|
||||
type Schema = SchemaV1
|
||||
|
||||
migration :: AlignedList (Migration (Terminally PQ IO)) (Public '[]) Schema
|
||||
migration =
|
||||
pureMigration schemaMigrationV1
|
||||
:>> Done
|
63
src/Server/Schema/V1.hs
Normal file
63
src/Server/Schema/V1.hs
Normal file
@ -0,0 +1,63 @@
|
||||
module Server.Schema.V1
|
||||
( SchemaV1,
|
||||
schemaMigrationV1,
|
||||
)
|
||||
where
|
||||
|
||||
import Data.Aeson
|
||||
import Data.Submission
|
||||
import Data.Text (Text)
|
||||
import Data.Time
|
||||
import Squeal.PostgreSQL
|
||||
|
||||
type SchemaV1 =
|
||||
Public
|
||||
'[ "tasks"
|
||||
::: 'Table
|
||||
( '[ "pk_task_payload" :=> 'PrimaryKey '["task", "payload"]
|
||||
]
|
||||
:=> '[ "task" ::: 'NoDef :=> 'NotNull (PG Text),
|
||||
"payload" ::: 'NoDef :=> 'NotNull (PG (Jsonb Value)),
|
||||
"creation_time" ::: 'Def :=> 'NotNull (PG UTCTime)
|
||||
]
|
||||
),
|
||||
"submissions"
|
||||
::: 'Table
|
||||
( '["pk_submission_repo_full_name_sha" :=> 'PrimaryKey '["repo_full_name", "sha"]]
|
||||
:=> '[ "user_name" ::: 'NoDef :=> 'NotNull (PG String),
|
||||
"repo_full_name" ::: 'NoDef :=> 'NotNull (PG String),
|
||||
"problem" ::: 'NoDef :=> 'NotNull (PG String),
|
||||
"sha" ::: 'NoDef :=> 'NotNull (PG String),
|
||||
"status" ::: 'NoDef :=> 'NotNull (PG (Jsonb SubmissionStatus))
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
schemaMigrationV1 :: Migration Definition (Public '[]) SchemaV1
|
||||
schemaMigrationV1 = Migration
|
||||
{ name = "v1",
|
||||
up =
|
||||
createTable
|
||||
#tasks
|
||||
( notNullable text `as` #task
|
||||
:* notNullable jsonb `as` #payload
|
||||
:* default_ now (notNullable timestampWithTimeZone) `as` #creation_time
|
||||
)
|
||||
(primaryKey (#task :* #payload) `as` #pk_task_payload)
|
||||
>>> createTable
|
||||
#submissions
|
||||
( notNullable text `as` #user_name
|
||||
:* notNullable text `as` #repo_full_name
|
||||
:* notNullable text `as` #problem
|
||||
:* notNullable text `as` #sha
|
||||
:* notNullable jsonb `as` #status
|
||||
)
|
||||
(primaryKey (#repo_full_name :* #sha) `as` #pk_submission_repo_full_name_sha)
|
||||
>>> createIndexes,
|
||||
down = dropTable #tasks >>> dropTable #submissions
|
||||
}
|
||||
where
|
||||
createIndexes :: Definition sch sch
|
||||
createIndexes =
|
||||
UnsafeDefinition
|
||||
"CREATE INDEX ON tasks (creation_time);"
|
75
stack.yaml
Normal file
75
stack.yaml
Normal file
@ -0,0 +1,75 @@
|
||||
# This file was automatically generated by 'stack init'
|
||||
#
|
||||
# Some commonly used options have been documented as comments in this file.
|
||||
# For advanced use and comprehensive documentation of the format, please see:
|
||||
# https://docs.haskellstack.org/en/stable/yaml_configuration/
|
||||
|
||||
# Resolver to choose a 'specific' stackage snapshot or a compiler version.
|
||||
# A snapshot resolver dictates the compiler version and the set of packages
|
||||
# to be used for project dependencies. For example:
|
||||
#
|
||||
# resolver: lts-3.5
|
||||
# resolver: nightly-2015-09-21
|
||||
# resolver: ghc-7.10.2
|
||||
#
|
||||
# The location of a snapshot can be provided as a file or url. Stack assumes
|
||||
# a snapshot provided as a file might change, whereas a url resource does not.
|
||||
#
|
||||
# resolver: ./custom-snapshot.yaml
|
||||
# resolver: https://example.com/snapshots/2018-01-01.yaml
|
||||
resolver: lts-14.19
|
||||
|
||||
# User packages to be built.
|
||||
# Various formats can be used as shown in the example below.
|
||||
#
|
||||
# packages:
|
||||
# - some-directory
|
||||
# - https://example.com/foo/bar/baz-0.0.2.tar.gz
|
||||
# subdirs:
|
||||
# - auto-update
|
||||
# - wai
|
||||
packages:
|
||||
- .
|
||||
# Dependency packages to be pulled from upstream that are not in the resolver.
|
||||
# These entries can reference officially published versions as well as
|
||||
# forks / in-progress versions pinned to a git hash. For example:
|
||||
#
|
||||
# extra-deps:
|
||||
# - acme-missiles-0.3
|
||||
# - git: https://github.com/commercialhaskell/stack.git
|
||||
# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
|
||||
#
|
||||
extra-deps:
|
||||
- github-0.24
|
||||
- binary-instances-1
|
||||
- squeal-postgresql-0.5.2.0
|
||||
- git: https://github.com/ilyakooo0/github-app
|
||||
commit: 0bede85ab53eb8f953cf6f4e58a756a205e88e0b
|
||||
- servant-github-webhook-0.4.2.0
|
||||
|
||||
# Override default flag values for local packages and extra-deps
|
||||
# flags: {}
|
||||
|
||||
# Extra package databases containing global packages
|
||||
# extra-package-dbs: []
|
||||
|
||||
# Control whether we use the GHC we find on the path
|
||||
# system-ghc: true
|
||||
#
|
||||
# Require a specific version of stack, using version ranges
|
||||
# require-stack-version: -any # Default
|
||||
# require-stack-version: ">=2.1"
|
||||
#
|
||||
# Override the architecture used by stack, especially useful on Windows
|
||||
# arch: i386
|
||||
# arch: x86_64
|
||||
#
|
||||
# Extra directories used by stack for building
|
||||
# extra-include-dirs: [/path/to/dir]
|
||||
# extra-lib-dirs: [/path/to/dir]
|
||||
#
|
||||
# Allow a newer minor version of GHC than the snapshot specifies
|
||||
# compiler-check: newer-minor
|
||||
|
||||
ghc-options:
|
||||
"$locals": -ddump-to-file -ddump-hi -fwarn-unused-binds -fwarn-unused-imports -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -Werror=missing-home-modules -Wmissing-home-modules -Widentities -Wredundant-constraints -Wmissing-export-lists
|
Loading…
Reference in New Issue
Block a user