on master help sign in
14 files changed+219−51
since splitsnapshots
unifiedsplit
README.md+47 app/Main.hs+23−6 package.yaml+2 src/Control/HasGithubStatus.hs+1 src/Control/Task/Scheduler.hs+4−1 src/Control/Task/Scheduler/Query.hs+6−1 src/Data/Submission.hs+1 src/Data/Tasks/Build.hs+20−8 src/Server.hs+55−25 src/Server/Config.hs+2−4 src/Server/Html.hs+7−4 src/Server/Schema.hs+3−1 src/Server/Schema/V2.hs+46 stack.yaml+2−1
README.md+47
@@ -4,6 +4,53 @@ Need both manual weebohook and app.
44
55Installation id needs to be configured manually.
66
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+
754For macos docker API:
855
956```bash
app/Main.hs+23−6
@@ -1,13 +1,30 @@
11module Main (main) where
22
33import qualified Data.ByteString.Char8 as BS
4−import Data.Yaml
4+import Data.Yaml (encode)
5+import Options.Applicative
56import Server
67import Server.Config
7−import System.Environment
88
99main :: IO ()
10−main = getArgs >>= \case
11− [] -> runServer
12− ["default"] -> putStrLn . BS.unpack . encode $ defaultConfig
13− xs -> error . show $ xs
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+2
@@ -52,6 +52,7 @@ library:
5252 - transformers
5353 - cassava
5454 - http-media
55+ - utf8-string
5556
5657executables:
5758 sc-build-exe:
@@ -65,6 +66,7 @@ executables:
6566 - sc-build
6667 - bytestring
6768 - yaml
69+ - optparse-applicative
6870 sc-build-migrations:
6971 main: Main.hs
7072 source-dirs: migrations
src/Control/HasGithubStatus.hs+1
@@ -11,3 +11,4 @@ class HasGithubStatus m where
1111 schedulePendingStatus :: WithLog env Message m => Name Owner -> Name Repo -> Name Commit -> m ()
1212 scheduleTestedStatus :: WithLog env Message m => TestResult -> Name Owner -> Name Repo -> Name Commit -> m ()
1313 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+4−1
@@ -21,6 +21,7 @@ import Data.Word
2121import GHC.TypeLits
2222import Server.Schema
2323import Squeal.PostgreSQL
24+import UnliftIO.Exception
2425
2526scheduleTask ::
2627 (StaticPQ m, WithLog env Message m, Task t s, MonadUnliftIO m) =>
@@ -66,7 +67,9 @@ instance
6667 <> e
6768 A.Success t -> do
6869 logInfo . T.pack $ "running task " <> task <> " with " <> show t <> " created at " <> show initialStartTime
69− 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
7073 when (r == Failure) $ logWarning . T.pack $ "Task " <> task <> " failed"
7174 case r of
7275 Success -> completeTaskQuery t
src/Control/Task/Scheduler/Query.hs+6−1
@@ -41,6 +41,7 @@ scheduleTaskQuery task = do
4141 :* Set (param @2) `as` #payload
4242 :* Default `as` #creation_time
4343 :* Default `as` #started
44+ :* Default `as` #failures
4445 )
4546 ( OnConflict
4647 (OnConstraint #pk_task_payload)
@@ -87,6 +88,7 @@ rescheduleTaskQuery payload = do
8788 #tasks
8889 ( Default `as` #creation_time
8990 :* Default `as` #started
91+ :* Set (#failures + 1) `as` #failures
9092 )
9193 (param @1 .== #task .&& param @2 .== #payload)
9294 dbWrite (const $ return ()) $
@@ -101,7 +103,7 @@ pickTasksQuery ::
101103 Word64 ->
102104 m [PickedTask]
103105pickTasksQuery tasks limitCount = do
104− logDebug "reading tasks"
106+ logDebug $ "reading tasks " <> T.unwords (T.pack <$> tasks)
105107 let query :: Query_ Schema () PickedTask
106108 query =
107109 select_
@@ -112,7 +114,10 @@ pickTasksQuery tasks limitCount = do
112114 ( from (table #tasks)
113115 & where_ (#task `in_` (literal <$> tasks))
114116 & where_ (not_ . notNull $ #started)
117+ & where_ (#failures .< 20)
118+ & orderBy [#creation_time & Asc]
115119 & limit limitCount
120+ & orderBy [#creation_time & Desc]
116121 )
117122 startedMan :: Manipulation_ Schema (String, Jsonb Value) ()
118123 startedMan =
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+20−8
@@ -16,6 +16,7 @@ import Control.Monad.IO.Unlift
1616import Control.Task
1717import Data.Aeson hiding (Success)
1818import qualified Data.ByteString.Lazy.Char8 as BS
19+import qualified Data.ByteString.Lazy.UTF8 as BS
1920import Data.String
2021import Data.Submission
2122import Data.Submission.Query
@@ -54,7 +55,7 @@ instance Task Build "build-repo" where
5455 (shellCode, _, shellErr) <- readProcess . setWorkingDir dir $ shell preProcessShell
5556 case shellCode of
5657 ExitFailure n -> do
57− let err = BS.unpack shellErr
58+ let err = BS.toString shellErr
5859 logError . T.pack $
5960 "preprocessing command for repo " <> show fullRepoName <> " at sha "
6061 <> show sha
@@ -90,7 +91,7 @@ instance Task Build "build-repo" where
9091 ExceptT $ D.buildImageFromDockerfile (D.defaultBuildOpts imageName) absDir
9192 ExceptT $ D.createContainer opts Nothing
9293 dockerRes <- fmap (either id id)
93− . race (liftIO (threadDelay (timeoutMinutes * 60000000)) >> return (ExitFailure 1, "Time out"))
94+ . race (liftIO (threadDelay (timeoutMinutes * 60000000)) >> return (ExitFailure 1, "Time out", "Time out"))
9495 $ flip
9596 finally
9697 ( runDocker $
@@ -114,10 +115,21 @@ instance Task Build "build-repo" where
114115 tail = D.All
115116 }
116117 cid
117− return (buildCode, buildOut)
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)
118130 case dockerRes of
119− (ExitFailure n, buildOut) -> do
120− let err = BS.unpack buildOut
131+ (ExitFailure n, _, buildErr) -> do
132+ let err = BS.toString buildErr
121133 logError . T.pack $
122134 "test command for repo " <> show fullRepoName <> " at sha "
123135 <> show sha
@@ -127,10 +139,10 @@ instance Task Build "build-repo" where
127139 <> err
128140 scheduleFailedStatus err owner repoName sha
129141 updateSubmissionStatus fullRepoName sha' (SubmissionFailed err)
130− (ExitSuccess, buildOut) ->
142+ (ExitSuccess, buildOut, _) ->
131143 case eitherDecode' (BS.dropWhile (/= '{') buildOut) of
132144 Left err' -> do
133− let err = err' <> " " <> BS.unpack buildOut
145+ let err = err' <> " " <> BS.toString buildOut
134146 logError . T.pack $
135147 "could not decode test result from repo " <> show fullRepoName
136148 <> " at sha "
@@ -138,7 +150,7 @@ instance Task Build "build-repo" where
138150 <> ": "
139151 <> err
140152 scheduleFailedStatus err owner repoName sha
141− updateSubmissionStatus fullRepoName sha' (SubmissionFailed err)
153+ updateSubmissionStatus fullRepoName sha' (SubmissionFailed (err <> BS.toString buildOut))
142154 Right testResult@TestResult {..} -> do
143155 scheduleTestedStatus testResult owner repoName sha
144156 updateSubmissionStatus fullRepoName sha' (SubmissionRun testResult)
src/Server.hs+55−25
@@ -30,6 +30,7 @@ import Data.Submission.Query
3030import Data.Tasks.Build
3131import Data.Tasks.StatusUpdate
3232import qualified Data.Text as T
33+import Data.Time.Clock
3334import Data.Yaml
3435import GHC.Generics
3536import GitHub hiding (Accept)
@@ -49,7 +50,6 @@ import Server.Config
4950import Server.Html
5051import Server.Schema
5152import Squeal.PostgreSQL
52−import System.IO
5353import Text.Blaze.Html
5454import UnliftIO.Exception (catch, displayException, handleAny, throwIO, try)
5555
@@ -75,14 +75,18 @@ type API =
7575 :> Post '[HTML] Markup
7676 :<|> "results" :> Capture "task name" String :> Get '[CSV, HTML] [Score]
7777
78−webhookInstallation :: MonadIO m => RepoWebhookEvent -> ((), InstallationEvent) -> m ()
78+webhookInstallation :: WithLog env Message m => RepoWebhookEvent -> ((), InstallationEvent) -> m ()
7979webhookInstallation _ ((), ev) =
80− liftIO $ do
81− print ev
82− hFlush stdout
80+ logInfo . T.pack . show $ ev
8381
8482webhookPushEvent ::
85− (StaticPQ m, MonadReader (ServerData m) m, MonadUnliftIO m, HasTasks m, MonadError ServerError 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+ ) =>
8690 RepoWebhookEvent ->
8791 ((), PushEvent) ->
8892 m ()
@@ -100,18 +104,28 @@ webhookPushEvent _ ((), ev) = do
100104 M.takeWhileAntitone (repoName >) tasks
101105 case task of
102106 Just (name, TaskConfig {..}) | T.take (T.length name) repoName == name -> do
103− schedulePendingStatus (N owner) (N repoName) (N sha)
104− createSubmission $
105− Submission
106− user
107− (T.unpack fullRepoName)
108− (T.unpack name)
109− (T.unpack sha)
110− (Jsonb BuildScheduled)
111− scheduleTask $ Build prebuild dockerfile (N owner) (N repoName) (N sha) timeoutMinutes
112− liftIO $ do
113− print ev
114− 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
115129 _ -> return ()
116130
117131retestSubmission ::
@@ -161,9 +175,9 @@ instance MonadError ServerError (ServerM schema) where
161175server :: ServerT API (ServerM Schema)
162176server = (webhookInstallation :<|> webhookPushEvent) :<|> getSubmissionR :<|> retestSubmission :<|> getResults
163177
164−runServer :: IO ()
165−runServer = do
166− c@Config {..} <- getConfig
178+runServer :: FilePath -> IO ()
179+runServer config = do
180+ c@Config {..} <- getConfig config
167181 print c
168182 pem <- BS.readFile appPkPemPath
169183 appPkPem <- case readRsaPem pem of
@@ -278,7 +292,7 @@ instance (MonadReader (ServerData m) m, StaticPQ m, MonadUnliftIO m) => HasGithu
278292 ( NewStatus
279293 StatusPending
280294 (Just . URL $ h <> "/submission/" <> owner <> "/" <> repo <> "/" <> sha)
281− Nothing
295+ (Just "⌛")
282296 (Just c)
283297 )
284298 scheduleTestedStatus TestResult {..} nOwner@(N owner) nRepo@(N repo) nCommit@(N sha) = do
@@ -286,7 +300,7 @@ instance (MonadReader (ServerData m) m, StaticPQ m, MonadUnliftIO m) => HasGithu
286300 c <- asks context
287301 let total = M.size tests
288302 passed = M.size . M.filter id $ tests
289− description = T.pack $ show passed <> "/" <> show total
303+ description = T.pack $ show passed <> "/" <> show total <> if passed == total then " ✅" else " ❌"
290304 status =
291305 StatusUpdate
292306 nOwner
@@ -311,7 +325,22 @@ instance (MonadReader (ServerData m) m, StaticPQ m, MonadUnliftIO m) => HasGithu
311325 NewStatus
312326 { newStatusState = StatusError,
313327 newStatusTargetUrl = Just . URL $ h <> "/submission/" <> owner <> "/" <> repo <> "/" <> sha,
314− newStatusDescription = Just "Build failed",
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 ⏰",
315344 newStatusContext = Just c
316345 }
317346 scheduleTask status
@@ -325,7 +354,8 @@ data TaskConfig
325354 = TaskConfig
326355 { prebuild :: String,
327356 dockerfile :: String,
328− timeoutMinutes :: Int
357+ timeoutMinutes :: Int,
358+ deadline :: UTCTime
329359 }
330360 deriving stock (Show, Eq, Generic)
331361 deriving anyclass (FromJSON, ToJSON)
src/Server/Config.hs+2−4
@@ -30,7 +30,6 @@ data Config
3030 databseUrl :: !ByteString,
3131 baseSiteUrl :: !Text,
3232 cfgDockerUrl :: !String,
33− cfgDockerFile :: !FilePath,
3433 logSeverity :: !Severity,
3534 builderCount :: !Int,
3635 githubContext :: !Text
@@ -67,14 +66,13 @@ defaultConfig =
6766 databseUrl = "host=localhost port=5432 dbname=sc-build connect_timeout=10",
6867 baseSiteUrl = "http://localhost:8080",
6968 cfgDockerUrl = "http://localhost:1234",
70− cfgDockerFile = "TmpDockerfile",
7169 logSeverity = Info,
7270 builderCount = 2,
7371 githubContext = "MY CI NAME"
7472 }
7573
76−getConfig :: IO Config
77−getConfig = decodeFileThrow "config.yaml"
74+getConfig :: FilePath -> IO Config
75+getConfig = decodeFileThrow
7876
7977-- getConfig :: IO Config
8078-- getConfig =
src/Server/Html.hs+7−4
@@ -39,15 +39,16 @@ getSubmissionR user repo sha' = do
3939 Nothing ->
4040 return . ("Not found",) $
4141 H.h1 "404. not found. go away."
42− Just Submission {..} -> return . (repo,) $ do
42+ Just Submission {..} -> return . (problem,) $ do
4343 H.h1 $ toHtml problem
4444 H.h2 $ toHtml userName
45+ H.h3 . H.code . toHtml . take 7 $ sha
4546 case status of
4647 Jsonb BuildScheduled -> do
47− H.h3 "Waiting to build"
48+ H.h3 "Waiting to build ⌛"
4849 restartBuild
4950 Jsonb (SubmissionFailed err) -> do
50− H.h3 "Failed to build"
51+ H.h3 "Failed to build ☠️"
5152 restartBuild
5253 H.code . H.pre $ toHtml err
5354 Jsonb (SubmissionRun (TestResult tests)) -> do
@@ -55,11 +56,13 @@ getSubmissionR user repo sha' = do
5556 passed = M.size . M.filter Prelude.id $ tests
5657 H.h3 . toHtml $
5758 "Tests have run: " <> show passed <> "/" <> show total
58− <> if passed == total then " ✅" else ""
59+ <> if passed == total then " 🎉" else " 🍌"
5960 restartBuild
6061 H.table $ flip M.foldMapWithKey tests $ \testName testPassed -> H.tr $ do
6162 (H.td H.! A.align "right") . H.p $ toHtml testName
6263 H.td $ if testPassed then "✅" else "❌"
64+ Jsonb PastDeadline ->
65+ H.h3 "This was submitted after the deadline ⏰"
6366 return . docTypeHtml $ do
6467 H.head $ do
6568 H.title $ toHtml titleText
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/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+2−1
@@ -46,8 +46,9 @@ extra-deps:
4646- git: https://github.com/ilyakooo0/github-app
4747 commit: 0bede85ab53eb8f953cf6f4e58a756a205e88e0b
4848- servant-github-webhook-0.4.2.0
49−- docker-0.6.0.3
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: {}