Test my staging site
Clone your repository in the session's shell, run its tests against your staging site, and open every page that failed in the session's browser for a screenshot.
When to use it: You want your own tests run against your staging site on a clean machine, with screenshots of what failed.
| Uses | exec with cwd and env (git, npm), the actions API (goto, screenshot) |
|---|---|
| Needs | a plan with shell sessions; no model |
| Site | your repository (REPO_URL) and staging site (STAGING_URL). The runner serves a small repository of site tests and a stand-in staging site with one broken page |
| Output | output/result.json and output/failures/N.png |
| Languages | Node and Python |
The code
The example as it is in its folder. Both versions do the same job and write the same output/, so one result check covers both.
/** * Test my staging site: clone your repository in the session's shell, run its tests against your staging site, and * open every page that failed in the session's browser for a screenshot. * * REPO_URL=… STAGING_URL=… npx tsx node/index.ts (BOXLINE_API_KEY; TEST_COMMAND, default "npm test") * * The tests get STAGING_URL in their environment. Pages that failed are the staging addresses the test output names. * Writes output/result.json and output/failures/N.png. */import { mkdirSync, writeFileSync } from "node:fs";import { join } from "node:path";import { Boxline } from "@boxline/sdk";const out = process.env.OUTPUT_DIR ?? "output";const repo = process.env.REPO_URL;const staging = process.env.STAGING_URL;const testCommand = process.env.TEST_COMMAND ?? "npm test";if (!repo || !staging) throw new Error("Set REPO_URL (a repository you may clone) and STAGING_URL (the site its tests should check)");const bx = new Boxline();const shown = (u: string) => u.replace(/\/\/[^/@]*@/, "//"); // never print a token that is part of the addressconst session = await bx.sessions.create({ shell: true, timeout: 900, userMetadata: { example: "test-my-staging-site" } });console.log(`Session: ${session.id}`);try { // 1. The shell: clone, install, test. Addresses go in as environment variables, never into the command text. const env = { REPO_URL: repo, STAGING_URL: staging }; // A shallow clone where the server offers it (plain-HTTP servers do not), else a full one. const clone = await session.exec('rm -rf app && { git clone --quiet --depth 1 "$REPO_URL" app 2>/dev/null || git clone --quiet "$REPO_URL" app; }', { env, timeoutMs: 180_000 }); if (clone.exitCode !== 0) throw new Error(`git clone failed: ${clone.stderr.trim()}`); console.log(`Cloned ${shown(repo)}`); const install = await session.exec("[ -f package.json ] && npm install --no-audit --no-fund --silent || true", { cwd: "app", timeoutMs: 600_000 }); if (install.exitCode !== 0) console.log(`npm install: ${install.stderr.trim()}`); console.log(`Running "${testCommand}" against ${staging}`); const tests = await session.exec(testCommand, { cwd: "app", env, timeoutMs: 600_000 }); const output = `${tests.stdout}\n${tests.stderr}`; const count = (what: string) => Number(new RegExp(`^\\s*(?:#|ℹ)\\s*${what}\\s+(\\d+)`, "m").exec(output)?.[1] ?? NaN); console.log(`Tests ${tests.exitCode === 0 ? "passed" : `failed (exit ${tests.exitCode})`}: ${count("pass")} passed, ${count("fail")} failed`); // 2. The browser: open each staging page the failures name, and screenshot it. const esc = staging.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const urls = [...new Set([...output.matchAll(new RegExp(`${esc}[^\\s"'<>)\\]]*`, "g"))].map((m) => m[0].replace(/[.,:;]+$/, "")))]; mkdirSync(join(out, "failures"), { recursive: true }); const failures: { url: string; status: number | null; title: string; screenshot: string }[] = []; for (const [i, url] of urls.entries()) { const page = await session.goto(url); const shot = await session.screenshot(); const file = `failures/${i + 1}.png`; writeFileSync(join(out, file), Buffer.from(shot.data, "base64")); failures.push({ url, status: page.status, title: page.title, screenshot: file }); console.log(` failed: ${url} (HTTP ${page.status}, "${page.title}") → output/${file}`); } writeFileSync( join(out, "result.json"), JSON.stringify({ repo: shown(repo), staging, command: testCommand, exitCode: tests.exitCode, passed: count("pass"), failed: count("fail"), failures, outputTail: output.trim().slice(-3000) }, null, 2), );} finally { await session.stop();}"""Test my staging site: clone your repository in the session's shell, run its tests against your staging site, andopen every page that failed in the session's browser for a screenshot. REPO_URL=… STAGING_URL=… python python/main.py (BOXLINE_API_KEY; TEST_COMMAND, default "npm test")The tests get STAGING_URL in their environment. Pages that failed are the staging addresses the test output names.Writes output/result.json and output/failures/N.png."""import jsonimport osimport refrom pathlib import Pathfrom boxline import Boxlineout = Path(os.environ.get("OUTPUT_DIR", "output"))repo = os.environ.get("REPO_URL")staging = os.environ.get("STAGING_URL")test_command = os.environ.get("TEST_COMMAND", "npm test")if not repo or not staging: raise SystemExit("Set REPO_URL (a repository you may clone) and STAGING_URL (the site its tests should check)")bx = Boxline()def shown(url: str) -> str: return re.sub(r"//[^/@]*@", "//", url) # never print a token that is part of the addresswith bx.sessions.create(shell=True, timeout=900, user_metadata={"example": "test-my-staging-site"}) as session: print(f"Session: {session.id}", flush=True) # 1. The shell: clone, install, test. Addresses go in as environment variables, never into the command text. env = {"REPO_URL": repo, "STAGING_URL": staging} # A shallow clone where the server offers it (plain-HTTP servers do not), else a full one. clone = session.exec('rm -rf app && { git clone --quiet --depth 1 "$REPO_URL" app 2>/dev/null || git clone --quiet "$REPO_URL" app; }', env=env, timeout_ms=180_000) if clone["exitCode"] != 0: raise SystemExit(f"git clone failed: {clone['stderr'].strip()}") print(f"Cloned {shown(repo)}") install = session.exec("[ -f package.json ] && npm install --no-audit --no-fund --silent || true", cwd="app", timeout_ms=600_000) if install["exitCode"] != 0: print(f"npm install: {install['stderr'].strip()}") print(f'Running "{test_command}" against {staging}') tests = session.exec(test_command, cwd="app", env=env, timeout_ms=600_000) output = f"{tests['stdout']}\n{tests['stderr']}" def count(what: str): m = re.search(rf"^\s*(?:#|ℹ)\s*{what}\s+(\d+)", output, re.M) return int(m.group(1)) if m else None state = "passed" if tests["exitCode"] == 0 else f"failed (exit {tests['exitCode']})" print(f"Tests {state}: {count('pass')} passed, {count('fail')} failed") # 2. The browser: open each staging page the failures name, and screenshot it. urls = list(dict.fromkeys(m.group(0).rstrip(".,:;") for m in re.finditer(re.escape(staging) + r"""[^\s"'<>)\]]*""", output))) (out / "failures").mkdir(parents=True, exist_ok=True) failures = [] for i, url in enumerate(urls, start=1): page = session.goto(url) file = f"failures/{i}.png" (out / file).write_bytes(session.screenshot()) failures.append({"url": url, "status": page["status"], "title": page["title"], "screenshot": file}) print(f' failed: {url} (HTTP {page["status"]}, "{page["title"]}") -> output/{file}') result = {"repo": shown(repo), "staging": staging, "command": test_command, "exitCode": tests["exitCode"], "passed": count("pass"), "failed": count("fail"), "failures": failures, "outputTail": output.strip()[-3000:]} (out / "result.json").write_text(json.dumps(result, indent=2, ensure_ascii=False))Inputs
Environment variables the example reads:
REPO_URL: a repository you may clone (required); for a private one, a read-only token in the address, kept in the environment.STAGING_URL: the site its tests check (required); the tests get it as STAGING_URL.TEST_COMMAND: how to run the tests (defaultnpm test).
Run it
Get the examples with git clone https://github.com/Boxline-dev/examples.git and open this one: cd examples/test-my-staging-site. Put your API key in BOXLINE_API_KEY (and BOXLINE_API_URL for another API), then run either version from the example’s folder. Both write to output/ and stop their sessions when they finish.
# Node 18 or newer, in the example's folderexport BOXLINE_API_KEY=… # your API keynpm install @boxline/sdk tsxnpx tsx node/index.ts# Python 3.9 or newer, in the example's folderexport BOXLINE_API_KEY=… # your API keypip install boxline-sdkpython python/main.pyThen check the result, from the same folder: npx tsx check.ts output. It exits with 0 only when the result is right.
What its check proves
check.ts reads what the example wrote and passes only when the result is right, not merely when the example ran.
On the stand-in, 2 tests passed and 1 failed, and the failing page (/staging/pricing, HTTP 500) was opened and screenshotted.
Source
github.com/Boxline-dev/examples/test-my-staging-site on GitHub, with its README.md, check.ts and .env.example. The examples are open source. See all examples.
These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.