commit ba2b2721eb6a1a9f14313ea1a2bc5b0e6d8e83f0 Author: Jānis Jansons Date: Tue Dec 10 21:34:36 2024 +0200 Initial commit diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..2812391 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f5e9e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Distribution +dist +out +build +node_modules + +# Logs +*.log* +logs + +# Environment +.env + +# Misc +.DS_Store +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e1533c2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "deno.enable": true, + "deno.lint": true +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..5a50110 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# SAP BAS dev space keep-alive + +This script will start a SAP BAS dev space and keep it running. + +It uses Playwright for browser automation and Deno 2 as a runtime. + +You can use SSH port forwarding with the started dev space to access SAP proxy locally for +remote system access for a true local development. + +## Setup and usage + +Copy the `example.env` to `.env` and fill in the necessary details for BAS access. + +```sh +brew install deno +deno run -A npm:playwright install +./run.sh +``` + +## Debugging + +Set `DENO_HEADLESS=false` to see the browser as the actions are taken. diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..e0d391e --- /dev/null +++ b/deno.json @@ -0,0 +1,16 @@ +{ + "tasks": { + "dev": "deno run --watch main.ts" + }, + "imports": { + "@std/assert": "jsr:@std/assert@1" + }, + "nodeModulesDir": "auto", + "fmt": { + "useTabs": false, + "lineWidth": 90, + "indentWidth": 2, + "semiColons": false, + "singleQuote": false + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..f8e278d --- /dev/null +++ b/deno.lock @@ -0,0 +1,56 @@ +{ + "version": "4", + "specifiers": { + "jsr:@std/assert@1": "1.0.7", + "jsr:@std/internal@^1.0.5": "1.0.5", + "npm:@playwright/test@*": "1.48.2", + "npm:@types/node@*": "22.5.4", + "npm:playwright@*": "1.48.2" + }, + "jsr": { + "@std/assert@1.0.7": { + "integrity": "64ce9fac879e0b9f3042a89b3c3f8ccfc9c984391af19e2087513a79d73e28c3", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/internal@1.0.5": { + "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" + } + }, + "npm": { + "@playwright/test@1.48.2": { + "integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==", + "dependencies": [ + "playwright" + ] + }, + "@types/node@22.5.4": { + "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", + "dependencies": [ + "undici-types" + ] + }, + "fsevents@2.3.2": { + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==" + }, + "playwright-core@1.48.2": { + "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==" + }, + "playwright@1.48.2": { + "integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==", + "dependencies": [ + "fsevents", + "playwright-core" + ] + }, + "undici-types@6.19.8": { + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + } + }, + "workspace": { + "dependencies": [ + "jsr:@std/assert@1" + ] + } +} diff --git a/example.env b/example.env new file mode 100644 index 0000000..12daeb6 --- /dev/null +++ b/example.env @@ -0,0 +1,9 @@ +BAS_URL=https://yolo.eu10cf.applicationstudio.cloud.sap + +BAS_PROFILE=yoloswag.accounts.ondemand.com +#BAS_PROFILE=Default Identity Provider + +BAS_USERNAME=user.name@company.lol +BAS_PASSWORD= + +BAS_DEVSPACE=dev diff --git a/main.ts b/main.ts new file mode 100644 index 0000000..3a5f3c4 --- /dev/null +++ b/main.ts @@ -0,0 +1,146 @@ +import playwright from "npm:playwright" + +const devSpace = Deno.env.get("BAS_DEVSPACE")! +const headless = Deno.env.get("DENO_HEADLESS")! === "true" ? true : false + +if (import.meta.main) { + while (true) { + console.log(`Starting new loop`) + await main(devSpace) + await new Promise((resolve) => setTimeout(resolve, 3 * 1000)) + } +} + +async function main(devSpacename: string) { + const browser = await playwright.chromium.launch({ + headless: headless, + }) + const context = await browser.newContext() + const page = await context.newPage() + + console.log(`Logging in to BAS`) + await page.goto(Deno.env.get("BAS_URL")!) + await page.locator(`text=${Deno.env.get("BAS_PROFILE")!}`).click() + await page + .getByLabel("Email or User Name") + .fill(Deno.env.get("BAS_USERNAME")!) + await page.getByLabel("Password").fill(Deno.env.get("BAS_PASSWORD")!) + await page.waitForTimeout(500) + await page.getByText("Keep me signed in").check() + await page.waitForTimeout(500) + await page.getByText("Continue").click() + await page.waitForLoadState() + await page.waitForURL("**/index.html") + + // Request manager page directly (without iframe) + console.log(`Loading management page`) + await page.goto(Deno.env.get("BAS_URL")! + "/workspace-manager-ui/") + await page.waitForTimeout(2000) + console.log(`Loaded management page`) + + // const pageSource = await page.content(); + // console.log(JSON.stringify(pageSource)); + + const devSpaceEl = await findCorrectDevspace(page, devSpacename) + if (!devSpaceEl) { + throw Error(`Could not find defined devspace: ${devSpacename}`) + } + + await startDevSpace(devSpaceEl, page) + + await loadVSCode(devSpaceEl, devSpacename) + + console.log(`Waiting for 5 minutes and then starting again...`) + await page.waitForTimeout(5 * 60 * 1000) + await page.close() + console.log(`Done waiting, closing`) + + if (!headless) { + // Close on done + await new Promise((resolve) => { + page.on("close", resolve) + }) + } + + await context.close() + await browser.close() +} + +async function findCorrectDevspace( + page: playwright.Page, + devSpacename: string, +) { + const rows = await page + .locator('div.dev-spaces-row:has-text("' + devSpacename + '")') + .all() + + let findFullStack = false + if (rows.length > 1) { + console.log(`Found multiple "${devSpacename}" dev spaces`) + findFullStack = true + } + + for (const el of rows) { + console.log(`Checking dev space row`) + const fullstack = await el + .locator( + 'div.left-row:has-text("Full-Stack Application Using Productivity Tools")', + ) + .isVisible() + + if (fullstack) { + console.log(`Found the fullstack dev space row`) + } + + // Skip non-fullstack lines if they exist for that name + if (findFullStack && !fullstack) { + continue + } + return el + } +} + +async function startDevSpace( + devSpaceEl: playwright.Locator, + page: playwright.Page, +) { + const running = await devSpaceEl + .locator('div.text-center a:has-text("RUNNING")') + .isVisible() + + if (running) { + console.log(`Dev Space running`) + return true + } + + const stopped = await devSpaceEl + .locator('div.text-center a:has-text("STOPPED")') + .isVisible() + + if (stopped) { + await devSpaceEl.locator('button[id^="startButton"]').click() + await page.waitForTimeout(5 * 1000) + } + + const stopping = await devSpaceEl + .locator('div.text-center a:has-text("STOPPING")') + .isVisible() + + const starting = await devSpaceEl + .locator('div.text-center a:has-text("STARTING")') + .isVisible() + + if (stopping || starting) { + await page.waitForTimeout(2 * 1000) + console.log(`Waiting for dev space to ${stopping ? "stop" : "start"}...`) + await startDevSpace(devSpaceEl, page) + } +} + +async function loadVSCode( + devSpaceEl: playwright.Locator, + devSpacename: string, +) { + console.log(`Opening VSCode dev space`) + await devSpaceEl.locator('a:has-text("' + devSpacename + '")').click() +} diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..e08f635 --- /dev/null +++ b/run.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +deno run --env-file=.env --allow-net --allow-read --allow-env --allow-sys --allow-write --allow-run --allow-ffi main.ts