diff --git a/.github/actions/rdcp-conformance/action.yml b/.github/actions/rdcp-conformance/action.yml index 451fb17..79ea3e6 100644 --- a/.github/actions/rdcp-conformance/action.yml +++ b/.github/actions/rdcp-conformance/action.yml @@ -48,6 +48,9 @@ runs: - name: Discovery shell: bash run: | + npx rdcp-conformance --base-url="${{ inputs.base-url }}" --out="${{ inputs.discovery-out }}" \ + ${INPUT_INCLUDE:+--include-tags="${{ inputs.include-tags }}"} \ + ${INPUT_EXCLUDE:+--exclude-tags="${{ inputs.exclude-tags }}"} || \ node scripts/rdcp-conformance.mjs --base-url="${{ inputs.base-url }}" --out="${{ inputs.discovery-out }}" \ ${INPUT_INCLUDE:+--include-tags="${{ inputs.include-tags }}"} \ ${INPUT_EXCLUDE:+--exclude-tags="${{ inputs.exclude-tags }}"} @@ -57,11 +60,11 @@ runs: - name: Generate JUnit report shell: bash run: | - node scripts/conformance-junit.mjs || echo 'JUnit generation skipped' + npx rdcp-conformance-junit || node scripts/conformance-junit.mjs || echo 'JUnit generation skipped' - name: Generate badges shell: bash run: | - node scripts/conformance-badges.mjs || echo 'Badge generation skipped' + npx rdcp-conformance-badges || node scripts/conformance-badges.mjs || echo 'Badge generation skipped' - name: Set outputs from results id: set shell: bash diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1cbcd17..b5dfc27 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,3 +113,15 @@ jobs: npm run build --prefix packages/otel-plugin (cd packages/otel-plugin && npm publish --access public) fi + + - name: Publish @rdcp.dev/conformance to npm (Node 20 only) + if: startsWith(github.ref, 'refs/tags/v-conformance-') && matrix.node-version == '20.x' + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + CONF_VERSION=$(node -p "require('./packages/rdcp-conformance/package.json').version") + if npm view @rdcp.dev/conformance@"$CONF_VERSION" version >/dev/null 2>&1; then + echo "@rdcp.dev/conformance@$CONF_VERSION already published, skipping" + else + (cd packages/rdcp-conformance && npm publish --access public) + fi diff --git a/package.json b/package.json index fd9ce54..a5f901f 100644 --- a/package.json +++ b/package.json @@ -35,10 +35,7 @@ "files": [ "dist", "README.md", - "LICENSE", - "scripts/rdcp-conformance.mjs", - "scripts/conformance-junit.mjs", - "scripts/conformance-badges.mjs" + "LICENSE" ], "exports": { ".": { @@ -175,9 +172,5 @@ "test": "tests" }, "type": "commonjs", - "bin": { - "rdcp-conformance": "scripts/rdcp-conformance.mjs", - "rdcp-conformance-junit": "scripts/conformance-junit.mjs", - "rdcp-conformance-badges": "scripts/conformance-badges.mjs" - } + "bin": {} } diff --git a/packages/rdcp-conformance/README.md b/packages/rdcp-conformance/README.md new file mode 100644 index 0000000..ad8d80c --- /dev/null +++ b/packages/rdcp-conformance/README.md @@ -0,0 +1,13 @@ +# RDCP Conformance CLI + +Lightweight CLI tools for RDCP conformance: +- rdcp-conformance: discovery and tag gating config +- rdcp-conformance-junit: generate JUnit XML from results +- rdcp-conformance-badges: generate Shields JSON + SVG badges + +Usage: +``` +npx rdcp-conformance --base-url=http://localhost:3000 +npx rdcp-conformance-junit +npx rdcp-conformance-badges +``` diff --git a/packages/rdcp-conformance/bin/conformance-badges.mjs b/packages/rdcp-conformance/bin/conformance-badges.mjs new file mode 100644 index 0000000..faf8463 --- /dev/null +++ b/packages/rdcp-conformance/bin/conformance-badges.mjs @@ -0,0 +1,16 @@ +#!/usr/bin/env node +// Badges CLI (moved from @rdcp.dev/server) +import fs from 'node:fs' +import path from 'node:path' + +const RESULTS=process.env.RDCP_RESULTS||'reports/rdcp.results.json' +const OUT_DIR=process.env.RDCP_BADGES_OUT||'reports/badges' +function ensureDir(p){fs.mkdirSync(p,{recursive:true})} +function readResults(file){const txt=fs.readFileSync(file,'utf8');return JSON.parse(txt)} +function colorFor(f){if(f===0)return'brightgreen';if(f<5)return'yellow';return'red'} +function svg(label,msg,color){const pad=6,charW=7,lh=label.length*charW+pad*2,rh=msg.length*charW+pad*2,w=lh+rh,h=20,labelX=lh/2,msgX=lh+rh/2;return`\n\n \n \n \n \n \n \n \n \n \n \n \n ${label}\n ${msg}\n \n`} +function collectCases(results){const out=[];const suites=Array.isArray(results.suites)?results.suites:[];for(const s of suites){const inner=Array.isArray(s.suites)?s.suites:[];for(const t of inner){out.push({status:t.status||'passed',tags:Array.isArray(t.tags)?t.tags.map(String):[],title:t.title||'',file:s.file||''})}}return out} +function write(prefix,label,passed,total){const failed=Math.max(0,total-passed),color=colorFor(failed);const shields={schemaVersion:1,label,message:`${passed}/${total}`,color};ensureDir(OUT_DIR);fs.writeFileSync(path.join(OUT_DIR,`${prefix}.json`),JSON.stringify(shields,null,2));fs.writeFileSync(path.join(OUT_DIR,`${prefix}.svg`),svg(label.toUpperCase(),`${passed}/${total}`,color))} +function summarize(list,fn=()=>true){const items=list.filter(fn);return{passed:items.filter(c=>c.status==='passed').length,total:items.length}} +const results=readResults(RESULTS);const all=collectCases(results);const overall=summarize(all);write('rdcp-summary','rdcp conformance',overall.passed,overall.total);for(const p of['basic','standard','enterprise']){const {passed,total}=summarize(all,c=>c.tags.includes(p));write(`profile-${p}`,`${p}`,passed,total)}for(const a of['control','jwks','keyring','jwt','admin','etag','util','otel','tenant','ttl','audit','rate-limit','client','integration','status','auth','headers','metrics','put','schema']){const {passed,total}=summarize(all,c=>c.tags.includes(a));write(`cap-${a}`,`${a}`,passed,total)} +console.log(`Badges written to ${OUT_DIR}`) diff --git a/packages/rdcp-conformance/bin/conformance-junit.mjs b/packages/rdcp-conformance/bin/conformance-junit.mjs new file mode 100644 index 0000000..a3014ad --- /dev/null +++ b/packages/rdcp-conformance/bin/conformance-junit.mjs @@ -0,0 +1,16 @@ +#!/usr/bin/env node +// JUnit CLI (moved from @rdcp.dev/server) +import fs from 'node:fs' +import path from 'node:path' + +function esc(s){return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,''')} +const resultsPath=process.env.RDCP_RESULTS||'reports/rdcp.results.json' +const outputPath=process.env.RDCP_JUNIT_OUT||'reports/rdcp.junit.xml' +function readResults(file){const txt=fs.readFileSync(file,'utf8');return JSON.parse(txt)} +function flatten(results){const cases=[];const suites=Array.isArray(results.suites)?results.suites:[];for(const s of suites){const inner=Array.isArray(s.suites)?s.suites:[];for(const t of inner){cases.push({name:t.title||'unnamed',file:s.file||'',status:t.status||'passed',duration:typeof t.durationMs==='number'?t.durationMs/1000:0})}}return cases} +function build(results){const cases=flatten(results);const tests=cases.length;const failures=cases.filter(c=>c.status&&c.status!=='passed').length;const time=(results.summary?.durationMs||0)/1000;const tsName='RDCP Conformance';let xml='';xml+='\n';xml+=`\n`;xml+=` \n`;for(const c of cases){const cls=path.dirname(c.file||'')||'rdcp.tests';xml+=` \n`;if(c.status&&c.status!=='passed'){xml+=` \n`;}xml+=' \n'}xml+=' \n';xml+='\n';return xml} +fs.mkdirSync(path.dirname(outputPath),{recursive:true}) +const results=readResults(resultsPath) +const xml=build(results) +fs.writeFileSync(outputPath,xml,'utf8') +console.log(`JUnit report written to ${outputPath}`) diff --git a/packages/rdcp-conformance/bin/rdcp-conformance.mjs b/packages/rdcp-conformance/bin/rdcp-conformance.mjs new file mode 100644 index 0000000..ae6aea0 --- /dev/null +++ b/packages/rdcp-conformance/bin/rdcp-conformance.mjs @@ -0,0 +1,70 @@ +#!/usr/bin/env node +// Discovery CLI (moved from @rdcp.dev/server) +import fs from 'node:fs' +import path from 'node:path' +import { argv, exit } from 'node:process' +import http from 'node:http' +import https from 'node:https' + +function getArg(name, def) { + const m = argv.find(a => a.startsWith(`--${name}=`)) + return m ? m.split('=')[1] : def +} + +const baseUrl = getArg('base-url', process.env.RDCP_BASE_URL) +const outFile = getArg('out', process.env.RDCP_DISCOVERY_FILE || 'reports/rdcp.discovery.json') +const includeTags = getArg('include-tags', process.env.RDCP_INCLUDE_TAGS || '') +const excludeTags = getArg('exclude-tags', process.env.RDCP_EXCLUDE_TAGS || '') +if (!baseUrl) { + console.error('Missing --base-url or RDCP_BASE_URL') + exit(2) +} + +function fetchJson(url) { + return new Promise((resolve, reject) => { + const client = url.startsWith('https') ? https : http + const req = client.get(url, res => { + let data = '' + res.on('data', chunk => (data += chunk)) + res.on('end', () => { + try { resolve(JSON.parse(data)) } catch (e) { reject(e) } + }) + }) + req.on('error', reject) + }) +} + +try { + const wellKnown = new URL('/.well-known/rdcp', baseUrl).toString() + const discovery = await fetchJson(wellKnown) + const endpoints = discovery?.endpoints || {} + const security = discovery?.security || {} + const capabilities = discovery?.capabilities || {} + + const profile = security.level || 'basic' + const methods = (security.methods || []).join(',') + + const payload = { + ok: true, + baseUrl, + profile, + methods, + capabilities, + endpoints, + ts: new Date().toISOString() + } + // Write to file for test gating/reporting + const dir = path.dirname(outFile) + fs.mkdirSync(dir, { recursive: true }) + fs.writeFileSync(outFile, JSON.stringify(payload, null, 2)) + // Save run config for tags gating + const runCfg = { + includeTags: includeTags ? includeTags.split(',').map(s => s.trim()).filter(Boolean) : [], + excludeTags: excludeTags ? excludeTags.split(',').map(s => s.trim()).filter(Boolean) : [], + } + fs.writeFileSync(path.join(dir, 'rdcp.run.json'), JSON.stringify(runCfg, null, 2)) + console.log(JSON.stringify(payload, null, 2)) +} catch (e) { + console.error('Discovery failed:', e?.message || String(e)) + exit(1) +} diff --git a/packages/rdcp-conformance/index.mjs b/packages/rdcp-conformance/index.mjs new file mode 100644 index 0000000..f14a4f7 --- /dev/null +++ b/packages/rdcp-conformance/index.mjs @@ -0,0 +1,3 @@ +#!/usr/bin/env node +// Thin wrapper to call the discovery script; kept minimal for zero deps +import '../bin/rdcp-conformance.mjs' diff --git a/packages/rdcp-conformance/package.json b/packages/rdcp-conformance/package.json new file mode 100644 index 0000000..32ff390 --- /dev/null +++ b/packages/rdcp-conformance/package.json @@ -0,0 +1,18 @@ +{ + "name": "@rdcp.dev/conformance", + "version": "0.1.0", + "description": "RDCP Conformance CLI: discovery, reports (JUnit/Badges)", + "license": "Apache-2.0", + "type": "module", + "bin": { + "rdcp-conformance": "bin/rdcp-conformance.mjs", + "rdcp-conformance-junit": "bin/conformance-junit.mjs", + "rdcp-conformance-badges": "bin/conformance-badges.mjs" + }, + "files": [ + "bin" + ], + "engines": { + "node": ">=18.0.0" + } +}