From c39fa065fccc090e295ac678e4efa807a97c3734 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Sat, 4 Apr 2026 21:06:59 -0500 Subject: [PATCH] fix: simplify updater to @mosaic/mosaic only, add explicit tea repo/login flags (#387) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: Remove legacy @mosaic/cli fallback from update-checker. The updater now targets only @mosaic/mosaic — legacy package compatibility is out of scope per approved breaking change. - update-checker.ts: remove LEGACY_PKG, INSTALLED_PACKAGE_ORDER, candidate iteration, and cache key comparison. Single query for @mosaic/mosaic only. - update-checker.test.ts: remove legacy fallback and cross-contamination tests. Add test asserting no @mosaic/cli queries are made. - pr-merge.sh: add --repo and --login flags to tea merge command; resolve OWNER/REPO before case block. - issue-close.sh: add --repo and --login flags to tea issue commands; resolve OWNER/REPO after detect_platform. - pr-ci-wait.sh: no changes needed (uses curl, not tea). - detect-platform.sh: no changes needed (provides get_repo_owner/name). Ref: #387 --- .../387-updater-wrapper-gitea-20260404.md | 54 +++++++++++++ .../mosaic/__tests__/update-checker.test.ts | 76 ++++++------------- .../mosaic/framework/tools/git/issue-close.sh | 6 +- .../mosaic/framework/tools/git/pr-merge.sh | 5 +- packages/mosaic/src/runtime/update-checker.ts | 66 +++++----------- 5 files changed, 106 insertions(+), 101 deletions(-) create mode 100644 docs/scratchpads/387-updater-wrapper-gitea-20260404.md diff --git a/docs/scratchpads/387-updater-wrapper-gitea-20260404.md b/docs/scratchpads/387-updater-wrapper-gitea-20260404.md new file mode 100644 index 0000000..f2dd9da --- /dev/null +++ b/docs/scratchpads/387-updater-wrapper-gitea-20260404.md @@ -0,0 +1,54 @@ +# Scratchpad — issue #387 updater simplification + Gitea wrapper repo context + +- Objective: simplify updater behavior to `@mosaic/mosaic` only, and fix Gitea wrapper scripts so merge/CI/issue operations work reliably when tea needs explicit repo/login context. +- Scope: + - `packages/mosaic/src/runtime/update-checker.ts` + - `packages/mosaic/__tests__/update-checker.test.ts` + - any package metadata/version bumps needed + - repo-source git wrappers under `packages/mosaic/framework/tools/git/` +- Constraints: + - Jason approved breaking changes; legacy `@mosaic/cli` support is out of scope. + - Keep changes focused and mergeable. +- Acceptance: + - updater only targets `@mosaic/mosaic` + - wrapper path works on Gitea in this environment without manual repo guessing + - PR merges squash-only after green CI + +## Progress + +### 2026-04-04 + +#### Update Checker Simplification (DONE) + +- Removed `LEGACY_PKG`, `INSTALLED_PACKAGE_ORDER` constants — only `PKG = '@mosaic/mosaic'` remains +- `getInstalledVersion()` — removed loop over multiple packages, only checks `@mosaic/mosaic` +- `getLatestVersion()` — removed `installedPackage` parameter and candidate iteration; single query for `@mosaic/mosaic` +- `checkForUpdate()` — removed `currentPackage`-based cache key comparison; cache is now package-agnostic +- `RegistryCache` — removed `currentPackage` and `targetPackage` fields +- `formatUpdateNotice()` — removed `targetChanged` branch (package migration notice no longer relevant) +- All legacy fallback/compatibility logic removed + +#### Test Updates (DONE) + +- Removed legacy `@mosaic/cli` fallback test +- Removed cache cross-contamination test (was testing legacy→modern package transition) +- Added `does not query legacy @mosaic/cli package` test — asserts no `@mosaic/cli` npm commands are issued +- Added `returns empty result when package is not installed` test +- All 8 tests pass ✅ + +#### Gitea Wrapper Fixes (DONE) + +- `pr-merge.sh`: + - Added `OWNER=$(get_repo_owner)` / `REPO=$(get_repo_name)` before case block + - tea merge command now includes `--repo $OWNER/$REPO --login ${GITEA_LOGIN:-mosaicstack}` +- `issue-close.sh`: + - Added `OWNER=$(get_repo_owner)` / `REPO=$(get_repo_name)` after detect_platform + - Both `tea issue comment` and `tea issue close` now include `--repo "$OWNER/$REPO" --login "${GITEA_LOGIN:-mosaicstack}"` +- `pr-ci-wait.sh`: No changes needed — uses curl API calls (not tea), already passes owner/repo correctly +- `detect-platform.sh`: No changes needed — provides the `get_repo_owner`/`get_repo_name` functions used by wrappers + +#### Verification + +- `vitest run` — 8/8 tests pass +- `tsc --noEmit` — no errors in update-checker.ts (pre-existing workspace dep errors unrelated) +- `eslint` — clean, no warnings diff --git a/packages/mosaic/__tests__/update-checker.test.ts b/packages/mosaic/__tests__/update-checker.test.ts index 6eaaa8e..4bbc0ca 100644 --- a/packages/mosaic/__tests__/update-checker.test.ts +++ b/packages/mosaic/__tests__/update-checker.test.ts @@ -88,19 +88,18 @@ describe('formatUpdateNotice', () => { expect(notice).toContain('Update available'); }); - it('uses @mosaic/mosaic hints for modern installs', async () => { + it('uses @mosaic/mosaic for installs', async () => { execSyncMock.mockImplementation((command: string) => { if (command.includes('ls -g --depth=0 --json')) { return JSON.stringify({ dependencies: { - '@mosaic/mosaic': { version: '0.0.17' }, - '@mosaic/cli': { version: '0.0.16' }, + '@mosaic/mosaic': { version: '0.0.19' }, }, }); } if (command.includes('view @mosaic/mosaic version')) { - return '0.0.18'; + return '0.0.20'; } throw new Error(`Unexpected command: ${command}`); @@ -110,28 +109,29 @@ describe('formatUpdateNotice', () => { const result = checkForUpdate({ skipCache: true }); const notice = formatUpdateNotice(result); - expect(result.current).toBe('0.0.17'); - expect(result.latest).toBe('0.0.18'); + expect(result.current).toBe('0.0.19'); + expect(result.latest).toBe('0.0.20'); + expect(result.currentPackage).toBe('@mosaic/mosaic'); + expect(result.targetPackage).toBe('@mosaic/mosaic'); expect(notice).toContain('@mosaic/mosaic@latest'); - expect(notice).not.toContain('@mosaic/cli@latest'); }); - it('falls back to @mosaic/mosaic for legacy @mosaic/cli installs when cli is unavailable', async () => { + it('does not query legacy @mosaic/cli package', async () => { execSyncMock.mockImplementation((command: string) => { + if (command.includes('view @mosaic/cli')) { + throw new Error('Should not query @mosaic/cli'); + } + if (command.includes('ls -g --depth=0 --json')) { return JSON.stringify({ dependencies: { - '@mosaic/cli': { version: '0.0.16' }, + '@mosaic/mosaic': { version: '0.0.19' }, }, }); } - if (command.includes('view @mosaic/cli version')) { - throw new Error('not found'); - } - if (command.includes('view @mosaic/mosaic version')) { - return '0.0.17'; + return '0.0.20'; } throw new Error(`Unexpected command: ${command}`); @@ -139,57 +139,31 @@ describe('formatUpdateNotice', () => { const { checkForUpdate } = await importUpdateChecker(); const result = checkForUpdate({ skipCache: true }); - const notice = formatUpdateNotice(result); - expect(result.current).toBe('0.0.16'); - expect(result.latest).toBe('0.0.17'); - expect(notice).toContain('@mosaic/mosaic@latest'); + expect(result.targetPackage).toBe('@mosaic/mosaic'); + expect(result.latest).toBe('0.0.20'); + // Verify no @mosaic/cli queries were made + const calls = execSyncMock.mock.calls.map((c: any[]) => c[0] as string); + expect(calls.some((c) => c.includes('@mosaic/cli'))).toBe(false); }); - it('does not reuse a cached modern-package result for a legacy install', async () => { - let installedPackage = '@mosaic/mosaic'; - + it('returns empty result when package is not installed', async () => { execSyncMock.mockImplementation((command: string) => { if (command.includes('ls -g --depth=0 --json')) { - return JSON.stringify({ - dependencies: - installedPackage === '@mosaic/mosaic' - ? { '@mosaic/mosaic': { version: '0.0.17' } } - : { '@mosaic/cli': { version: '0.0.16' } }, - }); + return JSON.stringify({ dependencies: {} }); } if (command.includes('view @mosaic/mosaic version')) { - return installedPackage === '@mosaic/mosaic' ? '0.0.18' : '0.0.17'; - } - - if (command.includes('view @mosaic/cli version')) { - throw new Error('not found'); + return ''; } throw new Error(`Unexpected command: ${command}`); }); const { checkForUpdate } = await importUpdateChecker(); + const result = checkForUpdate({ skipCache: true }); - const modernResult = checkForUpdate(); - installedPackage = '@mosaic/cli'; - const legacyResult = checkForUpdate(); - - expect(modernResult.currentPackage).toBe('@mosaic/mosaic'); - expect(modernResult.targetPackage).toBe('@mosaic/mosaic'); - expect(modernResult.latest).toBe('0.0.18'); - - expect(legacyResult.currentPackage).toBe('@mosaic/cli'); - expect(legacyResult.targetPackage).toBe('@mosaic/mosaic'); - expect(legacyResult.latest).toBe('0.0.17'); - expect(execSyncMock).toHaveBeenCalledWith( - expect.stringContaining('view @mosaic/cli version'), - expect.any(Object), - ); - expect(execSyncMock).toHaveBeenCalledWith( - expect.stringContaining('view @mosaic/mosaic version'), - expect.any(Object), - ); + expect(result.current).toBe(''); + expect(result.updateAvailable).toBe(false); }); }); diff --git a/packages/mosaic/framework/tools/git/issue-close.sh b/packages/mosaic/framework/tools/git/issue-close.sh index b831272..b773357 100755 --- a/packages/mosaic/framework/tools/git/issue-close.sh +++ b/packages/mosaic/framework/tools/git/issue-close.sh @@ -45,6 +45,8 @@ fi # Detect platform and close issue detect_platform +OWNER=$(get_repo_owner) +REPO=$(get_repo_name) if [[ "$PLATFORM" == "github" ]]; then if [[ -n "$COMMENT" ]]; then @@ -54,9 +56,9 @@ if [[ "$PLATFORM" == "github" ]]; then echo "Closed GitHub issue #$ISSUE_NUMBER" elif [[ "$PLATFORM" == "gitea" ]]; then if [[ -n "$COMMENT" ]]; then - tea issue comment "$ISSUE_NUMBER" "$COMMENT" + tea issue comment "$ISSUE_NUMBER" "$COMMENT" --repo "$OWNER/$REPO" --login "${GITEA_LOGIN:-mosaicstack}" fi - tea issue close "$ISSUE_NUMBER" + tea issue close "$ISSUE_NUMBER" --repo "$OWNER/$REPO" --login "${GITEA_LOGIN:-mosaicstack}" echo "Closed Gitea issue #$ISSUE_NUMBER" else echo "Error: Unknown platform" diff --git a/packages/mosaic/framework/tools/git/pr-merge.sh b/packages/mosaic/framework/tools/git/pr-merge.sh index 1289ccc..ad8c318 100755 --- a/packages/mosaic/framework/tools/git/pr-merge.sh +++ b/packages/mosaic/framework/tools/git/pr-merge.sh @@ -89,6 +89,8 @@ if [[ "$SKIP_QUEUE_GUARD" != true ]]; then fi PLATFORM=$(detect_platform) +OWNER=$(get_repo_owner) +REPO=$(get_repo_name) case "$PLATFORM" in github) @@ -97,8 +99,7 @@ case "$PLATFORM" in eval "$CMD" ;; gitea) - # tea pr merge syntax - CMD="tea pr merge $PR_NUMBER --style squash" + CMD="tea pr merge $PR_NUMBER --style squash --repo $OWNER/$REPO --login ${GITEA_LOGIN:-mosaicstack}" # Delete branch after merge if requested if [[ "$DELETE_BRANCH" == true ]]; then diff --git a/packages/mosaic/src/runtime/update-checker.ts b/packages/mosaic/src/runtime/update-checker.ts index 3069966..a54d566 100644 --- a/packages/mosaic/src/runtime/update-checker.ts +++ b/packages/mosaic/src/runtime/update-checker.ts @@ -1,6 +1,6 @@ /** - * Mosaic update checker — compares the installed Mosaic package against the - * Gitea npm registry and reports when an upgrade is available. + * Mosaic update checker — compares the installed @mosaic/mosaic package + * against the Gitea npm registry and reports when an upgrade is available. * * Used by: * - CLI startup (non-blocking background check) @@ -40,9 +40,7 @@ export interface UpdateCheckResult { // ─── Constants ────────────────────────────────────────────────────────────── const REGISTRY = 'https://git.mosaicstack.dev/api/packages/mosaic/npm/'; -const MODERN_PKG = '@mosaic/mosaic'; -const LEGACY_PKG = '@mosaic/cli'; -const INSTALLED_PACKAGE_ORDER = [MODERN_PKG, LEGACY_PKG] as const; +const PKG = '@mosaic/mosaic'; const CACHE_DIR = join(homedir(), '.cache', 'mosaic'); const CACHE_FILE = join(CACHE_DIR, 'update-check.json'); const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour @@ -131,20 +129,17 @@ export function semverLt(a: string, b: string): boolean { /** Cache stores only the latest registry version (the expensive network call). * The installed version is always checked fresh — it's a local `npm ls`. */ interface RegistryCache { - currentPackage?: string; latest: string; - targetPackage?: string; checkedAt: string; registry: string; } -function readCache(currentPackage: string): RegistryCache | null { +function readCache(): RegistryCache | null { try { if (!existsSync(CACHE_FILE)) return null; const raw = JSON.parse(readFileSync(CACHE_FILE, 'utf-8')) as RegistryCache; const age = Date.now() - new Date(raw.checkedAt).getTime(); if (age > CACHE_TTL_MS) return null; - if ((raw.currentPackage || '') !== currentPackage) return null; return raw; } catch { return null; @@ -163,22 +158,18 @@ function writeCache(entry: RegistryCache): void { // ─── Public API ───────────────────────────────────────────────────────────── /** - * Get the currently installed Mosaic package version. - * Prefers the consolidated @mosaic/mosaic package over legacy @mosaic/cli. + * Get the currently installed @mosaic/mosaic version. */ export function getInstalledVersion(): { name: string; version: string } { - // Fast path: check via package.json require chain try { const raw = npmExec(`ls -g --depth=0 --json 2>/dev/null`, 3000); if (raw) { const data = JSON.parse(raw) as { dependencies?: Record; }; - for (const pkg of INSTALLED_PACKAGE_ORDER) { - const version = data?.dependencies?.[pkg]?.version; - if (version) { - return { name: pkg, version }; - } + const version = data?.dependencies?.[PKG]?.version; + if (version) { + return { name: PKG, version }; } } } catch { @@ -188,27 +179,19 @@ export function getInstalledVersion(): { name: string; version: string } { } /** - * Fetch the latest published version from the Gitea npm registry. - * For legacy @mosaic/cli installs, try the legacy package first and fall back - * to @mosaic/mosaic to support the CLI -> mosaic package consolidation. + * Fetch the latest published @mosaic/mosaic version from the Gitea npm registry. * Returns empty string on failure. */ -export function getLatestVersion(installedPackage = ''): { name: string; version: string } { - const candidates = - installedPackage === LEGACY_PKG ? [LEGACY_PKG, MODERN_PKG] : [MODERN_PKG, LEGACY_PKG]; - - for (const pkg of candidates) { - const version = npmExec(`view ${pkg} version --registry=${REGISTRY}`); - if (version) { - return { name: pkg, version }; - } +export function getLatestVersion(): { name: string; version: string } { + const version = npmExec(`view ${PKG} version --registry=${REGISTRY}`); + if (version) { + return { name: PKG, version }; } - return { name: '', version: '' }; } export function getInstallCommand(result: Pick): string { - return `npm i -g ${result.targetPackage || MODERN_PKG}@latest`; + return `npm i -g ${result.targetPackage || PKG}@latest`; } /** @@ -225,31 +208,27 @@ export function checkForUpdate(options?: { skipCache?: boolean }): UpdateCheckRe let checkedAt: string; if (!options?.skipCache) { - const cached = readCache(currentInfo.name); + const cached = readCache(); if (cached) { latestInfo = { - name: cached.targetPackage || MODERN_PKG, + name: PKG, version: cached.latest, }; checkedAt = cached.checkedAt; } else { - latestInfo = getLatestVersion(currentInfo.name); + latestInfo = getLatestVersion(); checkedAt = new Date().toISOString(); writeCache({ - currentPackage: currentInfo.name, latest: latestInfo.version, - targetPackage: latestInfo.name, checkedAt, registry: REGISTRY, }); } } else { - latestInfo = getLatestVersion(currentInfo.name); + latestInfo = getLatestVersion(); checkedAt = new Date().toISOString(); writeCache({ - currentPackage: currentInfo.name, latest: latestInfo.version, - targetPackage: latestInfo.name, checkedAt, registry: REGISTRY, }); @@ -257,9 +236,9 @@ export function checkForUpdate(options?: { skipCache?: boolean }): UpdateCheckRe return { current, - currentPackage: currentInfo.name, + currentPackage: currentInfo.name || PKG, latest: latestInfo.version, - targetPackage: latestInfo.name || MODERN_PKG, + targetPackage: latestInfo.name || PKG, updateAvailable: !!(current && latestInfo.version && semverLt(current, latestInfo.version)), checkedAt, registry: REGISTRY, @@ -273,17 +252,12 @@ export function formatUpdateNotice(result: UpdateCheckResult): string { if (!result.updateAvailable) return ''; const installCommand = getInstallCommand(result); - const targetChanged = - result.currentPackage && result.targetPackage && result.currentPackage !== result.targetPackage; const lines = [ '', '╭─────────────────────────────────────────────────╮', '│ │', `│ Update available: ${result.current} → ${result.latest}`.padEnd(50) + '│', - ...(targetChanged - ? [`│ Package target: ${result.currentPackage} → ${result.targetPackage}`.padEnd(50) + '│'] - : []), '│ │', '│ Run: bash tools/install.sh │', `│ Or: ${installCommand}`.padEnd(50) + '│',