Repository: pasqal-io/Pulser (language: python)

Issue:
Automatic packaging
Once #362 goes through, it would be nice to have some automatic packaging routine and some CI test on it.

The packaging process is a bit unorthodox because we have three different PyPI distributions under the same repository:
- `pulser-core`, which contains the `pulser` package
- `pulser-simulation`, which contains the `pulser_simulation` package
- `pulser`, which has no package in itself but installs a specific version of `pulser-core` and `pulser-simulation`

AFAIK, [`cirq`](https://github.com/quantumlib/Cirq) has a similar setup, so it might be worth looking into for inspiration.


Discussion / hints:
@HGSilveri Hi can give this a shot would you be available to talk about this some more?
Hi @fred5825 , of course! I'll send you a message on Discord. Once you've decided you want to tackle this let me know and I'll assign it to you

Fix patch:
diff --git a/.github/scripts/package.sh b/.github/scripts/package.sh
new file mode 100755
index 000000000..93f5d66b2
--- /dev/null
+++ b/.github/scripts/package.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+
+# Exit if something fails
+set -e
+
+# Find and change to the repository directory
+repo_dir=$(git rev-parse --show-toplevel)
+cd "${repo_dir}"
+
+# Removing existing files in /dist
+rm -rf dist
+
+packages=$(cat packages.txt)
+# Build the pulser packages
+for pkg in $packages
+do
+  echo "Packaging $pkg"
+  python $pkg/setup.py -q bdist_wheel -d "../dist"
+  rm -r $pkg/build
+done
+
+# Build the pulser metapackage
+python setup.py -q bdist_wheel -d "dist"
+rm -r build
+
+echo "Built wheels:"
+ls dist
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8f825195b..0c028fa1a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,83 +12,59 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Check out Pulser
-        uses: actions/checkout@v2
-      - name: Set up Python
-        uses: actions/setup-python@v2
+        uses: actions/checkout@v3
+      - name: Pulser + flake8 install
+        uses: ./.github/workflows/pulser-setup
         with:
-          python-version: 3.8
-      - name: Install Python dependencies
-        run: |
-          python -m pip install --upgrade pip
-          pip install -e ./pulser-core -e ./pulser-simulation
-          pip install -r requirements.txt
+          extra-packages: flake8
       - name: Lint with flake8
         run: flake8
   black:
     runs-on: ubuntu-latest
     steps:
       - name: Check out Pulser
-        uses: actions/checkout@v2
-      - name: Set up Python
-        uses: actions/setup-python@v2
+        uses: actions/checkout@v3
+      - name: Pulser + black install
+        uses: ./.github/workflows/pulser-setup
         with:
-          python-version: 3.8
-      - name: Install black
-        run: |
-          python -m pip install --upgrade pip
-          pip install black
-          pip install 'black[jupyter]'
+          extra-packages: black
       - name: Check formatting with black
         run: black --check --diff .
   isort:
     runs-on: ubuntu-latest
     steps:
       - name: Check out Pulser
-        uses: actions/checkout@v2
-      - name: Set up Python
-        uses: actions/setup-python@v2
+        uses: actions/checkout@v3
+      - name: Pulser + isort install
+        uses: ./.github/workflows/pulser-setup
         with:
-          python-version: 3.8
-      - name: Install Python dependencies
-        run: |
-          python -m pip install --upgrade pip
-          pip install -e ./pulser-core -e ./pulser-simulation
-          pip install -r requirements.txt
+          extra-packages: isort
       - name: Check import sorting with isort
         run: isort --check-only --diff .
   typing:
     runs-on: ubuntu-latest
     steps:
       - name: Check out Pulser
-        uses: actions/checkout@v2
-      - name: Set up Python
-        uses: actions/setup-python@v2
+        uses: actions/checkout@v3
+      - name: Pulser + mypy install
+        uses: ./.github/workflows/pulser-setup
         with:
-          python-version: 3.8
-      - name: Install Python dependencies
-        run: |
-          python -m pip install --upgrade pip
-          pip install -e ./pulser-core -e ./pulser-simulation
-          pip install -r requirements.txt
+          extra-packages: '''mypy\|pytest'''
       - name: Type check with mypy
         run: mypy
   test:
-    runs-on: ${{ matrix.os }}
+    if: github.event_name != 'push'
+    runs-on: ubuntu-latest
     strategy:
       matrix:
-        os: [ubuntu-latest]
-        python-version: [3.7, 3.8, 3.9, "3.10"]
+        python-version: ['3.7', '3.9']
     steps:
       - name: Check out Pulser
-        uses: actions/checkout@v2
-      - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v2
+        uses: actions/checkout@v3
+      - name: Pulser + pytest install
+        uses: ./.github/workflows/pulser-setup
         with:
           python-version: ${{ matrix.python-version }}
-      - name: Install Python dependencies
-        run: |
-          python -m pip install --upgrade pip
-          pip install -e ./pulser-core -e ./pulser-simulation
-          pip install -r requirements.txt
+          extra-packages: pytest
       - name: Run the unit tests & generate coverage report
         run: pytest --cov --cov-fail-under=100
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 000000000..3ed52addc
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,79 @@
+name: Upload Release Package to PyPI
+
+on:
+  release:
+    types: [released]
+
+jobs:
+  deploy:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check out Pulser
+        uses: actions/checkout@v3
+        with:
+          ref: ${{ github.ref }}
+      - name: Set up Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: 3.9
+      - name: Install Python dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install setuptools wheel twine
+      - name: Build packages
+        shell: bash
+        run: ./.github/scripts/package.sh
+      - name: Publish to TestPyPI
+        env:
+          TWINE_USERNAME: ${{ secrets.TESTPYPI_USERNAME }}
+          TWINE_PASSWORD: ${{ secrets.TESTPYPI_PASSWORD }}
+        run: twine upload --repository testpypi dist/*
+      - name: Install from TestPyPI
+        timeout-minutes: 5
+        shell: bash
+        run: |
+          version="$(head -1 VERSION.txt)"
+          until pip install -i https://test.pypi.org/simple/ pulser==$version --extra-index-url https://pypi.org/simple
+          do
+            echo "Failed to install from TestPyPI, will wait for upload and retry."
+            sleep 30
+          done
+      - name: Test the installation
+        # Installs pytest from dev_requirements.txt (in case it has a version specifier)
+        run: |
+          grep -e pytest dev_requirements.txt | sed 's/ //g' | xargs pip install
+          pytest
+      - name: Publish to PyPI
+        env:
+          TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
+          TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
+        run: twine upload dist/*
+
+  check-release:
+    needs: deploy
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest, macos-latest, windows-latest]
+        python-version: ['3.7', '3.8', '3.9', '3.10']
+    steps:
+      - name: Check out Pulser
+        uses: actions/checkout@v3
+        with:
+          ref: ${{ github.ref }}
+      - name: Set up Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install Pulser from PyPI
+        shell: bash
+        run: |
+          python -m pip install --upgrade pip
+          pip install pulser
+      - name: Test the installation
+        shell: bash
+        run: |
+          version="$(head -1 VERSION.txt)"
+          python -c "import pulser; assert pulser.__version__ == '$version'"
+          grep -e pytest dev_requirements.txt | sed 's/ //g' | xargs pip install
+          pytest
\ No newline at end of file
diff --git a/.github/workflows/pulser-setup/action.yml b/.github/workflows/pulser-setup/action.yml
new file mode 100644
index 000000000..51b90b144
--- /dev/null
+++ b/.github/workflows/pulser-setup/action.yml
@@ -0,0 +1,31 @@
+name: Pulser setup
+description: "Sets up Python and installs Pulser."
+inputs:
+  python-version:
+    description: Python version
+    required: false
+    default: '3.9'
+  extra-packages:
+    description: Extra packages to install (give to grep)
+    required: false
+    default: ''
+runs:
+  using: 'composite'
+  steps: 
+    - name: Set up Python
+      uses: actions/setup-python@v4
+      with:
+        python-version: ${{ inputs.python-version }}
+        cache: 'pip'
+    - name: Install Pulser
+      shell: bash
+      run: |
+        python -m pip install --upgrade pip
+        pip install -e ./pulser-core -e ./pulser-simulation
+    - name: Install extra packages from the dev requirements
+      if: "${{ inputs.extra-packages != '' }}"
+      shell: bash
+      run: |
+        grep -e ${{ inputs.extra-packages }} dev_requirements.txt \
+          | sed 's/ //g' \
+          | xargs pip install
\ No newline at end of file
diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml
new file mode 100644
index 000000000..d07e59df2
--- /dev/null
+++ b/.github/workflows/version.yml
@@ -0,0 +1,48 @@
+name: version
+
+on:
+  pull_request:
+    paths:
+      - 'VERSION.txt'
+
+jobs:
+  validate-version:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check out base branch
+        uses: actions/checkout@v3
+        with:
+          ref:  ${{ github.event.pull_request.base.ref }}
+      - name: Get old version
+        run: |
+          old_version="$(head -1 VERSION.txt)"
+          echo "Old version: $old_version"
+          echo "old_version=$old_version" >> $GITHUB_ENV
+      - name: Check out head branch
+        uses: actions/checkout@v3
+      - name: Get new version 
+        run: |
+          new_version="$(head -1 VERSION.txt)"
+          echo "New version: $new_version"
+          echo "new_version=$new_version" >> $GITHUB_ENV
+      - name: Compare versions
+        run: dpkg --compare-versions "${{ env.old_version }}" lt "${{ env.new_version }}"
+      - name: Check stable version validity
+        if: github.event.pull_request.base.ref == 'master'
+        run: |
+          pattern=^\(0\|[1-9]\d*\)\.\(0\|[1-9]\d*\)\.\(0\|[1-9]\d*\)$
+          if [[ ${{ env.new_version }} =~ $pattern ]]; then
+            echo "New version is valid."; exit 0
+          else
+            echo "New version is invalid."; exit 1
+          fi
+      - name: Check development version validity
+        if: github.event.pull_request.base.ref != 'master'
+        run: |
+          pattern=^\(0\|[1-9]\d*\)\.\(0\|[1-9]\d*\)dev\(0\|[1-9]\d*\)$
+          if [[ ${{ env.new_version }} =~ $pattern ]]; then
+            echo "New version is valid."; exit 0
+          else
+            echo "New version is invalid."; exit 1
+          fi
+
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 716d9c9b9..61cab3a91 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -18,6 +18,6 @@ sphinx:
 python:
    install:
      - requirements: docs/requirements.txt
-     - requirements: requirements.txt
+     - requirements: dev_requirements.txt
      - requirements: pulser-core/rtd_requirements.txt
      - requirements: pulser-simulation/rtd_requirements.txt
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f1d15872c..c0ac85f51 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -70,7 +70,7 @@ Here are the steps you should follow to make your contribution:
 We enforce some continuous integration standards in order to maintain the quality of Pulser's code. Make sure you follow them, otherwise your pull requests will be blocked until you fix them. To check if your changes pass all CI tests before you make the PR, you'll need additional packages, which you can install by running
 
 ```shell
-pip install -r requirements.txt
+pip install -r dev_requirements.txt
 ```
 
 - **Tests**: We use [`pytest`](https://docs.pytest.org/en/latest/) to run unit tests on our code. If your changes break existing tests, you'll have to update these tests accordingly. Additionally, we aim for 100% coverage over our code. Try to cover all the new lines of code with simple tests, which should be placed in the `Pulser/pulser/tests` folder. To run all tests and check coverage, run:
diff --git a/MANIFEST.in b/MANIFEST.in
index 76adac73d..03d8c7343 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,3 @@
 include README.md
-include requirements.txt
 include LICENSE
 include VERSION.txt
diff --git a/README.md b/README.md
index 610f549c2..ef6607edf 100644
--- a/README.md
+++ b/README.md
@@ -55,7 +55,7 @@ your installation will change accordingly.
 To run the tutorials or the test suite locally, after installation first run the following to install the development requirements:
 
 ```bash
-pip install -r requirements.txt
+pip install -r dev_requirements.txt
 ```
 
 Then, you can do the following to run the test suite and report test coverage:
diff --git a/VERSION.txt b/VERSION.txt
index b8aacd3fb..2551af7e6 100644
--- a/VERSION.txt
+++ b/VERSION.txt
@@ -1,1 +1,1 @@
-0.6.0.dev
+0.7dev0
diff --git a/dev_requirements.txt b/dev_requirements.txt
new file mode 100644
index 000000000..038d95e3c
--- /dev/null
+++ b/dev_requirements.txt
@@ -0,0 +1,17 @@
+# tests
+black
+black[jupyter]
+flake8
+flake8-docstrings
+isort
+mypy == 0.921
+pytest
+pytest-cov
+
+# CI
+pre-commit
+
+# tutorials
+notebook
+python-igraph
+scikit-optimize
diff --git a/docs/source/installation.rst b/docs/source/installation.rst
index a6eab3a81..5ccc60f7c 100644
--- a/docs/source/installation.rst
+++ b/docs/source/installation.rst
@@ -42,4 +42,4 @@ your installation will change accordingly.
 If you want to install the development requirements, stay inside the same ``Pulser``
 directory and follow up by running: ::
 
-  pip install -r requirements.txt
+  pip install -r dev_requirements.txt
diff --git a/packages.txt b/packages.txt
new file mode 100644
index 000000000..888e92d31
--- /dev/null
+++ b/packages.txt
@@ -0,0 +1,2 @@
+pulser-core
+pulser-simulation
\ No newline at end of file
diff --git a/pulser-core/requirements.txt b/pulser-core/requirements.txt
new file mode 100644
index 000000000..146f8af7a
--- /dev/null
+++ b/pulser-core/requirements.txt
@@ -0,0 +1,6 @@
+jsonschema == 4.4.0
+matplotlib
+numpy >= 1.20
+scipy
+backports.cached-property; python_version == '3.7'
+typing-extensions; python_version == '3.7'
diff --git a/pulser-core/setup.py b/pulser-core/setup.py
index b4791e1e8..7667c387e 100644
--- a/pulser-core/setup.py
+++ b/pulser-core/setup.py
@@ -27,6 +27,9 @@
 # Changes to the directory where setup.py is
 os.chdir(current_directory)
 
+with open("requirements.txt") as f:
+    requirements = f.read().splitlines()
+
 # Stashes the source code for the local version file
 local_version_fpath = Path(package_name) / "_version.py"
 with open(local_version_fpath, "r") as f:
@@ -39,18 +42,7 @@
 setup(
     name=distribution_name,
     version=__version__,
-    install_requires=[
-        "matplotlib",
-        "numpy>=1.20",
-        "scipy",
-        "jsonschema==4.4.0",
-    ],
-    extras_require={
-        ":python_version == '3.7'": [
-            "backports.cached-property",
-            "typing-extensions",
-        ],
-    },
+    install_requires=requirements,
     packages=find_packages(),
     package_data={package_name: ["py.typed"]},
     include_package_data=True,
diff --git a/pulser-simulation/requirements.txt b/pulser-simulation/requirements.txt
new file mode 100644
index 000000000..7ccd7f816
--- /dev/null
+++ b/pulser-simulation/requirements.txt
@@ -0,0 +1,2 @@
+qutip>=4.6.3
+typing-extensions; python_version == '3.7'
diff --git a/pulser-simulation/setup.py b/pulser-simulation/setup.py
index 484fd835e..bd9ec0476 100644
--- a/pulser-simulation/setup.py
+++ b/pulser-simulation/setup.py
@@ -27,6 +27,10 @@
 # Changes to the directory where setup.py is
 os.chdir(current_directory)
 
+with open("requirements.txt") as f:
+    requirements = f.read().splitlines()
+requirements.append(f"pulser-core=={__version__}")
+
 # Stashes the source code for the local version file
 local_version_fpath = Path(package_name) / "_version.py"
 with open(local_version_fpath, "r") as f:
@@ -39,15 +43,7 @@
 setup(
     name=distribution_name,
     version=__version__,
-    install_requires=[
-        f"pulser-core=={__version__}",
-        "qutip>=4.6.3",
-    ],
-    extras_require={
-        ":python_version == '3.7'": [
-            "typing-extensions",
-        ],
-    },
+    install_requires=requirements,
     packages=find_packages(),
     package_data={package_name: ["py.typed"]},
     include_package_data=True,
diff --git a/release.md b/release.md
new file mode 100644
index 000000000..591e67b44
--- /dev/null
+++ b/release.md
@@ -0,0 +1,73 @@
+# Release Procedure
+
+
+## Versioning
+
+Pulser version follows the [Semantic Versioning 2.0.0 specifcation](https://semver.org/spec/v2.0.0.html), which means its versions are numbered as MAJOR.MINOR.PATCH. 
+
+Currently (as of July 2022), Pulser is still in an early development phase and has a MAJOR of 0 - as such, both breaking and backwards compatible changes and additions to the API will be introduced through increments in the MINOR until version 1.0.0 is released. 
+
+During this phase, only two type of releases are envisioned:
+
+- A scheduled release, where the MINOR is bumped and the PATCH is reset (`0.{x}.{y} -> 0.{x+1}.0`)
+- A hotfix, where the PATCH is bumped (`0.{x}.{y} -> 0.{x}.{y+1}`)
+
+Only releases are tracked and tagged in the `master` branch, while development is done in the `develop` branch. To signal this, the version in the `develop` branch should always be one MINOR ahead of `master` and follow the `MAJOR.{MINOR+1}dev{PATCH}` format (e.g. if the latest release tagged in `master` was `0.4.3`, then the version in `develop` should be `0.5dev3`). Through this format, we mark which release is under development and how many patches have occured since its development started (which tells us how many times we brought in changes done directly in `master` through an hotfix).
+
+The version number is centralized in the `VERSION.txt` file and is shared between all the Pulser packages.
+
+
+## Preparing a scheduled release
+
+A scheduled release is the result of a series of features that were added to the `develop` branch over time. The release process starts out with the creation of a release branch, which should be branched out from `develop` to contain all the desired features and be named `release/v{x}.{y}.{z}`, where `x, y, z` are the MAJOR, MINOR and PATCH of the version to be released (though usually a scheduled release will have PATCH=0). 
+
+In the release branch, no other features can be added. Changes to the documentation and bug fixes are allowed, but should only be done when the development in the `develop` branch needs to continue while the release is being prepared; otherwise, do all the changes in `develop` before checking out the release branch. Note that the release branch will ultimately be *merged* to the `master` branch *without being squashed*, so keep the ammount of commits small and document them well to preserve the quality of the history.
+
+Crucially, the release branch must feature a commit changing the development version in `VERSION.txt` to the desired version of the release. For a minor release, this should be of the form `{x}.{y}dev{z} -> {x}.{y}.0`. 
+
+Finally, open a PR from the `release/v{x}.{y}.{z}` branch to `master`, have someone review and accept the changes introduced in the release branch (all the changes done in `develop` will be there as well, but those have already been reviewed) and merge the branch to `master` **without squashing the commits**. To keep the `master` branch's history clean and informative, replace Github's default merge commit message with `Release v{x}.{y}.{z}`. Optionally, you can also include a summary of the most important changes introduced in the release.
+
+
+## Preparing a hotfix
+
+Unlike with a scheduled release, a hotfix serves only to fix bugs found in the latest release. The hotfix branch must be branched out from `master` and feature only the changes required to fix any bugs.
+
+Along with the bug fixes, the hotfix branch must also have a commit updating the version with an increment of the PATCH, ie `{x}.{y}.{z} -> {x}.{y}.{z+1}`.
+
+When ready, open a PR to merge the hotfix branch to `master` and, once that is reviewed and accepted, **squash and merge the commits** (note the difference with respect to the scheduled release procedure).
+
+
+## Writing the release notes
+
+In the [Pulser Releases](https://github.com/pasqal-io/Pulser/releases), draft a new release where you **tag the HEAD of `master`** with **`v{new-version}`** (eg for version 1.2.3, the tag will be `v1.2.3`).
+
+The release notes should include:
+
+- A summary of the main changes introduced (for scheduled releases)
+- A list of the bug fixes (if any)
+- The full list of changes since the last release. When on the `master` branch, you can get the list of changes since the last tag (which should be the last release) by running:
+    ```bash
+    previous_version=$(git describe --tags --abbrev=0)
+    git log $previous_version..HEAD "--pretty=%h %s"
+    ```
+    If you've tagged the latest version already, just manually replace `previous_version` with the previously released version number.
+- A thank you to all the contributors. Reusing the `previous_version` variable defined before, you can get this list by running:
+    ```bash
+    git log $previous_version..HEAD --pretty="%an" | sort | uniq
+    ```
+    Note that this will list only the authors of the PRs to `develop`. If you know of other contributors that do not appear listed, make sure to add them.
+
+
+## Deploying the release
+
+The publication of the release notes will trigger a Github Actions workflow that automatically builds all the packages, publishes them to PyPI and runs some tests to check the publication succeed. 
+
+Make sure this workflow ran without errors - if not, assess why it failed and, if it was a third-party problem (e.g. a network connection issue), try to rerun the workflow. 
+However, in the unlikely scenario that the deployment failed, it is more likely that there is something that needs to be fixed, in which case you should make an hotfix right away. 
+
+
+## Merging the changes back to `develop`
+
+Finally, you must open a PR from `master` to `develop` to merge the changes that occured in `master`. In this PR, you must also bump the version you just released, `{x}.{y}.{z}`, to the new development version, `{x}.{y+1}dev{z}` (e.g. `0.8.3 -> 0.9dev3`).
+
+Once the PR is accepted, merge it **without squashing** (again, replacing the merge commit message with something more informative) and that's it, you're done!
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 2b5a0746e..000000000
--- a/requirements.txt
+++ /dev/null
@@ -1,27 +0,0 @@
-matplotlib
-numpy >= 1.20
-qutip >= 4.6.3
-scipy
-
-# version specific
-backports.cached-property; python_version == '3.7'
-typing-extensions; python_version == '3.7'
-
-# tests
-black
-black[jupyter]
-flake8
-flake8-docstrings
-isort
-mypy == 0.921
-pytest
-pytest-cov
-jsonschema
-
-# CI
-pre-commit
-
-# tutorials
-notebook
-python-igraph
-scikit-optimize
diff --git a/setup.py b/setup.py
index e6017f250..fc1a95c21 100644
--- a/setup.py
+++ b/setup.py
@@ -25,14 +25,14 @@
         "`pip install -e ./pulser-core -e ./pulser-simulation` instead."
     )
 
+with open("packages.txt", "r") as f:
+    requirements = [f"{pkg.strip()}=={__version__}" for pkg in f.readlines()]
+
 # Just a meta-package that requires 'pulser-core' and 'pulser-simulation'
 setup(
     name="pulser",
     version=__version__,
-    install_requires=[
-        f"pulser-core=={__version__}",
-        f"pulser-simulation=={__version__}",
-    ],
+    install_requires=requirements,
     description="A pulse-level composer for neutral-atom quantum devices.",
     long_description=open("README.md").read(),
     long_description_content_type="text/markdown",


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 000000000..338561c05
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,25 @@
+name: test
+
+on:
+  push:
+    branches:
+      - master
+      - develop
+
+jobs:
+  full-tests:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest, macos-latest, windows-latest]
+        python-version: ['3.7', '3.8', '3.9', '3.10']
+    steps:
+      - name: Check out Pulser
+        uses: actions/checkout@v3
+      - name: Pulser + pytest setup
+        uses: ./.github/workflows/pulser-setup
+        with:
+          python-version: ${{ matrix.python-version }}
+          extra-packages: pytest
+      - name: Run the unit tests & generate coverage report
+        run: pytest --cov --cov-fail-under=100
\ No newline at end of file