From 696a42c9ad49f350cdf125364c50aefaad9ee37c Mon Sep 17 00:00:00 2001 From: DevOhMyCode Date: Tue, 20 Oct 2026 20:22:07 +0200 Subject: [PATCH] ci: check that files copied across the three atlases stay identical --- .github/workflows/shared.yml | 26 +++++++++++++++++ package.json | 1 + scripts/check-shared.ts | 56 ++++++++++++++++++++++++++++++++++++ test/shared.test.ts | 15 ++++++++++ 4 files changed, 98 insertions(+) create mode 100644 .github/workflows/shared.yml create mode 100644 scripts/check-shared.ts create mode 100644 test/shared.test.ts diff --git a/.github/workflows/shared.yml b/.github/workflows/shared.yml new file mode 100644 index 0000000..bd44bea --- /dev/null +++ b/.github/workflows/shared.yml @@ -0,0 +1,26 @@ +name: shared files + +# The three atlases copy a few terminal primitives byte for byte: catch a copy left behind. +on: + pull_request: + paths: ['src/browser.ts', 'src/ui/mouse.ts', 'src/ui/palette.ts', 'src/ui/render.ts', 'src/ui/width.ts', 'src/ui/ListSelect.tsx', 'scripts/check-shared.ts', '.github/workflows/shared.yml'] + # A sibling can change too: check once a week against their main branches. + schedule: + - cron: '23 5 * * 1' + workflow_dispatch: + +permissions: + contents: read + +jobs: + drift: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run check:shared diff --git a/package.json b/package.json index a3de91e..99b6754 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dev": "tsx src/launch.ts", "crawl": "tsx scripts/crawl.ts", "demo": "tsx scripts/build-demo.ts", + "check:shared": "tsx scripts/check-shared.ts", "sync-agents": "tsx scripts/sync-agents.ts", "reclassify": "tsx scripts/reclassify.ts", "typecheck": "tsc --noEmit", diff --git a/scripts/check-shared.ts b/scripts/check-shared.ts new file mode 100644 index 0000000..1d02ff7 --- /dev/null +++ b/scripts/check-shared.ts @@ -0,0 +1,56 @@ +/** + * skills-atlas, mcp-atlas and plugins-atlas copy a few terminal primitives byte for byte + * instead of sharing a package (the rest of their code has diverged too far for one). + * This checks that the copies still match, so that a fix made in one reaches the others. + * + * npm run check:shared compare with each sibling's main branch on GitHub + * npm run check:shared -- --local compare with sibling checkouts next to this one (../mcp-atlas…) + * + * Exits with 1 and names the files that differ. + */ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +/** File under src/ → the sibling repositories that must hold the same bytes. */ +export const SHARED: Record = { + 'browser.ts': ['mcp-atlas', 'plugins-atlas'], + 'ui/mouse.ts': ['mcp-atlas', 'plugins-atlas'], + 'ui/palette.ts': ['mcp-atlas', 'plugins-atlas'], + 'ui/render.ts': ['mcp-atlas', 'plugins-atlas'], + 'ui/width.ts': ['mcp-atlas', 'plugins-atlas'], + // plugins-atlas reads its strings from its own messages module, not i18n. + 'ui/ListSelect.tsx': ['mcp-atlas'], +}; + +const normalize = (text: string) => text.replace(/\r\n?/g, '\n'); + +export async function drift(read: (repo: string, file: string) => Promise, own = (file: string) => readFile(join('src', file), 'utf8')) { + const out: { file: string; repo: string; error?: string }[] = []; + for (const [file, repos] of Object.entries(SHARED)) { + const mine = normalize(await own(file)); + for (const repo of repos) { + try { + if (normalize(await read(repo, file)) !== mine) out.push({ file, repo }); + } catch (err) { + out.push({ file, repo, error: err instanceof Error ? err.message : String(err) }); + } + } + } + return out; +} + +const fromGitHub = async (repo: string, file: string) => { + const res = await fetch(`https://raw.githubusercontent.com/devohmycode/${repo}/main/src/${file}`, { signal: AbortSignal.timeout(20_000) }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.text(); +}; +const fromSibling = (repo: string, file: string) => readFile(join('..', repo, 'src', file), 'utf8'); + +if (process.argv[1]?.endsWith('check-shared.ts')) { + const local = process.argv.includes('--local'); + const found = await drift(local ? fromSibling : fromGitHub); + for (const d of found) console.error(`${d.error ? 'unreadable' : 'differs '} src/${d.file} ${d.repo}${d.error ? `: ${d.error}` : ''}`); + const checked = Object.values(SHARED).reduce((n, r) => n + r.length, 0); + console.log(found.length ? `${found.length} of ${checked} shared copies out of step` : `${checked} shared copies match (${local ? 'local checkouts' : 'GitHub main'})`); + process.exitCode = found.length ? 1 : 0; +} diff --git a/test/shared.test.ts b/test/shared.test.ts new file mode 100644 index 0000000..84656c6 --- /dev/null +++ b/test/shared.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest'; +import { drift, SHARED } from '../scripts/check-shared.js'; + +describe('shared files check', () => { + it('reports copies that differ or cannot be read, ignoring line endings', async () => { + const own = async (file: string) => `${file}\n`; + const found = await drift(async (repo, file) => { + if (repo === 'plugins-atlas' && file === 'ui/mouse.ts') return 'changed\n'; + if (repo === 'mcp-atlas' && file === 'ui/width.ts') throw new Error('HTTP 404'); + return `${file}\r\n`; + }, own); + expect(found).toEqual([{ file: 'ui/mouse.ts', repo: 'plugins-atlas' }, { file: 'ui/width.ts', repo: 'mcp-atlas', error: 'HTTP 404' }]); + expect(Object.keys(SHARED)).toContain('browser.ts'); + }); +});