# ADR-265 D2 — publish only from CI, with provenance. # # Manual `npm publish` from laptops stops: this workflow re-runs the ADR-265 D1 # gate for the selected package and then publishes with npm provenance # attestations (OIDC), tying every published version to a public commit + # workflow run — the npm-side analogue of the ADR-028 witness bundle. # # Requires: NPM_TOKEN repo secret (an npm automation token), or npm Trusted # Publishing configured for the package (in which case the token is unused). # Configure the `npm-release` environment for selected branch `main`, required # review, and prevention of self-review; the job also rejects non-main refs. name: ruview npm release on: workflow_dispatch: inputs: package: description: 'Package directory to publish' required: true type: choice options: - harness/ruview - harness/homecore - tools/ruview-mcp dist_tag: description: 'npm dist-tag' required: false default: 'latest' type: string permissions: contents: read id-token: write # npm --provenance jobs: publish: if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest environment: name: npm-release concurrency: group: npm-release cancel-in-progress: false defaults: run: working-directory: ${{ inputs.package }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false ref: refs/heads/main - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' registry-url: 'https://registry.npmjs.org' - name: Verify trusted-publishing runtime run: | node -e " const [major, minor] = process.versions.node.split('.').map(Number); if (major < 22 || (major === 22 && minor < 14)) { throw new Error('npm trusted publishing requires Node >=22.14.0'); } " node -e " const { execFileSync } = require('node:child_process'); const [major, minor, patch] = execFileSync('npm', ['--version'], { encoding: 'utf8' }).trim().split('.').map(Number); if (major < 11 || (major === 11 && (minor < 5 || (minor === 5 && patch < 1)))) { throw new Error('npm trusted publishing requires npm >=11.5.1'); } " - name: Install run: | if [ -f package-lock.json ]; then npm ci; else npm install --no-fund --no-audit; fi - name: Build (if present) run: npm run build --if-present - name: Test run: npm test --if-present # ADR-265 D3 — package.json is the only place a version string lives. - name: Version-literal gate run: | set -euo pipefail hits="" for d in src bin; do if [ -d "$d" ]; then hits+=$(grep -rEn '\b[0-9]+\.[0-9]+\.[0-9]+\b' "$d" | grep -vE '127\.0\.0\.1|0\.0\.0\.0' || true) fi done if [ -n "$hits" ]; then echo "Hardcoded version-like literals found (read package.json instead — ADR-265 D3):" echo "$hits" exit 1 fi # ADR-265 D1.3 — pack-content gate: no maps AND the per-package # unpacked-size budget (the budgets that npm-packages.yml enforces). - name: Pack gate (no maps + size budget) run: | set -euo pipefail case "${{ inputs.package }}" in # ADR-283: brain + local hosts + replay assets; no runtime deps. harness/ruview) export UNPACKED_BUDGET=131072 ;; # ADR-285: CLI + MCP + reviewed brain + WASM-kernel adapter. harness/homecore) export UNPACKED_BUDGET=180000 ;; # ADR-264 O2: map-free tarball (was 188 kB with maps). tools/ruview-mcp) export UNPACKED_BUDGET=140000 ;; *) echo "Unknown package '${{ inputs.package }}' — no budget defined"; exit 1 ;; esac npm pack --dry-run --json 2>/dev/null | node -e " const [info] = JSON.parse(require('fs').readFileSync(0, 'utf8')); const budget = Number(process.env.UNPACKED_BUDGET); const maps = info.files.filter((f) => f.path.endsWith('.map')); if (maps.length > 0) { console.error('Tarball contains source maps (ADR-264 F2):', maps.map((m) => m.path)); process.exit(1); } if (info.unpackedSize > budget) { console.error(\`Unpacked size \${info.unpackedSize} B exceeds budget \${budget} B\`); process.exit(1); } console.log(\`pack gate OK: \${info.files.length} files, \${info.unpackedSize} B unpacked (budget \${budget} B), 0 maps\`); " # ADR-265 D1.4 — install the real tarball and drive each bin/export. - name: Tarball smoke test run: | # zizmor: ignore[adhoc-packages] the locally built tarball is the artifact under test set -euo pipefail TGZ="$PWD/$(npm pack --silent 2>/dev/null | tail -1)" SHA512="$(sha512sum "$TGZ" | cut -d' ' -f1)" printf 'PACKAGE_TARBALL=%s\nPACKAGE_TARBALL_SHA512=%s\n' "$TGZ" "$SHA512" >> "$GITHUB_ENV" SMOKE="$(mktemp -d)" cd "$SMOKE" npm init -y > /dev/null npm i --no-fund --no-audit "$TGZ" case "${{ inputs.package }}" in harness/ruview) ./node_modules/.bin/ruview --version ./node_modules/.bin/ruview doctor ./node_modules/.bin/ruview guidance --topic homecore --query restore --limit 1 \ | grep -q '"homecore-runtime-restore"' # the honesty gate must fail closed on empty input (ADR-263 F1) if ./node_modules/.bin/ruview claim-check; then echo 'claim-check passed with no input — fail-open regression'; exit 1 fi node --input-type=module -e "const m = await import('@ruvnet/ruview'); if (!m.TOOLS) process.exit(1);" node --input-type=module -e "const m = await import('@ruvnet/ruview/guidance'); if (typeof m.getGuidance !== 'function') process.exit(1);" ;; harness/homecore) ./node_modules/.bin/homecore --version ./node_modules/.bin/homecore doctor --strict-wasm ./node_modules/.bin/homecore guidance --topic plugins --query Wasmtime --limit 1 \ | grep -q '"wasm-plugins"' printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci","version":"0"}}}\n' \ | timeout 30 ./node_modules/.bin/homecore mcp start | grep -q '"serverInfo"' node --input-type=module -e "const m = await import('homecore'); if (typeof m.runTool !== 'function') process.exit(1);" node --input-type=module -e "const m = await import('homecore/kernel'); const s = await m.getKernelStatus({strict:true}); if (!s.ok || s.resolvedBackend !== 'wasm') process.exit(1);" ;; tools/ruview-mcp) # initialize over stdio; server must answer and exit 0 on EOF printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci","version":"0"}}}\n' \ | timeout 30 ./node_modules/.bin/rvagent | grep -q '"serverInfo"' # the ESM export must resolve from the installed tarball (ADR-264 F1) timeout 30 node --input-type=module -e "await import('@ruvnet/rvagent');" < /dev/null ;; esac - name: Claim-check README run: | if [ -f README.md ]; then node "$GITHUB_WORKSPACE/harness/ruview/bin/cli.js" claim-check --file README.md fi - name: Publish (with provenance) run: | printf '%s %s\n' "$PACKAGE_TARBALL_SHA512" "$PACKAGE_TARBALL" | sha512sum --check - npm publish "$PACKAGE_TARBALL" --provenance --access public --tag "$NPM_DIST_TAG" env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_DIST_TAG: ${{ inputs.dist_tag }}