diff --git a/.github/workflows/cleanup.yaml b/.github/workflows/cleanup.yaml new file mode 100644 index 000000000..f866707e6 --- /dev/null +++ b/.github/workflows/cleanup.yaml @@ -0,0 +1,20 @@ +name: fuzzbucket +on: + workflow_call: +jobs: + cleanup-connect: + env: + DOCKER: false + FUZZBUCKET_SSH_KEY: ${{ secrets.FUZZBUCKET_SSH_KEY }} + FUZZBUCKET_URL: ${{ secrets.FUZZBUCKET_URL }} + FUZZBUCKET_CREDENTIALS: ${{ secrets.FUZZBUCKET_CREDENTIALS }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-python@v4 + with: + python-version: 3.11 + - uses: extractions/setup-just@v1 + - run: just cy fuzzbucket-stop diff --git a/.github/workflows/contract-deps.yaml b/.github/workflows/contract-deps.yaml new file mode 100644 index 000000000..0e87c1452 --- /dev/null +++ b/.github/workflows/contract-deps.yaml @@ -0,0 +1,21 @@ +name: contract dependencies +on: + workflow_call: +jobs: + connect: + env: + DOCKER: false + FUZZBUCKET_SSH_KEY: ${{ secrets.FUZZBUCKET_SSH_KEY }} + FUZZBUCKET_URL: ${{ secrets.FUZZBUCKET_URL }} + FUZZBUCKET_CREDENTIALS: ${{ secrets.FUZZBUCKET_CREDENTIALS }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-python@v4 + with: + python-version: 3.11 + - uses: extractions/setup-just@v1 + - run: echo "${FUZZBUCKET_SSH_KEY}" > test/cy/fuzzbucket-ssh-key && chmod 600 test/cy/fuzzbucket-ssh-key + - run: just cy fuzzbucket-start diff --git a/.github/workflows/contract.yaml b/.github/workflows/contract.yaml new file mode 100644 index 000000000..c27d04d74 --- /dev/null +++ b/.github/workflows/contract.yaml @@ -0,0 +1,34 @@ +name: Cypress-nightly +on: + workflow_call: +jobs: + native: + env: + DOCKER: false + FUZZBUCKET_SSH_KEY: ${{ secrets.FUZZBUCKET_SSH_KEY }} + FUZZBUCKET_URL: ${{ secrets.FUZZBUCKET_URL }} + FUZZBUCKET_CREDENTIALS: ${{ secrets.FUZZBUCKET_CREDENTIALS }} + strategy: + fail-fast: false + matrix: + os: [macos-latest, windows-latest, ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + + - uses: extractions/setup-just@v1 + - uses: actions/download-artifact@v3 + with: + name: executables + path: bin + - run: chmod -R +x ./bin + - run: echo "${FUZZBUCKET_SSH_KEY}" > test/cy/fuzzbucket-ssh-key && chmod 600 test/cy/fuzzbucket-ssh-key + - run: just cy install + - run: just cy test + - run: just cy test-deploy diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index d1cd25e5a..bb58c0ce0 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -15,7 +15,7 @@ jobs: uses: ./.github/workflows/bats.yaml cypress: needs: build - uses: ./.github/workflows/cypress.yaml + uses: ./.github/workflows/ui.yaml # Extensions jupyterlab: diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml new file mode 100644 index 000000000..b48e36aa3 --- /dev/null +++ b/.github/workflows/nightly.yaml @@ -0,0 +1,33 @@ +name: Nightly +on: + schedule: + - cron: "0 06 * * *" + workflow_dispatch: + +jobs: + agent: + uses: ./.github/workflows/agent.yaml + web: + uses: ./.github/workflows/web.yaml + build: + uses: ./.github/workflows/build.yaml + bats: + needs: build + uses: ./.github/workflows/bats.yaml + contract-deps: + secrets: inherit + uses: ./.github/workflows/contract-deps.yaml + cypress-nightly: + needs: [build, contract-deps] + secrets: inherit + uses: ./.github/workflows/contract.yaml + cleanup: + needs: cypress-nightly + secrets: inherit + uses: ./.github/workflows/cleanup.yaml + + # Extensions + jupyterlab: + uses: ./.github/workflows/jupyterlab.yaml + positron: + uses: ./.github/workflows/positron.yaml diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 2ed0521f8..fc49e966b 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -16,7 +16,7 @@ jobs: uses: ./.github/workflows/bats.yaml cypress: needs: build - uses: ./.github/workflows/cypress.yaml + uses: ./.github/workflows/ui.yaml # Extensions jupyterlab: diff --git a/.github/workflows/cypress.yaml b/.github/workflows/ui.yaml similarity index 100% rename from .github/workflows/cypress.yaml rename to .github/workflows/ui.yaml diff --git a/justfile b/justfile index 737c136f6..77a4bfa49 100644 --- a/justfile +++ b/justfile @@ -119,7 +119,7 @@ cy *args: #!/usr/bin/env bash set -eou pipefail {{ _with_debug }} - + just _with_docker just test/cy/{{ args }} # Prints the executable path for this operating system. It may not exist yet (see `just build`). diff --git a/test/cy/cypress/e2e/contract/deploy.cy.js b/test/cy/cypress/e2e/contract/deploy.cy.js new file mode 100644 index 000000000..75f927834 --- /dev/null +++ b/test/cy/cypress/e2e/contract/deploy.cy.js @@ -0,0 +1,27 @@ +describe('Publish', () => { + it('hit the publish button', () => { + cy.visit({ + url: '/', + qs: { token: Cypress.env('token') } + }); + cy.get('.block').contains('Publish') + .click(); + }); +}); + +describe('Check Connect Deployment', () => { + it('check deployment', { baseUrl: null }, () => { + cy.visit(Cypress.env('CYPRESS_CONNECT_ADDRESS')); + cy.get('#username').type('admin'); + cy.get('#password').type('password'); + cy.get('button[data-automation="login-panel-submit"]') + .click(); + cy.get('#rs_radio_cop-visibility_editor') + .click(); + cy.get('h1[data-automation="content-list-title"]') + .contains('Your Content'); + cy.get('td[data-automation="content-row-icon-title-cell"]') + .contains('Untitled') + .click(); + }); +}); diff --git a/test/cy/cypress/e2e/home.cy.js b/test/cy/cypress/e2e/ui/home.cy.js similarity index 100% rename from test/cy/cypress/e2e/home.cy.js rename to test/cy/cypress/e2e/ui/home.cy.js diff --git a/test/cy/justfile b/test/cy/justfile index e0a35a9bd..25e6ee7ab 100644 --- a/test/cy/justfile +++ b/test/cy/justfile @@ -2,6 +2,9 @@ alias c := clean alias i := install alias t := test +# RUNNER_OS from gh actions for windows tests +export RUNNER_OS := env_var_or_default("RUNNER_OS", "local") + _ci := env_var_or_default("CI", "false") _debug := env_var_or_default("DEBUG", "false") @@ -70,3 +73,29 @@ test: {{ _with_debug }} npm test + +fuzzbucket-start: + #!/usr/bin/env bash + set -eou pipefail + pip install -r ../setup/requirements.txt + python ../setup/connect_setup.py + +fuzzbucket-stop: + #!/usr/bin/env bash + set -eou pipefail + pip install fuzzbucket-client + fuzzbucket-client rm connect-publishing-client + +test-deploy: + #!/usr/bin/env bash + set -eou pipefail + {{ _with_debug }} + pip install -r ../setup/requirements.txt + export CONNECT_SERVER="$(python ../setup/connect_setup.py)" + export CONNECT_API_KEY="$(python ../setup/gen_apikey.py 'admin')" + export CYPRESS_CONNECT_ADDRESS="${CONNECT_SERVER}/connect/\#/login" + if [[ ${RUNNER_OS} =~ "Windows" ]]; then \ + npm run test-deploy-windows + else + npm run test-deploy + fi diff --git a/test/cy/package-lock.json b/test/cy/package-lock.json index 923085b37..cf14c3fa5 100644 --- a/test/cy/package-lock.json +++ b/test/cy/package-lock.json @@ -2661,9 +2661,9 @@ } }, "node_modules/start-server-and-test": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.0.1.tgz", - "integrity": "sha512-8PFo4DLLLCDMuS51/BEEtE1m9CAXw1LNVtZSS1PzkYQh6Qf9JUwM4huYeSoUumaaoAyuwYBwCa9OsrcpMqcOdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.0.2.tgz", + "integrity": "sha512-4sGS2QmETUwqeBUqtTLP7OqXp3PdDnevaWlPlrFQgn8+7uCgVg4Do7/H/ZhAAVyvnL3DqKyANhnLgcgxrjhrMA==", "dev": true, "dependencies": { "arg": "^5.0.2", @@ -2673,7 +2673,7 @@ "execa": "5.1.1", "lazy-ass": "1.6.0", "ps-tree": "1.2.0", - "wait-on": "7.0.1" + "wait-on": "7.1.0" }, "bin": { "server-test": "src/bin/start.js", @@ -2962,16 +2962,16 @@ } }, "node_modules/wait-on": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.0.1.tgz", - "integrity": "sha512-9AnJE9qTjRQOlTZIldAaf/da2eW0eSRSgcqq85mXQja/DW3MriHxkpODDSUEg+Gri/rKEcXUZHe+cevvYItaog==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.1.0.tgz", + "integrity": "sha512-U7TF/OYYzAg+OoiT/B8opvN48UHt0QYMi4aD3PjRFpybQ+o6czQF8Ig3SKCCMJdxpBrCalIJ4O00FBof27Fu9Q==", "dev": true, "dependencies": { "axios": "^0.27.2", - "joi": "^17.7.0", + "joi": "^17.11.0", "lodash": "^4.17.21", - "minimist": "^1.2.7", - "rxjs": "^7.8.0" + "minimist": "^1.2.8", + "rxjs": "^7.8.1" }, "bin": { "wait-on": "bin/wait-on" diff --git a/test/cy/package.json b/test/cy/package.json index 8c1beaf47..66e9d5d96 100644 --- a/test/cy/package.json +++ b/test/cy/package.json @@ -3,10 +3,14 @@ "scripts": { "lint": "eslint --ext .js,.ts,.cjs .", "fix": "eslint --ext .js,.ts,.cjs . --fix", - "start": "just ../../run publish-ui --listen 127.0.0.1:9000 ./test/sample-content/fastapi-simple", + "start": "just ../../run publish-ui --listen 127.0.0.1:9000 -n env ./test/sample-content/fastapi-simple", "test": "start-server-and-test --expect 200 start http-get://127.0.0.1:9000 run", + "test-deploy": "start-server-and-test --expect 200 start http-get://127.0.0.1:9000 run-deploy", + "test-deploy-windows": "start-server-and-test --expect 200 start http-get://127.0.0.1:9000 run-deploy-windows", "open": "cypress open", - "run": "cypress run" + "run": "cypress run --spec ./cypress/e2e/ui/*.cy.js --env token=token", + "run-deploy": "cypress run --spec ./cypress/e2e/contract/*.cy.js --env token=token,CYPRESS_CONNECT_ADDRESS=$CYPRESS_CONNECT_ADDRESS", + "run-deploy-windows": "cypress run --spec ./cypress/e2e/contract/*.cy.js --env token=token,CYPRESS_CONNECT_ADDRESS=%CYPRESS_CONNECT_ADDRESS%" }, "devDependencies": { "cypress": "^13.3.0", diff --git a/test/setup/connect_setup.py b/test/setup/connect_setup.py new file mode 100644 index 000000000..4a02c5b9b --- /dev/null +++ b/test/setup/connect_setup.py @@ -0,0 +1,87 @@ +import hashlib +import subprocess +import json +import requests +import time +import logging +import os + +# use the perftest fuzzbucket instance since it already has all the deps +alias = "ubuntu22-publishing-client" +box_name = "connect-publishing-client" +list_command = "fuzzbucket-client -j list" +create_command = "fuzzbucket-client create -c -S 20 -t m5.2xlarge " + alias + " -n " + box_name +remove_command = "fuzzbucket-client rm " + box_name +ssh_options = "-i fuzzbucket-ssh-key" + +def get_api_key(username): + # Calculate the MD5 hash for the username to get an API Key + api_key = hashlib.md5(username.encode()).hexdigest() + return api_key + +def get_connect_version(): + if "CONNECT_VERSION" in os.environ: + connect_version=os.environ['CONNECT_VERSION'] + return connect_version + else: + response = requests.get("https://cdn.posit.co/connect/latest-packages.json") + connect_version = response.json()['packages'][0]['version'] + return connect_version + +def get_current_connect_version(connect_ip, api_key): + response = requests.get( + 'http://' + connect_ip + ':3939/__api__/server_settings', + headers={'Authorization': 'Key ' + api_key}, + ) + current_connect = response.json()['version'] + return current_connect + +def check_existing_boxes(box_name): + output = subprocess.check_output(list_command, shell=True, text=True) + # use the existing box if one exists + if box_name+"\": {" in output: + boxes = json.loads(output) + connect_ip = boxes["boxes"][box_name]["public_ip"] + else: + subprocess.check_output(create_command, shell=True, text=True) + output = subprocess.check_output(list_command, shell=True, text=True) + boxes = json.loads(output) + time.sleep(5) + connect_ip = boxes["boxes"][box_name]["public_ip"] + return connect_ip + +def get_ip(box_name): + connect_ip = check_existing_boxes(box_name) + return connect_ip + +# check if fuzzbucket is up and taking requests +def connect_ready(box_name, max_attempts, interval): + connect_box=get_ip(box_name) + update_config="fuzzbucket-client ssh " + box_name + " " + ssh_options + " sudo sed -i 's/CONNECT_IP/" + connect_box + "/g' /etc/rstudio-connect/rstudio-connect.gcfg" + attempts = 0 + while attempts < max_attempts: + try: + logging.info("Checking Connect Status") + response = requests.get("http://"+connect_box+":3939/__ping__") + if response.status_code == 200: + if connect_version != get_current_connect_version(get_ip(box_name), api_key): + logging.info("Installing Connect on " + connect_box) + subprocess.check_output(install_connect, shell=True, text=True) + subprocess.check_output(update_config, shell=True, text=True) + return response.text + except requests.RequestException: + pass + + time.sleep(interval) + attempts += 1 + return None + +api_key=get_api_key('admin') +connect_version=get_connect_version() +install_connect = "fuzzbucket-client ssh " + box_name + " " + ssh_options + " sudo -E UNATTENDED=1 bash installer-ci.sh -d " + connect_version +response = connect_ready(box_name, 20, 5) + +if response: + print("http://" + get_ip(box_name) + ":3939") +else: + print("Server did not respond after multiple attempts.") \ No newline at end of file diff --git a/test/setup/gen_apikey.py b/test/setup/gen_apikey.py new file mode 100644 index 000000000..3ae4c21c1 --- /dev/null +++ b/test/setup/gen_apikey.py @@ -0,0 +1,9 @@ +import sys +import hashlib + +def get_api_key(username): + md5_hash = hashlib.md5(username.encode()).hexdigest() + print(md5_hash) + return md5_hash + +get_api_key(str(sys.argv[1])) \ No newline at end of file diff --git a/test/setup/requirements.txt b/test/setup/requirements.txt new file mode 100644 index 000000000..f84fa141a --- /dev/null +++ b/test/setup/requirements.txt @@ -0,0 +1,2 @@ +requests +fuzzbucket-client \ No newline at end of file