@@ -11,12 +11,12 @@ import qualified Data.Text as T
11
11
import qualified Data.Text.Encoding as TE
12
12
import qualified Network.HTTP.Client as HTTP
13
13
import Network.HTTP.Client.TLS (tlsManagerSettings )
14
- import System.Environment (getEnv )
14
+ import System.Environment (getEnv , lookupEnv , setEnv )
15
15
import Network.HTTP.Types.Status (Status (.. ))
16
16
import Data.Aeson.Decoding (eitherDecode )
17
17
import qualified Data.Text as Text
18
18
import Utils (getCurrentCommit , logError , logDebug )
19
- import Types (AppState )
19
+ import Types (AppState ( .. ), GithubClient ( .. ) )
20
20
21
21
-- Define the data types for the status update
22
22
data StatusRequest = StatusRequest
@@ -33,70 +33,105 @@ newtype InstallationTokenResponse = InstallationTokenResponse
33
33
} deriving (Show , Generic )
34
34
deriving anyclass (FromJSON )
35
35
36
- updateCommitStatus :: MonadIO m => AppState -> StatusRequest -> m ()
37
- updateCommitStatus appState statusRequest = liftIO do
36
+ getClient :: AppState -> IO GithubClient
37
+ getClient appState = do
38
+ mClient <- readIORef appState. githubClient
39
+ case mClient of
40
+ Just client -> pure client
41
+ Nothing -> do
42
+ client <- initClient appState
43
+ writeIORef appState. githubClient $ Just client
44
+ pure client
45
+
46
+ initClient :: AppState -> IO GithubClient
47
+ initClient appState = do
38
48
-- Load environment variables
49
+ apiUrl <- fromMaybe " https://api.github.com" <$> lookupEnv " GITHUB_API_URL"
39
50
appId <- getEnv " GITHUB_APP_ID"
40
51
installationId <- getEnv " GITHUB_INSTALLATION_ID"
41
52
privateKeyStr <- getEnv " GITHUB_APP_PRIVATE_KEY"
42
53
owner <- getEnv " GITHUB_REPOSITORY_OWNER"
43
54
repo <- getEnv " GITHUB_REPOSITORY"
44
-
45
- sha <- getCurrentCommit appState
46
-
47
- let privateKeyBytes = encodeUtf8 $ Text. replace " |" " \n " $ toText privateKeyStr
48
- let privateKey = fromMaybe (error " Invalid github key" ) $ readRsaSecret privateKeyBytes
49
-
50
- -- Create the JWT token
51
- now <- getPOSIXTime
52
- let claims = mempty { iss = stringOrURI $ T. pack appId
53
- , iat = numericDate now
54
- , exp = numericDate (now + 5 * 60 )
55
- }
56
- let jwt = encodeSigned (EncodeRSAPrivateKey privateKey) (mempty { alg = Just RS256 }) claims
57
-
58
55
-- Prepare the HTTP manager
59
56
manager <- HTTP. newManager tlsManagerSettings
60
57
61
- -- Get the installation access token
62
- let installUrl = " https://api.github.com/app/installations/" ++ installationId ++ " /access_tokens"
63
- initRequest <- HTTP. parseRequest installUrl
64
- let request = initRequest
65
- { HTTP. method = " POST"
66
- , HTTP. requestHeaders =
67
- [ (" Authorization" , " Bearer " <> TE. encodeUtf8 jwt)
68
- , (" Accept" , " application/vnd.github.v3+json" )
69
- , (" User-Agent" , " restaumatic-bot" )
70
- ]
71
- }
72
- response <- HTTP. httpLbs request manager
73
- let mTokenResponse = eitherDecode @ InstallationTokenResponse (HTTP. responseBody response)
74
- case mTokenResponse of
75
- Left err -> do
76
- logError appState $ " CommitStatus: Failed to parse installation token response: " <> show err
77
- logError appState $ " CommitStatus: Response: " <> decodeUtf8 response. responseBody
78
- exitFailure
79
- Right tokenResponse -> do
80
- let accessToken = tokenResponse. token
58
+ let createToken = do
59
+ let privateKeyBytes = encodeUtf8 $ Text. replace " |" " \n " $ toText privateKeyStr
60
+ let privateKey = fromMaybe (error " Invalid github key" ) $ readRsaSecret privateKeyBytes
61
+
62
+ -- Create the JWT token
63
+ now <- getPOSIXTime
64
+ let claims = mempty { iss = stringOrURI $ T. pack appId
65
+ , iat = numericDate now
66
+ , exp = numericDate (now + 5 * 60 )
67
+ }
68
+ let jwt = encodeSigned (EncodeRSAPrivateKey privateKey) (mempty { alg = Just RS256 }) claims
81
69
82
- -- Prepare the status update request
83
- let statusUrl = " https://api.github.com/repos/ " ++ owner ++ " /" ++ repo ++ " /statuses/ " ++ toString sha
84
- initStatusRequest <- HTTP. parseRequest statusUrl
85
- let statusReq = initStatusRequest
70
+ -- Get the installation access token
71
+ let installUrl = apiUrl <> " /app/installations/ " ++ installationId ++ " /access_tokens "
72
+ initRequest <- HTTP. parseRequest installUrl
73
+ let request = initRequest
86
74
{ HTTP. method = " POST"
87
75
, HTTP. requestHeaders =
88
- [ (" Authorization" , " Bearer " <> TE. encodeUtf8 accessToken )
76
+ [ (" Authorization" , " Bearer " <> TE. encodeUtf8 jwt )
89
77
, (" Accept" , " application/vnd.github.v3+json" )
90
- , (" Content-Type" , " application/json" )
91
78
, (" User-Agent" , " restaumatic-bot" )
92
79
]
93
- , HTTP. requestBody = HTTP. RequestBodyLBS $ encode statusRequest
94
80
}
95
- statusResponse <- HTTP. httpLbs statusReq manager
96
- if statusResponse. responseStatus. statusCode == 201
97
- then
98
- logDebug appState " Commit status updated successfully"
99
- else do
100
- logError appState $ " CommitStatus: Failed to update commit status: " <> show statusResponse
101
- logError appState $ " CommitStatus: Response: " <> decodeUtf8 response. responseBody
102
- exitFailure
81
+ response <- HTTP. httpLbs request manager
82
+ let mTokenResponse = eitherDecode @ InstallationTokenResponse (HTTP. responseBody response)
83
+ case mTokenResponse of
84
+ Left err -> do
85
+ logError appState $ " CommitStatus: Failed to parse installation token response: " <> show err
86
+ logError appState $ " CommitStatus: Response: " <> decodeUtf8 response. responseBody
87
+
88
+ -- FIXME: handle the error better
89
+ exitFailure
90
+ Right tokenResponse ->
91
+ pure tokenResponse. token
92
+
93
+ -- Try to read token from environment variable
94
+ -- Otherwise generate a new one, and set env for future uses (also in child processes)
95
+ accessToken <- lookupEnv " _taskrunner_github_access_token" >>= \ case
96
+ Just token -> pure $ T. pack token
97
+ Nothing -> do
98
+ token <- createToken
99
+ setEnv " _taskrunner_github_access_token" $ T. unpack token
100
+ pure token
101
+
102
+ pure $ GithubClient { apiUrl = T. pack apiUrl
103
+ , appId = T. pack appId
104
+ , installationId = T. pack installationId
105
+ , privateKey = T. pack privateKeyStr
106
+ , owner = T. pack owner
107
+ , repo = T. pack repo
108
+ , manager = manager
109
+ , accessToken = accessToken
110
+ }
111
+
112
+ updateCommitStatus :: MonadIO m => AppState -> StatusRequest -> m ()
113
+ updateCommitStatus appState statusRequest = liftIO do
114
+ client <- getClient appState
115
+ sha <- getCurrentCommit appState
116
+
117
+ -- Prepare the status update request
118
+ let statusUrl = toString client. apiUrl <> " /repos/" ++ toString client. owner ++ " /" ++ toString client. repo ++ " /statuses/" ++ toString sha
119
+ initStatusRequest <- HTTP. parseRequest statusUrl
120
+ let statusReq = initStatusRequest
121
+ { HTTP. method = " POST"
122
+ , HTTP. requestHeaders =
123
+ [ (" Authorization" , " Bearer " <> TE. encodeUtf8 client. accessToken)
124
+ , (" Accept" , " application/vnd.github.v3+json" )
125
+ , (" Content-Type" , " application/json" )
126
+ , (" User-Agent" , " restaumatic-bot" )
127
+ ]
128
+ , HTTP. requestBody = HTTP. RequestBodyLBS $ encode statusRequest
129
+ }
130
+ statusResponse <- HTTP. httpLbs statusReq client. manager
131
+ if statusResponse. responseStatus. statusCode == 201
132
+ then
133
+ logDebug appState " Commit status updated successfully"
134
+ else do
135
+ logError appState $ " CommitStatus: Failed to update commit status: " <> show statusResponse
136
+ logError appState $ " CommitStatus: Response: " <> decodeUtf8 statusResponse. responseBody
137
+ exitFailure
0 commit comments