on master help sign in
20 files changed+580−322
since splitsnapshots
unifiedsplit
.gitignore+1 README.md+64 app/Main.hs+25−1 package.yaml+7 sc-build.cabal−106 src/Control/GithubCloner.hs+4−10 src/Control/HasGithubStatus.hs+14 src/Control/Task/Scheduler.hs+5−4 src/Control/Task/Scheduler/Query.hs+37−4 src/Data/Submission.hs+1 src/Data/Tasks/Build.hs+92−65 src/Data/Tasks/StatusUpdate.hs−1 src/Server.hs+165−57 src/Server/Config.hs+72−35 src/Server/Html.hs+14−13 src/Server/Schema.hs+3−1 src/Server/Schema/V1.hs+25−22 src/Server/Schema/V2.hs+46 stack.yaml+4−3 tasks.yaml+1
.gitignore+1
@@ -1 +1,2 @@
11.stack-work
2+*.cabal
README.mdnew file+64
@@ -0,0 +1,64 @@
1+# sc-build
2+
3+Need both manual weebohook and app.
4+
5+Installation id needs to be configured manually.
6+
7+## Usage
8+
9+You want to have some predefined docker container with maximum dependencies cached.
10+
11+
12+Exmaple:
13+
14+```dockerfile
15+FROM fpco/stack-build:lts-14.27
16+
17+RUN stack update
18+RUN stack build tasty tasty-hspec aeson containers bytestring
19+
20+WORKDIR /
21+```
22+
23+You need to build that container and git it a name:
24+
25+```bash
26+docker build -t fast-lts-14.27 - < FastDockerFile
27+```
28+
29+After that you can use it in your `tasks.yaml`:
30+
31+```yaml
32+test-task:
33+ prebuild: |
34+ cp -f ../../test-task/package.yaml .
35+ cp -f ../../test-task/stack.yaml .
36+ cp -f -R ../../test-task/test .
37+ dockerfile: |
38+ FROM fast-lts-14.27
39+ COPY . src
40+ WORKDIR /src
41+ ENTRYPOINT stack test --fast
42+ timeoutMinutes: 10
43+```
44+
45+The docker container is expected to write json to stdout, conating test results:
46+
47+```json
48+{
49+ "test1": true,
50+ "test2": false
51+}
52+```
53+
54+For macos docker API:
55+
56+```bash
57+socat TCP-LISTEN:1234,range=127.0.0.1/32,reuseaddr,fork UNIX-CLIENT:/var/run/docker.sock
58+```
59+
60+or this (didn't work for me):
61+
62+```bash
63+docker run -d -v /var/run/docker.sock:/var/run/docker.sock -p 127.0.0.1:2357:1234 bobrik/socat TCP-LISTEN:1234,fork UNIX-CONNECT:/var/run/docker.sock
64+```
app/Main.hs+25−1
@@ -1,6 +1,30 @@
11module Main (main) where
22
3+import qualified Data.ByteString.Char8 as BS
4+import Data.Yaml (encode)
5+import Options.Applicative
36import Server
7+import Server.Config
48
59main :: IO ()
6−main = runServer
10+main =
11+ execParser (info (getOptions <**> helper) fullDesc) >>= \case
12+ PrintDefaultConfig -> putStrLn . BS.unpack . encode $ defaultConfig
13+ RunServer config -> runServer config
14+
15+data Run
16+ = PrintDefaultConfig
17+ | RunServer FilePath
18+
19+getOptions :: Parser Run
20+getOptions =
21+ subparser
22+ ( command "default" (info (pure PrintDefaultConfig <**> helper) fullDesc)
23+ )
24+ <|> ( RunServer
25+ <$> strOption
26+ ( long "config"
27+ <> help "Path to yaml config file."
28+ <> value "config.yaml"
29+ )
30+ )
package.yaml+7
@@ -17,6 +17,7 @@ dependencies:
1717library:
1818 source-dirs: src
1919 dependencies:
20+ - http-client-tls
2021 - servant
2122 - servant-server
2223 - process
@@ -49,6 +50,9 @@ library:
4950 - docker
5051 - exceptions
5152 - transformers
53+ - cassava
54+ - http-media
55+ - utf8-string
5256
5357executables:
5458 sc-build-exe:
@@ -60,6 +64,9 @@ executables:
6064 - -with-rtsopts=-N
6165 dependencies:
6266 - sc-build
67+ - bytestring
68+ - yaml
69+ - optparse-applicative
6370 sc-build-migrations:
6471 main: Main.hs
6572 source-dirs: migrations
sc-build.cabaldeleted−106
@@ -1,106 +0,0 @@
1−cabal-version: 1.12
2−
3−-- This file has been generated from package.yaml by hpack version 0.31.2.
4−--
5−-- see: https://github.com/sol/hpack
6−--
7−-- hash: 56eccc7f4958f234614ddcff75d3b965ad9a29747591e1e71b73d56faaf48d1c
8−
9−name: sc-build
10−version: 0.1.0.0
11−category: Web
12−homepage: https://github.com/ilyakooo0/sc-build#readme
13−author: Ilya Kostyuchenko
14−maintainer: ilyakooo0@gmail.com
15−copyright: Ilya Kostyuchenko
16−license: BSD3
17−license-file: LICENSE
18−build-type: Simple
19−extra-source-files:
20− README.md
21−
22−library
23− exposed-modules:
24− Control.AppRequestable
25− Control.GithubCloner
26− Control.Squeal
27− Control.Task
28− Control.Task.Scheduler
29− Control.Task.Scheduler.Query
30− Data.Submission
31− Data.Submission.Query
32− Data.Tasks.Build
33− Data.Tasks.StatusUpdate
34− Server
35− Server.Config
36− Server.Html
37− Server.Schema
38− Server.Schema.V1
39− other-modules:
40− Paths_sc_build
41− hs-source-dirs:
42− src
43− 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
44− build-depends:
45− aeson
46− , base >=4.7 && <5
47− , blaze-html
48− , blaze-markup
49− , bytestring
50− , clay
51− , co-log
52− , containers
53− , cryptonite
54− , directory
55− , docker
56− , exceptions
57− , generics-sop
58− , github
59− , github-app
60− , github-webhooks
61− , mtl
62− , process
63− , random
64− , servant
65− , servant-blaze
66− , servant-github-webhook
67− , servant-server
68− , squeal-postgresql
69− , text
70− , time
71− , transformers
72− , typed-process
73− , unliftio
74− , unliftio-core
75− , vector
76− , warp
77− , yaml
78− default-language: Haskell2010
79−
80−executable sc-build-exe
81− main-is: Main.hs
82− other-modules:
83− Paths_sc_build
84− hs-source-dirs:
85− app
86− 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
87− ghc-options: -threaded -rtsopts -with-rtsopts=-N
88− build-depends:
89− base >=4.7 && <5
90− , sc-build
91− default-language: Haskell2010
92−
93−executable sc-build-migrations
94− main-is: Main.hs
95− other-modules:
96− Paths_sc_build
97− hs-source-dirs:
98− migrations
99− 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
100− ghc-options: -threaded -rtsopts -with-rtsopts=-N
101− build-depends:
102− base >=4.7 && <5
103− , bytestring
104− , sc-build
105− , squeal-postgresql
106− default-language: Haskell2010
src/Control/GithubCloner.hs+4−10
@@ -1,16 +1,10 @@
11module Control.GithubCloner
2− ( GithubAccessToken (..),
3− GithubUserName (..),
4− GithubCloner (..),
2+ ( GithubCloner (..),
3+ InstallationToken (..),
54 )
65where
76
8−newtype GithubAccessToken = GithubAccessToken String
9−
10−newtype GithubUserName = GithubUserName String
7+newtype InstallationToken = InstallationToken String
118
129class GithubCloner m where
13−
14− getGithubAccessToken :: m GithubAccessToken
15−
16− getGithubUserName :: m GithubUserName
10+ getGithubInstallationToken :: m InstallationToken
src/Control/HasGithubStatus.hsnew file+14
@@ -0,0 +1,14 @@
1+module Control.HasGithubStatus
2+ ( HasGithubStatus (..),
3+ )
4+where
5+
6+import Colog
7+import Data.Submission
8+import GitHub.Data
9+
10+class HasGithubStatus m where
11+ schedulePendingStatus :: WithLog env Message m => Name Owner -> Name Repo -> Name Commit -> m ()
12+ scheduleTestedStatus :: WithLog env Message m => TestResult -> Name Owner -> Name Repo -> Name Commit -> m ()
13+ scheduleFailedStatus :: WithLog env Message m => String -> Name Owner -> Name Repo -> Name Commit -> m ()
14+ deadlineHasPassedStatus :: WithLog env Message m => Name Owner -> Name Repo -> Name Commit -> m ()
src/Control/Task/Scheduler.hs+5−4
@@ -3,6 +3,7 @@
33module Control.Task.Scheduler
44 ( scheduleTask,
55 runTasks,
6+ makeAllRunnable,
67 )
78where
89
@@ -20,6 +21,7 @@ import Data.Word
2021import GHC.TypeLits
2122import Server.Schema
2223import Squeal.PostgreSQL
24+import UnliftIO.Exception
2325
2426scheduleTask ::
2527 (StaticPQ m, WithLog env Message m, Task t s, MonadUnliftIO m) =>
@@ -45,7 +47,6 @@ rescheduleTaskTime ::
4547rescheduleTaskTime = rescheduleTaskQuery
4648
4749class RunTasks (ts :: [*]) m where
48−
4950 runTask :: WithLog env Message m => PickedTask -> m ()
5051
5152 taskNames :: [String]
@@ -55,7 +56,6 @@ instance
5556 (Task t ts, TaskMonad t m, StaticPQ m, RunTasks tt m, MonadUnliftIO m) =>
5657 RunTasks (t ': tt) m
5758 where
58−
5959 runTask (PickedTask s (Jsonb v) initialStartTime) | symbolVal @ts Proxy == s =
6060 case fromJSON @t v of
6161 A.Error e ->
@@ -67,7 +67,9 @@ instance
6767 <> e
6868 A.Success t -> do
6969 logInfo . T.pack $ "running task " <> task <> " with " <> show t <> " created at " <> show initialStartTime
70− r <- performTask @t @ts @m t
70+ r <- performTask @t @ts @m t `catchAny` \e -> do
71+ logError . T.pack . displayException $ e
72+ return Failure
7173 when (r == Failure) $ logWarning . T.pack $ "Task " <> task <> " failed"
7274 case r of
7375 Success -> completeTaskQuery t
@@ -79,7 +81,6 @@ instance
7981 taskNames = symbolVal @ts Proxy : taskNames @tt @m
8082
8183instance RunTasks '[] m where
82−
8384 runTask (PickedTask s (Jsonb v) _) =
8485 logError . T.pack $ "Unknown task " <> s <> " with payload " <> show v
8586
src/Control/Task/Scheduler/Query.hs+37−4
@@ -5,11 +5,13 @@ module Control.Task.Scheduler.Query
55 completeTaskQuery,
66 rescheduleTaskQuery,
77 pickTasksQuery,
8+ makeAllRunnable,
89 PickedTask (..),
910 )
1011where
1112
1213import Colog
14+import Control.Arrow
1315import Control.Monad.IO.Unlift
1416import Control.Squeal
1517import Control.Task
@@ -38,6 +40,8 @@ scheduleTaskQuery task = do
3840 Set (param @1) `as` #task
3941 :* Set (param @2) `as` #payload
4042 :* Default `as` #creation_time
43+ :* Default `as` #started
44+ :* Default `as` #failures
4145 )
4246 ( OnConflict
4347 (OnConstraint #pk_task_payload)
@@ -82,7 +86,10 @@ rescheduleTaskQuery payload = do
8286 query =
8387 update_
8488 #tasks
85− (Default `as` #creation_time)
89+ ( Default `as` #creation_time
90+ :* Default `as` #started
91+ :* Set (#failures + 1) `as` #failures
92+ )
8693 (param @1 .== #task .&& param @2 .== #payload)
8794 dbWrite (const $ return ()) $
8895 manipulateParams_
@@ -96,7 +103,7 @@ pickTasksQuery ::
96103 Word64 ->
97104 m [PickedTask]
98105pickTasksQuery tasks limitCount = do
99− logDebug "reading tasks"
106+ logDebug $ "reading tasks " <> T.unwords (T.pack <$> tasks)
100107 let query :: Query_ Schema () PickedTask
101108 query =
102109 select_
@@ -106,11 +113,23 @@ pickTasksQuery tasks limitCount = do
106113 )
107114 ( from (table #tasks)
108115 & where_ (#task `in_` (literal <$> tasks))
116+ & where_ (not_ . notNull $ #started)
117+ & where_ (#failures .< 20)
109118 & orderBy [#creation_time & Asc]
110119 & limit limitCount
120+ & orderBy [#creation_time & Desc]
121+ )
122+ startedMan :: Manipulation_ Schema (String, Jsonb Value) ()
123+ startedMan =
124+ update_
125+ #tasks
126+ ( Set true `as` #started
111127 )
112− dbRead (const $ return []) $
113− runQuery query >>= getRows
128+ (param @1 .== #task .&& param @2 .== #payload)
129+ dbWrite (const $ return []) $ do
130+ pickedTasks <- runQuery query >>= getRows
131+ traversePrepared_ startedMan $ (task &&& payload) <$> pickedTasks
132+ return pickedTasks
114133
115134data PickedTask
116135 = PickedTask
@@ -119,3 +138,17 @@ data PickedTask
119138 creationTime :: UTCTime
120139 }
121140 deriving (Eq, Show, Generic, SOP.Generic, SOP.HasDatatypeInfo)
141+
142+makeAllRunnable ::
143+ (StaticPQ m, WithLog env Message m, MonadUnliftIO m) =>
144+ m ()
145+makeAllRunnable = do
146+ let startAll :: Manipulation_ Schema () ()
147+ startAll =
148+ update_
149+ #tasks
150+ ( Set false `as` #started
151+ )
152+ (notNull #started)
153+ dbWrite (const $ return ()) $
154+ manipulate_ startAll
src/Data/Submission.hs+1
@@ -29,6 +29,7 @@ data SubmissionStatus
2929 = SubmissionRun TestResult
3030 | SubmissionFailed String
3131 | BuildScheduled
32+ | PastDeadline
3233 deriving (Generic, FromJSON, ToJSON, Eq, Show)
3334
3435newtype TestResult
src/Data/Tasks/Build.hs+92−65
@@ -8,18 +8,18 @@ module Data.Tasks.Build
88where
99
1010import Colog
11+import Control.Concurrent
1112import Control.GithubCloner
13+import Control.HasGithubStatus
1214import Control.Monad.Except
1315import Control.Monad.IO.Unlift
1416import Control.Task
15−import Control.Task.Scheduler
1617import Data.Aeson hiding (Success)
1718import qualified Data.ByteString.Lazy.Char8 as BS
18−import qualified Data.Map as M
19+import qualified Data.ByteString.Lazy.UTF8 as BS
1920import Data.String
2021import Data.Submission
2122import Data.Submission.Query
22−import Data.Tasks.StatusUpdate
2323import qualified Data.Text as T
2424import Data.Time.Clock.POSIX
2525import qualified Docker.Client as D
@@ -30,6 +30,7 @@ import System.Directory
3030import System.Exit
3131import System.Process.Typed
3232import System.Random
33+import UnliftIO.Async
3334import UnliftIO.Exception
3435
3536data Build
@@ -38,22 +39,23 @@ data Build
3839 dockerFile :: String,
3940 owner :: Name Owner,
4041 repoName :: Name Repo,
41− sha :: Name Commit
42+ sha :: Name Commit,
43+ timeoutMinutes :: Int
4244 }
4345 deriving (Eq, Show, Generic, FromJSON, ToJSON)
4446
4547instance Task Build "build-repo" where
46− type TaskMonad Build m = (MonadUnliftIO m, GithubCloner m, StaticPQ m, HasDockerInfo m)
48+ type
49+ TaskMonad Build m =
50+ (MonadUnliftIO m, StaticPQ m, HasDockerInfo m, GithubCloner m, HasGithubStatus m)
4751
4852 performTask Build {..} =
49− bracket createDir (liftIO . removePathForcibly) $ \dir -> do
50− let fullRepoName = T.unpack $ untagName owner <> "/" <> untagName repoName
51− sha' = T.unpack $ untagName sha
53+ bracket createDir (liftIO . removePathForcibly) $ \dir -> flip catchAny (const $ return Failure) $ do
5254 cloneRepo fullRepoName sha' dir
5355 (shellCode, _, shellErr) <- readProcess . setWorkingDir dir $ shell preProcessShell
5456 case shellCode of
5557 ExitFailure n -> do
56− let err = BS.unpack shellErr
58+ let err = BS.toString shellErr
5759 logError . T.pack $
5860 "preprocessing command for repo " <> show fullRepoName <> " at sha "
5961 <> show sha
@@ -61,14 +63,23 @@ instance Task Build "build-repo" where
6163 <> show n
6264 <> " and error "
6365 <> err
64− scheduleTask erroredTask
66+ scheduleFailedStatus err owner repoName sha
6567 updateSubmissionStatus fullRepoName sha' (SubmissionFailed err)
6668 ExitSuccess -> do
6769 h <- liftIO D.defaultHttpHandler
6870 dUrl <- getDockerUrl
6971 let opts = D.defaultCreateOpts imageName
70− runDocker :: forall a m. MonadIO m => D.DockerT IO a -> m a
71− runDocker = liftIO . D.runDockerT (D.defaultClientOpts {D.baseUrl = dUrl}, h)
72+ runDocker ::
73+ ( MonadIO m,
74+ Exception err,
75+ WithLog env Message m,
76+ StaticPQ m,
77+ MonadUnliftIO m,
78+ HasGithubStatus m
79+ ) =>
80+ D.DockerT IO (Either err a) ->
81+ m a
82+ runDocker = runEither <=< liftIO . D.runDockerT (D.defaultClientOpts {D.baseUrl = dUrl}, h)
7283 imageName = T.pack dir
7384 liftIO $ writeFile (dir <> "/Dockerfile") dockerFile
7485 absDir <- liftIO $ makeAbsolute dir
@@ -76,26 +87,49 @@ instance Task Build "build-repo" where
7687 D.waitContainer cid >>= \case
7788 Left D.GenericDockerError {} -> waitDockerForever cid
7889 a -> return a
79− dockerRes <- runDocker . runExceptT $ do
90+ cid <- runDocker . runExceptT $ do
8091 ExceptT $ D.buildImageFromDockerfile (D.defaultBuildOpts imageName) absDir
81− cid <- ExceptT $ D.createContainer opts Nothing
82− ExceptT $ D.startContainer D.defaultStartOpts cid
83− buildCode <- ExceptT $ waitDockerForever cid
84− buildOut <-
85− ExceptT $
86− D.getContainerLogs
87− D.LogOpts
88− { stdout = True,
89− stderr = False,
90− since = Nothing,
91− timestamps = False,
92− tail = D.All
93− }
94− cid
95− return (buildCode, buildOut)
92+ ExceptT $ D.createContainer opts Nothing
93+ dockerRes <- fmap (either id id)
94+ . race (liftIO (threadDelay (timeoutMinutes * 60000000)) >> return (ExitFailure 1, "Time out", "Time out"))
95+ $ flip
96+ finally
97+ ( runDocker $
98+ D.deleteContainer
99+ D.ContainerDeleteOpts {deleteVolumes = True, force = True}
100+ cid
101+ )
102+ . runDocker
103+ . runExceptT
104+ $ do
105+ ExceptT $ D.startContainer D.defaultStartOpts cid
106+ buildCode <- ExceptT $ waitDockerForever cid
107+ buildOut <-
108+ ExceptT $
109+ D.getContainerLogs
110+ D.LogOpts
111+ { stdout = True,
112+ stderr = False,
113+ since = Nothing,
114+ timestamps = False,
115+ tail = D.All
116+ }
117+ cid
118+ buildErr <-
119+ ExceptT $
120+ D.getContainerLogs
121+ D.LogOpts
122+ { stdout = True,
123+ stderr = True,
124+ since = Nothing,
125+ timestamps = False,
126+ tail = D.All
127+ }
128+ cid
129+ return (buildCode, buildOut, buildErr)
96130 case dockerRes of
97− Right (ExitFailure n, buildOut) -> do
98− let err = BS.unpack buildOut
131+ (ExitFailure n, _, buildErr) -> do
132+ let err = BS.toString buildErr
99133 logError . T.pack $
100134 "test command for repo " <> show fullRepoName <> " at sha "
101135 <> show sha
@@ -103,51 +137,43 @@ instance Task Build "build-repo" where
103137 <> show n
104138 <> " and error "
105139 <> err
106− scheduleTask erroredTask
140+ scheduleFailedStatus err owner repoName sha
107141 updateSubmissionStatus fullRepoName sha' (SubmissionFailed err)
108− Left err' -> do
109− let err = show err'
110− logError . T.pack $ err
111− scheduleTask erroredTask
112− updateSubmissionStatus fullRepoName sha' (SubmissionFailed err)
113− Right (ExitSuccess, buildOut) ->
142+ (ExitSuccess, buildOut, _) ->
114143 case eitherDecode' (BS.dropWhile (/= '{') buildOut) of
115144 Left err' -> do
116− let err = err' <> " " <> BS.unpack buildOut
145+ let err = err' <> " " <> BS.toString buildOut
117146 logError . T.pack $
118147 "could not decode test result from repo " <> show fullRepoName
119148 <> " at sha "
120149 <> show sha
121150 <> ": "
122151 <> err
123− scheduleTask erroredTask
124− updateSubmissionStatus fullRepoName sha' (SubmissionFailed err)
152+ scheduleFailedStatus err owner repoName sha
153+ updateSubmissionStatus fullRepoName sha' (SubmissionFailed (err <> BS.toString buildOut))
125154 Right testResult@TestResult {..} -> do
126− let total = M.size tests
127− passed = M.size . M.filter id $ tests
128− description = T.pack $ show passed <> "/" <> show total
129− status =
130− statusTask
131− NewStatus
132− { newStatusState =
133− if total == passed then StatusSuccess else StatusFailure,
134− newStatusTargetUrl = Nothing,
135− newStatusDescription = Just description,
136− newStatusContext = Nothing
137− }
138− scheduleTask status
155+ scheduleTestedStatus testResult owner repoName sha
139156 updateSubmissionStatus fullRepoName sha' (SubmissionRun testResult)
140157 return Success
141158 where
142− statusTask = StatusUpdate owner repoName sha
143− erroredTask =
144− statusTask $
145− NewStatus
146− { newStatusState = StatusError,
147− newStatusTargetUrl = Nothing,
148− newStatusDescription = Just "Build failed",
149− newStatusContext = Nothing
150− }
159+ fullRepoName = T.unpack $ untagName owner <> "/" <> untagName repoName
160+ sha' = T.unpack $ untagName sha
161+ runEither ::
162+ ( Exception err,
163+ WithLog env Message m,
164+ StaticPQ m,
165+ MonadUnliftIO m,
166+ HasGithubStatus m
167+ ) =>
168+ Either err b ->
169+ m b
170+ runEither (Left err) = do
171+ let err' = show err
172+ logError $ T.pack err'
173+ scheduleFailedStatus err' owner repoName sha
174+ updateSubmissionStatus fullRepoName sha' (SubmissionFailed err')
175+ throwIO err
176+ runEither (Right a) = return a
151177
152178createDir :: MonadIO m => m FilePath
153179createDir = do
@@ -157,10 +183,9 @@ createDir = do
157183
158184cloneRepo :: (MonadIO m, GithubCloner m) => String -> String -> FilePath -> m ()
159185cloneRepo repoName sha path = do
160− GithubAccessToken token <- getGithubAccessToken
161− GithubUserName username <- getGithubUserName
186+ InstallationToken token <- getGithubInstallationToken
162187 runProcess_ . setWorkingDir path . fromString $
163− "git clone https://" <> username <> ":" <> token <> "@github.com/" <> repoName <> ".git ."
188+ "git clone https://x-access-token:" <> token <> "@github.com/" <> repoName <> ".git ."
164189 runProcess_ . setWorkingDir path . fromString $ "git checkout " <> sha
165190 return ()
166191
@@ -172,3 +197,5 @@ getTmpDirName = liftIO $ do
172197
173198class HasDockerInfo m where
174199 getDockerUrl :: m T.Text
200+
201+instance Exception D.DockerError
src/Data/Tasks/StatusUpdate.hs−1
@@ -31,7 +31,6 @@ instance FromJSON NewStatus where
3131 <*> v .:? "context"
3232
3333instance Task StatusUpdate "update-commit-ci-status" where
34−
3534 type TaskMonad StatusUpdate m = (AppRequestable m)
3635
3736 performTask StatusUpdate {..} = do
src/Server.hs+165−57
@@ -10,12 +10,17 @@ import Colog
1010import Control.AppRequestable
1111import Control.Concurrent
1212import Control.GithubCloner
13+import Control.HasGithubStatus
1314import Control.Monad.Except
1415import Control.Monad.IO.Unlift
1516import Control.Monad.Reader
1617import Control.Task.Scheduler
18+import Crypto.PubKey.RSA.Read
1719import Data.Aeson
1820import 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
1924import qualified Data.Csv as Csv
2025import Data.IORef
2126import Data.Map (Map)
@@ -25,14 +30,17 @@ import Data.Submission.Query
2530import Data.Tasks.Build
2631import Data.Tasks.StatusUpdate
2732import qualified Data.Text as T
33+import Data.Time.Clock
2834import Data.Yaml
2935import GHC.Generics
3036import GitHub hiding (Accept)
3137import GitHub.App.Auth
3238import GitHub.App.Request
39+import GitHub.Auth (Auth (OAuth))
3340import GitHub.Data.Name
3441import 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)
3644import Network.HTTP.Media.MediaType
3745import Network.Wai.Handler.Warp
3846import Servant
@@ -42,9 +50,8 @@ import Server.Config
4250import Server.Html
4351import Server.Schema
4452import Squeal.PostgreSQL
45−import System.IO
4653import Text.Blaze.Html
47−import UnliftIO.Exception (try)
54+import UnliftIO.Exception (catch, displayException, handleAny, throwIO, try)
4855
4956type API =
5057 "github-webhook"
@@ -52,8 +59,8 @@ type API =
5259 :> GitHubSignedReqBody '[JSON] InstallationEvent
5360 :> Post '[JSON] ()
5461 :<|> GitHubEvent '[ 'WebhookPushEvent]
55− :> GitHubSignedReqBody '[JSON] PushEvent
56− :> Post '[JSON] ()
62+ :> GitHubSignedReqBody '[JSON] PushEvent
63+ :> Post '[JSON] ()
5764 )
5865 :<|> "submission"
5966 :> Capture "user name" String
@@ -68,17 +75,18 @@ type API =
6875 :> Post '[HTML] Markup
6976 :<|> "results" :> Capture "task name" String :> Get '[CSV, HTML] [Score]
7077
71−webhookInstallation :: MonadIO m => RepoWebhookEvent -> ((), InstallationEvent) -> m ()
78+webhookInstallation :: WithLog env Message m => RepoWebhookEvent -> ((), InstallationEvent) -> m ()
7279webhookInstallation _ ((), ev) =
73− liftIO $ do
74− print ev
75− hFlush stdout
76−
77−pendingStatus :: NewStatus
78−pendingStatus = NewStatus StatusPending Nothing Nothing Nothing
80+ logInfo . T.pack . show $ ev
7981
8082webhookPushEvent ::
81− (StaticPQ m, WithLog env Message m, MonadUnliftIO m, HasTasks m) =>
83+ ( StaticPQ m,
84+ MonadReader (ServerData m) m,
85+ MonadUnliftIO m,
86+ HasTasks m,
87+ MonadError ServerError m,
88+ WithLog env Message m
89+ ) =>
8290 RepoWebhookEvent ->
8391 ((), PushEvent) ->
8492 m ()
@@ -86,30 +94,42 @@ webhookPushEvent _ ((), ev) = do
8694 let repo = evPushRepository ev
8795 user = T.unpack . whUserLogin . evPushSender $ ev
8896 fullRepoName = whRepoFullName repo
89− [owner, repoName] = T.splitOn "/" fullRepoName
90− Just sha = evPushHeadSha ev
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
91101 tasks <- getTasks
92102 let task =
93103 M.lookupMax $
94104 M.takeWhileAntitone (repoName >) tasks
95105 case task of
96106 Just (name, TaskConfig {..}) | T.take (T.length name) repoName == name -> do
97− scheduleTask $ StatusUpdate (N owner) (N repoName) (N sha) pendingStatus
98− createSubmission $
99− Submission
100− user
101− (T.unpack fullRepoName)
102− (T.unpack name)
103− (T.unpack sha)
104− (Jsonb BuildScheduled)
105− scheduleTask $ Build prebuild dockerfile (N owner) (N repoName) (N sha)
106− liftIO $ do
107− print ev
108− hFlush stdout
107+ nowTime <- liftIO getCurrentTime
108+ if nowTime > deadline
109+ then do
110+ deadlineHasPassedStatus (N owner) (N repoName) (N sha)
111+ createSubmission $
112+ Submission
113+ user
114+ (T.unpack fullRepoName)
115+ (T.unpack name)
116+ (T.unpack sha)
117+ (Jsonb PastDeadline)
118+ else do
119+ schedulePendingStatus (N owner) (N repoName) (N sha)
120+ createSubmission $
121+ Submission
122+ user
123+ (T.unpack fullRepoName)
124+ (T.unpack name)
125+ (T.unpack sha)
126+ (Jsonb BuildScheduled)
127+ scheduleTask $ Build prebuild dockerfile (N owner) (N repoName) (N sha) timeoutMinutes
128+ logInfo . T.pack . show $ ev
109129 _ -> return ()
110130
111131retestSubmission ::
112− (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) =>
113133 String ->
114134 String ->
115135 String ->
@@ -128,9 +148,9 @@ retestSubmission owner repoName sha = do
128148 M.takeWhileAntitone (repoName' >) tasks
129149 case task of
130150 Just (name, TaskConfig {..}) | T.take (T.length name) repoName' == name -> do
131− scheduleTask $ StatusUpdate (N owner') (N repoName') (N sha') pendingStatus
151+ schedulePendingStatus (N owner') (N repoName') (N sha')
132152 updateSubmissionStatus fullRepoName sha BuildScheduled
133− scheduleTask $ Build prebuild dockerfile (N owner') (N repoName') (N sha')
153+ scheduleTask $ Build prebuild dockerfile (N owner') (N repoName') (N sha') timeoutMinutes
134154 _ -> return ()
135155 redirectToSubmission owner repoName sha
136156
@@ -148,13 +168,21 @@ newtype ServerM (schema :: SchemasType) a
148168 MonadUnliftIO
149169 )
150170
171+instance MonadError ServerError (ServerM schema) where
172+ throwError = throwIO
173+ catchError = catch
174+
151175server :: ServerT API (ServerM Schema)
152176server = (webhookInstallation :<|> webhookPushEvent) :<|> getSubmissionR :<|> retestSubmission :<|> getResults
153177
154−runServer :: IO ()
155−runServer = do
156− c@Config {..} <- getConfig
178+runServer :: FilePath -> IO ()
179+runServer config = do
180+ c@Config {..} <- getConfig config
157181 print c
182+ pem <- BS.readFile appPkPemPath
183+ appPkPem <- case readRsaPem pem of
184+ Right k -> return k
185+ Left e -> error (show e)
158186 auth <- mkInstallationAuth (AppAuth appId appPkPem) installationId
159187 ts <- newIORef mempty
160188 pool <- createConnectionPool databseUrl 1 0.5 10
@@ -164,29 +192,38 @@ runServer = do
164192 let context =
165193 gitPolyHubKey (return webhookSecret)
166194 :. EmptyContext
167− serverData = ServerData
168− { githubAppAuth = auth,
169− githubUserName = GithubUserName githubUsername,
170− githubAccessToken = GithubAccessToken personalAccessToken,
171− tasks = ts,
172− logger = cfilter ((logSeverity <=) . msgSeverity) richMessageAction,
173− baseUrl = baseSiteUrl,
174− dockerUrl = T.pack cfgDockerUrl
175− }
195+ serverData =
196+ ServerData
197+ { githubAppAuth = auth,
198+ tasks = ts,
199+ logger = cfilter ((logSeverity <=) . msgSeverity) richMessageAction,
200+ baseUrl = baseSiteUrl,
201+ dockerUrl = T.pack cfgDockerUrl,
202+ context = githubContext
203+ }
176204 repeatIfNotEmpty n f = f >>= \m -> do
177205 when (m == 0) $ liftIO $ threadDelay n
178206 repeatIfNotEmpty n f
179− _ <-
180− forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
207+ printingErrors =
208+ handleAny (logError . T.pack . displayException >=> const (return 0))
209+ usingConnectionPool pool
210+ . (`runReaderT` serverData)
211+ . unApp
212+ . handleAny (error . displayException)
213+ $ makeAllRunnable
214+ replicateM_ builderCount
215+ $ forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
181216 . usingConnectionPool pool
182217 . (`runReaderT` serverData)
183218 . unApp
184− $ runTasks @'[Build] 1
219+ . printingErrors
220+ $ runTasks @'[Build] 1
185221 _ <-
186222 forkIO . repeatIfNotEmpty (10 ^ (6 :: Int))
187223 . usingConnectionPool pool
188224 . (`runReaderT` serverData)
189225 . unApp
226+ . printingErrors
190227 $ runTasks @'[StatusUpdate] 5
191228 run port $ serveWithContext (Proxy @API) context $
192229 hoistServerWithContext
@@ -204,12 +241,11 @@ updateTasks ref path =
204241data ServerData m
205242 = ServerData
206243 { githubAppAuth :: !InstallationAuth,
207− githubUserName :: !GithubUserName,
208− githubAccessToken :: !GithubAccessToken,
209244 tasks :: IORef TasksConfig,
210245 logger :: LogAction m Message,
211− baseUrl :: String,
212− dockerUrl :: T.Text
246+ baseUrl :: T.Text,
247+ dockerUrl :: T.Text,
248+ context :: T.Text
213249 }
214250
215251newtype PolyGitHubKey = PolyGitHubKey (forall result. GitHubKey result)
@@ -220,11 +256,18 @@ gitPolyHubKey k = PolyGitHubKey (Servant.GitHub.Webhook.gitHubKey k)
220256instance HasContextEntry '[PolyGitHubKey] (GitHubKey result) where
221257 getContextEntry (PolyGitHubKey x :. _) = x
222258
223−instance MonadReader (ServerData n) m => GithubCloner m where
224−
225− getGithubAccessToken = asks githubAccessToken
226−
227− getGithubUserName = asks githubUserName
259+instance (MonadReader (ServerData n) m, MonadIO m, MonadError ServerError m) => GithubCloner m where
260+ getGithubInstallationToken =
261+ either
262+ (\e -> throwError err500 {errBody = BSCL.pack . displayException $ e})
263+ return
264+ <=< runExceptT
265+ $ do
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."
228271
229272instance (MonadIO m, MonadReader (ServerData n) m) => AppRequestable m where
230273 appRequest req = do
@@ -237,6 +280,71 @@ instance MonadReader (ServerData n) m => MonadHasBaseUrl m where
237280instance (MonadIO m, MonadReader (ServerData n) m) => HasTasks m where
238281 getTasks = asks tasks >>= liftIO . readIORef
239282
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
285+ h <- asks baseUrl
286+ c <- asks context
287+ scheduleTask $
288+ StatusUpdate
289+ nOwner
290+ nRepo
291+ nCommit
292+ ( NewStatus
293+ StatusPending
294+ (Just . URL $ h <> "/submission/" <> owner <> "/" <> repo <> "/" <> sha)
295+ (Just "⌛")
296+ (Just c)
297+ )
298+ scheduleTestedStatus TestResult {..} nOwner@(N owner) nRepo@(N repo) nCommit@(N sha) = do
299+ h <- asks baseUrl
300+ c <- asks context
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 " ❌"
304+ status =
305+ StatusUpdate
306+ nOwner
307+ nRepo
308+ nCommit
309+ NewStatus
310+ { newStatusState =
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
315+ }
316+ scheduleTask status
317+ scheduleFailedStatus _ nOwner@(N owner) nRepo@(N repo) nCommit@(N sha) = do
318+ h <- asks baseUrl
319+ c <- asks context
320+ let status =
321+ StatusUpdate
322+ nOwner
323+ nRepo
324+ nCommit
325+ NewStatus
326+ { newStatusState = StatusError,
327+ newStatusTargetUrl = Just . URL $ h <> "/submission/" <> owner <> "/" <> repo <> "/" <> sha,
328+ newStatusDescription = Just "Build failed ☠️",
329+ newStatusContext = Just c
330+ }
331+ scheduleTask status
332+ deadlineHasPassedStatus nOwner@(N owner) nRepo@(N repo) nCommit@(N sha) = do
333+ h <- asks baseUrl
334+ c <- asks context
335+ let status =
336+ StatusUpdate
337+ nOwner
338+ nRepo
339+ nCommit
340+ NewStatus
341+ { newStatusState = StatusError,
342+ newStatusTargetUrl = Just . URL $ h <> "/submission/" <> owner <> "/" <> repo <> "/" <> sha,
343+ newStatusDescription = Just "The deadline has passed ⏰",
344+ newStatusContext = Just c
345+ }
346+ scheduleTask status
347+
240348instance (MonadReader (ServerData n) m) => HasDockerInfo m where
241349 getDockerUrl = asks dockerUrl
242350
@@ -245,8 +353,9 @@ type TasksConfig = Map T.Text TaskConfig
245353data TaskConfig
246354 = TaskConfig
247355 { prebuild :: String,
248− dockerfile :: String
249− -- build :: T.Text
356+ dockerfile :: String,
357+ timeoutMinutes :: Int,
358+ deadline :: UTCTime
250359 }
251360 deriving stock (Show, Eq, Generic)
252361 deriving anyclass (FromJSON, ToJSON)
@@ -255,7 +364,6 @@ class HasTasks m where
255364 getTasks :: m TasksConfig
256365
257366instance HasLog (ServerData m) Message m where
258−
259367 getLogAction :: ServerData m -> LogAction m Message
260368 getLogAction = logger
261369 {-# INLINE getLogAction #-}
src/Server/Config.hs+72−35
@@ -2,62 +2,99 @@
22
33module Server.Config
44 ( Config (..),
5+ defaultConfig,
56 getConfig,
67 )
78where
89
910import Colog
10−import Crypto.PubKey.RSA (PrivateKey)
1111import Crypto.PubKey.RSA.Read
12−import Data.ByteString as BS
13−import qualified Data.ByteString.Char8 as BS8
14−import Data.Maybe
12+import Data.Aeson as A
13+import Data.ByteString.Char8 as BS
14+import Data.Text as T
15+import Data.Yaml
16+import GHC.Generics
1517import GitHub.Data.Apps
1618import GitHub.Data.Id
1719import GitHub.Data.Installations
18−import System.Environment
20+import Text.Read (readMaybe)
1921
2022data Config
2123 = Config
2224 { webhookSecret :: !ByteString,
2325 appId :: !(Id App),
24− appPkPem :: !PrivateKey,
26+ appPkPemPath :: !FilePath,
2527 port :: !Int,
2628 installationId :: !(Id Installation),
27− personalAccessToken :: !String,
28− githubUsername :: !String,
2929 tasksPath :: !FilePath,
3030 databseUrl :: !ByteString,
31− baseSiteUrl :: !String,
31+ baseSiteUrl :: !Text,
3232 cfgDockerUrl :: !String,
33− cfgDockerFile :: !FilePath,
34− logSeverity :: !Severity
33+ logSeverity :: !Severity,
34+ builderCount :: !Int,
35+ githubContext :: !Text
3536 }
36− deriving (Show)
37+ deriving (Show, Generic)
3738
38−getConfig :: IO Config
39−getConfig =
39+instance FromJSON ByteString where
40+ parseJSON (String s) = return . BS.pack . T.unpack $ s
41+ parseJSON _ = fail "not a string"
42+
43+instance ToJSON ByteString where
44+ toJSON = String . T.pack . BS.unpack
45+
46+instance FromJSON Severity where
47+ parseJSON (String s) = maybe (fail "oh no") return . readMaybe . T.unpack $ s
48+ parseJSON _ = fail "not a string"
49+
50+instance ToJSON Severity where
51+ toJSON = String . T.pack . show
52+
53+instance FromJSON Config
54+
55+instance ToJSON Config
56+
57+defaultConfig :: Config
58+defaultConfig =
4059 Config
41− <$> (maybe (error "no GITHUB_WEBHOOK_SECRET") BS8.pack <$> lookupEnv "GITHUB_WEBHOOK_SECRET")
42− <*> (maybe (error "no GITHUB_APP_ID") (Id . read) <$> lookupEnv "GITHUB_APP_ID")
43− <*> readPem
44− <*> (maybe 8080 read <$> lookupEnv "PORT")
45− <*> (maybe (error "no GITHUB_INSTALLATION_ID") (Id . read) <$> lookupEnv "GITHUB_INSTALLATION_ID")
46− <*> (fromMaybe (error "no PERSONAL_ACCESS_TOKEN") <$> lookupEnv "PERSONAL_ACCESS_TOKEN")
47− <*> (fromMaybe (error "no GITHUB_USERNAME") <$> lookupEnv "GITHUB_USERNAME")
48− <*> (fromMaybe (error "no TASKS") <$> lookupEnv "TASKS")
49− <*> (maybe (error "no DATABASE_URL") BS8.pack <$> lookupEnv "DATABASE_URL")
50− <*> (fromMaybe "http://localhost:8080" <$> lookupEnv "PORT")
51− <*> (fromMaybe "http://localhost:1234" <$> lookupEnv "DOCKER_URL")
52− <*> (fromMaybe "TmpDockerfile" <$> lookupEnv "DOCKER_FILE")
53− <*> (maybe Info read <$> lookupEnv "LOG_SEVERITY")
54− where
55− readPem :: IO PrivateKey
56− readPem = do
57− path <- fromMaybe (error "no app pk") <$> lookupEnv "GITHUB_APP_PK"
58− pem <- BS.readFile path
59− case readRsaPem pem of
60− Right k -> return k
61− Left e -> error (show e)
60+ { webhookSecret = "GITHUB SECRET",
61+ appId = Id 69,
62+ appPkPemPath = "PRIVATE KEY PATH",
63+ port = 8080,
64+ installationId = Id 69,
65+ tasksPath = "TASKS PATH",
66+ databseUrl = "host=localhost port=5432 dbname=sc-build connect_timeout=10",
67+ baseSiteUrl = "http://localhost:8080",
68+ cfgDockerUrl = "http://localhost:1234",
69+ logSeverity = Info,
70+ builderCount = 2,
71+ githubContext = "MY CI NAME"
72+ }
73+
74+getConfig :: FilePath -> IO Config
75+getConfig = decodeFileThrow
76+
77+-- getConfig :: IO Config
78+-- getConfig =
79+-- Config
80+-- <$> (maybe (error "no GITHUB_WEBHOOK_SECRET") BS8.pack <$> lookupEnv "GITHUB_WEBHOOK_SECRET")
81+-- <*> (maybe (error "no GITHUB_APP_ID") (Id . read) <$> lookupEnv "GITHUB_APP_ID")
82+-- <*> readPem
83+-- <*> (maybe 8080 read <$> lookupEnv "PORT")
84+-- <*> (maybe (error "no GITHUB_INSTALLATION_ID") (Id . read) <$> lookupEnv "GITHUB_INSTALLATION_ID")
85+-- <*> (fromMaybe (error "no TASKS") <$> lookupEnv "TASKS")
86+-- <*> (maybe (error "no DATABASE_URL") BS8.pack <$> lookupEnv "DATABASE_URL")
87+-- <*> (fromMaybe "http://localhost:8080" <$> lookupEnv "PORT")
88+-- <*> (fromMaybe <$> lookupEnv "DOCKER_URL")
89+-- <*> (fromMaybe "TmpDockerfile" <$> lookupEnv "DOCKER_FILE")
90+-- <*> (maybe Info read <$> lookupEnv "LOG_SEVERITY")
91+-- where
92+-- readPem :: IO PrivateKey
93+-- readPem = do
94+-- path <- fromMaybe (error "no app pk") <$> lookupEnv "GITHUB_APP_PK"
95+-- pem <- BS.readFile path
96+-- case readRsaPem pem of
97+-- Right k -> return k
98+-- Left e -> error (show e)
6299
63100deriving instance Show ReadRsaKeyError
src/Server/Html.hs+14−13
@@ -1,3 +1,5 @@
1+{-# OPTIONS_GHC -Wno-orphans #-}
2+
13module Server.Html
24 ( getSubmissionR,
35 MonadHasBaseUrl (..),
@@ -8,17 +10,13 @@ where
810
911import Clay as C
1012import Colog
11−import Control.Monad.IO.Class
1213import Control.Monad.IO.Unlift
13−import Data.Aeson hiding (Success)
14−import qualified Data.ByteString.Lazy.Char8 as BS
1514import Data.Foldable
1615import qualified Data.Map as M
1716import Data.String
1817import Data.Submission
1918import Data.Submission.Query
2019import qualified Data.Text as T
21−import GHC.Generics
2220import Server.Schema
2321import Squeal.PostgreSQL (Jsonb (..))
2422import Text.Blaze
@@ -32,24 +30,25 @@ getSubmissionR ::
3230 String ->
3331 String ->
3432 m Markup
35−getSubmissionR user repo sha = do
36− restartUrl <- (<> "/restart") <$> getUrl (user <> "/" <> repo) sha
33+getSubmissionR user repo sha' = do
34+ restartUrl <- (<> "/restart") <$> getUrl (user <> "/" <> repo) sha'
3735 let restartBuild = do
3836 H.form H.! A.action (fromString restartUrl) H.! A.method "post" $
3937 H.input H.! A.type_ "submit" H.! A.value "Restart test"
40− (titleText, inner) <- getSubmission (user <> "/" <> repo) sha >>= \case
38+ (titleText, inner) <- getSubmission (user <> "/" <> repo) sha' >>= \case
4139 Nothing ->
4240 return . ("Not found",) $
4341 H.h1 "404. not found. go away."
44− Just Submission {..} -> return . (repo,) $ do
42+ Just Submission {..} -> return . (problem,) $ do
4543 H.h1 $ toHtml problem
4644 H.h2 $ toHtml userName
45+ H.h3 . H.code . toHtml . take 7 $ sha
4746 case status of
4847 Jsonb BuildScheduled -> do
49− H.h3 "Waiting to build"
48+ H.h3 "Waiting to build ⌛"
5049 restartBuild
5150 Jsonb (SubmissionFailed err) -> do
52− H.h3 "Failed to build"
51+ H.h3 "Failed to build ☠️"
5352 restartBuild
5453 H.code . H.pre $ toHtml err
5554 Jsonb (SubmissionRun (TestResult tests)) -> do
@@ -57,11 +56,13 @@ getSubmissionR user repo sha = do
5756 passed = M.size . M.filter Prelude.id $ tests
5857 H.h3 . toHtml $
5958 "Tests have run: " <> show passed <> "/" <> show total
60− <> if passed == total then " ✅" else ""
59+ <> if passed == total then " 🎉" else " 🍌"
6160 restartBuild
6261 H.table $ flip M.foldMapWithKey tests $ \testName testPassed -> H.tr $ do
6362 (H.td H.! A.align "right") . H.p $ toHtml testName
6463 H.td $ if testPassed then "✅" else "❌"
64+ Jsonb PastDeadline ->
65+ H.h3 "This was submitted after the deadline ⏰"
6566 return . docTypeHtml $ do
6667 H.head $ do
6768 H.title $ toHtml titleText
@@ -94,12 +95,12 @@ getSubmissionR user repo sha = do
9495 H.body inner
9596
9697class MonadHasBaseUrl m where
97− getBaseUrl :: m String
98+ getBaseUrl :: m T.Text
9899
99100getUrl :: (MonadHasBaseUrl m, Monad m) => String -> String -> m String
100101getUrl fullName sha = do
101102 siteBase <- getBaseUrl
102− return $ siteBase <> "/submission/" <> fullName <> "/" <> sha
103+ return $ T.unpack siteBase <> "/submission/" <> fullName <> "/" <> sha
103104
104105redirectToSubmission ::
105106 (MonadHasBaseUrl m, Monad m) =>
src/Server/Schema.hs+3−1
@@ -6,13 +6,15 @@ module Server.Schema
66where
77
88import Server.Schema.V1
9+import Server.Schema.V2
910import Squeal.PostgreSQL
1011
1112type StaticPQ m = MonadPQ Schema m
1213
13−type Schema = SchemaV1
14+type Schema = SchemaV2
1415
1516migration :: AlignedList (Migration (Terminally PQ IO)) (Public '[]) Schema
1617migration =
1718 pureMigration schemaMigrationV1
19+ :>> pureMigration schemaMigrationV2
1820 :>> Done
src/Server/Schema/V1.hs+25−22
@@ -18,7 +18,8 @@ type SchemaV1 =
1818 ]
1919 :=> '[ "task" ::: 'NoDef :=> 'NotNull (PG Text),
2020 "payload" ::: 'NoDef :=> 'NotNull (PG (Jsonb Value)),
21− "creation_time" ::: 'Def :=> 'NotNull (PG UTCTime)
21+ "creation_time" ::: 'Def :=> 'NotNull (PG UTCTime),
22+ "started" ::: 'Def :=> 'NotNull (PG Bool)
2223 ]
2324 ),
2425 "submissions"
@@ -34,28 +35,30 @@ type SchemaV1 =
3435 ]
3536
3637schemaMigrationV1 :: Migration Definition (Public '[]) SchemaV1
37−schemaMigrationV1 = Migration
38− { name = "v1",
39− up =
40− createTable
41− #tasks
42− ( notNullable text `as` #task
43− :* notNullable jsonb `as` #payload
44− :* default_ now (notNullable timestampWithTimeZone) `as` #creation_time
45− )
46− (primaryKey (#task :* #payload) `as` #pk_task_payload)
47− >>> createTable
48− #submissions
49− ( notNullable text `as` #user_name
50− :* notNullable text `as` #repo_full_name
51− :* notNullable text `as` #problem
52− :* notNullable text `as` #sha
53− :* notNullable jsonb `as` #status
38+schemaMigrationV1 =
39+ Migration
40+ { name = "v1",
41+ up =
42+ createTable
43+ #tasks
44+ ( notNullable text `as` #task
45+ :* notNullable jsonb `as` #payload
46+ :* default_ now (notNullable timestampWithTimeZone) `as` #creation_time
47+ :* default_ false (notNullable bool) `as` #started
5448 )
55− (primaryKey (#repo_full_name :* #sha) `as` #pk_submission_repo_full_name_sha)
56− >>> createIndexes,
57− down = dropTable #tasks >>> dropTable #submissions
58− }
49+ (primaryKey (#task :* #payload) `as` #pk_task_payload)
50+ >>> createTable
51+ #submissions
52+ ( notNullable text `as` #user_name
53+ :* notNullable text `as` #repo_full_name
54+ :* notNullable text `as` #problem
55+ :* notNullable text `as` #sha
56+ :* notNullable jsonb `as` #status
57+ )
58+ (primaryKey (#repo_full_name :* #sha) `as` #pk_submission_repo_full_name_sha)
59+ >>> createIndexes,
60+ down = dropTable #tasks >>> dropTable #submissions
61+ }
5962 where
6063 createIndexes :: Definition sch sch
6164 createIndexes =
src/Server/Schema/V2.hsnew file+46
@@ -0,0 +1,46 @@
1+module Server.Schema.V2
2+ ( SchemaV2,
3+ schemaMigrationV2,
4+ )
5+where
6+
7+import Data.Aeson
8+import Data.Int
9+import Data.Submission
10+import Data.Text (Text)
11+import Data.Time
12+import Server.Schema.V1
13+import Squeal.PostgreSQL
14+
15+type SchemaV2 =
16+ Public
17+ '[ "tasks"
18+ ::: 'Table
19+ ( '[ "pk_task_payload" :=> 'PrimaryKey '["task", "payload"]
20+ ]
21+ :=> '[ "task" ::: 'NoDef :=> 'NotNull (PG Text),
22+ "payload" ::: 'NoDef :=> 'NotNull (PG (Jsonb Value)),
23+ "creation_time" ::: 'Def :=> 'NotNull (PG UTCTime),
24+ "started" ::: 'Def :=> 'NotNull (PG Bool),
25+ "failures" ::: 'Def :=> 'NotNull (PG Int32)
26+ ]
27+ ),
28+ "submissions"
29+ ::: 'Table
30+ ( '["pk_submission_repo_full_name_sha" :=> 'PrimaryKey '["repo_full_name", "sha"]]
31+ :=> '[ "user_name" ::: 'NoDef :=> 'NotNull (PG String),
32+ "repo_full_name" ::: 'NoDef :=> 'NotNull (PG String),
33+ "problem" ::: 'NoDef :=> 'NotNull (PG String),
34+ "sha" ::: 'NoDef :=> 'NotNull (PG String),
35+ "status" ::: 'NoDef :=> 'NotNull (PG (Jsonb SubmissionStatus))
36+ ]
37+ )
38+ ]
39+
40+schemaMigrationV2 :: Migration Definition SchemaV1 SchemaV2
41+schemaMigrationV2 =
42+ Migration
43+ { name = "v2",
44+ up = alterTable #tasks (addColumn #failures (default_ 0 $ notNullable int)),
45+ down = alterTable #tasks (dropColumn #failures)
46+ }
stack.yaml+4−3
@@ -17,7 +17,7 @@
1717#
1818# resolver: ./custom-snapshot.yaml
1919# resolver: https://example.com/snapshots/2018-01-01.yaml
20−resolver: lts-14.19
20+resolver: lts-14.27
2121
2222# User packages to be built.
2323# Various formats can be used as shown in the example below.
@@ -40,14 +40,15 @@ packages:
4040# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
4141#
4242extra-deps:
43−- github-0.24
43+- github-0.25
4444- binary-instances-1
4545- squeal-postgresql-0.5.2.0
4646- git: https://github.com/ilyakooo0/github-app
4747 commit: 0bede85ab53eb8f953cf6f4e58a756a205e88e0b
4848- servant-github-webhook-0.4.2.0
49−- docker-0.6.0.2
5049- http-client-0.5.14
50+- git: https://github.com/ilyakooo0/docker-hs
51+ commit: 5cfdc4d7facd7043f530bce4ce0f0c57c923b029
5152
5253# Override default flag values for local packages and extra-deps
5354# flags: {}
tasks.yaml+1
@@ -8,3 +8,4 @@ test-task:
88 COPY . src
99 WORKDIR /src
1010 ENTRYPOINT stack test --fast
11+ timeoutMinutes: 10