--- a
+++ b/.github/workflows/main.yml
@@ -0,0 +1,65 @@
+---
+name: CI
+
+on:
+  push:
+    branches:
+      - main
+  pull_request:
+
+jobs:
+  check:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v4
+      - uses: opensafely-core/setup-action@v1
+        with:
+          install-just: true
+          python-version: "3.11"
+      - name: Set up development environment
+        run: just devenv
+      - name: Check formatting and linting rules
+        run: just check
+
+  test:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v4
+      - uses: opensafely-core/setup-action@v1
+        with:
+          install-just: true
+          python-version: "3.11"
+      - name: Set up development environment
+        run: just devenv
+      - name: Run tests
+        run: |
+          just test-all
+
+  tag-new-version:
+    # This uses `conventional commits` to generate tags.  A full list
+    # of valid prefixes is here:
+    # https://github.com/commitizen/conventional-commit-types/blob/master/index.json
+    #
+    # fix, perf -> patch release
+    # feat -> minor release
+    # BREAKING CHANGE in footer -> major release
+    #
+    # anything else (docs, refactor, etc) does not create a release
+    if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
+    needs: [check, test]
+    runs-on: ubuntu-latest
+    outputs:
+      tag: ${{ steps.tag.outputs.new_version }}
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+      - name: Bump version and push tag
+        id: tag
+        uses: mathieudutour/github-tag-action@a22cf08638b34d5badda920f9daf6e72c477b07b #v6.2
+        with:
+          github_token: ${{ secrets.GITHUB_TOKEN }}
+          default_bump: false
+          release_branches: main