consolidation of some of the modules, localization fixes, product advisories work, qa work

This commit is contained in:
master
2026-03-05 03:54:22 +02:00
parent 7bafcc3eef
commit 8e1cb9448d
3878 changed files with 72600 additions and 46861 deletions

View File

@@ -0,0 +1,90 @@
#!/usr/bin/env node
import { execFileSync, execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const moduleRoot = path.resolve(__dirname, '..');
const outDir = path.join(moduleRoot, 'dist');
const bundleDir = path.join(moduleRoot, 'out');
const bundleFile = path.join(bundleDir, 'devportal-offline.tar.gz');
const specPath = path.join(moduleRoot, 'public', 'api', 'stella.yaml');
const sdkDir = path.join(moduleRoot, 'public', 'sdk');
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
function ensureSpec() {
if (!fs.existsSync(specPath)) {
throw new Error(`[devportal:offline] missing spec at ${specPath}; run npm run sync:spec`);
}
}
function ensureSdkFolder() {
if (!fs.existsSync(sdkDir)) {
fs.mkdirSync(sdkDir, { recursive: true });
fs.writeFileSync(
path.join(sdkDir, 'README.txt'),
'Place SDK archives here (e.g., stellaops-sdk-node-vX.Y.Z.tgz, stellaops-sdk-python-vX.Y.Z.tar.gz).\n'
);
}
}
function runBuild() {
console.log('[devportal:offline] running astro build');
if (process.platform === 'win32') {
execSync('npm run build', { stdio: 'inherit', cwd: moduleRoot, shell: true });
return;
}
execFileSync(npmCmd, ['run', 'build'], { stdio: 'inherit', cwd: moduleRoot });
}
function packageBundle() {
fs.mkdirSync(bundleDir, { recursive: true });
if (fs.existsSync(bundleFile)) {
fs.rmSync(bundleFile);
}
const deterministicArgs = [
'--sort=name',
'--mtime', '@0',
'--owner', '0',
'--group', '0',
'--numeric-owner',
'-czf', bundleFile,
'-C', moduleRoot,
'dist',
'public/api/stella.yaml',
'public/sdk'
];
const portableArgs = [
'-czf', bundleFile,
'-C', moduleRoot,
'dist',
'public/api/stella.yaml',
'public/sdk'
];
console.log(`[devportal:offline] creating ${bundleFile}`);
try {
execFileSync('tar', deterministicArgs, { stdio: 'inherit' });
} catch (error) {
// Some tar implementations (notably Windows bsdtar) don't support GNU deterministic flags.
if (process.platform !== 'win32') {
throw error;
}
console.warn('[devportal:offline] deterministic tar flags unsupported on host tar; falling back to portable archive flags.');
execFileSync('tar', portableArgs, { stdio: 'inherit' });
}
const size = (fs.statSync(bundleFile).size / 1024 / 1024).toFixed(2);
console.log(`[devportal:offline] bundle ready (${size} MiB)`);
}
function main() {
ensureSpec();
ensureSdkFolder();
runBuild();
packageBundle();
}
main();

View File

@@ -0,0 +1,153 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import { setTimeout as wait } from 'node:timers/promises';
import http from 'node:http';
import https from 'node:https';
import { LinkChecker } from 'linkinator';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const HOST = process.env.DEVPORT_HOST ?? '127.0.0.1';
const PORT = process.env.DEVPORT_PORT ?? '4321';
const BASE = `http://${HOST}:${PORT}`;
const __filename = fileURLToPath(import.meta.url);
const moduleRoot = path.resolve(path.dirname(__filename), '..');
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
function killPreviewIfRunning() {
if (process.platform === 'win32') return;
const child = spawn('pkill', ['-f', `astro preview --host ${HOST} --port ${PORT}`], { stdio: 'ignore' });
child.on('error', () => {
// best effort
});
}
async function startPreview() {
return new Promise((resolve, reject) => {
const child = spawn(process.platform === 'win32' ? 'npm' : npmCmd, ['run', 'preview', '--', '--host', HOST, '--port', PORT], {
cwd: moduleRoot,
stdio: 'ignore',
shell: process.platform === 'win32',
});
child.once('error', reject);
resolve(child);
});
}
async function waitForServer() {
const url = `${BASE}/`;
const clientFor = (u) => (u.protocol === 'https:' ? https : http);
const probe = () =>
new Promise((resolve, reject) => {
const target = new URL(url);
const req = clientFor(target).request(
target,
{ method: 'GET', timeout: 2000 },
(res) => {
resolve(res.statusCode ?? 503);
res.resume();
}
);
req.on('error', reject);
req.on('timeout', () => {
req.destroy(new Error('timeout'));
});
req.end();
});
for (let i = 0; i < 120; i++) {
try {
const status = await probe();
if (status < 500) {
await wait(500); // small buffer after first success
return;
}
} catch {
// keep polling
}
await wait(500);
}
// If we couldn't confirm readiness, proceed; link checker will surface real failures.
}
async function checkLinks() {
const checker = new LinkChecker();
const failures = [];
checker.on('link', (event) => {
if (event.state !== 'BROKEN') return;
failures.push({ url: event.url, status: event.status });
});
await checker.check({
path: `${BASE}/docs/`,
recurse: true,
maxDepth: 3,
concurrency: 16,
linksToSkip: [/mailto:/, /tel:/, /devportal\\.stellaops\\.local/, /git\\.stella-ops\\.org/],
});
// Astro preview on some hosts can serve directory pages only via /index.html.
// For internal trailing-slash links, re-probe /index.html before flagging broken.
const normalized = [];
for (const failure of failures) {
if (
failure.url.startsWith(BASE) &&
failure.url.endsWith('/')
) {
const indexUrl = `${failure.url}index.html`;
try {
const target = new URL(indexUrl);
const status = await new Promise((resolve, reject) => {
const req = (target.protocol === 'https:' ? https : http).request(
target,
{ method: 'GET', timeout: 2000 },
(res) => {
resolve(res.statusCode ?? 503);
res.resume();
}
);
req.on('error', reject);
req.on('timeout', () => req.destroy(new Error('timeout')));
req.end();
});
if (status < 400) {
continue;
}
} catch {
// keep original failure
}
}
normalized.push(failure);
}
const filtered = normalized.filter(
(f) =>
!f.url.includes('devportal.stellaops.local') &&
!f.url.includes('git.stella-ops.org')
);
if (filtered.length > 0) {
console.error('[links] broken links found');
filtered.forEach((f) => console.error(`- ${f.status} ${f.url}`));
process.exitCode = 1;
} else {
console.log('[links] no broken links detected');
}
}
async function main() {
killPreviewIfRunning();
const server = await startPreview();
try {
await waitForServer();
await checkLinks();
} finally {
server.kill('SIGINT');
}
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});

View File

@@ -0,0 +1,79 @@
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const moduleRoot = path.resolve(path.dirname(__filename), '..');
const distDir = path.join(moduleRoot, 'dist');
function folderSize(dir) {
let total = 0;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
total += folderSize(full);
} else {
total += fs.statSync(full).size;
}
}
return total;
}
function largestFile(dir) {
let max = { size: 0, file: '' };
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
const child = largestFile(full);
if (child.size > max.size) max = child;
} else {
const size = fs.statSync(full).size;
if (size > max.size) {
max = { size, file: full };
}
}
}
return max;
}
function formatMB(bytes) {
return (bytes / 1024 / 1024).toFixed(2);
}
function main() {
if (!fs.existsSync(distDir)) {
console.error('[budget] dist/ not found; run `npm run build` first');
process.exitCode = 1;
return;
}
const total = folderSize(distDir);
const largest = largestFile(distDir);
const budgetTotal = 30 * 1024 * 1024; // 30 MB
const budgetSingle = 1 * 1024 * 1024; // 1 MB
console.log(`[budget] dist size ${formatMB(total)} MiB (budget <= ${formatMB(budgetTotal)} MiB)`);
console.log(`[budget] largest file ${formatMB(largest.size)} MiB -> ${path.relative(moduleRoot, largest.file)} (budget <= ${formatMB(budgetSingle)} MiB)`);
let fail = false;
if (total > budgetTotal) {
console.error('[budget] total size exceeds budget');
fail = true;
}
if (largest.size > budgetSingle) {
console.error('[budget] single-asset size exceeds budget');
fail = true;
}
if (fail) {
process.exitCode = 1;
} else {
console.log('[budget] budgets satisfied');
}
}
main();

View File

@@ -0,0 +1,156 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import { setTimeout as wait } from 'node:timers/promises';
import http from 'node:http';
import https from 'node:https';
import { execSync } from 'node:child_process';
import { chromium } from 'playwright';
import AxeBuilder from '@axe-core/playwright';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const HOST = process.env.DEVPORT_HOST ?? '127.0.0.1';
const PORT = process.env.DEVPORT_PORT ?? '4321';
const BASE = `http://${HOST}:${PORT}`;
const PAGES = ['/docs/', '/docs/api-reference/', '/docs/try-it-console/'];
const __filename = fileURLToPath(import.meta.url);
const moduleRoot = path.resolve(path.dirname(__filename), '..');
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
function hasSystemDeps() {
if (process.platform === 'win32') {
return false;
}
try {
const out = execSync('ldconfig -p', { encoding: 'utf-8' });
return out.includes('libnss3') && out.includes('libnspr4') && out.match(/libasound2|libasound\.so/);
} catch {
return false;
}
}
function killPreviewIfRunning() {
if (process.platform === 'win32') return;
const child = spawn('pkill', ['-f', `astro preview --host ${HOST} --port ${PORT}`], { stdio: 'ignore' });
child.on('error', () => {
// best effort
});
}
async function startPreview() {
return new Promise((resolve, reject) => {
const child = spawn(process.platform === 'win32' ? 'npm' : npmCmd, ['run', 'preview', '--', '--host', HOST, '--port', PORT], {
cwd: moduleRoot,
stdio: 'inherit',
shell: process.platform === 'win32',
});
child.once('error', reject);
resolve(child);
});
}
async function waitForServer() {
const url = `${BASE}/`;
const clientFor = (u) => (u.protocol === 'https:' ? https : http);
const probe = () =>
new Promise((resolve, reject) => {
const target = new URL(url);
const req = clientFor(target).request(
target,
{ method: 'GET', timeout: 2000 },
(res) => {
resolve(res.statusCode ?? 503);
res.resume();
}
);
req.on('error', reject);
req.on('timeout', () => req.destroy(new Error('timeout')));
req.end();
});
for (let i = 0; i < 120; i++) {
try {
const status = await probe();
if (status < 500) {
await wait(500);
return;
}
} catch {
// keep polling
}
await wait(500);
}
// proceed even if probe failed; a11y run will surface real issues
}
async function runA11y() {
let browser;
try {
browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-dev-shm-usage'] });
} catch (err) {
console.warn('[a11y] skipped: Playwright browser failed to launch (missing system deps? libnss3/libnspr4/libasound2).', err.message);
return { skipped: true, failed: false };
}
const page = await browser.newPage();
const violationsAll = [];
for (const path of PAGES) {
const url = `${BASE}${path}`;
await page.goto(url, { waitUntil: 'networkidle' });
const axe = new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']);
const results = await axe.analyze();
if (results.violations.length > 0) {
violationsAll.push({ path, violations: results.violations });
}
}
await browser.close();
if (violationsAll.length > 0) {
console.error('[a11y] violations found');
for (const { path, violations } of violationsAll) {
console.error(`- ${path}`);
violations.forEach((v) => {
console.error(`${v.id}: ${v.description}`);
});
}
return { skipped: false, failed: true };
}
console.log('[a11y] no violations detected');
return { skipped: false, failed: false };
}
async function main() {
killPreviewIfRunning();
if (!hasSystemDeps()) {
console.warn('[a11y] skipped: host missing system deps (libnss3/libnspr4/libasound2).');
return;
}
const server = await startPreview();
try {
await waitForServer();
const result = await runA11y();
if (result?.failed) process.exitCode = 1;
} finally {
server.kill('SIGINT');
killPreviewIfRunning();
}
}
main().catch((err) => {
const msg = err?.message ?? '';
const missingDeps =
msg.includes('Host system is missing dependencies') ||
msg.includes('libnss3') ||
msg.includes('libnspr4') ||
msg.includes('libasound2');
if (missingDeps) {
console.warn('[a11y] skipped: host missing Playwright runtime deps (libnss3/libnspr4/libasound2).');
process.exitCode = 0;
return;
}
console.error(err);
process.exitCode = 1;
});

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const moduleRoot = path.resolve(__dirname, '..');
const repoRoot = path.resolve(moduleRoot, '..', '..', '..');
const sourceSpec = path.join(repoRoot, 'src/Api/StellaOps.Api.OpenApi/stella.yaml');
const targetDir = path.join(moduleRoot, 'public', 'api');
const targetSpec = path.join(targetDir, 'stella.yaml');
function hashFile(filePath) {
const hash = crypto.createHash('sha256');
hash.update(fs.readFileSync(filePath));
return hash.digest('hex');
}
if (!fs.existsSync(sourceSpec)) {
console.error(`[devportal:sync-spec] missing source spec at ${sourceSpec}`);
process.exitCode = 1;
process.exit();
}
fs.mkdirSync(targetDir, { recursive: true });
fs.copyFileSync(sourceSpec, targetSpec);
const sizeKb = (fs.statSync(targetSpec).size / 1024).toFixed(1);
const digest = hashFile(targetSpec).slice(0, 12);
console.log(`[devportal:sync-spec] copied aggregate spec -> public/api/stella.yaml (${sizeKb} KiB, sha256:${digest}...)`);