diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..af8fe42
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,36 @@
+# Run tests and dialyzer
+
+name: Test
+
+# Controls when the action will run. Triggers the workflow on push or pull request
+# events but only for the master branch
+on:
+  push:
+    branches: [ master ]
+  pull_request:
+    branches: [ master ]
+
+# A workflow run is made up of one or more jobs that can run sequentially or in parallel
+jobs:
+  linux:
+    name: Test on OTP ${{ matrix.otp_version }}
+    runs-on: ${{ matrix.os }}
+
+    strategy:
+      matrix:
+        otp_version: [21,22,23]
+        os: [ubuntu-latest]
+
+    container:
+      image: erlang:${{ matrix.otp_version }}
+
+    steps:
+      - uses: actions/checkout@v2
+      - name: Compile
+        run: make
+      - name: Test
+        run: make test
+      - name: XRef
+        run: make xref
+      - name: Dialyzer
+        run: make dialyzer
diff --git a/.gitignore b/.gitignore
index 089310a..2d93003 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ ebin
 deps
 src/mimetypes_scan.erl
 src/mimetypes_parse.erl
+rebar3
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index ea458d6..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-language: erlang
-sudo: false
-otp_release:
-  - 20.0
-  - 19.3
-
-jobs:
-  include:
-    - stage: deploy
-      otp_release: 20.0
-      script: skip
-      deploy:
-        provider: script
-        script: "bash -c 'source <(curl -s https://raw.githubusercontent.com/zotonic/hexpub/master/hexpub.sh)'"
-        skip_cleanup: true
-        on:
-          tags: true
-          all_branches: true
-install:
-  - wget https://s3.amazonaws.com/rebar3/rebar3 && chmod +x rebar3
-script: ./rebar3 eunit
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..9a3ebed
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,31 @@
+REBAR := ./rebar3
+REBAR_URL := https://s3.amazonaws.com/rebar3/rebar3
+ERL       ?= erl
+
+.PHONY: compile test
+
+all: compile
+
+compile: $(REBAR)
+	$(REBAR) compile
+
+shell: $(REBAR)
+	$(REBAR) shell
+
+test: $(REBAR)
+	$(REBAR) as test eunit
+
+dialyzer: $(REBAR)
+	$(REBAR) as test dialyzer
+
+xref: $(REBAR)
+	$(REBAR) as test xref
+
+clean: $(REBAR)
+	$(REBAR) clean
+
+./rebar3:
+	$(ERL) -noshell -s inets -s ssl \
+	  -eval '{ok, saved_to_file} = httpc:request(get, {"$(REBAR_URL)", []}, [], [{stream, "./rebar3"}])' \
+	  -s init stop
+	chmod +x ./rebar3
diff --git a/README.md b/README.md
index 1e80f58..a14294b 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 mimetypes
 =========
 
-[![Build Status](https://travis-ci.org/erlangpack/mimetypes.svg?branch=master)](https://travis-ci.org/erlangpack/mimetypes)
+![Test](https://github.com/erlangpack/mimetypes/workflows/Test/badge.svg)
 [![Hex pm](http://img.shields.io/hexpm/v/mimetypes.svg?style=flat)](https://hex.pm/packages/mimetypes)
 
 `mimetypes` is an Erlang library to fetch MIME extension/name mappings.
diff --git a/rebar.config b/rebar.config
index e904df3..0fb5f76 100644
--- a/rebar.config
+++ b/rebar.config
@@ -2,6 +2,35 @@
     {test, [
         {deps, [
             {proper, "1.2.0"}
+        ]},
+
+        {xref_checks, [
+            undefined_function_calls,
+            locals_not_used,
+            deprecated_function_calls
+        ]},
+
+        {xref_ignores, [
+            {mimetypes_parse, return_error, 2},
+            {mimetypes_map, ext_to_mimes, 1},
+            {mimetypes_map, exts, 0},
+            {mimetypes_map, mimes, 0},
+            {mimetypes_map, mimes, 1},
+            {mimetypes_map, module_info, 0},
+            {mimetypes_map, mime_to_exts, 1},
+            {mimetypes_map, modules, 0},
+
+            {mimetypes_disp, ext_to_mimes, 2},
+            {mimetypes_disp, exts, 1},
+            {mimetypes_disp, mime_to_exts, 2},
+            {mimetypes_disp, mimes, 1},
+            {mimetypes_disp, modules, 0}
+        ]},
+
+        {dialyzer, [
+          {warnings, [
+              no_return
+          ]}
         ]}
     ]}
 ]}.
diff --git a/rebar.lock b/rebar.lock
new file mode 100644
index 0000000..57afcca
--- /dev/null
+++ b/rebar.lock
@@ -0,0 +1 @@
+[].
diff --git a/src/mimetypes.erl b/src/mimetypes.erl
index 483d644..9215055 100644
--- a/src/mimetypes.erl
+++ b/src/mimetypes.erl
@@ -96,7 +96,7 @@ extension(Ext) ->
 %% [<<"text/plain">>]
 %% '''
 
--spec filename(Filename :: string()) -> [binary()].
+-spec filename(file:filename_all()) -> [binary()].
 filename(Filename) ->
     path_to_mimes(Filename).
 
@@ -138,12 +138,12 @@ extensions() ->
 %% @doc Create a new database of extension-mimetype mappings.
 %% The name 'default' is reserved.
 %% @end
--spec create(term()) -> ok.
+-spec create(term()) -> ok | exists.
 create(Database) when Database =/= default ->
     gen_server:call(?SERVER, {create, Database}).
 
 %% @doc Load a set of extension-mimetype mappings.
--spec load(term(), [{binary(), binary()}]) -> ok.
+-spec load(term(), [{binary(), binary()}]) -> ok | noexists.
 load(Database, Mappings) ->
     gen_server:call(?SERVER, {load, Database, Mappings}).
 
@@ -579,7 +579,7 @@ mimes_clause(Pairs) ->
 
 
 %% @private Compile a module.
--spec compile_forms(erlang_form(), compile_options()) -> {ok, atom, binary()}.
+-spec compile_forms(erlang_form(), compile_options()) -> {ok, atom(), binary()}.
 compile_forms(AbsCode, Opts) ->
     case compile:forms(AbsCode, Opts) of
         {ok, ModName, Binary} ->
diff --git a/test/mimetypes_tests.erl b/test/mimetypes_tests.erl
index f6b0d68..cd1ac29 100644
--- a/test/mimetypes_tests.erl
+++ b/test/mimetypes_tests.erl
@@ -47,9 +47,7 @@ prop_type() ->
             lists:all(fun (X) -> X end,
                       [ case mimetypes:extension(Ext) of
                             Types when is_list(Types) ->
-                                lists:member(Type, Types);
-                            Type1 ->
-                                Type1 =:= Type
+                                lists:member(Type, Types)
                         end || Ext <- mimetypes:extensions(Type) ])).
 
 prop_filename() ->
@@ -130,7 +128,7 @@ async_loader_test_() ->
         ]}.
 
 async_loader_onstart() ->
-    ?_assertEqual(undefined, mimetypes:ext_to_mimes(<<"foo2">>)).
+    ?_assertEqual([<<"application/octet-stream">>], mimetypes:ext_to_mimes(<<"foo2">>)).
 
 async_loader_wait() ->
     receive after 5000 -> ok end, %% @todo don't use an arbitrary time