diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..918d5a490920a60f9f3b5637381b1ff842aebecd
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,70 @@
+name: CI
+on:
+    push:
+        branches:
+        - master
+    pull_request:
+        paths-ignore:
+        - 'doc/*.rst'
+    schedule:
+        - cron:  '17 3 * * 0'
+
+jobs:
+    flake8:
+        name: Flake8
+        runs-on: ubuntu-latest
+        steps:
+        -   uses: actions/checkout@v2
+        -
+            uses: actions/setup-python@v1
+            with:
+                python-version: '3.x' 
+        -   name: "Main Script"
+            run: |
+                curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/prepare-and-run-flake8.sh
+                . ./prepare-and-run-flake8.sh ./grudge ./examples ./test
+
+    pytest2:
+        name: Pytest on Py2
+        runs-on: ubuntu-latest
+        steps:
+        -   uses: actions/checkout@v2
+        -   name: "Main Script"
+            run: |
+                sed 's/python=3/python=2.7/' .test-conda-env-py3.yml > .test-conda-env-py2.yml
+                grep -v mpi4py .test-conda-env-py2.yml > .test-conda-env.yml
+                CONDA_ENVIRONMENT=.test-conda-env.yml
+                curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/build-and-test-py-project-within-miniconda.sh
+                . ./build-and-test-py-project-within-miniconda.sh
+
+    pytest3:
+        name: Pytest on Py3
+        runs-on: ubuntu-latest
+        steps:
+        -   uses: actions/checkout@v2
+        -   name: "Main Script"
+            run: |
+                grep -v mpi4py .test-conda-env-py3.yml > .test-conda-env.yml
+                CONDA_ENVIRONMENT=.test-conda-env.yml
+                curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/build-and-test-py-project-within-miniconda.sh
+                . ./build-and-test-py-project-within-miniconda.sh
+
+    pyexamples3:
+        name: Examples on Py3
+        runs-on: ubuntu-latest
+        steps:
+        -   uses: actions/checkout@v2
+        -   name: "Main Script"
+            run: |
+                sudo apt-get update
+                sudo apt-get install openmpi-bin libopenmpi-dev
+                grep -v symengine .test-conda-env-py3.yml > .test-conda-env.yml
+                CONDA_ENVIRONMENT=.test-conda-env.yml
+                curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/build-py-project-within-miniconda.sh
+                . ./build-py-project-within-miniconda.sh
+                curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/run-examples.sh
+                . ./run-examples.sh
+
+# vim: sw=4
+
+
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 57caec74f47a8cc99b8c94ba1c6dfd460097715f..eb3a2e4aff06ef799b5a2018685b07e5d149646f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -95,7 +95,7 @@ Documentation:
 Flake8:
   script:
   - curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/prepare-and-run-flake8.sh
-  - ". ./prepare-and-run-flake8.sh grudge test"
+  - ". ./prepare-and-run-flake8.sh grudge examples test"
   tags:
   - python3
   except:
diff --git a/.test-conda-env-py3.yml b/.test-conda-env-py3.yml
new file mode 100644
index 0000000000000000000000000000000000000000..675a8dffad07ec92a1ea14d99bdb34445741a7a2
--- /dev/null
+++ b/.test-conda-env-py3.yml
@@ -0,0 +1,28 @@
+name: test-conda-env-py3
+channels:
+- conda-forge
+- defaults
+dependencies:
+- git
+- conda-forge::numpy
+
+# to avoid conflict with system openmpi's libhwloc
+- libhwloc=1
+- pocl
+
+- islpy
+- pyopencl
+- python=3
+- gmsh
+
+- pip
+- pip:
+    - git+https://github.com/inducer/pytools
+    - git+https://github.com/inducer/pymbolic
+    - git+https://github.com/inducer/loopy
+    - git+https://github.com/inducer/meshmode
+    - git+https://github.com/inducer/dagrt
+    - git+https://github.com/inducer/leap
+    - git+https://github.com/inducer/pyvisfile
+    - mpi4py
+    - pymetis
diff --git a/README.rst b/README.rst
index 7c6417bd376cd933191727eba0257963ed60c236..1862809be2f4c1dfe0136fee6bb9070d2ae4f85b 100644
--- a/README.rst
+++ b/README.rst
@@ -1,6 +1,18 @@
 grudge
 ======
 
+.. image:: https://gitlab.tiker.net/inducer/grudge/badges/master/pipeline.svg
+    :alt: Gitlab Build Status
+    :target: https://gitlab.tiker.net/inducer/grudge/commits/master
+.. image:: https://github.com/inducer/grudge/workflows/CI/badge.svg?branch=master&event=push
+    :alt: Github Build Status
+    :target: https://github.com/inducer/grudge/actions?query=branch%3Amaster+workflow%3ACI+event%3Apush
+
+..
+    .. image:: https://badge.fury.io/py/grudge.png
+        :alt: Python Package Index Release Page
+        :target: https://pypi.org/project/grudge/
+
 grudge helps you discretize discontinuous Galerkin operators, quickly
 and accurately.
 
diff --git a/examples/advection/weak.py b/examples/advection/weak.py
index bd9d61ca3e2bbfd416a271ffbbabca22f366e3fd..9b880ba8952761514c91743e5078794b346b7c4b 100644
--- a/examples/advection/weak.py
+++ b/examples/advection/weak.py
@@ -32,15 +32,12 @@ from grudge import sym, bind, DGDiscretizationWithBoundaries
 import numpy.linalg as la
 
 
-
-
 def main(write_output=True, order=4):
     cl_ctx = cl.create_some_context()
     queue = cl.CommandQueue(cl_ctx)
 
     dim = 2
 
-
     from meshmode.mesh.generation import generate_regular_rect_mesh
     mesh = generate_regular_rect_mesh(a=(-0.5, -0.5), b=(0.5, 0.5),
             n=(20, 20), order=order)
@@ -50,12 +47,10 @@ def main(write_output=True, order=4):
 
     discr = DGDiscretizationWithBoundaries(cl_ctx, mesh, order=order)
 
-    c = np.array([0.1,0.1])
+    c = np.array([0.1, 0.1])
     norm_c = la.norm(c)
 
-
     flux_type = "central"
-         
 
     def f(x):
         return sym.sin(3*x)
@@ -64,8 +59,7 @@ def main(write_output=True, order=4):
         return f(-np.dot(c, x)/norm_c+sym.var("t", sym.DD_SCALAR)*norm_c)
 
     from grudge.models.advection import WeakAdvectionOperator
-    from meshmode.mesh import BTAG_ALL, BTAG_NONE
-    
+
     discr = DGDiscretizationWithBoundaries(cl_ctx, mesh, order=order)
     op = WeakAdvectionOperator(c,
         inflow_u=u_analytic(sym.nodes(dim, sym.BTAG_ALL)),
@@ -84,19 +78,14 @@ def main(write_output=True, order=4):
     nsteps = (final_time // dt) + 1
     dt = final_time/nsteps + 1e-15
 
-
     from grudge.shortcuts import set_up_rk4
     dt_stepper = set_up_rk4("u", dt, u, rhs)
 
-    last_u = None
-
     from grudge.shortcuts import make_visualizer
     vis = make_visualizer(discr, vis_order=order)
 
     step = 0
 
-    norm = bind(discr, sym.norm(2, sym.var("u")))
-
     for event in dt_stepper.run(t_end=final_time):
         if isinstance(event, dt_stepper.StateComputed):
 
@@ -104,15 +93,8 @@ def main(write_output=True, order=4):
 
             #print(step, event.t, norm(queue, u=event.state_component[0]))
             vis.write_vtk_file("fld-weak-%04d.vtu" % step,
-                    [  ("u", event.state_component) ])
-
-
-
-
-
+                    [("u", event.state_component)])
 
 
 if __name__ == "__main__":
     main()
-
-