I happen to have written a generic package for doing things on save, such as running (shell) commands not too long ago.
It supports shell commands, interactive commands and generic Emacs Lisp code. When using shell commands, you get a reporting buffer containing output of the entire history and can configure on which condition it should pop up (like for example, only when the command failed with a non-zero status code). Finally, it supports format codes in the commands which are tremendously useful for calls that need to know the current file it's running upon or parts of it. Since it uses a customizable buffer-local variable to determine how to do its job, you can customize it globally, per mode, per directory, per file or even just interactively, unlike things like
M-x compile where you start with a default and need to change it for every "project" for each new Emacs session.
I believe it to be the simplest solution for a workflow approximating full CI solutions. If there's anything obvious missing from it, feel free to open an issue on its bug tracker.