Suppose you'd like to automate your GitLab server:
Every time a user forks a project from the DevOps GitLab group, add the CodeReviewers group as a member of the fork.
Or perhaps:
Every time a new user account is created, add them to the Newcomers GitLab group.
Or even maybe:
Every time a pushed commit fails a CI pipeline, raise a new Issue ticket against the project.
This is possible with GitLab hooks.
GitLab hooks allow 3rd party software to react to GitLab events. GitLab generates JSON data when events occur, which it sends to your 3rd party software. Hooks can be web services or executable file hooks uploaded to the GitLab server.
This post show how to write GitLab file hooks, using Haskell. Here's an example rule:
add_joe_to_webdev_projects :: Rule
=
add_joe_to_webdev_projects
matchIf"adds Joe Bloggs to projects forked from the web development group"
@ProjectCreate {} -> do
(\event"WebDev")
isForkFromNamespace (projectCreate_project_id event) @ProjectCreate {} -> do
(\event"joe_bloggs") addUserToProject (projectCreate_project_id event)
Event-Condition-Action (ECA) rules were initially designed for Active Database Systems (Dittrich et al, 1995) and event driven architectures.
An ECA rule has the general declarative syntax:
on
event if
condition do
event
Some GitLab event ECA examples are:
on event | if condition | do event |
---|---|---|
A new GitLab project created | a GitLab project's name is "foo" | add user "jane1" to the project |
A GitLab group has been created | always true | add user "jane1" to the group |
A GitLab group has been renamed | A GitLab project's new name is "bar" | add user "jane1" to the project |
The gitlab-haskell library includes an ECA API for GitLab Hooks.
Reactive GitLab programs are constructed using a receive
function:
receive :: [Rule] -> GitLab ()
Rules are constructed with match
or matchIf
:
class (FromJSON a) => SystemHook a where
match :: String -> (a -> GitLab ()) -> Rule
matchIf :: String -> (a -> GitLab Bool) -> (a -> GitLab ()) -> Rule
rule | event | condition | GitLab action |
---|---|---|---|
match |
An instance of SystemHook e.g. UserCreate |
n/a | (a -> GitLab ()) |
matchIf |
An instance of SystemHook e.g. ProjectUpdate |
(a -> GitLab Bool) |
(a -> GitLab ()) |
The match
rule fires if the a
argument is a GitLab event that matches
the event described in the JSON received from the GitLab server.
Firing a matchIf
rule also requires the additional condition function
to return True
.
SystemHook
typeclass instances
The a
type parameter for match
and matchIf
must be an instance of the
SystemHook
typeclass.
The instances are: ProjectCreate
, ProjectDestroy
, ProjectRename
,
ProjectTransfer
, ProjectUpdate
, UserAddToTeam
, UserUpdateForTeam
,
UserRemoveFromTeam
, UserCreate
, UserRemove
, KeyCreate
, KeyRemove
,
GroupCreate
, GroupRemove
, GroupRename
, NewGroupMember
,
GroupMemberRemove
, GroupMemberUpdate
, Push
and TagPush
.
Executing GitLab actions
The GitLab condition function (a -> GitLab Bool)
and the action
function (a -> GitLab ())
can perform any GitLab operations that the
gitlab-haskell library provides.
Some examples of GitLab functions in gitlab-haskell
are:
userProjects :: User -> GitLab (Maybe [Project])
branches :: Project -> GitLab [Branch]
projectCommits :: Project -> GitLab [Commit]
addUserToGroup :: Text -> AccessLevel -> User -> GitLab (Either Status Member)
projectOpenedIssues :: Project -> GitLab [Issue]
mergeRequests :: Project -> GitLab [MergeRequest]
repositoryFiles :: Project -> FilePath -> Text -> GitLab (Maybe RepositoryFile)
More about the gitlab-haskell
library is explained in this blog post.
Combining ECA rules with GitLab actions
An example of match
is:
rule1 :: Rule
=
rule1 "add Jane to every created project"
match @ProjectCreate{} -> do
(\eventJust jane <- searchUser "jane1"
addMemberToProject'
(projectCreate_project_id event)Reporter
(user_id jane))
The matchIf
predicate function is applied to the received GitLab event
data. If the condition function returns True
, the GitLab action
function is applied to the same event data.
An example of matchIf
if:
rule2 :: Rule
=
rule2 "add Jane to web-dev projects"
matchIf @ProjectCreate{} -> do
(\eventRight (Just project)) <- searchProjectId (projectCreate_project_id event)
(let name_space = namespace_path (namespace project)
return (name_space == "web-dev"))
@ProjectCreate{} -> do
(\eventJust jane <- searchUser "jane1"
addMemberToProject'
(projectCreate_project_id event)Reporter
(user_id jane))
The rule based API is documented here:
Combining ECA GitLab rules
Multiple rules can be combined. You can combine match
and matchIf
rules, and they can match on different SystemHook
instances:
myRules :: GitLab ()
= receive [rule1, rule2] myRules
To implement an executable file hook, use the runGitLab
function:
main :: IO ()
=
main
runGitLab
( defaultGitLabServer= "https://gitlab.example.com",
{ url = "abcde12345"
token
} ) myRules
When GitLab creates JSON for an event, it will run this program and
the receive
function reads the JSON from standard input and will then
try to fire each rule in sequence.
Documentation for runGitLab
is:
http://hackage.haskell.org/package/gitlab-haskell/docs/GitLab.html
Once you've compiled your Haskell code to an executable file, on the
GitLab server copy it to
/opt/gitlab/embedded/service/gitlab-rails/file_hooks
I recommend compiling the Haskell source on the GitLab server to ensure all linked shared libraries can be found at runtime.
Navigate to: https://<your GitLab domain>/admin/hooks
, you should see
your file hook listed.
For example, below shows a file hook called gitlab-hooks-hw-exe
in
operation: