Feat v0.7.2 package runners ci gate (#56)
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-runners (push) Has been skipped
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m39s
CI/CD / test-sqlite (push) Successful in 2m55s
CI/CD / build-and-deploy (push) Successful in 29s
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-runners (push) Has been skipped
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m39s
CI/CD / test-sqlite (push) Successful in 2m55s
CI/CD / build-and-deploy (push) Successful in 29s
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #56.
This commit is contained in:
11
ci/Dockerfile.test-runner
Normal file
11
ci/Dockerfile.test-runner
Normal file
@@ -0,0 +1,11 @@
|
||||
# ci/Dockerfile.test-runner — Pre-built Playwright test runner
|
||||
#
|
||||
# Build & push (one-time, repeat when Playwright version bumps):
|
||||
# docker build -t registry.gobha.me:5000/ci-test-runner:latest -f ci/Dockerfile.test-runner .
|
||||
# docker push registry.gobha.me:5000/ci-test-runner:latest
|
||||
#
|
||||
# The CI compose override uses this image directly, avoiding npm install
|
||||
# on every run. Only the ci/ scripts are COPY'd at compose build time.
|
||||
FROM mcr.microsoft.com/playwright:v1.52.0-noble
|
||||
WORKDIR /work
|
||||
RUN npm init -y && npm install playwright@1.52.0
|
||||
56
ci/run-surface-tests.sh
Executable file
56
ci/run-surface-tests.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
# ═══════════════════════════════════════════════
|
||||
# Surface Test Runner — CI Entrypoint
|
||||
# ═══════════════════════════════════════════════
|
||||
#
|
||||
# Authenticates as admin, navigates to the test-runners surface
|
||||
# via Playwright, runs all test suites, and asserts zero failures.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Server running at $SERVER_URL (default: http://localhost:3000)
|
||||
# - npx playwright install chromium
|
||||
# - ADMIN_USER / ADMIN_PASS env vars (default: admin/admin)
|
||||
#
|
||||
# Exit codes: 0 = all tests passed, 1 = failures detected
|
||||
set -euo pipefail
|
||||
|
||||
SERVER_URL="${SERVER_URL:-http://localhost:3000}"
|
||||
ADMIN_USER="${ADMIN_USER:-admin}"
|
||||
ADMIN_PASS="${ADMIN_PASS:-admin}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${YELLOW}═══ Surface Test Runner ═══${NC}"
|
||||
echo " Server: ${SERVER_URL}"
|
||||
|
||||
# ── Authenticate ─────────────────────────────
|
||||
echo -e "${YELLOW}Authenticating...${NC}"
|
||||
TOKEN=$(curl -sf -X POST "${SERVER_URL}/api/v1/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"login\":\"${ADMIN_USER}\",\"password\":\"${ADMIN_PASS}\"}" \
|
||||
| node -e "process.stdin.resume(); let d=''; process.stdin.on('data',c=>d+=c); process.stdin.on('end',()=>{try{console.log(JSON.parse(d).token)}catch(e){process.exit(1)}})")
|
||||
|
||||
if [ -z "$TOKEN" ]; then
|
||||
echo -e "${RED}Failed to authenticate${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}Authenticated${NC}"
|
||||
|
||||
# ── Run via Playwright ───────────────────────
|
||||
echo -e "${YELLOW}Running surface tests via Playwright...${NC}"
|
||||
node "$(dirname "$0")/surface-test-driver.js" \
|
||||
--server="${SERVER_URL}" \
|
||||
--token="${TOKEN}"
|
||||
|
||||
EXIT_CODE=$?
|
||||
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
echo -e "${GREEN}═══ All surface tests passed ═══${NC}"
|
||||
else
|
||||
echo -e "${RED}═══ Surface tests FAILED ═══${NC}"
|
||||
fi
|
||||
|
||||
exit $EXIT_CODE
|
||||
139
ci/surface-test-driver.js
Normal file
139
ci/surface-test-driver.js
Normal file
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Surface Test Driver — Playwright-based CI runner
|
||||
*
|
||||
* Navigates to /s/test-runners as admin, clicks "Run All",
|
||||
* waits for completion, then fetches results from the API.
|
||||
*
|
||||
* Usage:
|
||||
* node ci/surface-test-driver.js --server=http://localhost:3000 --token=TOKEN
|
||||
*
|
||||
* Exit codes: 0 = all passed, 1 = failures
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
// Parse args
|
||||
const args = {};
|
||||
process.argv.slice(2).forEach(a => {
|
||||
const [k, v] = a.replace(/^--/, '').split('=');
|
||||
args[k] = v;
|
||||
});
|
||||
|
||||
const SERVER = args.server || 'http://localhost:3000';
|
||||
const TOKEN = args.token || '';
|
||||
|
||||
if (!TOKEN) {
|
||||
console.error('ERROR: --token is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext();
|
||||
|
||||
// Set auth cookie — name must match server's SetCookie ("arm_token")
|
||||
await context.addCookies([{
|
||||
name: 'arm_token',
|
||||
value: TOKEN,
|
||||
domain: new URL(SERVER).hostname,
|
||||
path: '/',
|
||||
}]);
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
// Collect console errors
|
||||
const consoleErrors = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') consoleErrors.push(msg.text());
|
||||
});
|
||||
|
||||
try {
|
||||
// Navigate to test runners surface
|
||||
console.log('Navigating to test-runners surface...');
|
||||
await page.goto(SERVER + '/s/test-runners', { waitUntil: 'networkidle', timeout: 30000 });
|
||||
|
||||
// Wait for the "Run All" button — suites load asynchronously so the
|
||||
// button only appears once runners have registered their suites.
|
||||
console.log('Waiting for Run All button...');
|
||||
const runAllBtn = await page.waitForSelector('button:has-text("Run All")', { timeout: 60000 })
|
||||
.catch(() => null);
|
||||
if (!runAllBtn) {
|
||||
// Dump page content for debugging
|
||||
const text = await page.textContent('body').catch(() => '(empty)');
|
||||
console.error('ERROR: "Run All" button not found. Page text:', text.substring(0, 500));
|
||||
await browser.close();
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Clicking Run All...');
|
||||
await runAllBtn.click();
|
||||
|
||||
// Wait for results — poll until running state clears
|
||||
console.log('Waiting for test completion...');
|
||||
// The running indicator disappears when tests finish
|
||||
// Max wait: 5 minutes
|
||||
const maxWait = 300000;
|
||||
const start = Date.now();
|
||||
let done = false;
|
||||
|
||||
while (!done && (Date.now() - start) < maxWait) {
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check if results are available via API
|
||||
try {
|
||||
const resp = await page.evaluate(async (serverUrl) => {
|
||||
const r = await fetch(serverUrl + '/api/v1/admin/test-runners/results', {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (r.status === 200) return await r.json();
|
||||
return null;
|
||||
}, SERVER);
|
||||
|
||||
if (resp && resp.summary) {
|
||||
done = true;
|
||||
console.log('\n═══ Results ═══');
|
||||
console.log(` Total: ${resp.summary.total}`);
|
||||
console.log(` Passed: ${resp.summary.passed}`);
|
||||
console.log(` Failed: ${resp.summary.failed}`);
|
||||
console.log(` Warned: ${resp.summary.warned}`);
|
||||
console.log(` Skipped: ${resp.summary.skipped}`);
|
||||
console.log(` Duration: ${resp.duration_ms}ms`);
|
||||
|
||||
if (resp.summary.failed > 0) {
|
||||
console.log('\n═══ Failures ═══');
|
||||
for (const suite of (resp.suites || [])) {
|
||||
for (const test of (suite.tests || [])) {
|
||||
if (test.status === 'failed') {
|
||||
console.log(` ✗ ${suite.name} > ${test.name}: ${test.detail || 'failed'}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
await browser.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (e) {
|
||||
// Results not ready yet
|
||||
}
|
||||
}
|
||||
|
||||
if (!done) {
|
||||
console.error('ERROR: Tests did not complete within timeout');
|
||||
await browser.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error('Driver error:', e.message);
|
||||
if (consoleErrors.length > 0) {
|
||||
console.error('\nBrowser console errors:');
|
||||
consoleErrors.forEach(e => console.error(' ' + e));
|
||||
}
|
||||
await browser.close();
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
21
ci/wait-for-healthy.sh
Executable file
21
ci/wait-for-healthy.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# ═══════════════════════════════════════════════
|
||||
# Wait for Armature server to be healthy
|
||||
# ═══════════════════════════════════════════════
|
||||
# Usage: ./ci/wait-for-healthy.sh [URL] [MAX_RETRIES]
|
||||
set -euo pipefail
|
||||
|
||||
HOST="${1:-http://localhost:3000}"
|
||||
MAX_RETRIES="${2:-60}"
|
||||
|
||||
echo "Waiting for ${HOST} to be healthy..."
|
||||
for i in $(seq 1 "$MAX_RETRIES"); do
|
||||
if curl -sf --connect-timeout 2 "${HOST}/api/v1/health" -o /dev/null 2>/dev/null; then
|
||||
echo "Server healthy after ${i}s"
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Server not healthy after ${MAX_RETRIES}s"
|
||||
exit 1
|
||||
Reference in New Issue
Block a user