diff --git a/.github/workflows/README.md b/.github/workflows/README.md
index ca5de021704..aa8fd99d3e8 100644
--- a/.github/workflows/README.md
+++ b/.github/workflows/README.md
@@ -69,5 +69,17 @@ Documentation for used GitHub secrets.
+
+ | COVERAGE_SCRIPT_CLONE |
+ Yes |
+
+ Github Deploy token linked to the [development repo](https://github.com/specify/specify-development) to allow actions to clone it.
+ |
+
+
+ #8278
+
+ |
+
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
new file mode 100644
index 00000000000..7b358ba6250
--- /dev/null
+++ b/.github/workflows/coverage.yml
@@ -0,0 +1,168 @@
+name: Coverage
+
+on:
+ pull_request:
+ push:
+ workflow_dispatch:
+
+jobs:
+ # copied from test.yml
+ changes:
+ name: Check if back-end or front-end files changed
+ # Don't run this for pushes generated by push.yml on weblate-localization
+ # branch → prevents an infinite loop of actions triggering actions
+ #
+ # Also, "push" event is not triggered for forks, so need to listen for
+ # pull_requests, but only for external ones, so as not to run the action
+ # twice
+ if: |
+ (github.event_name == 'workflow_dispatch' ||
+ (github.event.head_commit.committer.name != 'Hosted Weblate' &&
+ github.event.head_commit.committer.name != 'github-actions'))
+ &&
+ (
+ github.event_name != 'pull_request' ||
+ github.event.pull_request.head.repo.fork == true
+ )
+ runs-on: ubuntu-latest
+ outputs:
+ backend_changed: ${{ steps.back-end-check.outputs.changed }}
+ frontend: ${{ steps.filter.outputs.frontend }}
+ frontend_changes: ${{ steps.filter.outputs.frontend_files }}
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+
+ - name: Find all changed files
+ uses: dorny/paths-filter@v3
+ id: filter
+ with:
+ base: ${{ github.ref }}
+ list-files: escape
+ filters: |
+ frontend:
+ - 'specifyweb/frontend/**'
+ backend:
+ - '**'
+
+ - name: Check if any non-front-end files changed
+ id: back-end-check
+ run: |
+ changed_files=`echo "${{steps.filter.outputs.backend_files}}" | tr " " "\n" | grep -v 'specifyweb/frontend/' || echo ""`
+ echo "Changed back-end files: ${changed_files}"
+ echo "changed=$([[ -z "${changed_files}" ]] && echo "" || echo "1")" >> $GITHUB_OUTPUT
+
+ setup-and-run:
+ needs: changes
+ if: |
+ (needs.changes.outputs.backend_changed ||
+ needs.changes.outputs.frontend) ||
+ github.event_name == 'workflow_dispatch'
+ runs-on: ubuntu-latest
+
+
+ steps:
+ - name: Install needed packages
+ id: apt
+ run: |
+ sudo apt update
+ sudo apt install docker-compose-v2 openssh-client
+
+ - uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+
+ - name: Fetch coverage script
+ id: clone-coverage
+ run: |
+ eval "$(ssh-agent -s)"
+ ssh-add - <<< '${{ secrets.COVERAGE_SCRIPT_CLONE }}'
+ cd ~
+ git clone git@github.com:specify/specify-development
+ sudo install -m 0777 -o "$USER" specify-development/scripts/tests/{run-coverage,test_coverage.py} /usr/bin/
+ cd -
+
+ - name: Configure specify
+ id: config-specify
+ run: |
+ cat > .env <<-EOS
+ DATABASE_HOST=mariadb
+ DATABASE_PORT=3306
+ MYSQL_ROOT_PASSWORD=password
+ DATABASE_NAME=specify
+
+ MASTER_NAME=root
+ MASTER_PASSWORD=password
+ MASTER_HOST=%
+
+ MIGRATOR_NAME=root
+ MIGRATOR_PASSWORD=password
+ MIGRATOR_HOST=%
+
+ APP_USER_NAME=root
+ APP_USER_PASSWORD=password
+ APP_HOST=%
+
+ ALLOW_SUPPORT_LOGIN=false
+ SUPPORT_LOGIN_TTL=180
+
+ SECRET_KEY=change_this_to_some_unique_random_string
+
+ ASSET_SERVER_URL=http://host.docker.internal/web_asset_store.xml
+ ASSET_SERVER_KEY=your_asset_server_access_key
+ REDIS_HOST=redis
+ REDIS_PORT=6379
+ REDIS_DB_INDEX=0
+
+ REPORT_RUNNER_HOST=report-runner
+ REPORT_RUNNER_PORT=8080
+
+ CELERY_BROKER_URL=redis://redis/0
+ CELERY_RESULT_BACKEND=redis://redis/1
+
+ TIME_ZONE=America/Chicago
+
+ LOG_LEVEL=WARNING
+
+ SP7_DEBUG=true
+ EOS
+
+ sed -i 's/#[ ]*\(.*requirements-testing.txt\)/\1/; s/\(COPY\) \(requirements-testing.txt\)/\1 --chown=specify:specify \2/' Dockerfile
+ sed -i 's/\(\(DATABASE_\|MASTER_\|MIGRATOR\|APP_USER_\)[^O].*\) = \(.*\)/\1 = os.environ.get("\1", "")/' specifyweb/settings/specify_settings.py
+ for i in range {0..3}; do
+ sudo chmod a+x $(find . -type d)
+ sudo chmod -R a+rw .
+ done
+ docker compose up --build -d
+
+ - name: Fix mariadb
+ id: reboot-mariadb
+ run: |
+ sleep 10
+ docker compose up -d mariadb
+ sleep 10
+ docker exec mariadb mariadb -uroot -ppassword -e"DROP USER root;
+ CREATE USER 'root'@'%' IDENTIFIED BY 'password';
+ GRANT ALL ON *.* TO 'root'@'%' IDENTIFIED BY 'password';
+ DROP DATABASE IF EXISTS specify;"
+ docker compose down
+ docker compose up -d
+ sleep 10
+
+
+ - name: Run the script
+ id: run-coverage
+ run: run-coverage -t pj -a pj -d pj -o html,csv
+
+
+ - name: Upload report as artifact
+ uses: actions/upload-artifact@v7
+ with:
+ name: coverage-report
+ path: ./report.tar.bz2
+ archive: false
+ retention-days: 1
+
+
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 00000000000..c5c37d97a12
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,2 @@
+[pytest]
+DJANGO_SETTINGS_MODULE = specifyweb.settings
diff --git a/requirements-testing.txt b/requirements-testing.txt
index 21bba85648c..aab161ee7f1 100644
--- a/requirements-testing.txt
+++ b/requirements-testing.txt
@@ -3,4 +3,6 @@ mypy==1.5.1
hypothesis>=6.131.9,<7
hypothesis-jsonschema==0.19.0
types-requests==2.28.5
-lxml
\ No newline at end of file
+lxml
+coverage
+pytest-django