Repository: jupyterhub/jupyter-server-proxy (language: python)

Issue:
Modernize packaging infrastructure
### References

- follow-on to #368

### Proposed change

Update the packaging infrastructure to be in line with current common practices e.g. [cookiecutter](https://github.com/jupyterlab/extension-cookiecutter-ts). 

> I'd say "best practices," but i'm still not 100% convinced of the relatively extreme extensibility of `hatch` and the number of things it does _magically_... i actually _prefer_ the relatively bare-bones, but extremely stable, `flit`, and generally use that on my own personal and `$DAY_JOB` projects, with an external tool such as `make` or `doit`.

### Who would use this feature?

- maintainers and contributors _should_ be able to `pip install -e .[atest,docs]` and get a full working dev environment
  - (modulo perhaps `nodejs`, `geckodriver`)

### (Optional): Suggest a solution

- use the `hatch` build backend and plugins to normalize on [`pyproject.toml`](https://github.com/jupyterlab/extension-cookiecutter-ts/blob/3.0/%7B%7Bcookiecutter.python_name%7D%7D/pyproject.toml)
- hoist the `package.json` to the top level of the repo to simplify some of the path stuff
- only ship a single copy of the built assets in the `.whl`

### Alternative options

- do nothing: keep using `setuptools`, etc. with legacy `setup.py` build?
  - will keep working for a while, yet
- keep using `setup.py`, etc. ?
  - add a `pyproject.toml` that declares the build system?
  - move most of the metadata to `setup.cfg`


Discussion / hints:
Thank you soo much @bollwyvl for all you do in this project!!!!

I'm 👍 for adoption of hatch and extensive use of pyproject.toml, to the extent setup.py is removed if possible. We have adopted use of hatch in other jupyterhub projects (such as jupyterhub/jupyterhub) to no longer use setup.py for example with good success so i'm fine with adopting it here also.

If possible, i appreciate that we have the code/logic for the labextension separated to reduce the complexity of understanding the project's code files. If its only package.json, thats better than if all files are extracted and a lesser compromise very reasonable to do still.
yeah, it would just be `package.json` that would go to the top-level of the repo. but the complexity of getting it into the whl is unavoidable. i'll do a strawman PR.

Fix patch:
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
index 9d915c09..bf6a83ab 100644
--- a/.github/workflows/publish.yaml
+++ b/.github/workflows/publish.yaml
@@ -35,14 +35,26 @@ jobs:
 
     steps:
       - uses: actions/checkout@v3
+
       - uses: actions/setup-python@v4
         with:
           python-version: "3.11"
 
+      - uses: actions/setup-node@v3
+        with:
+          cache: yarn
+          node-version: 18.x
+          registry-url: https://registry.npmjs.org
+          cache-dependency-path: labextension/yarn.lock
+
+      - name: Update root build packages
+        run: |
+          pip install --upgrade build pip
+
       - name: Build dist
         run: |
-          pip install jupyter_packaging wheel jupyterlab~=3.0
-          python setup.py sdist bdist_wheel
+          pyproject-build
+          cd dist && sha256sum * | tee SHA256SUMS
 
       - name: Check dist sizes
         run: |
@@ -53,8 +65,9 @@ jobs:
       - name: Javascript package
         run: |
           mkdir jsdist
-          cd labextension
-          jlpm pack --filename ../jsdist/labextension-jlpmpack.tgz
+          cd jsdist
+          npm pack ../labextension
+          sha256sum * | tee SHA256SUMS
 
       - name: Upload Python artifact
         uses: actions/upload-artifact@v3
@@ -112,9 +125,11 @@ jobs:
           name: jsdist
           path: jsdist
 
-      - run: npm publish --dry-run ./jsdist/labextension-jlpmpack.tgz
+      - run: |
+          npm publish --dry-run ./jsdist/jupyterhub-jupyter-server-proxy-*.tgz
 
-      - run: npm publish ./jsdist/labextension-jlpmpack.tgz
+      - run: |
+          npm publish ./jsdist/jupyterhub-jupyter-server-proxy-*.tgz
         if: startsWith(github.ref, 'refs/tags')
         env:
           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8b019aaa..8c1889ec 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -10,7 +10,7 @@ the README.md file.
 ### Python package
 
 ```bash
-pip install -e .[test]
+pip install -e ".[test]"
 
 # explicit install needed with editable mode (-e) jupyter
 jupyter serverextension enable --sys-prefix jupyter_server_proxy
@@ -19,7 +19,7 @@ jupyter server extension enable --sys-prefix jupyter_server_proxy
 
 Before running tests, you need a server that we can test against.
 
-```
+```bash
 JUPYTER_TOKEN=secret jupyter-lab --config=./tests/resources/jupyter_server_config.py --no-browser
 ```
 
@@ -39,7 +39,7 @@ To install these in addition to the [Python package](#python-package) test
 dependencies, run:
 
 ```bash
-pip install -e .[test,acceptance]
+pip install -e ".[acceptance]"
 ```
 
 In addition, compatible versions of:
@@ -47,7 +47,7 @@ In addition, compatible versions of:
 - `geckodriver`
 - `firefox`
 
-Needs to be on your `$PATH` and compatible with each other.
+Need to be on your `$PATH` and compatible with each other.
 
 To run _only_ the acceptance tests, use the `-k` switch:
 
@@ -74,11 +74,47 @@ jlpm build:prod            # Build:
 jlpm install:extension     # Symlink into `{sys.prefix}/share/jupyter/labextensions`
 ```
 
-You can watch the source directory and automatically rebuild the `lib` folder:
+You can watch the source directory and automatically rebuild the `labextension/lib`
+and `jupyter_server_proxy/labextension` folders:
+
+```bash
+cd labextension
+jlpm watch
+```
+
+### Documentation
+
+The documentation uses a fairly standard [Sphinx](https://www.sphinx-doc.org)
+build chain, and requires `make` on Linux/MacOS, which cannot be installed with
+`pip`.
+
+In addition to any system packages, building the documentation requires
+additional packages. To install the needed packages:
+
+```bash
+pip install -r docs/requirements.txt
+```
+
+Once installed, enter the docs folder with:
 
 ```bash
-jlpm watch   # ... watch the source directory in another terminal tab
+cd docs
 ```
 
-However, the built-in `jupyter labextension watch` does _not_ work with this repo,
-as the `package.json` and `setup.py` would need to be at the same level.
+... then build the HTML site:
+
+```bash
+make
+```
+
+... or check that all hyperlinks can be resolved:
+
+```bash
+make linkcheck
+```
+
+... or start an auto-reloading server and open a web browser:
+
+```bash
+make devenv
+```
diff --git a/README.md b/README.md
index b908e527..469fba4a 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ contains information on installation & usage.
 ## Security warning
 
 Jupyter Server Proxy is often used to start a user defined process listening to
-some network port (e.g. http://localhost:4567) for a user starting a Jupyter Server
+some network port (e.g. `http://localhost:4567`) for a user starting a Jupyter Server
 that only that user has permission to access. The user can then access the
 started process proxied through the Jupyter Server.
 
@@ -47,7 +47,7 @@ A common strategy to enforce access proxied via Jupyter Server is to start
 Jupyter Server within a container and only allow network access to the Jupyter
 Server via the container.
 
-For more insights, see [Ryan Lovetts comment about
+> For more insights, see [Ryan Lovett's comment about
 it](https://github.com/jupyterhub/jupyter-server-proxy/pull/359#issuecomment-1350118197).
 
 ## Install
@@ -58,18 +58,22 @@ it](https://github.com/jupyterhub/jupyter-server-proxy/pull/359#issuecomment-135
 
 ### Python package
 
-#### pip
+#### `pip`
 
-```
+```bash
 pip install jupyter-server-proxy
 ```
 
-#### conda
+#### `conda`
 
-```
+```bash
 conda install jupyter-server-proxy -c conda-forge
 ```
 
+#### Local development
+
+> See the [contributing guide](https://github.com/jupyterhub/jupyter-server-proxy/blob/main/CONTRIBUTING.md).
+
 ### JupyterLab extension
 
 Note that as the JupyterLab extension only is a graphical interface to
@@ -79,7 +83,7 @@ requires the python package to be installed.
 As of version 3.0.0 the Python package ships with a JupyterLab 3 compatible
 extension, making this step only needed for JupyterLab 2.
 
-```
+```bash
 jupyter labextension install @jupyterhub/jupyter-server-proxy
 ```
 
@@ -87,22 +91,19 @@ jupyter labextension install @jupyterhub/jupyter-server-proxy
 
 ### Server extension
 
-``` 
-jupyter serverextension disable jupyter_server_proxy
+```bash
+jupyter serverextension disable --sys-prefix jupyter_server_proxy
+jupyter server extension disable --sys-prefix jupyter_server_proxy
 ```
 
 ### Notebook classic extension
 
-```
-jupyter nbextension disable --py jupyter_server_proxy
+```bash
+jupyter nbextension disable --sys-prefix --py jupyter_server_proxy
 ```
 
 ### JupyterLab extension
 
-```
+```bash
 jupyter labextension disable @jupyterhub/jupyter-server-proxy
 ```
-
-## Local development
-
-See [CONTRIBUTING.md](CONTRIBUTING.md).
diff --git a/RELEASE.md b/RELEASE.md
index 144e0bb3..f7ce7baa 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -20,8 +20,8 @@ These are instructions on how to make a release.
    ```shell
    rm labextension/yarn.lock
 
-   pip install jupyter_packaging wheel jupyterlab~=3.0
-   python setup.py sdist bdist_wheel
+   pip install --upgrade pip build
+   pyproject-build
    ```
 
 2. Create a PR updating `docs/source/changelog.md` with [github-activity][] and
diff --git a/jupyter_server_proxy/etc/jupyter-server-proxy-notebookserverextension.json b/jupyter_server_proxy/etc/jupyter_notebook_config.d/jupyter-server-proxy.json
similarity index 100%
rename from jupyter_server_proxy/etc/jupyter-server-proxy-notebookserverextension.json
rename to jupyter_server_proxy/etc/jupyter_notebook_config.d/jupyter-server-proxy.json
diff --git a/jupyter_server_proxy/etc/jupyter-server-proxy-jupyterserverextension.json b/jupyter_server_proxy/etc/jupyter_server_config.d/jupyter-server-proxy.json
similarity index 100%
rename from jupyter_server_proxy/etc/jupyter-server-proxy-jupyterserverextension.json
rename to jupyter_server_proxy/etc/jupyter_server_config.d/jupyter-server-proxy.json
diff --git a/jupyter_server_proxy/etc/jupyter-server-proxy-nbextension.json b/jupyter_server_proxy/etc/nbconfig/tree.d/jupyter-server-proxy.json
similarity index 100%
rename from jupyter_server_proxy/etc/jupyter-server-proxy-nbextension.json
rename to jupyter_server_proxy/etc/nbconfig/tree.d/jupyter-server-proxy.json
diff --git a/labextension/README.md b/labextension/README.md
index 025b6b6b..238bca96 100644
--- a/labextension/README.md
+++ b/labextension/README.md
@@ -1,8 +1,7 @@
-# @jupyterhub/jupyter-server-proxy labextension
+# `@jupyterhub/jupyter-server-proxy`
 
-This JupyterLab extension that is bundled and requires with the PyPI package
-jupyter-server-proxy adds items to the JupyterLab [Launcher] representing the
-configured server processes.
+A JupyterLab extension that adds items to the JupyterLab [Launcher] representing the
+configured server processes managed by the python package `jupyter-server-proxy` (required).
 
 [launcher]: https://jupyterlab.readthedocs.io/en/stable/extension/extension_points.html#launcher
 
@@ -25,7 +24,7 @@ conda install jupyter-server-proxy
 ```
 
 > As a _prebuilt_ extension, it will "just work," only a simple page reload should be required
-> to see launcher items. However, a full restart of `jupyter_server` or `notebook` is required 
+> to see launcher items. However, a full restart of `jupyter_server` or `notebook` is required
 > to reload the `jupyter_server_proxy` serverextension which provides most of the functionality.
 
 ### JupyterLab 2
diff --git a/labextension/package.json b/labextension/package.json
index 30718c46..905a1ff7 100644
--- a/labextension/package.json
+++ b/labextension/package.json
@@ -38,7 +38,7 @@
     "clean:all": "jlpm run clean:lib && jlpm run clean:labextension",
     "install:extension": "jupyter labextension develop --overwrite .",
     "watch": "run-p watch:src watch:labextension",
-    "watch:src": "jlpm build:lib -w",
+    "watch:src": "jlpm build:lib -w --preserveWatchOutput",
     "watch:labextension": "jupyter labextension watch .",
     "deduplicate": "yarn-deduplicate -s fewer --fail"
   },
diff --git a/pyproject.toml b/pyproject.toml
index 6394cd86..b7e688da 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,10 +1,107 @@
-# build-system
-# - ref 1: https://peps.python.org/pep-0517/
-#
 [build-system]
-requires = ["jupyter_packaging~=0.7.9", "jupyterlab~=3.0", "setuptools>=40.8.0", "wheel"]
-build-backend = "setuptools.build_meta"
+build-backend = "hatchling.build"
+requires = [
+    "hatch-jupyter-builder >=0.5",
+    "hatch-nodejs-version",
+    "hatchling >=1.4.0",
+    "jupyterlab >=3.4.7,<4.0.0",
+]
+
+[project]
+name = "jupyter_server_proxy"
+dynamic = [
+    "authors",
+    "description",
+    "keywords",
+    "urls",
+    "version",
+]
+readme = "README.md"
+license = { file = "LICENSE" }
+requires-python = ">=3.7"
+classifiers = [
+    "Framework :: Jupyter",
+    "Framework :: Jupyter :: JupyterLab",
+    "Framework :: Jupyter :: JupyterLab :: 2",
+    "Framework :: Jupyter :: JupyterLab :: 3",
+    "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt",
+    "Framework :: Jupyter :: JupyterLab :: Extensions",
+    "License :: OSI Approved :: BSD License",
+    "Operating System :: MacOS :: MacOS X",
+    "Operating System :: Microsoft :: Windows",
+    "Operating System :: POSIX :: Linux",
+    "Programming Language :: Python",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.7",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+]
+dependencies = [
+    "aiohttp",
+    "jupyter-server >=1.0",
+    "simpervisor >=0.4",
+]
+
+[project.optional-dependencies]
+test = [
+    "pytest",
+    "pytest-cov",
+    "pytest-html",
+]
+# acceptance tests additionally require firefox and geckodriver
+acceptance = [
+    "jupyter-server-proxy[test]",
+    "notebook <7",
+    "robotframework-jupyterlibrary >=0.4.2",
+]
+
+[tool.hatch.version]
+source = "nodejs"
+path = "labextension/package.json"
+
+[tool.hatch.build.targets.sdist]
+artifacts = [
+    "jupyter_server_proxy/labextension",
+]
+exclude = [
+    ".git",
+    ".github",
+    ".readthedocs.yaml",
+    "contrib",
+    "docs",
+]
+
+[tool.hatch.build.targets.wheel.shared-data]
+"jupyter_server_proxy/etc" = "etc/jupyter"
+"jupyter_server_proxy/labextension" = "share/jupyter/labextensions/@jupyterhub/jupyter-server-proxy"
+"jupyter_server_proxy/static" = "share/jupyter/nbextensions/jupyter_server_proxy"
+
+[tool.hatch.metadata.hooks.nodejs]
+path = "labextension/package.json"
+fields = ["description", "authors", "urls"]
+
+[tool.hatch.build.hooks.jupyter-builder]
+build-function = "hatch_jupyter_builder.npm_builder"
+ensured-targets = [
+    "jupyter_server_proxy/labextension/static/style.js",
+    "jupyter_server_proxy/labextension/package.json",
+    "jupyter_server_proxy/labextension/static/third-party-licenses.json",
+]
+skip-if-exists = ["jupyter_server_proxy/labextension/package.json"]
+
+[tool.hatch.build.hooks.jupyter-builder.build-kwargs]
+path = "labextension"
+build_cmd = "build:prod"
+npm = ["jlpm"]
 
+[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs]
+path = "labextension"
+build_cmd = "install:extension"
+npm = ["jlpm"]
+source_dir = "labextension/src"
+build_dir = "jupyter_server_proxy/labextension"
 
 # tbump is used to simplify and standardize the release process when updating
 # the version, making a git commit and tag, and pushing changes.
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 8183238a..00000000
--- a/setup.cfg
+++ /dev/null
@@ -1,2 +0,0 @@
-[metadata]
-license_files = LICENSE
diff --git a/setup.py b/setup.py
index 38eda7ff..cc0b3a54 100644
--- a/setup.py
+++ b/setup.py
@@ -1,144 +1,1 @@
-"""
-jupyter-server-proxy setup
-"""
-import json
-from glob import glob
-from pathlib import Path
-
-import setuptools
-from jupyter_packaging import (
-    combine_commands,
-    create_cmdclass,
-    ensure_targets,
-    install_npm,
-    skip_if_exists,
-)
-
-HERE = Path(__file__).parent.resolve()
-
-# The name of the project
-name = "jupyter_server_proxy"
-
-lab_path = HERE / name / "labextension"
-
-# Representative files that should exist after a successful build
-jstargets = [
-    str(lab_path / "package.json"),
-]
-
-package_data_spec = {
-    name: ["*"],
-}
-
-labext_name = "@jupyterhub/jupyter-server-proxy"
-
-data_files_spec = [
-    ("share/jupyter/labextensions/%s" % labext_name, str(lab_path), "**"),
-    ("share/jupyter/labextensions/%s" % labext_name, str(HERE), "install.json"),
-    (
-        "etc/jupyter/jupyter_server_config.d",
-        "jupyter_server_proxy/etc",
-        "jupyter-server-proxy-jupyterserverextension.json",
-    ),
-    (
-        "etc/jupyter/jupyter_notebook_config.d",
-        "jupyter_server_proxy/etc",
-        "jupyter-server-proxy-notebookserverextension.json",
-    ),
-    (
-        "etc/jupyter/nbconfig/tree.d",
-        "jupyter_server_proxy/etc",
-        "jupyter-server-proxy-nbextension.json",
-    ),
-]
-
-cmdclass = create_cmdclass(
-    "jsdeps", package_data_spec=package_data_spec, data_files_spec=data_files_spec
-)
-
-js_command = combine_commands(
-    install_npm(HERE / "labextension", build_cmd="build:prod", npm=["jlpm"]),
-    ensure_targets(jstargets),
-)
-
-is_repo = (HERE / ".git").exists()
-if is_repo:
-    cmdclass["jsdeps"] = js_command
-else:
-    cmdclass["jsdeps"] = skip_if_exists(jstargets, js_command)
-
-long_description = (HERE / "README.md").read_text()
-
-# Get the package info from package.json
-pkg_json = json.loads((HERE / "labextension" / "package.json").read_bytes())
-
-setup_args = dict(
-    name=name.replace("_", "-"),
-    version=pkg_json["version"],
-    url=pkg_json["homepage"],
-    author=pkg_json["author"]["name"],
-    author_email=pkg_json["author"]["email"],
-    description=pkg_json["description"],
-    license=pkg_json["license"],
-    long_description=long_description,
-    long_description_content_type="text/markdown",
-    cmdclass=cmdclass,
-    packages=setuptools.find_packages(),
-    install_requires=[
-        "aiohttp",
-        "jupyter-server>=1.0",
-        "simpervisor>=0.4",
-    ],
-    extras_require={
-        # acceptance tests additionally require firefox and geckodriver
-        "test": [
-            "pytest",
-            "pytest-cov",
-            "pytest-html"
-        ],
-        "acceptance": [
-            "robotframework-jupyterlibrary>=0.4.2"
-        ]
-    },
-    zip_safe=False,
-    include_package_data=True,
-    python_requires=">=3.7",
-    keywords=["Jupyter", "JupyterLab", "JupyterLab3"],
-    classifiers=[
-        "Framework :: Jupyter",
-        "Framework :: Jupyter :: JupyterLab :: 2",
-        "Framework :: Jupyter :: JupyterLab :: 3",
-        "Framework :: Jupyter :: JupyterLab :: Extensions",
-        "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt",
-        "Operating System :: Microsoft :: Windows",
-        "Operating System :: POSIX :: Linux",
-        "Operating System :: MacOS :: MacOS X",
-        "Programming Language :: Python",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.7",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Programming Language :: Python :: 3.11",
-        "Framework :: Jupyter",
-    ],
-    data_files=[
-        ("share/jupyter/nbextensions/jupyter_server_proxy", glob("jupyter_server_proxy/static/*")),
-        (
-            "etc/jupyter/jupyter_notebook_config.d",
-            ["jupyter_server_proxy/etc/jupyter-server-proxy-notebookserverextension.json"],
-        ),
-        (
-            "etc/jupyter/jupyter_server_config.d",
-            ["jupyter_server_proxy/etc/jupyter-server-proxy-jupyterserverextension.json"],
-        ),
-        (
-            "etc/jupyter/nbconfig/tree.d",
-            ["jupyter_server_proxy/etc/jupyter-server-proxy-nbextension.json"],
-        ),
-    ],
-)
-
-
-if __name__ == "__main__":
-    setuptools.setup(**setup_args)
+# this file intentionally left blank for legacy tools to find


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 5758124f..6bac9121 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -43,43 +43,52 @@ jobs:
 
     steps:
       - uses: actions/checkout@v3
+
       - uses: actions/setup-python@v4
         with:
           python-version: "${{ matrix.python-version }}"
 
+      - uses: actions/setup-node@v3
+        with:
+          cache: yarn
+          node-version: 18.x
+          registry-url: https://registry.npmjs.org
+          cache-dependency-path: labextension/yarn.lock
+
+      - name: Update root build packages
+        run: |
+          pip install --upgrade build pip
+
       - name: Build Python package
         run: |
-          pip install jupyter_packaging wheel jupyterlab
-          python setup.py sdist bdist_wheel
+          pyproject-build
 
-      - name: Install Python dependencies
+      - name: Install Python package
         # NOTE: See CONTRIBUTING.md for a local development setup that differs
         #       slightly from this.
         #
         #       Pytest options are set in tests/pytest.ini.
         run: |
-          pip install --upgrade pip
-          pip install jupyter_packaging jupyterlab~=${{ matrix.jupyterlab-version }}.0
-          pip install ./dist/jupyter_server_proxy-*.whl
-          pip install pytest pytest-cov pytest-html
-          # Ensure we don't accidentally depend on notebook
-          if [ "${{ matrix.jupyter-app }}" == "notebook" ]; then
-            pip install "notebook<7"
-          fi
+          pip install -vv $(ls ./dist/jupyter_server_proxy-*.whl)\[acceptance\] 'jupyterlab~=${{ matrix.jupyterlab-version }}.0'
+
+      - name: List Python packages
+        run: |
           pip freeze
+          pip check
 
       - name: Run tests
         run: |
           JUPYTER_TOKEN=secret jupyter-${{ matrix.jupyter-app }} --config=./tests/resources/jupyter_server_config.py &
           sleep 5
           cd tests
-          pytest
+          pytest -k "not acceptance"
 
       - name: Upload pytest and coverage reports
         if: always()
         uses: actions/upload-artifact@v3
         with:
-          name: unit-tests-${{ matrix.python-version }}-${{ matrix.jupyter-app }}-${{ matrix.jupyterlab-version }}-${{ github.run_number }}
+          name: |-
+            unit-tests-${{ matrix.python-version }}-${{ matrix.jupyter-app }}-${{ matrix.jupyterlab-version }}-${{ github.run_number }}
           path: |
             ./build/pytest
             ./build/coverage
@@ -87,17 +96,18 @@ jobs:
       - name: Check the Notebook Server extension is installed
         run: |
           jupyter serverextension list
-          jupyter serverextension list 2>&1 | grep -ie "jupyter_server_proxy.*enabled" -
+          jupyter serverextension list 2>&1 | grep -iE "jupyter_server_proxy.*OK" -
 
       - name: Check the Jupyter Server extension is installed
         run: |
           pip install jupyter-server
           jupyter server extension list
-          jupyter server extension list 2>&1 | grep -ie "jupyter_server_proxy.*enabled" -
+          jupyter server extension list 2>&1 | grep -iE "jupyter_server_proxy.*OK" -
 
       - name: Install JupyterLab Extension
         if: matrix.jupyterlab-version == '2'
         run: |
+          export NODE_OPTIONS=--openssl-legacy-provider
           cd labextension
           jupyter labextension install . --no-build --debug
           jupyter lab build --minimize=False --debug
@@ -108,25 +118,21 @@ jobs:
         # jupyterlab.browser_check with jupyterlab 2 and a modern version of
         # python (3.11+).
         #
-        if: ${{ !(matrix.jupyterlab-version == '2' && startsWith(matrix.python-version, '3.1')) }}
+        if: ${{ !(matrix.jupyterlab-version == '2' && startsWith(matrix.python-version, '3.11')) }}
         run: |
           jupyter labextension list
-          jupyter labextension list 2>&1 | grep -ie '@jupyterhub/jupyter-server-proxy.*OK.*'
+          jupyter labextension list 2>&1 | grep -iE '@jupyterhub/jupyter-server-proxy.*OK.*'
           python -m jupyterlab.browser_check
 
-      - name: Install Acceptance test dependencies
-        run: |
-          # the acceptance test requires notebook to run
-          pip install "notebook<7" "robotframework-jupyterlibrary>=0.4.2"
-
       - name: Run acceptance tests
         run: |
-          pytest -k acceptance -s
+          pytest -s -k "acceptance"
 
       - name: Upload acceptance test reports
         if: always()
         uses: actions/upload-artifact@v3
         with:
-          name: acceptance-tests-${{ matrix.python-version }}-${{ matrix.jupyter-app }}-${{ matrix.jupyterlab-version }}-${{ github.run_number }}
+          name: |-
+            acceptance-tests-${{ matrix.python-version }}-${{ matrix.jupyter-app }}-${{ matrix.jupyterlab-version }}-${{ github.run_number }}
           path: |
             ./build/robot