Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/actions/rdcp-conformance/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"}
Expand All @@ -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
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 2 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,7 @@
"files": [
"dist",
"README.md",
"LICENSE",
"scripts/rdcp-conformance.mjs",
"scripts/conformance-junit.mjs",
"scripts/conformance-badges.mjs"
"LICENSE"
],
"exports": {
".": {
Expand Down Expand Up @@ -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": {}
}
13 changes: 13 additions & 0 deletions packages/rdcp-conformance/README.md
Original file line number Diff line number Diff line change
@@ -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
```
16 changes: 16 additions & 0 deletions packages/rdcp-conformance/bin/conformance-badges.mjs
Original file line number Diff line number Diff line change
@@ -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`<?xml version="1.0" encoding="UTF-8"?>\n<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">\n <linearGradient id="s" x2="0" y2="100%">\n <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>\n <stop offset="1" stop-opacity=".1"/>\n </linearGradient>\n <mask id="m"><rect width="${w}" height="${h}" rx="3" fill="#fff"/></mask>\n <g mask="url(#m)">\n <rect width="${lh}" height="${h}" fill="#555"/>\n <rect x="${lh}" width="${rh}" height="${h}" fill="${color==='brightgreen'?'#4c1':color==='yellow'?'#dfb317':'#e05d44'}"/>\n <rect width="${w}" height="${h}" fill="url(#s)"/>\n </g>\n <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">\n <text x="${labelX}" y="14">${label}</text>\n <text x="${msgX}" y="14">${msg}</text>\n </g>\n</svg>`}
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}`)
16 changes: 16 additions & 0 deletions packages/rdcp-conformance/bin/conformance-junit.mjs
Original file line number Diff line number Diff line change
@@ -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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&apos;')}
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+='<?xml version="1.0" encoding="UTF-8"?>\n';xml+=`<testsuites tests="${tests}" failures="${failures}" time="${time.toFixed(3)}">\n`;xml+=` <testsuite name="${esc(tsName)}" tests="${tests}" failures="${failures}" time="${time.toFixed(3)}">\n`;for(const c of cases){const cls=path.dirname(c.file||'')||'rdcp.tests';xml+=` <testcase classname="${esc(cls)}" name="${esc(c.name)}" time="${(c.duration||0).toFixed(3)}">\n`;if(c.status&&c.status!=='passed'){xml+=` <failure message="${esc(c.status)}"/>\n`;}xml+=' </testcase>\n'}xml+=' </testsuite>\n';xml+='</testsuites>\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}`)
70 changes: 70 additions & 0 deletions packages/rdcp-conformance/bin/rdcp-conformance.mjs
Original file line number Diff line number Diff line change
@@ -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)
}
3 changes: 3 additions & 0 deletions packages/rdcp-conformance/index.mjs
Original file line number Diff line number Diff line change
@@ -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'
18 changes: 18 additions & 0 deletions packages/rdcp-conformance/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Loading