Skip to content

Commit 97ddcce

Browse files
Merge branch 'ruvnet:main' into main
2 parents a1eda51 + e6f26e9 commit 97ddcce

63 files changed

Lines changed: 2984 additions & 1023 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ on:
99

1010
env:
1111
PYTHON_VERSION: '3.11'
12-
NODE_VERSION: '18'
12+
NODE_VERSION: '20' # ADR-265: all Node packages in this repo declare engines >= 20
1313
REGISTRY: ghcr.io
1414
IMAGE_NAME: ${{ github.repository }}
1515

.github/workflows/npm-packages.yml

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# ADR-265 D1 — the npm-package gate.
2+
#
3+
# Every Node package in this repo (published or private) gets: install, build,
4+
# tests, a version-literal gate (D3 — package.json is the only place a version
5+
# lives), a pack-content gate (no source maps, unpacked-size budget), a
6+
# tarball-install smoke test (would have caught ADR-264 F1's broken `require`
7+
# export), and the claim-check honesty lint on the README (D4).
8+
9+
name: npm packages
10+
11+
on:
12+
push:
13+
branches: [main]
14+
paths:
15+
- 'harness/ruview/**'
16+
- 'tools/ruview-mcp/**'
17+
- 'tools/ruview-cli/**'
18+
- '.github/workflows/npm-packages.yml'
19+
pull_request:
20+
paths:
21+
- 'harness/ruview/**'
22+
- 'tools/ruview-mcp/**'
23+
- 'tools/ruview-cli/**'
24+
- '.github/workflows/npm-packages.yml'
25+
26+
permissions:
27+
contents: read
28+
29+
jobs:
30+
gate:
31+
name: ${{ matrix.package.dir }} (node ${{ matrix.node }})
32+
runs-on: ubuntu-latest
33+
strategy:
34+
fail-fast: false
35+
matrix:
36+
node: ['20', '22']
37+
package:
38+
- dir: harness/ruview
39+
build: false
40+
publishable: true
41+
# ADR-263: dependency-free harness; budget guards against dep creep.
42+
unpacked_budget: 65536
43+
- dir: tools/ruview-mcp
44+
build: true
45+
publishable: true
46+
# ADR-264 O2: map-free tarball (was 188 kB with maps).
47+
unpacked_budget: 140000
48+
- dir: tools/ruview-cli
49+
build: true
50+
publishable: false
51+
unpacked_budget: 0
52+
defaults:
53+
run:
54+
working-directory: ${{ matrix.package.dir }}
55+
steps:
56+
- uses: actions/checkout@v4
57+
58+
- uses: actions/setup-node@v4
59+
with:
60+
node-version: ${{ matrix.node }}
61+
62+
# Repo policy gitignores lockfiles under harness/ (the harness is
63+
# dependency-free anyway); the TS packages commit theirs.
64+
- name: Install
65+
run: |
66+
if [ -f package-lock.json ]; then npm ci; else npm install --no-fund --no-audit; fi
67+
68+
- name: Build
69+
if: ${{ matrix.package.build }}
70+
run: npm run build
71+
72+
- name: Test
73+
run: npm test --if-present
74+
75+
# ADR-265 D3 — package.json is the only place a version string lives.
76+
- name: Version-literal gate
77+
run: |
78+
set -euo pipefail
79+
hits=""
80+
for d in src bin; do
81+
if [ -d "$d" ]; then
82+
hits+=$(grep -rEn '\b[0-9]+\.[0-9]+\.[0-9]+\b' "$d" | grep -vE '127\.0\.0\.1|0\.0\.0\.0' || true)
83+
fi
84+
done
85+
if [ -n "$hits" ]; then
86+
echo "Hardcoded version-like literals found (read package.json instead — ADR-265 D3):"
87+
echo "$hits"
88+
exit 1
89+
fi
90+
91+
# ADR-265 D1.3 — pack-content gate: no maps, size budget enforced.
92+
- name: Pack gate
93+
if: ${{ matrix.package.publishable }}
94+
run: |
95+
npm pack --dry-run --json 2>/dev/null | node -e "
96+
const [info] = JSON.parse(require('fs').readFileSync(0, 'utf8'));
97+
const budget = Number(process.env.UNPACKED_BUDGET);
98+
const maps = info.files.filter((f) => f.path.endsWith('.map'));
99+
if (maps.length > 0) {
100+
console.error('Tarball contains source maps (ADR-264 F2):', maps.map((m) => m.path));
101+
process.exit(1);
102+
}
103+
if (info.unpackedSize > budget) {
104+
console.error(\`Unpacked size \${info.unpackedSize} B exceeds budget \${budget} B\`);
105+
process.exit(1);
106+
}
107+
console.log(\`pack gate OK: \${info.files.length} files, \${info.unpackedSize} B unpacked (budget \${budget} B), 0 maps\`);
108+
"
109+
env:
110+
UNPACKED_BUDGET: ${{ matrix.package.unpacked_budget }}
111+
112+
# ADR-265 D1.4 — install the real tarball and drive each bin/export.
113+
- name: Tarball smoke test
114+
if: ${{ matrix.package.publishable }}
115+
run: |
116+
set -euo pipefail
117+
TGZ="$PWD/$(npm pack --silent 2>/dev/null | tail -1)"
118+
SMOKE="$(mktemp -d)"
119+
cd "$SMOKE"
120+
npm init -y > /dev/null
121+
npm i --no-fund --no-audit "$TGZ"
122+
case "${{ matrix.package.dir }}" in
123+
harness/ruview)
124+
./node_modules/.bin/ruview --version
125+
./node_modules/.bin/ruview doctor
126+
# the honesty gate must fail closed on empty input (ADR-263 F1)
127+
if ./node_modules/.bin/ruview claim-check; then
128+
echo 'claim-check passed with no input — fail-open regression'; exit 1
129+
fi
130+
node --input-type=module -e "const m = await import('@ruvnet/ruview'); if (!m.TOOLS) process.exit(1);"
131+
;;
132+
tools/ruview-mcp)
133+
# initialize over stdio; server must answer and exit 0 on EOF
134+
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci","version":"0"}}}\n' \
135+
| timeout 30 ./node_modules/.bin/rvagent | grep -q '"serverInfo"'
136+
# the ESM export must resolve from the installed tarball (ADR-264 F1)
137+
timeout 30 node --input-type=module -e "await import('@ruvnet/rvagent');" < /dev/null
138+
;;
139+
esac
140+
141+
# ADR-265 D4 — package READMEs must pass the project's own honesty lint.
142+
- name: Claim-check README
143+
run: |
144+
if [ -f README.md ]; then
145+
node "$GITHUB_WORKSPACE/harness/ruview/bin/cli.js" claim-check --file README.md
146+
else
147+
echo "no README.md — skipping"
148+
fi
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# ADR-265 D2 — publish only from CI, with provenance.
2+
#
3+
# Manual `npm publish` from laptops stops: this workflow re-runs the ADR-265 D1
4+
# gate for the selected package and then publishes with npm provenance
5+
# attestations (OIDC), tying every published version to a public commit +
6+
# workflow run — the npm-side analogue of the ADR-028 witness bundle.
7+
#
8+
# Requires: NPM_TOKEN repo secret (an npm automation token), or npm Trusted
9+
# Publishing configured for the package (in which case the token is unused).
10+
11+
name: ruview npm release
12+
13+
on:
14+
workflow_dispatch:
15+
inputs:
16+
package:
17+
description: 'Package directory to publish'
18+
required: true
19+
type: choice
20+
options:
21+
- harness/ruview
22+
- tools/ruview-mcp
23+
dist_tag:
24+
description: 'npm dist-tag'
25+
required: false
26+
default: 'latest'
27+
type: string
28+
29+
permissions:
30+
contents: read
31+
id-token: write # npm --provenance
32+
33+
jobs:
34+
publish:
35+
runs-on: ubuntu-latest
36+
defaults:
37+
run:
38+
working-directory: ${{ inputs.package }}
39+
steps:
40+
- uses: actions/checkout@v4
41+
42+
- uses: actions/setup-node@v4
43+
with:
44+
node-version: '20'
45+
registry-url: 'https://registry.npmjs.org'
46+
47+
- name: Install
48+
run: |
49+
if [ -f package-lock.json ]; then npm ci; else npm install --no-fund --no-audit; fi
50+
51+
- name: Build (if present)
52+
run: npm run build --if-present
53+
54+
- name: Test
55+
run: npm test --if-present
56+
57+
# ADR-265 D3 — package.json is the only place a version string lives.
58+
- name: Version-literal gate
59+
run: |
60+
set -euo pipefail
61+
hits=""
62+
for d in src bin; do
63+
if [ -d "$d" ]; then
64+
hits+=$(grep -rEn '\b[0-9]+\.[0-9]+\.[0-9]+\b' "$d" | grep -vE '127\.0\.0\.1|0\.0\.0\.0' || true)
65+
fi
66+
done
67+
if [ -n "$hits" ]; then
68+
echo "Hardcoded version-like literals found (read package.json instead — ADR-265 D3):"
69+
echo "$hits"
70+
exit 1
71+
fi
72+
73+
# ADR-265 D1.3 — pack-content gate: no maps AND the per-package
74+
# unpacked-size budget (the budgets that npm-packages.yml enforces).
75+
- name: Pack gate (no maps + size budget)
76+
run: |
77+
set -euo pipefail
78+
case "${{ inputs.package }}" in
79+
# ADR-263: dependency-free harness; budget guards against dep creep.
80+
harness/ruview) export UNPACKED_BUDGET=65536 ;;
81+
# ADR-264 O2: map-free tarball (was 188 kB with maps).
82+
tools/ruview-mcp) export UNPACKED_BUDGET=140000 ;;
83+
*) echo "Unknown package '${{ inputs.package }}' — no budget defined"; exit 1 ;;
84+
esac
85+
npm pack --dry-run --json 2>/dev/null | node -e "
86+
const [info] = JSON.parse(require('fs').readFileSync(0, 'utf8'));
87+
const budget = Number(process.env.UNPACKED_BUDGET);
88+
const maps = info.files.filter((f) => f.path.endsWith('.map'));
89+
if (maps.length > 0) {
90+
console.error('Tarball contains source maps (ADR-264 F2):', maps.map((m) => m.path));
91+
process.exit(1);
92+
}
93+
if (info.unpackedSize > budget) {
94+
console.error(\`Unpacked size \${info.unpackedSize} B exceeds budget \${budget} B\`);
95+
process.exit(1);
96+
}
97+
console.log(\`pack gate OK: \${info.files.length} files, \${info.unpackedSize} B unpacked (budget \${budget} B), 0 maps\`);
98+
"
99+
100+
# ADR-265 D1.4 — install the real tarball and drive each bin/export.
101+
- name: Tarball smoke test
102+
run: |
103+
set -euo pipefail
104+
TGZ="$PWD/$(npm pack --silent 2>/dev/null | tail -1)"
105+
SMOKE="$(mktemp -d)"
106+
cd "$SMOKE"
107+
npm init -y > /dev/null
108+
npm i --no-fund --no-audit "$TGZ"
109+
case "${{ inputs.package }}" in
110+
harness/ruview)
111+
./node_modules/.bin/ruview --version
112+
./node_modules/.bin/ruview doctor
113+
# the honesty gate must fail closed on empty input (ADR-263 F1)
114+
if ./node_modules/.bin/ruview claim-check; then
115+
echo 'claim-check passed with no input — fail-open regression'; exit 1
116+
fi
117+
node --input-type=module -e "const m = await import('@ruvnet/ruview'); if (!m.TOOLS) process.exit(1);"
118+
;;
119+
tools/ruview-mcp)
120+
# initialize over stdio; server must answer and exit 0 on EOF
121+
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci","version":"0"}}}\n' \
122+
| timeout 30 ./node_modules/.bin/rvagent | grep -q '"serverInfo"'
123+
# the ESM export must resolve from the installed tarball (ADR-264 F1)
124+
timeout 30 node --input-type=module -e "await import('@ruvnet/rvagent');" < /dev/null
125+
;;
126+
esac
127+
128+
- name: Claim-check README
129+
run: |
130+
if [ -f README.md ]; then
131+
node "$GITHUB_WORKSPACE/harness/ruview/bin/cli.js" claim-check --file README.md
132+
fi
133+
134+
- name: Publish (with provenance)
135+
run: npm publish --provenance --access public --tag "${{ inputs.dist_tag }}"
136+
env:
137+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

0 commit comments

Comments
 (0)