diff --git a/.github/workflows/deploy-tests.yaml b/.github/workflows/deploy-tests.yaml index 775316b14..67da5add2 100644 --- a/.github/workflows/deploy-tests.yaml +++ b/.github/workflows/deploy-tests.yaml @@ -9,13 +9,63 @@ jobs: playwright-deploys: # Only allow one `playwright-deploys` job to run at a time. (Independent of branch / PR) # Only one is allowed to run at a time because it is deploying to the same server location. - concurrency: playwright-deploys + concurrency: playwright-deploys-${{ matrix.config.name }} runs-on: ${{ matrix.os }} + name: ${{ matrix.config.name }} strategy: matrix: # Matches deploy server python version python-version: ["3.10"] os: [ubuntu-latest] + config: + # Released server, shiny, and rsconnect + - name: "pypi-shiny-rsconnect-connect" + released_connect_server: true + pypi_shiny: true + pypi_rsconnect: true + base_test_dir: "./tests/playwright/deploys/express-page_sidebar" + app_name: "pypi-shiny-rsconnect" + test_shinyappsio: false + + # Released shiny and rsconnect + # Dev server + - name: "pypi-shiny-rsconnect-dogfood" + released_connect_server: false + pypi_shiny: true + pypi_rsconnect: true + base_test_dir: "./tests/playwright/deploys/express-page_sidebar" + app_name: "pypi-shiny-rsconnect" + test_shinyappsio: false + + # Released shiny + # Dogfood server and rsconnect + - name: "pypi-shiny-dev-rsconnect-dogfood" + released_connect_server: false + pypi_shiny: true + pypi_rsconnect: false + base_test_dir: "./tests/playwright/deploys/express-page_sidebar" + app_name: "pypi-shiny-dev-rsconnect" + test_shinyappsio: false + + # GitHub shiny v1.0.0 - test if github packages can be installed + # Dogfood server and rsconnect + - name: "github-shiny-dev-rsconnect-dogfood" + released_connect_server: false + github_shiny: true + pypi_shiny: false + pypi_rsconnect: false + base_test_dir: "./tests/playwright/deploys/express-page_sidebar" + app_name: "pypi-shiny-dev-rsconnect" + test_shinyappsio: false + + # Dev server, shiny, and rsconnect + - name: "dev-shiny-rsconnect-dogfood" + released_connect_server: false + pypi_shiny: false + pypi_rsconnect: false + base_test_dir: "./tests/playwright/deploys" + test_shinyappsio: true + fail-fast: false steps: @@ -27,7 +77,24 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install rsconnect + - name: Install pypi shiny and htmltools (uninstall GitHub versions) + if: ${{ matrix.config.pypi_shiny }} + run: | + uv pip uninstall shiny htmltools + uv pip install shiny htmltools + + - name: Install GitHub shiny@v1.0.0 and htmltools@v0.5.3 (uninstall PyPI versions) + if: ${{ matrix.config.github_shiny }} + run: | + uv pip uninstall shiny htmltools + uv pip install "htmltools @ git+https://github.com/posit-dev/py-htmltools.git@v0.5.3" "shiny @ git+https://github.com/posit-dev/py-shiny.git@v1.0.0" + + - name: Install rsconnect (PyPI) + if: ${{ matrix.config.pypi_rsconnect }} + run: | + uv pip install rsconnect + - name: Install rsconnect (GitHub) + if: ${{ ! matrix.config.pypi_rsconnect }} run: | make ci-install-rsconnect @@ -36,25 +103,27 @@ jobs: env: DEPLOY_APPS: "false" run: | - make playwright-deploys SUB_FILE=". -vv" + make playwright-deploys TEST_FILE="${{ matrix.config.base_test_dir }} -vv" - name: Deploy apps and run tests (on `push` or `deploy**` branches) env: DEPLOY_APPS: "true" - DEPLOY_CONNECT_SERVER_URL: "https://rsc.radixu.com/" - DEPLOY_CONNECT_SERVER_API_KEY: "${{ secrets.DEPLOY_CONNECT_SERVER_API_KEY }}" - DEPLOY_SHINYAPPS_NAME: "${{ secrets.DEPLOY_SHINYAPPS_NAME }}" - DEPLOY_SHINYAPPS_TOKEN: "${{ secrets.DEPLOY_SHINYAPPS_TOKEN }}" - DEPLOY_SHINYAPPS_SECRET: "${{ secrets.DEPLOY_SHINYAPPS_SECRET }}" + DEPLOY_CONNECT_SERVER_URL: "${{ (matrix.config.released_connect_server && 'https://connect.posit.it/') || 'https://rsc.radixu.com/' }}" + DEPLOY_CONNECT_SERVER_API_KEY: "${{ (matrix.config.released_connect_server && secrets.DEPLOY_CONNECT_POSIT_SERVER_API_KEY) || secrets.DEPLOY_CONNECT_SERVER_API_KEY }}" + DEPLOY_SHINYAPPS_NAME: "${{ matrix.config.test_shinyappsio && matrix.config.shinyapps_name }}" + DEPLOY_SHINYAPPS_TOKEN: "${{ matrix.config.test_shinyappsio && matrix.config.shinyapps_token }}" + DEPLOY_SHINYAPPS_SECRET: "${{ matrix.config.test_shinyappsio && matrix.config.shinyapps_secret }}" + EXPRESS_PAGE_SIDEBAR_NAME: "${{ matrix.config.app_name }}" + DEPLOY_GITHUB_REQUIREMENTS_TXT: "${{ !matrix.config.pypi_shiny }}" timeout-minutes: 30 # Given we are waiting for external servers to finish, # we can have many local processes waiting for deployment to finish run: | - make playwright-deploys SUB_FILE=". -vv --numprocesses 12" + make playwright-deploys TEST_FILE="${{ matrix.config.base_test_dir }} -vv --numprocesses 12" - - uses: actions/upload-artifact@v4 - if: failure() - with: - name: "playright-deploys-${{ matrix.os }}-${{ matrix.python-version }}-results" - path: test-results/ - retention-days: 5 + # - uses: actions/upload-artifact@v4 + # if: failure() + # with: + # name: "playright-deploys-${{ matrix.os }}-${{ matrix.python-version }}-results" + # path: test-results/ + # retention-days: 5 diff --git a/Makefile b/Makefile index cc6eb67f2..7921200cc 100644 --- a/Makefile +++ b/Makefile @@ -173,6 +173,8 @@ playwright-shiny: FORCE # end-to-end tests on deployed apps with playwright; (SUB_FILE="" within tests/playwright/deploys/) playwright-deploys: FORCE + $(MAKE) playwright PYTEST_BROWSERS="$(PYTEST_DEPLOYS_BROWSERS)" TEST_FILE="$(TEST_FILE)" +playwright-deploys-legacy: FORCE $(MAKE) playwright TEST_FILE="tests/playwright/deploys/$(SUB_FILE)" PYTEST_BROWSERS="$(PYTEST_DEPLOYS_BROWSERS)" # end-to-end tests on all py-shiny examples with playwright; (SUB_FILE="" within tests/playwright/examples/) diff --git a/tests/playwright/deploys/express-accordion/test_deploys_express_accordion.py b/tests/playwright/deploys/express-accordion/test_deploys_express_accordion.py index 4160084d6..bf33a21c1 100644 --- a/tests/playwright/deploys/express-accordion/test_deploys_express_accordion.py +++ b/tests/playwright/deploys/express-accordion/test_deploys_express_accordion.py @@ -1,7 +1,7 @@ import pytest from playwright.sync_api import Page from utils.deploy_utils import ( - create_deploys_app_url_fixture, + local_deploys_app_url_fixture, reruns, reruns_delay, skip_if_not_chrome, @@ -9,7 +9,7 @@ from shiny.playwright import controller -app_url = create_deploys_app_url_fixture("shiny_express_accordion") +app_url = local_deploys_app_url_fixture("shiny_express_accordion") @skip_if_not_chrome diff --git a/tests/playwright/deploys/express-dataframe/test_deploys_express_dataframe.py b/tests/playwright/deploys/express-dataframe/test_deploys_express_dataframe.py index 46fdd4e45..d7415e9bb 100644 --- a/tests/playwright/deploys/express-dataframe/test_deploys_express_dataframe.py +++ b/tests/playwright/deploys/express-dataframe/test_deploys_express_dataframe.py @@ -1,7 +1,7 @@ import pytest from playwright.sync_api import Page from utils.deploy_utils import ( - create_deploys_app_url_fixture, + local_deploys_app_url_fixture, reruns, reruns_delay, skip_if_not_chrome, @@ -9,7 +9,7 @@ from shiny.playwright import controller -app_url = create_deploys_app_url_fixture("shiny-express-dataframe") +app_url = local_deploys_app_url_fixture("shiny-express-dataframe") @skip_if_not_chrome diff --git a/tests/playwright/deploys/express-folium/test_deploys_express_folium.py b/tests/playwright/deploys/express-folium/test_deploys_express_folium.py index 247fc0b89..19e95a3e1 100644 --- a/tests/playwright/deploys/express-folium/test_deploys_express_folium.py +++ b/tests/playwright/deploys/express-folium/test_deploys_express_folium.py @@ -1,13 +1,13 @@ import pytest from playwright.sync_api import Page, expect from utils.deploy_utils import ( - create_deploys_app_url_fixture, + local_deploys_app_url_fixture, reruns, reruns_delay, skip_if_not_chrome, ) -app_url = create_deploys_app_url_fixture("shiny-express-folium") +app_url = local_deploys_app_url_fixture("shiny-express-folium") @skip_if_not_chrome diff --git a/tests/playwright/deploys/express-page_default/test_deploys_express_page_default.py b/tests/playwright/deploys/express-page_default/test_deploys_express_page_default.py index 36fdfc2d0..eeb9663fd 100644 --- a/tests/playwright/deploys/express-page_default/test_deploys_express_page_default.py +++ b/tests/playwright/deploys/express-page_default/test_deploys_express_page_default.py @@ -1,7 +1,7 @@ import pytest from playwright.sync_api import Page, expect from utils.deploy_utils import ( - create_deploys_app_url_fixture, + local_deploys_app_url_fixture, reruns, reruns_delay, skip_if_not_chrome, @@ -11,7 +11,7 @@ TIMEOUT = 2 * 60 * 1000 -app_url = create_deploys_app_url_fixture("shiny_express_page_default") +app_url = local_deploys_app_url_fixture("shiny_express_page_default") @skip_if_not_chrome diff --git a/tests/playwright/deploys/express-page_fillable/test_deploys_express_page_fillable.py b/tests/playwright/deploys/express-page_fillable/test_deploys_express_page_fillable.py index c528f8723..295002bd1 100644 --- a/tests/playwright/deploys/express-page_fillable/test_deploys_express_page_fillable.py +++ b/tests/playwright/deploys/express-page_fillable/test_deploys_express_page_fillable.py @@ -1,7 +1,7 @@ import pytest from playwright.sync_api import Page from utils.deploy_utils import ( - create_deploys_app_url_fixture, + local_deploys_app_url_fixture, reruns, reruns_delay, skip_if_not_chrome, @@ -9,7 +9,7 @@ from shiny.playwright import controller -app_url = create_deploys_app_url_fixture("express_page_fillable") +app_url = local_deploys_app_url_fixture("express_page_fillable") @skip_if_not_chrome diff --git a/tests/playwright/deploys/express-page_fluid/test_deploys_express_page_fluid.py b/tests/playwright/deploys/express-page_fluid/test_deploys_express_page_fluid.py index 567e9da66..05c0e0d8f 100644 --- a/tests/playwright/deploys/express-page_fluid/test_deploys_express_page_fluid.py +++ b/tests/playwright/deploys/express-page_fluid/test_deploys_express_page_fluid.py @@ -1,7 +1,7 @@ import pytest from playwright.sync_api import Page from utils.deploy_utils import ( - create_deploys_app_url_fixture, + local_deploys_app_url_fixture, reruns, reruns_delay, skip_if_not_chrome, @@ -9,7 +9,7 @@ from shiny.playwright import controller -app_url = create_deploys_app_url_fixture("express_page_fluid") +app_url = local_deploys_app_url_fixture("express_page_fluid") @skip_if_not_chrome diff --git a/tests/playwright/deploys/express-page_sidebar/test_deploys_express_page_sidebar.py b/tests/playwright/deploys/express-page_sidebar/test_deploys_express_page_sidebar.py index 0fdfcb7fe..2658512d0 100644 --- a/tests/playwright/deploys/express-page_sidebar/test_deploys_express_page_sidebar.py +++ b/tests/playwright/deploys/express-page_sidebar/test_deploys_express_page_sidebar.py @@ -1,7 +1,9 @@ +import os + import pytest from playwright.sync_api import Page from utils.deploy_utils import ( - create_deploys_app_url_fixture, + local_deploys_app_url_fixture, reruns, reruns_delay, skip_if_not_chrome, @@ -9,7 +11,10 @@ from shiny.playwright import controller -app_url = create_deploys_app_url_fixture("express_page_sidebar") +app_url = local_deploys_app_url_fixture( + # Possibly use a different app name given by an GHA env var + os.getenv("EXPRESS_PAGE_SIDEBAR_NAME", "express_page_sidebar") +) @skip_if_not_chrome diff --git a/tests/playwright/deploys/plotly/test_plotly_app.py b/tests/playwright/deploys/plotly/test_plotly_app.py index 52760c57c..7eb8489cc 100644 --- a/tests/playwright/deploys/plotly/test_plotly_app.py +++ b/tests/playwright/deploys/plotly/test_plotly_app.py @@ -1,14 +1,14 @@ import pytest from playwright.sync_api import Page, expect from utils.deploy_utils import ( - create_deploys_app_url_fixture, + local_deploys_app_url_fixture, reruns, reruns_delay, skip_if_not_chrome, ) TIMEOUT = 2 * 60 * 1000 -app_url = create_deploys_app_url_fixture("example_deploy_app_a1") +app_url = local_deploys_app_url_fixture("example_deploy_app_a1") @skip_if_not_chrome diff --git a/tests/playwright/deploys/shiny-client-console-error/test_shiny_client_error.py b/tests/playwright/deploys/shiny-client-console-error/test_shiny_client_error.py index be697f413..d9f62b622 100644 --- a/tests/playwright/deploys/shiny-client-console-error/test_shiny_client_error.py +++ b/tests/playwright/deploys/shiny-client-console-error/test_shiny_client_error.py @@ -1,13 +1,13 @@ import pytest from playwright.sync_api import Page, expect from utils.deploy_utils import ( - create_deploys_app_url_fixture, + local_deploys_app_url_fixture, reruns, reruns_delay, skip_if_not_chrome, ) -app_url = create_deploys_app_url_fixture("shiny_client_console_error") +app_url = local_deploys_app_url_fixture("shiny_client_console_error") @skip_if_not_chrome diff --git a/tests/playwright/utils/deploy_utils.py b/tests/playwright/utils/deploy_utils.py index 95f7798ee..53501624b 100644 --- a/tests/playwright/utils/deploy_utils.py +++ b/tests/playwright/utils/deploy_utils.py @@ -7,7 +7,8 @@ import sys import tempfile import time -from typing import Any, Callable, TypeVar +import warnings +from typing import Any, Callable, List, TypeVar import pytest import requests @@ -22,7 +23,7 @@ LOCAL_LOCATION = "local" __all__ = ( - "create_deploys_app_url_fixture", + "local_deploys_app_url_fixture", "skip_if_not_chrome", ) @@ -30,12 +31,20 @@ server_url = os.environ.get("DEPLOY_CONNECT_SERVER_URL") api_key = os.environ.get("DEPLOY_CONNECT_SERVER_API_KEY") # shinyapps.io -name = os.environ.get("DEPLOY_SHINYAPPS_NAME") -token = os.environ.get("DEPLOY_SHINYAPPS_TOKEN") -secret = os.environ.get("DEPLOY_SHINYAPPS_SECRET") +shinyappsio_name = os.environ.get("DEPLOY_SHINYAPPS_NAME") +shinyappsio_token = os.environ.get("DEPLOY_SHINYAPPS_TOKEN") +shinyappsio_secret = os.environ.get("DEPLOY_SHINYAPPS_SECRET") +run_on_ci = os.environ.get("CI", "False") == "true" +repo = os.environ.get("GITHUB_REPOSITORY", "unknown") + +should_use_github_requirements_txt = ( + os.environ.get("DEPLOY_GITHUB_REQUIREMENTS_TXT", "true").lower() == "true" +) + + +deploy_locations: List[str] = ["connect", "shinyapps"] -deploy_locations = ["connect", "shinyapps"] CallableT = TypeVar("CallableT", bound=Callable[..., Any]) @@ -89,6 +98,7 @@ def deploy_to_connect(app_name: str, app_dir: str) -> str: output = run_command(connect_server_lookup_command) url = json.loads(output)[0]["content_url"] app_id = json.loads(output)[0]["guid"] + # change visibility of app to public connect_app_url = f"{server_url}/__api__/v1/content/{app_id}" payload = '{"access_type":"all"}' @@ -98,20 +108,30 @@ def deploy_to_connect(app_name: str, app_dir: str) -> str: } response = requests.request("PATCH", connect_app_url, headers=headers, data=payload) if response.status_code != 200: - raise RuntimeError("Failed to change visibility of app.") + warnings.warn( + f"Failed to change visibility of app. {response.text}", + RuntimeWarning, + stacklevel=1, + ) + pytest.skip( + "Skipping test as deployed app is not visible to public. Test is kept as it does confirm the app deployment has succeeded." + ) + return + return url # TODO-future: Supress web browser from opening after deploying - https://github.com/rstudio/rsconnect-python/issues/462 def deploy_to_shinyapps(app_name: str, app_dir: str) -> str: # Deploy to shinyapps.io - shinyapps_deploy = f"rsconnect deploy shiny {app_dir} --account {name} --token {token} --secret {secret} --title {app_name} --verbose" + shinyapps_deploy = f"rsconnect deploy shiny {app_dir} --account {shinyappsio_name} --token {shinyappsio_token} --secret {shinyappsio_secret} --title {app_name} --verbose" run_command(shinyapps_deploy) - return f"https://{name}.shinyapps.io/{app_name}/" + return f"https://{shinyappsio_name}.shinyapps.io/{app_name}/" # Since connect parses python packages, we need to get latest version of shiny on HEAD -def write_requirements_txt(app_dir: str) -> None: +def write_github_requirements_txt(app_dir: str) -> None: + print("Writing github requirements.txt") app_requirements_file_path = os.path.join(app_dir, "app_requirements.txt") requirements_file_path = os.path.join(app_dir, "requirements.txt") git_cmd = subprocess.run(["git", "rev-parse", "HEAD"], stdout=subprocess.PIPE) @@ -123,6 +143,18 @@ def write_requirements_txt(app_dir: str) -> None: f.write(f"git+https://github.com/posit-dev/py-shiny.git@{git_hash}\n") +def write_pypi_requirements_txt(app_dir: str) -> None: + print("Writing pypi requirements.txt") + app_requirements_file_path = os.path.join(app_dir, "app_requirements.txt") + requirements_file_path = os.path.join(app_dir, "requirements.txt") + + with open(app_requirements_file_path) as f: + requirements = f.read() + with open(requirements_file_path, "w") as f: + f.write(f"{requirements}\n") + f.write("shiny\n") + + def assert_rsconnect_file_updated(file_path: str, min_mtime: float) -> None: """ Asserts that the specified file has been updated since `min_mtime` (seconds since epoch). @@ -143,9 +175,6 @@ def deploy_app( if not should_deploy_apps: pytest.skip("`DEPLOY_APPS` does not equal `true`") - run_on_ci = os.environ.get("CI", "False") == "true" - repo = os.environ.get("GITHUB_REPOSITORY", "unknown") - if not (run_on_ci and repo == "posit-dev/py-shiny"): pytest.skip("Not on CI and within posit-dev/py-shiny repo") @@ -162,7 +191,10 @@ def deploy_app( tmp_app_dir = os.path.join(tmpdir, app_dir_name) os.mkdir(tmp_app_dir) shutil.copytree(app_dir, tmp_app_dir, dirs_exist_ok=True) - write_requirements_txt(tmp_app_dir) + if should_use_github_requirements_txt: + write_github_requirements_txt(tmp_app_dir) + else: + write_pypi_requirements_txt(tmp_app_dir) deployment_function = { "connect": deploy_to_connect, @@ -179,12 +211,13 @@ def deploy_app( return url -def create_deploys_app_url_fixture( +def local_deploys_app_url_fixture( app_name: str, scope: ScopeName = "module", ): @pytest.fixture(scope=scope, params=[*deploy_locations, LOCAL_LOCATION]) def fix_fn(request: pytest.FixtureRequest): + app_file = os.path.join(os.path.dirname(request.path), "app.py") deploy_location = request.param @@ -193,6 +226,19 @@ def fix_fn(request: pytest.FixtureRequest): # Return the `url` yield next(shinyapp_proc_gen).url elif deploy_location in deploy_locations: + + if deploy_location == "connect" and not (server_url and api_key): + pytest.skip("Connect server url or api key not found. Cannot deploy.") + if ( + deploy_location == "shinyapps" + and shinyappsio_name + and shinyappsio_token + and shinyappsio_secret + ): + pytest.skip( + "Shinyapps.io name, token or secret not found. Cannot deploy." + ) + app_url = deploy_app( app_file, deploy_location,