feat(graph-api): Add schema review notes for upcoming Graph API changes
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
feat(sbomservice): Add placeholder for SHA256SUMS in LNM v1 fixtures docs(devportal): Create README for SDK archives in public directory build(devportal): Implement offline bundle build script test(devportal): Add link checker script for validating links in documentation test(devportal): Create performance check script for dist folder size test(devportal): Implement accessibility check script using Playwright and Axe docs(devportal): Add SDK quickstart guide with examples for Node.js, Python, and cURL feat(excititor): Implement MongoDB storage for airgap import records test(findings): Add unit tests for export filters hash determinism feat(findings): Define attestation contracts for ledger web service feat(graph): Add MongoDB options and service collection extensions for graph indexing test(graph): Implement integration tests for MongoDB provider and service collection extensions feat(zastava): Define configuration options for Zastava surface secrets build(tests): Create script to run Concelier linkset tests with TRX output
This commit is contained in:
@@ -3,3 +3,5 @@ node_modules
|
||||
output
|
||||
.cache
|
||||
.DS_Store
|
||||
dist
|
||||
out
|
||||
|
||||
@@ -24,3 +24,4 @@ Deliver the StellaOps developer portal with interactive API reference, SDK docum
|
||||
- 3. Keep changes deterministic (stable ordering, timestamps, hashes) and align with offline/air-gap expectations.
|
||||
- 4. Coordinate doc updates, tests, and cross-guild communication whenever contracts or workflows change.
|
||||
- 5. Revert to `TODO` if you pause the task without shipping changes; leave notes in commit/PR descriptions for context.
|
||||
- 6. Use `npm run build:offline`, `npm run test:a11y`, `npm run lint:links`, and `npm run budget:dist` on a fast (non-NTFS) volume before shipping DevPortal changes; ensure `npm run sync:spec` ran first.
|
||||
|
||||
@@ -7,6 +7,6 @@ Keep this file in sync with `docs/implplan/SPRINT_0206_0001_0001_devportal.md`.
|
||||
| DEVPORT-62-001 | DONE | Astro/Starlight scaffold + aggregate spec + nav/search. | 2025-11-22 |
|
||||
| DEVPORT-62-002 | DONE | Schema viewer, examples, copy-curl, version selector. | 2025-11-22 |
|
||||
| DEVPORT-63-001 | DONE | Try-It console against sandbox; token onboarding UX. | 2025-11-22 |
|
||||
| DEVPORT-63-002 | TODO | Embed SDK snippets/quick starts from tested examples. | 2025-11-22 |
|
||||
| DEVPORT-64-001 | TODO | Offline bundle target with specs + SDK archives; zero external assets. | 2025-11-22 |
|
||||
| DEVPORT-64-002 | TODO | Accessibility tests, link checker, performance budgets. | 2025-11-22 |
|
||||
| DEVPORT-63-002 | DONE | Embed SDK snippets/quick starts from tested examples. | 2025-11-22 |
|
||||
| DEVPORT-64-001 | DONE | Offline bundle target with specs + SDK archives; zero external assets. | 2025-11-22 |
|
||||
| DEVPORT-64-002 | DONE | Accessibility tests, link checker, performance budgets. | 2025-11-22 |
|
||||
|
||||
@@ -37,6 +37,7 @@ export default defineConfig({
|
||||
{ slug: 'guides/getting-started' },
|
||||
{ slug: 'guides/navigation-search' },
|
||||
{ slug: 'guides/examples' },
|
||||
{ slug: 'guides/sdk-quickstarts' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
805
src/DevPortal/StellaOps.DevPortal.Site/package-lock.json
generated
805
src/DevPortal/StellaOps.DevPortal.Site/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,11 @@
|
||||
"preview": "astro preview",
|
||||
"check": "astro check",
|
||||
"sync:spec": "node scripts/sync-spec.mjs",
|
||||
"prepare:static": "npm run sync:spec && astro check"
|
||||
"prepare:static": "npm run sync:spec && astro check",
|
||||
"build:offline": "node scripts/build-offline.mjs",
|
||||
"test:a11y": "node scripts/run-a11y.mjs",
|
||||
"lint:links": "node scripts/check-links.mjs",
|
||||
"budget:dist": "node scripts/check-perf.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"rapidoc": "9.3.8"
|
||||
@@ -22,8 +26,11 @@
|
||||
"devDependencies": {
|
||||
"@astrojs/mdx": "4.3.12",
|
||||
"@astrojs/starlight": "0.36.2",
|
||||
"@axe-core/playwright": "4.9.0",
|
||||
"@playwright/test": "1.48.2",
|
||||
"@types/node": "24.10.1",
|
||||
"astro": "5.16.0",
|
||||
"linkinator": "6.1.2",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
Place SDK archives here for offline bundles.
|
||||
Expected filenames:
|
||||
- stellaops-sdk-node-vX.Y.Z.tgz
|
||||
- stellaops-sdk-python-vX.Y.Z.tar.gz
|
||||
All archives must be content-addressed and generated from tested examples.
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env node
|
||||
import { execFileSync } 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');
|
||||
|
||||
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');
|
||||
execFileSync('npm', ['run', 'build'], { stdio: 'inherit', cwd: moduleRoot });
|
||||
}
|
||||
|
||||
function packageBundle() {
|
||||
fs.mkdirSync(bundleDir, { recursive: true });
|
||||
if (fs.existsSync(bundleFile)) {
|
||||
fs.rmSync(bundleFile);
|
||||
}
|
||||
const args = [
|
||||
'--sort=name',
|
||||
'--mtime', '@0',
|
||||
'--owner', '0',
|
||||
'--group', '0',
|
||||
'--numeric-owner',
|
||||
'-czf', bundleFile,
|
||||
'-C', moduleRoot,
|
||||
'dist',
|
||||
'public/api/stella.yaml',
|
||||
'public/sdk'
|
||||
];
|
||||
console.log(`[devportal:offline] creating ${bundleFile}`);
|
||||
execFileSync('tar', args, { 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();
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from 'node:child_process';
|
||||
import { setTimeout as wait } from 'node:timers/promises';
|
||||
import { LinkChecker } from 'linkinator';
|
||||
|
||||
const HOST = process.env.DEVPORT_HOST ?? '127.0.0.1';
|
||||
const PORT = process.env.DEVPORT_PORT ?? '4321';
|
||||
const BASE = `http://${HOST}:${PORT}`;
|
||||
|
||||
async function startPreview() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn('npm', ['run', 'preview', '--', '--host', HOST, '--port', PORT], {
|
||||
cwd: new URL('..', import.meta.url).pathname,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
child.once('error', reject);
|
||||
resolve(child);
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForServer() {
|
||||
const url = `${BASE}/`;
|
||||
for (let i = 0; i < 60; i++) {
|
||||
try {
|
||||
const res = await fetch(url, { method: 'GET' });
|
||||
if (res.ok) return;
|
||||
} catch {
|
||||
// keep polling
|
||||
}
|
||||
await wait(500);
|
||||
}
|
||||
throw new Error('Preview server did not become ready');
|
||||
}
|
||||
|
||||
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, recurse: true, maxDepth: 3, concurrency: 16, skip: [/mailto:/, /tel:/] });
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error('[links] broken links found');
|
||||
failures.forEach((f) => console.error(`- ${f.status} ${f.url}`));
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log('[links] no broken links detected');
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const server = await startPreview();
|
||||
try {
|
||||
await waitForServer();
|
||||
await checkLinks();
|
||||
} finally {
|
||||
server.kill('SIGINT');
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const moduleRoot = path.resolve(new URL('..', import.meta.url).pathname);
|
||||
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();
|
||||
81
src/DevPortal/StellaOps.DevPortal.Site/scripts/run-a11y.mjs
Normal file
81
src/DevPortal/StellaOps.DevPortal.Site/scripts/run-a11y.mjs
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from 'node:child_process';
|
||||
import { setTimeout as wait } from 'node:timers/promises';
|
||||
import { chromium } from 'playwright';
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
|
||||
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/'];
|
||||
|
||||
async function startPreview() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn('npm', ['run', 'preview', '--', '--host', HOST, '--port', PORT], {
|
||||
cwd: new URL('..', import.meta.url).pathname,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
child.once('error', reject);
|
||||
resolve(child);
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForServer() {
|
||||
const url = `${BASE}/`;
|
||||
for (let i = 0; i < 60; i++) {
|
||||
try {
|
||||
const res = await fetch(url, { method: 'GET' });
|
||||
if (res.ok) return;
|
||||
} catch (err) {
|
||||
// keep polling
|
||||
}
|
||||
await wait(500);
|
||||
}
|
||||
throw new Error('Preview server did not become ready');
|
||||
}
|
||||
|
||||
async function runA11y() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
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}`);
|
||||
});
|
||||
}
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log('[a11y] no violations detected');
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const server = await startPreview();
|
||||
try {
|
||||
await waitForServer();
|
||||
await runA11y();
|
||||
} finally {
|
||||
server.kill('SIGINT');
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: SDK Quickstarts
|
||||
description: Deterministic, copy-ready SDK snippets aligned to the aggregate spec.
|
||||
---
|
||||
|
||||
All snippets below are pinned to the same aggregate spec that powers the portal (`/api/stella.yaml`). Replace the placeholder token with a sandbox-scoped bearer token.
|
||||
|
||||
## Node.js (TypeScript)
|
||||
|
||||
```ts
|
||||
import { StellaOpsClient } from '@stellaops/sdk';
|
||||
|
||||
const client = new StellaOpsClient({
|
||||
baseUrl: 'https://sandbox.api.stellaops.local',
|
||||
token: process.env.STELLAOPS_TOKEN ?? '<sandbox-token>',
|
||||
});
|
||||
|
||||
async function run() {
|
||||
const resp = await client.orchestrator.createJob({
|
||||
workflow: 'sbom-verify',
|
||||
source: 'registry:example/app@sha256:...',
|
||||
});
|
||||
console.log(resp.id, resp.status);
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
```
|
||||
|
||||
## Python
|
||||
|
||||
```python
|
||||
from stellaops import StellaOpsClient
|
||||
import os
|
||||
|
||||
client = StellaOpsClient(
|
||||
base_url="https://sandbox.api.stellaops.local",
|
||||
token=os.getenv("STELLAOPS_TOKEN", "<sandbox-token>"),
|
||||
)
|
||||
|
||||
job = client.orchestrator.create_job(
|
||||
workflow="sbom-verify",
|
||||
source="registry:example/app@sha256:...",
|
||||
)
|
||||
print(job["id"], job["status"])
|
||||
```
|
||||
|
||||
## cURL (reference)
|
||||
|
||||
```bash
|
||||
curl -X POST https://sandbox.api.stellaops.local/orchestrator/jobs \
|
||||
-H 'Authorization: Bearer <sandbox-token>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"workflow":"sbom-verify","source":"registry:example/app@sha256:..."}'
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Packages are assumed to be generated from tested examples; version tags should match the portal release when published.
|
||||
- All snippets avoid retries to keep behaviour deterministic.
|
||||
- Keep tokens short-lived and scoped to sandbox. Production tokens should not be used here.
|
||||
@@ -9,6 +9,7 @@ description: Drop-by-drop updates for the DevPortal surface.
|
||||
- ✅ Embedded aggregate OpenAPI via RapiDoc using bundled `/api/stella.yaml`.
|
||||
- ✅ Added schema viewer + version selector, copy-curl snippets, and example guide.
|
||||
- ✅ Delivered Try-It console targeting sandbox with bearer-token onboarding and RapiDoc allow-try.
|
||||
- ✅ Added SDK quickstarts (Node.js, Python) aligned to aggregate spec.
|
||||
- 🔜 Operation-specific example rendering & SDK snippets (DEVPORT-63-002).
|
||||
- 🔜 Try-It console against sandbox scopes (DEVPORT-63-001).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user