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`}
+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"
+ }
+}