This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

add dep-diff port (e18e action logic) + base fixture + both engine workflows

+408
+2
.gitignore
··· 1 + CLAUDE.md 2 + node_modules/
+23
.tangled/workflows/dep-diff-microvm.yml
··· 1 + # e18e dependency diff on the microvm engine — same logic as dep-diff-nixery.yml 2 + when: 3 + - event: ["push", "pull_request", "manual"] 4 + branch: ["**"] 5 + 6 + engine: microvm 7 + image: nixos 8 + 9 + clone: 10 + depth: 50 11 + 12 + dependencies: 13 + - nodejs_22 14 + - git 15 + 16 + steps: 17 + - name: install diff tooling 18 + command: npm ci --prefix tools --no-audit --no-fund 19 + 20 + - name: dependency diff vs base 21 + command: | 22 + git fetch origin "${TANGLED_PR_TARGET_BRANCH:-main}" 23 + node tools/dep-diff.mjs FETCH_HEAD
+23
.tangled/workflows/dep-diff-nixery.yml
··· 1 + # e18e dependency diff on the nixery engine 2 + when: 3 + - event: ["push", "pull_request", "manual"] 4 + branch: ["**"] 5 + 6 + engine: nixery 7 + 8 + clone: 9 + depth: 50 10 + 11 + dependencies: 12 + nixpkgs: 13 + - nodejs_22 14 + - git 15 + 16 + steps: 17 + - name: install diff tooling 18 + command: npm ci --prefix tools --no-audit --no-fund 19 + 20 + - name: dependency diff vs base 21 + command: | 22 + git fetch origin "${TANGLED_PR_TARGET_BRANCH:-main}" 23 + node tools/dep-diff.mjs FETCH_HEAD
+25
package-lock.json
··· 1 + { 2 + "name": "spindle-dep-diff-fixture", 3 + "lockfileVersion": 3, 4 + "requires": true, 5 + "packages": { 6 + "": { 7 + "name": "spindle-dep-diff-fixture", 8 + "dependencies": { 9 + "semver": "^7.7.2" 10 + } 11 + }, 12 + "node_modules/semver": { 13 + "version": "7.8.5", 14 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", 15 + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", 16 + "license": "ISC", 17 + "bin": { 18 + "semver": "bin/semver.js" 19 + }, 20 + "engines": { 21 + "node": ">=10" 22 + } 23 + } 24 + } 25 + }
+8
package.json
··· 1 + { 2 + "name": "spindle-dep-diff-fixture", 3 + "private": true, 4 + "description": "fixture for testing e18e dependency-diff logic on spindle CI", 5 + "dependencies": { 6 + "semver": "^7.7.2" 7 + } 8 + }
+281
tools/dep-diff.mjs
··· 1 + #!/usr/bin/env node 2 + // port of e18e/action-dependency-diff (by 43081j) for tangled spindle CI. 3 + // same libs (lockparse, module-replacements, packumeta), same checks, but 4 + // reads refs via `git show` and prints markdown to stdout instead of 5 + // posting a github PR comment. 6 + // 7 + // usage: node tools/dep-diff.mjs [base-ref] [head-ref] 8 + // base-ref defaults to origin/$TANGLED_PR_TARGET_BRANCH, else origin/main 9 + // head-ref defaults to HEAD 10 + 11 + import {execFileSync} from 'node:child_process'; 12 + import {parse as parseLockfile} from 'lockparse'; 13 + import {all as allReplacements, resolveDocUrl} from 'module-replacements'; 14 + import {getTrustLevel, getTrustLevelName, getTrustStatus} from 'packumeta'; 15 + 16 + const LOCKFILE = 'package-lock.json'; 17 + // ponytail: action defaults hardcoded; make them flags if we ever tune them 18 + const DEPENDENCY_THRESHOLD = 10; 19 + const SIZE_THRESHOLD = 100_000; 20 + const DUPLICATE_THRESHOLD = 1; 21 + 22 + const baseRef = 23 + process.argv[2] ?? 24 + `origin/${process.env.TANGLED_PR_TARGET_BRANCH || 'main'}`; 25 + const headRef = process.argv[3] ?? 'HEAD'; 26 + 27 + function getFileFromRef(ref, path) { 28 + try { 29 + return execFileSync('git', ['show', `${ref}:${path}`], { 30 + encoding: 'utf8', 31 + stdio: 'pipe', 32 + maxBuffer: 1024 * 1024 * 100 33 + }); 34 + } catch { 35 + return null; 36 + } 37 + } 38 + 39 + function computeDependencyVersions(lockFile) { 40 + const result = new Map(); 41 + for (const pkg of lockFile.packages) { 42 + if (!pkg.name || !pkg.version) continue; 43 + let set = result.get(pkg.name); 44 + if (!set) result.set(pkg.name, (set = new Set())); 45 + set.add(pkg.version); 46 + } 47 + return result; 48 + } 49 + 50 + function directDeps(pkgJson) { 51 + return new Map(Object.entries(pkgJson.dependencies ?? {})); 52 + } 53 + 54 + const metaCache = new Map(); 55 + async function fetchPackageMetadata(name, version) { 56 + const key = `${name}@${version}`; 57 + if (metaCache.has(key)) return metaCache.get(key); 58 + let meta = null; 59 + try { 60 + const res = await fetch(`https://registry.npmjs.org/${name}/${version}`); 61 + if (res.ok) meta = await res.json(); 62 + } catch {} 63 + metaCache.set(key, meta); 64 + return meta; 65 + } 66 + 67 + function formatBytes(bytes) { 68 + if (bytes === 0) return '0 B'; 69 + const abs = Math.abs(bytes); 70 + const k = 1000; 71 + const sizes = ['B', 'kB', 'MB', 'GB']; 72 + const i = Math.floor(Math.log(abs) / Math.log(k)); 73 + const v = parseFloat((abs / Math.pow(k, i)).toFixed(1)); 74 + return `${bytes < 0 ? -v : v} ${sizes[i]}`; 75 + } 76 + 77 + // --- checks, ported 1:1 from src/checks/* --- 78 + 79 + function scanForReplacements(messages, baseDeps, currentDeps) { 80 + const rows = []; 81 + for (const [name] of currentDeps) { 82 + if (baseDeps.has(name)) continue; 83 + const mapping = Object.values(allReplacements.mappings).find( 84 + (m) => m.moduleName === name && m.type === 'module' 85 + ); 86 + if (!mapping) continue; 87 + const replacement = allReplacements.replacements[mapping.replacements[0]]; 88 + if (!replacement) continue; 89 + switch (replacement.type) { 90 + case 'removal': 91 + case 'simple': 92 + rows.push(`| ${name} | ${replacement.description} |`); 93 + break; 94 + case 'native': { 95 + const url = resolveDocUrl(mapping.url ?? replacement.url); 96 + rows.push( 97 + `| ${name} | Use ${url ? `[${replacement.id}](${url})` : replacement.id} |` 98 + ); 99 + break; 100 + } 101 + case 'documented': { 102 + const url = resolveDocUrl(mapping.url ?? replacement.url); 103 + rows.push( 104 + `| ${name} | Use ${url ? `[${replacement.replacementModule}](${url})` : replacement.replacementModule} |` 105 + ); 106 + break; 107 + } 108 + } 109 + } 110 + if (rows.length > 0) { 111 + messages.push( 112 + `## ⚠️ Recommended Package Replacements 113 + 114 + The following new packages or versions have community recommended replacements: 115 + 116 + | 📦 Package | 💡 Recommendation | 117 + | --- | --- | 118 + ${rows.join('\n')} 119 + 120 + > These recommendations have been defined by the [e18e](https://e18e.dev) community.` 121 + ); 122 + } 123 + } 124 + 125 + function scanForDependencyCount(messages, currentDeps, baseDeps) { 126 + const count = (m) => 127 + Array.from(m.values()).reduce((sum, v) => sum + v.size, 0); 128 + const increase = count(currentDeps) - count(baseDeps); 129 + console.error( 130 + `dependency count: ${count(baseDeps)} -> ${count(currentDeps)}` 131 + ); 132 + if (increase >= DEPENDENCY_THRESHOLD) { 133 + messages.push( 134 + `## ⚠️ Dependency Count 135 + 136 + This change adds ${increase} new dependencies (${count(baseDeps)} → ${count(currentDeps)}), which exceeds the threshold of ${DEPENDENCY_THRESHOLD}.` 137 + ); 138 + } 139 + } 140 + 141 + // ponytail: upstream renders collapsible dependency paths per duplicate; 142 + // a flat version list is enough to know the check fired 143 + function scanForDuplicates(messages, dependencyMap) { 144 + const rows = []; 145 + for (const [name, versions] of dependencyMap) { 146 + if (versions.size > DUPLICATE_THRESHOLD) { 147 + rows.push(`| ${name} | ${Array.from(versions).sort().join(', ')} |`); 148 + } 149 + } 150 + if (rows.length > 0) { 151 + messages.push( 152 + `## ⚠️ Duplicate Dependencies (found: ${rows.length}, threshold: ${DUPLICATE_THRESHOLD}) 153 + 154 + | 📦 Package | 📋 Versions | 155 + | --- | --- | 156 + ${rows.join('\n')}` 157 + ); 158 + } 159 + } 160 + 161 + // ponytail: skips upstream's unsupported-optional-dependency filtering 162 + // (platform-specific binaries like esbuild); add if the fixture grows any 163 + async function scanForDependencySize(messages, currentDeps, baseDeps) { 164 + const changed = []; 165 + for (const [name, versions] of currentDeps) { 166 + const base = baseDeps.get(name); 167 + for (const v of versions) 168 + if (!base?.has(v)) changed.push({name, version: v, sign: 1}); 169 + } 170 + for (const [name, versions] of baseDeps) { 171 + const curr = currentDeps.get(name); 172 + for (const v of versions) 173 + if (!curr?.has(v)) changed.push({name, version: v, sign: -1}); 174 + } 175 + if (changed.length === 0) return; 176 + 177 + let totalSize = 0; 178 + const rows = []; 179 + for (const {name, version, sign} of changed) { 180 + const meta = await fetchPackageMetadata(name, version); 181 + const size = meta?.dist?.unpackedSize; 182 + if (size === undefined) { 183 + rows.push({label: `${name}@${version}`, size: null}); 184 + } else { 185 + totalSize += sign * size; 186 + rows.push({label: `${name}@${version}`, size: sign * size}); 187 + } 188 + } 189 + rows.sort((a, b) => Math.abs(b.size ?? 0) - Math.abs(a.size ?? 0)); 190 + console.error(`total dependency size change: ${formatBytes(totalSize)}`); 191 + 192 + if (totalSize >= SIZE_THRESHOLD) { 193 + messages.push( 194 + `## 📊 Dependency Size Changes 195 + 196 + > ⚠️ This change adds ${formatBytes(totalSize)} of new dependencies, which exceeds the threshold of ${formatBytes(SIZE_THRESHOLD)}. 197 + 198 + | 📦 Package | 📏 Size | 199 + | --- | --- | 200 + ${rows.map((r) => `| ${r.label} | ${r.size === null ? '_Unknown_' : formatBytes(r.size)} |`).join('\n')} 201 + 202 + **Total size change:** ${formatBytes(totalSize)}` 203 + ); 204 + } 205 + } 206 + 207 + async function scanForProvenance(messages, currentDeps, baseDeps) { 208 + const rows = []; 209 + for (const [name, currentVersions] of currentDeps) { 210 + const baseVersions = baseDeps.get(name); 211 + if (!baseVersions || baseVersions.size === 0) continue; 212 + if (currentVersions.isSubsetOf(baseVersions)) continue; 213 + 214 + const trustFor = async (versions) => { 215 + const statuses = []; 216 + for (const v of versions) { 217 + const meta = await fetchPackageMetadata(name, v); 218 + if (meta) statuses.push(getTrustStatus(meta)); 219 + } 220 + return statuses; 221 + }; 222 + const minTrust = (statuses) => { 223 + let min = null; 224 + for (const s of statuses) { 225 + const level = getTrustLevel(s); 226 + if (min === null || level < min.level) 227 + min = {level, status: getTrustLevelName(s)}; 228 + } 229 + return min; 230 + }; 231 + 232 + const base = minTrust(await trustFor(baseVersions)); 233 + const curr = minTrust(await trustFor(currentVersions)); 234 + if (base && curr && curr.level < base.level) { 235 + rows.push(`| ${name} | ${base.status} | ${curr.status} |`); 236 + } 237 + } 238 + if (rows.length > 0) { 239 + messages.push( 240 + `## ⚠️ Package Trust Level Decreased 241 + 242 + > ⚠️ Decreased trust levels may indicate a higher risk of supply chain attacks. 243 + 244 + | 📦 Package | 🔒 Before | 🔓 After | 245 + | --- | --- | --- | 246 + ${rows.join('\n')}` 247 + ); 248 + } 249 + } 250 + 251 + // --- main --- 252 + 253 + console.error(`comparing ${LOCKFILE} between ${baseRef} and ${headRef}`); 254 + 255 + const baseLockRaw = getFileFromRef(baseRef, LOCKFILE); 256 + const headLockRaw = getFileFromRef(headRef, LOCKFILE); 257 + if (!baseLockRaw || !headLockRaw) { 258 + console.error(`missing ${LOCKFILE} in ${!baseLockRaw ? baseRef : headRef}`); 259 + process.exit(1); 260 + } 261 + const basePkg = JSON.parse(getFileFromRef(baseRef, 'package.json') ?? '{}'); 262 + const headPkg = JSON.parse(getFileFromRef(headRef, 'package.json') ?? '{}'); 263 + 264 + const baseLock = await parseLockfile(baseLockRaw, LOCKFILE, basePkg); 265 + const headLock = await parseLockfile(headLockRaw, LOCKFILE, headPkg); 266 + const baseDeps = computeDependencyVersions(baseLock); 267 + const headDeps = computeDependencyVersions(headLock); 268 + 269 + const messages = []; 270 + scanForReplacements(messages, directDeps(basePkg), directDeps(headPkg)); 271 + scanForDependencyCount(messages, headDeps, baseDeps); 272 + scanForDuplicates(messages, headDeps); 273 + await scanForDependencySize(messages, headDeps, baseDeps); 274 + await scanForProvenance(messages, headDeps, baseDeps); 275 + 276 + if (messages.length === 0) { 277 + console.log(`# Dependency Diff\n\nNo findings for ${baseRef}...${headRef}.`); 278 + } else { 279 + console.log(`# Dependency Diff (${baseRef}...${headRef})\n`); 280 + console.log(messages.join('\n\n')); 281 + }
+36
tools/package-lock.json
··· 1 + { 2 + "name": "dep-diff-tools", 3 + "lockfileVersion": 3, 4 + "requires": true, 5 + "packages": { 6 + "": { 7 + "name": "dep-diff-tools", 8 + "dependencies": { 9 + "lockparse": "^0.5.2", 10 + "module-replacements": "^3.0.0", 11 + "packumeta": "^0.4.1" 12 + } 13 + }, 14 + "node_modules/lockparse": { 15 + "version": "0.5.2", 16 + "resolved": "https://registry.npmjs.org/lockparse/-/lockparse-0.5.2.tgz", 17 + "integrity": "sha512-iu19W86kvrT2MLpvyXoIn/351Rb3PO7v7ljgFRQYj8tGpBRvNO9cZEI8x/7GGN+WN6BbrWpXc5CPrgPUjgX6Iw==", 18 + "license": "MIT" 19 + }, 20 + "node_modules/module-replacements": { 21 + "version": "3.0.0", 22 + "resolved": "https://registry.npmjs.org/module-replacements/-/module-replacements-3.0.0.tgz", 23 + "integrity": "sha512-tHIZqde+RlyNRobAIjfcH5UIgIrEbZbDGRL6J/x+HERX/g8O9mrm0p6knJbsTXmQtDvZ+eFP+xfOP3/9jHk6YA==", 24 + "license": "MIT" 25 + }, 26 + "node_modules/packumeta": { 27 + "version": "0.4.1", 28 + "resolved": "https://registry.npmjs.org/packumeta/-/packumeta-0.4.1.tgz", 29 + "integrity": "sha512-n+4VKW4+05duTeQP8BRUDke6rTHYqxqsrVyLEt0Y4Kmisp+ZL3JKREjaxv+ajSd9GpjZkJLY+zG1A0f1Jib21A==", 30 + "license": "MIT", 31 + "engines": { 32 + "node": ">=20.0.0" 33 + } 34 + } 35 + } 36 + }
+10
tools/package.json
··· 1 + { 2 + "name": "dep-diff-tools", 3 + "private": true, 4 + "type": "module", 5 + "dependencies": { 6 + "lockparse": "^0.5.2", 7 + "module-replacements": "^3.0.0", 8 + "packumeta": "^0.4.1" 9 + } 10 + }