Repository: e-valuation/EvaP

Issue:
Investigate Github Action caching
One can cache certain directories in github actions. For Python, caching the entire installation is used (including all the `site-packages`), so that wheels don't need to be rebuild. Additionally, the download could be faster. We should investigate how much there is to gain.


Discussion:
Agree as long as its not much hassle (causes errors, lots of configuration)

Installing python requirements takes ~15s on a github runner, installing the node dependencies takes ~25s -- this is what we could gain, right?

Our slowest actions currently are
[Test + Coverage](https://github.com/e-valuation/EvaP/runs/6247385090) (~2:25min) with
- 30s container setup (waiting for postgres to start)
- 17s installing dependencies
- 1:20min running tests

[Backup process](https://github.com/e-valuation/EvaP/runs/6247384862) (~2:45min)
- 30s container setup (waiting for postgres to start)
- 16s installing dependencies
- 35s load testdata
- 24s backup
- 46s loading that backup

so it seems to me that the 15s for installing dependencies wouldn't really be the most relevant optimization -- but still, we could do it.

In theory, backing up and restoring the database state can be done in ~1s (pg_dump can create an sql file that can be loaded back). It's so slow here because django does this one-object-at-a-time and does a lot of extra work that we don't actually need ([check out this monstrosity](https://github.com/django/django/blob/7119f40c9881666b6f9b5cf7df09ee1d21cc8344/django/core/management/commands/loaddata.py#L253)). We could consider changing our format to the .sql files created by pg_dump, for the backup during an upgrade as well as for our test_data.json. (Somewhat related: There was https://github.com/e-valuation/EvaP/issues/333 8 years ago :D)

It doesn't really make sense to wait for postgres to start, only to _then_ start installing dependencies. We could make that step parallel (don't know if this is specifiable in github actions?)

Resolving patch:
diff --git a/evap/evaluation/management/commands/scss.py b/evap/evaluation/management/commands/scss.py
--- a/evap/evaluation/management/commands/scss.py
+++ b/evap/evaluation/management/commands/scss.py
@@ -2,7 +2,7 @@
 import subprocess  # nosec
 
 from django.conf import settings
-from django.core.management.base import BaseCommand
+from django.core.management.base import BaseCommand, CommandError
 
 
 class Command(BaseCommand):
@@ -36,7 +36,7 @@ def handle(self, *args, **options):
 
         try:
             subprocess.run(command, check=True)  # nosec
-        except FileNotFoundError:
-            print("Could not find sass command", file=self.stderr)
+        except FileNotFoundError as e:
+            raise CommandError("Could not find sass command") from e
         except KeyboardInterrupt:
             pass
diff --git a/evap/evaluation/management/commands/ts.py b/evap/evaluation/management/commands/ts.py
--- a/evap/evaluation/management/commands/ts.py
+++ b/evap/evaluation/management/commands/ts.py
@@ -54,8 +54,8 @@ def handle(self, *args, **options):
     def run_command(self, command):
         try:
             subprocess.run(command, check=True)  # nosec
-        except FileNotFoundError:
-            print(f"Could not find {command[0]} command", file=self.stderr)
+        except FileNotFoundError as e:
+            raise CommandError(f"Could not find {command[0]} command") from e
         except KeyboardInterrupt:
             pass
         except subprocess.CalledProcessError as e:


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -12,9 +12,6 @@ jobs:
 
     runs-on: ubuntu-20.04
 
-    container:
-      image: python:3.8
-
     services:
       postgres:
         image: postgres
@@ -28,24 +25,21 @@ jobs:
 
     steps:
       - name: Check out repository code
-        uses: actions/checkout@v2
-      - name: Install dependencies
-        run: pip install -r requirements-dev.txt
-      - name: Add localsettings
-        run: cp evap/settings_test.py evap/localsettings.py
+        uses: actions/checkout@v3
+
+      - name: Setup python
+        uses: ./.github/setup_python
+
       - name: Run tests
         run: coverage run manage.py test
       - name: Upload coverage
-        uses: codecov/codecov-action@v2
+        uses: codecov/codecov-action@v3
 
   test_shuffled:
     name: Test (shuffled order)
 
     runs-on: ubuntu-20.04
 
-    container:
-      image: python:3.8
-
     services:
       postgres:
         image: postgres
@@ -59,11 +53,11 @@ jobs:
 
     steps:
       - name: Check out repository code
-        uses: actions/checkout@v2
-      - name: Install dependencies
-        run: pip install -r requirements-dev.txt
-      - name: Add localsettings
-        run: cp evap/settings_test.py evap/localsettings.py
+        uses: actions/checkout@v3
+
+      - name: Setup python
+        uses: ./.github/setup_python
+
       - name: Run tests
         run: python manage.py test --shuffle
 
@@ -77,29 +71,26 @@ jobs:
 
     steps:
       - name: Check out repository code
-        uses: actions/checkout@v2
-      - name: Install dependencies
-        run: pip install -r requirements-dev.txt
-      - name: Add localsettings
-        run: cp evap/settings_test.py evap/localsettings.py
+        uses: actions/checkout@v3
+
+      - name: Setup python
+        uses: ./.github/setup_python
+
       - name: Run MyPy
         run: mypy -p evap
 
   linter:
     runs-on: ubuntu-20.04
 
-    container:
-      image: python:3.8
-
     name: Linter
 
     steps:
       - name: Check out repository code
-        uses: actions/checkout@v2
-      - name: Install dependencies
-        run: pip install -r requirements-dev.txt
-      - name: Add localsettings
-        run: cp evap/settings_test.py evap/localsettings.py
+        uses: actions/checkout@v3
+
+      - name: Setup python
+        uses: ./.github/setup_python
+
       - name: Run linter
         run: pylint evap -j 0
 
@@ -111,18 +102,14 @@ jobs:
 
     steps:
       - name: Check out repository code
-        uses: actions/checkout@v2
-      - uses: actions/setup-python@v2
-        with:
-          python-version: 3.8
-      - name: Install Python dependencies
-        run: pip install -r requirements-dev.txt
-      - name: Setup Node
-        uses: actions/setup-node@v2
-      - name: Install Node dependencies
-        run: npm ci
-      - name: Add localsettings
-        run: cp evap/settings_test.py evap/localsettings.py
+        uses: actions/checkout@v3
+
+      - name: Setup python
+        uses: ./.github/setup_python
+
+      - name: Setup nodejs
+        uses: ./.github/setup_nodejs
+
       - name: Check code formatting
         run: black --check evap
       - name: Check imports formatting
@@ -135,9 +122,6 @@ jobs:
   backup-process:
     runs-on: ubuntu-20.04
 
-    container:
-      image: python:3.8
-
     services:
       postgres:
         image: postgres
@@ -157,19 +141,25 @@ jobs:
     name: Backup process
 
     steps:
-      - name: Install sudo package
-        # otherwise, sudo commands in the scripts called will fail
-        run: apt update && apt install sudo
       - name: Check out repository code
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
+        with:
+          submodules: true
+
+      - name: Setup python
+        uses: ./.github/setup_python
+
+      - name: Setup nodejs
+        uses: ./.github/setup_nodejs
+
+      - name: Install additional dependencies
+        run: sudo apt install gettext
+
       - name: GitHub actions has wrong file ownership here, the checkout actions has a problem here (see their 1049)
         run: |
           git config --global --add safe.directory '*'
           sudo -H -u root git config --global --add safe.directory '*'
-      - name: Install dependencies
-        run: pip install -r requirements-dev.txt
-      - name: Add localsettings
-        run: cp evap/settings_test.py evap/localsettings.py
+
       - name: Load test data
         run: |
           python manage.py migrate
@@ -189,17 +179,17 @@ jobs:
 
     steps:
       - name: Check out repository code
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
         with:
           submodules: true
-      - name: Setup Node
-        uses: actions/setup-node@v2
-      - name: Install Node dependencies
-        run: npm ci
+
+      - name: Setup nodejs
+        uses: ./.github/setup_nodejs
+
       - name: Compile Scss
         run: npx sass evap/static/scss/evap.scss evap/static/css/evap.css
       - name: Store Css
-        uses: actions/upload-artifact@v2
+        uses: actions/upload-artifact@v3
         with:
           name: css
           path: evap/static/css/evap.css
@@ -208,9 +198,6 @@ jobs:
   render_pages:
     runs-on: ubuntu-20.04
 
-    container:
-      image: python:3.8
-
     name: Render Html pages
 
     services:
@@ -231,19 +218,19 @@ jobs:
 
     steps:
       - name: Check out repository code
-        uses: actions/checkout@v2
-      - name: Install dependencies
-        run: pip install -r requirements-dev.txt
-      - name: Add localsettings
-        run: cp evap/settings_test.py evap/localsettings.py
+        uses: actions/checkout@v3
+
+      - name: Setup python
+        uses: ./.github/setup_python
+
       - name: Render pages
         run: coverage run manage.py ts render_pages
       - name: Upload coverage
-        uses: codecov/codecov-action@v2
+        uses: codecov/codecov-action@v3
         with:
           flags: render-pages
       - name: Store rendered pages
-        uses: actions/upload-artifact@v2
+        uses: actions/upload-artifact@v3
         with:
           name: rendered-pages
           path: evap/static/ts/rendered
@@ -258,24 +245,22 @@ jobs:
 
     steps:
       - name: Check out repository code
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
         with:
           submodules: true
-      - name: Install packages
-        run: sudo apt-get install xvfb libgbm1
-      - name: Setup Node
-        uses: actions/setup-node@v2
-      - name: Install Node dependencies
-        run: npm ci
+
+      - name: Setup nodejs
+        uses: ./.github/setup_nodejs
+
       - name: Compile Typescript
         run: npx tsc --project evap/static/ts/tsconfig.compile.json
       - name: Load rendered pages
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v3
         with:
           name: rendered-pages
           path: evap/static/ts/rendered
       - name: Load Css
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v3
         with:
           name: css
           path: evap/static/css
diff --git a/evap/evaluation/tests/test_commands.py b/evap/evaluation/tests/test_commands.py
--- a/evap/evaluation/tests/test_commands.py
+++ b/evap/evaluation/tests/test_commands.py
@@ -8,6 +8,7 @@
 
 from django.conf import settings
 from django.core import mail, management
+from django.core.management import CommandError
 from django.db.models import Sum
 from django.test import TestCase
 from django.test.utils import override_settings
@@ -212,10 +213,8 @@ def test_scss_production_called(self, mock_subprocess_run):
     def test_scss_called_with_no_sass_installed(self, mock_subprocess_run):
         mock_subprocess_run.side_effect = FileNotFoundError()
 
-        stderr = StringIO()
-        management.call_command("scss", stderr=stderr)
-
-        self.assertEqual(stderr.getvalue(), "Could not find sass command\n\n")
+        with self.assertRaisesMessage(CommandError, "Could not find sass command"):
+            management.call_command("scss")
 
 
 class TestTsCommend(TestCase):
@@ -261,6 +260,13 @@ def test_ts_test(self, mock_render_pages, mock_call_command, mock_subprocess_run
             ]
         )
 
+    @patch("subprocess.run")
+    def test_ts_called_with_no_npm_installed(self, mock_subprocess_run):
+        mock_subprocess_run.side_effect = FileNotFoundError()
+
+        with self.assertRaisesMessage(CommandError, "Could not find npx command"):
+            management.call_command("ts", "compile")
+
 
 class TestUpdateEvaluationStatesCommand(TestCase):
     def test_update_evaluations_called(self):
diff --git a/evap/settings_test.py b/evap/settings_test.py
--- a/evap/settings_test.py
+++ b/evap/settings_test.py
@@ -5,22 +5,22 @@
         "NAME": "evap",
         "USER": "postgres",
         "PASSWORD": "postgres",
-        "HOST": "postgres",
+        "HOST": "localhost",
         "PORT": "5432",
     }
 }
 CACHES = {
     "default": {
         "BACKEND": "django.core.cache.backends.redis.RedisCache",
-        "LOCATION": "redis://redis:6379/0",
+        "LOCATION": "redis://localhost:6379/0",
     },
     "results": {
         "BACKEND": "django.core.cache.backends.redis.RedisCache",
-        "LOCATION": "redis://redis:6379/1",
+        "LOCATION": "redis://localhost:6379/1",
         "TIMEOUT": None,
     },
     "sessions": {
         "BACKEND": "django.core.cache.backends.redis.RedisCache",
-        "LOCATION": "redis://redis:6379/2",
+        "LOCATION": "redis://localhost:6379/2",
     },
 }